Next.js MUI Comments Section with Basic Authentication

Photo by <a href="https://unsplash.com/@ninjason?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Jason Leung</a> on <a href="https://unsplash.com/photos/mZNRsYE9Qi4?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a>

Overview

A comments section is critical to any dynamic website, as it allows users to engage with the content and provide feedback. In this blog, we will explore the process of building a comments section in a Next.js project. We will cover how to build a simple and user-friendly comments section. Whether a beginner or an experienced web developer, this guide will provide the knowledge along with a practical example to help you create a comments section for your Next.js project. So, let’s dive in and start building!

Source Files

GitHub

Prerequisites

If you are not using the source files above, get the Next.js template from MUI. You might have to clone the entire repository to get it, but it is worth it. The template from MUI has everything configured out of the box and is ready to use. Installing Material UI yourself can be challenging unless you are already comfortable with Next.js and Material UI.

https://github.com/mui/material-ui/tree/master/examples/nextjs

After cloning or downloading the template, go into the directory and run

npm install

Additional Libraries Used

If you want to do this from scratch. I recommend cloning the source files in case the library versions have changed. Plus the example application in GitHub has a few extra pages. For more information on how to create a new Next.js app click here.

Axios is used for client side data operations but not necessary if you would rather use fetch.

MomentJS for date functions. Example – Post 3 Days ago

SQLite and SQLite3 for database connectivity.

React Infinite Scroll Component for loading records as you scroll down.

npm install axios moment sqlite sqlite3 react-infinite-scroll-component 

Next-Auth

Next Auth is used to authenticate and secure pages. There are two hardcoded users in the example application.

npm install next-auth

Configure Next.js with tsconfig.json

To add aliases for paths and other framework configurations, add a file labeled ‘.\tsconfig.json’ to the root of your application.

The configuration below lets you use ‘@\components\filename.js’ instead of ‘..\..\..\components\filename.js’.

JSON source for tsconfig.json. For JavaScript only with no TypeScript label the file jsconfig.json.

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
        "@/components/*": ["components/*"],
        "@/src/*": ["src/*"],
        "@/lib/*": ["lib/*"],
        "@/styles/*": ["styles/*"],
    },
    "target": "es5",
    "lib": [
      "dom",
      "dom.iterable",
      "esnext"
    ],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": false,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "incremental": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve"
  },
  "include": [
    "next-env.d.ts",
    "**/*.ts",
    "**/*.tsx"
  ],
  "exclude": [
    "node_modules"
  ] 
}

Running Next.js App

Make sure you are in the root of the application when you run it.

npm run dev

SQLITE

There are two tables for this example. A Users and Comments table. Please use the database file labeled comments.db from GitHub. Get the SQLite Studio desktop application here.

Comments Table

Set the UserId column as the primary key with auto increment.

CREATE TABLE Users (
     UserId INTEGER PRIMARY KEY,
     Name TEXT,         
     Email TEXT,
     Image TEXT,
     DateJoined DATETIME
 );

Comment Replies Table

Set the CommentId column as the primary key with auto increment.

CREATE TABLE Comments (
     CommentId INTEGER PRIMARY KEY,
     UserId INTEGER NOT NULL,          
     Content TEXT,         
     IsActive TINYINT,
     DateCreated DATETIME,
     DateModified DATETIME
 );

Building the API

The API is built locally in NextJS using Next-Auth to handle authentication

Next-Auth

There are two users hardcoded using the CredentialsProvider provider.

Code for “pages\api\auth\[…nextauth].ts“. Note: […nextauth].ts must be in the folder structure ”.\pages\auth\[…nextauth].ts” in order for it to work.

There are two hard coded users named Jennifer Smith and Terry Oscar. Both users have a unique avatar picture and user id.

import NextAuth, { NextAuthOptions } from "next-auth"
import CredentialsProvider from "next-auth/providers/credentials"

export const authOptions: NextAuthOptions = {

  providers: [
    CredentialsProvider({
      id: "domain-login",
      name: "Jennifer Smith",
      
      credentials: {
        username: { label: "Username", type: "text", placeholder: "jsmith" },
        password: { label: "Password", type: "password", }
      },
      async authorize(credentials, req) {
        const user = { id: 1, name: "Jennifer Smith", email: "jennifers@example.com", image: 'https://randomuser.me/api/portraits/women/72.jpg' }
          
        if (user) {
          return user
        } else {
          return null  
        }
      }
    }),
    CredentialsProvider({
      id: "user-login",
      name: "Terry Oscar",

      credentials: {
        username: { label: "Username", type: "text", placeholder: "jsmith" },
        password: {  label: "Password", type: "password" }
      },
      async authorize(credentials, req) {        
        const user = { id: 2, name: "Terry Oscar", email: "terryo@example.com", image: 'https://randomuser.me/api/portraits/women/67.jpg' }
  
        if (user) {          
          return user
        } else {          
          return null  
        }
      }
    }),
  ],
  pages: {
    signIn: '/login',
    signOut: '/auth/signout', 
  },
  theme: {
    colorScheme: "light",
  },
  callbacks: {
    async jwt({ token }) {
      token.userRole = "admin"
      return token
    },
  },
}

