├── .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 | ![Example](https://alessiofranceschi.me/assets/img/react-commenting/reactions-example.gif) 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 | 80 | {submitMessage !== undefined && ( 81 | 82 | )} 83 |
84 | {/* */} 89 | 95 | {errors.name && } 96 | 102 | {errors.email && } 103 |