├── client
├── vercel.json
├── public
│ ├── favicon.ico
│ ├── favicon-16x16.png
│ ├── favicon-32x32.png
│ ├── apple-touch-icon.png
│ ├── android-chrome-192x192.png
│ ├── android-chrome-512x512.png
│ └── site.webmanifest
├── src
│ ├── assets
│ │ ├── blogs.png
│ │ ├── home.png
│ │ ├── logo.jpg
│ │ ├── logo.webp
│ │ ├── users.png
│ │ ├── contact.png
│ │ ├── mockup.png
│ │ ├── profile.png
│ │ ├── recipes.png
│ │ ├── signin.png
│ │ ├── signup.png
│ │ ├── add-blog.png
│ │ ├── add-recipe.png
│ │ ├── logo_nobg.png
│ │ ├── logo_nobg.webp
│ │ ├── auth-banner.webp
│ │ ├── mockup-nobg.png
│ │ ├── single-blog.png
│ │ ├── blog-dashboard.png
│ │ ├── single-recipe.png
│ │ ├── recipes-dashboard.png
│ │ ├── index.js
│ │ ├── photo.svg
│ │ ├── componentLoading.json
│ │ └── profile_details.svg
│ ├── common
│ │ ├── roles.js
│ │ ├── dateFormat.js
│ │ └── uploadImage.js
│ ├── layouts
│ │ ├── index.js
│ │ ├── RootLayout.jsx
│ │ └── DashboardLayout.jsx
│ ├── components
│ │ ├── noData
│ │ │ └── NoData.jsx
│ │ ├── scrollToTop
│ │ │ └── ScrollToTop.jsx
│ │ ├── dashboard
│ │ │ ├── Table.jsx
│ │ │ └── Sidebar.jsx
│ │ ├── loading
│ │ │ ├── PageLoading.jsx
│ │ │ └── ComponentLoading.jsx
│ │ ├── logo
│ │ │ └── Logo.jsx
│ │ ├── backToTop
│ │ │ └── BackToTop.jsx
│ │ ├── index.js
│ │ ├── hero
│ │ │ └── Hero.jsx
│ │ ├── homeCategories
│ │ │ └── HomeCategories.jsx
│ │ ├── shareButton
│ │ │ └── ShareButton.jsx
│ │ ├── subscribe
│ │ │ └── Subscribe.jsx
│ │ ├── cards
│ │ │ ├── AllCards.jsx
│ │ │ ├── SubscribeCard.jsx
│ │ │ └── SingleCard.jsx
│ │ ├── input
│ │ │ └── Input.jsx
│ │ ├── button
│ │ │ └── Button.jsx
│ │ ├── comment
│ │ │ └── Comment.jsx
│ │ ├── header
│ │ │ ├── Header.jsx
│ │ │ └── Menu.jsx
│ │ ├── footer
│ │ │ └── Footer.jsx
│ │ └── avatar
│ │ │ └── Avatar.jsx
│ ├── pages
│ │ ├── message
│ │ │ ├── Error.jsx
│ │ │ ├── CheckoutFailure.jsx
│ │ │ ├── CheckoutSuccess.jsx
│ │ │ └── Message.jsx
│ │ ├── recipe
│ │ │ ├── SavedRecipes.jsx
│ │ │ ├── MyRecipes.jsx
│ │ │ └── Recipe.jsx
│ │ ├── home
│ │ │ └── Home.jsx
│ │ ├── blogs
│ │ │ ├── MyBlogs.jsx
│ │ │ ├── Blogs.jsx
│ │ │ ├── AddBlog.jsx
│ │ │ ├── EditBlog.jsx
│ │ │ └── SingleBlog.jsx
│ │ ├── index.js
│ │ ├── dashboard
│ │ │ ├── DashboardBlogs.jsx
│ │ │ ├── DashboardRecipes.jsx
│ │ │ └── Users.jsx
│ │ ├── auth
│ │ │ ├── SignIn.jsx
│ │ │ └── SignUp.jsx
│ │ ├── profile
│ │ │ └── Profile.jsx
│ │ └── contact
│ │ │ └── Contact.jsx
│ ├── hooks
│ │ ├── useTitle.jsx
│ │ └── useAuth.jsx
│ ├── features
│ │ ├── blog
│ │ │ ├── blogSlice.js
│ │ │ └── blogApiSlice.js
│ │ ├── user
│ │ │ ├── userSlice.js
│ │ │ └── userApiSlice.js
│ │ ├── recipe
│ │ │ ├── recipeSlice.js
│ │ │ └── recipeApiSlice.js
│ │ └── auth
│ │ │ ├── authSlice.js
│ │ │ ├── PersistLogin.jsx
│ │ │ ├── RequireAuth.jsx
│ │ │ └── authApiSlice.js
│ ├── redux
│ │ ├── store.js
│ │ └── apiSlice.js
│ ├── main.jsx
│ ├── index.css
│ └── App.jsx
├── postcss.config.js
├── .env.example
├── vite.config.js
├── .gitignore
├── README.md
├── .eslintrc.cjs
├── tailwind.config.js
├── package.json
└── index.html
├── .gitignore
├── server
├── .env.example
├── config
│ ├── rolesList.js
│ ├── allowedOrigins.js
│ └── corsOptions.js
├── db
│ └── conn.js
├── middleware
│ ├── errorHandler.js
│ ├── credentials.js
│ ├── verifyRoles.js
│ ├── loginLimit.js
│ └── verifyJwt.js
├── routes
│ ├── authRoutes.js
│ ├── subscriptionRoutes.js
│ ├── userRoutes.js
│ ├── blogRoutes.js
│ └── recipeRoutes.js
├── package.json
├── models
│ ├── userModel.js
│ ├── blogModel.js
│ └── recipeModel.js
├── index.js
└── controllers
│ ├── subscriptionController.js
│ ├── userController.js
│ ├── blogController.js
│ ├── authController.js
│ └── recipeController.js
└── LICENSE
/client/vercel.json:
--------------------------------------------------------------------------------
1 | { "routes": [{ "src": "/[^.]+", "dest": "/", "status": 200 }] }
2 |
--------------------------------------------------------------------------------
/client/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Avinash905/Recipen/HEAD/client/public/favicon.ico
--------------------------------------------------------------------------------
/client/src/assets/blogs.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Avinash905/Recipen/HEAD/client/src/assets/blogs.png
--------------------------------------------------------------------------------
/client/src/assets/home.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Avinash905/Recipen/HEAD/client/src/assets/home.png
--------------------------------------------------------------------------------
/client/src/assets/logo.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Avinash905/Recipen/HEAD/client/src/assets/logo.jpg
--------------------------------------------------------------------------------
/client/src/assets/logo.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Avinash905/Recipen/HEAD/client/src/assets/logo.webp
--------------------------------------------------------------------------------
/client/src/assets/users.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Avinash905/Recipen/HEAD/client/src/assets/users.png
--------------------------------------------------------------------------------
/client/src/assets/contact.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Avinash905/Recipen/HEAD/client/src/assets/contact.png
--------------------------------------------------------------------------------
/client/src/assets/mockup.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Avinash905/Recipen/HEAD/client/src/assets/mockup.png
--------------------------------------------------------------------------------
/client/src/assets/profile.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Avinash905/Recipen/HEAD/client/src/assets/profile.png
--------------------------------------------------------------------------------
/client/src/assets/recipes.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Avinash905/Recipen/HEAD/client/src/assets/recipes.png
--------------------------------------------------------------------------------
/client/src/assets/signin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Avinash905/Recipen/HEAD/client/src/assets/signin.png
--------------------------------------------------------------------------------
/client/src/assets/signup.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Avinash905/Recipen/HEAD/client/src/assets/signup.png
--------------------------------------------------------------------------------
/client/public/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Avinash905/Recipen/HEAD/client/public/favicon-16x16.png
--------------------------------------------------------------------------------
/client/public/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Avinash905/Recipen/HEAD/client/public/favicon-32x32.png
--------------------------------------------------------------------------------
/client/src/assets/add-blog.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Avinash905/Recipen/HEAD/client/src/assets/add-blog.png
--------------------------------------------------------------------------------
/client/src/assets/add-recipe.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Avinash905/Recipen/HEAD/client/src/assets/add-recipe.png
--------------------------------------------------------------------------------
/client/src/assets/logo_nobg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Avinash905/Recipen/HEAD/client/src/assets/logo_nobg.png
--------------------------------------------------------------------------------
/client/src/assets/logo_nobg.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Avinash905/Recipen/HEAD/client/src/assets/logo_nobg.webp
--------------------------------------------------------------------------------
/client/public/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Avinash905/Recipen/HEAD/client/public/apple-touch-icon.png
--------------------------------------------------------------------------------
/client/src/assets/auth-banner.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Avinash905/Recipen/HEAD/client/src/assets/auth-banner.webp
--------------------------------------------------------------------------------
/client/src/assets/mockup-nobg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Avinash905/Recipen/HEAD/client/src/assets/mockup-nobg.png
--------------------------------------------------------------------------------
/client/src/assets/single-blog.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Avinash905/Recipen/HEAD/client/src/assets/single-blog.png
--------------------------------------------------------------------------------
/client/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/client/src/assets/blog-dashboard.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Avinash905/Recipen/HEAD/client/src/assets/blog-dashboard.png
--------------------------------------------------------------------------------
/client/src/assets/single-recipe.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Avinash905/Recipen/HEAD/client/src/assets/single-recipe.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.vscode
2 | /client/node_modules
3 | /server/node_modules
4 | /server/.env
5 | /client/.env
6 | /client/src/common/data.js
--------------------------------------------------------------------------------
/client/src/assets/recipes-dashboard.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Avinash905/Recipen/HEAD/client/src/assets/recipes-dashboard.png
--------------------------------------------------------------------------------
/client/public/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Avinash905/Recipen/HEAD/client/public/android-chrome-192x192.png
--------------------------------------------------------------------------------
/client/public/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Avinash905/Recipen/HEAD/client/public/android-chrome-512x512.png
--------------------------------------------------------------------------------
/client/src/common/roles.js:
--------------------------------------------------------------------------------
1 | const ROLES = {
2 | Admin: "Admin",
3 | BasicUser: "BasicUser",
4 | ProUser: "ProUser",
5 | };
6 |
7 | export default ROLES;
8 |
--------------------------------------------------------------------------------
/client/src/layouts/index.js:
--------------------------------------------------------------------------------
1 | import RootLayout from "./RootLayout";
2 | import DashboardLayout from "./DashboardLayout";
3 |
4 | export { RootLayout, DashboardLayout };
5 |
--------------------------------------------------------------------------------
/server/.env.example:
--------------------------------------------------------------------------------
1 | PORT=5000
2 | CLIENT_BASE_URL=http://localhost:5173
3 | MONGODB_URI=
4 | ACCESS_TOKEN_SECRET=
5 | REFRESH_TOKEN_SECRET=
6 | STRIPE_KEY=
7 | STRIPE_PRICE_ID=
--------------------------------------------------------------------------------
/server/config/rolesList.js:
--------------------------------------------------------------------------------
1 | const ROLES_LIST = {
2 | Admin: "Admin",
3 | BasicUser: "BasicUser",
4 | ProUser: "ProUser",
5 | };
6 |
7 | module.exports = ROLES_LIST;
8 |
--------------------------------------------------------------------------------
/server/config/allowedOrigins.js:
--------------------------------------------------------------------------------
1 | const allowedOrigins = [
2 | "https://recipen.vercel.app",
3 | "https://recipen-backend.onrender.com",
4 | ];
5 |
6 | module.exports = allowedOrigins;
7 |
--------------------------------------------------------------------------------
/client/.env.example:
--------------------------------------------------------------------------------
1 | VITE_BASE_URL=http://localhost:5173
2 | VITE_SERVER_BASE_URL=http://localhost:5000/api
3 | VITE_FORMIK_SECRET=
4 | VITE_CLOUDINARY_PRESET=
5 | VITE_CLOUDINARY_CLOUD_NAME=
6 | VITE_CLOUDINARY_BASE_URL=
--------------------------------------------------------------------------------
/client/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import react from '@vitejs/plugin-react'
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 | plugins: [react({
7 | // Add this line
8 | include: "**/*.jsx",
9 | })]
10 | })
--------------------------------------------------------------------------------
/client/public/site.webmanifest:
--------------------------------------------------------------------------------
1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
--------------------------------------------------------------------------------
/client/src/components/noData/NoData.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const NoData = ({ text }) => {
4 | return (
5 |
6 | No {text} Found
7 |
8 | );
9 | };
10 |
11 | export default NoData;
12 |
--------------------------------------------------------------------------------
/client/src/pages/message/Error.jsx:
--------------------------------------------------------------------------------
1 | import { errorAnimation } from "../../assets";
2 | import Message from "./Message";
3 |
4 | const Error = () => {
5 | return (
6 |
10 | );
11 | };
12 |
13 | export default Error;
14 |
--------------------------------------------------------------------------------
/server/db/conn.js:
--------------------------------------------------------------------------------
1 | const mongoose = require("mongoose");
2 | mongoose.set("strictQuery", false);
3 |
4 | const connectDB = () => {
5 | return mongoose.connect(process.env.MONGODB_URI, {
6 | useUnifiedTopology: true,
7 | useNewUrlParser: true,
8 | });
9 | };
10 |
11 | module.exports = connectDB;
12 |
--------------------------------------------------------------------------------
/client/src/hooks/useTitle.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react";
2 |
3 | const useTitle = (title) => {
4 | useEffect(() => {
5 | const prevTitle = document.title;
6 | document.title = title;
7 |
8 | return () => (document.title = prevTitle);
9 | }, [title]);
10 | };
11 |
12 | export default useTitle;
13 |
--------------------------------------------------------------------------------
/client/src/common/dateFormat.js:
--------------------------------------------------------------------------------
1 | const dateFormat = (inputDate) => {
2 | const date = new Date(inputDate);
3 | const options = { year: "numeric", month: "short", day: "numeric" };
4 | const formattedDate = new Intl.DateTimeFormat("en-US", options).format(date);
5 | return formattedDate;
6 | };
7 |
8 | export default dateFormat;
9 |
--------------------------------------------------------------------------------
/server/middleware/errorHandler.js:
--------------------------------------------------------------------------------
1 | const errorHandler = (err, req, res, next) => {
2 | const errStatus = req.statusCode || 500;
3 | const errMessage = err.message || "Something went wrong";
4 | return res.status(errStatus).json({
5 | message: errMessage,
6 | isError: true,
7 | });
8 | };
9 |
10 | module.exports = errorHandler;
11 |
--------------------------------------------------------------------------------
/client/src/pages/message/CheckoutFailure.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { paymentFailed } from "../../assets";
3 | import Message from "./Message";
4 |
5 | const CheckoutFailure = () => {
6 | return (
7 |
11 | );
12 | };
13 |
14 | export default CheckoutFailure;
15 |
--------------------------------------------------------------------------------
/server/middleware/credentials.js:
--------------------------------------------------------------------------------
1 | const allowedOrigins = require("../config/allowedOrigins");
2 |
3 | const credentials = (req, res, next) => {
4 | const origin = req.headers.origin;
5 | if (allowedOrigins.includes(origin)) {
6 | res.header("Access-Control-Allow-Credentials", true);
7 | }
8 | next();
9 | };
10 |
11 | module.exports = credentials;
12 |
--------------------------------------------------------------------------------
/client/src/components/scrollToTop/ScrollToTop.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react";
2 | import { useLocation } from "react-router-dom";
3 |
4 | const ScrollToTop = () => {
5 | const { pathname } = useLocation();
6 |
7 | useEffect(() => {
8 | window.scrollTo(0, 0);
9 | }, [pathname]);
10 |
11 | return null;
12 | };
13 |
14 | export default ScrollToTop;
15 |
--------------------------------------------------------------------------------
/client/src/pages/message/CheckoutSuccess.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { paymentSuccessful } from "../../assets";
3 | import Message from "./Message";
4 |
5 | const CheckoutSuccess = () => {
6 | return (
7 |
11 | );
12 | };
13 |
14 | export default CheckoutSuccess;
15 |
--------------------------------------------------------------------------------
/client/.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 |
--------------------------------------------------------------------------------
/client/src/layouts/RootLayout.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Outlet } from "react-router-dom";
3 | import { Header, Footer, BackToTop } from "../components";
4 |
5 | const RootLayout = () => {
6 | return (
7 | <>
8 |
9 |
10 |
11 |
12 | >
13 | );
14 | };
15 |
16 | export default RootLayout;
17 |
--------------------------------------------------------------------------------
/server/config/corsOptions.js:
--------------------------------------------------------------------------------
1 | const allowedOrigins = require("./allowedOrigins");
2 |
3 | const corsOptions = {
4 | origin: (origin, callback) => {
5 | if (allowedOrigins.indexOf(origin) !== -1 || !origin) {
6 | callback(null, true);
7 | } else {
8 | callback(new Error("Not allowed by CORS"));
9 | }
10 | },
11 | optionsSuccessStatus: 200,
12 | };
13 |
14 | module.exports = corsOptions;
15 |
--------------------------------------------------------------------------------
/client/src/components/dashboard/Table.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { DataGrid, GridToolbar } from "@mui/x-data-grid";
3 |
4 | const Table = ({ cols, rows }) => {
5 | return (
6 |
13 | );
14 | };
15 |
16 | export default Table;
17 |
--------------------------------------------------------------------------------
/server/middleware/verifyRoles.js:
--------------------------------------------------------------------------------
1 | const verifyRoles = (...allowedRoles) => {
2 | return (req, res, next) => {
3 | if (!req.roles) return res.status(401).json({ message: "Unauthorized" });
4 |
5 | const result = req.roles.some((role) => allowedRoles.includes(role));
6 |
7 | if (!result) return res.status(401).json({ message: "Unauthorized" });
8 | next();
9 | };
10 | };
11 |
12 | module.exports = verifyRoles;
13 |
--------------------------------------------------------------------------------
/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/src/components/loading/PageLoading.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Lottie from "lottie-react";
3 | import { pageLoading } from "../../assets/index";
4 |
5 | const PageLoading = () => {
6 | return (
7 |
8 |
13 |
14 | );
15 | };
16 |
17 | export default PageLoading;
18 |
--------------------------------------------------------------------------------
/client/src/features/blog/blogSlice.js:
--------------------------------------------------------------------------------
1 | import { createSlice } from "@reduxjs/toolkit";
2 |
3 | const blogSlice = createSlice({
4 | name: "blog",
5 | initialState: {
6 | blogs: null,
7 | },
8 | reducers: {
9 | setBlogs: (state, action) => {
10 | state.blogs = action.payload;
11 | },
12 | },
13 | });
14 |
15 | export const { setBlogs } = blogSlice.actions;
16 | export default blogSlice.reducer;
17 |
18 | export const selectCurrentBlogs = (state) => state.blog.blogs;
19 |
--------------------------------------------------------------------------------
/client/src/features/user/userSlice.js:
--------------------------------------------------------------------------------
1 | import { createSlice } from "@reduxjs/toolkit";
2 |
3 | const userSlice = createSlice({
4 | name: "user",
5 | initialState: {
6 | users: null,
7 | },
8 | reducers: {
9 | setUsers: (state, action) => {
10 | state.users = action.payload;
11 | },
12 | },
13 | });
14 |
15 | export const { setUsers } = userSlice.actions;
16 | export default userSlice.reducer;
17 |
18 | export const selectCurrentUsers = (state) => state.user.users;
19 |
--------------------------------------------------------------------------------
/client/src/components/loading/ComponentLoading.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Lottie from "lottie-react";
3 | import { componentLoading } from "../../assets/index";
4 |
5 | const ComponentLoading = () => {
6 | return (
7 |
8 |
13 |
14 | );
15 | };
16 |
17 | export default ComponentLoading;
18 |
--------------------------------------------------------------------------------
/server/middleware/loginLimit.js:
--------------------------------------------------------------------------------
1 | const rateLimit = require("express-rate-limit");
2 |
3 | const loginLimit = rateLimit({
4 | windowMs: 60 * 1000,
5 | max: 5,
6 | message: {
7 | message:
8 | "Too many login attempts from this IP, please try again after a minute",
9 | },
10 | handler: (req, res, next, options) => {
11 | res.status(options.statusCode).send(options.message);
12 | },
13 | standardHeaders: true,
14 | legacyHeaders: false,
15 | });
16 |
17 | module.exports = loginLimit;
18 |
--------------------------------------------------------------------------------
/client/src/features/recipe/recipeSlice.js:
--------------------------------------------------------------------------------
1 | import { createSlice } from "@reduxjs/toolkit";
2 |
3 | const recipeSlice = createSlice({
4 | name: "recipe",
5 | initialState: {
6 | recipes: null,
7 | },
8 | reducers: {
9 | setRecipes: (state, action) => {
10 | state.recipes = action.payload;
11 | },
12 | },
13 | });
14 |
15 | export const { setRecipes } = recipeSlice.actions;
16 | export default recipeSlice.reducer;
17 |
18 | export const selectCurrentRecipes = (state) => state.recipe.recipes;
19 |
--------------------------------------------------------------------------------
/server/routes/authRoutes.js:
--------------------------------------------------------------------------------
1 | const express = require("express");
2 | const {
3 | register,
4 | login,
5 | refreshToken,
6 | logout,
7 | } = require("../controllers/authController");
8 | const loginLimit = require("../middleware/loginLimit");
9 |
10 | const router = express.Router();
11 |
12 | router.route("/login").post(loginLimit, login);
13 | router.route("/register").post(register);
14 | router.route("/refresh").get(refreshToken);
15 | router.route("/logout").post(logout);
16 |
17 | module.exports = router;
18 |
--------------------------------------------------------------------------------
/server/middleware/verifyJwt.js:
--------------------------------------------------------------------------------
1 | const jwt = require("jsonwebtoken");
2 |
3 | const verifyJwt = (req, res, next) => {
4 | const authHeader = req.headers.authorization;
5 |
6 | if (!authHeader?.startsWith("Bearer ")) return res.sendStatus(401);
7 |
8 | const token = authHeader.split(" ")[1];
9 |
10 | jwt.verify(token, process.env.ACCESS_TOKEN_SECRET, (err, decoded) => {
11 | if (err) return res.sendStatus(403);
12 | req.user = decoded.UserInfo.userId;
13 | req.roles = decoded.UserInfo.roles;
14 | next();
15 | });
16 | };
17 |
18 | module.exports = verifyJwt;
19 |
--------------------------------------------------------------------------------
/client/src/features/auth/authSlice.js:
--------------------------------------------------------------------------------
1 | import { createSlice } from "@reduxjs/toolkit";
2 |
3 | const authSlice = createSlice({
4 | name: "auth",
5 | initialState: {
6 | token: null,
7 | },
8 | reducers: {
9 | setCredentials: (state, action) => {
10 | state.token = action.payload.accessToken;
11 | },
12 | logOut: (state) => {
13 | state.token = null;
14 | },
15 | },
16 | });
17 |
18 | export const { setCredentials, logOut } = authSlice.actions;
19 | export default authSlice.reducer;
20 |
21 | export const selectCurrentToken = (state) => state.auth.token;
22 |
--------------------------------------------------------------------------------
/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "recipen-backend",
3 | "version": "1.0.0",
4 | "main": "index.js",
5 | "scripts": {
6 | "start": "node index.js",
7 | "server": "nodemon index.js"
8 | },
9 | "keywords": [],
10 | "author": "Avinash905",
11 | "license": "ISC",
12 | "dependencies": {
13 | "bcrypt": "^5.1.0",
14 | "cookie-parser": "^1.4.6",
15 | "cors": "^2.8.5",
16 | "dotenv": "^16.3.1",
17 | "express": "^4.18.2",
18 | "express-rate-limit": "^6.9.0",
19 | "jsonwebtoken": "^9.0.1",
20 | "mongoose": "^7.4.1",
21 | "stripe": "^12.17.0"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/server/routes/subscriptionRoutes.js:
--------------------------------------------------------------------------------
1 | const express = require("express");
2 | const { subscribe } = require("../controllers/subscriptionController.js");
3 | const ROLES_LIST = require("../config/rolesList");
4 | const verifyJwt = require("../middleware/verifyJwt");
5 | const verifyRoles = require("../middleware/verifyRoles");
6 |
7 | const router = express.Router();
8 |
9 | router
10 | .route("/create-checkout-session")
11 | .post(
12 | [
13 | verifyJwt,
14 | verifyRoles(ROLES_LIST.BasicUser, ROLES_LIST.Admin, ROLES_LIST.ProUser),
15 | ],
16 | subscribe
17 | );
18 |
19 | module.exports = router;
20 |
--------------------------------------------------------------------------------
/client/src/redux/store.js:
--------------------------------------------------------------------------------
1 | import { configureStore } from "@reduxjs/toolkit";
2 | import { apiSlice } from "./apiSlice";
3 | import authReducer from "../features/auth/authSlice";
4 | import blogReducer from "../features/blog/blogSlice";
5 | import recipeReducer from "../features/recipe/recipeSlice";
6 |
7 | export const store = configureStore({
8 | reducer: {
9 | [apiSlice.reducerPath]: apiSlice.reducer,
10 | auth: authReducer,
11 | blog: blogReducer,
12 | recipe: recipeReducer,
13 | },
14 | middleware: (getDefaultMiddleware) =>
15 | getDefaultMiddleware().concat(apiSlice.middleware),
16 | devTools: false,
17 | });
18 |
--------------------------------------------------------------------------------
/client/src/assets/index.js:
--------------------------------------------------------------------------------
1 | import logo from "./logo.webp";
2 | import logoNoBg from "./logo_nobg.webp";
3 | import profileBg from "./profile_details.svg";
4 | import errorAnimation from "./error.json";
5 | import paymentFailed from "./payment_failed.json";
6 | import paymentSuccessful from "./payment_successful.json";
7 | import pageLoading from "./loading.json";
8 | import componentLoading from "./componentLoading.json";
9 | import photo from "./photo.svg";
10 |
11 | export {
12 | logo,
13 | logoNoBg,
14 | profileBg,
15 | errorAnimation,
16 | paymentSuccessful,
17 | paymentFailed,
18 | photo,
19 | pageLoading,
20 | componentLoading,
21 | };
22 |
--------------------------------------------------------------------------------
/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 { Provider } from "react-redux";
6 | import { store } from "./redux/store.js";
7 | import { AnimatePresence } from "framer-motion";
8 | import { disableReactDevTools } from "@fvilers/disable-react-devtools";
9 |
10 | disableReactDevTools();
11 |
12 | ReactDOM.createRoot(document.getElementById("root")).render(
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | );
21 |
--------------------------------------------------------------------------------
/client/src/pages/message/Message.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Lottie from "lottie-react";
3 | import { Button } from "../../components";
4 | import { Link } from "react-router-dom";
5 |
6 | const Message = ({ animation, loop }) => {
7 | return (
8 |
20 | );
21 | };
22 |
23 | export default Message;
24 |
--------------------------------------------------------------------------------
/client/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | export default {
3 | content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
4 | theme: {
5 | extend: {
6 | colors: {
7 | primary: "#ffaa07",
8 | primaryLight: "#ffb629",
9 | light: "#fffefc",
10 | },
11 | backgroundImage: {
12 | login:
13 | "url('https://images.pexels.com/photos/2454533/pexels-photo-2454533.jpeg?auto=compress&cs=tinysrgb&w=720&dpr=1')",
14 | hero: "url('https://images.pexels.com/photos/916925/pexels-photo-916925.jpeg?auto=compress&cs=tinysrgb&w=720&dpr=1')",
15 | },
16 | },
17 | },
18 | plugins: [],
19 | };
20 |
--------------------------------------------------------------------------------
/client/src/layouts/DashboardLayout.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { Sidebar } from "../components";
3 | import { Outlet } from "react-router-dom";
4 | import useTitle from "../hooks/useTitle";
5 |
6 | const DashboardLayout = () => {
7 | const [isCollapsed, setIsCollapsed] = useState(true);
8 | useTitle("Recipen - Dashboard");
9 |
10 | return (
11 |
20 | );
21 | };
22 |
23 | export default DashboardLayout;
24 |
--------------------------------------------------------------------------------
/client/src/components/logo/Logo.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { logoNoBg } from "../../assets";
3 | import { Link } from "react-router-dom";
4 |
5 | const Logo = ({ customCss, hideName = false }) => {
6 | return (
7 |
11 |
12 |
13 |
18 |
19 | {!hideName &&
Recipen }
20 |
21 |
22 | );
23 | };
24 |
25 | export default Logo;
26 |
--------------------------------------------------------------------------------
/server/models/userModel.js:
--------------------------------------------------------------------------------
1 | const mongoose = require("mongoose");
2 |
3 | const schema = mongoose.Schema(
4 | {
5 | name: {
6 | type: String,
7 | },
8 | email: {
9 | type: String,
10 | },
11 | password: {
12 | type: String,
13 | },
14 | profilePicture: { type: String, default: "" },
15 | favorites: [
16 | {
17 | type: mongoose.Schema.Types.ObjectId,
18 | ref: "Recipe",
19 | },
20 | ],
21 | roles: {
22 | type: [String],
23 | default: ["BasicUser"],
24 | },
25 | isDisabled: { type: Boolean, default: false },
26 | refreshToken: { type: [String] },
27 | },
28 | {
29 | timestamps: true,
30 | }
31 | );
32 |
33 | const User = mongoose.model("User", schema);
34 | module.exports = User;
35 |
--------------------------------------------------------------------------------
/client/src/features/auth/PersistLogin.jsx:
--------------------------------------------------------------------------------
1 | import { Outlet } from "react-router-dom";
2 | import { useEffect } from "react";
3 | import { useRefreshMutation } from "./authApiSlice";
4 | import PageLoading from "../../components/loading/PageLoading";
5 |
6 | const PersistLogin = () => {
7 | const persist = localStorage.getItem("persist");
8 | const [refresh, { isLoading }] = useRefreshMutation();
9 |
10 | useEffect(() => {
11 | const verifyRefreshToken = async () => {
12 | try {
13 | await refresh();
14 | } catch (err) {
15 | console.error(err);
16 | }
17 | };
18 | if (persist === "true") verifyRefreshToken();
19 | }, []);
20 |
21 | if (isLoading) {
22 | return ;
23 | } else return ;
24 | };
25 | export default PersistLogin;
26 |
--------------------------------------------------------------------------------
/server/routes/userRoutes.js:
--------------------------------------------------------------------------------
1 | const express = require("express");
2 | const {
3 | getAllUsers,
4 | updateUser,
5 | disableUser,
6 | } = require("../controllers/userController");
7 | const ROLES_LIST = require("../config/rolesList");
8 | const verifyJwt = require("../middleware/verifyJwt");
9 | const verifyRoles = require("../middleware/verifyRoles");
10 |
11 | const router = express.Router();
12 |
13 | router.route("/").get([verifyJwt, verifyRoles(ROLES_LIST.Admin)], getAllUsers);
14 |
15 | router
16 | .route("/:id")
17 | .put(
18 | [
19 | verifyJwt,
20 | verifyRoles(ROLES_LIST.BasicUser, ROLES_LIST.ProUser, ROLES_LIST.Admin),
21 | ],
22 | updateUser
23 | );
24 |
25 | router
26 | .route("/disable/:id")
27 | .put([verifyJwt, verifyRoles(ROLES_LIST.Admin)], disableUser);
28 |
29 | module.exports = router;
30 |
--------------------------------------------------------------------------------
/client/src/hooks/useAuth.jsx:
--------------------------------------------------------------------------------
1 | import { useSelector } from "react-redux";
2 | import { selectCurrentToken } from "../features/auth/authSlice";
3 | import jwtDecode from "jwt-decode";
4 | import { profileBg } from "../assets";
5 |
6 | const useAuth = () => {
7 | const token = useSelector(selectCurrentToken);
8 | let isProUser = false;
9 | let isAdmin = false;
10 |
11 | if (token) {
12 | const decoded = jwtDecode(token);
13 | const { userId, name, email, profilePicture, roles, favorites } =
14 | decoded.UserInfo;
15 |
16 | isProUser = roles.includes("ProUser");
17 | isAdmin = roles.includes("Admin");
18 |
19 | return {
20 | userId,
21 | name,
22 | email,
23 | profilePicture: profilePicture || profileBg,
24 | roles,
25 | favorites,
26 | isProUser,
27 | isAdmin,
28 | };
29 | }
30 |
31 | return null;
32 | };
33 | export default useAuth;
34 |
--------------------------------------------------------------------------------
/client/src/pages/recipe/SavedRecipes.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { AllCards, ComponentLoading } from "../../components";
3 | import { useGetRecipesQuery } from "../../features/recipe/recipeApiSlice";
4 | import useAuth from "../../hooks/useAuth";
5 |
6 | const index = () => {
7 | const { data, isLoading } = useGetRecipesQuery();
8 | const user = useAuth();
9 |
10 | const updatedData = data?.filter((obj) =>
11 | user?.favorites?.includes(obj._id.toString())
12 | );
13 |
14 | return (
15 | <>
16 | {isLoading ? (
17 |
18 | ) : (
19 |
27 | )}
28 | >
29 | );
30 | };
31 |
32 | export default index;
33 |
--------------------------------------------------------------------------------
/client/src/pages/home/Home.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Hero, HomeCategories, Subscribe } from "../../components";
3 | import { useGetRecipesQuery } from "../../features/recipe/recipeApiSlice";
4 | import { useGetBlogsQuery } from "../../features/blog/blogApiSlice";
5 | import useAuth from "../../hooks/useAuth";
6 |
7 | const Home = () => {
8 | const user = useAuth();
9 | const recipes = useGetRecipesQuery();
10 | const blogs = useGetBlogsQuery();
11 |
12 | return (
13 | <>
14 |
15 |
20 | {!user?.roles?.some((role) => role === "ProUser" || role === "Admin") && (
21 |
22 | )}
23 |
28 | >
29 | );
30 | };
31 |
32 | export default Home;
33 |
--------------------------------------------------------------------------------
/server/models/blogModel.js:
--------------------------------------------------------------------------------
1 | const mongoose = require("mongoose");
2 |
3 | const schema = mongoose.Schema(
4 | {
5 | title: {
6 | type: String,
7 | },
8 | author: {
9 | type: mongoose.Schema.Types.ObjectId,
10 | ref: "User",
11 | },
12 | description: {
13 | type: String,
14 | },
15 | image: { type: String },
16 | ratings: [
17 | {
18 | user: { type: mongoose.Schema.Types.ObjectId, ref: "User" },
19 | rating: { type: Number },
20 | },
21 | ],
22 | comments: [
23 | {
24 | user: {
25 | type: mongoose.Schema.Types.ObjectId,
26 | ref: "User",
27 | },
28 | comment: {
29 | type: String,
30 | },
31 | date: {
32 | type: Date,
33 | default: Date.now(),
34 | },
35 | },
36 | ],
37 | },
38 | {
39 | timestamps: true,
40 | }
41 | );
42 |
43 | const Blog = mongoose.model("Blog", schema);
44 | module.exports = Blog;
45 |
--------------------------------------------------------------------------------
/client/src/pages/recipe/MyRecipes.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { AllCards, ComponentLoading } from "../../components";
3 | import { useGetRecipesQuery } from "../../features/recipe/recipeApiSlice";
4 | import useAuth from "../../hooks/useAuth";
5 | import useTitle from "../../hooks/useTitle";
6 |
7 | const index = () => {
8 | const { data, isLoading } = useGetRecipesQuery();
9 | const user = useAuth();
10 | useTitle("Recipen - My Recipes");
11 |
12 | const updatedData = data?.filter((obj) => obj.author._id === user?.userId);
13 |
14 | return (
15 | <>
16 | {isLoading ? (
17 |
18 | ) : (
19 |
27 | )}
28 | >
29 | );
30 | };
31 |
32 | export default index;
33 |
--------------------------------------------------------------------------------
/client/src/pages/blogs/MyBlogs.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { AllCards, ComponentLoading } from "../../components";
3 | import { useGetBlogsQuery } from "../../features/blog/blogApiSlice";
4 | import useAuth from "../../hooks/useAuth";
5 | import useTitle from "../../hooks/useTitle";
6 |
7 | const index = () => {
8 | const { data, isLoading } = useGetBlogsQuery();
9 | const user = useAuth();
10 | useTitle("Recipen - My Blogs");
11 |
12 | const updatedData = data?.filter((obj) => obj.author._id === user?.userId);
13 |
14 | return (
15 | <>
16 | {isLoading ? (
17 |
18 | ) : (
19 |
27 | )}
28 | >
29 | );
30 | };
31 |
32 | export default index;
33 |
--------------------------------------------------------------------------------
/client/src/common/uploadImage.js:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 |
3 | const uploadImage = async (e, setProgress, setFormDetails, formDetails) => {
4 | if (
5 | e.target.files[0].type === "image/jpeg" ||
6 | e.target.files[0].type === "image/png"
7 | ) {
8 | const data = new FormData();
9 | data.append("file", e.target.files[0]);
10 |
11 | data.append("upload_preset", import.meta.env.VITE_CLOUDINARY_PRESET);
12 |
13 | const config = {
14 | onUploadProgress: (e) => {
15 | const { loaded, total } = e;
16 | setProgress((loaded / total) * 100);
17 | },
18 | };
19 |
20 | data.append("cloud_name", import.meta.env.VITE_CLOUDINARY_CLOUD_NAME);
21 |
22 | const {
23 | data: { url },
24 | } = await axios.post(
25 | import.meta.env.VITE_CLOUDINARY_BASE_URL,
26 | data,
27 | config
28 | );
29 | setFormDetails({ ...formDetails, [e.target.id]: url });
30 | } else {
31 | console.error("Please select an image in jpeg or png format");
32 | }
33 | };
34 |
35 | export default uploadImage;
36 |
--------------------------------------------------------------------------------
/client/src/pages/blogs/Blogs.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 | import { AllCards, ComponentLoading } from "../../components";
3 | import { useDispatch } from "react-redux";
4 | import { setBlogs } from "../../features/blog/blogSlice";
5 | import { useGetBlogsQuery } from "../../features/blog/blogApiSlice";
6 | import useTitle from "../../hooks/useTitle";
7 |
8 | const Blogs = () => {
9 | const { data, isLoading } = useGetBlogsQuery();
10 | const dispatch = useDispatch();
11 | useTitle("Recipen - All Blogs");
12 |
13 | useEffect(() => {
14 | if (!isLoading) {
15 | dispatch(setBlogs(data));
16 | }
17 | }, [isLoading]);
18 |
19 | return (
20 | <>
21 | {isLoading ? (
22 |
23 | ) : (
24 |
32 | )}
33 | >
34 | );
35 | };
36 |
37 | export default Blogs;
38 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Dunna Avinash
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/server/models/recipeModel.js:
--------------------------------------------------------------------------------
1 | const mongoose = require("mongoose");
2 |
3 | const schema = mongoose.Schema(
4 | {
5 | title: {
6 | type: String,
7 | },
8 | author: {
9 | type: mongoose.Schema.Types.ObjectId,
10 | ref: "User",
11 | },
12 | description: { type: String },
13 | image: { type: String },
14 | cookingTime: { type: String },
15 | calories: { type: String },
16 | ingredients: [{ type: String }],
17 | instructions: [{ type: String }],
18 | ratings: [
19 | {
20 | user: { type: mongoose.Schema.Types.ObjectId, ref: "User" },
21 | rating: { type: Number },
22 | },
23 | ],
24 | comments: [
25 | {
26 | user: {
27 | type: mongoose.Schema.Types.ObjectId,
28 | ref: "User",
29 | },
30 | comment: {
31 | type: String,
32 | },
33 | date: {
34 | type: Date,
35 | default: Date.now(),
36 | },
37 | },
38 | ],
39 | },
40 | {
41 | timestamps: true,
42 | }
43 | );
44 |
45 | const Recipe = mongoose.model("Recipe", schema);
46 | module.exports = Recipe;
47 |
--------------------------------------------------------------------------------
/client/src/pages/recipe/Recipe.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 | import { AllCards, ComponentLoading } from "../../components";
3 | import { useDispatch } from "react-redux";
4 | import { setRecipes } from "../../features/recipe/recipeSlice";
5 | import { useGetRecipesQuery } from "../../features/recipe/recipeApiSlice";
6 | import useTitle from "../../hooks/useTitle";
7 |
8 | const Recipe = () => {
9 | const { data, isLoading } = useGetRecipesQuery();
10 | const dispatch = useDispatch();
11 | useTitle("Recipen - All Recipes");
12 |
13 | useEffect(() => {
14 | if (!isLoading) {
15 | dispatch(setRecipes(data));
16 | }
17 | }, [isLoading]);
18 |
19 | return (
20 | <>
21 | {isLoading ? (
22 |
23 | ) : (
24 |
32 | )}
33 | >
34 | );
35 | };
36 |
37 | export default Recipe;
38 |
--------------------------------------------------------------------------------
/client/src/components/backToTop/BackToTop.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import { MdKeyboardArrowUp } from "react-icons/md";
3 |
4 | const BackToTop = () => {
5 | const [isVisible, setIsVisible] = useState(false);
6 |
7 | const handleScroll = () => {
8 | const scrollY = window.scrollY;
9 | const isVisible = scrollY > 300; // Show the button when the user has scrolled 300 pixels down
10 | setIsVisible(isVisible);
11 | };
12 |
13 | const scrollToTop = () => {
14 | window.scrollTo({ top: 0, behavior: "smooth" }); // Scroll to top with smooth animation
15 | };
16 |
17 | useEffect(() => {
18 | window.addEventListener("scroll", handleScroll);
19 | return () => {
20 | window.removeEventListener("scroll", handleScroll);
21 | };
22 | }, []);
23 |
24 | return (
25 |
26 |
30 |
31 | );
32 | };
33 |
34 | export default BackToTop;
35 |
--------------------------------------------------------------------------------
/client/src/features/user/userApiSlice.js:
--------------------------------------------------------------------------------
1 | import { apiSlice } from "../../redux/apiSlice";
2 |
3 | export const userApiSlice = apiSlice.injectEndpoints({
4 | endpoints: (builder) => ({
5 | getUsers: builder.query({
6 | query: () => "/user",
7 | providesTags: ["users"],
8 | }),
9 | updateUser: builder.mutation({
10 | query: (args) => {
11 | const { userId, ...userData } = args;
12 | return {
13 | url: `/user/${userId}`,
14 | method: "PUT",
15 | body: { ...userData },
16 | };
17 | },
18 | invalidatesTags: ["users"],
19 | }),
20 | disableUser: builder.mutation({
21 | query: (userId) => ({
22 | url: `/user/disable/${userId}`,
23 | method: "PUT",
24 | }),
25 | invalidatesTags: ["users"],
26 | }),
27 | subscribeUser: builder.mutation({
28 | query: () => ({
29 | url: `/stripe/create-checkout-session`,
30 | method: "POST",
31 | }),
32 | invalidatesTags: ["users"],
33 | }),
34 | }),
35 | });
36 |
37 | export const {
38 | useGetUsersQuery,
39 | useUpdateUserMutation,
40 | useDisableUserMutation,
41 | useSubscribeUserMutation,
42 | } = userApiSlice;
43 |
--------------------------------------------------------------------------------
/client/src/features/auth/RequireAuth.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import { useLocation, Navigate, Outlet } from "react-router-dom";
3 | import useAuth from "../../hooks/useAuth";
4 | import { PageLoading } from "../../components";
5 |
6 | const RequireAuth = ({ allowedRoles }) => {
7 | const user = useAuth();
8 | const location = useLocation();
9 | const [redirect, setRedirect] = useState(null);
10 |
11 | useEffect(() => {
12 | if (!user) {
13 | const timer = setTimeout(() => {
14 | setRedirect("/auth/signin");
15 | }, 4000);
16 |
17 | return () => clearTimeout(timer);
18 | }
19 | }, [user]);
20 |
21 | if (redirect) {
22 | return (
23 |
28 | );
29 | }
30 |
31 | if (user) {
32 | if (user?.roles?.some((role) => allowedRoles.includes(role))) {
33 | return ;
34 | } else {
35 | return (
36 |
41 | );
42 | }
43 | }
44 |
45 | return ;
46 | };
47 |
48 | export default RequireAuth;
49 |
--------------------------------------------------------------------------------
/client/src/redux/apiSlice.js:
--------------------------------------------------------------------------------
1 | import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
2 | import { setCredentials, logOut } from "../features/auth/authSlice";
3 |
4 | const baseQuery = fetchBaseQuery({
5 | baseUrl: import.meta.env.VITE_SERVER_BASE_URL,
6 | credentials: "include",
7 |
8 | prepareHeaders: (headers, { getState }) => {
9 | const token = getState().auth.token;
10 | if (token) {
11 | headers.set("authorization", `Bearer ${token}`);
12 | }
13 | return headers;
14 | },
15 | });
16 |
17 | const baseQueryWithReauth = async (args, api, extraOptions) => {
18 | let result = await baseQuery(args, api, extraOptions);
19 |
20 | if (result?.error?.originalStatus === 403) {
21 | const refreshResult = await baseQuery("/auth/refresh", api, extraOptions);
22 |
23 | if (refreshResult?.data) {
24 | api.dispatch(setCredentials({ ...refreshResult.data }));
25 |
26 | result = await baseQuery(args, api, extraOptions);
27 | } else {
28 | if (refreshResult?.error?.status === 403) {
29 | refreshResult.error.data.message = "Your login has expired.";
30 | }
31 | return refreshResult;
32 | }
33 | }
34 | return result;
35 | };
36 |
37 | export const apiSlice = createApi({
38 | baseQuery: baseQueryWithReauth,
39 | endpoints: (builder) => ({}),
40 | });
41 |
--------------------------------------------------------------------------------
/server/index.js:
--------------------------------------------------------------------------------
1 | require("dotenv").config();
2 | const cors = require("cors");
3 | const corsOptions = require("./config/corsOptions");
4 | const express = require("express");
5 | const errorHandler = require("./middleware/errorHandler");
6 | const credentials = require("./middleware/credentials");
7 | const cookieParser = require("cookie-parser");
8 | const mongoose = require("mongoose");
9 | const connectDB = require("./db/conn");
10 |
11 | const app = express();
12 | const port = process.env.PORT || 5000;
13 |
14 | // cors middleware
15 | app.use(credentials);
16 | app.use(cors(corsOptions));
17 |
18 | app.use(express.urlencoded({ extended: false }));
19 |
20 | app.use(cookieParser());
21 |
22 | app.use(express.json());
23 |
24 | // route middleware
25 | app.use("/api/auth", require("./routes/authRoutes"));
26 | app.use("/api/user", require("./routes/userRoutes"));
27 | app.use("/api/recipe", require("./routes/recipeRoutes"));
28 | app.use("/api/blog", require("./routes/blogRoutes"));
29 | app.use("/api/stripe", require("./routes/subscriptionRoutes"));
30 |
31 | app.use(errorHandler);
32 |
33 | connectDB()
34 | .then(() => {
35 | console.log("Connected to MongoDB");
36 | app.listen(port, () => console.log(`Server running on port ${port}`));
37 | })
38 | .catch((err) => {
39 | console.error(`Error connecting to MongoDB ${err}`);
40 | });
41 |
--------------------------------------------------------------------------------
/server/routes/blogRoutes.js:
--------------------------------------------------------------------------------
1 | const express = require("express");
2 | const {
3 | getAllBlogs,
4 | getBlog,
5 | addBlog,
6 | updateBlog,
7 | deleteBlog,
8 | addComment,
9 | deleteComment,
10 | } = require("../controllers/blogController");
11 | const ROLES_LIST = require("../config/rolesList");
12 | const verifyJwt = require("../middleware/verifyJwt");
13 | const verifyRoles = require("../middleware/verifyRoles");
14 |
15 | const router = express.Router();
16 |
17 | router
18 | .route("/")
19 | .get(getAllBlogs)
20 | .post(
21 | [verifyJwt, verifyRoles(ROLES_LIST.Admin, ROLES_LIST.ProUser)],
22 | addBlog
23 | );
24 |
25 | router
26 | .route("/:id")
27 | .get(getBlog)
28 | .put(
29 | [verifyJwt, verifyRoles(ROLES_LIST.Admin, ROLES_LIST.ProUser)],
30 | updateBlog
31 | )
32 | .delete(
33 | [verifyJwt, verifyRoles(ROLES_LIST.Admin, ROLES_LIST.ProUser)],
34 | deleteBlog
35 | );
36 |
37 | router
38 | .route("/comment/:id")
39 | .put(
40 | [
41 | verifyJwt,
42 | verifyRoles(ROLES_LIST.BasicUser, ROLES_LIST.ProUser, ROLES_LIST.Admin),
43 | ],
44 | addComment
45 | );
46 |
47 | router
48 | .route("/comment/:blogId/:commentId")
49 | .delete(
50 | [
51 | verifyJwt,
52 | verifyRoles(ROLES_LIST.BasicUser, ROLES_LIST.ProUser, ROLES_LIST.Admin),
53 | ],
54 | deleteComment
55 | );
56 | module.exports = router;
57 |
--------------------------------------------------------------------------------
/client/src/components/index.js:
--------------------------------------------------------------------------------
1 | import Header from "./header/Header";
2 | import Menu from "./header/Menu.jsx";
3 | import Footer from "./footer/Footer";
4 | import Input from "./input/Input";
5 | import Button from "./button/Button";
6 | import Logo from "./logo/Logo";
7 | import PageLoading from "./loading/PageLoading";
8 | import ComponentLoading from "./loading/ComponentLoading";
9 | import Hero from "./hero/Hero";
10 | import BackToTop from "./backToTop/BackToTop";
11 | import SingleCard from "./cards/SingleCard";
12 | import AllCards from "./cards/AllCards";
13 | import Comment from "./comment/Comment";
14 | import HomeCategories from "./homeCategories/HomeCategories";
15 | import Subscribe from "./subscribe/Subscribe";
16 | import SubscribeCard from "./cards/SubscribeCard";
17 | import Sidebar from "./dashboard/Sidebar";
18 | import Table from "./dashboard/Table";
19 | import Avatar from "./avatar/Avatar";
20 | import ScrollToTop from "./scrollToTop/ScrollToTop";
21 | import ShareButton from "./shareButton/ShareButton";
22 | import NoData from "./noData/NoData";
23 |
24 | export {
25 | Header,
26 | Menu,
27 | Footer,
28 | Input,
29 | Button,
30 | Logo,
31 | PageLoading,
32 | ComponentLoading,
33 | Hero,
34 | BackToTop,
35 | SingleCard,
36 | AllCards,
37 | Comment,
38 | HomeCategories,
39 | Subscribe,
40 | SubscribeCard,
41 | Sidebar,
42 | Table,
43 | Avatar,
44 | ScrollToTop,
45 | ShareButton,
46 | NoData,
47 | };
48 |
--------------------------------------------------------------------------------
/client/src/index.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | body {
6 | background-color: #fffefc;
7 | scroll-behavior: smooth;
8 | }
9 |
10 | nav .active {
11 | @apply border-b-[2px] border-primary;
12 | }
13 |
14 | .box {
15 | @apply max-w-7xl mx-auto px-6 mb-8 md:mb-10;
16 | }
17 |
18 | h1,
19 | h2,
20 | h3,
21 | h4,
22 | h5,
23 | h6 {
24 | @apply text-gray-800;
25 | }
26 |
27 | ::-webkit-scrollbar {
28 | @apply w-2 bg-primaryLight;
29 | }
30 |
31 | /* Track */
32 | ::-webkit-scrollbar-track {
33 | @apply border-4 bg-gray-200;
34 | }
35 |
36 | /* Handle */
37 | ::-webkit-scrollbar-thumb {
38 | @apply border-2 border-primaryLight bg-primaryLight;
39 | }
40 |
41 | .MuiDataGrid-root {
42 | @apply border-none;
43 | }
44 |
45 | .MuiDataGrid-cell {
46 | @apply border-none;
47 | }
48 |
49 | .MuiDataGrid-columnHeaders {
50 | @apply border-2 border-primaryLight bg-gray-100;
51 | }
52 |
53 | .MuiDataGrid-columnHeaders .MuiSvgIcon-root,
54 | .MuiDataGrid-cellCheckbox .MuiSvgIcon-root {
55 | font-size: large !important;
56 | @apply text-primary;
57 | }
58 |
59 | .MuiDataGrid-toolbarContainer .MuiButton-text {
60 | @apply text-gray-600;
61 | font-size: smaller !important;
62 | }
63 |
64 | .MuiDataGrid-footerContainer {
65 | @apply border-none bg-gray-100;
66 | }
67 |
68 | input:invalid[focused="true"] {
69 | @apply border-red-500;
70 | }
71 |
72 | input:invalid[focused="true"] ~ span {
73 | @apply block;
74 | }
75 |
--------------------------------------------------------------------------------
/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "recipen-frontend",
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 | "@emotion/react": "^11.11.1",
14 | "@emotion/styled": "^11.11.0",
15 | "@fvilers/disable-react-devtools": "^1.3.0",
16 | "@mui/icons-material": "^5.14.3",
17 | "@mui/material": "^5.14.3",
18 | "@mui/x-data-grid": "^6.11.0",
19 | "@reduxjs/toolkit": "^1.9.5",
20 | "axios": "^1.4.0",
21 | "framer-motion": "^10.15.0",
22 | "jwt-decode": "^3.1.2",
23 | "lottie-react": "^2.4.0",
24 | "react": "^18.2.0",
25 | "react-dom": "^18.2.0",
26 | "react-icons": "^4.10.1",
27 | "react-markdown": "^8.0.7",
28 | "react-redux": "^8.1.2",
29 | "react-router-dom": "^6.14.2",
30 | "react-share": "^4.4.1",
31 | "react-toastify": "^9.1.3"
32 | },
33 | "devDependencies": {
34 | "@types/react": "^18.2.15",
35 | "@types/react-dom": "^18.2.7",
36 | "@vitejs/plugin-react": "^4.0.3",
37 | "autoprefixer": "^10.4.14",
38 | "eslint": "^8.45.0",
39 | "eslint-plugin-react": "^7.32.2",
40 | "eslint-plugin-react-hooks": "^4.6.0",
41 | "eslint-plugin-react-refresh": "^0.4.3",
42 | "postcss": "^8.4.27",
43 | "tailwindcss": "^3.3.3",
44 | "vite": "^4.4.5"
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/client/src/components/hero/Hero.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { GiKnifeFork } from "react-icons/gi";
3 | import { Button } from "..";
4 | import { Link } from "react-router-dom";
5 |
6 | const Hero = () => {
7 | return (
8 |
9 |
10 |
11 | Feast. Share. Connect.
12 |
13 |
14 | Welcome to Recipen
15 |
16 |
17 | Where food lovers unite to discover mouthwatering recipes, delightful
18 | restaurants, and engaging food discussions. Explore, share, and
19 | connect over the joy of cooking and dining.
20 |
21 |
22 |
}
28 | />
29 |
30 |
31 |
32 |
33 | );
34 | };
35 |
36 | export default Hero;
37 |
--------------------------------------------------------------------------------
/client/src/components/homeCategories/HomeCategories.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Button, ComponentLoading, NoData, SingleCard } from "..";
3 | import { BsArrowUpRight } from "react-icons/bs";
4 | import { Link } from "react-router-dom";
5 |
6 | const HomeCategories = ({ title, data, isLoading }) => {
7 | return (
8 | <>
9 | {isLoading ? (
10 |
11 | ) : (
12 |
13 |
14 |
Latest {title}s
15 |
16 | }
20 | />
21 |
22 |
23 |
24 | {/* Cards container */}
25 | {data?.length ? (
26 |
27 | {data?.slice(0, 4).map((singleData) => (
28 |
33 | ))}
34 |
35 | ) : (
36 |
37 | )}
38 |
39 | )}
40 | >
41 | );
42 | };
43 |
44 | export default HomeCategories;
45 |
--------------------------------------------------------------------------------
/client/src/features/auth/authApiSlice.js:
--------------------------------------------------------------------------------
1 | import { apiSlice } from "../../redux/apiSlice";
2 | import { logOut, setCredentials } from "./authSlice";
3 |
4 | export const authApiSlice = apiSlice.injectEndpoints({
5 | endpoints: (builder) => ({
6 | signIn: builder.mutation({
7 | query: (credentials) => ({
8 | url: "/auth/login",
9 | method: "POST",
10 | body: { ...credentials },
11 | }),
12 | }),
13 | signUp: builder.mutation({
14 | query: (credentials) => ({
15 | url: "/auth/register",
16 | method: "POST",
17 | body: { ...credentials },
18 | }),
19 | }),
20 | logout: builder.mutation({
21 | query: () => ({
22 | url: "/auth/logout",
23 | method: "POST",
24 | }),
25 | async onQueryStarted(arg, { dispatch, queryFulfilled }) {
26 | try {
27 | await queryFulfilled;
28 | dispatch(logOut());
29 | } catch (err) {
30 | console.error(err);
31 | }
32 | },
33 | }),
34 | refresh: builder.mutation({
35 | query: () => ({
36 | url: "/auth/refresh",
37 | method: "GET",
38 | }),
39 | async onQueryStarted(arg, { dispatch, queryFulfilled }) {
40 | try {
41 | const { data } = await queryFulfilled;
42 | dispatch(setCredentials({ accessToken: data.accessToken }));
43 | } catch (err) {
44 | console.error(err);
45 | }
46 | },
47 | }),
48 | }),
49 | });
50 |
51 | export const {
52 | useSignInMutation,
53 | useSignUpMutation,
54 | useLogoutMutation,
55 | useRefreshMutation,
56 | } = authApiSlice;
57 |
--------------------------------------------------------------------------------
/server/controllers/subscriptionController.js:
--------------------------------------------------------------------------------
1 | const User = require("../models/userModel");
2 | const jwt = require("jsonwebtoken");
3 | const Stripe = require("stripe");
4 |
5 | const stripe = Stripe(process.env.STRIPE_KEY);
6 |
7 | const subscribe = async (req, res, next) => {
8 | try {
9 | const customer = await stripe.customers.create({
10 | metadata: {
11 | userId: req.user,
12 | },
13 | });
14 |
15 | const session = await stripe.checkout.sessions.create({
16 | line_items: [
17 | {
18 | price: process.env.STRIPE_PRICE_ID,
19 | quantity: 1,
20 | },
21 | ],
22 | customer: customer.id,
23 | mode: "payment",
24 | success_url: `${process.env.CLIENT_BASE_URL}/payment-success`,
25 | cancel_url: `${process.env.CLIENT_BASE_URL}/payment-failed`,
26 | });
27 |
28 | const foundUser = await User.findByIdAndUpdate(
29 | req.user,
30 | {
31 | roles: {
32 | BasicUser: 101,
33 | ProUser: 102,
34 | },
35 | },
36 | { new: true }
37 | );
38 | if (!foundUser) {
39 | return res.sendStatus(401);
40 | }
41 |
42 | const roles = Object.values(foundUser.roles);
43 |
44 | const accessToken = jwt.sign(
45 | {
46 | UserInfo: {
47 | userId: foundUser._id,
48 | name: foundUser.name,
49 | email: foundUser.email,
50 | profilePicture: foundUser.profilePicture,
51 | roles: roles,
52 | favorites: foundUser.favorites,
53 | },
54 | },
55 | process.env.ACCESS_TOKEN_SECRET,
56 | { expiresIn: "1d" }
57 | );
58 |
59 | res.status(200).send({ url: session.url, accessToken });
60 | } catch (error) {
61 | next(error);
62 | }
63 | };
64 |
65 | module.exports = { subscribe };
66 |
--------------------------------------------------------------------------------
/client/src/pages/index.js:
--------------------------------------------------------------------------------
1 | import { lazy } from "react";
2 |
3 | const Home = lazy(() => import("./home/Home"));
4 | const Contact = lazy(() => import("./contact/Contact"));
5 | const Profile = lazy(() => import("./profile/Profile"));
6 |
7 | const Error = lazy(() => import("./message/Error"));
8 | const CheckoutSuccess = lazy(() => import("./message/CheckoutSuccess"));
9 | const CheckoutFailure = lazy(() => import("./message/CheckoutFailure"));
10 |
11 | const Recipe = lazy(() => import("./recipe/Recipe"));
12 | const SingleRecipe = lazy(() => import("./recipe/SingleRecipe"));
13 | const SavedRecipes = lazy(() => import("./recipe/SavedRecipes"));
14 | const AddRecipe = lazy(() => import("./recipe/AddRecipe"));
15 | const MyRecipes = lazy(() => import("./recipe/MyRecipes"));
16 | const EditRecipe = lazy(() => import("./recipe/EditRecipe"));
17 |
18 | const Blogs = lazy(() => import("./blogs/Blogs"));
19 | const AddBlog = lazy(() => import("./blogs/AddBlog"));
20 | const SingleBlog = lazy(() => import("./blogs/SingleBlog"));
21 | const MyBlogs = lazy(() => import("./blogs/MyBlogs"));
22 | const EditBlog = lazy(() => import("./blogs/EditBlog"));
23 |
24 | const Users = lazy(() => import("./dashboard/Users"));
25 | const DashboardRecipes = lazy(() => import("./dashboard/DashboardRecipes"));
26 | const DashboardBlogs = lazy(() => import("./dashboard/DashboardBlogs"));
27 |
28 | const SignIn = lazy(() => import("./auth/SignIn"));
29 | const SignUp = lazy(() => import("./auth/SignUp"));
30 |
31 | export {
32 | Home,
33 | Contact,
34 | Profile,
35 | Recipe,
36 | SingleRecipe,
37 | SavedRecipes,
38 | AddRecipe,
39 | MyRecipes,
40 | EditRecipe,
41 | Blogs,
42 | AddBlog,
43 | SingleBlog,
44 | MyBlogs,
45 | EditBlog,
46 | Users,
47 | DashboardRecipes,
48 | DashboardBlogs,
49 | Error,
50 | CheckoutSuccess,
51 | CheckoutFailure,
52 | SignIn,
53 | SignUp,
54 | };
55 |
--------------------------------------------------------------------------------
/server/routes/recipeRoutes.js:
--------------------------------------------------------------------------------
1 | const express = require("express");
2 | const {
3 | getAllRecipes,
4 | getRecipe,
5 | addRecipe,
6 | updateRecipe,
7 | rateRecipe,
8 | deleteRecipe,
9 | addComment,
10 | deleteComment,
11 | toggleFavoriteRecipe,
12 | } = require("../controllers/recipeController");
13 | const ROLES_LIST = require("../config/rolesList");
14 | const verifyJwt = require("../middleware/verifyJwt");
15 | const verifyRoles = require("../middleware/verifyRoles");
16 |
17 | const router = express.Router();
18 |
19 | router
20 | .route("/")
21 | .get(getAllRecipes)
22 | .post(
23 | [verifyJwt, verifyRoles(ROLES_LIST.Admin, ROLES_LIST.ProUser)],
24 | addRecipe
25 | );
26 |
27 | router
28 | .route("/rate/:id")
29 | .put(
30 | [
31 | verifyJwt,
32 | verifyRoles(ROLES_LIST.BasicUser, ROLES_LIST.ProUser, ROLES_LIST.Admin),
33 | ],
34 | rateRecipe
35 | );
36 |
37 | router
38 | .route("/:id")
39 | .get(getRecipe)
40 | .put(
41 | [verifyJwt, verifyRoles(ROLES_LIST.Admin, ROLES_LIST.ProUser)],
42 | updateRecipe
43 | )
44 | .delete(
45 | [verifyJwt, verifyRoles(ROLES_LIST.Admin, ROLES_LIST.ProUser)],
46 | deleteRecipe
47 | );
48 |
49 | router
50 | .route("/comment/:id")
51 | .put(
52 | [
53 | verifyJwt,
54 | verifyRoles(ROLES_LIST.BasicUser, ROLES_LIST.ProUser, ROLES_LIST.Admin),
55 | ],
56 | addComment
57 | );
58 |
59 | router
60 | .route("/comment/:recipeId/:commentId")
61 | .delete(
62 | [
63 | verifyJwt,
64 | verifyRoles(ROLES_LIST.BasicUser, ROLES_LIST.ProUser, ROLES_LIST.Admin),
65 | ],
66 | deleteComment
67 | );
68 |
69 | router
70 | .route("/favorite/:id")
71 | .put(
72 | [
73 | verifyJwt,
74 | verifyRoles(ROLES_LIST.BasicUser, ROLES_LIST.ProUser, ROLES_LIST.Admin),
75 | ],
76 | toggleFavoriteRecipe
77 | );
78 |
79 | module.exports = router;
80 |
--------------------------------------------------------------------------------
/client/src/components/shareButton/ShareButton.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { AiOutlineShareAlt } from "react-icons/ai";
3 | import {
4 | FacebookShareButton,
5 | FacebookIcon,
6 | LinkedinShareButton,
7 | LinkedinIcon,
8 | TwitterShareButton,
9 | TwitterIcon,
10 | } from "react-share";
11 |
12 | const ShareButton = ({ url }) => {
13 | const [isMenuOpen, setIsMenuOpen] = useState(false);
14 |
15 | const toggleMenu = () => {
16 | setIsMenuOpen(!isMenuOpen);
17 | };
18 |
19 | return (
20 |
21 |
25 | {isMenuOpen && (
26 |
27 |
31 |
36 |
37 |
41 |
46 |
47 |
51 |
56 |
57 |
58 | )}
59 |
60 | );
61 | };
62 |
63 | export default ShareButton;
64 |
--------------------------------------------------------------------------------
/client/src/components/subscribe/Subscribe.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import SubscribeCard from "../cards/SubscribeCard";
3 | import { AiOutlineThunderbolt } from "react-icons/ai";
4 | import { IoDiamondOutline } from "react-icons/io5";
5 |
6 | const Subscribe = () => {
7 | return (
8 |
9 |
10 |
11 | Pay once, use forever
12 |
13 |
14 | Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam
15 | perferendis optio omnis? Id, ipsa eos.
16 |
17 |
18 |
19 |
23 | }
24 | price={"Free"}
25 | subtitle={"Perfect to get started"}
26 | featureTitle={"Basic includes:"}
27 | features={["Access to recipes", "Access to blogs", "Save recipes"]}
28 | btnText={"Continue for free"}
29 | link={"/recipe"}
30 | />
31 |
35 | }
36 | price={"₹999"}
37 | subtitle={"Best for professionals"}
38 | featureTitle={"Everything in Basic, plus:"}
39 | features={[
40 | "Add your own recipe",
41 | "Add your own blog",
42 | "Manage your content",
43 | ]}
44 | btnText={"Get started"}
45 | />
46 |
47 |
48 | );
49 | };
50 |
51 | export default Subscribe;
52 |
--------------------------------------------------------------------------------
/server/controllers/userController.js:
--------------------------------------------------------------------------------
1 | const User = require("../models/userModel");
2 | const bcrypt = require("bcrypt");
3 | const jwt = require("jsonwebtoken");
4 |
5 | const getAllUsers = async (req, res, next) => {
6 | try {
7 | const users = await User.find()
8 | .find({ _id: { $ne: req.user } })
9 | .select(["-password", "-refreshToken", "-favorites"]);
10 | res.status(200).json(users);
11 | } catch (error) {
12 | next(error);
13 | }
14 | };
15 |
16 | const updateUser = async (req, res, next) => {
17 | try {
18 | const { name, email, password, image } = req.body;
19 |
20 | const foundUser = await User.findOne({ email });
21 |
22 | if (foundUser._id.toString() !== req.user) {
23 | return res.status(409).json({ message: "Email already in use" });
24 | }
25 |
26 | const hashedPassword = await bcrypt.hash(password, 10);
27 |
28 | foundUser.name = name;
29 | foundUser.email = email;
30 | foundUser.password = hashedPassword;
31 | foundUser.profilePicture = image || foundUser.profilePicture;
32 |
33 | await foundUser.save();
34 |
35 | const accessToken = jwt.sign(
36 | {
37 | UserInfo: {
38 | userId: req.params._id,
39 | name: name,
40 | email: email,
41 | profilePicture: image || foundUser.profilePicture,
42 | roles: foundUser.roles,
43 | favorites: foundUser.favorites,
44 | },
45 | },
46 | process.env.ACCESS_TOKEN_SECRET,
47 | { expiresIn: "30m" }
48 | );
49 | return res.status(201).json({ accessToken });
50 | } catch (error) {
51 | next(error);
52 | }
53 | };
54 |
55 | const disableUser = async (req, res, next) => {
56 | try {
57 | const user = await User.findOneAndUpdate(
58 | { _id: req.params.id },
59 | { isDisabled: true }
60 | );
61 | res.sendStatus(204);
62 | } catch (error) {
63 | next(error);
64 | }
65 | };
66 |
67 | module.exports = { getAllUsers, updateUser, disableUser };
68 |
--------------------------------------------------------------------------------
/client/src/features/blog/blogApiSlice.js:
--------------------------------------------------------------------------------
1 | import { apiSlice } from "../../redux/apiSlice";
2 |
3 | export const blogApiSlice = apiSlice.injectEndpoints({
4 | endpoints: (builder) => ({
5 | getBlog: builder.query({
6 | query: (blogId) => `/blog/${blogId}`,
7 | providesTags: ["blogs"],
8 | }),
9 | getBlogs: builder.query({
10 | query: () => "/blog",
11 | providesTags: ["blogs"],
12 | }),
13 | addBlog: builder.mutation({
14 | query: (blogData) => ({
15 | url: "/blog",
16 | method: "POST",
17 | body: { ...blogData },
18 | }),
19 | invalidatesTags: ["blogs"],
20 | }),
21 | updateBlog: builder.mutation({
22 | query: (args) => {
23 | const { blogId, ...blogData } = args;
24 | return {
25 | url: `/blog/${blogId}`,
26 | method: "PUT",
27 | body: { ...blogData },
28 | };
29 | },
30 | invalidatesTags: ["blogs"],
31 | }),
32 | deleteBlog: builder.mutation({
33 | query: (blogId) => ({
34 | url: `/blog/${blogId}`,
35 | method: "DELETE",
36 | }),
37 | invalidatesTags: ["blogs"],
38 | }),
39 | commentBlog: builder.mutation({
40 | query: (args) => {
41 | const { blogId, comment } = args;
42 | return {
43 | url: `/blog/comment/${blogId}`,
44 | method: "PUT",
45 | body: { comment },
46 | };
47 | },
48 | invalidatesTags: ["blogs"],
49 | }),
50 | deleteCommentBlog: builder.mutation({
51 | query: (args) => {
52 | const { blogId, commentId } = args;
53 | return {
54 | url: `/blog/comment/${blogId}/${commentId}`,
55 | method: "DELETE",
56 | };
57 | },
58 | invalidatesTags: ["blogs"],
59 | }),
60 | }),
61 | });
62 |
63 | export const {
64 | useGetBlogQuery,
65 | useGetBlogsQuery,
66 | useAddBlogMutation,
67 | useUpdateBlogMutation,
68 | useDeleteBlogMutation,
69 | useCommentBlogMutation,
70 | useDeleteCommentBlogMutation,
71 | } = blogApiSlice;
72 |
--------------------------------------------------------------------------------
/client/src/components/cards/AllCards.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import { NoData, SingleCard } from "..";
3 |
4 | const index = ({ mainTitle, tagline, type, data }) => {
5 | const [searchTerm, setSearchTerm] = useState("");
6 | const [filteredData, setFilteredData] = useState(data);
7 |
8 | useEffect(() => {
9 | const newFilteredData = data?.filter((element) =>
10 | element.title.toLowerCase().includes(searchTerm.toLowerCase())
11 | );
12 | setFilteredData(newFilteredData);
13 | }, [searchTerm, data]);
14 |
15 | return (
16 |
17 |
18 | {/* Main heading */}
19 |
20 | {mainTitle}
21 |
22 | {/* Subtitle */}
23 |
{tagline}
24 | {/* Search */}
25 |
26 | setSearchTerm(e.target.value)}
31 | className="focus:outline-none w-full py-2"
32 | placeholder={`Search ${type}...`}
33 | />
34 |
35 |
36 |
37 | {/* Sub heading */}
38 |
Recent {type}s
39 | {/* Cards container */}
40 | {filteredData?.length ? (
41 |
42 | {filteredData?.map((singleData) => (
43 |
48 | ))}
49 |
50 | ) : (
51 |
52 | )}
53 |
54 |
55 | );
56 | };
57 |
58 | export default index;
59 |
--------------------------------------------------------------------------------
/client/src/components/input/Input.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { AiOutlineEyeInvisible, AiOutlineEye } from "react-icons/ai";
3 |
4 | const Input = ({
5 | icon,
6 | handleChange,
7 | label,
8 | id,
9 | type,
10 | value,
11 | placeholder,
12 | pattern,
13 | errorMessage,
14 | }) => {
15 | const [showPassword, setShowPassword] = useState(false);
16 | const [focused, setFocused] = useState(false);
17 |
18 | const handleFocus = () => {
19 | setFocused(true);
20 | };
21 |
22 | return (
23 |
24 |
28 | {label}
29 |
30 |
31 | {icon}
32 |
33 |
48 | {type === "password" && (
49 | <>
50 | {showPassword && (
51 |
setShowPassword(!showPassword)}
53 | className="absolute top-[42px] right-3 cursor-pointer text-gray-700 text-lg"
54 | />
55 | )}
56 | {!showPassword && (
57 | setShowPassword(!showPassword)}
59 | className="absolute top-[42px] right-3 cursor-pointer text-gray-700 text-lg"
60 | />
61 | )}
62 | >
63 | )}
64 |
68 | {errorMessage}
69 |
70 |
71 | );
72 | };
73 |
74 | export default Input;
75 |
--------------------------------------------------------------------------------
/client/src/components/cards/SubscribeCard.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Button } from "..";
3 | import { BsCheckLg } from "react-icons/bs";
4 | import { Link } from "react-router-dom";
5 | import { toast } from "react-toastify";
6 | import { useSubscribeUserMutation } from "../../features/user/userApiSlice";
7 |
8 | const SubscribeCard = ({
9 | title,
10 | icon,
11 | price,
12 | subtitle,
13 | features,
14 | featureTitle,
15 | btnText,
16 | link,
17 | }) => {
18 | const [subscribeUser] = useSubscribeUserMutation();
19 |
20 | const handleClick = async () => {
21 | try {
22 | const { url } = await subscribeUser().unwrap();
23 | window.location.href = url;
24 | } catch (error) {
25 | toast.error(error.data);
26 | console.error(error);
27 | }
28 | };
29 |
30 | return (
31 |
32 |
33 | {icon}
34 |
{title}
35 |
36 |
{subtitle}
37 |
{price}
38 | {link ? (
39 |
43 |
47 |
48 | ) : (
49 |
54 | )}
55 |
56 |
{featureTitle}
57 |
58 | {features?.map((feature) => (
59 |
63 |
64 | {feature}
65 |
66 | ))}
67 |
68 |
69 |
70 | );
71 | };
72 |
73 | export default SubscribeCard;
74 |
--------------------------------------------------------------------------------
/client/src/components/button/Button.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { motion } from "framer-motion";
3 |
4 | const Button = ({
5 | content,
6 | customCss,
7 | type = "button",
8 | loading,
9 | handleClick = null,
10 | icon = null,
11 | }) => {
12 | return (
13 | <>
14 |
21 | {loading ? (
22 | <>
23 |
31 |
35 |
39 |
40 | {"Loading..."}
41 | >
42 | ) : (
43 | <>
44 | {content}
45 | {icon}
46 | >
47 | )}
48 |
49 | >
50 | );
51 | };
52 |
53 | export default Button;
54 |
--------------------------------------------------------------------------------
/client/src/components/comment/Comment.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { BsCalendarCheck } from "react-icons/bs";
3 | import { IconButton, Menu, MenuItem, Avatar as MuiAvatar } from "@mui/material";
4 | import dateFormat from "../../common/dateFormat";
5 | import { MoreVert } from "@mui/icons-material";
6 |
7 | const Comment = ({ comment, userId, handleDeleteComment }) => {
8 | const formattedDate = dateFormat(comment?.date);
9 | const [anchorEl, setAnchorEl] = React.useState(null);
10 | const open = Boolean(anchorEl);
11 |
12 | const handleClick = (event) => {
13 | setAnchorEl(event.currentTarget);
14 | };
15 |
16 | const handleClose = () => {
17 | setAnchorEl(null);
18 | };
19 |
20 | const handleDelete = () => {
21 | setAnchorEl(null);
22 | handleDeleteComment(comment?._id);
23 | };
24 |
25 | return (
26 |
27 | {/* Commented user details */}
28 |
34 |
35 |
36 |
{comment?.user?.name}
37 |
38 |
39 | {formattedDate}
40 |
41 |
42 | {/* Comment content */}
43 |
44 |
{comment?.comment}
45 | {comment?.user?._id === userId && (
46 | <>
47 |
56 |
57 |
58 |
69 | >
70 | )}
71 |
72 |
73 |
74 | );
75 | };
76 |
77 | export default Comment;
78 |
--------------------------------------------------------------------------------
/client/src/features/recipe/recipeApiSlice.js:
--------------------------------------------------------------------------------
1 | import { apiSlice } from "../../redux/apiSlice";
2 |
3 | export const recipeApiSlice = apiSlice.injectEndpoints({
4 | endpoints: (builder) => ({
5 | getRecipe: builder.query({
6 | query: (recipeId) => `/recipe/${recipeId}`,
7 | providesTags: ["recipes"],
8 | }),
9 | getRecipes: builder.query({
10 | query: () => "/recipe",
11 | providesTags: ["recipes"],
12 | }),
13 | addRecipe: builder.mutation({
14 | query: (recipeData) => ({
15 | url: "/recipe",
16 | method: "POST",
17 | body: { ...recipeData },
18 | }),
19 | invalidatesTags: ["recipes"],
20 | }),
21 | updateRecipe: builder.mutation({
22 | query: (args) => {
23 | const { recipeId, ...recipeData } = args;
24 | return {
25 | url: `/recipe/${recipeId}`,
26 | method: "PUT",
27 | body: { ...recipeData },
28 | };
29 | },
30 | invalidatesTags: ["recipes"],
31 | }),
32 | rateRecipe: builder.mutation({
33 | query: (args) => {
34 | const { recipeId, rating } = args;
35 | return {
36 | url: `/recipe/rate/${recipeId}`,
37 | method: "PUT",
38 | body: { rating },
39 | };
40 | },
41 | invalidatesTags: ["recipes"],
42 | }),
43 | deleteRecipe: builder.mutation({
44 | query: (recipeId) => ({
45 | url: `/recipe/${recipeId}`,
46 | method: "DELETE",
47 | }),
48 | invalidatesTags: ["recipes"],
49 | }),
50 | commentRecipe: builder.mutation({
51 | query: (args) => {
52 | const { recipeId, comment } = args;
53 | return {
54 | url: `/recipe/comment/${recipeId}`,
55 | method: "PUT",
56 | body: { comment },
57 | };
58 | },
59 | invalidatesTags: ["recipes"],
60 | }),
61 | deleteCommentRecipe: builder.mutation({
62 | query: (args) => {
63 | const { recipeId, commentId } = args;
64 | return {
65 | url: `/recipe/comment/${recipeId}/${commentId}`,
66 | method: "DELETE",
67 | };
68 | },
69 | invalidatesTags: ["recipes"],
70 | }),
71 | toggleFavorite: builder.mutation({
72 | query: ({ recipeId }) => {
73 | return {
74 | url: `/recipe/favorite/${recipeId}`,
75 | method: "PUT",
76 | };
77 | },
78 | invalidatesTags: ["recipes"],
79 | }),
80 | }),
81 | });
82 |
83 | export const {
84 | useGetRecipeQuery,
85 | useGetRecipesQuery,
86 | useAddRecipeMutation,
87 | useUpdateRecipeMutation,
88 | useRateRecipeMutation,
89 | useDeleteRecipeMutation,
90 | useCommentRecipeMutation,
91 | useDeleteCommentRecipeMutation,
92 | useToggleFavoriteMutation,
93 | } = recipeApiSlice;
94 |
--------------------------------------------------------------------------------
/client/src/assets/photo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/src/assets/componentLoading.json:
--------------------------------------------------------------------------------
1 | {"v":"5.5.5","fr":30,"ip":0,"op":90,"w":300,"h":150,"nm":"Loading-2","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"icon 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":39,"s":[-90]},{"t":79,"s":[270]}],"ix":10},"p":{"a":0,"k":[150,75,0],"ix":2},"a":{"a":0,"k":[53,53,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-15.464],[15.464,0],[0,15.464],[-15.464,0]],"o":[[0,15.464],[-15.464,0],[0,-15.464],[15.464,0]],"v":[[28,0],[0,28],[-28,0],[0,-28]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.9607843137254902,0.6509803921568628,0.13725490196078433,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":10,"ix":5},"lc":2,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[53,53],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":51,"s":[0]},{"t":79,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":39,"s":[0]},{"t":64,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":39,"op":79,"st":39,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"icon","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[-90]},{"t":40,"s":[270]}],"ix":10},"p":{"a":0,"k":[150,75,0],"ix":2},"a":{"a":0,"k":[53,53,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-15.464],[15.464,0],[0,15.464],[-15.464,0]],"o":[[0,15.464],[-15.464,0],[0,-15.464],[15.464,0]],"v":[[28,0],[0,28],[-28,0],[0,-28]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.9607843137254902,0.6509803921568628,0.13725490196078433,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":10,"ix":5},"lc":2,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[53,53],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":12,"s":[0]},{"t":40,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"t":25,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":40,"st":0,"bm":0}],"markers":[]}
--------------------------------------------------------------------------------
/client/src/pages/dashboard/DashboardBlogs.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 | import { ComponentLoading, Table } from "../../components";
3 | import { setBlogs } from "../../features/blog/blogSlice";
4 | import { useDispatch } from "react-redux";
5 | import {
6 | useGetBlogsQuery,
7 | useDeleteBlogMutation,
8 | } from "../../features/blog/blogApiSlice";
9 | import { Avatar as MuiAvatar } from "@mui/material";
10 | import dateFormat from "../../common/dateFormat";
11 |
12 | const DashboardBlogs = () => {
13 | const { data, isLoading } = useGetBlogsQuery();
14 | const dispatch = useDispatch();
15 | const updatedData = data?.map((item, index) => ({
16 | ...item,
17 | id: index + 1,
18 | }));
19 | const [deleteBlog] = useDeleteBlogMutation();
20 |
21 | const handleDelete = (_id) => {
22 | if (window.confirm("Are you sure you want to delete?")) {
23 | deleteBlog(_id);
24 | }
25 | };
26 |
27 | useEffect(() => {
28 | if (!isLoading) {
29 | dispatch(setBlogs(data));
30 | }
31 | }, [isLoading]);
32 |
33 | const cols = [
34 | { field: "id", headerName: "ID", width: 100 },
35 | {
36 | field: "title",
37 | headerName: "Title",
38 | width: 350,
39 | headerAlign: "center",
40 | align: "left",
41 | },
42 | {
43 | field: "author",
44 | headerName: "Author",
45 | headerAlign: "center",
46 | align: "left",
47 | minWidth: 250,
48 | renderCell: ({ row: { author } }) => {
49 | return (
50 |
51 |
57 | {author.name}
58 |
59 | );
60 | },
61 | },
62 | {
63 | field: "createdAt",
64 | headerName: "Date Created",
65 | width: 200,
66 | headerAlign: "center",
67 | align: "center",
68 | renderCell: ({ row: { createdAt } }) => {
69 | const formattedDate = dateFormat(createdAt);
70 | return {formattedDate}
;
71 | },
72 | },
73 | {
74 | headerName: "Actions",
75 | headerAlign: "center",
76 | align: "center",
77 | minWidth: 250,
78 | renderCell: ({ row: { _id } }) => {
79 | return (
80 | handleDelete(_id)}
84 | >
85 | Delete
86 |
87 | );
88 | },
89 | },
90 | ];
91 |
92 | return (
93 |
94 |
95 | {isLoading ? (
96 |
97 | ) : (
98 |
102 | )}
103 |
104 |
105 | );
106 | };
107 |
108 | export default DashboardBlogs;
109 |
--------------------------------------------------------------------------------
/client/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
9 |
13 |
17 |
21 |
25 |
29 |
33 |
37 |
38 |
39 |
43 |
47 |
51 |
55 |
59 |
63 |
64 |
65 |
69 |
70 | Recipen
71 |
76 |
82 |
88 |
92 |
96 |
97 |
98 |
99 |
103 |
104 |
105 |
--------------------------------------------------------------------------------
/client/src/pages/dashboard/DashboardRecipes.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 | import { ComponentLoading, Table } from "../../components";
3 | import { setRecipes } from "../../features/recipe/recipeSlice";
4 | import { useDispatch } from "react-redux";
5 | import {
6 | useGetRecipesQuery,
7 | useDeleteRecipeMutation,
8 | } from "../../features/recipe/recipeApiSlice";
9 | import { Avatar as MuiAvatar, Rating } from "@mui/material";
10 |
11 | const DashboardRecipes = () => {
12 | const { data, isLoading } = useGetRecipesQuery();
13 |
14 | const dispatch = useDispatch();
15 | const updatedData = data?.map((item, index) => ({
16 | ...item,
17 | id: index + 1,
18 | }));
19 | const [deleteRecipe] = useDeleteRecipeMutation();
20 |
21 | const handleDelete = (_id) => {
22 | if (window.confirm("Are you sure you want to delete?")) {
23 | deleteRecipe(_id);
24 | }
25 | };
26 |
27 | useEffect(() => {
28 | if (!isLoading) {
29 | dispatch(setRecipes(data));
30 | }
31 | }, [isLoading]);
32 |
33 | const cols = [
34 | { field: "id", headerName: "ID", width: 100 },
35 | {
36 | field: "title",
37 | headerName: "Title",
38 | width: 280,
39 | headerAlign: "center",
40 | align: "left",
41 | },
42 | {
43 | field: "author",
44 | headerName: "Author",
45 | headerAlign: "center",
46 | align: "left",
47 | minWidth: 250,
48 | renderCell: ({ row: { author } }) => {
49 | return (
50 |
51 |
57 | {author.name}
58 |
59 | );
60 | },
61 | },
62 | {
63 | field: "ratings",
64 | headerName: "Rating",
65 | width: 250,
66 | headerAlign: "center",
67 | align: "center",
68 | renderCell: ({ row: { ratings } }) => {
69 | const sumOfRatings = ratings.reduce(
70 | (sum, item) => sum + item.rating,
71 | 0
72 | );
73 | const averageRating =
74 | sumOfRatings === 0 ? 0 : sumOfRatings / ratings.length;
75 | return (
76 |
81 | );
82 | },
83 | },
84 | {
85 | headerName: "Actions",
86 | headerAlign: "center",
87 | align: "center",
88 | minWidth: 250,
89 | renderCell: ({ row: { _id } }) => {
90 | return (
91 | handleDelete(_id)}
95 | >
96 | Delete
97 |
98 | );
99 | },
100 | },
101 | ];
102 |
103 | return (
104 |
105 |
106 | {isLoading ? (
107 |
108 | ) : (
109 |
113 | )}
114 |
115 |
116 | );
117 | };
118 |
119 | export default DashboardRecipes;
120 |
--------------------------------------------------------------------------------
/client/src/pages/auth/SignIn.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { Button, Input, Logo } from "../../components";
3 | import { IoMailOutline } from "react-icons/io5";
4 | import { BiLockAlt } from "react-icons/bi";
5 | import { Link, useNavigate } from "react-router-dom";
6 | import { useSignInMutation } from "../../features/auth/authApiSlice";
7 | import { useDispatch } from "react-redux";
8 | import { setCredentials } from "../../features/auth/authSlice";
9 | import { toast } from "react-toastify";
10 | import useTitle from "../../hooks/useTitle";
11 |
12 | const SignIn = () => {
13 | const [formDetails, setFormDetails] = useState({
14 | email: "",
15 | password: "",
16 | });
17 | const [signIn, { isLoading }] = useSignInMutation();
18 | const dispatch = useDispatch();
19 | const navigate = useNavigate();
20 | useTitle("Recipen - Sign In");
21 |
22 | const handleChange = (e) => {
23 | setFormDetails({ ...formDetails, [e.target.id]: e.target.value });
24 | };
25 |
26 | const handleSubmit = async (e) => {
27 | e.preventDefault();
28 |
29 | try {
30 | const userData = await toast.promise(
31 | signIn({ ...formDetails }).unwrap(),
32 | {
33 | pending: "Please wait...",
34 | success: "Sign in successfull",
35 | error: "Sign in failed",
36 | }
37 | );
38 | dispatch(setCredentials({ ...userData }));
39 | localStorage.setItem("persist", true);
40 | setFormDetails({ email: "", password: "" });
41 | navigate("/");
42 | } catch (error) {
43 | toast.error(error.data);
44 | console.error(error);
45 | }
46 | };
47 |
48 | return (
49 |
50 | {/* Sign in form container */}
51 |
52 |
53 | {/* Sign in form heading */}
54 |
55 |
56 | Welcome back
57 |
58 |
59 | New to Recipen?{" "}
60 |
64 | Create an account
65 |
66 |
67 |
68 | {/* Sign in form */}
69 |
98 |
99 | {/* Sign in banner image */}
100 |
101 |
102 | );
103 | };
104 |
105 | export default SignIn;
106 |
--------------------------------------------------------------------------------
/client/src/components/header/Header.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { Logo, Button, Menu, Avatar } from "..";
3 | import { Link, NavLink } from "react-router-dom";
4 | import { FiLogIn, FiMenu } from "react-icons/fi";
5 | import useAuth from "../../hooks/useAuth";
6 |
7 | const Header = () => {
8 | const [isCollapsed, setIsCollapsed] = useState(true);
9 | const user = useAuth();
10 |
11 | return (
12 |
92 | );
93 | };
94 |
95 | export default Header;
96 |
--------------------------------------------------------------------------------
/client/src/pages/dashboard/Users.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 | import { ComponentLoading, Table } from "../../components";
3 | import { MdAdminPanelSettings } from "react-icons/md";
4 | import { BiSolidUser } from "react-icons/bi";
5 | import { RiAdminFill } from "react-icons/ri";
6 | import { useDispatch } from "react-redux";
7 | import { setUsers } from "../../features/user/userSlice";
8 | import {
9 | useGetUsersQuery,
10 | useDisableUserMutation,
11 | } from "../../features/user/userApiSlice";
12 | import { Avatar as MuiAvatar } from "@mui/material";
13 |
14 | const Users = () => {
15 | const { data, isLoading } = useGetUsersQuery();
16 | const dispatch = useDispatch();
17 | const updatedData = data?.map((item, index) => ({
18 | ...item,
19 | id: index + 1,
20 | }));
21 | const [disableUser] = useDisableUserMutation();
22 |
23 | const handleDisable = (_id) => {
24 | disableUser(_id);
25 | };
26 | console.log(data);
27 | useEffect(() => {
28 | if (!isLoading) {
29 | dispatch(setUsers(data));
30 | }
31 | }, [isLoading]);
32 |
33 | const cols = [
34 | { field: "id", headerName: "ID", width: 100 },
35 | {
36 | field: "name",
37 | headerName: "Name",
38 | headerAlign: "center",
39 | align: "left",
40 | minWidth: 250,
41 | renderCell: ({ row: { name, profilePicture } }) => {
42 | return (
43 |
44 |
50 | {name}
51 |
52 | );
53 | },
54 | },
55 | {
56 | field: "email",
57 | headerName: "Email",
58 | headerAlign: "center",
59 | align: "left",
60 | minWidth: 280,
61 | },
62 | {
63 | field: "roles",
64 | headerName: "Role",
65 | headerAlign: "center",
66 | minWidth: 300,
67 | renderCell: ({ row: { roles } }) => {
68 | return (
69 |
74 | {roles?.includes("Admin") ? (
75 |
76 | ) : roles?.includes("ProUser") ? (
77 |
78 | ) : (
79 |
80 | )}
81 | {roles?.includes("Admin")
82 | ? "Admin"
83 | : roles?.includes("ProUser")
84 | ? "Pro User"
85 | : "Basic User"}
86 |
87 | );
88 | },
89 | },
90 | {
91 | field: "isDisabled",
92 | headerName: "Disabled",
93 | headerAlign: "center",
94 | align: "center",
95 | minWidth: 250,
96 | renderCell: ({ row: { isDisabled, _id } }) => {
97 | return (
98 | handleDisable(_id)}
105 | >
106 | {isDisabled ? "Disabled" : "Disable"}
107 |
108 | );
109 | },
110 | },
111 | ];
112 |
113 | return (
114 |
115 |
116 | {isLoading ? (
117 |
118 | ) : (
119 |
123 | )}
124 |
125 |
126 | );
127 | };
128 |
129 | export default Users;
130 |
--------------------------------------------------------------------------------
/client/src/components/header/Menu.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { FiLogIn, FiMenu } from "react-icons/fi";
3 | import { Link, NavLink } from "react-router-dom";
4 | import { motion } from "framer-motion";
5 | import { Button } from "..";
6 |
7 | const Menu = ({ isCollapsed, setIsCollapsed, user }) => {
8 | return (
9 | <>
10 | {!isCollapsed && (
11 |
17 | {/* Menu button */}
18 | setIsCollapsed(!isCollapsed)}
21 | />
22 | {/* Navbar links */}
23 |
24 |
25 | setIsCollapsed(!isCollapsed)}
28 | className="relative w-fit block after:block after:content-[''] after:absolute after:h-[2px] after:bg-primary after:w-full after:scale-x-0 after:hover:scale-x-100 after:transition after:duration-300 after:origin-center font-semibold text-gray-600"
29 | >
30 | Home
31 |
32 |
33 | {user && user?.isAdmin && (
34 |
35 | setIsCollapsed(!isCollapsed)}
38 | className="relative w-fit block after:block after:content-[''] after:absolute after:h-[2px] after:bg-primary after:w-full after:scale-x-0 after:hover:scale-x-100 after:transition after:duration-300 after:origin-center font-semibold text-gray-600"
39 | >
40 | Dashboard
41 |
42 |
43 | )}
44 |
45 | setIsCollapsed(!isCollapsed)}
48 | className="relative w-fit block after:block after:content-[''] after:absolute after:h-[2px] after:bg-primary after:w-full after:scale-x-0 after:hover:scale-x-100 after:transition after:duration-300 after:origin-center font-semibold text-gray-600"
49 | >
50 | Recipes
51 |
52 |
53 |
54 | setIsCollapsed(!isCollapsed)}
57 | className="relative w-fit block after:block after:content-[''] after:absolute after:h-[2px] after:bg-primary after:w-full after:scale-x-0 after:hover:scale-x-100 after:transition after:duration-300 after:origin-center font-semibold text-gray-600"
58 | >
59 | Blogs
60 |
61 |
62 |
63 | setIsCollapsed(!isCollapsed)}
66 | className="relative w-fit block after:block after:content-[''] after:absolute after:h-[2px] after:bg-primary after:w-full after:scale-x-0 after:hover:scale-x-100 after:transition after:duration-300 after:origin-center font-semibold text-gray-600"
67 | >
68 | Contact
69 |
70 |
71 | {!user && (
72 |
73 | setIsCollapsed(!isCollapsed)}
76 | >
77 | }
81 | />
82 |
83 |
84 | )}
85 |
86 |
87 | )}
88 | >
89 | );
90 | };
91 |
92 | export default Menu;
93 |
--------------------------------------------------------------------------------
/server/controllers/blogController.js:
--------------------------------------------------------------------------------
1 | const Blog = require("../models/blogModel");
2 |
3 | const getAllBlogs = async (req, res, next) => {
4 | try {
5 | const blogs = await Blog.find().sort({ createdAt: -1 }).populate("author");
6 | res.status(200).json(blogs);
7 | } catch (error) {
8 | next(error);
9 | }
10 | };
11 |
12 | const getBlog = async (req, res, next) => {
13 | try {
14 | const blog = await Blog.findOne({ _id: req.params.id })
15 | .populate("author", "name")
16 | .populate("comments.user", ["name", "profilePicture"]);
17 |
18 | if (!blog) return res.status(404).json({ message: "Blog not found" });
19 |
20 | res.status(200).json(blog);
21 | } catch (error) {
22 | next(error);
23 | }
24 | };
25 |
26 | const addBlog = async (req, res, next) => {
27 | try {
28 | const { title, image, description } = req.body;
29 | if (!title || !image || !description) {
30 | return res.status(422).json({ message: "Insufficient data" });
31 | }
32 |
33 | const blog = Blog({ ...req.body, author: req.user });
34 | await blog.save();
35 | return res.status(201).json({ success: "Blog added successfully" });
36 | } catch (error) {
37 | next(error);
38 | }
39 | };
40 |
41 | const updateBlog = async (req, res, next) => {
42 | try {
43 | const { title, image, description } = req.body;
44 | if (!title || !image || !description) {
45 | return res.status(422).json({ message: "Insufficient data" });
46 | }
47 |
48 | const foundBlog = await Blog.findById(req.params.id);
49 | if (!foundBlog) return res.status(404).json({ message: "Blog not found" });
50 |
51 | if (foundBlog.author !== req.user)
52 | return res.status(401).json({ message: "Unauthorized" });
53 |
54 | foundBlog.title = title;
55 | foundBlog.image = image;
56 | foundBlog.description = description;
57 |
58 | const updatedBlog = await foundBlog.save();
59 |
60 | return res.status(201).json(updatedBlog);
61 | } catch (error) {
62 | next(error);
63 | }
64 | };
65 |
66 | const deleteBlog = async (req, res, next) => {
67 | try {
68 | const foundBlog = await Blog.findById(req.params.id);
69 |
70 | if (!foundBlog) return res.status(404).json({ message: "Blog not found" });
71 |
72 | if (foundBlog.author !== req.user)
73 | return res.status(401).json({ message: "Unauthorized" });
74 |
75 | await foundBlog.deleteOne({ _id: req.params.id });
76 |
77 | res.sendStatus(204);
78 | } catch (error) {
79 | next(error);
80 | }
81 | };
82 |
83 | const addComment = async (req, res, next) => {
84 | try {
85 | const { comment } = req.body;
86 |
87 | // Validate userId and commentText
88 | if (!comment) {
89 | return res.status(400).json({ message: "Comment is required." });
90 | }
91 |
92 | const blog = await Blog.findById(req.params.id);
93 | if (!blog) {
94 | return res.status(404).json({ message: "Blog not found." });
95 | }
96 |
97 | // Add the new comment
98 | blog.comments.push({ user: req.user, comment });
99 | await blog.save();
100 |
101 | res.status(201).json({ message: "Comment added successfully." });
102 | } catch (error) {
103 | next(error);
104 | }
105 | };
106 |
107 | const deleteComment = async (req, res, next) => {
108 | try {
109 | const { blogId, commentId } = req.params;
110 |
111 | const blog = await Blog.findById(blogId);
112 | if (!blog) {
113 | return res.status(404).json({ message: "Blog not found." });
114 | }
115 |
116 | const commentIndex = blog.comments.findIndex((comment) =>
117 | comment._id.equals(commentId)
118 | );
119 | if (commentIndex === -1) {
120 | return res.status(404).json({ message: "Comment not found." });
121 | }
122 |
123 | blog.comments.splice(commentIndex, 1);
124 | await blog.save();
125 |
126 | res.status(200).json({ message: "Comment deleted successfully." });
127 | } catch (error) {
128 | next(error);
129 | }
130 | };
131 |
132 | module.exports = {
133 | getAllBlogs,
134 | getBlog,
135 | addBlog,
136 | updateBlog,
137 | deleteBlog,
138 | addComment,
139 | deleteComment,
140 | };
141 |
--------------------------------------------------------------------------------
/client/src/pages/auth/SignUp.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { Button, Input, Logo } from "../../components";
3 | import { IoMailOutline } from "react-icons/io5";
4 | import { BiLockAlt } from "react-icons/bi";
5 | import { AiOutlineUser } from "react-icons/ai";
6 | import { Link, useNavigate } from "react-router-dom";
7 | import { useSignUpMutation } from "../../features/auth/authApiSlice";
8 | import { toast } from "react-toastify";
9 | import useTitle from "../../hooks/useTitle";
10 |
11 | const SignUp = () => {
12 | const [formDetails, setFormDetails] = useState({
13 | name: "",
14 | email: "",
15 | password: "",
16 | });
17 | const [signUp, { isLoading }] = useSignUpMutation();
18 | const navigate = useNavigate();
19 | useTitle("Recipen - Sign Up");
20 |
21 | const handleChange = (e) => {
22 | setFormDetails({ ...formDetails, [e.target.id]: e.target.value });
23 | };
24 |
25 | const handleSubmit = async (e) => {
26 | e.preventDefault();
27 |
28 | try {
29 | const userData = await toast.promise(
30 | signUp({ ...formDetails }).unwrap(),
31 | {
32 | pending: "Please wait...",
33 | success: "Sign up successfull",
34 | error: "Sign up failed",
35 | }
36 | );
37 | setFormDetails({ name: "", email: "", password: "" });
38 | navigate("/auth/signin");
39 | } catch (error) {
40 | toast.error(error.data);
41 | console.error(error);
42 | }
43 | };
44 |
45 | return (
46 |
47 | {/* Sign up form container */}
48 |
49 |
50 | {/* Sign up form heading */}
51 |
52 |
53 | Create an account
54 |
55 |
56 | Already have an account?{" "}
57 |
61 | Sign In
62 |
63 |
64 |
65 | {/* Sign up form */}
66 |
114 |
115 | {/* Sign up banner image */}
116 |
117 |
118 | );
119 | };
120 |
121 | export default SignUp;
122 |
--------------------------------------------------------------------------------
/client/src/components/cards/SingleCard.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { BsArrowUpRight } from "react-icons/bs";
3 | import { AiOutlineHeart, AiFillHeart } from "react-icons/ai";
4 | import { Link, useNavigate } from "react-router-dom";
5 | import { Rating } from "@mui/material";
6 | import dateFormat from "../../common/dateFormat";
7 | import { toast } from "react-toastify";
8 | import { useToggleFavoriteMutation } from "../../features/recipe/recipeApiSlice";
9 | import { setCredentials } from "../../features/auth/authSlice";
10 | import { useDispatch } from "react-redux";
11 | import ShareButton from "../shareButton/ShareButton";
12 | import useAuth from "../../hooks/useAuth";
13 |
14 | const SingleCard = ({ singleData, type }) => {
15 | const user = useAuth();
16 |
17 | const navigate = useNavigate();
18 |
19 | const dispatch = useDispatch();
20 | const [toggleFavorite] = useToggleFavoriteMutation();
21 |
22 | const formattedDate = dateFormat(singleData?.createdAt);
23 | const sumOfRatings = singleData?.ratings.reduce(
24 | (sum, item) => sum + item.rating,
25 | 0
26 | );
27 | const averageRating =
28 | sumOfRatings === 0 ? 0 : sumOfRatings / singleData?.ratings.length;
29 |
30 | const handleToggleFavorite = async () => {
31 | try {
32 | if (!user) {
33 | toast.error("You must sign in first");
34 | return navigate("/auth/signin");
35 | }
36 | const userData = await toast.promise(
37 | toggleFavorite({ recipeId: singleData._id }).unwrap(),
38 | {
39 | pending: "Please wait...",
40 | success: "Favorites updated",
41 | error: "Unable to update favorites",
42 | }
43 | );
44 | dispatch(setCredentials({ ...userData }));
45 | } catch (error) {
46 | toast.error(error.data);
47 | console.error(error);
48 | }
49 | };
50 |
51 | return (
52 |
53 | {/* Card Top */}
54 |
55 |
56 | {/* Only for singleData */}
57 | {/* Favorite & share button */}
58 | {type === "recipe" && (
59 |
60 | {user?.favorites?.some((ele) => ele === singleData._id) ? (
61 |
65 | ) : (
66 |
70 | )}
71 |
76 |
77 | )}
78 | {/* Card image */}
79 |
84 | {/* Overlay */}
85 |
86 |
{singleData?.author?.name}
87 | {formattedDate}
88 |
89 |
90 | {/* Card Bottom details */}
91 |
92 | {/* Card heading */}
93 |
{singleData?.title}
94 | {/* Card description */}
95 |
96 | {singleData?.description.substring(0, 100)}...
97 |
98 | {/* Card rating */}
99 | {type === "recipe" && (
100 |
105 | )}
106 |
107 |
108 | {/* Read more link */}
109 |
113 | Read more
114 |
115 |
116 |
117 | );
118 | };
119 |
120 | export default SingleCard;
121 |
--------------------------------------------------------------------------------
/client/src/components/dashboard/Sidebar.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { FiMenu } from "react-icons/fi";
3 | import { NavLink, useNavigate } from "react-router-dom";
4 | import { motion } from "framer-motion";
5 | import { BiHomeAlt2 } from "react-icons/bi";
6 | import { BsFileEarmarkText } from "react-icons/bs";
7 | import { HiOutlineUsers, HiOutlineLogout } from "react-icons/hi";
8 | import { IoRestaurantOutline } from "react-icons/io5";
9 | import { Logo } from "..";
10 | import { Avatar as MuiAvatar } from "@mui/material";
11 | import useAuth from "../../hooks/useAuth";
12 | import { useLogoutMutation } from "../../features/auth/authApiSlice";
13 |
14 | const index = ({ isCollapsed, setIsCollapsed }) => {
15 | const user = useAuth();
16 |
17 | const [logout] = useLogoutMutation();
18 |
19 | const navigate = useNavigate();
20 |
21 | const handleLogout = () => {
22 | logout();
23 | localStorage.setItem("persist", false);
24 | navigate("/auth/signin");
25 | };
26 |
27 | return (
28 |
29 |
30 |
35 |
36 | {isCollapsed &&
}
37 | setIsCollapsed(!isCollapsed)}
40 | />
41 |
42 |
43 | {/* Profile details */}
44 |
45 |
46 |
52 |
53 | {!isCollapsed && (
54 |
55 |
{user?.name}
56 |
{user?.email}
57 |
58 | )}
59 |
60 |
61 | {/* Navbar links */}
62 |
63 |
69 |
70 | {!isCollapsed && "Home"}
71 |
72 |
78 |
79 | {!isCollapsed && "Users"}
80 |
81 |
87 |
88 | {!isCollapsed && "Recipes"}
89 |
90 |
96 |
97 | {!isCollapsed && "Blogs"}
98 |
99 |
100 |
101 |
102 |
108 |
109 | {!isCollapsed && "Logout"}
110 |
111 |
112 | );
113 | };
114 |
115 | export default index;
116 |
--------------------------------------------------------------------------------
/server/controllers/authController.js:
--------------------------------------------------------------------------------
1 | const User = require("../models/userModel");
2 | const bcrypt = require("bcrypt");
3 | const jwt = require("jsonwebtoken");
4 |
5 | const register = async (req, res, next) => {
6 | const { name, email, password } = req.body;
7 |
8 | if (!name || !email || !password) {
9 | return res.status(400).json({ message: "All inputs are required" });
10 | }
11 |
12 | const duplicate = await User.findOne({ email });
13 | if (duplicate) return res.sendStatus(409);
14 |
15 | try {
16 | const hashedPassword = await bcrypt.hash(password, 10);
17 | const user = await User({
18 | ...req.body,
19 | password: hashedPassword,
20 | });
21 |
22 | const result = user.save();
23 | res.status(201).json({ success: "User registered successfully" });
24 | } catch (error) {
25 | next(error);
26 | }
27 | };
28 |
29 | const login = async (req, res, next) => {
30 | try {
31 | const { email, password } = req.body;
32 | if (!email || !password) {
33 | return res
34 | .status(400)
35 | .json({ message: "Email and password are required" });
36 | }
37 |
38 | const foundUser = await User.findOne({ email });
39 | if (!foundUser) {
40 | return res.status(401).json({ message: "Unauthorized" });
41 | }
42 | if (foundUser.disabled) {
43 | return res.status(403).json({ message: "Account terminated" });
44 | }
45 |
46 | const match = await bcrypt.compare(password, foundUser.password);
47 |
48 | if (!match) {
49 | return res.status(401).json({ message: "Unauthorized" });
50 | }
51 |
52 | const accessToken = jwt.sign(
53 | {
54 | UserInfo: {
55 | userId: foundUser._id.toString(),
56 | name: foundUser.name,
57 | email: foundUser.email,
58 | profilePicture: foundUser.profilePicture,
59 | roles: foundUser.roles,
60 | favorites: foundUser.favorites,
61 | },
62 | },
63 | process.env.ACCESS_TOKEN_SECRET,
64 | { expiresIn: "30m" }
65 | );
66 |
67 | const refreshToken = jwt.sign(
68 | {
69 | userId: foundUser._id,
70 | },
71 | process.env.REFRESH_TOKEN_SECRET,
72 | { expiresIn: "2d" }
73 | );
74 |
75 | foundUser.refreshToken = refreshToken;
76 | const result = await foundUser.save();
77 |
78 | res.cookie("jwt", refreshToken, {
79 | httpOnly: true,
80 | secure: true,
81 | sameSite: "None",
82 | maxAge: 2 * 24 * 60 * 60 * 1000,
83 | });
84 |
85 | res.json({ accessToken });
86 | } catch (error) {
87 | next(error);
88 | }
89 | };
90 |
91 | const refreshToken = async (req, res) => {
92 | const cookies = req.cookies;
93 | if (!cookies?.jwt) return res.status(401).json({ message: "Unauthorized" });
94 |
95 | const refreshToken = cookies.jwt;
96 |
97 | const foundUser = await User.findOne({ refreshToken });
98 | if (!foundUser) {
99 | return res.status(403).json({ message: "Forbidden" });
100 | }
101 |
102 | jwt.verify(
103 | refreshToken,
104 | process.env.REFRESH_TOKEN_SECRET,
105 | async (err, decoded) => {
106 | if (err || foundUser._id.toString() !== decoded.userId) {
107 | return res.status(403).json({ message: "Forbidden" });
108 | }
109 |
110 | const accessToken = jwt.sign(
111 | {
112 | UserInfo: {
113 | userId: foundUser._id.toString(),
114 | name: foundUser.name,
115 | email: foundUser.email,
116 | profilePicture: foundUser.profilePicture,
117 | roles: foundUser.roles,
118 | favorites: foundUser.favorites,
119 | },
120 | },
121 | process.env.ACCESS_TOKEN_SECRET,
122 | { expiresIn: "30m" }
123 | );
124 | res.json({ accessToken });
125 | }
126 | );
127 | };
128 |
129 | const logout = async (req, res) => {
130 | // On client, also delete the accessToken
131 |
132 | const cookies = req.cookies;
133 | if (!cookies?.jwt) return res.sendStatus(204); //No content
134 | const refreshToken = cookies.jwt;
135 |
136 | // Is refreshToken in db?
137 | const foundUser = await User.findOne({ refreshToken });
138 |
139 | if (!foundUser) {
140 | res.clearCookie("jwt", { httpOnly: true, sameSite: "None", secure: true });
141 | return res.sendStatus(204);
142 | }
143 |
144 | // Delete refreshToken in db
145 | foundUser.refreshToken = "";
146 | const result = await foundUser.save();
147 |
148 | res.clearCookie("jwt", { httpOnly: true, sameSite: "None", secure: true });
149 | res.sendStatus(204);
150 | };
151 |
152 | module.exports = { register, login, refreshToken, logout };
153 |
--------------------------------------------------------------------------------
/client/src/components/footer/Footer.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {
3 | AiFillGithub,
4 | AiFillLinkedin,
5 | AiFillTwitterCircle,
6 | } from "react-icons/ai";
7 | import { motion } from "framer-motion";
8 | import { Logo } from "..";
9 | import { Link } from "react-router-dom";
10 |
11 | const Footer = () => {
12 | return (
13 |
118 | );
119 | };
120 |
121 | export default Footer;
122 |
--------------------------------------------------------------------------------
/client/src/components/avatar/Avatar.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import {
3 | Avatar as MuiAvatar,
4 | Divider,
5 | IconButton,
6 | ListItemIcon,
7 | Menu,
8 | MenuItem,
9 | Tooltip,
10 | Box,
11 | } from "@mui/material";
12 | import {
13 | Article,
14 | Description,
15 | Fastfood,
16 | Favorite,
17 | Logout,
18 | RestaurantMenu,
19 | } from "@mui/icons-material";
20 | import { Link, useNavigate } from "react-router-dom";
21 | import useAuth from "../../hooks/useAuth";
22 | import { useLogoutMutation } from "../../features/auth/authApiSlice";
23 |
24 | const Avatar = () => {
25 | const [anchorEl, setAnchorEl] = useState(null);
26 | const user = useAuth();
27 |
28 | const [logout] = useLogoutMutation();
29 |
30 | const open = Boolean(anchorEl);
31 | const navigate = useNavigate();
32 |
33 | const handleClick = (event) => {
34 | setAnchorEl(event.currentTarget);
35 | };
36 |
37 | const handleClose = () => {
38 | setAnchorEl(null);
39 | };
40 |
41 | const handleLogout = () => {
42 | setAnchorEl(null);
43 | logout();
44 | localStorage.setItem("persist", false);
45 | navigate("/auth/signin");
46 | };
47 |
48 | return (
49 |
50 | {/* Yellow border only for pro users */}
51 |
52 |
59 |
64 |
65 |
66 |
180 |
181 | );
182 | };
183 |
184 | export default Avatar;
185 |
--------------------------------------------------------------------------------
/client/src/pages/profile/Profile.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { Button, Input } from "../../components";
3 | import { BiLockAlt } from "react-icons/bi";
4 | import { IoMailOutline } from "react-icons/io5";
5 | import { AiOutlineUser } from "react-icons/ai";
6 | import { profileBg } from "../../assets";
7 | import { CircularProgress, Avatar as MuiAvatar } from "@mui/material";
8 | import { useDispatch } from "react-redux";
9 | import { setCredentials } from "../../features/auth/authSlice";
10 | import uploadImage from "../../common/uploadImage";
11 | import { toast } from "react-toastify";
12 | import { useUpdateUserMutation } from "../../features/user/userApiSlice";
13 | import useAuth from "../../hooks/useAuth";
14 | import useTitle from "../../hooks/useTitle";
15 |
16 | const Profile = () => {
17 | const user = useAuth();
18 | useTitle("Recipen - Profile");
19 |
20 | const [formDetails, setFormDetails] = useState({
21 | name: user?.name || "",
22 | email: user?.email || "",
23 | image: "",
24 | password: "",
25 | });
26 |
27 | const [progress, setProgress] = useState(0);
28 | const [updateUser, { isLoading }] = useUpdateUserMutation();
29 | const dispatch = useDispatch();
30 |
31 | const handleChange = (e) => {
32 | if (e.target.id === "image") {
33 | uploadImage(e, setProgress, setFormDetails, formDetails);
34 | } else {
35 | setFormDetails({ ...formDetails, [e.target.id]: e.target.value });
36 | }
37 | };
38 |
39 | const handleSubmit = async (e) => {
40 | e.preventDefault();
41 |
42 | try {
43 | const updatedUser = await toast.promise(
44 | updateUser({ ...formDetails, userId: user?.userId }).unwrap(),
45 | {
46 | pending: "Please wait...",
47 | success: "User updated successfully",
48 | error: "Unable to update user",
49 | }
50 | );
51 | setFormDetails({
52 | name: formDetails?.name,
53 | email: formDetails?.email,
54 | image: formDetails?.image,
55 | password: "",
56 | });
57 | dispatch(setCredentials({ ...updatedUser }));
58 | } catch (error) {
59 | toast.error(error.data);
60 | console.error(error);
61 | }
62 | };
63 |
64 | return (
65 |
66 | {/* Profile heading */}
67 |
68 |
Profile
69 |
70 | You can update your profile details here
71 |
72 |
73 |
74 | {/* Profile form */}
75 |
144 | {/* Profile banner */}
145 |
146 |
150 |
151 |
152 |
153 | );
154 | };
155 |
156 | export default Profile;
157 |
--------------------------------------------------------------------------------
/client/src/App.jsx:
--------------------------------------------------------------------------------
1 | import { Suspense } from "react";
2 | import { BrowserRouter as Router, Route, Routes } from "react-router-dom";
3 | import {
4 | AddBlog,
5 | AddRecipe,
6 | Blogs,
7 | Contact,
8 | DashboardBlogs,
9 | DashboardRecipes,
10 | EditBlog,
11 | EditRecipe,
12 | Error,
13 | Home,
14 | MyBlogs,
15 | MyRecipes,
16 | Profile,
17 | Recipe,
18 | SavedRecipes,
19 | SingleBlog,
20 | SingleRecipe,
21 | Users,
22 | SignIn,
23 | SignUp,
24 | CheckoutSuccess,
25 | CheckoutFailure,
26 | } from "./pages";
27 | import { ScrollToTop, PageLoading } from "./components";
28 | import { RootLayout, DashboardLayout } from "./layouts";
29 | import RequireAuth from "./features/auth/RequireAuth";
30 | import { ToastContainer } from "react-toastify";
31 | import "react-toastify/dist/ReactToastify.css";
32 | import ROLES from "./common/roles";
33 | import PersistLogin from "./features/auth/PersistLogin";
34 | import useTitle from "./hooks/useTitle";
35 |
36 | function App() {
37 | useTitle("Recipen - Home");
38 |
39 | return (
40 |
41 |
42 |
48 | }>
49 |
50 |
51 | }
54 | />
55 | }
58 | />
59 |
60 |
61 | }>
62 | {/* Dashboard */}
63 | }>
64 | }
67 | >
68 | }
71 | />
72 | }
75 | />
76 | }
79 | />
80 |
81 |
82 |
83 | }
86 | >
87 | }
90 | />
91 |
92 | }
95 | />
96 | }
99 | />
100 | }
103 | />
104 |
105 |
108 | }
109 | >
110 | }
113 | />
114 | }
117 | />
118 | }
121 | />
122 |
123 |
124 | }
127 | />
128 |
129 | }
132 | />
133 | }
136 | />
137 |
140 | }
141 | >
142 | }
145 | />
146 | }
149 | />
150 | }
153 | />
154 |
155 |
156 |
161 | }
162 | >
163 | }
166 | />
167 | }
170 | />
171 | }
174 | />
175 |
176 | }
179 | />
180 |
181 |
182 |
183 |
184 |
185 | );
186 | }
187 |
188 | export default App;
189 |
--------------------------------------------------------------------------------
/client/src/pages/blogs/AddBlog.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { Button } from "../../components";
3 | import { photo } from "../../assets";
4 | import uploadImage from "../../common/uploadImage";
5 | import { LinearProgress } from "@mui/material";
6 | import { toast } from "react-toastify";
7 | import { useAddBlogMutation } from "../../features/blog/blogApiSlice";
8 | import { ReactMarkdown } from "react-markdown/lib/react-markdown";
9 | import useTitle from "../../hooks/useTitle";
10 |
11 | const AddBlog = () => {
12 | useTitle("Recipen - Add Blog");
13 |
14 | const [formDetails, setFormDetails] = useState({
15 | title: "",
16 | image: "",
17 | description: "",
18 | });
19 | const [progress, setProgress] = useState(0);
20 | const [focused, setFocused] = useState({
21 | title: "",
22 | });
23 | const [addBlog, { isLoading }] = useAddBlogMutation();
24 |
25 | const handleFocus = (e) => {
26 | setFocused({ ...focused, [e.target.id]: true });
27 | };
28 |
29 | const handleChange = (e) => {
30 | if (e.target.id === "image") {
31 | uploadImage(e, setProgress, setFormDetails, formDetails);
32 | } else {
33 | setFormDetails({ ...formDetails, [e.target.id]: e.target.value });
34 | }
35 | };
36 |
37 | const handleSubmit = async (e) => {
38 | e.preventDefault();
39 |
40 | if (!formDetails.image) return toast.error("Upload blog image");
41 | if (!formDetails.description)
42 | return toast.error("Blog content cannot be empty");
43 |
44 | try {
45 | const blog = await toast.promise(addBlog({ ...formDetails }).unwrap(), {
46 | pending: "Please wait...",
47 | success: "Blog added successfully",
48 | error: "Unable to add blog",
49 | });
50 | setFormDetails({
51 | title: "",
52 | image: "",
53 | description: "",
54 | });
55 | setFocused({
56 | title: "",
57 | });
58 | } catch (error) {
59 | toast.error(error.data);
60 | console.error(error);
61 | }
62 | };
63 |
64 | return (
65 |
66 | Add New Blog
67 |
68 |
175 |
176 | );
177 | };
178 |
179 | export default AddBlog;
180 |
--------------------------------------------------------------------------------
/client/src/pages/blogs/EditBlog.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import { Button, ComponentLoading } from "../../components";
3 | import { photo } from "../../assets";
4 | import uploadImage from "../../common/uploadImage";
5 | import { LinearProgress } from "@mui/material";
6 | import { toast } from "react-toastify";
7 | import {
8 | useGetBlogQuery,
9 | useUpdateBlogMutation,
10 | } from "../../features/blog/blogApiSlice";
11 | import { useParams } from "react-router-dom";
12 |
13 | const EditBlog = () => {
14 | const { id } = useParams();
15 |
16 | const { data, ...rest } = useGetBlogQuery(id);
17 | const [updateBlog, { isLoading }] = useUpdateBlogMutation();
18 |
19 | const [formDetails, setFormDetails] = useState({
20 | title: data?.title || "",
21 | image: data?.image || "",
22 | description: data?.description || "",
23 | });
24 | const [progress, setProgress] = useState(0);
25 | const [focused, setFocused] = useState({
26 | title: "",
27 | });
28 |
29 | useEffect(() => {
30 | if (!rest?.isLoading) {
31 | setFormDetails({
32 | title: data?.title,
33 | image: data?.image,
34 | description: data?.description,
35 | });
36 | }
37 | }, [rest?.isLoading]);
38 |
39 | const handleFocus = (e) => {
40 | setFocused({ ...focused, [e.target.id]: true });
41 | };
42 |
43 | const handleChange = (e) => {
44 | if (e.target.id === "image") {
45 | uploadImage(e, setProgress, setFormDetails, formDetails);
46 | } else {
47 | setFormDetails({ ...formDetails, [e.target.id]: e.target.value });
48 | }
49 | };
50 |
51 | const handleSubmit = async (e) => {
52 | e.preventDefault();
53 |
54 | if (!formDetails.image) return toast.error("Upload blog image");
55 | if (!formDetails.description)
56 | return toast.error("Blog content cannot be empty");
57 |
58 | try {
59 | const blog = await toast.promise(
60 | updateBlog({ ...formDetails, blogId: id }).unwrap(),
61 | {
62 | pending: "Please wait...",
63 | success: "Blog updated successfully",
64 | error: "Unable to update blog",
65 | }
66 | );
67 | } catch (error) {
68 | toast.error(error.data);
69 | console.error(error);
70 | }
71 | };
72 |
73 | return (
74 |
75 | Add New Blog
76 |
77 | {rest.isLoading ? (
78 |
79 | ) : (
80 |
84 |
85 |
86 |
90 | Blog title
91 |
92 |
93 |
108 |
112 | Title should at least 5 characters long
113 |
114 |
115 |
116 |
117 |
118 |
122 | Content
123 |
124 |
125 |
136 |
137 |
138 |
144 |
145 |
146 | {/* Upload recipe image */}
147 |
148 |
152 |
155 | {progress > 0 && progress < 100 ? (
156 |
161 | ) : (
162 |
167 | )}
168 |
169 |
170 | Drag your image here, or
171 | browse
172 |
173 |
174 |
180 |
181 |
182 | )}
183 |
184 | );
185 | };
186 |
187 | export default EditBlog;
188 |
--------------------------------------------------------------------------------
/server/controllers/recipeController.js:
--------------------------------------------------------------------------------
1 | const Recipe = require("../models/recipeModel");
2 | const User = require("../models/userModel");
3 | const jwt = require("jsonwebtoken");
4 |
5 | const getAllRecipes = async (req, res, next) => {
6 | try {
7 | const recipes = await Recipe.find()
8 | .sort({ createdAt: -1 })
9 | .populate("author", "name");
10 | res.status(200).send(recipes);
11 | } catch (error) {
12 | next(error);
13 | }
14 | };
15 |
16 | const getRecipe = async (req, res, next) => {
17 | try {
18 | const recipe = await Recipe.findOne({ _id: req.params.id })
19 | .populate("author", "name")
20 | .populate("comments.user", ["name", "profilePicture"]);
21 |
22 | if (!recipe) return res.status(404).json({ message: "Recipe not found" });
23 |
24 | res.status(200).send(recipe);
25 | } catch (error) {
26 | next(error);
27 | }
28 | };
29 |
30 | const addRecipe = async (req, res, next) => {
31 | try {
32 | const {
33 | title,
34 | image,
35 | description,
36 | calories,
37 | cookingTime,
38 | ingredients,
39 | instructions,
40 | } = req.body;
41 | if (
42 | !title ||
43 | !image ||
44 | !description ||
45 | !calories ||
46 | !cookingTime ||
47 | !ingredients.length ||
48 | !instructions.length
49 | ) {
50 | return res.status(422).json({ message: "Insufficient data" });
51 | }
52 | const recipe = Recipe({ ...req.body, author: req.user });
53 | await recipe.save();
54 | res.status(201).json({ success: "Recipe added successfully" });
55 | } catch (error) {
56 | next(error);
57 | }
58 | };
59 |
60 | const updateRecipe = async (req, res, next) => {
61 | try {
62 | const {
63 | title,
64 | image,
65 | description,
66 | calories,
67 | cookingTime,
68 | ingredients,
69 | instructions,
70 | } = req.body;
71 | if (
72 | !title ||
73 | !image ||
74 | !description ||
75 | !calories ||
76 | !cookingTime ||
77 | !ingredients.length ||
78 | !instructions.length
79 | ) {
80 | return res.status(422).json({ message: "Insufficient data" });
81 | }
82 |
83 | const foundRecipe = await Recipe.findById(req.params.id);
84 | if (!foundRecipe)
85 | return res.status(404).json({ message: "Recipe not found" });
86 |
87 | if (foundRecipe.author !== req.user)
88 | return res.status(401).json({ message: "Unauthorized" });
89 |
90 | foundRecipe.title = title;
91 | foundRecipe.description = description;
92 | foundRecipe.image = image;
93 | foundRecipe.calories = calories;
94 | foundRecipe.ingredients = ingredients;
95 | foundRecipe.cookingTime = cookingTime;
96 | foundRecipe.instructions = instructions;
97 |
98 | const updatedRecipe = await foundRecipe.save();
99 | res.status(201).json(updatedRecipe);
100 | } catch (error) {
101 | next(error);
102 | }
103 | };
104 |
105 | const rateRecipe = async (req, res, next) => {
106 | try {
107 | const { rating } = req.body;
108 |
109 | const recipe = await Recipe.findById(req.params.id);
110 | if (!recipe) {
111 | return res.status(404).json({ message: "Recipe not found." });
112 | }
113 |
114 | // Check if the user has already rated this recipe
115 | const existingRating = recipe.ratings.find((rate) =>
116 | rate.user.equals(req.user)
117 | );
118 | if (existingRating) {
119 | return res
120 | .status(400)
121 | .json({ message: "User has already rated this recipe" });
122 | }
123 |
124 | // Add the new rating
125 | recipe.ratings.push({ user: req.user, rating: rating });
126 | await recipe.save();
127 |
128 | res.status(201).json({ message: "Rating added successfully." });
129 | } catch (error) {
130 | next(error);
131 | }
132 | };
133 |
134 | const deleteRecipe = async (req, res, next) => {
135 | try {
136 | const foundRecipe = await Recipe.findById(req.params.id);
137 | if (!foundRecipe)
138 | return res.status(404).json({ message: "Recipe not found" });
139 |
140 | if (foundRecipe.author !== req.user)
141 | return res.status(401).json({ message: "Unauthorized" });
142 |
143 | await foundRecipe.deleteOne({ _id: req.params.id });
144 | res.sendStatus(204);
145 | } catch (error) {
146 | next(error);
147 | }
148 | };
149 |
150 | const addComment = async (req, res, next) => {
151 | try {
152 | const { comment } = req.body;
153 |
154 | // Validate userId and commentText
155 | if (!comment) {
156 | return res.status(400).json({ message: "Comment is required." });
157 | }
158 |
159 | const recipe = await Recipe.findById(req.params.id);
160 | if (!recipe) {
161 | return res.status(404).json({ message: "Recipe not found." });
162 | }
163 |
164 | // Add the new comment
165 | recipe.comments.push({ user: req.user, comment });
166 | await recipe.save();
167 |
168 | res.status(201).json({ message: "Comment added successfully." });
169 | } catch (error) {
170 | next(error);
171 | }
172 | };
173 |
174 | const deleteComment = async (req, res, next) => {
175 | try {
176 | const { recipeId, commentId } = req.params;
177 |
178 | const recipe = await Recipe.findById(recipeId);
179 | if (!recipe) {
180 | return res.status(404).json({ message: "Recipe not found." });
181 | }
182 |
183 | const commentIndex = recipe.comments.findIndex((comment) =>
184 | comment._id.equals(commentId)
185 | );
186 | if (commentIndex === -1) {
187 | return res.status(404).json({ message: "Comment not found." });
188 | }
189 |
190 | recipe.comments.splice(commentIndex, 1);
191 | await recipe.save();
192 |
193 | res.status(200).json({ message: "Comment deleted successfully." });
194 | } catch (error) {
195 | next(error);
196 | }
197 | };
198 |
199 | const toggleFavoriteRecipe = async (req, res, next) => {
200 | try {
201 | const user = await User.findById(req.user);
202 |
203 | if (!user) {
204 | return res.status(404).json({ message: "User not found" });
205 | }
206 |
207 | const recipeIndex = user.favorites.indexOf(req.params.id);
208 | if (recipeIndex === -1) {
209 | // Recipe not present, add it to favorites
210 | user.favorites.push(req.params.id);
211 | } else {
212 | // Recipe already present, remove it from favorites
213 | user.favorites.splice(recipeIndex, 1);
214 | }
215 |
216 | await user.save();
217 |
218 | const roles = Object.values(user.roles);
219 |
220 | const accessToken = jwt.sign(
221 | {
222 | UserInfo: {
223 | userId: user._id,
224 | name: user.name,
225 | email: user.email,
226 | profilePicture: user.profilePicture,
227 | roles: roles,
228 | favorites: user.favorites,
229 | },
230 | },
231 | process.env.ACCESS_TOKEN_SECRET,
232 | { expiresIn: "1d" }
233 | );
234 | return res.status(201).json({ accessToken });
235 | } catch (error) {
236 | next(error);
237 | }
238 | };
239 |
240 | module.exports = {
241 | getAllRecipes,
242 | getRecipe,
243 | addRecipe,
244 | updateRecipe,
245 | rateRecipe,
246 | deleteRecipe,
247 | addComment,
248 | deleteComment,
249 | toggleFavoriteRecipe,
250 | };
251 |
--------------------------------------------------------------------------------
/client/src/pages/contact/Contact.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { Button, Input } from "../../components";
3 | import { IoMailOutline } from "react-icons/io5";
4 | import { FaRegPaperPlane } from "react-icons/fa";
5 | import {
6 | AiFillGithub,
7 | AiFillLinkedin,
8 | AiFillTwitterCircle,
9 | AiOutlineUser,
10 | } from "react-icons/ai";
11 | import { motion } from "framer-motion";
12 | import useAuth from "../../hooks/useAuth";
13 | import useTitle from "../../hooks/useTitle";
14 |
15 | const Contact = () => {
16 | const user = useAuth();
17 | useTitle("Recipen - Contact Us");
18 |
19 | const [formDetails, setFormDetails] = useState({
20 | firstName: "",
21 | lastName: "",
22 | email: user?.email || "",
23 | message: "",
24 | });
25 | const [focused, setFocused] = useState(false);
26 | const handleFocus = () => {
27 | setFocused(true);
28 | };
29 |
30 | const handleChange = (e) => {
31 | setFormDetails({ ...formDetails, [e.target.name]: e.target.value });
32 | };
33 |
34 | return (
35 |
36 | {/* Contact page left */}
37 |
38 |
Get in touch
39 |
40 |
Visit us
41 |
Come say hello to our office
42 |
43 | Friends Colony, Mumbai, Maharashtra 400070
44 |
45 |
46 |
56 |
66 |
67 |
Social media
68 |
94 |
95 |
96 | {/* Contact form container */}
97 |
98 | {/* Contact form container details */}
99 |
100 |
We'd love to help
101 |
102 | Reach out and we'll get in touch in 24 hours
103 |
104 |
105 | {/* Contact form */}
106 |
113 |
114 | }
118 | handleChange={handleChange}
119 | value={formDetails.firstName}
120 | label={"First Name"}
121 | placeholder={"John"}
122 | errorMessage={
123 | "Should be more than 3 characters long and should not include special characters!"
124 | }
125 | pattern={"^[a-zA-Z]{3,}(?: [a-zA-Z]{3,})*$"}
126 | />
127 | }
131 | handleChange={handleChange}
132 | value={formDetails.lastName}
133 | label={"Last Name"}
134 | placeholder={"Doe"}
135 | errorMessage={
136 | "Should be more than 3 characters long and should not include special characters!"
137 | }
138 | pattern={"^[a-zA-Z]{3,}(?: [a-zA-Z]{3,})*$"}
139 | />
140 |
141 | }
145 | handleChange={handleChange}
146 | value={formDetails.email}
147 | label={"Email"}
148 | placeholder={"example@abc.com"}
149 | errorMessage={"Enter a valid email address!"}
150 | pattern={/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$/}
151 | />
152 |
153 |
157 | Message
158 |
159 |
174 |
178 | Message should be at least 10 characters long!
179 |
180 |
181 | }
184 | type={"submit"}
185 | customCss={"rounded-lg gap-3"}
186 | />
187 |
188 |
189 |
190 | );
191 | };
192 |
193 | export default Contact;
194 |
--------------------------------------------------------------------------------
/client/src/pages/blogs/SingleBlog.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import {
3 | Comment,
4 | Button,
5 | Input,
6 | NoData,
7 | ComponentLoading,
8 | } from "../../components";
9 | import { IoMailOutline } from "react-icons/io5";
10 | import { FaRegPaperPlane } from "react-icons/fa";
11 | import { BsFillPersonFill, BsCalendarCheck } from "react-icons/bs";
12 | import { AiOutlineUser } from "react-icons/ai";
13 | import {
14 | useGetBlogQuery,
15 | useCommentBlogMutation,
16 | useDeleteCommentBlogMutation,
17 | useDeleteBlogMutation,
18 | } from "../../features/blog/blogApiSlice";
19 | import { Link, useNavigate, useParams } from "react-router-dom";
20 | import dateFormat from "../../common/dateFormat";
21 | import { toast } from "react-toastify";
22 | import { IconButton, Menu, MenuItem } from "@mui/material";
23 | import { MoreVert } from "@mui/icons-material";
24 | import ReactMarkdown from "react-markdown";
25 | import useAuth from "../../hooks/useAuth";
26 | import useTitle from "../../hooks/useTitle";
27 |
28 | const SingleBlog = () => {
29 | useTitle("Recipen - Blog");
30 |
31 | const user = useAuth();
32 | const { id } = useParams();
33 | const [anchorEl, setAnchorEl] = React.useState(null);
34 | const open = Boolean(anchorEl);
35 | const navigate = useNavigate();
36 |
37 | const { data, ...rest } = useGetBlogQuery(id);
38 | const [commentBlog, { isLoading }] = useCommentBlogMutation();
39 | const [deleteComment] = useDeleteCommentBlogMutation();
40 | const [deleteBlog] = useDeleteBlogMutation();
41 |
42 | const [formDetails, setFormDetails] = useState({
43 | name: user?.name || "",
44 | email: user?.email || "",
45 | message: "",
46 | });
47 |
48 | const handleChange = (e) => {
49 | setFormDetails({ ...formDetails, [e.target.id]: e.target.value });
50 | };
51 |
52 | const handleSubmit = async (e) => {
53 | e.preventDefault();
54 | if (!user) {
55 | toast.error("You must sign in first");
56 | return navigate("/auth/signin");
57 | }
58 | try {
59 | await toast.promise(
60 | commentBlog({ blogId: id, comment: formDetails.message }).unwrap(),
61 | {
62 | pending: "Please wait...",
63 | success: "Comment added",
64 | error: "Could not add comment",
65 | }
66 | );
67 | setFormDetails({ ...formDetails, message: "" });
68 | } catch (error) {
69 | toast.error(error.data);
70 | console.error(error);
71 | }
72 | };
73 |
74 | const handleDeleteComment = async (_id) => {
75 | try {
76 | await toast.promise(
77 | deleteComment({ blogId: id, commentId: _id }).unwrap(),
78 | {
79 | pending: "Please wait...",
80 | success: "Comment deleted",
81 | error: "Could not delete comment",
82 | }
83 | );
84 | } catch (error) {
85 | toast.error(error.data);
86 | console.error(error);
87 | }
88 | };
89 |
90 | const handleMenu = (event) => {
91 | setAnchorEl(event.currentTarget);
92 | };
93 |
94 | const handleMenuClose = () => {
95 | setAnchorEl(null);
96 | };
97 |
98 | const handleMenuDelete = () => {
99 | if (window.confirm("Are you sure you want to delete?")) {
100 | deleteBlog(data?._id);
101 | navigate("/blog");
102 | }
103 | setAnchorEl(null);
104 | };
105 |
106 | return (
107 | <>
108 | {rest?.isLoading ? (
109 |
110 | ) : (
111 |
112 |
113 |
114 | {/* Blog heading */}
115 |
116 | {data?.title}
117 |
118 | {data?.author?._id === user?.userId && (
119 | <>
120 |
129 |
130 |
131 |
145 | >
146 | )}
147 |
148 | {/* Blog image */}
149 |
154 | {/* Blog author & date */}
155 |
156 |
157 |
158 | {data?.author?.name}
159 |
160 |
161 |
162 | {data && dateFormat(data?.createdAt)}
163 |
164 |
165 |
166 | {/* Blog content */}
167 |
{data?.description}
168 |
169 |
170 | {/* Blog comment form */}
171 |
222 |
223 | {/* Blog comments */}
224 |
225 |
Comments
226 | {data?.comments?.length ? (
227 |
228 | {data?.comments?.map((comment) => (
229 |
235 | ))}
236 |
237 | ) : (
238 |
239 | )}
240 |
241 |
242 | )}
243 | >
244 | );
245 | };
246 |
247 | export default SingleBlog;
248 |
--------------------------------------------------------------------------------
/client/src/assets/profile_details.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------