export default NextAuth(authOptions)

Enable Custom Sign In and Sign Out Page

There is a custom Sign In and Sign Out pages enabled pages in the […nextauth].ts file below.

...
export const authOptions: NextAuthOptions = {

  providers: [
    ...
  ],
  pages: {
    signIn: '/login',
    signOut: '/auth/signout', 
  },
  theme: {
    colorScheme: "light",
  },
  ... 
}

Site Connection File

The list, update, add, and delete API calls are from a secure page under ‘.\pages\auth\comments\index.js’.

For the demo application in GitHub, there is a file with all of the connection strings under ‘.\lib\SiteConn.js’. The connection string for the Comments and Northwind are included.

Code for the SiteConn.js file.

export default {
  get_config: function(){
    return {
      BASE_URL: "http://localhost:3000",        
      NorthwindDbFileName: "northwind.db",
      CommentDbFileName: "comments.db",
    }
  },
}

API CRUD Operations

The list.js, update.js, add.js, and delete.js API routes are called by the secure page ‘.\pages\auth\comments\index.js’.

A nicer way to do this, would be to put all the CRUD operations in one file and use GET, POST, PUT, and DELETE,

List Comments

Get a list of all the comments from the comments table. The APS is located ‘.\pages\comments\list’ and called by ‘.\pages\comments’.

const sqlite = require('sqlite');
const sqlite3= require('sqlite3');
import {open} from 'sqlite';

import LibConst from "@/libs/SiteConn";

export default async function (req, res){
  try{
    var dbFile = LibConst.get_config().CommentDbFileName
    
// console.log(d)
    const db = await open(
      {filename: dbFile , driver: sqlite3.Database}
    );
    const sqlQuery = 'SELECT c.CommentId, c.UserId, c.Content, ' +
    'c.Replies, c.IsActive, c.DateCreated, c.DateModified,' +
    'u.UserId, u.Name, u.Picture ' +
    'FROM Comments c, Users u WHERE c.UserId = u.UserId ORDER BY c.CommentId DESC';  
    
    const items = await db.all(sqlQuery);    var ret ={
      items: items
    }
    res.status(200).json(ret);

  } catch (err) {
      console.error(err);
      res.status(500).send();    
  }   
};

Update Comment

Updates a single record from the comments table. The APS is located ‘.\pages\comments\delete’ and called by ‘.\pages\comments\index.js’.

const sqlite = require('sqlite');
const sqlite3= require('sqlite3');
import {open} from 'sqlite';

import LibConst from "@/libs/SiteConn";

//
export default async function (req, res){
  try{
    console.log('API', req.body);
    var data = req.body
    

    var dbFile = LibConst.get_config().CommentDbFileName
    const db = await open(
      {filename: dbFile , driver: sqlite3.Database}
    );
    const result = await db.run(
      'UPDATE Comments SET Content = ? WHERE CommentId = ?',      
      data.Content,
      data.CommentId
    )    

    res.json(result);
  } catch (err) {
      console.log(err);
      res.status(500).send();    
  }   
};

Add Comment

Adds a single record from the comments table. The APS is located ‘.\pages\comments\delete’ and called by ‘.\pages\comments\index.js’.

const sqlite = require('sqlite');
const sqlite3= require('sqlite3');
import {open} from 'sqlite';
import moment from 'moment';
import LibConst from "@/libs/SiteConn";

