├── .eslintrc ├── .gitignore ├── LICENSE ├── README.md ├── components ├── feed │ ├── posts.tsx │ └── skeleton.tsx ├── layout │ ├── container.tsx │ ├── meta.tsx │ ├── navbar.tsx │ └── sidebar.tsx └── listing.tsx ├── next-env.d.ts ├── next.config.js ├── package.json ├── pages ├── _app.tsx ├── _document.tsx └── index.tsx ├── public ├── assets │ └── images │ │ ├── bell.svg │ │ ├── comment.svg │ │ ├── like.svg │ │ ├── logo.svg │ │ ├── notification.svg │ │ ├── settings.svg │ │ └── sidebar │ │ ├── home.svg │ │ ├── listing.svg │ │ ├── podcast.svg │ │ ├── reading.svg │ │ ├── tag.svg │ │ └── video.svg ├── devto.png └── favicon.png ├── tsconfig.json └── yarn.lock /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next", "next/core-web-vitals"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | MIT License 3 | 4 | Copyright (c) 2021 Muhammad Ahmad 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | Logo 4 | 5 | 6 |

Dev.to Clone

7 | 8 |

9 | Built with Typescript, React & Next.js 10 |

11 |

12 | 13 | ## Preview 14 | 15 | ![Homepage](/public/devto.png) 16 | 17 | ## Overview 18 | 19 | This application is built with the following technologies: 20 | 21 | - [Typescript](https://www.typescriptlang.org/) 22 | - [Nextjs](https://nextjs.org/) 23 | - [Chakra UI](https://chakra-ui.com) 24 | - [SWR](https://swr.vercel.app/) 25 | - [Dev.to api](https://docs.forem.com/api/) 26 | 27 | 28 | ### Installation 29 | 30 | 1. Clone the repo 31 | ```sh 32 | git clone https://github.com/MA-Ahmad/dev.to-clone 33 | ``` 34 | 2. Install NPM packages 35 | ```sh 36 | yarn install 37 | ``` 38 | 3. Start the application 39 | ```sh 40 | yarn dev 41 | ``` 42 | The above command will start the application on [http://localhost:3000/](http://localhost:3000). 43 | 44 | ## View and copy code of your favourite components 45 | [TemplatesKart website](https://templateskart.com/projects/devto-clone) 46 | 47 | ## License 48 | 49 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 50 | -------------------------------------------------------------------------------- /components/feed/posts.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Box, 3 | Heading, 4 | Spacer, 5 | Button, 6 | VStack, 7 | HStack, 8 | Grid, 9 | Text, 10 | Link, 11 | Image, 12 | } from '@chakra-ui/react' 13 | import { css } from '@emotion/react' 14 | import styled from '@emotion/styled' 15 | import useSWR from 'swr' 16 | import { useState } from 'react' 17 | import SkeletonCards from './skeleton' 18 | 19 | const HeaderBtn = styled(Button)` 20 | position: relative; 21 | padding: 0.4rem 0.5rem; 22 | font-weight: normal; 23 | 24 | &:hover { 25 | color: #3b49df; 26 | 27 | &::after { 28 | width: 100%; 29 | } 30 | } 31 | 32 | &:focus { 33 | box-shadow: none; 34 | } 35 | 36 | ${(props) => 37 | props.isCurrent && 38 | css` 39 | font-weight: 500; 40 | 41 | &::after { 42 | transition: width 0.2s ease; 43 | position: absolute; 44 | bottom: 0; 45 | margin: auto; 46 | content: ''; 47 | height: 3px; 48 | width: 70%; 49 | border-radius: 4px; 50 | background-color: #3b49df; 51 | } 52 | `} 53 | ` 54 | 55 | const Header = ({ isActive, setIsActive }) => { 56 | return ( 57 | 58 | 59 | Posts 60 | 61 | {timeperiods.map((item, idx) => { 62 | if (isActive === item) { 63 | return ( 64 | setIsActive(item)} 68 | > 69 | {item} 70 | 71 | ) 72 | } 73 | return ( 74 | setIsActive(item)}> 75 | {item} 76 | 77 | ) 78 | })} 79 | 80 | 81 | ) 82 | } 83 | 84 | function Card({ 85 | title, 86 | username, 87 | userProfile, 88 | publishedDate, 89 | tagList, 90 | headerImage, 91 | postLink, 92 | readingTime, 93 | reactionCount, 94 | commentCount, 95 | }) { 96 | return ( 97 | 105 | {headerImage ? : ''} 106 | 111 | {/* */} 112 | 113 | 114 | user profile 120 | 125 | 126 | {username} 127 | 128 | 129 | {publishedDate} 130 | 131 | 132 | 133 | 134 | 139 | 140 | {username} 141 | 142 | 143 | {publishedDate} 144 | 145 | 146 | 147 | 152 | {title} 153 | 154 | 155 | 156 | {tagList.map((tag, idx) => ( 157 | 158 | #{tag} 159 | 160 | ))} 161 | 162 | 163 | 180 | 196 | 197 | {readingTime} min read 198 | 208 | 209 | 210 | 211 | 212 | ) 213 | } 214 | 215 | const fetcher = (url) => fetch(url).then((res) => res.json()) 216 | 217 | const timeperiods = ['Week', 'Month', 'Year', 'Latest'] 218 | function returnFetchUrl(isActive) { 219 | if (isActive === 'Week') { 220 | return '' 221 | } 222 | return isActive.toLowerCase() 223 | } 224 | 225 | const Posts = () => { 226 | const [isActive, setIsActive] = useState(timeperiods[3]) 227 | const { data, error } = useSWR( 228 | `https://dev.to/stories/feed/${returnFetchUrl(isActive)}?page=1`, 229 | fetcher 230 | ) 231 | 232 | if (error) return failed to load 233 | if (!data) 234 | return ( 235 | 236 |
237 | 238 | 239 | ) 240 | 241 | return ( 242 | 243 |
244 | {data.map((post, idx) => ( 245 | 258 | ))} 259 | 260 | ) 261 | } 262 | 263 | export default Posts 264 | -------------------------------------------------------------------------------- /components/feed/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { Box, HStack, Skeleton, Stack } from "@chakra-ui/react"; 2 | 3 | const SkeletonCards = () => { 4 | return ( 5 | 6 | {[1, 2, 3, 4, 5, 6, 7, 8].map(id => { 7 | return ( 8 | 18 | {id === 1 ? ( 19 | 20 | ) : ( 21 | "" 22 | )} 23 | 24 | 25 | 26 | 27 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 47 | 48 | 54 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | ); 68 | })} 69 | 70 | ); 71 | }; 72 | 73 | export default SkeletonCards; 74 | -------------------------------------------------------------------------------- /components/layout/container.tsx: -------------------------------------------------------------------------------- 1 | import { Box } from "@chakra-ui/layout"; 2 | import { ReactNode } from "react"; 3 | 4 | type ContainerProps = { 5 | children: ReactNode; 6 | }; 7 | 8 | const Container = ({ children }: ContainerProps) => { 9 | return ( 10 | 11 | {children} 12 | 13 | ); 14 | }; 15 | 16 | export default Container; 17 | -------------------------------------------------------------------------------- /components/layout/meta.tsx: -------------------------------------------------------------------------------- 1 | import Head from "next/head"; 2 | 3 | const Meta = ({ title, keywords, description }) => { 4 | return ( 5 | 6 | 7 | 8 | 9 | 10 | {title} 11 | 12 | ); 13 | }; 14 | 15 | Meta.defaultProps = { 16 | title: "DEV Community 👨‍💻👨‍💻", 17 | keywords: "Nextjs, Charkra-UI, React", 18 | description: "Dev.to clone build by Muhammad Ahmad" 19 | }; 20 | 21 | export default Meta; 22 | -------------------------------------------------------------------------------- /components/layout/navbar.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Box, 3 | Avatar, 4 | Button, 5 | HStack, 6 | VStack, 7 | Image, 8 | Input, 9 | Spacer, 10 | Menu, 11 | MenuButton, 12 | MenuList, 13 | MenuItem, 14 | Text, 15 | Link, 16 | MenuDivider 17 | } from "@chakra-ui/react"; 18 | import Container from "./container"; 19 | import { ReactNode } from "react"; 20 | 21 | type IconButtonProps = { 22 | children: ReactNode; 23 | }; 24 | 25 | const IconButton = ({ children }: IconButtonProps) => { 26 | return ( 27 | 37 | ); 38 | }; 39 | 40 | const Navbar = () => { 41 | return ( 42 | 52 | 53 | 54 | 55 | 62 | 63 | 64 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 84 | 85 | 92 | 97 | 98 | 99 | Muhammad Ahmad 100 | 101 | @m_ahmad 102 | 103 | 104 | 105 | 106 | 107 | 108 | Dashboard 109 | 110 | 111 | Create Post 112 | 113 | 114 | Reading List 115 | 116 | 117 | Settings 118 | 119 | 120 | 121 | Sign Out 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | ); 130 | }; 131 | 132 | export default Navbar; 133 | -------------------------------------------------------------------------------- /components/layout/sidebar.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Box, 3 | Button, 4 | Flex, 5 | Heading, 6 | Image, 7 | Text, 8 | Spacer 9 | } from "@chakra-ui/react"; 10 | import { ReactNode } from "react"; 11 | 12 | type LinkButtonProps = { 13 | children: ReactNode; 14 | }; 15 | 16 | const LinkButton = ({ children }: LinkButtonProps) => { 17 | return ( 18 | 28 | ); 29 | }; 30 | 31 | const Links = () => { 32 | return ( 33 | 34 | 35 | 36 | Home 37 | 38 | 39 | 40 | Reading List 41 | 42 | 43 | 44 | Listings 45 | 46 | 47 | 48 | Podcasts 49 | 50 | 51 | 52 | Videos 53 | 54 | 55 | 56 | Tags 57 | 58 | 59 | 60 | More... 61 | 62 | 63 | 64 | ); 65 | }; 66 | 67 | const TagList = ({ children }) => { 68 | return ( 69 | 70 | {children && 71 | children.map((item, idx) => #{item})} 72 | 73 | ); 74 | }; 75 | 76 | const Tags = () => { 77 | return ( 78 | 79 | 80 | 81 | My Tags 82 | 83 | 84 | 85 | 86 | 87 | 88 | {[ 89 | "Nextjs", 90 | "react", 91 | "javascript", 92 | "ruby", 93 | "ruby on rails", 94 | "beginners", 95 | "typescript" 96 | ]} 97 | 98 | 99 | 100 | ); 101 | }; 102 | 103 | const Sidebar = props => { 104 | return ( 105 | 106 | 107 | 108 | 109 | ); 110 | }; 111 | 112 | export default Sidebar; 113 | -------------------------------------------------------------------------------- /components/listing.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Box, 3 | Flex, 4 | Link, 5 | Heading, 6 | Text, 7 | Spacer, 8 | VStack, 9 | Skeleton 10 | } from "@chakra-ui/react"; 11 | import useSWR from "swr"; 12 | import { useState } from "react"; 13 | 14 | function ListHeading() { 15 | return ( 16 | 17 | 18 | Listings 19 | 20 | 21 | See all 22 | 23 | 24 | 25 | ); 26 | } 27 | 28 | function HashTagHeading() { 29 | return ( 30 | 31 | #news 32 | 33 | ); 34 | } 35 | 36 | const fetcher = url => fetch(url).then(res => res.json()); 37 | 38 | const List = () => { 39 | const { data, error } = useSWR("https://dev.to/api/listings", fetcher); 40 | 41 | if (error) return failed to load; 42 | if (!data) 43 | return ( 44 | 45 | 46 | {[1, 2, 3, 4, 5].map(id => { 47 | return ( 48 | 49 | 50 | 51 | ); 52 | })} 53 | 54 | ); 55 | 56 | return ( 57 | 58 | 59 | {data.slice(0, 7).map((list, index) => ( 60 | 61 | ))} 62 | 63 | ); 64 | }; 65 | 66 | const ListBox = ({ title, category, slug }) => { 67 | return ( 68 | 73 | 74 | 75 | {title} 76 | 77 | {category} 78 | 79 | 80 | 81 | 82 | ); 83 | }; 84 | 85 | const News = () => { 86 | return ( 87 | 88 | 89 | {[ 90 | { 91 | title: "Javascript developer (~ 2 YOE) looking for remote work", 92 | category: "events", 93 | slug: "" 94 | }, 95 | { 96 | title: 97 | "Want to know how a JavaScript framework works under the hood?", 98 | category: "education", 99 | slug: "" 100 | }, 101 | { title: "Pair Programming with Jhey", category: "events", slug: "" } 102 | ].map((news, index) => ( 103 | 104 | ))} 105 | 106 | ); 107 | }; 108 | 109 | const Listing = props => { 110 | return ( 111 | 112 | 113 | 114 | 115 | ); 116 | }; 117 | 118 | export default Listing; 119 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | reactStrictMode: true, 3 | } 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "devto-clone", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@chakra-ui/react": "^1.6.4", 13 | "@emotion/react": "^11", 14 | "@emotion/styled": "^11", 15 | "framer-motion": "^4", 16 | "next": "11.0.1", 17 | "react": "17.0.2", 18 | "react-dom": "17.0.2", 19 | "swr": "^0.5.6" 20 | }, 21 | "devDependencies": { 22 | "@types/react": "17.0.11", 23 | "eslint": "7.29.0", 24 | "eslint-config-next": "11.0.1", 25 | "typescript": "4.3.4" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import { ChakraProvider } from "@chakra-ui/react"; 2 | import { AppProps } from "next/app"; 3 | import Head from "next/head"; 4 | 5 | const MyApp = ({ Component, pageProps }: AppProps) => { 6 | return ( 7 | 8 | 9 | 13 | 14 | 15 | 16 | ); 17 | }; 18 | 19 | export default MyApp; 20 | -------------------------------------------------------------------------------- /pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import Document, { Html, Head, Main, NextScript } from "next/document"; 2 | 3 | class MyDocument extends Document { 4 | render() { 5 | return ( 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | ); 18 | } 19 | } 20 | 21 | export default MyDocument; 22 | -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { Fragment } from "react"; 2 | import Navbar from "../components/layout/navbar"; 3 | import { Box, Grid } from "@chakra-ui/react"; 4 | import Sidebar from "../components/layout/sidebar"; 5 | import Container from "../components/layout/container"; 6 | import Listing from "../components/listing"; 7 | import Posts from "../components/feed/posts"; 8 | import Meta from "../components/layout/meta"; 9 | 10 | export default function Index() { 11 | return ( 12 | 13 | 14 | 15 | 16 | 17 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /public/assets/images/bell.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/assets/images/comment.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/assets/images/like.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/assets/images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /public/assets/images/notification.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/assets/images/settings.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/assets/images/sidebar/home.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /public/assets/images/sidebar/listing.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /public/assets/images/sidebar/podcast.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /public/assets/images/sidebar/reading.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /public/assets/images/sidebar/tag.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /public/assets/images/sidebar/video.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /public/devto.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MA-Ahmad/dev.to-clone/9811951978cc8131028b7111bf540e8f95364f4f/public/devto.png -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MA-Ahmad/dev.to-clone/9811951978cc8131028b7111bf540e8f95364f4f/public/favicon.png -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": false, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve" 16 | }, 17 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 18 | "exclude": ["node_modules"] 19 | } 20 | --------------------------------------------------------------------------------