├── .eslintrc.json
├── .gitignore
├── README.md
├── components
├── AddComment
│ ├── AddCommentForm.js
│ ├── ImageUpload.js
│ ├── InputError.js
│ └── SubmitMessage.js
├── Comments
│ ├── AllComments.js
│ ├── CommentImage.js
│ └── SingleComment.js
├── Emoji
│ ├── Emoji.js
│ ├── EmojiAdder.js
│ ├── EmojiWithCounter.js
│ └── ReactionBlock.js
├── Header.js
└── LoadingComponent.js
├── lib
├── dynamicScriptLoader.js
├── emojiConfig.js
├── keyGen.js
├── sanityClient.js
├── snarkdown.js
├── stringHash.js
└── utils.js
├── next.config.js
├── package.json
├── pages
├── _app.js
├── api
│ ├── addReaction.js
│ ├── sendComment.js
│ └── setCommentImage.js
└── index.js
├── studio
├── README.md
├── config
│ ├── .checksums
│ └── @sanity
│ │ ├── data-aspects.json
│ │ ├── default-layout.json
│ │ ├── default-login.json
│ │ └── form-builder.json
├── dist
│ ├── index.html
│ └── static
│ │ ├── .gitkeep
│ │ ├── css
│ │ └── main.css
│ │ ├── favicon.ico
│ │ └── js
│ │ ├── app.bundle.js
│ │ └── vendor.bundle.js
├── package.json
├── plugins
│ └── .gitkeep
├── sanity.json
├── schemas
│ ├── comment.js
│ ├── commentReactions.js
│ └── schema.js
├── static
│ ├── .gitkeep
│ └── favicon.ico
├── tsconfig.json
└── yarn.lock
├── styles
└── globals.css
├── vercel.json
└── yarn.lock
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "es2021": true,
5 | "node": true
6 | },
7 | "extends": [
8 | "eslint:recommended",
9 | "plugin:react/recommended",
10 | "plugin:react-hooks/recommended"
11 | ],
12 | "parserOptions": {
13 | "ecmaFeatures": {
14 | "jsx": true
15 | },
16 | "ecmaVersion": 12,
17 | "sourceType": "module"
18 | },
19 | "plugins": [
20 | "react"
21 | ],
22 | "rules": {
23 | "react/react-in-jsx-scope": "off"
24 | },
25 | "globals": {
26 | "React": "writable"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | package-lock.json
6 | /studio/node_modules
7 | /.pnp
8 | .pnp.js
9 |
10 | # testing
11 | /coverage
12 |
13 | # next.js
14 | /.next/
15 | /out/
16 |
17 | # production
18 | /build
19 |
20 | # misc
21 | .DS_Store
22 | *.pem
23 |
24 | # debug
25 | npm-debug.log*
26 | yarn-debug.log*
27 | yarn-error.log*
28 |
29 | # local env files
30 | .env.local
31 | .env.development.local
32 | .env.test.local
33 | .env.production.local
34 |
35 | # vercel
36 | .vercel
37 | .env
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React Real-Time Commenting System
2 |
3 | Real-Time Commenting System built on top of [Next.js](https://nextjs.org/) and with [Sanity.io](https://www.sanity.io/) as the data store.
4 | This is meant as a reference repository, currently there's no package available but you can follow my tutorials to recreate this project for your website: [my blog here](https://alessiofranceschi.me/blog/react-commenting-system).
5 |
6 | 
7 |
8 | ## Demo
9 | Test out this project [here](https://react-commenting-system.vercel.app/).
10 |
11 | ## Features
12 | - Anonymous by default, username and email not needed
13 | - Nested comments without limits
14 | - Reactions with emoticons
15 | - Real-Time: new comments and reaction shown without reloading the page
16 | - Markdown support
17 | - ReCaptcha v3
18 | - Responsive
19 | - Comments without urls are approved by default
20 |
21 | ## Tutorial
22 | I wrote a three-part series on how to build this project:
23 | - [Building a Real-Time Commenting System in React [Part 1/3]](https://alessiofranceschi.me/blog/react-commenting-system)
24 | - [Making Nested Comments - Building a Real-Time Commenting System in React [Part 2/3]](https://alessiofranceschi.me/blog/react-commenting-system-part-2)
25 | - [Emoji Reactions for Comments - Building a Real-Time Commenting System in React [Part 3/3]](https://alessiofranceschi.me/blog/react-commenting-system-part-3)
26 |
--------------------------------------------------------------------------------
/components/AddComment/AddCommentForm.js:
--------------------------------------------------------------------------------
1 | import { useForm } from "react-hook-form";
2 | import InputError from "./InputError";
3 | import { Fragment, useState } from "react";
4 | import SubmitMessage from "./SubmitMessage";
5 | // import ImageUpload from "./ImageUpload";
6 |
7 | export default function AddCommentForm({
8 | parentCommentId,
9 | firstParentId,
10 | extraClass,
11 | }) {
12 | const [submitMessage, setSubmitMessage] = useState({});
13 | const [submittedFormData, setSubmittedFormData] = useState({});
14 | const [isSending, setIsSending] = useState(false);
15 | const { register, errors, handleSubmit, reset } = useForm();
16 |
17 | // const [imgSrc, setImgSrc] = useState("");
18 |
19 | // function uploadHandler(e) {
20 | // setImgSrc(URL.createObjectURL(e.target.files[0]));
21 | // }
22 |
23 | const onSubmit = data => {
24 | setSubmittedFormData(data);
25 | setIsSending(true);
26 |
27 | // if (data.userImage) data.userImage = data.userImage[0];
28 |
29 | if (parentCommentId) {
30 | data.parentCommentId = parentCommentId;
31 | data.firstParentId = firstParentId;
32 | }
33 |
34 | grecaptcha.ready(() => {
35 | grecaptcha
36 | .execute(process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY, {
37 | action: "submit",
38 | })
39 | .then(token => {
40 | data.token = token;
41 | fetch("/api/sendComment", {
42 | method: "POST",
43 | body: JSON.stringify(data),
44 | })
45 | .then(r => {
46 | if (r.status !== 200) {
47 | r.json().then(e => {
48 | throw new Error(e);
49 | });
50 | } else return r.json();
51 | })
52 | .then(j => {
53 | reset({ ...submittedFormData });
54 | setSubmittedFormData({});
55 | setIsSending(false);
56 |
57 | setSubmitMessage({
58 | text: j.message,
59 | success: true,
60 | });
61 | setTimeout(() => {
62 | setSubmitMessage({});
63 | }, 2000);
64 | })
65 | .catch(err => {
66 | setSubmitMessage({
67 | text: String(err.message),
68 | success: false,
69 | });
70 | setTimeout(() => {
71 | setSubmitMessage({});
72 | }, 2000);
73 | });
74 | });
75 | });
76 | };
77 |
78 | return (
79 |
" +
85 | outdent(encodeAttr(t).replace(/^\n+|\n+$/g, "")) +
86 | "
";
87 | }
88 | // > Quotes, -* lists:
89 | else if ((t = token[6])) {
90 | if (t.match(/\./)) {
91 | token[5] = token[5].replace(/^\d+/gm, "");
92 | }
93 | inner = parse(outdent(token[5].replace(/^\s*[>*+.-]/gm, "")));
94 | if (t == ">") t = "blockquote";
95 | else {
96 | t = t.match(/\./) ? "ol" : "ul";
97 | inner = inner.replace(/^(.*)(\n|$)/gm, "" + encodeAttr(token[16]) + "
";
134 | }
135 | // Inline formatting: *em*, **strong** & friends
136 | else if (token[17] || token[1]) {
137 | chunk = tag(token[17] || "--");
138 | }
139 | out += prev;
140 | out += chunk;
141 | }
142 |
143 | return (out + md.substring(last) + flush()).replace(/^\n+|\n+$/g, "");
144 | }
145 |
--------------------------------------------------------------------------------
/lib/stringHash.js:
--------------------------------------------------------------------------------
1 | // Basic string hasher
2 |
3 | const hashString = input => {
4 | let hash = 0,
5 | i,
6 | chr;
7 | for (i = 0; i < input.length; i++) {
8 | chr = input.charCodeAt(i);
9 | hash = (hash << 5) - hash + chr;
10 | hash |= 0;
11 | }
12 | return hash;
13 | };
14 |
15 | export default hashString;
16 |
--------------------------------------------------------------------------------
/lib/utils.js:
--------------------------------------------------------------------------------
1 | const formatDate = fullDate => {
2 | const date = fullDate?.split("T")[0];
3 | const year = date.split("-")[0];
4 | const month = new Date(date).toLocaleDateString("default", {
5 | month: "long",
6 | });
7 | const day = date.split("-")[2];
8 |
9 | return `${day} ${month} ${year}`;
10 | };
11 |
12 | export { formatDate };
13 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | eslint: {
3 | // Warning: This allows production builds to successfully complete even if
4 | // your project has ESLint errors.
5 | ignoreDuringBuilds: true,
6 | },
7 | };
8 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "javascript-commenting-system",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start"
9 | },
10 | "dependencies": {
11 | "@pandasekh/dynamic-script-loader": "^1.2.0",
12 | "@sanity/client": "^2.10.5",
13 | "@sanity/image-url": "^1.0.1",
14 | "dotenv": "^11.0.0",
15 | "lepre": "^0.5.1",
16 | "next": "^12.0.7",
17 | "react": "17.0.2",
18 | "react-dom": "17.0.2",
19 | "react-hook-form": "^7.7.1",
20 | "sanitize-html": "^2.4.0"
21 | },
22 | "devDependencies": {
23 | "eslint": "^8.6.0",
24 | "eslint-plugin-react": "^7.28.0",
25 | "eslint-plugin-react-hooks": "^4.3.0"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/pages/_app.js:
--------------------------------------------------------------------------------
1 | import Head from "next/head";
2 | import { Fragment } from "react";
3 | import "../styles/globals.css";
4 |
5 | function MyApp({ Component, pageProps }) {
6 | return (
7 |