//
export default async function (req, res){
  try{
    var data = req.body
    
    var today = new Date();
    const createData = moment(today).format('YYYY-MM-DD HH:MM:SS');
    
    var dbFile = LibConst.get_config().CommentDbFileName
    const db = await open(
      {filename: dbFile , driver: sqlite3.Database}
    );
    
    const result = await db.run(
      'INSERT INTO Comments (UserId, Content, Replies, IsActive, DateCreated) VALUES ( ?, ?, ?, ?, ?)',      
      data.UserId,
      data.Content,
      0,
      1,
      createData,
    )    
    
    const sqlSelectQuery = 'SELECT c.CommentId, c.UserId, c.Content, ' +
    'c.Replies, c.IsActive, c.DateCreated, c.DateModified,' +
    'u.UserId, u.Name, u.Picture ' +
    'FROM Comments c, Users u WHERE c.UserId = u.UserId ORDER BY c.CommentId DESC';  

    const items = await db.all(sqlSelectQuery);
    var ret ={
      items: items
    }
    
    res.json(ret);

  } catch (err) {
      console.log(err);
      res.status(500).send();    
  }   
};

Delete Comment

Deletes a single record from the comments table. The APS is located ‘.\pages\comments\delete’ and called by ‘.\pages\comments\index.js’.

const sqlite = require('sqlite');
const sqlite3= require('sqlite3');
import {open} from 'sqlite';
import LibConst from "@/libs/SiteConn";

//* D E L E T E   A   S I N G L E   C O M M E N T
export default async function (req, res){
  try{
    console.log('DELETE API', req.body);

    var data = req.body
        
    var dbFile = LibConst.get_config().CommentDbFileName
    const db = await open(
      {filename: dbFile , driver: sqlite3.Database}
    );
    
    const result = await db.run(
      'DELETE FROM Comments WHERE CommentId = ?',      
      data.CommentId,
    )    
    
    const sqlSelectQuery = 'SELECT c.CommentId, c.UserId, c.Content, ' +
    'c.Replies, c.IsActive, c.DateCreated, c.DateModified,' +
    'u.UserId, u.Name, u.Picture ' +
    'FROM Comments c, Users u WHERE c.UserId = u.UserId ORDER BY c.CommentId DESC';  

    const items = await db.all(sqlSelectQuery);
    var ret ={
      items: items
    }
    res.json(ret);


    res.json(ret);
  } catch (err) {
      console.log(err);
      res.status(500).send();    
  }   
};

Securing Pages with Middleware

Using NextJS ‘.\middleware.ts’ file at the root of the application.

You can secure folders and individual pages with Next-Auth using middleware.ts. The following line secures everything under the ‘.\pages\auth\’ and ‘.\pages\profile’ folders.

export const config = { matcher: ["/auth/:path*", "/profile"] }

Code for middleware.ts.

import { withAuth } from "next-auth/middleware"

// More on how NextAuth.js middleware works: https://next-auth.js.org/configuration/nextjs#middleware
export default withAuth({
  callbacks: {
    authorized({ req, token }) {
      // `/admin` requires admin role
      if (req.nextUrl.pathname === "/admin") {
        return token?.userRole === "admin"
      }
      // `/me` only requires the user to be logged in
      return !!token
    },
  },
})

export const config = { matcher: "/auth/:path*", "/profile"] }

Login Page

Sign In Page CSS Styling

This example uses a custom login page which requires an option enabled

.main{
  position: absolute;
  top: 0;
  left: 0;
  width: 100vw;
  height: 100vh;
  background: url('/background.avif') fixed no-repeat;
  background-size: cover;
}

#container{
    width: 350px;
    height: 500px;
    background: inherit;
    position: absolute;
    overflow: hidden;
    top: 50%;
    left: 50%;
    margin-left: -175px;
    margin-top: -250px;
    border-radius: 8px;
  }
  #container:before{
    width: 400px;
    height: 550px;
    content: "";
    position: absolute;
    top: -25px;
    left: -25px;
    bottom: 0;
    right: 0;
    background: inherit;
    box-shadow: inset 0 0 0 200px rgba(255,255,255,0.2);
    filter: blur(10px);
  }
  .form img{
    width: 120px;
    height: 120px;
    border-radius: 100%;
  }
  .form{
    text-align: center;
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate(-50%,-50%);
  }
  .input{
    background: 0;
    width: 200px;
    outline: 0;
    border: 0;
    border-bottom: 2px solid rgba(255,255,255, 0.3);
    margin: 20px 0;
    padding-bottom: 10px;
    font-size: 18px;
    font-weight: bold;
    color: rgba(255,255,255, 0.8);
  }
  .input[type="submit"]{
    border: 0;
    border-radius: 8px;
    padding-bottom: 0;
    height: 60px;
    background: #df2359;
    color: white;
    cursor: pointer;
    transition: all 600ms ease-in-out;
  }
  .input[type="submit"]:hover{
    background: #C0392B;
  }
  .span a{
    color: rgba(255,255,255, 0.8);
  }

Sign In Page Code

