Let's build Dev.to clone with Next.js & ChakraUI

Let's build Dev.to clone with Next.js & ChakraUI

Let's clone dev.to with the actual dev.to api to get the posts and listings.

Overview

This application is built with the following technologies:

Live demo: dev-to-clone-ma.vercel.app

Breaking down the layout of Dev.to

  • Top navbar
    • Profile menu dropdown
  • Left sidebar
  • Posts section
  • Right sidebar

I'll discuss some components code here otherwise this article will become very long.

1. Setup the Project

Create a Next.js app

yarn create next-app --typescript

Install chakra-ui

yarn add @chakra-ui/react @emotion/react@^11 @emotion/styled@^11 framer-motion@^4

2. Start coding

Top navbar

const Navbar = () => {
  return (
    <Box
      py="2"
      boxShadow="sm"
      border="0 solid #e5e7eb"
      position="fixed"
      top="0"
      bg="#fff"
      width="100%"
      zIndex="1"
    >
      <Container>
        <HStack spacing={4}>
          <Image src="/assets/images/logo.svg" />
          <Input
            maxW="26rem"
            placeholder="Search..."
            borderColor="#b5bdc4"
            borderRadius="5px"
            d={{ base: "none", md: "block" }}
          />
          <Spacer />
          <HStack spacing={3}>
            <Button
              color="#fff"
              borderRadius="4px"
              bg="#3b49df"
              _hover={{ bg: "#323ebe" }}
            >
              Create a post
            </Button>
            <IconButton>
              <Image src="/assets/images/notification.svg" />
            </IconButton>
            <IconButton>
              <Image src="/assets/images/bell.svg" />
            </IconButton>
            <Avatar
              size={"sm"}
              src={"https://avatars2.githubusercontent.com/u/37842853?v=4"}
            />
          </HStack>
        </HStack>
      </Container>
    </Box>
  );
};

navbar.png

dropdown.png

Links section

const Links = () => {
  return (
    <Box as="nav">
      <LinkButton>
        <Image src="/assets/images/sidebar/home.svg" mr="3" />
        Home
      </LinkButton>
      <LinkButton>
        <Image src="/assets/images/sidebar/reading.svg" mr="3" />
        Reading List
      </LinkButton>
      <LinkButton>
        <Image src="/assets/images/sidebar/tag.svg" mr="3" />
        Tags
      </LinkButton>
      <LinkButton>
        <Text fontWeight="normal" color="#4d5760" ml="2.3rem">
          More...
        </Text>
      </LinkButton>
    </Box>
  );
};

Tags section

const Tags = () => {
  return (
    <Box mt="6">
      <Flex pl="2" py="4">
        <Heading as="h3" fontSize="1rem">
          My Tags
        </Heading>
        <Spacer />
        <Image src="/assets/settings.svg" />
      </Flex>
      <Box maxH="50vh" overflowY="auto">
        <TagList>
          {[
            "Nextjs",
            "react",
            "javascript",
            "ruby",
            "ruby on rails",
            "css",
            "beginners",
            "html",
            "typescript"
          ]}
        </TagList>
      </Box>
    </Box>
  );
};

left-navbar.png

Posts section

End point for showing feed https://dev.to/stories/feed

Post card component

<Box
      mt="3"
      as="article"
      bg="white"
      borderRadius="md"
      overflow="hidden"
      border="1px solid #08090a1a"
    >
      {headerImage ? <Image src={headerImage} /> : ""}
      <Grid templateColumns="max-content 1fr" gap={2} p={4}>
        <Image src={userProfile} w="8" borderRadius="full" />

        <Box>
          <VStack align="flex-start" spacing={0}>
            <Text color="#4d5760" fontSize="14px" fontWeight="500">
              {username}
            </Text>
            <Text color="#4d5760" fontSize="12px">
              {publishedDate}
            </Text>
          </VStack>
          <Heading fontSize={headerImage ? "30px" : "24px"} mt="3">
            <Link
              href={postLink}
              _hover={{ color: "#323ebe", textDecoration: "none" }}
              isExternal
            >
              {title}
            </Link>
          </Heading>
          <HStack mt="3" fontSize="14px" color="#64707d">
            {tagList.map((tag, idx) => (
              <Text as={Link} key={idx}>
                #{tag}
              </Text>
            ))}
          </HStack>
          <HStack mt={3}>
            <Button
              leftIcon={<Image src="/assets/images/like.svg" />}
              ml={-2}
              bg="transparent"
              padding="6px 8px"
              height="auto"
              fontWeight="normal"
              fontSize="14px"
              lineHeight="1.2"
              borderRadius="4px"
              _hover={{ bg: "#f6f6f6" }}
            >
              {reactionCount} reactions
            </Button>
            <Button
              leftIcon={<Image src="/assets/images/comment.svg" />}
              bg="transparent"
              padding="6px 8px"
              height="auto"
              fontWeight="normal"
              fontSize="14px"
              lineHeight="1.2"
              borderRadius="4px"
              _hover={{ bg: "#f6f6f6" }}
            >
              {commentCount} comments
            </Button>
            <Spacer />
            <Text fontSize="12px">{readingTime} min read</Text>
            <Button
              bg="#d2d6db"
              padding="8px 12px"
              height="auto"
              fontWeight="normal"
              fontSize="14px"
              _hover={{ bg: "#b5bdc4" }}
            >
              Save
            </Button>
          </HStack>
        </Box>
      </Grid>
    </Box>

posts-skeleton.png

posts.png

End point for showing list https://dev.to/api/listings

const List = () => {
  const { data, error } = useSWR("https://dev.to/api/listings", fetcher);

  if (error) return <Box>failed to load</Box>;
  if (!data)
    return (
      <Box as="section" bg="white" borderRadius="md" border="1px solid #E2E4E6" width="100%">
        <ListHeading />
        {[1, 2, 3, 4, 5].map(id => {
          return (
            <Box borderBottom="1px solid #E2E4E6" width="100%" p="3">
              <Skeleton height="15vh" borderRadius="5px" width="100%" />
            </Box>
          );
        })}
      </Box>
    );

  return (
    <Box as="section" bg="white" borderRadius="md" border="1px solid #E2E4E6">
      <ListHeading />
      {data.slice(0, 7).map(list => (
        <ListBox title={list.title} category={list.category} slug={list.slug} />
      ))}
    </Box>
  );
}

right-sidebar.png

Responsiveness

responsiveness1.png

responsiveness2.png