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: https://dev-to-clone-ab.vercel.app
Github repo: https://github.com/MA-Ahmad/dev.to-clone
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>
);
};
Update
Created profile menu dropdown
Left sidebar
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>
);
};
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>
Right sidebar
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>
);
}
Responsiveness