import {useState, useEffect} from 'react';
import Avatar from '@mui/material/Avatar';
import Button from '@mui/material/Button';
import CssBaseline from '@mui/material/CssBaseline';
import Paper from '@mui/material/Paper';
import Grid from '@mui/material/Grid';
import Box from '@mui/material/Box';
import LockOutlinedIcon from '@mui/icons-material/LockOutlined';
import Typography from '@mui/material/Typography';
import { useRouter } from 'next/router';
import { Stack } from '@mui/material';
import GoogleIcon from '@mui/icons-material/Google';
import GitHubIcon from '@mui/icons-material/GitHub';
import UserIcon from '@mui/icons-material/Person';
import { getProviders, signIn,  getSession } from "next-auth/react";
import { getToken } from "next-auth/jwt"
import compStyle from './login.module.css'


export default function Signin({ token, csrfToken, providers }) { 

  const router = useRouter();

  useEffect(() => {
    if(token !== null) router.push('/auth/comments')  
  }, []);

  const [values, setValues] = useState({
    email: '',
    password: '',
    pin: '',
    showPassword: false    
  });
      
  const [showAlert, setShowAlert] = useState(false);

  const getProviderIcon = (provider) => {
    console.log('provider', provider)
    if(provider === 'Google') return <GoogleIcon/>
    if(provider === 'GitHub') return <GitHubIcon/>
    else return <UserIcon/>
  }

  console.log('token', token)
  return (
    <div className={compStyle.main}>
    <div id={compStyle.container}>
      <CssBaseline />
      
      <Paper elevation={2}>  
      <Box 
        blur={10}
          sx={{
            padding: 2,
            marginTop: 8,
            display: 'flex',
            flexDirection: 'column',
            alignItems: 'center',
            
          }}
        >
        
        <Avatar >
          <LockOutlinedIcon />
        </Avatar>
        <Typography component="h1" variant="h5" style={{color: 'white', zIndex: 99}}>
          Sign in
        </Typography>
     
        <form  noValidate>
          <Grid container spacing={2}>
      
       
            <Grid item xs={12}>
            
              <Stack spacing={2} sx={{mt: 8, mb: 4}}>
              {Object.values(providers).map((provider) => (
                <div key={provider.name}>                  
                  <Button size="large" variant="contained" fullWidth onClick={() => signIn(provider.id)} startIcon={getProviderIcon(provider.name)}>
                    Sign in as {provider.name}
                  </Button>
                </div>
              ))}
              </Stack>

            </Grid>
          </Grid>
        </form>
        
      </Box>
      </Paper>
    </div>
    </div>
  );
}

export async function getServerSideProps(context) {
  const { req, res } = context
  
  try {    
    const secret = process.env.NEXTAUTH_SECRET
    const token = await getToken({ req, secret })
 
    return { props: { token: token,  providers: await getProviders() } };
  } catch (e) {
    return { props: {  providers: await getProviders() } };
  }
  
}

Components for the Comments Page

Comment Card

Code for CommentCard.js

import * as React from 'react';
import moment from 'moment';
import { Avatar, Grid, Paper, Button, TextField } from '@mui/material';
import EditIcon from '@mui/icons-material/Edit';
import SaveIcon from '@mui/icons-material/Save';
import ClearIcon from '@mui/icons-material/Clear';
import DialogDeleteComment from './DialogDeleteComment'
import Stack from '@mui/material/Stack';
import axiosConn from '@/src/axiosConn'

