├── .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 | 
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 |
44 | );
45 | };
46 |
47 | export default AddComment;
48 |
--------------------------------------------------------------------------------
/src/Components/Comment.js:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 |
3 | import "./Styles/Comment.scss";
4 |
5 | import AddComment from "./AddComment";
6 | import ReplyContainer from "./ReplyContainer";
7 | import DeleteModal from "./DeleteModal";
8 |
9 | import { CommentHeader, CommentFooter, CommentVotes } from "./commentParts";
10 |
11 | const Comment = ({
12 | commentData,
13 | updateScore,
14 | updateReplies,
15 | editComment,
16 | commentDelete,
17 | setDeleteModalState,
18 | }) => {
19 | const [replying, setReplying] = useState(false);
20 | const [editing, setEditing] = useState(false);
21 | const [content, setContent] = useState(commentData.content);
22 | const [deleting, setDeleting] = useState(false);
23 |
24 | const addReply = (newReply) => {
25 | const replies = [...commentData.replies, newReply];
26 | updateReplies(replies, commentData.id);
27 | setReplying(false);
28 | };
29 |
30 | const updateComment = () => {
31 | editComment(content, commentData.id, "comment");
32 | setEditing(false);
33 | };
34 |
35 | const deleteComment = (id, type) => {
36 | const finalType = type !== undefined ? type : "comment";
37 | const finalId = id !== undefined ? id : commentData.id;
38 | commentDelete(finalId, finalType, commentData.id);
39 | setDeleting(false);
40 | };
41 |
42 | return (
43 |
48 |
49 |
54 |
55 |
63 | {!editing ? (
64 |
{commentData.content}
65 | ) : (
66 |
80 |
{" "}
89 |
90 |
91 | {replying && (
92 |
97 | )}
98 | {commentData.replies !== [] && (
99 |
108 | )}
109 |
110 | {deleting && (
111 |
116 | )}
117 |
118 | );
119 | };
120 |
121 | export default Comment;
122 |
--------------------------------------------------------------------------------
/src/Components/DeleteModal.js:
--------------------------------------------------------------------------------
1 | import "./Styles/DeleteModal.scss";
2 |
3 | const DeleteModal = ({ setDeleting, deleteComment, setDeleteModalState }) => {
4 | const cancelDelete = () => {
5 | setDeleting(false);
6 | setDeleteModalState(false);
7 | };
8 |
9 | const deleteBtnClick = () => {
10 | deleteComment();
11 | setDeleteModalState(false);
12 | };
13 |
14 | return (
15 |
16 |
17 |
Delete comment
18 |
19 | Are you sure you want to delete this comment? This will remove the
20 | comment and can't be undone.
21 |
22 |
23 |
26 |
29 |
30 |
31 |
32 | );
33 | };
34 |
35 | export default DeleteModal;
36 |
--------------------------------------------------------------------------------
/src/Components/Reply.js:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 |
3 | import "./Styles/Comment.scss";
4 |
5 | import AddComment from "./AddComment";
6 | import DeleteModal from "./DeleteModal";
7 |
8 | import { CommentHeader, CommentFooter, CommentVotes } from "./commentParts";
9 |
10 | const Reply = ({
11 | commentData,
12 | updateScore,
13 | addNewReply,
14 | editComment,
15 | deleteComment,
16 | setDeleteModalState,
17 | }) => {
18 | const [replying, setReplying] = useState(false);
19 | const [editing, setEditing] = useState(false);
20 | const [content, setContent] = useState(commentData.content);
21 | const [deleting, setDeleting] = useState(false);
22 |
23 | // adding reply
24 | const addReply = (newReply) => {
25 | addNewReply(newReply);
26 | setReplying(false);
27 | };
28 |
29 | const commentContent = () => {
30 | const text = commentData.content.trim().split(" ");
31 | const firstWord = text.shift().split(",");
32 |
33 | return !editing ? (
34 |
35 | {firstWord}
36 | {text.join(" ")}
37 |
38 | ) : (
39 |