├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public ├── data │ └── data.json ├── favicon.ico ├── index.html ├── manifest.json └── robots.txt └── src ├── App.js ├── Assets ├── avatars │ ├── image-amyrobson.png │ ├── image-juliusomo.png │ ├── image-maxblagun.png │ └── image-ramsesmiron.png ├── design │ ├── active-states.jpg │ ├── desktop-design.jpg │ ├── desktop-modal.jpg │ ├── desktop-preview.jpg │ ├── mobile-design.jpg │ └── mobile-modal.jpg └── images │ ├── icon-delete.svg │ ├── icon-edit.svg │ ├── icon-minus.svg │ ├── icon-plus.svg │ ├── icon-reply.svg │ └── screenshot.png ├── Components ├── AddComment.js ├── Comment.js ├── DeleteModal.js ├── Reply.js ├── ReplyContainer.js ├── Styles │ ├── AddComment.scss │ ├── App.scss │ ├── Comment.scss │ ├── DeleteModal.scss │ ├── _mixins.scss │ ├── _variables.scss │ └── index.scss └── commentParts │ ├── CommentBtn.js │ ├── CommentFooter.js │ ├── CommentHeader.js │ ├── CommentVotes.js │ └── index.js ├── index.js └── utils ├── index.js └── time.js /.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 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | # assets 26 | /Assets/design -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Interactive comments section 2 | 3 | ![](./src/Assets/images/screenshot.png) 4 | 5 | ## The challenge 6 | 7 | - optimal layout for the app depending on their device's screen size. 8 | - hover states for all interactive elements on the page. 9 | - create, Read, Update, and Delete comments and replies. 10 | - up-vote and down-vote comments. 11 | - tracks the time dynamically since the comment or reply was posted. 12 | 13 | ### Information 14 | - this is a solution to the [Interactive comments section challenge on Frontend Mentor](https://www.frontendmentor.io/challenges/interactive-comments-section-iG1RugEG9). 15 | - by [Frontend Mentor](https://www.frontendmentor.io) 16 | 17 | ## Built with 18 | 19 | - CSS custom properties 20 | - Flex-box 21 | - Mobile-first workflow 22 | - [SCSS](https://sass-lang.com) - CSS Preprocessor 23 | - [React](https://reactjs.org/) - JS library 24 | 25 | ## What I learned 26 | 27 | - I learned how to calculate the time between current time and given time. 28 | 29 | ## Continued development 30 | 31 | - I still want to add the feature : 32 | - which sorts the comments the comment of the basis of their votes 33 | 34 | ## Author 35 | 36 | - Frontend Mentor - [@arshGoyalDev](https://www.frontendmentor.io/profile/arshGoyalDev) 37 | - Twitter - [@arshGoyalDDev](https://twitter.com/arshGoyalDev) 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "interactive-comments-section", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.16.1", 7 | "@testing-library/react": "^12.1.2", 8 | "@testing-library/user-event": "^13.5.0", 9 | "node": "^17.3.0", 10 | "react": "^18.1.0", 11 | "react-dom": "^18.1.0", 12 | "react-scripts": "5.0.0", 13 | "sass": "^1.45.1", 14 | "web-vitals": "^2.1.2" 15 | }, 16 | "scripts": { 17 | "start": "react-scripts start", 18 | "build": "react-scripts build", 19 | "test": "react-scripts test", 20 | "eject": "react-scripts eject" 21 | }, 22 | "eslintConfig": { 23 | "extends": [ 24 | "react-app", 25 | "react-app/jest" 26 | ] 27 | }, 28 | "browserslist": { 29 | "production": [ 30 | ">0.2%", 31 | "not dead", 32 | "not op_mini all" 33 | ], 34 | "development": [ 35 | "last 1 chrome version", 36 | "last 1 firefox version", 37 | "last 1 safari version" 38 | ] 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /public/data/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "currentUser": { 3 | "username": "juliusomo" 4 | }, 5 | "comments": [ 6 | { 7 | "id": 1, 8 | "content": "Impressive! Though it seems the drag feature could be improved. But overall it looks incredible. You've nailed the design and the responsiveness at various breakpoints works really well.", 9 | "createdAt": "23 November 2021", 10 | "score": 12, 11 | "username": "amyrobson", 12 | "currentUser": false, 13 | "replies": [] 14 | }, 15 | { 16 | "id": 2, 17 | "content": "Woah, your project looks awesome! How long have you been coding for? I'm still new, but think I want to dive into React as well soon. Perhaps you can give me an insight on where I can learn React? Thanks!", 18 | "createdAt": "5 December 2021", 19 | "score": 5, 20 | "username": "maxblagun", 21 | "currentUser": false, 22 | "replies": [ 23 | { 24 | "id": 3, 25 | "content": "@maxblaugn, If you're still new, I'd recommend focusing on the fundamentals of HTML, CSS, and JS before considering React. It's very tempting to jump ahead but lay a solid foundation first.", 26 | "createdAt": "18 December 2021", 27 | "score": 4, 28 | "username": "ramsesmiron", 29 | "currentUser": false, 30 | "replies": [] 31 | }, 32 | { 33 | "id": 4, 34 | "content": "@ramsesmiron, I couldn't agree more with this. Everything moves so fast and it always seems like everyone knows the newest library/framework. But the fundamentals are what stay constant.", 35 | "createdAt": "30 December 2021", 36 | "score": 2, 37 | "username": "juliusomo", 38 | "currentUser": true, 39 | "replies": [] 40 | } 41 | ] 42 | } 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arshGoyalDev/interactive-comments-section/8573c3d71d50c019ecf9555e2fbefc6c8ed6c0e8/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | Interactive comments section 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.png", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "favicon.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "favicon.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | 3 | import "./Components/Styles/App.scss"; 4 | 5 | import Comment from "./Components/Comment"; 6 | import AddComment from "./Components/AddComment"; 7 | 8 | const App = () => { 9 | const [comments, updateComments] = useState([]); 10 | const [deleteModalState, setDeleteModalState] = useState(false); 11 | 12 | const getData = async () => { 13 | const res = await fetch("./data/data.json"); 14 | const data = await res.json(); 15 | updateComments(data.comments); 16 | }; 17 | 18 | useEffect(() => { 19 | localStorage.getItem("comments") !== null 20 | ? updateComments(JSON.parse(localStorage.getItem("comments"))) 21 | : getData(); 22 | }, []); 23 | 24 | useEffect(() => { 25 | localStorage.setItem("comments", JSON.stringify(comments)); 26 | deleteModalState 27 | ? document.body.classList.add("overflow--hidden") 28 | : document.body.classList.remove("overflow--hidden"); 29 | }, [comments, deleteModalState]); 30 | 31 | // update score 32 | const updateScore = (score, id, type, method) => { 33 | let updatedComments = [...comments]; 34 | 35 | if (type === "comment") { 36 | updatedComments.forEach((data) => { 37 | if (data.id === id) { 38 | data.score = score; 39 | data.voted = method === "upvote" ? true : false; 40 | } 41 | }); 42 | } else if (type === "reply") { 43 | updatedComments.forEach((comment) => { 44 | comment.replies.forEach((data) => { 45 | if (data.id === id) { 46 | data.score = score; 47 | data.voted = method === "upvote" ? true : false; 48 | } 49 | }); 50 | }); 51 | } 52 | updateComments(updatedComments); 53 | }; 54 | 55 | // add comments 56 | const addComments = (newComment) => { 57 | const updatedComments = [...comments, newComment]; 58 | updateComments(updatedComments); 59 | }; 60 | 61 | // add replies 62 | const updateReplies = (replies, id) => { 63 | let updatedComments = [...comments]; 64 | updatedComments.forEach((data) => { 65 | if (data.id === id) { 66 | data.replies = [...replies]; 67 | } 68 | }); 69 | updateComments(updatedComments); 70 | }; 71 | 72 | // edit comment 73 | const editComment = (content, id, type) => { 74 | let updatedComments = [...comments]; 75 | 76 | if (type === "comment") { 77 | updatedComments.forEach((data) => { 78 | if (data.id === id) { 79 | data.content = content; 80 | } 81 | }); 82 | } else if (type === "reply") { 83 | updatedComments.forEach((comment) => { 84 | comment.replies.forEach((data) => { 85 | if (data.id === id) { 86 | data.content = content; 87 | } 88 | }); 89 | }); 90 | } 91 | 92 | updateComments(updatedComments); 93 | }; 94 | 95 | // delete comment 96 | let commentDelete = (id, type, parentComment) => { 97 | let updatedComments = [...comments]; 98 | let updatedReplies = []; 99 | 100 | if (type === "comment") { 101 | updatedComments = updatedComments.filter((data) => data.id !== id); 102 | } else if (type === "reply") { 103 | comments.forEach((comment) => { 104 | if (comment.id === parentComment) { 105 | updatedReplies = comment.replies.filter((data) => data.id !== id); 106 | comment.replies = updatedReplies; 107 | } 108 | }); 109 | } 110 | 111 | updateComments(updatedComments); 112 | }; 113 | 114 | return ( 115 |
116 | {comments.map((comment) => ( 117 | 126 | ))} 127 | 128 |
129 | ); 130 | }; 131 | 132 | export default App; 133 | -------------------------------------------------------------------------------- /src/Assets/avatars/image-amyrobson.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arshGoyalDev/interactive-comments-section/8573c3d71d50c019ecf9555e2fbefc6c8ed6c0e8/src/Assets/avatars/image-amyrobson.png -------------------------------------------------------------------------------- /src/Assets/avatars/image-juliusomo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arshGoyalDev/interactive-comments-section/8573c3d71d50c019ecf9555e2fbefc6c8ed6c0e8/src/Assets/avatars/image-juliusomo.png -------------------------------------------------------------------------------- /src/Assets/avatars/image-maxblagun.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arshGoyalDev/interactive-comments-section/8573c3d71d50c019ecf9555e2fbefc6c8ed6c0e8/src/Assets/avatars/image-maxblagun.png -------------------------------------------------------------------------------- /src/Assets/avatars/image-ramsesmiron.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arshGoyalDev/interactive-comments-section/8573c3d71d50c019ecf9555e2fbefc6c8ed6c0e8/src/Assets/avatars/image-ramsesmiron.png -------------------------------------------------------------------------------- /src/Assets/design/active-states.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arshGoyalDev/interactive-comments-section/8573c3d71d50c019ecf9555e2fbefc6c8ed6c0e8/src/Assets/design/active-states.jpg -------------------------------------------------------------------------------- /src/Assets/design/desktop-design.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arshGoyalDev/interactive-comments-section/8573c3d71d50c019ecf9555e2fbefc6c8ed6c0e8/src/Assets/design/desktop-design.jpg -------------------------------------------------------------------------------- /src/Assets/design/desktop-modal.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arshGoyalDev/interactive-comments-section/8573c3d71d50c019ecf9555e2fbefc6c8ed6c0e8/src/Assets/design/desktop-modal.jpg -------------------------------------------------------------------------------- /src/Assets/design/desktop-preview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arshGoyalDev/interactive-comments-section/8573c3d71d50c019ecf9555e2fbefc6c8ed6c0e8/src/Assets/design/desktop-preview.jpg -------------------------------------------------------------------------------- /src/Assets/design/mobile-design.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arshGoyalDev/interactive-comments-section/8573c3d71d50c019ecf9555e2fbefc6c8ed6c0e8/src/Assets/design/mobile-design.jpg -------------------------------------------------------------------------------- /src/Assets/design/mobile-modal.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arshGoyalDev/interactive-comments-section/8573c3d71d50c019ecf9555e2fbefc6c8ed6c0e8/src/Assets/design/mobile-modal.jpg -------------------------------------------------------------------------------- /src/Assets/images/icon-delete.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/Assets/images/icon-edit.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/Assets/images/icon-minus.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/Assets/images/icon-plus.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/Assets/images/icon-reply.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/Assets/images/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arshGoyalDev/interactive-comments-section/8573c3d71d50c019ecf9555e2fbefc6c8ed6c0e8/src/Assets/images/screenshot.png -------------------------------------------------------------------------------- /src/Components/AddComment.js: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | 3 | import "./Styles/AddComment.scss"; 4 | 5 | const AddComment = ({ buttonValue, addComments, replyingTo }) => { 6 | const replyingToUser = replyingTo ? `@${replyingTo}, ` : ""; 7 | const [comment, setComment] = useState(replyingToUser); 8 | 9 | const clickHandler = () => { 10 | if (comment === "" || comment === " ") return; 11 | 12 | const newComment = { 13 | id: Math.floor(Math.random() * 100) + 5, 14 | content: replyingToUser + comment.replace(replyingToUser, ""), 15 | createdAt: new Date(), 16 | score: 0, 17 | username: "juliusomo", 18 | currentUser: true, 19 | replies: [], 20 | }; 21 | 22 | addComments(newComment); 23 | setComment(""); 24 | }; 25 | 26 | return ( 27 |
28 |
29 |