//https://randomuser.me/api/ results.picture.thumbnail  results.name.first results.name.last
export default function CommentCard(props) {
  

  const [isEditComment, setIsEditComment] = React.useState(false);
  const [comment, setComment] = React.useState('');
  const [isRepliesExpanded, setIsRepliesExpanded] = React.useState(false);
  const [replyToComment, setReplyToComment] = React.useState('');
  const [replySkeletonCount, setReplySkeletonCount] = React.useState([]);
  const [openDeleteDialog, setOpenDeleteDialog] = React.useState(false);
  const [openAddReply, setOpenAddReply] = React.useState(false);
  const [repliesData, setRepliesData] = React.useState([]);
  const [isRepliesDataLoaded, setIsRepliesDataLoaded] = React.useState(false);
  const [repliesCount, setRepliesCount] = React.useState(0);
  
  //* I N I T
  
  React.useEffect(() => {
    setComment(props.data.Content)
    var tmpCntArr = []
    for (let i = 0; i < props.data.Replies; i++) {
      tmpCntArr.push(i)
    }
    setReplySkeletonCount(tmpCntArr)
    setRepliesCount(props.data.Replies);
  }, []);

  
  
  //* H A N D L E   E N A B L E   E D I T   C O M M E N T
  
  const handleEnableEditComment = (boolVal) => {
    setIsEditComment(boolVal)
  }

    
  
  //* H A N D L E   C A N C E L   E D I T
  
  const handleCancelEdit = () => {
    setComment(props.data.Content)
    setIsEditComment(false)
  }

  
  //* H A N D L E   O P E N   D I A L O G

  const handleDialogOpen = () => {
    setOpenDeleteDialog(true);
  };


  //* H A N D L E   C L O S E   D I A L O G
  
  const handleDialogClose = () => {
    setOpenDeleteDialog(false);
  };

  //* H A N D L E   D I A L O G   D E L E T E   C O M M E N T

  const handleDialogDeleteComment = (ID) => {
    
    setOpenDeleteDialog(false);
    props.handleDeleteComment(props.data.CommentId)
    
  };

  //* H A N D L E   S A V E   E D I T

  const handleSaveEdit = (commentStr) => {  
    axiosConn.post('/api/comments/update', { CommentId: props.data.CommentId, Content: comment })
    .then((data) => {
      console.log(data); // JSON data parsed by `data.json()` call          
      setRepliesData(data.data.data)
      setRepliesCount(repliesCount + 1)                    
      setIsEditComment(false);    
    });            

    
  }
    
  var momentDate=new moment(props.data.DateCreated);
  
  return (
    <div style={{ padding: 14 }} className="App">
      <Paper style={{ padding: "10px 10px", marginTop: 10 }}>
        <Grid container wrap="nowrap" spacing={2}>
          <Grid item justifyContent="left">
            <Avatar alt={props.data.Name} src={props.data.Picture} />
          </Grid>
          <Grid justifyContent="left" item xs zeroMinWidth>
            <Grid container wrap="nowrap" spacing={2}>            
              <Grid justifyContent="right" item xs zeroMinWidth>
                <h4 style={{ margin: 0, textAlign: "left" }}>{props.data.Name}</h4>  
              </Grid>          
              <Grid justifyContent="right" item>
                {props.data.UserId == props.currentUserId &&
                <>
                  {isEditComment ?
                  <>
                    <Button variant="text" color="warning" startIcon={<ClearIcon/>} onClick={() => handleCancelEdit()}>Cancel</Button>
                    <Button variant="text" color="secondary" startIcon={<SaveIcon/>} onClick={() => handleSaveEdit()}>Save</Button>
                  </>
                  :
                  <>                    
                    <Stack direction="row" spacing={2}>
                      <DialogDeleteComment 
                                          handleDialogOpen={handleDialogOpen} 
                                          handleDialogClose={handleDialogClose}
                                          handleDialogDeleteComment={handleDialogDeleteComment} 
                                          openDeleteDialog={openDeleteDialog}/>
                      <Button variant="text" color="primary" startIcon={<EditIcon/>} onClick={() => handleEnableEditComment(true)}>Edit</Button>
                    </Stack>
                  </>
                  }

                </>
                }
              </Grid>          
            </Grid>
            
            {isEditComment ?
                <>
                  <TextField
                    fullWidth
                    id="filled-multiline-static"                      
                    label=""
                    multiline
                    rows={4}
                    defaultValue=""                      
                    variant="outlined"
                    value={comment}
                    onChange={(e) => setComment(e.target.value)}
                  />
                </>
                :
                <>
                  <p style={{ textAlign: "left" }}>              
                    {comment}
                  </p>                  
                </>
            }
          
            <Grid container wrap="nowrap" spacing={6}>
              <Grid item align="left" xs={2}>
                <p style={{ textAlign: "left", color: "gray" }}>
                  Posted {moment(momentDate).fromNow(true)} ago 
                </p>                          
              </Grid>
              
            </Grid>
            
          </Grid>          
        </Grid>
      </Paper>          
    </div>
  );
}

Comment Skeleton Component

Comment Skeleton Component

import * as React from 'react';
import { Grid, Paper} from '@mui/material';
import Skeleton from '@mui/material/Skeleton';

