├── .gitignore
├── api
├── controllers
│ ├── auth.controller.js
│ ├── comment.controller.js
│ ├── post.controller.js
│ └── user.controller.js
├── index.js
├── models
│ ├── comment.model.js
│ ├── post.model.js
│ └── user.model.js
├── routes
│ ├── auth.route.js
│ ├── comment.route.js
│ ├── post.route.js
│ └── user.route.js
└── utils
│ ├── error.js
│ └── verifyUser.js
├── client
├── .eslintrc.cjs
├── README.md
├── index.html
├── package-lock.json
├── package.json
├── postcss.config.js
├── src
│ ├── App.jsx
│ ├── components
│ │ ├── CallToAction.jsx
│ │ ├── Comment.jsx
│ │ ├── CommentSection.jsx
│ │ ├── DashComments.jsx
│ │ ├── DashPosts.jsx
│ │ ├── DashProfile.jsx
│ │ ├── DashSidebar.jsx
│ │ ├── DashUsers.jsx
│ │ ├── DashboardComp.jsx
│ │ ├── Footer.jsx
│ │ ├── Header.jsx
│ │ ├── OAuth.jsx
│ │ ├── OnlyAdminPrivateRoute.jsx
│ │ ├── PostCard.jsx
│ │ ├── PrivateRoute.jsx
│ │ ├── ScrollToTop.jsx
│ │ └── ThemeProvider.jsx
│ ├── firebase.js
│ ├── index.css
│ ├── main.jsx
│ ├── pages
│ │ ├── About.jsx
│ │ ├── CreatePost.jsx
│ │ ├── Dashboard.jsx
│ │ ├── Home.jsx
│ │ ├── PostPage.jsx
│ │ ├── Projects.jsx
│ │ ├── Search.jsx
│ │ ├── SignIn.jsx
│ │ ├── SignUp.jsx
│ │ └── UpdatePost.jsx
│ └── redux
│ │ ├── store.js
│ │ ├── theme
│ │ └── themeSlice.js
│ │ └── user
│ │ └── userSlice.js
├── tailwind.config.js
└── vite.config.js
├── package-lock.json
└── package.json
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 | .env
26 |
--------------------------------------------------------------------------------
/api/controllers/auth.controller.js:
--------------------------------------------------------------------------------
1 | import User from '../models/user.model.js';
2 | import bcryptjs from 'bcryptjs';
3 | import { errorHandler } from '../utils/error.js';
4 | import jwt from 'jsonwebtoken';
5 |
6 | export const signup = async (req, res, next) => {
7 | const { username, email, password } = req.body;
8 |
9 | if (
10 | !username ||
11 | !email ||
12 | !password ||
13 | username === '' ||
14 | email === '' ||
15 | password === ''
16 | ) {
17 | next(errorHandler(400, 'All fields are required'));
18 | }
19 |
20 | const hashedPassword = bcryptjs.hashSync(password, 10);
21 |
22 | const newUser = new User({
23 | username,
24 | email,
25 | password: hashedPassword,
26 | });
27 |
28 | try {
29 | await newUser.save();
30 | res.json('Signup successful');
31 | } catch (error) {
32 | next(error);
33 | }
34 | };
35 |
36 | export const signin = async (req, res, next) => {
37 | const { email, password } = req.body;
38 |
39 | if (!email || !password || email === '' || password === '') {
40 | next(errorHandler(400, 'All fields are required'));
41 | }
42 |
43 | try {
44 | const validUser = await User.findOne({ email });
45 | if (!validUser) {
46 | return next(errorHandler(404, 'User not found'));
47 | }
48 | const validPassword = bcryptjs.compareSync(password, validUser.password);
49 | if (!validPassword) {
50 | return next(errorHandler(400, 'Invalid password'));
51 | }
52 | const token = jwt.sign(
53 | { id: validUser._id, isAdmin: validUser.isAdmin },
54 | process.env.JWT_SECRET
55 | );
56 |
57 | const { password: pass, ...rest } = validUser._doc;
58 |
59 | res
60 | .status(200)
61 | .cookie('access_token', token, {
62 | httpOnly: true,
63 | })
64 | .json(rest);
65 | } catch (error) {
66 | next(error);
67 | }
68 | };
69 |
70 | export const google = async (req, res, next) => {
71 | const { email, name, googlePhotoUrl } = req.body;
72 | try {
73 | const user = await User.findOne({ email });
74 | if (user) {
75 | const token = jwt.sign(
76 | { id: user._id, isAdmin: user.isAdmin },
77 | process.env.JWT_SECRET
78 | );
79 | const { password, ...rest } = user._doc;
80 | res
81 | .status(200)
82 | .cookie('access_token', token, {
83 | httpOnly: true,
84 | })
85 | .json(rest);
86 | } else {
87 | const generatedPassword =
88 | Math.random().toString(36).slice(-8) +
89 | Math.random().toString(36).slice(-8);
90 | const hashedPassword = bcryptjs.hashSync(generatedPassword, 10);
91 | const newUser = new User({
92 | username:
93 | name.toLowerCase().split(' ').join('') +
94 | Math.random().toString(9).slice(-4),
95 | email,
96 | password: hashedPassword,
97 | profilePicture: googlePhotoUrl,
98 | });
99 | await newUser.save();
100 | const token = jwt.sign(
101 | { id: newUser._id, isAdmin: newUser.isAdmin },
102 | process.env.JWT_SECRET
103 | );
104 | const { password, ...rest } = newUser._doc;
105 | res
106 | .status(200)
107 | .cookie('access_token', token, {
108 | httpOnly: true,
109 | })
110 | .json(rest);
111 | }
112 | } catch (error) {
113 | next(error);
114 | }
115 | };
116 |
--------------------------------------------------------------------------------
/api/controllers/comment.controller.js:
--------------------------------------------------------------------------------
1 | import Comment from '../models/comment.model.js';
2 |
3 | export const createComment = async (req, res, next) => {
4 | try {
5 | const { content, postId, userId } = req.body;
6 |
7 | if (userId !== req.user.id) {
8 | return next(
9 | errorHandler(403, 'You are not allowed to create this comment')
10 | );
11 | }
12 |
13 | const newComment = new Comment({
14 | content,
15 | postId,
16 | userId,
17 | });
18 | await newComment.save();
19 |
20 | res.status(200).json(newComment);
21 | } catch (error) {
22 | next(error);
23 | }
24 | };
25 |
26 | export const getPostComments = async (req, res, next) => {
27 | try {
28 | const comments = await Comment.find({ postId: req.params.postId }).sort({
29 | createdAt: -1,
30 | });
31 | res.status(200).json(comments);
32 | } catch (error) {
33 | next(error);
34 | }
35 | };
36 |
37 | export const likeComment = async (req, res, next) => {
38 | try {
39 | const comment = await Comment.findById(req.params.commentId);
40 | if (!comment) {
41 | return next(errorHandler(404, 'Comment not found'));
42 | }
43 | const userIndex = comment.likes.indexOf(req.user.id);
44 | if (userIndex === -1) {
45 | comment.numberOfLikes += 1;
46 | comment.likes.push(req.user.id);
47 | } else {
48 | comment.numberOfLikes -= 1;
49 | comment.likes.splice(userIndex, 1);
50 | }
51 | await comment.save();
52 | res.status(200).json(comment);
53 | } catch (error) {
54 | next(error);
55 | }
56 | };
57 |
58 | export const editComment = async (req, res, next) => {
59 | try {
60 | const comment = await Comment.findById(req.params.commentId);
61 | if (!comment) {
62 | return next(errorHandler(404, 'Comment not found'));
63 | }
64 | if (comment.userId !== req.user.id && !req.user.isAdmin) {
65 | return next(
66 | errorHandler(403, 'You are not allowed to edit this comment')
67 | );
68 | }
69 |
70 | const editedComment = await Comment.findByIdAndUpdate(
71 | req.params.commentId,
72 | {
73 | content: req.body.content,
74 | },
75 | { new: true }
76 | );
77 | res.status(200).json(editedComment);
78 | } catch (error) {
79 | next(error);
80 | }
81 | };
82 |
83 | export const deleteComment = async (req, res, next) => {
84 | try {
85 | const comment = await Comment.findById(req.params.commentId);
86 | if (!comment) {
87 | return next(errorHandler(404, 'Comment not found'));
88 | }
89 | if (comment.userId !== req.user.id && !req.user.isAdmin) {
90 | return next(
91 | errorHandler(403, 'You are not allowed to delete this comment')
92 | );
93 | }
94 | await Comment.findByIdAndDelete(req.params.commentId);
95 | res.status(200).json('Comment has been deleted');
96 | } catch (error) {
97 | next(error);
98 | }
99 | };
100 |
101 | export const getcomments = async (req, res, next) => {
102 | if (!req.user.isAdmin)
103 | return next(errorHandler(403, 'You are not allowed to get all comments'));
104 | try {
105 | const startIndex = parseInt(req.query.startIndex) || 0;
106 | const limit = parseInt(req.query.limit) || 9;
107 | const sortDirection = req.query.sort === 'desc' ? -1 : 1;
108 | const comments = await Comment.find()
109 | .sort({ createdAt: sortDirection })
110 | .skip(startIndex)
111 | .limit(limit);
112 | const totalComments = await Comment.countDocuments();
113 | const now = new Date();
114 | const oneMonthAgo = new Date(
115 | now.getFullYear(),
116 | now.getMonth() - 1,
117 | now.getDate()
118 | );
119 | const lastMonthComments = await Comment.countDocuments({
120 | createdAt: { $gte: oneMonthAgo },
121 | });
122 | res.status(200).json({ comments, totalComments, lastMonthComments });
123 | } catch (error) {
124 | next(error);
125 | }
126 | };
127 |
--------------------------------------------------------------------------------
/api/controllers/post.controller.js:
--------------------------------------------------------------------------------
1 | import Post from '../models/post.model.js';
2 | import { errorHandler } from '../utils/error.js';
3 |
4 | export const create = async (req, res, next) => {
5 | if (!req.user.isAdmin) {
6 | return next(errorHandler(403, 'You are not allowed to create a post'));
7 | }
8 | if (!req.body.title || !req.body.content) {
9 | return next(errorHandler(400, 'Please provide all required fields'));
10 | }
11 | const slug = req.body.title
12 | .split(' ')
13 | .join('-')
14 | .toLowerCase()
15 | .replace(/[^a-zA-Z0-9-]/g, '');
16 | const newPost = new Post({
17 | ...req.body,
18 | slug,
19 | userId: req.user.id,
20 | });
21 | try {
22 | const savedPost = await newPost.save();
23 | res.status(201).json(savedPost);
24 | } catch (error) {
25 | next(error);
26 | }
27 | };
28 |
29 | export const getposts = async (req, res, next) => {
30 | try {
31 | const startIndex = parseInt(req.query.startIndex) || 0;
32 | const limit = parseInt(req.query.limit) || 9;
33 | const sortDirection = req.query.order === 'asc' ? 1 : -1;
34 | const posts = await Post.find({
35 | ...(req.query.userId && { userId: req.query.userId }),
36 | ...(req.query.category && { category: req.query.category }),
37 | ...(req.query.slug && { slug: req.query.slug }),
38 | ...(req.query.postId && { _id: req.query.postId }),
39 | ...(req.query.searchTerm && {
40 | $or: [
41 | { title: { $regex: req.query.searchTerm, $options: 'i' } },
42 | { content: { $regex: req.query.searchTerm, $options: 'i' } },
43 | ],
44 | }),
45 | })
46 | .sort({ updatedAt: sortDirection })
47 | .skip(startIndex)
48 | .limit(limit);
49 |
50 | const totalPosts = await Post.countDocuments();
51 |
52 | const now = new Date();
53 |
54 | const oneMonthAgo = new Date(
55 | now.getFullYear(),
56 | now.getMonth() - 1,
57 | now.getDate()
58 | );
59 |
60 | const lastMonthPosts = await Post.countDocuments({
61 | createdAt: { $gte: oneMonthAgo },
62 | });
63 |
64 | res.status(200).json({
65 | posts,
66 | totalPosts,
67 | lastMonthPosts,
68 | });
69 | } catch (error) {
70 | next(error);
71 | }
72 | };
73 |
74 | export const deletepost = async (req, res, next) => {
75 | if (!req.user.isAdmin || req.user.id !== req.params.userId) {
76 | return next(errorHandler(403, 'You are not allowed to delete this post'));
77 | }
78 | try {
79 | await Post.findByIdAndDelete(req.params.postId);
80 | res.status(200).json('The post has been deleted');
81 | } catch (error) {
82 | next(error);
83 | }
84 | };
85 |
86 | export const updatepost = async (req, res, next) => {
87 | if (!req.user.isAdmin || req.user.id !== req.params.userId) {
88 | return next(errorHandler(403, 'You are not allowed to update this post'));
89 | }
90 | try {
91 | const updatedPost = await Post.findByIdAndUpdate(
92 | req.params.postId,
93 | {
94 | $set: {
95 | title: req.body.title,
96 | content: req.body.content,
97 | category: req.body.category,
98 | image: req.body.image,
99 | },
100 | },
101 | { new: true }
102 | );
103 | res.status(200).json(updatedPost);
104 | } catch (error) {
105 | next(error);
106 | }
107 | };
108 |
--------------------------------------------------------------------------------
/api/controllers/user.controller.js:
--------------------------------------------------------------------------------
1 | import bcryptjs from 'bcryptjs';
2 | import { errorHandler } from '../utils/error.js';
3 | import User from '../models/user.model.js';
4 |
5 | export const test = (req, res) => {
6 | res.json({ message: 'API is working!' });
7 | };
8 |
9 | export const updateUser = async (req, res, next) => {
10 | if (req.user.id !== req.params.userId) {
11 | return next(errorHandler(403, 'You are not allowed to update this user'));
12 | }
13 | if (req.body.password) {
14 | if (req.body.password.length < 6) {
15 | return next(errorHandler(400, 'Password must be at least 6 characters'));
16 | }
17 | req.body.password = bcryptjs.hashSync(req.body.password, 10);
18 | }
19 | if (req.body.username) {
20 | if (req.body.username.length < 7 || req.body.username.length > 20) {
21 | return next(
22 | errorHandler(400, 'Username must be between 7 and 20 characters')
23 | );
24 | }
25 | if (req.body.username.includes(' ')) {
26 | return next(errorHandler(400, 'Username cannot contain spaces'));
27 | }
28 | if (req.body.username !== req.body.username.toLowerCase()) {
29 | return next(errorHandler(400, 'Username must be lowercase'));
30 | }
31 | if (!req.body.username.match(/^[a-zA-Z0-9]+$/)) {
32 | return next(
33 | errorHandler(400, 'Username can only contain letters and numbers')
34 | );
35 | }
36 | }
37 | try {
38 | const updatedUser = await User.findByIdAndUpdate(
39 | req.params.userId,
40 | {
41 | $set: {
42 | username: req.body.username,
43 | email: req.body.email,
44 | profilePicture: req.body.profilePicture,
45 | password: req.body.password,
46 | },
47 | },
48 | { new: true }
49 | );
50 | const { password, ...rest } = updatedUser._doc;
51 | res.status(200).json(rest);
52 | } catch (error) {
53 | next(error);
54 | }
55 | };
56 |
57 | export const deleteUser = async (req, res, next) => {
58 | if (!req.user.isAdmin && req.user.id !== req.params.userId) {
59 | return next(errorHandler(403, 'You are not allowed to delete this user'));
60 | }
61 | try {
62 | await User.findByIdAndDelete(req.params.userId);
63 | res.status(200).json('User has been deleted');
64 | } catch (error) {
65 | next(error);
66 | }
67 | };
68 |
69 | export const signout = (req, res, next) => {
70 | try {
71 | res
72 | .clearCookie('access_token')
73 | .status(200)
74 | .json('User has been signed out');
75 | } catch (error) {
76 | next(error);
77 | }
78 | };
79 |
80 | export const getUsers = async (req, res, next) => {
81 | if (!req.user.isAdmin) {
82 | return next(errorHandler(403, 'You are not allowed to see all users'));
83 | }
84 | try {
85 | const startIndex = parseInt(req.query.startIndex) || 0;
86 | const limit = parseInt(req.query.limit) || 9;
87 | const sortDirection = req.query.sort === 'asc' ? 1 : -1;
88 |
89 | const users = await User.find()
90 | .sort({ createdAt: sortDirection })
91 | .skip(startIndex)
92 | .limit(limit);
93 |
94 | const usersWithoutPassword = users.map((user) => {
95 | const { password, ...rest } = user._doc;
96 | return rest;
97 | });
98 |
99 | const totalUsers = await User.countDocuments();
100 |
101 | const now = new Date();
102 |
103 | const oneMonthAgo = new Date(
104 | now.getFullYear(),
105 | now.getMonth() - 1,
106 | now.getDate()
107 | );
108 | const lastMonthUsers = await User.countDocuments({
109 | createdAt: { $gte: oneMonthAgo },
110 | });
111 |
112 | res.status(200).json({
113 | users: usersWithoutPassword,
114 | totalUsers,
115 | lastMonthUsers,
116 | });
117 | } catch (error) {
118 | next(error);
119 | }
120 | };
121 |
122 | export const getUser = async (req, res, next) => {
123 | try {
124 | const user = await User.findById(req.params.userId);
125 | if (!user) {
126 | return next(errorHandler(404, 'User not found'));
127 | }
128 | const { password, ...rest } = user._doc;
129 | res.status(200).json(rest);
130 | } catch (error) {
131 | next(error);
132 | }
133 | };
134 |
--------------------------------------------------------------------------------
/api/index.js:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import mongoose from 'mongoose';
3 | import dotenv from 'dotenv';
4 | import userRoutes from './routes/user.route.js';
5 | import authRoutes from './routes/auth.route.js';
6 | import postRoutes from './routes/post.route.js';
7 | import commentRoutes from './routes/comment.route.js';
8 | import cookieParser from 'cookie-parser';
9 | import path from 'path';
10 |
11 | dotenv.config();
12 |
13 | mongoose
14 | .connect(process.env.MONGO)
15 | .then(() => {
16 | console.log('MongoDb is connected');
17 | })
18 | .catch((err) => {
19 | console.log(err);
20 | });
21 |
22 | const __dirname = path.resolve();
23 |
24 | const app = express();
25 |
26 | app.use(express.json());
27 | app.use(cookieParser());
28 |
29 | app.listen(3000, () => {
30 | console.log('Server is running on port 3000!');
31 | });
32 |
33 | app.use('/api/user', userRoutes);
34 | app.use('/api/auth', authRoutes);
35 | app.use('/api/post', postRoutes);
36 | app.use('/api/comment', commentRoutes);
37 |
38 | app.use(express.static(path.join(__dirname, '/client/dist')));
39 |
40 | app.get('*', (req, res) => {
41 | res.sendFile(path.join(__dirname, 'client', 'dist', 'index.html'));
42 | });
43 |
44 | app.use((err, req, res, next) => {
45 | const statusCode = err.statusCode || 500;
46 | const message = err.message || 'Internal Server Error';
47 | res.status(statusCode).json({
48 | success: false,
49 | statusCode,
50 | message,
51 | });
52 | });
53 |
--------------------------------------------------------------------------------
/api/models/comment.model.js:
--------------------------------------------------------------------------------
1 | import mongoose from 'mongoose';
2 |
3 | const commentSchema = new mongoose.Schema(
4 | {
5 | content: {
6 | type: String,
7 | required: true,
8 | },
9 | postId: {
10 | type: String,
11 | required: true,
12 | },
13 | userId: {
14 | type: String,
15 | required: true,
16 | },
17 | likes: {
18 | type: Array,
19 | default: [],
20 | },
21 | numberOfLikes: {
22 | type: Number,
23 | default: 0,
24 | },
25 | },
26 | { timestamps: true }
27 | );
28 |
29 | const Comment = mongoose.model('Comment', commentSchema);
30 |
31 | export default Comment;
32 |
--------------------------------------------------------------------------------
/api/models/post.model.js:
--------------------------------------------------------------------------------
1 | import mongoose from 'mongoose';
2 |
3 | const postSchema = new mongoose.Schema(
4 | {
5 | userId: {
6 | type: String,
7 | required: true,
8 | },
9 | content: {
10 | type: String,
11 | required: true,
12 | },
13 | title: {
14 | type: String,
15 | required: true,
16 | unique: true,
17 | },
18 | image: {
19 | type: String,
20 | default:
21 | 'https://www.hostinger.com/tutorials/wp-content/uploads/sites/2/2021/09/how-to-write-a-blog-post.png',
22 | },
23 | category: {
24 | type: String,
25 | default: 'uncategorized',
26 | },
27 | slug: {
28 | type: String,
29 | required: true,
30 | unique: true,
31 | },
32 | },
33 | { timestamps: true }
34 | );
35 |
36 | const Post = mongoose.model('Post', postSchema);
37 |
38 | export default Post;
39 |
--------------------------------------------------------------------------------
/api/models/user.model.js:
--------------------------------------------------------------------------------
1 | import mongoose from 'mongoose';
2 |
3 | const userSchema = new mongoose.Schema(
4 | {
5 | username: {
6 | type: String,
7 | required: true,
8 | unique: true,
9 | },
10 | email: {
11 | type: String,
12 | required: true,
13 | unique: true,
14 | },
15 | password: {
16 | type: String,
17 | required: true,
18 | },
19 | profilePicture: {
20 | type: String,
21 | default:
22 | 'https://cdn.pixabay.com/photo/2015/10/05/22/37/blank-profile-picture-973460_960_720.png',
23 | },
24 | isAdmin: {
25 | type: Boolean,
26 | default: false,
27 | },
28 | },
29 | { timestamps: true }
30 | );
31 |
32 | const User = mongoose.model('User', userSchema);
33 |
34 | export default User;
35 |
--------------------------------------------------------------------------------
/api/routes/auth.route.js:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import { google, signin, signup } from '../controllers/auth.controller.js';
3 |
4 | const router = express.Router();
5 |
6 |
7 | router.post('/signup', signup);
8 | router.post('/signin', signin);
9 | router.post('/google', google)
10 |
11 | export default router;
--------------------------------------------------------------------------------
/api/routes/comment.route.js:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import { verifyToken } from '../utils/verifyUser.js';
3 | import {
4 | createComment,
5 | deleteComment,
6 | editComment,
7 | getPostComments,
8 | getcomments,
9 | likeComment,
10 | } from '../controllers/comment.controller.js';
11 |
12 | const router = express.Router();
13 |
14 | router.post('/create', verifyToken, createComment);
15 | router.get('/getPostComments/:postId', getPostComments);
16 | router.put('/likeComment/:commentId', verifyToken, likeComment);
17 | router.put('/editComment/:commentId', verifyToken, editComment);
18 | router.delete('/deleteComment/:commentId', verifyToken, deleteComment);
19 | router.get('/getcomments', verifyToken, getcomments);
20 |
21 | export default router;
22 |
--------------------------------------------------------------------------------
/api/routes/post.route.js:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import { verifyToken } from '../utils/verifyUser.js';
3 | import { create, deletepost, getposts, updatepost } from '../controllers/post.controller.js';
4 |
5 | const router = express.Router();
6 |
7 | router.post('/create', verifyToken, create)
8 | router.get('/getposts', getposts)
9 | router.delete('/deletepost/:postId/:userId', verifyToken, deletepost)
10 | router.put('/updatepost/:postId/:userId', verifyToken, updatepost)
11 |
12 |
13 | export default router;
--------------------------------------------------------------------------------
/api/routes/user.route.js:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import {
3 | deleteUser,
4 | getUser,
5 | getUsers,
6 | signout,
7 | test,
8 | updateUser,
9 | } from '../controllers/user.controller.js';
10 | import { verifyToken } from '../utils/verifyUser.js';
11 |
12 | const router = express.Router();
13 |
14 | router.get('/test', test);
15 | router.put('/update/:userId', verifyToken, updateUser);
16 | router.delete('/delete/:userId', verifyToken, deleteUser);
17 | router.post('/signout', signout);
18 | router.get('/getusers', verifyToken, getUsers);
19 | router.get('/:userId', getUser);
20 |
21 | export default router;
22 |
--------------------------------------------------------------------------------
/api/utils/error.js:
--------------------------------------------------------------------------------
1 | export const errorHandler = (statusCode, message) => {
2 | const error = new Error();
3 | error.statusCode = statusCode;
4 | error.message = message;
5 | return error;
6 | };
7 |
--------------------------------------------------------------------------------
/api/utils/verifyUser.js:
--------------------------------------------------------------------------------
1 | import jwt from 'jsonwebtoken';
2 | import { errorHandler } from './error.js';
3 | export const verifyToken = (req, res, next) => {
4 | const token = req.cookies.access_token;
5 | if (!token) {
6 | return next(errorHandler(401, 'Unauthorized'));
7 | }
8 | jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
9 | if (err) {
10 | return next(errorHandler(401, 'Unauthorized'));
11 | }
12 | req.user = user;
13 | next();
14 | });
15 | };
16 |
--------------------------------------------------------------------------------
/client/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: { browser: true, es2020: true },
4 | extends: [
5 | 'eslint:recommended',
6 | 'plugin:react/recommended',
7 | 'plugin:react/jsx-runtime',
8 | 'plugin:react-hooks/recommended',
9 | ],
10 | ignorePatterns: ['dist', '.eslintrc.cjs'],
11 | parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
12 | settings: { react: { version: '18.2' } },
13 | plugins: ['react-refresh'],
14 | rules: {
15 | 'react-refresh/only-export-components': [
16 | 'warn',
17 | { allowConstantExport: true },
18 | ],
19 | },
20 | }
21 |
--------------------------------------------------------------------------------
/client/README.md:
--------------------------------------------------------------------------------
1 | # React + Vite
2 |
3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4 |
5 | Currently, two official plugins are available:
6 |
7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
9 |
--------------------------------------------------------------------------------
/client/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | MERN Blog
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "client",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "vite build",
9 | "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
10 | "preview": "vite preview"
11 | },
12 | "dependencies": {
13 | "@reduxjs/toolkit": "^2.0.1",
14 | "caniuse-lite": "^1.0.30001707",
15 | "firebase": "^10.7.1",
16 | "flowbite-react": "^0.7.0",
17 | "moment": "^2.29.4",
18 | "react": "^18.2.0",
19 | "react-circular-progressbar": "^2.1.0",
20 | "react-dom": "^18.2.0",
21 | "react-icons": "^4.12.0",
22 | "react-quill": "^2.0.0",
23 | "react-redux": "^9.0.1",
24 | "react-router-dom": "^6.20.0",
25 | "redux-persist": "^6.0.0"
26 | },
27 | "devDependencies": {
28 | "@tailwindcss/line-clamp": "^0.4.4",
29 | "@types/react": "^18.2.37",
30 | "@types/react-dom": "^18.2.15",
31 | "@vitejs/plugin-react-swc": "^3.5.0",
32 | "autoprefixer": "^10.4.16",
33 | "eslint": "^8.53.0",
34 | "eslint-plugin-react": "^7.33.2",
35 | "eslint-plugin-react-hooks": "^4.6.0",
36 | "eslint-plugin-react-refresh": "^0.4.4",
37 | "postcss": "^8.4.31",
38 | "tailwind-scrollbar": "^3.0.5",
39 | "tailwindcss": "^3.3.5",
40 | "vite": "^5.0.0"
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/client/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/client/src/App.jsx:
--------------------------------------------------------------------------------
1 | import { BrowserRouter, Routes, Route } from 'react-router-dom';
2 | import Home from './pages/Home';
3 | import About from './pages/About';
4 | import SignIn from './pages/SignIn';
5 | import Dashboard from './pages/Dashboard';
6 | import Projects from './pages/Projects';
7 | import SignUp from './pages/SignUp';
8 | import Header from './components/Header';
9 | import Footer from './components/Footer';
10 | import PrivateRoute from './components/PrivateRoute';
11 | import OnlyAdminPrivateRoute from './components/OnlyAdminPrivateRoute';
12 | import CreatePost from './pages/CreatePost';
13 | import UpdatePost from './pages/UpdatePost';
14 | import PostPage from './pages/PostPage';
15 | import ScrollToTop from './components/ScrollToTop';
16 | import Search from './pages/Search';
17 |
18 | export default function App() {
19 | return (
20 |
21 |
22 |
23 |
24 | } />
25 | } />
26 | } />
27 | } />
28 | } />
29 | }>
30 | } />
31 |
32 | }>
33 | } />
34 | } />
35 |
36 |
37 | } />
38 | } />
39 |
40 |
41 |
42 | );
43 | }
44 |
--------------------------------------------------------------------------------
/client/src/components/CallToAction.jsx:
--------------------------------------------------------------------------------
1 | import { Button } from 'flowbite-react';
2 |
3 | export default function CallToAction() {
4 | return (
5 |
6 |
7 |
8 | Want to learn HTML, CSS and JavaScript by building fun and engaging
9 | projects?
10 |
11 |
12 | Check our 100 js projects website and start building your own projects
13 |
14 |
19 |
23 | 100 JS Projects Website
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | );
32 | }
33 |
--------------------------------------------------------------------------------
/client/src/components/Comment.jsx:
--------------------------------------------------------------------------------
1 | import moment from 'moment';
2 | import { useEffect, useState } from 'react';
3 | import { FaThumbsUp } from 'react-icons/fa';
4 | import { useSelector } from 'react-redux';
5 | import { Button, Textarea } from 'flowbite-react';
6 | import { set } from 'mongoose';
7 |
8 | export default function Comment({ comment, onLike, onEdit, onDelete }) {
9 | const [user, setUser] = useState({});
10 | const [isEditing, setIsEditing] = useState(false);
11 | const [editedContent, setEditedContent] = useState(comment.content);
12 | const { currentUser } = useSelector((state) => state.user);
13 | useEffect(() => {
14 | const getUser = async () => {
15 | try {
16 | const res = await fetch(`/api/user/${comment.userId}`);
17 | const data = await res.json();
18 | if (res.ok) {
19 | setUser(data);
20 | }
21 | } catch (error) {
22 | console.log(error.message);
23 | }
24 | };
25 | getUser();
26 | }, [comment]);
27 |
28 | const handleEdit = () => {
29 | setIsEditing(true);
30 | setEditedContent(comment.content);
31 | };
32 |
33 | const handleSave = async () => {
34 | try {
35 | const res = await fetch(`/api/comment/editComment/${comment._id}`, {
36 | method: 'PUT',
37 | headers: {
38 | 'Content-Type': 'application/json',
39 | },
40 | body: JSON.stringify({
41 | content: editedContent,
42 | }),
43 | });
44 | if (res.ok) {
45 | setIsEditing(false);
46 | onEdit(comment, editedContent);
47 | }
48 | } catch (error) {
49 | console.log(error.message);
50 | }
51 | };
52 | return (
53 |
54 |
55 |
60 |
61 |
62 |
63 |
64 | {user ? `@${user.username}` : 'anonymous user'}
65 |
66 |
67 | {moment(comment.createdAt).fromNow()}
68 |
69 |
70 | {isEditing ? (
71 | <>
72 |
141 |
142 | );
143 | }
144 |
--------------------------------------------------------------------------------
/client/src/components/CommentSection.jsx:
--------------------------------------------------------------------------------
1 | import { Alert, Button, Modal, TextInput, Textarea } from 'flowbite-react';
2 | import { useEffect, useState } from 'react';
3 | import { useSelector } from 'react-redux';
4 | import { Link, useNavigate } from 'react-router-dom';
5 | import Comment from './Comment';
6 | import { HiOutlineExclamationCircle } from 'react-icons/hi';
7 |
8 | export default function CommentSection({ postId }) {
9 | const { currentUser } = useSelector((state) => state.user);
10 | const [comment, setComment] = useState('');
11 | const [commentError, setCommentError] = useState(null);
12 | const [comments, setComments] = useState([]);
13 | const [showModal, setShowModal] = useState(false);
14 | const [commentToDelete, setCommentToDelete] = useState(null);
15 | const navigate = useNavigate();
16 | const handleSubmit = async (e) => {
17 | e.preventDefault();
18 | if (comment.length > 200) {
19 | return;
20 | }
21 | try {
22 | const res = await fetch('/api/comment/create', {
23 | method: 'POST',
24 | headers: {
25 | 'Content-Type': 'application/json',
26 | },
27 | body: JSON.stringify({
28 | content: comment,
29 | postId,
30 | userId: currentUser._id,
31 | }),
32 | });
33 | const data = await res.json();
34 | if (res.ok) {
35 | setComment('');
36 | setCommentError(null);
37 | setComments([data, ...comments]);
38 | }
39 | } catch (error) {
40 | setCommentError(error.message);
41 | }
42 | };
43 |
44 | useEffect(() => {
45 | const getComments = async () => {
46 | try {
47 | const res = await fetch(`/api/comment/getPostComments/${postId}`);
48 | if (res.ok) {
49 | const data = await res.json();
50 | setComments(data);
51 | }
52 | } catch (error) {
53 | console.log(error.message);
54 | }
55 | };
56 | getComments();
57 | }, [postId]);
58 |
59 | const handleLike = async (commentId) => {
60 | try {
61 | if (!currentUser) {
62 | navigate('/sign-in');
63 | return;
64 | }
65 | const res = await fetch(`/api/comment/likeComment/${commentId}`, {
66 | method: 'PUT',
67 | });
68 | if (res.ok) {
69 | const data = await res.json();
70 | setComments(
71 | comments.map((comment) =>
72 | comment._id === commentId
73 | ? {
74 | ...comment,
75 | likes: data.likes,
76 | numberOfLikes: data.likes.length,
77 | }
78 | : comment
79 | )
80 | );
81 | }
82 | } catch (error) {
83 | console.log(error.message);
84 | }
85 | };
86 |
87 | const handleEdit = async (comment, editedContent) => {
88 | setComments(
89 | comments.map((c) =>
90 | c._id === comment._id ? { ...c, content: editedContent } : c
91 | )
92 | );
93 | };
94 |
95 | const handleDelete = async (commentId) => {
96 | setShowModal(false);
97 | try {
98 | if (!currentUser) {
99 | navigate('/sign-in');
100 | return;
101 | }
102 | const res = await fetch(`/api/comment/deleteComment/${commentId}`, {
103 | method: 'DELETE',
104 | });
105 | if (res.ok) {
106 | const data = await res.json();
107 | setComments(comments.filter((comment) => comment._id !== commentId));
108 | }
109 | } catch (error) {
110 | console.log(error.message);
111 | }
112 | };
113 | return (
114 |
115 | {currentUser ? (
116 |
117 |
Signed in as:
118 |
123 |
127 | @{currentUser.username}
128 |
129 |
130 | ) : (
131 |
132 | You must be signed in to comment.
133 |
134 | Sign In
135 |
136 |
137 | )}
138 | {currentUser && (
139 |
164 | )}
165 | {comments.length === 0 ? (
166 |
No comments yet!
167 | ) : (
168 | <>
169 |
170 |
Comments
171 |
172 |
{comments.length}
173 |
174 |
175 | {comments.map((comment) => (
176 |
{
182 | setShowModal(true);
183 | setCommentToDelete(commentId);
184 | }}
185 | />
186 | ))}
187 | >
188 | )}
189 | setShowModal(false)}
192 | popup
193 | size='md'
194 | >
195 |
196 |
197 |
198 |
199 |
200 | Are you sure you want to delete this comment?
201 |
202 |
203 | handleDelete(commentToDelete)}
206 | >
207 | Yes, I'm sure
208 |
209 | setShowModal(false)}>
210 | No, cancel
211 |
212 |
213 |
214 |
215 |
216 |
217 | );
218 | }
219 |
--------------------------------------------------------------------------------
/client/src/components/DashComments.jsx:
--------------------------------------------------------------------------------
1 | import { Modal, Table, Button } from 'flowbite-react';
2 | import { useEffect, useState } from 'react';
3 | import { useSelector } from 'react-redux';
4 | import { HiOutlineExclamationCircle } from 'react-icons/hi';
5 | import { FaCheck, FaTimes } from 'react-icons/fa';
6 |
7 | export default function DashComments() {
8 | const { currentUser } = useSelector((state) => state.user);
9 | const [comments, setComments] = useState([]);
10 | const [showMore, setShowMore] = useState(true);
11 | const [showModal, setShowModal] = useState(false);
12 | const [commentIdToDelete, setCommentIdToDelete] = useState('');
13 | useEffect(() => {
14 | const fetchComments = async () => {
15 | try {
16 | const res = await fetch(`/api/comment/getcomments`);
17 | const data = await res.json();
18 | if (res.ok) {
19 | setComments(data.comments);
20 | if (data.comments.length < 9) {
21 | setShowMore(false);
22 | }
23 | }
24 | } catch (error) {
25 | console.log(error.message);
26 | }
27 | };
28 | if (currentUser.isAdmin) {
29 | fetchComments();
30 | }
31 | }, [currentUser._id]);
32 |
33 | const handleShowMore = async () => {
34 | const startIndex = comments.length;
35 | try {
36 | const res = await fetch(
37 | `/api/comment/getcomments?startIndex=${startIndex}`
38 | );
39 | const data = await res.json();
40 | if (res.ok) {
41 | setComments((prev) => [...prev, ...data.comments]);
42 | if (data.comments.length < 9) {
43 | setShowMore(false);
44 | }
45 | }
46 | } catch (error) {
47 | console.log(error.message);
48 | }
49 | };
50 |
51 | const handleDeleteComment = async () => {
52 | setShowModal(false);
53 | try {
54 | const res = await fetch(
55 | `/api/comment/deleteComment/${commentIdToDelete}`,
56 | {
57 | method: 'DELETE',
58 | }
59 | );
60 | const data = await res.json();
61 | if (res.ok) {
62 | setComments((prev) =>
63 | prev.filter((comment) => comment._id !== commentIdToDelete)
64 | );
65 | setShowModal(false);
66 | } else {
67 | console.log(data.message);
68 | }
69 | } catch (error) {
70 | console.log(error.message);
71 | }
72 | };
73 |
74 | return (
75 |
76 | {currentUser.isAdmin && comments.length > 0 ? (
77 | <>
78 |
79 |
80 | Date updated
81 | Comment content
82 | Number of likes
83 | PostId
84 | UserId
85 | Delete
86 |
87 | {comments.map((comment) => (
88 |
89 |
90 |
91 | {new Date(comment.updatedAt).toLocaleDateString()}
92 |
93 | {comment.content}
94 | {comment.numberOfLikes}
95 | {comment.postId}
96 | {comment.userId}
97 |
98 | {
100 | setShowModal(true);
101 | setCommentIdToDelete(comment._id);
102 | }}
103 | className='font-medium text-red-500 hover:underline cursor-pointer'
104 | >
105 | Delete
106 |
107 |
108 |
109 |
110 | ))}
111 |
112 | {showMore && (
113 |
117 | Show more
118 |
119 | )}
120 | >
121 | ) : (
122 |
You have no comments yet!
123 | )}
124 |
setShowModal(false)}
127 | popup
128 | size='md'
129 | >
130 |
131 |
132 |
133 |
134 |
135 | Are you sure you want to delete this comment?
136 |
137 |
138 |
139 | Yes, I'm sure
140 |
141 | setShowModal(false)}>
142 | No, cancel
143 |
144 |
145 |
146 |
147 |
148 |
149 | );
150 | }
151 |
--------------------------------------------------------------------------------
/client/src/components/DashPosts.jsx:
--------------------------------------------------------------------------------
1 | import { Modal, Table, Button } from 'flowbite-react';
2 | import { useEffect, useState } from 'react';
3 | import { useSelector } from 'react-redux';
4 | import { Link } from 'react-router-dom';
5 | import { HiOutlineExclamationCircle } from 'react-icons/hi';
6 | import { set } from 'mongoose';
7 |
8 | export default function DashPosts() {
9 | const { currentUser } = useSelector((state) => state.user);
10 | const [userPosts, setUserPosts] = useState([]);
11 | const [showMore, setShowMore] = useState(true);
12 | const [showModal, setShowModal] = useState(false);
13 | const [postIdToDelete, setPostIdToDelete] = useState('');
14 | useEffect(() => {
15 | const fetchPosts = async () => {
16 | try {
17 | const res = await fetch(`/api/post/getposts?userId=${currentUser._id}`);
18 | const data = await res.json();
19 | if (res.ok) {
20 | setUserPosts(data.posts);
21 | if (data.posts.length < 9) {
22 | setShowMore(false);
23 | }
24 | }
25 | } catch (error) {
26 | console.log(error.message);
27 | }
28 | };
29 | if (currentUser.isAdmin) {
30 | fetchPosts();
31 | }
32 | }, [currentUser._id]);
33 |
34 | const handleShowMore = async () => {
35 | const startIndex = userPosts.length;
36 | try {
37 | const res = await fetch(
38 | `/api/post/getposts?userId=${currentUser._id}&startIndex=${startIndex}`
39 | );
40 | const data = await res.json();
41 | if (res.ok) {
42 | setUserPosts((prev) => [...prev, ...data.posts]);
43 | if (data.posts.length < 9) {
44 | setShowMore(false);
45 | }
46 | }
47 | } catch (error) {
48 | console.log(error.message);
49 | }
50 | };
51 |
52 | const handleDeletePost = async () => {
53 | setShowModal(false);
54 | try {
55 | const res = await fetch(
56 | `/api/post/deletepost/${postIdToDelete}/${currentUser._id}`,
57 | {
58 | method: 'DELETE',
59 | }
60 | );
61 | const data = await res.json();
62 | if (!res.ok) {
63 | console.log(data.message);
64 | } else {
65 | setUserPosts((prev) =>
66 | prev.filter((post) => post._id !== postIdToDelete)
67 | );
68 | }
69 | } catch (error) {
70 | console.log(error.message);
71 | }
72 | };
73 |
74 | return (
75 |
76 | {currentUser.isAdmin && userPosts.length > 0 ? (
77 | <>
78 |
79 |
80 | Date updated
81 | Post image
82 | Post title
83 | Category
84 | Delete
85 |
86 | Edit
87 |
88 |
89 | {userPosts.map((post) => (
90 |
91 |
92 |
93 | {new Date(post.updatedAt).toLocaleDateString()}
94 |
95 |
96 |
97 |
102 |
103 |
104 |
105 |
109 | {post.title}
110 |
111 |
112 | {post.category}
113 |
114 | {
116 | setShowModal(true);
117 | setPostIdToDelete(post._id);
118 | }}
119 | className='font-medium text-red-500 hover:underline cursor-pointer'
120 | >
121 | Delete
122 |
123 |
124 |
125 |
129 | Edit
130 |
131 |
132 |
133 |
134 | ))}
135 |
136 | {showMore && (
137 |
141 | Show more
142 |
143 | )}
144 | >
145 | ) : (
146 |
You have no posts yet!
147 | )}
148 |
setShowModal(false)}
151 | popup
152 | size='md'
153 | >
154 |
155 |
156 |
157 |
158 |
159 | Are you sure you want to delete this post?
160 |
161 |
162 |
163 | Yes, I'm sure
164 |
165 | setShowModal(false)}>
166 | No, cancel
167 |
168 |
169 |
170 |
171 |
172 |
173 | );
174 | }
175 |
--------------------------------------------------------------------------------
/client/src/components/DashProfile.jsx:
--------------------------------------------------------------------------------
1 | import { Alert, Button, Modal, ModalBody, TextInput } from 'flowbite-react';
2 | import { useEffect, useRef, useState } from 'react';
3 | import { useSelector } from 'react-redux';
4 | import {
5 | getDownloadURL,
6 | getStorage,
7 | ref,
8 | uploadBytesResumable,
9 | } from 'firebase/storage';
10 | import { app } from '../firebase';
11 | import { CircularProgressbar } from 'react-circular-progressbar';
12 | import 'react-circular-progressbar/dist/styles.css';
13 | import {
14 | updateStart,
15 | updateSuccess,
16 | updateFailure,
17 | deleteUserStart,
18 | deleteUserSuccess,
19 | deleteUserFailure,
20 | signoutSuccess,
21 | } from '../redux/user/userSlice';
22 | import { useDispatch } from 'react-redux';
23 | import { HiOutlineExclamationCircle } from 'react-icons/hi';
24 | import { Link } from 'react-router-dom';
25 |
26 | export default function DashProfile() {
27 | const { currentUser, error, loading } = useSelector((state) => state.user);
28 | const [imageFile, setImageFile] = useState(null);
29 | const [imageFileUrl, setImageFileUrl] = useState(null);
30 | const [imageFileUploadProgress, setImageFileUploadProgress] = useState(null);
31 | const [imageFileUploadError, setImageFileUploadError] = useState(null);
32 | const [imageFileUploading, setImageFileUploading] = useState(false);
33 | const [updateUserSuccess, setUpdateUserSuccess] = useState(null);
34 | const [updateUserError, setUpdateUserError] = useState(null);
35 | const [showModal, setShowModal] = useState(false);
36 | const [formData, setFormData] = useState({});
37 | const filePickerRef = useRef();
38 | const dispatch = useDispatch();
39 | const handleImageChange = (e) => {
40 | const file = e.target.files[0];
41 | if (file) {
42 | setImageFile(file);
43 | setImageFileUrl(URL.createObjectURL(file));
44 | }
45 | };
46 | useEffect(() => {
47 | if (imageFile) {
48 | uploadImage();
49 | }
50 | }, [imageFile]);
51 |
52 | const uploadImage = async () => {
53 | // service firebase.storage {
54 | // match /b/{bucket}/o {
55 | // match /{allPaths=**} {
56 | // allow read;
57 | // allow write: if
58 | // request.resource.size < 2 * 1024 * 1024 &&
59 | // request.resource.contentType.matches('image/.*')
60 | // }
61 | // }
62 | // }
63 | setImageFileUploading(true);
64 | setImageFileUploadError(null);
65 | const storage = getStorage(app);
66 | const fileName = new Date().getTime() + imageFile.name;
67 | const storageRef = ref(storage, fileName);
68 | const uploadTask = uploadBytesResumable(storageRef, imageFile);
69 | uploadTask.on(
70 | 'state_changed',
71 | (snapshot) => {
72 | const progress =
73 | (snapshot.bytesTransferred / snapshot.totalBytes) * 100;
74 |
75 | setImageFileUploadProgress(progress.toFixed(0));
76 | },
77 | (error) => {
78 | setImageFileUploadError(
79 | 'Could not upload image (File must be less than 2MB)'
80 | );
81 | setImageFileUploadProgress(null);
82 | setImageFile(null);
83 | setImageFileUrl(null);
84 | setImageFileUploading(false);
85 | },
86 | () => {
87 | getDownloadURL(uploadTask.snapshot.ref).then((downloadURL) => {
88 | setImageFileUrl(downloadURL);
89 | setFormData({ ...formData, profilePicture: downloadURL });
90 | setImageFileUploading(false);
91 | });
92 | }
93 | );
94 | };
95 |
96 | const handleChange = (e) => {
97 | setFormData({ ...formData, [e.target.id]: e.target.value });
98 | };
99 |
100 | const handleSubmit = async (e) => {
101 | e.preventDefault();
102 | setUpdateUserError(null);
103 | setUpdateUserSuccess(null);
104 | if (Object.keys(formData).length === 0) {
105 | setUpdateUserError('No changes made');
106 | return;
107 | }
108 | if (imageFileUploading) {
109 | setUpdateUserError('Please wait for image to upload');
110 | return;
111 | }
112 | try {
113 | dispatch(updateStart());
114 | const res = await fetch(`/api/user/update/${currentUser._id}`, {
115 | method: 'PUT',
116 | headers: {
117 | 'Content-Type': 'application/json',
118 | },
119 | body: JSON.stringify(formData),
120 | });
121 | const data = await res.json();
122 | if (!res.ok) {
123 | dispatch(updateFailure(data.message));
124 | setUpdateUserError(data.message);
125 | } else {
126 | dispatch(updateSuccess(data));
127 | setUpdateUserSuccess("User's profile updated successfully");
128 | }
129 | } catch (error) {
130 | dispatch(updateFailure(error.message));
131 | setUpdateUserError(error.message);
132 | }
133 | };
134 | const handleDeleteUser = async () => {
135 | setShowModal(false);
136 | try {
137 | dispatch(deleteUserStart());
138 | const res = await fetch(`/api/user/delete/${currentUser._id}`, {
139 | method: 'DELETE',
140 | });
141 | const data = await res.json();
142 | if (!res.ok) {
143 | dispatch(deleteUserFailure(data.message));
144 | } else {
145 | dispatch(deleteUserSuccess(data));
146 | }
147 | } catch (error) {
148 | dispatch(deleteUserFailure(error.message));
149 | }
150 | };
151 |
152 | const handleSignout = async () => {
153 | try {
154 | const res = await fetch('/api/user/signout', {
155 | method: 'POST',
156 | });
157 | const data = await res.json();
158 | if (!res.ok) {
159 | console.log(data.message);
160 | } else {
161 | dispatch(signoutSuccess());
162 | }
163 | } catch (error) {
164 | console.log(error.message);
165 | }
166 | };
167 | return (
168 |
169 |
Profile
170 |
171 |
178 | filePickerRef.current.click()}
181 | >
182 | {imageFileUploadProgress && (
183 |
202 | )}
203 |
212 |
213 | {imageFileUploadError && (
214 | {imageFileUploadError}
215 | )}
216 |
223 |
230 |
236 |
242 | {loading ? 'Loading...' : 'Update'}
243 |
244 | {currentUser.isAdmin && (
245 |
246 |
251 | Create a post
252 |
253 |
254 | )}
255 |
256 |
257 | setShowModal(true)} className='cursor-pointer'>
258 | Delete Account
259 |
260 |
261 | Sign Out
262 |
263 |
264 | {updateUserSuccess && (
265 |
266 | {updateUserSuccess}
267 |
268 | )}
269 | {updateUserError && (
270 |
271 | {updateUserError}
272 |
273 | )}
274 | {error && (
275 |
276 | {error}
277 |
278 | )}
279 |
setShowModal(false)}
282 | popup
283 | size='md'
284 | >
285 |
286 |
287 |
288 |
289 |
290 | Are you sure you want to delete your account?
291 |
292 |
293 |
294 | Yes, I'm sure
295 |
296 | setShowModal(false)}>
297 | No, cancel
298 |
299 |
300 |
301 |
302 |
303 |
304 | );
305 | }
306 |
--------------------------------------------------------------------------------
/client/src/components/DashSidebar.jsx:
--------------------------------------------------------------------------------
1 | import { Sidebar } from 'flowbite-react';
2 | import {
3 | HiUser,
4 | HiArrowSmRight,
5 | HiDocumentText,
6 | HiOutlineUserGroup,
7 | HiAnnotation,
8 | HiChartPie,
9 | } from 'react-icons/hi';
10 | import { useEffect, useState } from 'react';
11 | import { Link, useLocation } from 'react-router-dom';
12 | import { signoutSuccess } from '../redux/user/userSlice';
13 | import { useDispatch } from 'react-redux';
14 | import { useSelector } from 'react-redux';
15 |
16 | export default function DashSidebar() {
17 | const location = useLocation();
18 | const dispatch = useDispatch();
19 | const { currentUser } = useSelector((state) => state.user);
20 | const [tab, setTab] = useState('');
21 | useEffect(() => {
22 | const urlParams = new URLSearchParams(location.search);
23 | const tabFromUrl = urlParams.get('tab');
24 | if (tabFromUrl) {
25 | setTab(tabFromUrl);
26 | }
27 | }, [location.search]);
28 | const handleSignout = async () => {
29 | try {
30 | const res = await fetch('/api/user/signout', {
31 | method: 'POST',
32 | });
33 | const data = await res.json();
34 | if (!res.ok) {
35 | console.log(data.message);
36 | } else {
37 | dispatch(signoutSuccess());
38 | }
39 | } catch (error) {
40 | console.log(error.message);
41 | }
42 | };
43 | return (
44 |
45 |
46 |
47 | {currentUser && currentUser.isAdmin && (
48 |
49 |
54 | Dashboard
55 |
56 |
57 | )}
58 |
59 |
66 | Profile
67 |
68 |
69 | {currentUser.isAdmin && (
70 |
71 |
76 | Posts
77 |
78 |
79 | )}
80 | {currentUser.isAdmin && (
81 | <>
82 |
83 |
88 | Users
89 |
90 |
91 |
92 |
97 | Comments
98 |
99 |
100 | >
101 | )}
102 |
107 | Sign Out
108 |
109 |
110 |
111 |
112 | );
113 | }
114 |
--------------------------------------------------------------------------------
/client/src/components/DashUsers.jsx:
--------------------------------------------------------------------------------
1 | import { Modal, Table, Button } from 'flowbite-react';
2 | import { useEffect, useState } from 'react';
3 | import { useSelector } from 'react-redux';
4 | import { HiOutlineExclamationCircle } from 'react-icons/hi';
5 | import { FaCheck, FaTimes } from 'react-icons/fa';
6 |
7 | export default function DashUsers() {
8 | const { currentUser } = useSelector((state) => state.user);
9 | const [users, setUsers] = useState([]);
10 | const [showMore, setShowMore] = useState(true);
11 | const [showModal, setShowModal] = useState(false);
12 | const [userIdToDelete, setUserIdToDelete] = useState('');
13 | useEffect(() => {
14 | const fetchUsers = async () => {
15 | try {
16 | const res = await fetch(`/api/user/getusers`);
17 | const data = await res.json();
18 | if (res.ok) {
19 | setUsers(data.users);
20 | if (data.users.length < 9) {
21 | setShowMore(false);
22 | }
23 | }
24 | } catch (error) {
25 | console.log(error.message);
26 | }
27 | };
28 | if (currentUser.isAdmin) {
29 | fetchUsers();
30 | }
31 | }, [currentUser._id]);
32 |
33 | const handleShowMore = async () => {
34 | const startIndex = users.length;
35 | try {
36 | const res = await fetch(`/api/user/getusers?startIndex=${startIndex}`);
37 | const data = await res.json();
38 | if (res.ok) {
39 | setUsers((prev) => [...prev, ...data.users]);
40 | if (data.users.length < 9) {
41 | setShowMore(false);
42 | }
43 | }
44 | } catch (error) {
45 | console.log(error.message);
46 | }
47 | };
48 |
49 | const handleDeleteUser = async () => {
50 | try {
51 | const res = await fetch(`/api/user/delete/${userIdToDelete}`, {
52 | method: 'DELETE',
53 | });
54 | const data = await res.json();
55 | if (res.ok) {
56 | setUsers((prev) => prev.filter((user) => user._id !== userIdToDelete));
57 | setShowModal(false);
58 | } else {
59 | console.log(data.message);
60 | }
61 | } catch (error) {
62 | console.log(error.message);
63 | }
64 | };
65 |
66 | return (
67 |
68 | {currentUser.isAdmin && users.length > 0 ? (
69 | <>
70 |
71 |
72 | Date created
73 | User image
74 | Username
75 | Email
76 | Admin
77 | Delete
78 |
79 | {users.map((user) => (
80 |
81 |
82 |
83 | {new Date(user.createdAt).toLocaleDateString()}
84 |
85 |
86 |
91 |
92 | {user.username}
93 | {user.email}
94 |
95 | {user.isAdmin ? (
96 |
97 | ) : (
98 |
99 | )}
100 |
101 |
102 | {
104 | setShowModal(true);
105 | setUserIdToDelete(user._id);
106 | }}
107 | className='font-medium text-red-500 hover:underline cursor-pointer'
108 | >
109 | Delete
110 |
111 |
112 |
113 |
114 | ))}
115 |
116 | {showMore && (
117 |
121 | Show more
122 |
123 | )}
124 | >
125 | ) : (
126 |
You have no users yet!
127 | )}
128 |
setShowModal(false)}
131 | popup
132 | size='md'
133 | >
134 |
135 |
136 |
137 |
138 |
139 | Are you sure you want to delete this user?
140 |
141 |
142 |
143 | Yes, I'm sure
144 |
145 | setShowModal(false)}>
146 | No, cancel
147 |
148 |
149 |
150 |
151 |
152 |
153 | );
154 | }
155 |
--------------------------------------------------------------------------------
/client/src/components/DashboardComp.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import { useSelector } from 'react-redux';
3 | import {
4 | HiAnnotation,
5 | HiArrowNarrowUp,
6 | HiDocumentText,
7 | HiOutlineUserGroup,
8 | } from 'react-icons/hi';
9 | import { Button, Table } from 'flowbite-react';
10 | import { Link } from 'react-router-dom';
11 |
12 | export default function DashboardComp() {
13 | const [users, setUsers] = useState([]);
14 | const [comments, setComments] = useState([]);
15 | const [posts, setPosts] = useState([]);
16 | const [totalUsers, setTotalUsers] = useState(0);
17 | const [totalPosts, setTotalPosts] = useState(0);
18 | const [totalComments, setTotalComments] = useState(0);
19 | const [lastMonthUsers, setLastMonthUsers] = useState(0);
20 | const [lastMonthPosts, setLastMonthPosts] = useState(0);
21 | const [lastMonthComments, setLastMonthComments] = useState(0);
22 | const { currentUser } = useSelector((state) => state.user);
23 | useEffect(() => {
24 | const fetchUsers = async () => {
25 | try {
26 | const res = await fetch('/api/user/getusers?limit=5');
27 | const data = await res.json();
28 | if (res.ok) {
29 | setUsers(data.users);
30 | setTotalUsers(data.totalUsers);
31 | setLastMonthUsers(data.lastMonthUsers);
32 | }
33 | } catch (error) {
34 | console.log(error.message);
35 | }
36 | };
37 | const fetchPosts = async () => {
38 | try {
39 | const res = await fetch('/api/post/getposts?limit=5');
40 | const data = await res.json();
41 | if (res.ok) {
42 | setPosts(data.posts);
43 | setTotalPosts(data.totalPosts);
44 | setLastMonthPosts(data.lastMonthPosts);
45 | }
46 | } catch (error) {
47 | console.log(error.message);
48 | }
49 | };
50 | const fetchComments = async () => {
51 | try {
52 | const res = await fetch('/api/comment/getcomments?limit=5');
53 | const data = await res.json();
54 | if (res.ok) {
55 | setComments(data.comments);
56 | setTotalComments(data.totalComments);
57 | setLastMonthComments(data.lastMonthComments);
58 | }
59 | } catch (error) {
60 | console.log(error.message);
61 | }
62 | };
63 | if (currentUser.isAdmin) {
64 | fetchUsers();
65 | fetchPosts();
66 | fetchComments();
67 | }
68 | }, [currentUser]);
69 | return (
70 |
71 |
72 |
73 |
74 |
75 |
Total Users
76 |
{totalUsers}
77 |
78 |
79 |
80 |
81 |
82 |
83 | {lastMonthUsers}
84 |
85 |
Last month
86 |
87 |
88 |
89 |
90 |
91 |
92 | Total Comments
93 |
94 |
{totalComments}
95 |
96 |
97 |
98 |
99 |
100 |
101 | {lastMonthComments}
102 |
103 |
Last month
104 |
105 |
106 |
107 |
108 |
109 |
Total Posts
110 |
{totalPosts}
111 |
112 |
113 |
114 |
115 |
116 |
117 | {lastMonthPosts}
118 |
119 |
Last month
120 |
121 |
122 |
123 |
124 |
125 |
126 |
Recent users
127 |
128 | See all
129 |
130 |
131 |
132 |
133 | User image
134 | Username
135 |
136 | {users &&
137 | users.map((user) => (
138 |
139 |
140 |
141 |
146 |
147 | {user.username}
148 |
149 |
150 | ))}
151 |
152 |
153 |
154 |
155 |
Recent comments
156 |
157 | See all
158 |
159 |
160 |
161 |
162 | Comment content
163 | Likes
164 |
165 | {comments &&
166 | comments.map((comment) => (
167 |
168 |
169 |
170 | {comment.content}
171 |
172 | {comment.numberOfLikes}
173 |
174 |
175 | ))}
176 |
177 |
178 |
179 |
180 |
Recent posts
181 |
182 | See all
183 |
184 |
185 |
186 |
187 | Post image
188 | Post Title
189 | Category
190 |
191 | {posts &&
192 | posts.map((post) => (
193 |
194 |
195 |
196 |
201 |
202 | {post.title}
203 | {post.category}
204 |
205 |
206 | ))}
207 |
208 |
209 |
210 |
211 | );
212 | }
213 |
--------------------------------------------------------------------------------
/client/src/components/Footer.jsx:
--------------------------------------------------------------------------------
1 | import { Footer } from 'flowbite-react';
2 | import { Link } from 'react-router-dom';
3 | import { BsFacebook, BsInstagram, BsTwitter, BsGithub, BsDribbble } from 'react-icons/bs';
4 | export default function FooterCom() {
5 | return (
6 |
7 |
8 |
9 |
10 |
14 |
15 | Sahand's
16 |
17 | Blog
18 |
19 |
20 |
21 |
22 |
23 |
24 |
29 | 100 JS Projects
30 |
31 |
36 | Sahand's Blog
37 |
38 |
39 |
40 |
41 |
42 |
43 |
48 | Github
49 |
50 | Discord
51 |
52 |
53 |
54 |
55 |
56 | Privacy Policy
57 | Terms & Conditions
58 |
59 |
60 |
61 |
62 |
63 |
64 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 | );
81 | }
82 |
--------------------------------------------------------------------------------
/client/src/components/Header.jsx:
--------------------------------------------------------------------------------
1 | import { Avatar, Button, Dropdown, Navbar, TextInput } from 'flowbite-react';
2 | import { Link, useLocation, useNavigate } from 'react-router-dom';
3 | import { AiOutlineSearch } from 'react-icons/ai';
4 | import { FaMoon, FaSun } from 'react-icons/fa';
5 | import { useSelector, useDispatch } from 'react-redux';
6 | import { toggleTheme } from '../redux/theme/themeSlice';
7 | import { signoutSuccess } from '../redux/user/userSlice';
8 | import { useEffect, useState } from 'react';
9 |
10 | export default function Header() {
11 | const path = useLocation().pathname;
12 | const location = useLocation();
13 | const navigate = useNavigate();
14 | const dispatch = useDispatch();
15 | const { currentUser } = useSelector((state) => state.user);
16 | const { theme } = useSelector((state) => state.theme);
17 | const [searchTerm, setSearchTerm] = useState('');
18 |
19 | useEffect(() => {
20 | const urlParams = new URLSearchParams(location.search);
21 | const searchTermFromUrl = urlParams.get('searchTerm');
22 | if (searchTermFromUrl) {
23 | setSearchTerm(searchTermFromUrl);
24 | }
25 | }, [location.search]);
26 |
27 | const handleSignout = async () => {
28 | try {
29 | const res = await fetch('/api/user/signout', {
30 | method: 'POST',
31 | });
32 | const data = await res.json();
33 | if (!res.ok) {
34 | console.log(data.message);
35 | } else {
36 | dispatch(signoutSuccess());
37 | }
38 | } catch (error) {
39 | console.log(error.message);
40 | }
41 | };
42 |
43 | const handleSubmit = (e) => {
44 | e.preventDefault();
45 | const urlParams = new URLSearchParams(location.search);
46 | urlParams.set('searchTerm', searchTerm);
47 | const searchQuery = urlParams.toString();
48 | navigate(`/search?${searchQuery}`);
49 | };
50 |
51 | return (
52 |
53 |
57 |
58 | Sahand's
59 |
60 | Blog
61 |
62 |
63 | setSearchTerm(e.target.value)}
70 | />
71 |
72 |
73 |
74 |
75 |
76 | dispatch(toggleTheme())}
81 | >
82 | {theme === 'light' ? : }
83 |
84 | {currentUser ? (
85 |
90 | }
91 | >
92 |
93 | @{currentUser.username}
94 |
95 | {currentUser.email}
96 |
97 |
98 |
99 | Profile
100 |
101 |
102 | Sign out
103 |
104 | ) : (
105 |
106 |
107 | Sign In
108 |
109 |
110 | )}
111 |
112 |
113 |
114 |
115 | Home
116 |
117 |
118 | About
119 |
120 |
121 | Projects
122 |
123 |
124 |
125 | );
126 | }
127 |
--------------------------------------------------------------------------------
/client/src/components/OAuth.jsx:
--------------------------------------------------------------------------------
1 | import { Button } from 'flowbite-react';
2 | import { AiFillGoogleCircle } from 'react-icons/ai';
3 | import { GoogleAuthProvider, signInWithPopup, getAuth } from 'firebase/auth';
4 | import { app } from '../firebase';
5 | import { useDispatch } from 'react-redux';
6 | import { signInSuccess } from '../redux/user/userSlice';
7 | import { useNavigate } from 'react-router-dom';
8 |
9 | export default function OAuth() {
10 | const auth = getAuth(app)
11 | const dispatch = useDispatch()
12 | const navigate = useNavigate()
13 | const handleGoogleClick = async () =>{
14 | const provider = new GoogleAuthProvider()
15 | provider.setCustomParameters({ prompt: 'select_account' })
16 | try {
17 | const resultsFromGoogle = await signInWithPopup(auth, provider)
18 | const res = await fetch('/api/auth/google', {
19 | method: 'POST',
20 | headers: { 'Content-Type': 'application/json' },
21 | body: JSON.stringify({
22 | name: resultsFromGoogle.user.displayName,
23 | email: resultsFromGoogle.user.email,
24 | googlePhotoUrl: resultsFromGoogle.user.photoURL,
25 | }),
26 | })
27 | const data = await res.json()
28 | if (res.ok){
29 | dispatch(signInSuccess(data))
30 | navigate('/')
31 | }
32 | } catch (error) {
33 | console.log(error);
34 | }
35 | }
36 | return (
37 |
38 |
39 | Continue with Google
40 |
41 | )
42 | }
--------------------------------------------------------------------------------
/client/src/components/OnlyAdminPrivateRoute.jsx:
--------------------------------------------------------------------------------
1 | import { useSelector } from 'react-redux';
2 | import { Outlet, Navigate } from 'react-router-dom';
3 |
4 | export default function OnlyAdminPrivateRoute() {
5 | const { currentUser } = useSelector((state) => state.user);
6 | return currentUser && currentUser.isAdmin ? (
7 |
8 | ) : (
9 |
10 | );
11 | }
12 |
--------------------------------------------------------------------------------
/client/src/components/PostCard.jsx:
--------------------------------------------------------------------------------
1 | import { Link } from 'react-router-dom';
2 |
3 | export default function PostCard({ post }) {
4 | return (
5 |
6 |
7 |
12 |
13 |
14 |
{post.title}
15 |
{post.category}
16 |
20 | Read article
21 |
22 |
23 |
24 | );
25 | }
26 |
--------------------------------------------------------------------------------
/client/src/components/PrivateRoute.jsx:
--------------------------------------------------------------------------------
1 | import { useSelector } from 'react-redux';
2 | import { Outlet, Navigate } from 'react-router-dom';
3 |
4 | export default function PrivateRoute() {
5 | const { currentUser } = useSelector((state) => state.user);
6 | return currentUser ? : ;
7 | }
8 |
--------------------------------------------------------------------------------
/client/src/components/ScrollToTop.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 | import { useLocation } from 'react-router-dom';
3 |
4 | const ScrollToTop = () => {
5 | const { pathname } = useLocation();
6 | useEffect(() => {
7 | window.scrollTo(0, 0);
8 | }, [pathname]);
9 | return null;
10 | };
11 |
12 | export default ScrollToTop;
13 |
--------------------------------------------------------------------------------
/client/src/components/ThemeProvider.jsx:
--------------------------------------------------------------------------------
1 | import { useSelector } from 'react-redux';
2 |
3 | export default function ThemeProvider({ children }) {
4 | const { theme } = useSelector((state) => state.theme);
5 | return (
6 |
7 |
8 | {children}
9 |
10 |
11 | );
12 | }
13 |
--------------------------------------------------------------------------------
/client/src/firebase.js:
--------------------------------------------------------------------------------
1 | // Import the functions you need from the SDKs you need
2 | import { initializeApp } from 'firebase/app';
3 | // TODO: Add SDKs for Firebase products that you want to use
4 | // https://firebase.google.com/docs/web/setup#available-libraries
5 |
6 | // Your web app's Firebase configuration
7 |
8 | const firebaseConfig = {
9 | apiKey: import.meta.env.VITE_FIREBASE_API_KEY,
10 | authDomain: 'mern-blog-b327f.firebaseapp.com',
11 | projectId: 'mern-blog-b327f',
12 | storageBucket: 'mern-blog-b327f.appspot.com',
13 | messagingSenderId: '699397991367',
14 | appId: '1:699397991367:web:88ff565ef72a182d6b87e2',
15 | };
16 |
17 | // Initialize Firebase
18 | export const app = initializeApp(firebaseConfig);
19 |
--------------------------------------------------------------------------------
/client/src/index.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | body {
6 | height: 100vh;
7 | }
8 |
9 | .ql-editor {
10 | font-size: 1.05rem;
11 | }
12 |
13 | .post-content p {
14 | margin-bottom: 0.5rem;
15 | }
16 |
17 | .post-content h1 {
18 | font-size: 1.5rem;
19 | font-weight: 600;
20 | font-family: sans-serif;
21 | margin: 1.5rem 0;
22 | }
23 | .post-content h2 {
24 | font-size: 1.4rem;
25 | font-family: sans-serif;
26 | margin: 1.5rem 0;
27 | }
28 |
29 | .post-content ul li {
30 | margin: 1.15rem 0;
31 | margin-left: 1.3rem;
32 | list-style-type: circle;
33 | }
34 |
35 | .post-content ol li {
36 | margin: 1.15rem 0;
37 | margin-left: 1.3rem;
38 | list-style-type: decimal;
39 | }
40 |
41 | .post-content a {
42 | /* light blue */
43 | color: rgb(73, 149, 199);
44 | text-decoration: none;
45 | }
46 |
47 | .post-content a:hover {
48 | text-decoration: underline;
49 | }
50 |
51 | .post-content pre {
52 | background-color: rgb(30, 41, 59);
53 | border: 1px solid rgb(61, 69, 83);
54 | color: rgb(232, 231, 231);
55 | white-space: pre-wrap;
56 | padding: 0.5rem;
57 | border-radius: 0.5rem;
58 | margin-bottom: 0.5rem;
59 | }
60 |
61 | .post-content code {
62 | background-color: rgb(236, 236, 236);
63 | padding: 0.25rem;
64 | border-radius: 0.25rem;
65 | font-size: 0.9rem;
66 | }
67 |
68 | .dark .post-content code {
69 | background-color: rgb(30, 41, 59);
70 | border: 1px solid rgb(61, 69, 83);
71 | }
--------------------------------------------------------------------------------
/client/src/main.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom/client';
3 | import App from './App.jsx';
4 | import './index.css';
5 | import { store, persistor } from './redux/store.js';
6 | import { Provider } from 'react-redux';
7 | import { PersistGate } from 'redux-persist/integration/react';
8 | import ThemeProvider from './components/ThemeProvider.jsx';
9 |
10 | ReactDOM.createRoot(document.getElementById('root')).render(
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | );
19 |
--------------------------------------------------------------------------------
/client/src/pages/About.jsx:
--------------------------------------------------------------------------------
1 | import CallToAction from '../components/CallToAction';
2 |
3 | export default function About() {
4 | return (
5 |
6 |
7 |
8 |
9 | About Sahand's Blog
10 |
11 |
12 |
13 | Welcome to Sahand's Blog! This blog was created by Sahand Ghavidel
14 | as a personal project to share his thoughts and ideas with the
15 | world. Sahand is a passionate developer who loves to write about
16 | technology, coding, and everything in between.
17 |
18 |
19 |
20 | On this blog, you'll find weekly articles and tutorials on topics
21 | such as web development, software engineering, and programming
22 | languages. Sahand is always learning and exploring new
23 | technologies, so be sure to check back often for new content!
24 |
25 |
26 |
27 | We encourage you to leave comments on our posts and engage with
28 | other readers. You can like other people's comments and reply to
29 | them as well. We believe that a community of learners can help
30 | each other grow and improve.
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | );
40 | }
--------------------------------------------------------------------------------
/client/src/pages/CreatePost.jsx:
--------------------------------------------------------------------------------
1 | import { Alert, Button, FileInput, Select, TextInput } from 'flowbite-react';
2 | import ReactQuill from 'react-quill';
3 | import 'react-quill/dist/quill.snow.css';
4 | import {
5 | getDownloadURL,
6 | getStorage,
7 | ref,
8 | uploadBytesResumable,
9 | } from 'firebase/storage';
10 | import { app } from '../firebase';
11 | import { useState } from 'react';
12 | import { CircularProgressbar } from 'react-circular-progressbar';
13 | import 'react-circular-progressbar/dist/styles.css';
14 | import { useNavigate } from 'react-router-dom';
15 |
16 | export default function CreatePost() {
17 | const [file, setFile] = useState(null);
18 | const [imageUploadProgress, setImageUploadProgress] = useState(null);
19 | const [imageUploadError, setImageUploadError] = useState(null);
20 | const [formData, setFormData] = useState({});
21 | const [publishError, setPublishError] = useState(null);
22 |
23 | const navigate = useNavigate();
24 |
25 | const handleUpdloadImage = async () => {
26 | try {
27 | if (!file) {
28 | setImageUploadError('Please select an image');
29 | return;
30 | }
31 | setImageUploadError(null);
32 | const storage = getStorage(app);
33 | const fileName = new Date().getTime() + '-' + file.name;
34 | const storageRef = ref(storage, fileName);
35 | const uploadTask = uploadBytesResumable(storageRef, file);
36 | uploadTask.on(
37 | 'state_changed',
38 | (snapshot) => {
39 | const progress =
40 | (snapshot.bytesTransferred / snapshot.totalBytes) * 100;
41 | setImageUploadProgress(progress.toFixed(0));
42 | },
43 | (error) => {
44 | setImageUploadError('Image upload failed');
45 | setImageUploadProgress(null);
46 | },
47 | () => {
48 | getDownloadURL(uploadTask.snapshot.ref).then((downloadURL) => {
49 | setImageUploadProgress(null);
50 | setImageUploadError(null);
51 | setFormData({ ...formData, image: downloadURL });
52 | });
53 | }
54 | );
55 | } catch (error) {
56 | setImageUploadError('Image upload failed');
57 | setImageUploadProgress(null);
58 | console.log(error);
59 | }
60 | };
61 | const handleSubmit = async (e) => {
62 | e.preventDefault();
63 | try {
64 | const res = await fetch('/api/post/create', {
65 | method: 'POST',
66 | headers: {
67 | 'Content-Type': 'application/json',
68 | },
69 | body: JSON.stringify(formData),
70 | });
71 | const data = await res.json();
72 | if (!res.ok) {
73 | setPublishError(data.message);
74 | return;
75 | }
76 |
77 | if (res.ok) {
78 | setPublishError(null);
79 | navigate(`/post/${data.slug}`);
80 | }
81 | } catch (error) {
82 | setPublishError('Something went wrong');
83 | }
84 | };
85 | return (
86 |
87 |
Create a post
88 |
89 |
90 |
97 | setFormData({ ...formData, title: e.target.value })
98 | }
99 | />
100 |
102 | setFormData({ ...formData, category: e.target.value })
103 | }
104 | >
105 | Select a category
106 | JavaScript
107 | React.js
108 | Next.js
109 |
110 |
111 |
112 |
setFile(e.target.files[0])}
116 | />
117 |
125 | {imageUploadProgress ? (
126 |
127 |
131 |
132 | ) : (
133 | 'Upload Image'
134 | )}
135 |
136 |
137 | {imageUploadError && {imageUploadError} }
138 | {formData.image && (
139 |
144 | )}
145 | {
151 | setFormData({ ...formData, content: value });
152 | }}
153 | />
154 |
155 | Publish
156 |
157 | {publishError && (
158 |
159 | {publishError}
160 |
161 | )}
162 |
163 |
164 | );
165 | }
166 |
--------------------------------------------------------------------------------
/client/src/pages/Dashboard.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import { useLocation } from 'react-router-dom';
3 | import DashSidebar from '../components/DashSidebar';
4 | import DashProfile from '../components/DashProfile';
5 | import DashPosts from '../components/DashPosts';
6 | import DashUsers from '../components/DashUsers';
7 | import DashComments from '../components/DashComments';
8 | import DashboardComp from '../components/DashboardComp';
9 |
10 | export default function Dashboard() {
11 | const location = useLocation();
12 | const [tab, setTab] = useState('');
13 | useEffect(() => {
14 | const urlParams = new URLSearchParams(location.search);
15 | const tabFromUrl = urlParams.get('tab');
16 | if (tabFromUrl) {
17 | setTab(tabFromUrl);
18 | }
19 | }, [location.search]);
20 | return (
21 |
22 |
23 | {/* Sidebar */}
24 |
25 |
26 | {/* profile... */}
27 | {tab === 'profile' &&
}
28 | {/* posts... */}
29 | {tab === 'posts' &&
}
30 | {/* users */}
31 | {tab === 'users' &&
}
32 | {/* comments */}
33 | {tab === 'comments' &&
}
34 | {/* dashboard comp */}
35 | {tab === 'dash' &&
}
36 |
37 | );
38 | }
39 |
--------------------------------------------------------------------------------
/client/src/pages/Home.jsx:
--------------------------------------------------------------------------------
1 | import { Link } from 'react-router-dom';
2 | import CallToAction from '../components/CallToAction';
3 | import { useEffect, useState } from 'react';
4 | import PostCard from '../components/PostCard';
5 |
6 | export default function Home() {
7 | const [posts, setPosts] = useState([]);
8 |
9 | useEffect(() => {
10 | const fetchPosts = async () => {
11 | const res = await fetch('/api/post/getPosts');
12 | const data = await res.json();
13 | setPosts(data.posts);
14 | };
15 | fetchPosts();
16 | }, []);
17 | return (
18 |
19 |
20 |
Welcome to my Blog
21 |
22 | Welcome to my blog! Here you'll find a wide range of articles,
23 | tutorials, and resources designed to help you grow as a developer.
24 | Whether you're interested in web development, software engineering,
25 | programming languages, or best practices in the tech industry, there's
26 | something here for everyone. Dive in and explore the content to expand
27 | your knowledge and skills.
28 |
29 |
33 | View all posts
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 | {posts && posts.length > 0 && (
42 |
43 |
Recent Posts
44 |
45 | {posts.map((post) => (
46 |
47 | ))}
48 |
49 |
53 | View all posts
54 |
55 |
56 | )}
57 |
58 |
59 | );
60 | }
61 |
--------------------------------------------------------------------------------
/client/src/pages/PostPage.jsx:
--------------------------------------------------------------------------------
1 | import { Button, Spinner } from 'flowbite-react';
2 | import { useEffect, useState } from 'react';
3 | import { Link, useParams } from 'react-router-dom';
4 | import CallToAction from '../components/CallToAction';
5 | import CommentSection from '../components/CommentSection';
6 | import PostCard from '../components/PostCard';
7 |
8 | export default function PostPage() {
9 | const { postSlug } = useParams();
10 | const [loading, setLoading] = useState(true);
11 | const [error, setError] = useState(false);
12 | const [post, setPost] = useState(null);
13 | const [recentPosts, setRecentPosts] = useState(null);
14 |
15 | useEffect(() => {
16 | const fetchPost = async () => {
17 | try {
18 | setLoading(true);
19 | const res = await fetch(`/api/post/getposts?slug=${postSlug}`);
20 | const data = await res.json();
21 | if (!res.ok) {
22 | setError(true);
23 | setLoading(false);
24 | return;
25 | }
26 | if (res.ok) {
27 | setPost(data.posts[0]);
28 | setLoading(false);
29 | setError(false);
30 | }
31 | } catch (error) {
32 | setError(true);
33 | setLoading(false);
34 | }
35 | };
36 | fetchPost();
37 | }, [postSlug]);
38 |
39 | useEffect(() => {
40 | try {
41 | const fetchRecentPosts = async () => {
42 | const res = await fetch(`/api/post/getposts?limit=3`);
43 | const data = await res.json();
44 | if (res.ok) {
45 | setRecentPosts(data.posts);
46 | }
47 | };
48 | fetchRecentPosts();
49 | } catch (error) {
50 | console.log(error.message);
51 | }
52 | }, []);
53 |
54 | if (loading)
55 | return (
56 |
57 |
58 |
59 | );
60 | return (
61 |
62 |
63 | {post && post.title}
64 |
65 |
69 |
70 | {post && post.category}
71 |
72 |
73 |
78 |
79 | {post && new Date(post.createdAt).toLocaleDateString()}
80 |
81 | {post && (post.content.length / 1000).toFixed(0)} mins read
82 |
83 |
84 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
Recent articles
95 |
96 | {recentPosts &&
97 | recentPosts.map((post) =>
)}
98 |
99 |
100 |
101 | );
102 | }
103 |
--------------------------------------------------------------------------------
/client/src/pages/Projects.jsx:
--------------------------------------------------------------------------------
1 | import CallToAction from '../components/CallToAction';
2 |
3 | export default function Projects() {
4 | return (
5 |
6 |
Explore Our Projects
7 |
8 | Dive into a collection of fun and engaging projects designed to help you
9 | learn and master HTML, CSS, and JavaScript. Whether you're a beginner or
10 | an experienced developer, these projects will challenge your skills and
11 | inspire creativity. Start building today and take your development
12 | journey to the next level!
13 |
14 |
15 |
16 |
17 | Why Build Projects?
18 |
19 |
20 | Building projects is one of the best ways to learn programming. It
21 | allows you to apply theoretical knowledge in a practical way, solve
22 | real-world problems, and create a portfolio that showcases your
23 | skills to potential employers or clients.
24 |
25 |
26 |
27 |
28 | What You'll Learn
29 |
30 |
31 | How to structure HTML for clean and semantic code
32 | Styling with CSS to create visually appealing designs
33 | Adding interactivity with JavaScript
34 | Debugging and problem-solving techniques
35 | Best practices for responsive and accessible web design
36 |
37 |
38 |
39 |
40 |
41 | );
42 | }
43 |
--------------------------------------------------------------------------------
/client/src/pages/Search.jsx:
--------------------------------------------------------------------------------
1 | import { Button, Select, TextInput } from 'flowbite-react';
2 | import { useEffect, useState } from 'react';
3 | import { useLocation, useNavigate } from 'react-router-dom';
4 | import PostCard from '../components/PostCard';
5 |
6 | export default function Search() {
7 | const [sidebarData, setSidebarData] = useState({
8 | searchTerm: '',
9 | sort: 'desc',
10 | category: 'uncategorized',
11 | });
12 |
13 | console.log(sidebarData);
14 | const [posts, setPosts] = useState([]);
15 | const [loading, setLoading] = useState(false);
16 | const [showMore, setShowMore] = useState(false);
17 |
18 | const location = useLocation();
19 |
20 | const navigate = useNavigate();
21 |
22 | useEffect(() => {
23 | const urlParams = new URLSearchParams(location.search);
24 | const searchTermFromUrl = urlParams.get('searchTerm');
25 | const sortFromUrl = urlParams.get('sort');
26 | const categoryFromUrl = urlParams.get('category');
27 | if (searchTermFromUrl || sortFromUrl || categoryFromUrl) {
28 | setSidebarData({
29 | ...sidebarData,
30 | searchTerm: searchTermFromUrl,
31 | sort: sortFromUrl,
32 | category: categoryFromUrl,
33 | });
34 | }
35 |
36 | const fetchPosts = async () => {
37 | setLoading(true);
38 | const searchQuery = urlParams.toString();
39 | const res = await fetch(`/api/post/getposts?${searchQuery}`);
40 | if (!res.ok) {
41 | setLoading(false);
42 | return;
43 | }
44 | if (res.ok) {
45 | const data = await res.json();
46 | setPosts(data.posts);
47 | setLoading(false);
48 | if (data.posts.length === 9) {
49 | setShowMore(true);
50 | } else {
51 | setShowMore(false);
52 | }
53 | }
54 | };
55 | fetchPosts();
56 | }, [location.search]);
57 |
58 | const handleChange = (e) => {
59 | if (e.target.id === 'searchTerm') {
60 | setSidebarData({ ...sidebarData, searchTerm: e.target.value });
61 | }
62 | if (e.target.id === 'sort') {
63 | const order = e.target.value || 'desc';
64 | setSidebarData({ ...sidebarData, sort: order });
65 | }
66 | if (e.target.id === 'category') {
67 | const category = e.target.value || 'uncategorized';
68 | setSidebarData({ ...sidebarData, category });
69 | }
70 | };
71 |
72 | const handleSubmit = (e) => {
73 | e.preventDefault();
74 | const urlParams = new URLSearchParams(location.search);
75 | urlParams.set('searchTerm', sidebarData.searchTerm);
76 | urlParams.set('sort', sidebarData.sort);
77 | urlParams.set('category', sidebarData.category);
78 | const searchQuery = urlParams.toString();
79 | navigate(`/search?${searchQuery}`);
80 | };
81 |
82 | const handleShowMore = async () => {
83 | const numberOfPosts = posts.length;
84 | const startIndex = numberOfPosts;
85 | const urlParams = new URLSearchParams(location.search);
86 | urlParams.set('startIndex', startIndex);
87 | const searchQuery = urlParams.toString();
88 | const res = await fetch(`/api/post/getposts?${searchQuery}`);
89 | if (!res.ok) {
90 | return;
91 | }
92 | if (res.ok) {
93 | const data = await res.json();
94 | setPosts([...posts, ...data.posts]);
95 | if (data.posts.length === 9) {
96 | setShowMore(true);
97 | } else {
98 | setShowMore(false);
99 | }
100 | }
101 | };
102 |
103 | return (
104 |
105 |
106 |
107 |
108 |
109 | Search Term:
110 |
111 |
118 |
119 |
120 | Sort:
121 |
122 | Latest
123 | Oldest
124 |
125 |
126 |
127 | Category:
128 |
133 | Uncategorized
134 | React.js
135 | Next.js
136 | JavaScript
137 |
138 |
139 |
140 | Apply Filters
141 |
142 |
143 |
144 |
145 |
146 | Posts results:
147 |
148 |
149 | {!loading && posts.length === 0 && (
150 |
No posts found.
151 | )}
152 | {loading &&
Loading...
}
153 | {!loading &&
154 | posts &&
155 | posts.map((post) =>
)}
156 | {showMore && (
157 |
161 | Show More
162 |
163 | )}
164 |
165 |
166 |
167 | );
168 | }
169 |
--------------------------------------------------------------------------------
/client/src/pages/SignIn.jsx:
--------------------------------------------------------------------------------
1 | import { Alert, Button, Label, Spinner, TextInput } from 'flowbite-react';
2 | import { useState } from 'react';
3 | import { Link, useNavigate } from 'react-router-dom';
4 | import { useDispatch, useSelector } from 'react-redux';
5 | import {
6 | signInStart,
7 | signInSuccess,
8 | signInFailure,
9 | } from '../redux/user/userSlice';
10 | import OAuth from '../components/OAuth';
11 |
12 | export default function SignIn() {
13 | const [formData, setFormData] = useState({});
14 | const { loading, error: errorMessage } = useSelector((state) => state.user);
15 | const dispatch = useDispatch();
16 | const navigate = useNavigate();
17 | const handleChange = (e) => {
18 | setFormData({ ...formData, [e.target.id]: e.target.value.trim() });
19 | };
20 | const handleSubmit = async (e) => {
21 | e.preventDefault();
22 | if (!formData.email || !formData.password) {
23 | return dispatch(signInFailure('Please fill all the fields'));
24 | }
25 | try {
26 | dispatch(signInStart());
27 | const res = await fetch('/api/auth/signin', {
28 | method: 'POST',
29 | headers: { 'Content-Type': 'application/json' },
30 | body: JSON.stringify(formData),
31 | });
32 | const data = await res.json();
33 | if (data.success === false) {
34 | dispatch(signInFailure(data.message));
35 | }
36 |
37 | if (res.ok) {
38 | dispatch(signInSuccess(data));
39 | navigate('/');
40 | }
41 | } catch (error) {
42 | dispatch(signInFailure(error.message));
43 | }
44 | };
45 | return (
46 |
47 |
48 | {/* left */}
49 |
50 |
51 |
52 | Sahand's
53 |
54 | Blog
55 |
56 |
57 | This is a demo project. You can sign in with your email and password
58 | or with Google.
59 |
60 |
61 | {/* right */}
62 |
63 |
64 |
65 |
66 |
67 |
73 |
74 |
75 |
76 |
82 |
83 |
88 | {loading ? (
89 | <>
90 |
91 | Loading...
92 | >
93 | ) : (
94 | 'Sign In'
95 | )}
96 |
97 |
98 |
99 |
100 | Dont Have an account?
101 |
102 | Sign Up
103 |
104 |
105 | {errorMessage && (
106 |
107 | {errorMessage}
108 |
109 | )}
110 |
111 |
112 |
113 | );
114 | }
115 |
--------------------------------------------------------------------------------
/client/src/pages/SignUp.jsx:
--------------------------------------------------------------------------------
1 | import { Alert, Button, Label, Spinner, TextInput } from 'flowbite-react';
2 | import { useState } from 'react';
3 | import { Link, useNavigate } from 'react-router-dom';
4 | import OAuth from '../components/OAuth';
5 |
6 | export default function SignUp() {
7 | const [formData, setFormData] = useState({});
8 | const [errorMessage, setErrorMessage] = useState(null);
9 | const [loading, setLoading] = useState(false);
10 | const navigate = useNavigate();
11 | const handleChange = (e) => {
12 | setFormData({ ...formData, [e.target.id]: e.target.value.trim() });
13 | };
14 | const handleSubmit = async (e) => {
15 | e.preventDefault();
16 | if (!formData.username || !formData.email || !formData.password) {
17 | return setErrorMessage('Please fill out all fields.');
18 | }
19 | try {
20 | setLoading(true);
21 | setErrorMessage(null);
22 | const res = await fetch('/api/auth/signup', {
23 | method: 'POST',
24 | headers: { 'Content-Type': 'application/json' },
25 | body: JSON.stringify(formData),
26 | });
27 | const data = await res.json();
28 | if (data.success === false) {
29 | return setErrorMessage(data.message);
30 | }
31 | setLoading(false);
32 | if(res.ok) {
33 | navigate('/sign-in');
34 | }
35 | } catch (error) {
36 | setErrorMessage(error.message);
37 | setLoading(false);
38 | }
39 | };
40 | return (
41 |
42 |
43 | {/* left */}
44 |
45 |
46 |
47 | Sahand's
48 |
49 | Blog
50 |
51 |
52 | This is a demo project. You can sign up with your email and password
53 | or with Google.
54 |
55 |
56 | {/* right */}
57 |
58 |
59 |
60 |
61 |
62 |
68 |
69 |
70 |
71 |
77 |
78 |
79 |
80 |
86 |
87 |
92 | {loading ? (
93 | <>
94 |
95 | Loading...
96 | >
97 | ) : (
98 | 'Sign Up'
99 | )}
100 |
101 |
102 |
103 |
104 | Have an account?
105 |
106 | Sign In
107 |
108 |
109 | {errorMessage && (
110 |
111 | {errorMessage}
112 |
113 | )}
114 |
115 |
116 |
117 | );
118 | }
119 |
--------------------------------------------------------------------------------
/client/src/pages/UpdatePost.jsx:
--------------------------------------------------------------------------------
1 | import { Alert, Button, FileInput, Select, TextInput } from 'flowbite-react';
2 | import ReactQuill from 'react-quill';
3 | import 'react-quill/dist/quill.snow.css';
4 | import {
5 | getDownloadURL,
6 | getStorage,
7 | ref,
8 | uploadBytesResumable,
9 | } from 'firebase/storage';
10 | import { app } from '../firebase';
11 | import { useEffect, useState } from 'react';
12 | import { CircularProgressbar } from 'react-circular-progressbar';
13 | import 'react-circular-progressbar/dist/styles.css';
14 | import { useNavigate, useParams } from 'react-router-dom';
15 | import { useSelector } from 'react-redux';
16 |
17 | export default function UpdatePost() {
18 | const [file, setFile] = useState(null);
19 | const [imageUploadProgress, setImageUploadProgress] = useState(null);
20 | const [imageUploadError, setImageUploadError] = useState(null);
21 | const [formData, setFormData] = useState({});
22 | const [publishError, setPublishError] = useState(null);
23 | const { postId } = useParams();
24 |
25 | const navigate = useNavigate();
26 | const { currentUser } = useSelector((state) => state.user);
27 |
28 | useEffect(() => {
29 | try {
30 | const fetchPost = async () => {
31 | const res = await fetch(`/api/post/getposts?postId=${postId}`);
32 | const data = await res.json();
33 | if (!res.ok) {
34 | console.log(data.message);
35 | setPublishError(data.message);
36 | return;
37 | }
38 | if (res.ok) {
39 | setPublishError(null);
40 | setFormData(data.posts[0]);
41 | }
42 | };
43 |
44 | fetchPost();
45 | } catch (error) {
46 | console.log(error.message);
47 | }
48 | }, [postId]);
49 |
50 | const handleUpdloadImage = async () => {
51 | try {
52 | if (!file) {
53 | setImageUploadError('Please select an image');
54 | return;
55 | }
56 | setImageUploadError(null);
57 | const storage = getStorage(app);
58 | const fileName = new Date().getTime() + '-' + file.name;
59 | const storageRef = ref(storage, fileName);
60 | const uploadTask = uploadBytesResumable(storageRef, file);
61 | uploadTask.on(
62 | 'state_changed',
63 | (snapshot) => {
64 | const progress =
65 | (snapshot.bytesTransferred / snapshot.totalBytes) * 100;
66 | setImageUploadProgress(progress.toFixed(0));
67 | },
68 | (error) => {
69 | setImageUploadError('Image upload failed');
70 | setImageUploadProgress(null);
71 | },
72 | () => {
73 | getDownloadURL(uploadTask.snapshot.ref).then((downloadURL) => {
74 | setImageUploadProgress(null);
75 | setImageUploadError(null);
76 | setFormData({ ...formData, image: downloadURL });
77 | });
78 | }
79 | );
80 | } catch (error) {
81 | setImageUploadError('Image upload failed');
82 | setImageUploadProgress(null);
83 | console.log(error);
84 | }
85 | };
86 | const handleSubmit = async (e) => {
87 | e.preventDefault();
88 | try {
89 | const res = await fetch(`/api/post/updatepost/${formData._id}/${currentUser._id}`, {
90 | method: 'PUT',
91 | headers: {
92 | 'Content-Type': 'application/json',
93 | },
94 | body: JSON.stringify(formData),
95 | });
96 | const data = await res.json();
97 | if (!res.ok) {
98 | setPublishError(data.message);
99 | return;
100 | }
101 |
102 | if (res.ok) {
103 | setPublishError(null);
104 | navigate(`/post/${data.slug}`);
105 | }
106 | } catch (error) {
107 | setPublishError('Something went wrong');
108 | }
109 | };
110 | return (
111 |
112 |
Update post
113 |
114 |
115 |
122 | setFormData({ ...formData, title: e.target.value })
123 | }
124 | value={formData.title}
125 | />
126 |
128 | setFormData({ ...formData, category: e.target.value })
129 | }
130 | value={formData.category}
131 | >
132 | Select a category
133 | JavaScript
134 | React.js
135 | Next.js
136 |
137 |
138 |
139 |
setFile(e.target.files[0])}
143 | />
144 |
152 | {imageUploadProgress ? (
153 |
154 |
158 |
159 | ) : (
160 | 'Upload Image'
161 | )}
162 |
163 |
164 | {imageUploadError && {imageUploadError} }
165 | {formData.image && (
166 |
171 | )}
172 | {
179 | setFormData({ ...formData, content: value });
180 | }}
181 | />
182 |
183 | Update post
184 |
185 | {publishError && (
186 |
187 | {publishError}
188 |
189 | )}
190 |
191 |
192 | );
193 | }
194 |
--------------------------------------------------------------------------------
/client/src/redux/store.js:
--------------------------------------------------------------------------------
1 | import { configureStore, combineReducers } from '@reduxjs/toolkit';
2 | import userReducer from './user/userSlice';
3 | import themeReducer from './theme/themeSlice';
4 | import { persistReducer, persistStore } from 'redux-persist';
5 | import storage from 'redux-persist/lib/storage';
6 |
7 | const rootReducer = combineReducers({
8 | user: userReducer,
9 | theme: themeReducer,
10 | });
11 |
12 | const persistConfig = {
13 | key: 'root',
14 | storage,
15 | version: 1,
16 | };
17 |
18 | const persistedReducer = persistReducer(persistConfig, rootReducer);
19 |
20 | export const store = configureStore({
21 | reducer: persistedReducer,
22 | middleware: (getDefaultMiddleware) =>
23 | getDefaultMiddleware({ serializableCheck: false }),
24 | });
25 |
26 | export const persistor = persistStore(store);
27 |
--------------------------------------------------------------------------------
/client/src/redux/theme/themeSlice.js:
--------------------------------------------------------------------------------
1 | import {createSlice} from '@reduxjs/toolkit';
2 |
3 | const initialState = {
4 | theme: 'light',
5 | };
6 |
7 | const themeSlice = createSlice({
8 | name: 'theme',
9 | initialState,
10 | reducers: {
11 | toggleTheme: (state) => {
12 | state.theme = state.theme === 'light' ? 'dark' : 'light';
13 | },
14 | }
15 | });
16 |
17 | export const {toggleTheme} = themeSlice.actions;
18 |
19 | export default themeSlice.reducer;
--------------------------------------------------------------------------------
/client/src/redux/user/userSlice.js:
--------------------------------------------------------------------------------
1 | import { createSlice } from '@reduxjs/toolkit';
2 |
3 | const initialState = {
4 | currentUser: null,
5 | error: null,
6 | loading: false,
7 | };
8 |
9 | const userSlice = createSlice({
10 | name: 'user',
11 | initialState,
12 | reducers: {
13 | signInStart: (state) => {
14 | state.loading = true;
15 | state.error = null;
16 | },
17 | signInSuccess: (state, action) => {
18 | state.currentUser = action.payload;
19 | state.loading = false;
20 | state.error = null;
21 | },
22 | signInFailure: (state, action) => {
23 | state.loading = false;
24 | state.error = action.payload;
25 | },
26 | updateStart: (state) => {
27 | state.loading = true;
28 | state.error = null;
29 | },
30 | updateSuccess: (state, action) => {
31 | state.currentUser = action.payload;
32 | state.loading = false;
33 | state.error = null;
34 | },
35 | updateFailure: (state, action) => {
36 | state.loading = false;
37 | state.error = action.payload;
38 | },
39 | deleteUserStart: (state) => {
40 | state.loading = true;
41 | state.error = null;
42 | },
43 | deleteUserSuccess: (state) => {
44 | state.currentUser = null;
45 | state.loading = false;
46 | state.error = null;
47 | },
48 | deleteUserFailure: (state, action) => {
49 | state.loading = false;
50 | state.error = action.payload;
51 | },
52 | signoutSuccess: (state) => {
53 | state.currentUser = null;
54 | state.error = null;
55 | state.loading = false;
56 | },
57 | },
58 | });
59 |
60 | export const {
61 | signInStart,
62 | signInSuccess,
63 | signInFailure,
64 | updateStart,
65 | updateSuccess,
66 | updateFailure,
67 | deleteUserStart,
68 | deleteUserSuccess,
69 | deleteUserFailure,
70 | signoutSuccess,
71 | } = userSlice.actions;
72 |
73 | export default userSlice.reducer;
74 |
--------------------------------------------------------------------------------
/client/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | import flowbite from 'flowbite/plugin';
3 | import tailwindScrollbar from 'tailwind-scrollbar';
4 |
5 | export default {
6 | content: [
7 | './index.html',
8 | './src/**/*.{js,ts,jsx,tsx}',
9 | 'node_modules/flowbite-react/**/*.{js,jsx,ts,tsx}',
10 | ],
11 | theme: {
12 | extend: {},
13 | },
14 | plugins: [flowbite, tailwindScrollbar],
15 | };
16 |
--------------------------------------------------------------------------------
/client/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite';
2 | import react from '@vitejs/plugin-react-swc';
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 | server: {
7 | proxy: {
8 | '/api': {
9 | target: 'http://localhost:3000',
10 | secure: false,
11 | },
12 | },
13 | },
14 | plugins: [react()],
15 | });
16 |
--------------------------------------------------------------------------------
/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mern-blog",
3 | "version": "1.0.0",
4 | "lockfileVersion": 3,
5 | "requires": true,
6 | "packages": {
7 | "": {
8 | "name": "mern-blog",
9 | "version": "1.0.0",
10 | "license": "ISC",
11 | "dependencies": {
12 | "bcryptjs": "^2.4.3",
13 | "cookie-parser": "^1.4.6",
14 | "dotenv": "^16.3.1",
15 | "express": "^4.18.2",
16 | "jsonwebtoken": "^9.0.2",
17 | "mongoose": "^8.0.2",
18 | "nodemon": "^3.0.2"
19 | }
20 | },
21 | "node_modules/@mongodb-js/saslprep": {
22 | "version": "1.1.1",
23 | "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.1.tgz",
24 | "integrity": "sha512-t7c5K033joZZMspnHg/gWPE4kandgc2OxE74aYOtGKfgB9VPuVJPix0H6fhmm2erj5PBJ21mqcx34lpIGtUCsQ==",
25 | "dependencies": {
26 | "sparse-bitfield": "^3.0.3"
27 | }
28 | },
29 | "node_modules/@types/node": {
30 | "version": "20.10.2",
31 | "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.2.tgz",
32 | "integrity": "sha512-37MXfxkb0vuIlRKHNxwCkb60PNBpR94u4efQuN4JgIAm66zfCDXGSAFCef9XUWFovX2R1ok6Z7MHhtdVXXkkIw==",
33 | "dependencies": {
34 | "undici-types": "~5.26.4"
35 | }
36 | },
37 | "node_modules/@types/webidl-conversions": {
38 | "version": "7.0.3",
39 | "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz",
40 | "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA=="
41 | },
42 | "node_modules/@types/whatwg-url": {
43 | "version": "8.2.2",
44 | "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-8.2.2.tgz",
45 | "integrity": "sha512-FtQu10RWgn3D9U4aazdwIE2yzphmTJREDqNdODHrbrZmmMqI0vMheC/6NE/J1Yveaj8H+ela+YwWTjq5PGmuhA==",
46 | "dependencies": {
47 | "@types/node": "*",
48 | "@types/webidl-conversions": "*"
49 | }
50 | },
51 | "node_modules/abbrev": {
52 | "version": "1.1.1",
53 | "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
54 | "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="
55 | },
56 | "node_modules/accepts": {
57 | "version": "1.3.8",
58 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
59 | "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
60 | "dependencies": {
61 | "mime-types": "~2.1.34",
62 | "negotiator": "0.6.3"
63 | },
64 | "engines": {
65 | "node": ">= 0.6"
66 | }
67 | },
68 | "node_modules/anymatch": {
69 | "version": "3.1.3",
70 | "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
71 | "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
72 | "dependencies": {
73 | "normalize-path": "^3.0.0",
74 | "picomatch": "^2.0.4"
75 | },
76 | "engines": {
77 | "node": ">= 8"
78 | }
79 | },
80 | "node_modules/array-flatten": {
81 | "version": "1.1.1",
82 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
83 | "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
84 | },
85 | "node_modules/balanced-match": {
86 | "version": "1.0.2",
87 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
88 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
89 | },
90 | "node_modules/bcryptjs": {
91 | "version": "2.4.3",
92 | "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz",
93 | "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ=="
94 | },
95 | "node_modules/binary-extensions": {
96 | "version": "2.2.0",
97 | "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
98 | "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
99 | "engines": {
100 | "node": ">=8"
101 | }
102 | },
103 | "node_modules/body-parser": {
104 | "version": "1.20.1",
105 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz",
106 | "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==",
107 | "dependencies": {
108 | "bytes": "3.1.2",
109 | "content-type": "~1.0.4",
110 | "debug": "2.6.9",
111 | "depd": "2.0.0",
112 | "destroy": "1.2.0",
113 | "http-errors": "2.0.0",
114 | "iconv-lite": "0.4.24",
115 | "on-finished": "2.4.1",
116 | "qs": "6.11.0",
117 | "raw-body": "2.5.1",
118 | "type-is": "~1.6.18",
119 | "unpipe": "1.0.0"
120 | },
121 | "engines": {
122 | "node": ">= 0.8",
123 | "npm": "1.2.8000 || >= 1.4.16"
124 | }
125 | },
126 | "node_modules/brace-expansion": {
127 | "version": "1.1.11",
128 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
129 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
130 | "dependencies": {
131 | "balanced-match": "^1.0.0",
132 | "concat-map": "0.0.1"
133 | }
134 | },
135 | "node_modules/braces": {
136 | "version": "3.0.2",
137 | "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
138 | "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
139 | "dependencies": {
140 | "fill-range": "^7.0.1"
141 | },
142 | "engines": {
143 | "node": ">=8"
144 | }
145 | },
146 | "node_modules/bson": {
147 | "version": "6.2.0",
148 | "resolved": "https://registry.npmjs.org/bson/-/bson-6.2.0.tgz",
149 | "integrity": "sha512-ID1cI+7bazPDyL9wYy9GaQ8gEEohWvcUl/Yf0dIdutJxnmInEEyCsb4awy/OiBfall7zBA179Pahi3vCdFze3Q==",
150 | "engines": {
151 | "node": ">=16.20.1"
152 | }
153 | },
154 | "node_modules/buffer-equal-constant-time": {
155 | "version": "1.0.1",
156 | "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
157 | "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="
158 | },
159 | "node_modules/bytes": {
160 | "version": "3.1.2",
161 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
162 | "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
163 | "engines": {
164 | "node": ">= 0.8"
165 | }
166 | },
167 | "node_modules/call-bind": {
168 | "version": "1.0.5",
169 | "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz",
170 | "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==",
171 | "dependencies": {
172 | "function-bind": "^1.1.2",
173 | "get-intrinsic": "^1.2.1",
174 | "set-function-length": "^1.1.1"
175 | },
176 | "funding": {
177 | "url": "https://github.com/sponsors/ljharb"
178 | }
179 | },
180 | "node_modules/chokidar": {
181 | "version": "3.5.3",
182 | "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
183 | "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
184 | "funding": [
185 | {
186 | "type": "individual",
187 | "url": "https://paulmillr.com/funding/"
188 | }
189 | ],
190 | "dependencies": {
191 | "anymatch": "~3.1.2",
192 | "braces": "~3.0.2",
193 | "glob-parent": "~5.1.2",
194 | "is-binary-path": "~2.1.0",
195 | "is-glob": "~4.0.1",
196 | "normalize-path": "~3.0.0",
197 | "readdirp": "~3.6.0"
198 | },
199 | "engines": {
200 | "node": ">= 8.10.0"
201 | },
202 | "optionalDependencies": {
203 | "fsevents": "~2.3.2"
204 | }
205 | },
206 | "node_modules/concat-map": {
207 | "version": "0.0.1",
208 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
209 | "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
210 | },
211 | "node_modules/content-disposition": {
212 | "version": "0.5.4",
213 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
214 | "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
215 | "dependencies": {
216 | "safe-buffer": "5.2.1"
217 | },
218 | "engines": {
219 | "node": ">= 0.6"
220 | }
221 | },
222 | "node_modules/content-type": {
223 | "version": "1.0.5",
224 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
225 | "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
226 | "engines": {
227 | "node": ">= 0.6"
228 | }
229 | },
230 | "node_modules/cookie": {
231 | "version": "0.5.0",
232 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
233 | "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==",
234 | "engines": {
235 | "node": ">= 0.6"
236 | }
237 | },
238 | "node_modules/cookie-parser": {
239 | "version": "1.4.6",
240 | "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.6.tgz",
241 | "integrity": "sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==",
242 | "dependencies": {
243 | "cookie": "0.4.1",
244 | "cookie-signature": "1.0.6"
245 | },
246 | "engines": {
247 | "node": ">= 0.8.0"
248 | }
249 | },
250 | "node_modules/cookie-parser/node_modules/cookie": {
251 | "version": "0.4.1",
252 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz",
253 | "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==",
254 | "engines": {
255 | "node": ">= 0.6"
256 | }
257 | },
258 | "node_modules/cookie-signature": {
259 | "version": "1.0.6",
260 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
261 | "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
262 | },
263 | "node_modules/debug": {
264 | "version": "2.6.9",
265 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
266 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
267 | "dependencies": {
268 | "ms": "2.0.0"
269 | }
270 | },
271 | "node_modules/define-data-property": {
272 | "version": "1.1.1",
273 | "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz",
274 | "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==",
275 | "dependencies": {
276 | "get-intrinsic": "^1.2.1",
277 | "gopd": "^1.0.1",
278 | "has-property-descriptors": "^1.0.0"
279 | },
280 | "engines": {
281 | "node": ">= 0.4"
282 | }
283 | },
284 | "node_modules/depd": {
285 | "version": "2.0.0",
286 | "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
287 | "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
288 | "engines": {
289 | "node": ">= 0.8"
290 | }
291 | },
292 | "node_modules/destroy": {
293 | "version": "1.2.0",
294 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
295 | "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
296 | "engines": {
297 | "node": ">= 0.8",
298 | "npm": "1.2.8000 || >= 1.4.16"
299 | }
300 | },
301 | "node_modules/dotenv": {
302 | "version": "16.3.1",
303 | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz",
304 | "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==",
305 | "engines": {
306 | "node": ">=12"
307 | },
308 | "funding": {
309 | "url": "https://github.com/motdotla/dotenv?sponsor=1"
310 | }
311 | },
312 | "node_modules/ecdsa-sig-formatter": {
313 | "version": "1.0.11",
314 | "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
315 | "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
316 | "dependencies": {
317 | "safe-buffer": "^5.0.1"
318 | }
319 | },
320 | "node_modules/ee-first": {
321 | "version": "1.1.1",
322 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
323 | "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
324 | },
325 | "node_modules/encodeurl": {
326 | "version": "1.0.2",
327 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
328 | "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
329 | "engines": {
330 | "node": ">= 0.8"
331 | }
332 | },
333 | "node_modules/escape-html": {
334 | "version": "1.0.3",
335 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
336 | "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
337 | },
338 | "node_modules/etag": {
339 | "version": "1.8.1",
340 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
341 | "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
342 | "engines": {
343 | "node": ">= 0.6"
344 | }
345 | },
346 | "node_modules/express": {
347 | "version": "4.18.2",
348 | "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
349 | "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==",
350 | "dependencies": {
351 | "accepts": "~1.3.8",
352 | "array-flatten": "1.1.1",
353 | "body-parser": "1.20.1",
354 | "content-disposition": "0.5.4",
355 | "content-type": "~1.0.4",
356 | "cookie": "0.5.0",
357 | "cookie-signature": "1.0.6",
358 | "debug": "2.6.9",
359 | "depd": "2.0.0",
360 | "encodeurl": "~1.0.2",
361 | "escape-html": "~1.0.3",
362 | "etag": "~1.8.1",
363 | "finalhandler": "1.2.0",
364 | "fresh": "0.5.2",
365 | "http-errors": "2.0.0",
366 | "merge-descriptors": "1.0.1",
367 | "methods": "~1.1.2",
368 | "on-finished": "2.4.1",
369 | "parseurl": "~1.3.3",
370 | "path-to-regexp": "0.1.7",
371 | "proxy-addr": "~2.0.7",
372 | "qs": "6.11.0",
373 | "range-parser": "~1.2.1",
374 | "safe-buffer": "5.2.1",
375 | "send": "0.18.0",
376 | "serve-static": "1.15.0",
377 | "setprototypeof": "1.2.0",
378 | "statuses": "2.0.1",
379 | "type-is": "~1.6.18",
380 | "utils-merge": "1.0.1",
381 | "vary": "~1.1.2"
382 | },
383 | "engines": {
384 | "node": ">= 0.10.0"
385 | }
386 | },
387 | "node_modules/fill-range": {
388 | "version": "7.0.1",
389 | "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
390 | "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
391 | "dependencies": {
392 | "to-regex-range": "^5.0.1"
393 | },
394 | "engines": {
395 | "node": ">=8"
396 | }
397 | },
398 | "node_modules/finalhandler": {
399 | "version": "1.2.0",
400 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
401 | "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==",
402 | "dependencies": {
403 | "debug": "2.6.9",
404 | "encodeurl": "~1.0.2",
405 | "escape-html": "~1.0.3",
406 | "on-finished": "2.4.1",
407 | "parseurl": "~1.3.3",
408 | "statuses": "2.0.1",
409 | "unpipe": "~1.0.0"
410 | },
411 | "engines": {
412 | "node": ">= 0.8"
413 | }
414 | },
415 | "node_modules/forwarded": {
416 | "version": "0.2.0",
417 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
418 | "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
419 | "engines": {
420 | "node": ">= 0.6"
421 | }
422 | },
423 | "node_modules/fresh": {
424 | "version": "0.5.2",
425 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
426 | "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
427 | "engines": {
428 | "node": ">= 0.6"
429 | }
430 | },
431 | "node_modules/fsevents": {
432 | "version": "2.3.3",
433 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
434 | "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
435 | "hasInstallScript": true,
436 | "optional": true,
437 | "os": [
438 | "darwin"
439 | ],
440 | "engines": {
441 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
442 | }
443 | },
444 | "node_modules/function-bind": {
445 | "version": "1.1.2",
446 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
447 | "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
448 | "funding": {
449 | "url": "https://github.com/sponsors/ljharb"
450 | }
451 | },
452 | "node_modules/get-intrinsic": {
453 | "version": "1.2.2",
454 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz",
455 | "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==",
456 | "dependencies": {
457 | "function-bind": "^1.1.2",
458 | "has-proto": "^1.0.1",
459 | "has-symbols": "^1.0.3",
460 | "hasown": "^2.0.0"
461 | },
462 | "funding": {
463 | "url": "https://github.com/sponsors/ljharb"
464 | }
465 | },
466 | "node_modules/glob-parent": {
467 | "version": "5.1.2",
468 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
469 | "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
470 | "dependencies": {
471 | "is-glob": "^4.0.1"
472 | },
473 | "engines": {
474 | "node": ">= 6"
475 | }
476 | },
477 | "node_modules/gopd": {
478 | "version": "1.0.1",
479 | "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
480 | "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
481 | "dependencies": {
482 | "get-intrinsic": "^1.1.3"
483 | },
484 | "funding": {
485 | "url": "https://github.com/sponsors/ljharb"
486 | }
487 | },
488 | "node_modules/has-flag": {
489 | "version": "3.0.0",
490 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
491 | "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
492 | "engines": {
493 | "node": ">=4"
494 | }
495 | },
496 | "node_modules/has-property-descriptors": {
497 | "version": "1.0.1",
498 | "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz",
499 | "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==",
500 | "dependencies": {
501 | "get-intrinsic": "^1.2.2"
502 | },
503 | "funding": {
504 | "url": "https://github.com/sponsors/ljharb"
505 | }
506 | },
507 | "node_modules/has-proto": {
508 | "version": "1.0.1",
509 | "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz",
510 | "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==",
511 | "engines": {
512 | "node": ">= 0.4"
513 | },
514 | "funding": {
515 | "url": "https://github.com/sponsors/ljharb"
516 | }
517 | },
518 | "node_modules/has-symbols": {
519 | "version": "1.0.3",
520 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
521 | "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
522 | "engines": {
523 | "node": ">= 0.4"
524 | },
525 | "funding": {
526 | "url": "https://github.com/sponsors/ljharb"
527 | }
528 | },
529 | "node_modules/hasown": {
530 | "version": "2.0.0",
531 | "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz",
532 | "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==",
533 | "dependencies": {
534 | "function-bind": "^1.1.2"
535 | },
536 | "engines": {
537 | "node": ">= 0.4"
538 | }
539 | },
540 | "node_modules/http-errors": {
541 | "version": "2.0.0",
542 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
543 | "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
544 | "dependencies": {
545 | "depd": "2.0.0",
546 | "inherits": "2.0.4",
547 | "setprototypeof": "1.2.0",
548 | "statuses": "2.0.1",
549 | "toidentifier": "1.0.1"
550 | },
551 | "engines": {
552 | "node": ">= 0.8"
553 | }
554 | },
555 | "node_modules/iconv-lite": {
556 | "version": "0.4.24",
557 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
558 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
559 | "dependencies": {
560 | "safer-buffer": ">= 2.1.2 < 3"
561 | },
562 | "engines": {
563 | "node": ">=0.10.0"
564 | }
565 | },
566 | "node_modules/ignore-by-default": {
567 | "version": "1.0.1",
568 | "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
569 | "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA=="
570 | },
571 | "node_modules/inherits": {
572 | "version": "2.0.4",
573 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
574 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
575 | },
576 | "node_modules/ipaddr.js": {
577 | "version": "1.9.1",
578 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
579 | "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
580 | "engines": {
581 | "node": ">= 0.10"
582 | }
583 | },
584 | "node_modules/is-binary-path": {
585 | "version": "2.1.0",
586 | "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
587 | "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
588 | "dependencies": {
589 | "binary-extensions": "^2.0.0"
590 | },
591 | "engines": {
592 | "node": ">=8"
593 | }
594 | },
595 | "node_modules/is-extglob": {
596 | "version": "2.1.1",
597 | "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
598 | "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
599 | "engines": {
600 | "node": ">=0.10.0"
601 | }
602 | },
603 | "node_modules/is-glob": {
604 | "version": "4.0.3",
605 | "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
606 | "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
607 | "dependencies": {
608 | "is-extglob": "^2.1.1"
609 | },
610 | "engines": {
611 | "node": ">=0.10.0"
612 | }
613 | },
614 | "node_modules/is-number": {
615 | "version": "7.0.0",
616 | "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
617 | "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
618 | "engines": {
619 | "node": ">=0.12.0"
620 | }
621 | },
622 | "node_modules/jsonwebtoken": {
623 | "version": "9.0.2",
624 | "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
625 | "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==",
626 | "dependencies": {
627 | "jws": "^3.2.2",
628 | "lodash.includes": "^4.3.0",
629 | "lodash.isboolean": "^3.0.3",
630 | "lodash.isinteger": "^4.0.4",
631 | "lodash.isnumber": "^3.0.3",
632 | "lodash.isplainobject": "^4.0.6",
633 | "lodash.isstring": "^4.0.1",
634 | "lodash.once": "^4.0.0",
635 | "ms": "^2.1.1",
636 | "semver": "^7.5.4"
637 | },
638 | "engines": {
639 | "node": ">=12",
640 | "npm": ">=6"
641 | }
642 | },
643 | "node_modules/jsonwebtoken/node_modules/ms": {
644 | "version": "2.1.3",
645 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
646 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
647 | },
648 | "node_modules/jwa": {
649 | "version": "1.4.1",
650 | "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
651 | "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==",
652 | "dependencies": {
653 | "buffer-equal-constant-time": "1.0.1",
654 | "ecdsa-sig-formatter": "1.0.11",
655 | "safe-buffer": "^5.0.1"
656 | }
657 | },
658 | "node_modules/jws": {
659 | "version": "3.2.2",
660 | "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
661 | "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
662 | "dependencies": {
663 | "jwa": "^1.4.1",
664 | "safe-buffer": "^5.0.1"
665 | }
666 | },
667 | "node_modules/kareem": {
668 | "version": "2.5.1",
669 | "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.5.1.tgz",
670 | "integrity": "sha512-7jFxRVm+jD+rkq3kY0iZDJfsO2/t4BBPeEb2qKn2lR/9KhuksYk5hxzfRYWMPV8P/x2d0kHD306YyWLzjjH+uA==",
671 | "engines": {
672 | "node": ">=12.0.0"
673 | }
674 | },
675 | "node_modules/lodash.includes": {
676 | "version": "4.3.0",
677 | "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
678 | "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="
679 | },
680 | "node_modules/lodash.isboolean": {
681 | "version": "3.0.3",
682 | "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
683 | "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="
684 | },
685 | "node_modules/lodash.isinteger": {
686 | "version": "4.0.4",
687 | "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
688 | "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="
689 | },
690 | "node_modules/lodash.isnumber": {
691 | "version": "3.0.3",
692 | "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
693 | "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw=="
694 | },
695 | "node_modules/lodash.isplainobject": {
696 | "version": "4.0.6",
697 | "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
698 | "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="
699 | },
700 | "node_modules/lodash.isstring": {
701 | "version": "4.0.1",
702 | "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
703 | "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="
704 | },
705 | "node_modules/lodash.once": {
706 | "version": "4.1.1",
707 | "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
708 | "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="
709 | },
710 | "node_modules/lru-cache": {
711 | "version": "6.0.0",
712 | "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
713 | "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
714 | "dependencies": {
715 | "yallist": "^4.0.0"
716 | },
717 | "engines": {
718 | "node": ">=10"
719 | }
720 | },
721 | "node_modules/media-typer": {
722 | "version": "0.3.0",
723 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
724 | "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
725 | "engines": {
726 | "node": ">= 0.6"
727 | }
728 | },
729 | "node_modules/memory-pager": {
730 | "version": "1.5.0",
731 | "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz",
732 | "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg=="
733 | },
734 | "node_modules/merge-descriptors": {
735 | "version": "1.0.1",
736 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
737 | "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w=="
738 | },
739 | "node_modules/methods": {
740 | "version": "1.1.2",
741 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
742 | "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
743 | "engines": {
744 | "node": ">= 0.6"
745 | }
746 | },
747 | "node_modules/mime": {
748 | "version": "1.6.0",
749 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
750 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
751 | "bin": {
752 | "mime": "cli.js"
753 | },
754 | "engines": {
755 | "node": ">=4"
756 | }
757 | },
758 | "node_modules/mime-db": {
759 | "version": "1.52.0",
760 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
761 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
762 | "engines": {
763 | "node": ">= 0.6"
764 | }
765 | },
766 | "node_modules/mime-types": {
767 | "version": "2.1.35",
768 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
769 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
770 | "dependencies": {
771 | "mime-db": "1.52.0"
772 | },
773 | "engines": {
774 | "node": ">= 0.6"
775 | }
776 | },
777 | "node_modules/minimatch": {
778 | "version": "3.1.2",
779 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
780 | "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
781 | "dependencies": {
782 | "brace-expansion": "^1.1.7"
783 | },
784 | "engines": {
785 | "node": "*"
786 | }
787 | },
788 | "node_modules/mongodb": {
789 | "version": "6.2.0",
790 | "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.2.0.tgz",
791 | "integrity": "sha512-d7OSuGjGWDZ5usZPqfvb36laQ9CPhnWkAGHT61x5P95p/8nMVeH8asloMwW6GcYFeB0Vj4CB/1wOTDG2RA9BFA==",
792 | "dependencies": {
793 | "@mongodb-js/saslprep": "^1.1.0",
794 | "bson": "^6.2.0",
795 | "mongodb-connection-string-url": "^2.6.0"
796 | },
797 | "engines": {
798 | "node": ">=16.20.1"
799 | },
800 | "peerDependencies": {
801 | "@aws-sdk/credential-providers": "^3.188.0",
802 | "@mongodb-js/zstd": "^1.1.0",
803 | "gcp-metadata": "^5.2.0",
804 | "kerberos": "^2.0.1",
805 | "mongodb-client-encryption": ">=6.0.0 <7",
806 | "snappy": "^7.2.2",
807 | "socks": "^2.7.1"
808 | },
809 | "peerDependenciesMeta": {
810 | "@aws-sdk/credential-providers": {
811 | "optional": true
812 | },
813 | "@mongodb-js/zstd": {
814 | "optional": true
815 | },
816 | "gcp-metadata": {
817 | "optional": true
818 | },
819 | "kerberos": {
820 | "optional": true
821 | },
822 | "mongodb-client-encryption": {
823 | "optional": true
824 | },
825 | "snappy": {
826 | "optional": true
827 | },
828 | "socks": {
829 | "optional": true
830 | }
831 | }
832 | },
833 | "node_modules/mongodb-connection-string-url": {
834 | "version": "2.6.0",
835 | "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-2.6.0.tgz",
836 | "integrity": "sha512-WvTZlI9ab0QYtTYnuMLgobULWhokRjtC7db9LtcVfJ+Hsnyr5eo6ZtNAt3Ly24XZScGMelOcGtm7lSn0332tPQ==",
837 | "dependencies": {
838 | "@types/whatwg-url": "^8.2.1",
839 | "whatwg-url": "^11.0.0"
840 | }
841 | },
842 | "node_modules/mongoose": {
843 | "version": "8.0.2",
844 | "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.0.2.tgz",
845 | "integrity": "sha512-Vsi9GzTXjdBVzheT1HZOZ2jHNzzR9Xwb5OyLz/FvDEAhlwrRnXnuqJf0QHINUOQSm7aoyvnPks0q85HJkd6yDw==",
846 | "dependencies": {
847 | "bson": "^6.2.0",
848 | "kareem": "2.5.1",
849 | "mongodb": "6.2.0",
850 | "mpath": "0.9.0",
851 | "mquery": "5.0.0",
852 | "ms": "2.1.3",
853 | "sift": "16.0.1"
854 | },
855 | "engines": {
856 | "node": ">=16.20.1"
857 | },
858 | "funding": {
859 | "type": "opencollective",
860 | "url": "https://opencollective.com/mongoose"
861 | }
862 | },
863 | "node_modules/mongoose/node_modules/ms": {
864 | "version": "2.1.3",
865 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
866 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
867 | },
868 | "node_modules/mpath": {
869 | "version": "0.9.0",
870 | "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz",
871 | "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==",
872 | "engines": {
873 | "node": ">=4.0.0"
874 | }
875 | },
876 | "node_modules/mquery": {
877 | "version": "5.0.0",
878 | "resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz",
879 | "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==",
880 | "dependencies": {
881 | "debug": "4.x"
882 | },
883 | "engines": {
884 | "node": ">=14.0.0"
885 | }
886 | },
887 | "node_modules/mquery/node_modules/debug": {
888 | "version": "4.3.4",
889 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
890 | "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
891 | "dependencies": {
892 | "ms": "2.1.2"
893 | },
894 | "engines": {
895 | "node": ">=6.0"
896 | },
897 | "peerDependenciesMeta": {
898 | "supports-color": {
899 | "optional": true
900 | }
901 | }
902 | },
903 | "node_modules/mquery/node_modules/ms": {
904 | "version": "2.1.2",
905 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
906 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
907 | },
908 | "node_modules/ms": {
909 | "version": "2.0.0",
910 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
911 | "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
912 | },
913 | "node_modules/negotiator": {
914 | "version": "0.6.3",
915 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
916 | "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
917 | "engines": {
918 | "node": ">= 0.6"
919 | }
920 | },
921 | "node_modules/nodemon": {
922 | "version": "3.0.2",
923 | "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.0.2.tgz",
924 | "integrity": "sha512-9qIN2LNTrEzpOPBaWHTm4Asy1LxXLSickZStAQ4IZe7zsoIpD/A7LWxhZV3t4Zu352uBcqVnRsDXSMR2Sc3lTA==",
925 | "dependencies": {
926 | "chokidar": "^3.5.2",
927 | "debug": "^4",
928 | "ignore-by-default": "^1.0.1",
929 | "minimatch": "^3.1.2",
930 | "pstree.remy": "^1.1.8",
931 | "semver": "^7.5.3",
932 | "simple-update-notifier": "^2.0.0",
933 | "supports-color": "^5.5.0",
934 | "touch": "^3.1.0",
935 | "undefsafe": "^2.0.5"
936 | },
937 | "bin": {
938 | "nodemon": "bin/nodemon.js"
939 | },
940 | "engines": {
941 | "node": ">=10"
942 | },
943 | "funding": {
944 | "type": "opencollective",
945 | "url": "https://opencollective.com/nodemon"
946 | }
947 | },
948 | "node_modules/nodemon/node_modules/debug": {
949 | "version": "4.3.4",
950 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
951 | "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
952 | "dependencies": {
953 | "ms": "2.1.2"
954 | },
955 | "engines": {
956 | "node": ">=6.0"
957 | },
958 | "peerDependenciesMeta": {
959 | "supports-color": {
960 | "optional": true
961 | }
962 | }
963 | },
964 | "node_modules/nodemon/node_modules/ms": {
965 | "version": "2.1.2",
966 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
967 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
968 | },
969 | "node_modules/nopt": {
970 | "version": "1.0.10",
971 | "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz",
972 | "integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==",
973 | "dependencies": {
974 | "abbrev": "1"
975 | },
976 | "bin": {
977 | "nopt": "bin/nopt.js"
978 | },
979 | "engines": {
980 | "node": "*"
981 | }
982 | },
983 | "node_modules/normalize-path": {
984 | "version": "3.0.0",
985 | "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
986 | "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
987 | "engines": {
988 | "node": ">=0.10.0"
989 | }
990 | },
991 | "node_modules/object-inspect": {
992 | "version": "1.13.1",
993 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz",
994 | "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==",
995 | "funding": {
996 | "url": "https://github.com/sponsors/ljharb"
997 | }
998 | },
999 | "node_modules/on-finished": {
1000 | "version": "2.4.1",
1001 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
1002 | "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
1003 | "dependencies": {
1004 | "ee-first": "1.1.1"
1005 | },
1006 | "engines": {
1007 | "node": ">= 0.8"
1008 | }
1009 | },
1010 | "node_modules/parseurl": {
1011 | "version": "1.3.3",
1012 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
1013 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
1014 | "engines": {
1015 | "node": ">= 0.8"
1016 | }
1017 | },
1018 | "node_modules/path-to-regexp": {
1019 | "version": "0.1.7",
1020 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
1021 | "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
1022 | },
1023 | "node_modules/picomatch": {
1024 | "version": "2.3.1",
1025 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
1026 | "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
1027 | "engines": {
1028 | "node": ">=8.6"
1029 | },
1030 | "funding": {
1031 | "url": "https://github.com/sponsors/jonschlinkert"
1032 | }
1033 | },
1034 | "node_modules/proxy-addr": {
1035 | "version": "2.0.7",
1036 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
1037 | "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
1038 | "dependencies": {
1039 | "forwarded": "0.2.0",
1040 | "ipaddr.js": "1.9.1"
1041 | },
1042 | "engines": {
1043 | "node": ">= 0.10"
1044 | }
1045 | },
1046 | "node_modules/pstree.remy": {
1047 | "version": "1.1.8",
1048 | "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
1049 | "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w=="
1050 | },
1051 | "node_modules/punycode": {
1052 | "version": "2.3.1",
1053 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
1054 | "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
1055 | "engines": {
1056 | "node": ">=6"
1057 | }
1058 | },
1059 | "node_modules/qs": {
1060 | "version": "6.11.0",
1061 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
1062 | "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
1063 | "dependencies": {
1064 | "side-channel": "^1.0.4"
1065 | },
1066 | "engines": {
1067 | "node": ">=0.6"
1068 | },
1069 | "funding": {
1070 | "url": "https://github.com/sponsors/ljharb"
1071 | }
1072 | },
1073 | "node_modules/range-parser": {
1074 | "version": "1.2.1",
1075 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
1076 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
1077 | "engines": {
1078 | "node": ">= 0.6"
1079 | }
1080 | },
1081 | "node_modules/raw-body": {
1082 | "version": "2.5.1",
1083 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz",
1084 | "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==",
1085 | "dependencies": {
1086 | "bytes": "3.1.2",
1087 | "http-errors": "2.0.0",
1088 | "iconv-lite": "0.4.24",
1089 | "unpipe": "1.0.0"
1090 | },
1091 | "engines": {
1092 | "node": ">= 0.8"
1093 | }
1094 | },
1095 | "node_modules/readdirp": {
1096 | "version": "3.6.0",
1097 | "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
1098 | "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
1099 | "dependencies": {
1100 | "picomatch": "^2.2.1"
1101 | },
1102 | "engines": {
1103 | "node": ">=8.10.0"
1104 | }
1105 | },
1106 | "node_modules/safe-buffer": {
1107 | "version": "5.2.1",
1108 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
1109 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
1110 | "funding": [
1111 | {
1112 | "type": "github",
1113 | "url": "https://github.com/sponsors/feross"
1114 | },
1115 | {
1116 | "type": "patreon",
1117 | "url": "https://www.patreon.com/feross"
1118 | },
1119 | {
1120 | "type": "consulting",
1121 | "url": "https://feross.org/support"
1122 | }
1123 | ]
1124 | },
1125 | "node_modules/safer-buffer": {
1126 | "version": "2.1.2",
1127 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
1128 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
1129 | },
1130 | "node_modules/semver": {
1131 | "version": "7.5.4",
1132 | "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
1133 | "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
1134 | "dependencies": {
1135 | "lru-cache": "^6.0.0"
1136 | },
1137 | "bin": {
1138 | "semver": "bin/semver.js"
1139 | },
1140 | "engines": {
1141 | "node": ">=10"
1142 | }
1143 | },
1144 | "node_modules/send": {
1145 | "version": "0.18.0",
1146 | "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
1147 | "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
1148 | "dependencies": {
1149 | "debug": "2.6.9",
1150 | "depd": "2.0.0",
1151 | "destroy": "1.2.0",
1152 | "encodeurl": "~1.0.2",
1153 | "escape-html": "~1.0.3",
1154 | "etag": "~1.8.1",
1155 | "fresh": "0.5.2",
1156 | "http-errors": "2.0.0",
1157 | "mime": "1.6.0",
1158 | "ms": "2.1.3",
1159 | "on-finished": "2.4.1",
1160 | "range-parser": "~1.2.1",
1161 | "statuses": "2.0.1"
1162 | },
1163 | "engines": {
1164 | "node": ">= 0.8.0"
1165 | }
1166 | },
1167 | "node_modules/send/node_modules/ms": {
1168 | "version": "2.1.3",
1169 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
1170 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
1171 | },
1172 | "node_modules/serve-static": {
1173 | "version": "1.15.0",
1174 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
1175 | "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
1176 | "dependencies": {
1177 | "encodeurl": "~1.0.2",
1178 | "escape-html": "~1.0.3",
1179 | "parseurl": "~1.3.3",
1180 | "send": "0.18.0"
1181 | },
1182 | "engines": {
1183 | "node": ">= 0.8.0"
1184 | }
1185 | },
1186 | "node_modules/set-function-length": {
1187 | "version": "1.1.1",
1188 | "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz",
1189 | "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==",
1190 | "dependencies": {
1191 | "define-data-property": "^1.1.1",
1192 | "get-intrinsic": "^1.2.1",
1193 | "gopd": "^1.0.1",
1194 | "has-property-descriptors": "^1.0.0"
1195 | },
1196 | "engines": {
1197 | "node": ">= 0.4"
1198 | }
1199 | },
1200 | "node_modules/setprototypeof": {
1201 | "version": "1.2.0",
1202 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
1203 | "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
1204 | },
1205 | "node_modules/side-channel": {
1206 | "version": "1.0.4",
1207 | "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
1208 | "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
1209 | "dependencies": {
1210 | "call-bind": "^1.0.0",
1211 | "get-intrinsic": "^1.0.2",
1212 | "object-inspect": "^1.9.0"
1213 | },
1214 | "funding": {
1215 | "url": "https://github.com/sponsors/ljharb"
1216 | }
1217 | },
1218 | "node_modules/sift": {
1219 | "version": "16.0.1",
1220 | "resolved": "https://registry.npmjs.org/sift/-/sift-16.0.1.tgz",
1221 | "integrity": "sha512-Wv6BjQ5zbhW7VFefWusVP33T/EM0vYikCaQ2qR8yULbsilAT8/wQaXvuQ3ptGLpoKx+lihJE3y2UTgKDyyNHZQ=="
1222 | },
1223 | "node_modules/simple-update-notifier": {
1224 | "version": "2.0.0",
1225 | "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
1226 | "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==",
1227 | "dependencies": {
1228 | "semver": "^7.5.3"
1229 | },
1230 | "engines": {
1231 | "node": ">=10"
1232 | }
1233 | },
1234 | "node_modules/sparse-bitfield": {
1235 | "version": "3.0.3",
1236 | "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz",
1237 | "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==",
1238 | "dependencies": {
1239 | "memory-pager": "^1.0.2"
1240 | }
1241 | },
1242 | "node_modules/statuses": {
1243 | "version": "2.0.1",
1244 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
1245 | "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
1246 | "engines": {
1247 | "node": ">= 0.8"
1248 | }
1249 | },
1250 | "node_modules/supports-color": {
1251 | "version": "5.5.0",
1252 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
1253 | "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
1254 | "dependencies": {
1255 | "has-flag": "^3.0.0"
1256 | },
1257 | "engines": {
1258 | "node": ">=4"
1259 | }
1260 | },
1261 | "node_modules/to-regex-range": {
1262 | "version": "5.0.1",
1263 | "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
1264 | "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
1265 | "dependencies": {
1266 | "is-number": "^7.0.0"
1267 | },
1268 | "engines": {
1269 | "node": ">=8.0"
1270 | }
1271 | },
1272 | "node_modules/toidentifier": {
1273 | "version": "1.0.1",
1274 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
1275 | "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
1276 | "engines": {
1277 | "node": ">=0.6"
1278 | }
1279 | },
1280 | "node_modules/touch": {
1281 | "version": "3.1.0",
1282 | "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz",
1283 | "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==",
1284 | "dependencies": {
1285 | "nopt": "~1.0.10"
1286 | },
1287 | "bin": {
1288 | "nodetouch": "bin/nodetouch.js"
1289 | }
1290 | },
1291 | "node_modules/tr46": {
1292 | "version": "3.0.0",
1293 | "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz",
1294 | "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==",
1295 | "dependencies": {
1296 | "punycode": "^2.1.1"
1297 | },
1298 | "engines": {
1299 | "node": ">=12"
1300 | }
1301 | },
1302 | "node_modules/type-is": {
1303 | "version": "1.6.18",
1304 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
1305 | "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
1306 | "dependencies": {
1307 | "media-typer": "0.3.0",
1308 | "mime-types": "~2.1.24"
1309 | },
1310 | "engines": {
1311 | "node": ">= 0.6"
1312 | }
1313 | },
1314 | "node_modules/undefsafe": {
1315 | "version": "2.0.5",
1316 | "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
1317 | "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA=="
1318 | },
1319 | "node_modules/undici-types": {
1320 | "version": "5.26.5",
1321 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
1322 | "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
1323 | },
1324 | "node_modules/unpipe": {
1325 | "version": "1.0.0",
1326 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
1327 | "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
1328 | "engines": {
1329 | "node": ">= 0.8"
1330 | }
1331 | },
1332 | "node_modules/utils-merge": {
1333 | "version": "1.0.1",
1334 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
1335 | "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
1336 | "engines": {
1337 | "node": ">= 0.4.0"
1338 | }
1339 | },
1340 | "node_modules/vary": {
1341 | "version": "1.1.2",
1342 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
1343 | "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
1344 | "engines": {
1345 | "node": ">= 0.8"
1346 | }
1347 | },
1348 | "node_modules/webidl-conversions": {
1349 | "version": "7.0.0",
1350 | "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
1351 | "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
1352 | "engines": {
1353 | "node": ">=12"
1354 | }
1355 | },
1356 | "node_modules/whatwg-url": {
1357 | "version": "11.0.0",
1358 | "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz",
1359 | "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==",
1360 | "dependencies": {
1361 | "tr46": "^3.0.0",
1362 | "webidl-conversions": "^7.0.0"
1363 | },
1364 | "engines": {
1365 | "node": ">=12"
1366 | }
1367 | },
1368 | "node_modules/yallist": {
1369 | "version": "4.0.0",
1370 | "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
1371 | "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
1372 | }
1373 | }
1374 | }
1375 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mern-blog",
3 | "version": "1.0.0",
4 | "description": "",
5 | "type": "module",
6 | "main": "index.js",
7 | "scripts": {
8 | "dev": "nodemon api/index.js",
9 | "start": "node api/index.js",
10 | "build": "npm install && npm install --prefix client && npm run build --prefix client"
11 | },
12 | "keywords": [],
13 | "author": "",
14 | "license": "ISC",
15 | "dependencies": {
16 | "bcryptjs": "^2.4.3",
17 | "cookie-parser": "^1.4.6",
18 | "dotenv": "^16.3.1",
19 | "express": "^4.18.2",
20 | "jsonwebtoken": "^9.0.2",
21 | "mongoose": "^8.0.2",
22 | "nodemon": "^3.0.2"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------