export default function CommentSkeleton(props) {
  
  return (
    <div style={{ padding: 14 }} className="App">
      <Paper style={{ padding: "10px 10px", marginTop: 10 }}>
        <Grid container wrap="nowrap" spacing={2}>
          <Grid item justifyContent="left">
          <Skeleton animation="wave" variant="circular" width={40} height={40} />
          </Grid>
          <Grid justifyContent="left" item xs zeroMinWidth>
            <Grid container wrap="nowrap" spacing={2}>            
              <Grid justifyContent="right" item xs zeroMinWidth>
                <Skeleton animation="wave" height={30} width="10%" style={{ marginBottom: 6 }}/>
              </Grid>          
              <Grid justifyContent="right" item>
                <Skeleton animation="wave" height={30} width="100%" style={{ marginRight: 66 }} />
              </Grid>          
            </Grid>
            <Skeleton animation="wave" height={25} width="80%" />
            <p style={{ textAlign: "left", color: "gray" }}>
            <Skeleton animation="wave" height={25} width="30%" />
            </p>                          
          </Grid>          
        </Grid>
      </Paper>
    </div>
  );
}

New Comment Card Component

Code for NewComment.js.

import React from "react";
import ReactDOM from "react-dom";

import { Divider, Avatar, Grid, Paper, TextField, Button } from '@mui/material';



  // "https://images.pexels.com/photos/1681010/pexels-photo-1681010.jpeg?auto=compress&cs=tinysrgb&dpr=3&h=750&w=1260";

export default function CommentCard(props) {
  console.log('props.userSession', props.userSession)
  const imgLink = props.userSession.picture;
  return (
    <div style={{ padding: 14 }} className="App">
      <Paper style={{ padding: "10px 10px", marginTop: 10 }} elevation={3}>
        <Grid container wrap="nowrap" spacing={2} style={{padding: 8}}>
          <Grid item>
            <Avatar alt={''} src={imgLink} />
          </Grid>
          <Grid justifyContent="left" item xs zeroMinWidth>
            <TextField
              fullWidth
              id="filled-multiline-static"
              placeholder="Add a comment"
              label=""
              multiline
              rows={4}
              defaultValue=""
              variant="outlined"
              value={props.newComment}
              onChange={(e) => props.setNewComment(e.target.value)}
            />
          </Grid>
          <Grid item>
              <Button variant="contained" color="primary" onClick={props.handleNewComment}>Send</Button>
          </Grid>

        </Grid>
      </Paper>
    </div>
  );
}

Delete Comment Dialog Component

Code for DialogDeleteCommenty.js.

import * as React from 'react';
import Button from '@mui/material/Button';
import Dialog from '@mui/material/Dialog';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogContentText from '@mui/material/DialogContentText';
import DialogTitle from '@mui/material/DialogTitle';
import useMediaQuery from '@mui/material/useMediaQuery';
import { useTheme } from '@mui/material/styles';
import DeleteIcon from '@mui/icons-material/Delete';

export default function DialogDeleteComment(props) {
  const [open, setOpen] = React.useState(false);
  const theme = useTheme();
  const fullScreen = useMediaQuery(theme.breakpoints.down('md'));


  return (
    <div>
      <Button variant="text" color="warning" onClick={props.handleDialogOpen} startIcon={<DeleteIcon/>} >
        Delete
      </Button>
      <Dialog
        fullScreen={fullScreen}
        open={props.openDeleteDialog}
        onClose={props.handleDialogClose}
        aria-labelledby="responsive-dialog-title"
      >
        <DialogTitle id="responsive-dialog-title">
          {"Are you sure?"}
        </DialogTitle>
        <DialogContent>
          <DialogContentText>
            Do you want to delete this comment?
          </DialogContentText>
        </DialogContent>
        <DialogActions>
          <Button variant="text" autoFocus onClick={props.handleDialogClose} color="warning">
            No
          </Button>
          <Button onClick={(e) =>props.handleDialogDeleteComment(e)} >
            Yes
          </Button>
        </DialogActions>
      </Dialog>
    </div>
  );
}

Comments Page

The comments page is under

import React, {useEffect, useState} from "react";
import Head from 'next/head';
import { styled } from '@mui/material/styles';
import { Container, Typography } from '@mui/material';
import CommentCard from '@/components/comments/CommentCard';
import NewComment from '@/components/comments/NewComment';
import CommentSkeleton from '@/components/comments/CommentSkeleton';
import { Grid, Paper } from '@mui/material';
import axiosConn from '@/src/axiosConn'
import { getToken } from "next-auth/jwt";

export default function Comments({ token, ssrData }) {
  
  const [newComment, setNewComment] = useState('');
  const [commentsData, setCommentsData] = useState([]);
  const [isLoading, setIsLoading] = useState(true);

  //* I N I T

  useEffect(() => {
    // console.log(ssrData.items)
    setCommentsData(ssrData.items)
     setTimeout(() => {
       setIsLoading(false);  
     }, 600);
    
  }, []);

  
  //* H A N D L E   N E W   C O M M E N T

  const handleNewComment = async () => {
    var tmpArr = []
    var id=0

    axiosConn.post('/api/comments/add', { UserId: Number(token.sub), Content: newComment })
    .then((data) => {
   //   console.log(data); // JSON data parsed by `data.json()` call
      // setOpen(false);
      commentsData.forEach(elem => {
        tmpArr.push(elem)
        id++
      })
    
      tmpArr.push({Id: 2, UserId: 22, Name: 'John Smith', Content: newComment})
      setCommentsData(tmpArr)
      setNewComment('')  
    
    });            
  }

  //* H A N D L E   D E L E T E   C O M M E N T

  const handleDeleteComment = (ID) => {
    console.log('handleDeleteComment', ID)
    axiosConn.post('/api/comments/delete', { CommentId: ID })
    .then((data) => {    
      setCommentsData(data.data.items)      
    });      
  }
    

  //* H A N D L E   S A V E   E D I T

  const handleSaveReplyEdit = (ID, commentStr) => {    
    setCommentsData(tmpArr)    
  }


  //* R E N D E R

  return (
    <>
      <Head>
        <meta name="viewport" content="initial-scale=1, width=device-width" />
      </Head>
      
      <Container maxWidth="lg">
        
        <Typography variant="h6">
            Comments Section
        </Typography>        

        <Grid container spacing={1}>
          {isLoading ?
            <>
              {[1,2,3,4].map(item => (
                <Grid item xs={12} key={item}>
                  <CommentSkeleton key={item}/> 
                </Grid>
              ))}
            </>
          :
          <>
            {commentsData.map((item, index) => (
                <Grid item xs={12} key={index}>                                   
                  <CommentCard currentUserId={token.sub}
                               data={item} 
                               key={index}
                               handleDeleteComment={handleDeleteComment} 
                               handleSaveReplyEdit={handleSaveReplyEdit}
                               //handleDeleteReply={handleDeleteReply}
                               />                
                 
                </Grid>
              ))}
            
              <Grid item xs={12}>
                <NewComment handleNewComment={handleNewComment}                               
                            userName={'John Smith'}
                            setNewComment={setNewComment} 
                            newComment={newComment}/>                
              </Grid>
            </>
          }
        </Grid>
            
      </Container>
    </>
  );
}


export async function getServerSideProps(context) {
  const { req, res } = context;
  const secret = process.env.NEXTAUTH_SECRET
  const token = await getToken({ req, secret })   
  
  const result = await fetch('http://localhost:3000/api/comments/list')
  const ssrData = await result.json()

  return {
    props: {
      ssrData: ssrData,
      token: token
    },
  }
}

Server Side Data Fetching & User Info

The code below fetches the user token and comments data.

User and Token Info

Below is the authenticated user token.

{
    "name": "Jennifer Smith",
    "email": "jennifers@example.com",
    "picture": "https://randomuser.me/api/portraits/women/72.jpg",
    "sub": "1",
    "userRole": "admin",
    "iat": 1674165138,
    "exp": 1676757138,
    "jti": "483afcb9-78bf-4205-ac59-1f95d8f23f69"
}

Code

The code for fetching the user token and comments data.

export async function getServerSideProps(context) {
  const { req, res } = context;
  const secret = process.env.NEXTAUTH_SECRET
  const token = await getToken({ req, secret })   
  
  const result = await fetch('http://localhost:3000/api/comments/list')
  const ssrData = await result.json()

  return {
    props: {
      ssrData: ssrData,
      token: token
    },
  }
...

CommentRepliesParentLayout.js

//* H A N D L E   N E W   C O M M E N T

  const handleNewComment = async () => {
    var tmpArr = []
    var id=0

    axiosConn.post('/api/comments/add', { UserId: Number(token.sub), Content: newComment })
    .then((data) => {
      commentsData.forEach(elem => {
        tmpArr.push(elem)
        id++
      })
    
      tmpArr.push({Id: 2, UserId: 22, Name: 'John Smith', Content: newComment})
      setCommentsData(tmpArr)
      setNewComment('')  
    
    });            
  }

  //* H A N D L E   D E L E T E   C O M M E N T

  const handleDeleteComment = (ID) => {
    console.log('handleDeleteComment', ID)
    axiosConn.post('/api/comments/delete', { CommentId: ID })
    .then((data) => {    
      setCommentsData(data.data.items)      
    });      
  }


  //* H A N D L E   S A V E   E D I T

  const handleSaveReplyEdit = (ID, commentStr) => {    
    setCommentsData(tmpArr)    
  }
  //* H A N D L E   S A V E   E D I T

  const handleSaveReplyEdit = (ID, commentStr) => {    
    setCommentsData(tmpArr)    
  }

CommentRepliesParentLayout.js

import React, {useEffect, useState} from "react";
import Head from 'next/head';
import { styled } from '@mui/material/styles';
import { Container, Typography } from '@mui/material';
import CommentCard from '@/components/comments/CommentCard';
import NewComment from '@/components/comments/NewComment';
import CommentSkeleton from '@/components/comments/CommentSkeleton';
import { Grid, Paper } from '@mui/material';
import axiosConn from '@/src/axiosConn'
import { getToken } from "next-auth/jwt";


export default function Comments({ token, ssrData }) {
  
  const [newComment, setNewComment] = useState('');
  const [commentsData, setCommentsData] = useState([]);
  const [isLoading, setIsLoading] = useState(true);

  //* I N I T

  useEffect(() => {
    // console.log(ssrData.items)
    setCommentsData(ssrData.items)
     setTimeout(() => {
       setIsLoading(false);  
     }, 600);
    
  }, []);

  
  //* H A N D L E   N E W   C O M M E N T

  const handleNewComment = async () => {
    var tmpArr = []
    var id=0

    axiosConn.post('/api/comments/add', { UserId: Number(token.sub), Content: newComment })
    .then((data) => {
   //   console.log(data); // JSON data parsed by `data.json()` call
      // setOpen(false);
      commentsData.forEach(elem => {
        tmpArr.push(elem)
        id++
      })
    
      tmpArr.push({Id: 2, UserId: 22, Name: 'John Smith', Content: newComment})
      setCommentsData(tmpArr)
      setNewComment('')  
    
    });            
  }

  //* H A N D L E   D E L E T E   C O M M E N T

  const handleDeleteComment = (ID) => {
    console.log('handleDeleteComment', ID)
    axiosConn.post('/api/comments/delete', { CommentId: ID })
    .then((data) => {    
      setCommentsData(data.data.items)      
    });      
  }
    

  //* H A N D L E   S A V E   E D I T

  const handleSaveReplyEdit = (ID, commentStr) => {    
    setCommentsData(tmpArr)    
  }


  //* R E N D E R
  return (
    <>
      <Head>
        <meta name="viewport" content="initial-scale=1, width=device-width" />
      </Head>
      
      <Container maxWidth="lg">
        
        <Typography variant="h6">
            Comments Section
        </Typography>        

        <Grid container spacing={1}>
          {isLoading ?
            <>
              {[1,2,3,4].map(item => (
                <Grid item xs={12} key={item}>
                  <CommentSkeleton key={item}/> 
                </Grid>
              ))}
            </>
          :
          <>
            {commentsData.map((item, index) => (
                <Grid item xs={12} key={index}>                                   
                  <CommentCard currentUserId={token.sub}
                               data={item} 
                               key={index}
                               handleDeleteComment={handleDeleteComment} 
                               handleSaveReplyEdit={handleSaveReplyEdit}
                               //handleDeleteReply={handleDeleteReply}
                               />                
                 
                </Grid>
              ))}
            
              <Grid item xs={12}>
                <NewComment handleNewComment={handleNewComment}                               
                            userName={'John Smith'}
                            setNewComment={setNewComment} 
                            newComment={newComment}/>                
              </Grid>
            </>
          }
        </Grid>
            
      </Container>
    </>
  );
}


export async function getServerSideProps(context) {
  const { req, res } = context;
  const secret = process.env.NEXTAUTH_SECRET
  const token = await getToken({ req, secret })   
  
  const result = await fetch('http://localhost:3000/api/comments/list')
  const ssrData = await result.json()

  return {
    props: {
      ssrData: ssrData,
      token: token
    },
  }
}

CommentReplies.js

CommentReplyBox.js

CommentParentLayout.js

Conclusion

 This NextJS CRUD application is a good candidate for a state management library like React-Query. The example application from GitHub has React-Query installed and configured, just in case you want to try it yourself. Otherwise, I will write another article on how to set up and use React-Query for this application.

Another challenge is to add replies to any comment. Adding Replies is a bit more complex since you must keep track of how many there are and collapse/expand the child records for each comment.

Photo by <a href="https://unsplash.com/@justinlim?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Justin Lim</a> on <a href="https://unsplash.com/photos/tloFnD-7EpI?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a>