├── README.md ├── admin ├── README.md ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt ├── src │ ├── App.js │ ├── assets │ │ ├── image.svg │ │ ├── profile.png │ │ └── writer.jpg │ ├── components │ │ ├── Comments.jsx │ │ ├── ConfirmDialog.jsx │ │ ├── Graph.jsx │ │ ├── Loading.jsx │ │ ├── LoginForm.jsx │ │ ├── Logo.jsx │ │ ├── Navbar.jsx │ │ ├── PasswordStrength.jsx │ │ ├── Sidebar.jsx │ │ ├── SignUpForm.jsx │ │ ├── Stats.jsx │ │ └── Table.jsx │ ├── hooks │ │ ├── auth-hook.js │ │ ├── followers-hook.js │ │ └── post-hook.js │ ├── index.css │ ├── index.js │ ├── pages │ │ ├── Analytics.jsx │ │ ├── Content.jsx │ │ ├── Dashboard.jsx │ │ ├── Followers.jsx │ │ ├── OTPVerification.jsx │ │ ├── StartPage.jsx │ │ ├── StartPage.sx │ │ └── WritePost.jsx │ ├── store │ │ ├── commentStore.js │ │ └── index.js │ └── utils │ │ ├── dummyData.js │ │ ├── firebase.js │ │ └── index.js └── tailwind.config.js ├── client ├── README.md ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt ├── src │ ├── App.js │ ├── assets │ │ └── profile.png │ ├── components │ │ ├── Banner.jsx │ │ ├── Button.jsx │ │ ├── Card.jsx │ │ ├── Divider.jsx │ │ ├── Footer.jsx │ │ ├── Inputbox.jsx │ │ ├── Loading.jsx │ │ ├── Logo.jsx │ │ ├── Navbar.jsx │ │ ├── Pagination.jsx │ │ ├── PopularPosts.jsx │ │ ├── PopularWriters.jsx │ │ ├── PostComments.jsx │ │ ├── Switch.jsx │ │ └── index.js │ ├── index.css │ ├── index.js │ ├── pages │ │ ├── BlogDetails.jsx │ │ ├── CategoriesPage.jsx │ │ ├── Home.jsx │ │ ├── LoginPage.jsx │ │ ├── SignupPage.1.jsx │ │ ├── SignupPage.jsx │ │ ├── WriterPage.jsx │ │ └── index.js │ ├── store │ │ └── index.js │ └── utils │ │ ├── dummyData.js │ │ └── index.js └── tailwind.config.js └── server ├── controllers ├── authController.js ├── postController.js └── userController.js ├── dbConfig └── index.js ├── index.js ├── middleware ├── authMiddleware.js └── errorMiddleware.js ├── models ├── commentModel.js ├── emailVerification.js ├── followersModel.js ├── postModel.js ├── userModel.js └── viewsModel.js ├── package.json ├── routes ├── authRoute.js ├── index.js ├── postRoute.js └── userRoute.js └── utils ├── index.js └── sendEmail.js /README.md: -------------------------------------------------------------------------------- 1 | # Fullstack Blog Application (MERN Stack) 2 | 3 | Welcome to the Fullstack Blog Application documentation! This application allows users to explore a variety of blog posts with a user-friendly interface. Below is a detailed guide on how to set up and run the application. 4 | 5 |   6 | 7 | ## Overview 8 | 9 | This full-stack blog application is built using the MERN stack (MongoDB, Express, React, Node.js). The state management is handled using Zustand instead of Redux. Users can enjoy features like post categorization, pagination, commenting, and a dual Dark & Light theme UI. Admins have access to additional functionalities in the Admin Dashboard, including analytics on post content and views. 10 | 11 |   12 | 13 | ## System Requirements: 14 | 15 | - Node.js version 18 or above. 16 | - npm version 10.2.3. 17 | - MongoDB Atlas 18 | - Text Editor or IDE 19 | - Git 20 | - Postman (Optional) 21 | 22 |   23 | 24 | ## Technologies Used: 25 | 26 | 1. ReactJs 27 | 2. NodeJs (Node version 18 or above) 28 | 3. ExpressJs 29 | 4. MongoDB (Database) 30 | 5. Tailwind CSS (for Styling) 31 | 6. Zustand (for State Management) 32 | 33 |   34 | 35 | ## Features Include (Client side): 36 | 37 | 1. User Account Creation (Optional) 38 | 2. Google Sign In (Optional) - Client side only 39 | 3. Post Categories 40 | 4. Pagination with Page Numbers 41 | 5. Commenting on Posts (Only Available to Signed-in Users) 42 | 6. Dark and Light Theme Settings 43 | 7. Blog Details Page with Dynamic URL (Slug) 44 | 8. Fully Mobile-Responsive Design 45 | 46 | ## Features Include (Admin Dashboard): 47 | 48 | 1. Account Creation 49 | 2. Email Verification with OTP 50 | 3. Create Posts, Delete Posts, Enable or Disable Posts 51 | 4. Dashboard Analytics 52 | 5. Content and Views Analytics (7 days, 28 days, 900 days & 365 days) 53 | 6. Ability to delete user comments on a blog post 54 | 7. Dark and Light Theme Settings 55 | 8. Fully Mobile-Responsive Design 56 | 57 | ## Modern User Interface (UI) 58 | 59 | Classic UI with DARK & LIGHT theme settings 60 | 61 |   62 | 63 | ## Screenshots:- 64 | 65 | ### Dark Theme >> 66 | ![Banner](https://res.cloudinary.com/djs3wu5bg/image/upload/v1702011737/Codewave/q3k5ayoi1v696wl3ixvf.png) 67 | 68 | 69 | ![Home Page](https://firebasestorage.googleapis.com/v0/b/codewave-codebase-e052b.appspot.com/o/nextjs-blog%2FHome.png?alt=media&token=b157b478-9a66-4ee9-9f94-c21b92c2e16e) 70 | 71 | ![Pagination](https://firebasestorage.googleapis.com/v0/b/codewave-codebase-e052b.appspot.com/o/nextjs-blog%2Fposts%20with%20pagination.png?alt=media&token=b39e1bc8-73b6-41dc-8a47-e0e4f03d7d21) 72 | 73 | ![Post Detail](https://res.cloudinary.com/djs3wu5bg/image/upload/v1702011971/Codewave/bytcb7p3kpwxvu6f4h7q.png) 74 | 75 | ![Sign-in Page](https://firebasestorage.googleapis.com/v0/b/codewave-codebase-e052b.appspot.com/o/nextjs-blog%2FSignin%20-%20Dark.png?alt=media&token=b3abdef1-5c0c-404c-a81b-94d558a6bb76) 76 | 77 | ### Light Theme >> 78 | 79 | ![Sign-in Page](https://firebasestorage.googleapis.com/v0/b/codewave-codebase-e052b.appspot.com/o/nextjs-blog%2FSignin%20-light.png?alt=media&token=066aac30-bf42-4ee2-952e-3d089bb1dd72) 80 | 81 | 82 | ### Admin Dashboard >> 83 | 84 | ![Admin Dashboard](https://firebasestorage.googleapis.com/v0/b/codewave-codebase-e052b.appspot.com/o/nextjs-blog%2Fadmin.png?alt=media&token=a96e398b-8d91-40ed-a938-b3714f7bccc4) 85 | 86 |   87 |   88 | 89 | # Server Setup 90 | 91 | ## Environment variables 92 | First, create the environment variables file `.env` in the server folder. The `.env` file contains the following environment variables: 93 | 94 | - MONGODB_URL = `your MongoDB URL` 95 | - JWT_SECRET_KEY = `any secret key - must be secured` 96 | - PORT = `8800` or any port number 97 | - AUTH_EMAIL = `your email address to send the OTP` 98 | - AUTH_PASSWORD = `password to your email account` (used Hotmail for email verification) 99 | 100 |   101 | 102 | ## Set Up MongoDB: 103 | 104 | 1. Setting up MongoDB involves a few steps: 105 | - Visit MongoDB Atlas Website 106 | - Go to the MongoDB Atlas website: [https://www.mongodb.com/cloud/atlas](https://www.mongodb.com/cloud/atlas). 107 | 108 | - Create an Account 109 | - Log in to your MongoDB Atlas account. 110 | - Create a New Cluster 111 | - Choose a Cloud Provider and Region 112 | - Configure Cluster Settings 113 | - Create Cluster 114 | - Wait for Cluster to Deploy 115 | - Create Database User 116 | - Set Up IP Whitelist 117 | - Connect to Cluster 118 | - Configure Your Application 119 | - Test the Connection 120 | 121 | 2. Create a new database and configure the `.env` file with the MongoDB connection URL. 122 | 123 | ## Steps to run server 124 | 125 | 1. Open the project in any editor of choice. 126 | 2. Navigate into the server directory `cd server`. 127 | 3. Run `npm i` to install the packages. 128 | 4. Run `npm start` to start the server. 129 | 130 | If configured correctly, you should see a message indicating that the server is running successfully and `Database Connected`. 131 | 132 |   133 | 134 | # Client Side Setup 135 | 136 | ## Environment variables 137 | First, create the environment variables file `.env` in the client folder. The `.env` file contains the following environment variables: 138 | 139 | - REACT_APP_GOOGLE_CLIENT_ID = `Google client ID for Google Sign In` 140 | - REACT_APP_FIREBASE_API_KEY = `Firebase key` 141 | 142 | ## Steps to run client 143 | 144 | 1. Navigate into the client directory `cd client`. 145 | 2. Run `npm i` to install the packages. 146 | 3. Run `npm start` to run the app on `http://localhost:3000`. 147 | 4. Open [http://localhost:3000](http://localhost:3000) to view it in your browser. 148 | 149 |   150 | 151 | # Admin Dashboard Setup 152 | 153 | ## Environment variables 154 | First, create the environment variables file `.env` in the admin folder. The `.env` file contains the following environment variables: 155 | 156 | - REACT_APP_FIREBASE_API_KEY = `Firebase key` 157 | 158 | ## Steps to run admin dashboard 159 | 160 | 1. Navigate into the admin directory `cd admin`. 161 | 2. Run `npm i` to install the packages. 162 | 3. Run `npm start` to run the app on `http://localhost:3000` or any other available port. 163 | 164 |   165 | 166 | # Security Note: 167 | 168 | ## Environment Variables: 169 | 170 | - Safeguard your environment variables by storing them securely and not exposing them unintentionally. 171 | 172 | - Ensure that only authorized personnel have access to the environment variable configurations. 173 | 174 | 175 |   176 | 177 | ## For Support, Contact: 178 | 179 | - Email: codewavewithasante@gmail.com 180 | - Telegram Chat: [https://t.me/Codewave_with_asante](https://t.me/Codewave_with_asante) -------------------------------------------------------------------------------- /admin/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in your browser. 13 | 14 | The page will reload when you make changes.\ 15 | You may also see any lint errors in the console. 16 | 17 | ### `npm test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `npm run build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `npm run eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can't go back!** 35 | 36 | If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own. 39 | 40 | You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | 48 | ### Code Splitting 49 | 50 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) 51 | 52 | ### Analyzing the Bundle Size 53 | 54 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) 55 | 56 | ### Making a Progressive Web App 57 | 58 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) 59 | 60 | ### Advanced Configuration 61 | 62 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) 63 | 64 | ### Deployment 65 | 66 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) 67 | 68 | ### `npm run build` fails to minify 69 | 70 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) 71 | -------------------------------------------------------------------------------- /admin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "admin", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@mantine/core": "^7.1.3", 7 | "@mantine/form": "^7.1.3", 8 | "@mantine/hooks": "^7.1.3", 9 | "@mantine/modals": "^7.1.3", 10 | "@mantine/tiptap": "^7.1.3", 11 | "@tabler/icons-react": "^2.39.0", 12 | "@tanstack/react-query": "^5.0.5", 13 | "@testing-library/jest-dom": "^5.17.0", 14 | "@testing-library/react": "^13.4.0", 15 | "@testing-library/user-event": "^13.5.0", 16 | "@tiptap/extension-code-block-lowlight": "^2.1.12", 17 | "@tiptap/extension-color": "^2.1.12", 18 | "@tiptap/extension-highlight": "^2.1.12", 19 | "@tiptap/extension-link": "^2.1.12", 20 | "@tiptap/extension-placeholder": "^2.1.12", 21 | "@tiptap/extension-subscript": "^2.1.12", 22 | "@tiptap/extension-superscript": "^2.1.12", 23 | "@tiptap/extension-text-align": "^2.1.12", 24 | "@tiptap/extension-text-style": "^2.1.12", 25 | "@tiptap/extension-underline": "^2.1.12", 26 | "@tiptap/pm": "^2.1.12", 27 | "@tiptap/react": "^2.1.12", 28 | "@tiptap/starter-kit": "^2.1.12", 29 | "axios": "^1.5.1", 30 | "clsx": "^2.0.0", 31 | "firebase": "^10.7.1", 32 | "lowlight": "^3.1.0", 33 | "moment": "^2.29.4", 34 | "react": "^18.2.0", 35 | "react-dom": "^18.2.0", 36 | "react-icons": "^4.11.0", 37 | "react-router-dom": "^6.17.0", 38 | "react-scripts": "5.0.1", 39 | "recharts": "^2.9.0", 40 | "sonner": "^1.0.3", 41 | "web-vitals": "^2.1.4", 42 | "zustand": "^4.4.4" 43 | }, 44 | "scripts": { 45 | "start": "react-scripts start", 46 | "build": "react-scripts build", 47 | "test": "react-scripts test", 48 | "eject": "react-scripts eject" 49 | }, 50 | "eslintConfig": { 51 | "extends": [ 52 | "react-app", 53 | "react-app/jest" 54 | ] 55 | }, 56 | "browserslist": { 57 | "production": [ 58 | ">0.2%", 59 | "not dead", 60 | "not op_mini all" 61 | ], 62 | "development": [ 63 | "last 1 chrome version", 64 | "last 1 firefox version", 65 | "last 1 safari version" 66 | ] 67 | }, 68 | "devDependencies": { 69 | "postcss": "^8.4.31", 70 | "postcss-preset-mantine": "^1.9.0", 71 | "postcss-simple-vars": "^7.0.1", 72 | "tailwindcss": "^3.3.5" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /admin/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeWaveWithAsante/fullstack_blog_app/657972bddcee6bb7d4eedbddabf9cd1988a3ccee/admin/public/favicon.ico -------------------------------------------------------------------------------- /admin/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /admin/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeWaveWithAsante/fullstack_blog_app/657972bddcee6bb7d4eedbddabf9cd1988a3ccee/admin/public/logo192.png -------------------------------------------------------------------------------- /admin/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeWaveWithAsante/fullstack_blog_app/657972bddcee6bb7d4eedbddabf9cd1988a3ccee/admin/public/logo512.png -------------------------------------------------------------------------------- /admin/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /admin/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /admin/src/App.js: -------------------------------------------------------------------------------- 1 | import { Navigate, Outlet, Route, Routes, useLocation } from "react-router-dom"; 2 | import Navbar from "./components/Navbar"; 3 | import Sidebar from "./components/Sidebar"; 4 | import OTPVerification from "./pages/OTPVerification"; 5 | import StartPage from "./pages/StartPage"; 6 | import useStore from "./store/index"; 7 | import Dashboard from "./pages/Dashboard"; 8 | import Analytics from "./pages/Analytics"; 9 | import Followers from "./pages/Followers"; 10 | import Contents from "./pages/Content"; 11 | import WritePost from "./pages/WritePost"; 12 | 13 | function Layout() { 14 | const { user } = useStore((state) => state); 15 | 16 | const location = useLocation(); 17 | 18 | return user?.token ? ( 19 |
20 | 21 |
22 |
23 | 24 |
25 | 26 |
27 | 28 |
29 |
30 |
31 | ) : ( 32 | 33 | ); 34 | } 35 | 36 | function App() { 37 | return ( 38 |
39 | 40 | }> 41 | } /> 42 | } /> 43 | } /> 44 | } /> 45 | } /> 46 | } /> 47 | 48 | 49 | } /> 50 | } /> 51 | 52 |
53 | ); 54 | } 55 | 56 | export default App; 57 | -------------------------------------------------------------------------------- /admin/src/assets/profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeWaveWithAsante/fullstack_blog_app/657972bddcee6bb7d4eedbddabf9cd1988a3ccee/admin/src/assets/profile.png -------------------------------------------------------------------------------- /admin/src/assets/writer.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeWaveWithAsante/fullstack_blog_app/657972bddcee6bb7d4eedbddabf9cd1988a3ccee/admin/src/assets/writer.jpg -------------------------------------------------------------------------------- /admin/src/components/Comments.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import useCommentStore from "../store/commentStore"; 3 | import useStore from "../store"; 4 | import { Modal } from "@mantine/core"; 5 | import { useComments, useDeleteComment } from "../hooks/post-hook"; 6 | import NoProfile from "../assets/profile.png"; 7 | 8 | const Comments = () => { 9 | const { openComment, commentId, setOpen } = useCommentStore(); 10 | const { user } = useStore(); 11 | 12 | const { data, mutate } = useComments(); 13 | 14 | const useDelete = useDeleteComment(user?.token); 15 | 16 | const handleClose = () => { 17 | setOpen(false); 18 | }; 19 | 20 | const handleDelete = (id) => { 21 | useDelete.mutate({ id, postId: commentId }); 22 | }; 23 | 24 | useEffect(() => { 25 | mutate(commentId); 26 | }, [commentId]); 27 | 28 | return ( 29 | <> 30 | 37 |
38 |
39 | {data?.data?.map(({ _id, user, desc, post, createdAt }) => { 40 |
41 | Profile 46 | 47 |
48 |
49 |
50 |

51 | {user.name} 52 |

53 | 54 | {new Date(createdAt).toDateString()} 55 | 56 |
57 | 58 | handleDelete(_id)} 61 | > 62 | Delete 63 | 64 |
65 | 66 | 67 | {desc} 68 | 69 |
70 |
; 71 | })} 72 |
73 |
74 |
75 | 76 | ); 77 | }; 78 | 79 | export default Comments; 80 | -------------------------------------------------------------------------------- /admin/src/components/ConfirmDialog.jsx: -------------------------------------------------------------------------------- 1 | import { Button, Modal } from "@mantine/core"; 2 | 3 | const ConfirmDialog = ({ message, opened, close, handleClick }) => { 4 | return ( 5 | 6 |

{message}

7 | 8 |
9 | 15 | 21 |
22 |
23 | ); 24 | }; 25 | 26 | export default ConfirmDialog; 27 | -------------------------------------------------------------------------------- /admin/src/components/Graph.jsx: -------------------------------------------------------------------------------- 1 | import { 2 | Area, 3 | AreaChart, 4 | ResponsiveContainer, 5 | Tooltip, 6 | XAxis, 7 | YAxis, 8 | } from "recharts"; 9 | 10 | const Graph = ({ dt }) => { 11 | return ( 12 | 13 | {dt?.length > 0 ? ( 14 | 15 | 16 | 17 | 18 | 24 | 25 | ) : ( 26 | No Data 31 | )} 32 | 33 | ); 34 | }; 35 | 36 | export default Graph; 37 | -------------------------------------------------------------------------------- /admin/src/components/Loading.jsx: -------------------------------------------------------------------------------- 1 | import { LoadingOverlay } from "@mantine/core"; 2 | 3 | export default function Loading({ visible, toggle }) { 4 | return ( 5 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /admin/src/components/LoginForm.jsx: -------------------------------------------------------------------------------- 1 | import { Button, Group, TextInput, useMantineColorScheme } from "@mantine/core"; 2 | import { useForm } from "@mantine/form"; 3 | import { useInputState } from "@mantine/hooks"; 4 | import clsx from "clsx"; 5 | import React, { useState } from "react"; 6 | import { useNavigate } from "react-router-dom"; 7 | import useStore from "../store/index"; 8 | import { PasswordStrength } from "./PasswordStrength"; 9 | import { useSignin } from "../hooks/auth-hook"; 10 | 11 | const LoginForm = ({ toast, isSignin, setIsSignin, toggle, setFormClose }) => { 12 | const { colorScheme } = useMantineColorScheme(); 13 | const theme = colorScheme === "dark"; 14 | 15 | const { signIn } = useStore(); 16 | const { data, isPemding, isSuccess, mutate } = useSignin(toast, toggle); 17 | 18 | const [strength, setStrength] = useState(0); 19 | const [passValue, setPassValue] = useInputState(""); 20 | const navigate = useNavigate(); 21 | 22 | const form = useForm({ 23 | initialValues: { 24 | email: "", 25 | }, 26 | validate: { 27 | email: (value) => (/^\S+@\S+$/.test(value) ? null : "Invalid email"), 28 | }, 29 | }); 30 | 31 | const handleSubmit = async (values) => { 32 | setFormClose(true); 33 | 34 | mutate({ 35 | ...values, 36 | password: passValue, 37 | }); 38 | 39 | if (isSuccess) { 40 | setFormClose(false); 41 | setTimeout(() => { 42 | signIn(data); 43 | 44 | navigate("/dashboard"); 45 | }, 2000); 46 | } 47 | }; 48 | 49 | return ( 50 |
54 | 60 | 61 | 67 | 68 | 75 | 81 | 82 |

83 | {isSignin ? "Don't have an account?" : "Already has an account?"} 84 | setIsSignin((prev) => !prev)} 87 | > 88 | {isSignin ? "Sign up" : "Sign in"} 89 | 90 |

91 | 92 | ); 93 | }; 94 | 95 | export default LoginForm; 96 | -------------------------------------------------------------------------------- /admin/src/components/Logo.jsx: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router-dom"; 2 | 3 | const Logo = ({ type }) => { 4 | return ( 5 |
6 | 10 | Blog 11 | 14 | Wave 15 | 16 | 17 |
18 | ); 19 | }; 20 | 21 | export default Logo; 22 | -------------------------------------------------------------------------------- /admin/src/components/Navbar.jsx: -------------------------------------------------------------------------------- 1 | import { 2 | Button, 3 | Drawer, 4 | Menu, 5 | rem, 6 | useMantineColorScheme, 7 | } from "@mantine/core"; 8 | import { useDisclosure } from "@mantine/hooks"; 9 | import { IconTrash } from "@tabler/icons-react"; 10 | import clsx from "clsx"; 11 | import React from "react"; 12 | import { AiOutlineLogout } from "react-icons/ai"; 13 | import { BiMenu } from "react-icons/bi"; 14 | import { 15 | FaFacebook, 16 | FaInstagram, 17 | FaTwitterSquare, 18 | FaUser, 19 | FaYoutube, 20 | } from "react-icons/fa"; 21 | import { MdArrowForward } from "react-icons/md"; 22 | import { Link, useLocation } from "react-router-dom"; 23 | import useStore from "../store"; 24 | import Logo from "./Logo"; 25 | import Sidebar from "./Sidebar"; 26 | 27 | const MobileDrawer = ({ theme }) => { 28 | const { user } = useStore(); 29 | const [opened, { open, close }] = useDisclosure(false); 30 | 31 | return ( 32 | <> 33 | 38 | 39 | 40 |
41 | 42 |
43 | 44 | 50 |
51 | 52 | 58 | 59 | ); 60 | }; 61 | 62 | function UserMenu({ user, theme }) { 63 | const { signOut } = useStore(); 64 | 65 | const handleSignOut = () => { 66 | localStorage.removeItem("user"); 67 | signOut(); 68 | }; 69 | 70 | return ( 71 | 72 | 73 | 88 | 89 | 90 | 91 | Application 92 | } 94 | > 95 | Profile 96 | 97 | 100 | } 101 | onClick={() => handleSignOut()} 102 | > 103 | Logout 104 | 105 | 106 | 107 | 108 | Danger Zone 109 | 113 | } 114 | onClick={() => {}} 115 | > 116 | Delete account 117 | 118 | 119 | 120 | ); 121 | } 122 | 123 | const Navbar = () => { 124 | const { colorScheme } = useMantineColorScheme(); 125 | 126 | const { user, signInModal, setSignInModal } = useStore(); 127 | const location = useLocation(); 128 | const theme = colorScheme === "dark"; 129 | 130 | const handleLogin = () => { 131 | location.pathname === "/auth" && setSignInModal(!signInModal); 132 | }; 133 | 134 | return ( 135 |
136 | {user && ( 137 |
138 | 139 |
140 | )} 141 | 142 |
143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 |
156 | 157 | 158 | 159 |
160 |
161 | {user?.token ? ( 162 | 163 | ) : ( 164 | 172 | Login 173 | 174 | 175 | )} 176 |
177 |
178 |
179 | ); 180 | }; 181 | 182 | export default Navbar; 183 | -------------------------------------------------------------------------------- /admin/src/components/PasswordStrength.jsx: -------------------------------------------------------------------------------- 1 | import { 2 | Box, 3 | Progress, 4 | PasswordInput, 5 | Group, 6 | Text, 7 | Center, 8 | } from "@mantine/core"; 9 | import { IconCheck, IconX } from "@tabler/icons-react"; 10 | 11 | function PasswordRequirement({ meets, label }) { 12 | return ( 13 | 14 |
15 | {meets ? ( 16 | 17 | ) : ( 18 | 19 | )} 20 | {label} 21 |
22 |
23 | ); 24 | } 25 | 26 | const requirements = [ 27 | { re: /[0-9]/, label: "Includes number" }, 28 | { re: /[a-z]/, label: "Includes lowercase letter" }, 29 | { re: /[A-Z]/, label: "Includes uppercase letter" }, 30 | { re: /[$&+,:;=?@#|'<>.^*()%!-]/, label: "Includes special symbol" }, 31 | ]; 32 | 33 | function getStrength(password) { 34 | let multiplier = password.length > 5 ? 0 : 1; 35 | 36 | requirements.forEach((requirement) => { 37 | if (!requirement.re.test(password)) { 38 | multiplier += 1; 39 | } 40 | }); 41 | 42 | return Math.max(100 - (100 / (requirements.length + 1)) * multiplier, 0); 43 | } 44 | 45 | export function PasswordStrength({ value, setValue, setStrength, isSignin }) { 46 | const strength = getStrength(value); 47 | setStrength(strength); 48 | 49 | const checks = requirements.map((requirement, index) => ( 50 | 55 | )); 56 | 57 | const bars = Array(4) 58 | .fill(0) 59 | .map((_, index) => ( 60 | 0 && index === 0 64 | ? 100 65 | : strength >= ((index + 1) / 4) * 100 66 | ? 100 67 | : 0 68 | } 69 | color={strength > 80 ? "teal" : strength > 50 ? "yellow" : "red"} 70 | key={index} 71 | size={4} 72 | /> 73 | )); 74 | 75 | return ( 76 |
77 | 84 | 85 | {!isSignin && ( 86 | <> 87 | 88 | {bars} 89 | 90 | 91 | 5} 94 | /> 95 | {checks} 96 | 97 | )} 98 |
99 | ); 100 | } 101 | -------------------------------------------------------------------------------- /admin/src/components/Sidebar.jsx: -------------------------------------------------------------------------------- 1 | import { 2 | IconCalendarStats, 3 | IconDeviceDesktopAnalytics, 4 | IconGauge, 5 | IconSettings, 6 | IconUser, 7 | } from "@tabler/icons-react"; 8 | import { 9 | ActionIcon, 10 | Stack, 11 | Tooltip, 12 | UnstyledButton, 13 | rem, 14 | useMantineColorScheme, 15 | } from "@mantine/core"; 16 | import { IconSun, IconMoon } from "@tabler/icons-react"; 17 | import { BsPencilSquare } from "react-icons/bs"; 18 | import { useLocation, useNavigate } from "react-router-dom"; 19 | import clsx from "clsx"; 20 | 21 | const mockdata = [ 22 | { icon: IconGauge, label: "Dashboard", to: "dashboard" }, 23 | { icon: IconDeviceDesktopAnalytics, label: "Analytics", to: "analytics" }, 24 | { icon: IconCalendarStats, label: "Content", to: "contents" }, 25 | { icon: IconUser, label: "Followers", to: "followers" }, 26 | { icon: BsPencilSquare, label: "Create Post", to: "write" }, 27 | { icon: IconSettings, label: "Settings" }, 28 | ]; 29 | 30 | const NavbarLink = ({ icon: Icon, label, active, onClick }) => { 31 | return ( 32 | 33 | 41 | 42 | 43 | {label} 44 | 45 | 46 | ); 47 | }; 48 | 49 | const Sidebar = ({ close = () => {} }) => { 50 | const { colorScheme, setColorScheme } = useMantineColorScheme(); 51 | 52 | const navigate = useNavigate(); 53 | const location = useLocation(); 54 | 55 | const path = location.pathname?.slice(1); 56 | 57 | const handleClick = (to) => { 58 | close(); 59 | navigate(to); 60 | }; 61 | 62 | const links = mockdata.map((link, index) => ( 63 | handleClick(link.to)} 68 | /> 69 | )); 70 | 71 | return ( 72 | 95 | ); 96 | }; 97 | 98 | export default Sidebar; 99 | -------------------------------------------------------------------------------- /admin/src/components/SignUpForm.jsx: -------------------------------------------------------------------------------- 1 | import { Button, Group, TextInput, useMantineColorScheme } from "@mantine/core"; 2 | import { useForm } from "@mantine/form"; 3 | import { useInputState } from "@mantine/hooks"; 4 | import React, { useEffect, useState } from "react"; 5 | import { BiImages } from "react-icons/bi"; 6 | import { useNavigate } from "react-router-dom"; 7 | import { useSignUp } from "../hooks/auth-hook"; 8 | import { uploadFile } from "../utils"; 9 | import { PasswordStrength } from "./PasswordStrength"; 10 | import { clsx } from "clsx"; 11 | 12 | const SignUpForm = ({ toast, isSignin, setIsSignin, toggle, setFormClose }) => { 13 | const { colorScheme } = useMantineColorScheme(); 14 | const theme = colorScheme === "dark"; 15 | 16 | const { mutate } = useSignUp(toast, toggle); 17 | const [strength, setStrength] = useState(0); 18 | const [file, setFile] = useState(""); 19 | const [fileURL, setFileURL] = useState(""); 20 | const [passValue, setPassValue] = useInputState(""); 21 | const navigate = useNavigate(); 22 | 23 | const form = useForm({ 24 | initialValues: { 25 | email: "", 26 | firstName: "", 27 | lastName: "", 28 | }, 29 | validate: { 30 | firstName: (value) => 31 | value.length < 3 ? "First name is too short" : null, 32 | lastName: (value) => (value.length < 2 ? "Last name is too short" : null), 33 | email: (value) => (/^\S+@\S+$/.test(value) ? null : "Invalid email"), 34 | }, 35 | }); 36 | 37 | const handleSubmit = (values) => { 38 | if (!isSignin && strength < 90) return; 39 | setFormClose(true); 40 | 41 | const res = mutate({ 42 | ...values, 43 | password: passValue, 44 | image: fileURL, 45 | accountType: "Writer", 46 | }); 47 | }; 48 | 49 | useEffect(() => { 50 | file && uploadFile(setFileURL, file); 51 | }, [file]); 52 | 53 | return ( 54 |
58 |
59 | 66 | 73 |
74 | 75 | 81 | 82 | 88 | 89 | 90 |
91 | 109 |
110 | 111 | 117 |
118 |

119 | {isSignin ? "Don't have an account?" : "Already has an account?"} 120 | setIsSignin((prev) => !prev)} 123 | > 124 | {isSignin ? "Sign up" : "Sign in"} 125 | 126 |

127 | 128 | ); 129 | }; 130 | 131 | export default SignUpForm; 132 | -------------------------------------------------------------------------------- /admin/src/components/Stats.jsx: -------------------------------------------------------------------------------- 1 | import { Group, Paper, SimpleGrid, Text } from "@mantine/core"; 2 | import { IconArrowDownRight, IconArrowUpRight } from "@tabler/icons-react"; 3 | import { BsEye, BsPostcardHeart } from "react-icons/bs"; 4 | import { FaUsers, FaUsersCog } from "react-icons/fa"; 5 | import { formatNumber } from "../utils"; 6 | 7 | const icons = { 8 | user: FaUsersCog, 9 | view: BsEye, 10 | post: BsPostcardHeart, 11 | users: FaUsers, 12 | }; 13 | 14 | const Stats = ({ dt }) => { 15 | const data = [ 16 | { 17 | title: "TOTAL POST", 18 | icon: "post", 19 | value: formatNumber(dt?.totalPosts ?? 0), 20 | diff: 34, 21 | }, 22 | { 23 | title: "FOLLOWERS", 24 | icon: "users", 25 | value: formatNumber(dt?.followers ?? 0), 26 | diff: -13, 27 | }, 28 | { 29 | title: "TOTAL VIEWS", 30 | icon: "view", 31 | value: formatNumber(dt?.totalViews ?? 0), 32 | diff: 18, 33 | }, 34 | { 35 | title: "TOTAL WRITERS", 36 | icon: "user", 37 | value: formatNumber(dt?.totalWriters ?? 0), 38 | diff: -30, 39 | }, 40 | ]; 41 | 42 | const stats = data?.map((stat) => { 43 | const Icon = icons[stat.icon]; 44 | const DiffIcon = stat.diff > 0 ? IconArrowUpRight : IconArrowDownRight; 45 | 46 | return ( 47 | 48 | 49 | {stat.title} 50 | 51 | 52 | 53 | 54 | {stat.value} 55 | 56 | 0 ? "teal" : "red"} 58 | fz='sm' 59 | fw='500' 60 | className='font-medium' 61 | > 62 | {stat.diff}% 63 | 64 | 65 | 66 | 67 | 68 | Compare to previous month 69 | 70 | 71 | ); 72 | }); 73 | return {stats}; 74 | }; 75 | 76 | export default Stats; 77 | -------------------------------------------------------------------------------- /admin/src/components/Table.jsx: -------------------------------------------------------------------------------- 1 | import { Table } from "@mantine/core"; 2 | import { formatNumber, getInitials } from "../utils"; 3 | import moment from "moment"; 4 | 5 | export const RecentFollowerTable = ({ data, theme }) => { 6 | const tableData = data?.map(({ _id, createdAt, followerId: follower }) => ( 7 | 8 | 9 | {follower?.image ? ( 10 | {follower.name} 15 | ) : ( 16 |

17 | {getInitials(follower.name)} 18 |

19 | )} 20 | 21 | <> 22 |

{follower.name}

23 |
24 | 25 | {follower.accountType} 26 | 27 | 28 | {follower.followers.length > 0 && ( 29 | 30 | {formatNumber(follower.followers.length)} 31 | 32 | )} 33 |
34 | 35 |
36 | 37 | {moment(createdAt).fromNow()} 38 |
39 | )); 40 | 41 | return ( 42 | 43 | 44 | 45 | Follower 46 | Join Date 47 | 48 | 49 | 50 | {data?.length === 0 && No Data Found.} 51 | 52 | {tableData} 53 |
54 | ); 55 | }; 56 | 57 | export const RecentPostTable = ({ data, theme }) => { 58 | const tableData = data?.map((el) => ( 59 | 63 | 64 | {el?.title} 69 | 70 | <> 71 |

{el?.title}

72 | {el?.cat} 73 | 74 |
75 | {formatNumber(el?.views.length)} 76 | {moment(el?.createdAt).fromNow()} 77 |
78 | )); 79 | 80 | return ( 81 | 82 | 83 | 84 | Post Title 85 | Views 86 | Post Date 87 | 88 | 89 | {data?.length === 0 && No Data Found.} 90 | {tableData} 91 |
92 | ); 93 | }; 94 | -------------------------------------------------------------------------------- /admin/src/hooks/auth-hook.js: -------------------------------------------------------------------------------- 1 | import { useMutation } from "@tanstack/react-query"; 2 | import axios from "axios"; 3 | import { API_URI } from "../utils"; 4 | 5 | export const useSignUp = (toast, toggle) => { 6 | return useMutation({ 7 | mutationFn: async (formData) => { 8 | toggle(); 9 | const { data } = await axios.post(`${API_URI}/auth/register`, formData); 10 | 11 | return data; 12 | }, 13 | onError: (error, data) => { 14 | toggle(); 15 | toast.error(error?.response?.data?.message ?? error.message); 16 | }, 17 | onSuccess: (data) => { 18 | toggle(); 19 | console.log(data); 20 | toast.success(data?.message); 21 | localStorage.setItem( 22 | "otp_data", 23 | JSON.stringify({ 24 | otpLevel: true, 25 | id: data.user._id, 26 | }) 27 | ); 28 | setTimeout(() => { 29 | window.location.replace("/otp-verification"); 30 | }, 3000); 31 | }, 32 | }); 33 | }; 34 | 35 | export const useSignin = (toast, toggle) => { 36 | return useMutation({ 37 | mutationFn: async (formData) => { 38 | toggle(); 39 | const { data } = await axios.post(`${API_URI}/auth/login`, formData); 40 | 41 | localStorage.setItem("user", JSON.stringify(data)); 42 | 43 | return data; 44 | }, 45 | onError: (error) => { 46 | toggle(); 47 | toast.error(error?.response?.data?.message ?? error.message); 48 | }, 49 | onSuccess: (data) => { 50 | toggle(); 51 | toast.success(data?.message); 52 | 53 | setTimeout(() => { 54 | window.location.replace("/"); 55 | }, 1000); 56 | }, 57 | }); 58 | }; 59 | 60 | export const useResend = (toast, toggle) => { 61 | return useMutation({ 62 | mutationFn: async (id) => { 63 | toggle(); 64 | const { data } = await axios.post(`${API_URI}/users/resend-link/${id}`); 65 | 66 | return data; 67 | }, 68 | onError: (error, data) => { 69 | toggle(); 70 | toast.error(error?.response?.data?.message ?? error.message); 71 | }, 72 | onSuccess: (data) => { 73 | toggle(); 74 | toast.success(data?.message); 75 | 76 | localStorage.setItem( 77 | "otp_data", 78 | JSON.stringify({ 79 | otpLevel: true, 80 | id: data.user._id, 81 | }) 82 | ); 83 | 84 | setTimeout(() => { 85 | window.location.reload(); 86 | }, 1000); 87 | }, 88 | }); 89 | }; 90 | 91 | export const useVerification = (toast) => { 92 | return useMutation({ 93 | mutationFn: async ({ id, otp }) => { 94 | const { data } = await axios.post(`${API_URI}/users/verify/${id}/${otp}`); 95 | return data; 96 | }, 97 | onError: (error, data) => { 98 | toast.error(error?.response?.data?.message ?? error.message); 99 | }, 100 | onSuccess: (data) => { 101 | toast.success(data?.message); 102 | 103 | setTimeout(() => { 104 | localStorage.removeItem("otp_data"); 105 | window.location.replace("/auth"); 106 | }, 1000); 107 | }, 108 | }); 109 | }; 110 | -------------------------------------------------------------------------------- /admin/src/hooks/followers-hook.js: -------------------------------------------------------------------------------- 1 | import { useMutation } from "@tanstack/react-query"; 2 | import axios from "axios"; 3 | import { API_URI } from "../utils"; 4 | 5 | export const useFollowers = (toast, toggle, token) => { 6 | return useMutation({ 7 | mutationFn: async (page) => { 8 | toggle(); 9 | 10 | const { data } = await axios.post( 11 | `${API_URI}/posts/admin-followers?page=${page}`, 12 | null, 13 | { 14 | headers: { 15 | Authorization: "Bearer " + token, 16 | }, 17 | } 18 | ); 19 | 20 | return data; 21 | }, 22 | onError: (error) => { 23 | toggle(); 24 | const errMsg = error?.response?.data?.message; 25 | toast.error(errMsg ?? error.message); 26 | if (errMsg === "Authentication failed") { 27 | localStorage.removeItem("user"); 28 | } 29 | }, 30 | onSuccess: (data) => { 31 | toggle(); 32 | toast.success("Loaded Successfully"); 33 | }, 34 | }); 35 | }; 36 | -------------------------------------------------------------------------------- /admin/src/hooks/post-hook.js: -------------------------------------------------------------------------------- 1 | import { useMutation } from "@tanstack/react-query"; 2 | import axios from "axios"; 3 | 4 | import { API_URI } from "../utils"; 5 | 6 | export const useAnalytics = (toast, toggle, token) => { 7 | return useMutation({ 8 | mutationFn: async (val) => { 9 | toggle(); 10 | 11 | const { data } = await axios.post( 12 | `${API_URI}/posts/admin-analytics?query=${val}`, 13 | null, 14 | { 15 | headers: { 16 | Authorization: `Bearer ${token}`, 17 | }, 18 | } 19 | ); 20 | 21 | return data; 22 | }, 23 | 24 | onError: (error) => { 25 | toggle(); 26 | const errMsg = error?.response?.data?.message; 27 | toast.error(errMsg ?? error.message); 28 | if (errMsg === "Authentication failed") { 29 | localStorage.removeItem("user"); 30 | } 31 | }, 32 | onSuccess: (data) => { 33 | toggle(); 34 | toast.success(data?.message); 35 | }, 36 | }); 37 | }; 38 | 39 | export const useCreatePost = (toast, toggle, token) => { 40 | return useMutation({ 41 | mutationFn: async (formData) => { 42 | toggle(); 43 | 44 | const { data } = await axios.post( 45 | `${API_URI}/posts/create-post`, 46 | formData, 47 | { 48 | headers: { 49 | Authorization: `Bearer ${token}`, 50 | }, 51 | } 52 | ); 53 | 54 | return data; 55 | }, 56 | 57 | onError: (error) => { 58 | toggle(); 59 | toast.error(error?.response?.data?.message ?? error.message); 60 | }, 61 | onSuccess: (data) => { 62 | toggle(); 63 | toast.success(data?.message); 64 | 65 | setTimeout(() => { 66 | window.location.replace("/contents"); 67 | }, 2000); 68 | }, 69 | }); 70 | }; 71 | 72 | export const useContent = (toast, toggle, token) => { 73 | return useMutation({ 74 | mutationFn: async (page) => { 75 | toggle(); 76 | 77 | const { data } = await axios.post( 78 | `${API_URI}/posts/admin-content?page=${page}`, 79 | null, 80 | { 81 | headers: { 82 | Authorization: `Bearer ${token}`, 83 | }, 84 | } 85 | ); 86 | 87 | return data; 88 | }, 89 | 90 | onError: (error) => { 91 | toggle(); 92 | const errMsg = error?.response?.data?.message; 93 | toast.error(errMsg ?? error.message); 94 | if (errMsg === "Authentication failed") { 95 | localStorage.removeItem("user"); 96 | } 97 | }, 98 | onSuccess: (data) => { 99 | toggle(); 100 | toast.success(data?.message); 101 | }, 102 | }); 103 | }; 104 | 105 | export const useDeletePost = (toast, token) => { 106 | return useMutation({ 107 | mutationFn: async (id) => { 108 | const { data } = await axios.delete( 109 | `${API_URI}/posts/${id}`, 110 | 111 | { 112 | headers: { 113 | Authorization: `Bearer ${token}`, 114 | }, 115 | } 116 | ); 117 | 118 | return data; 119 | }, 120 | 121 | onError: (error) => { 122 | toast.error(error?.response?.data?.message ?? error.message); 123 | }, 124 | onSuccess: (data) => { 125 | toast.success(data?.message); 126 | }, 127 | }); 128 | }; 129 | 130 | export const useAction = (toast, token) => { 131 | return useMutation({ 132 | mutationFn: async ({ id, status }) => { 133 | const { data } = await axios.patch( 134 | `${API_URI}/posts/update/` + id, 135 | { status: status }, 136 | { 137 | headers: { 138 | Authorization: `Bearer ${token}`, 139 | }, 140 | } 141 | ); 142 | 143 | return data; 144 | }, 145 | onError: (error) => { 146 | toast.error(error?.response?.data?.message ?? error.message); 147 | }, 148 | onSuccess: (data) => { 149 | toast.success(data?.message); 150 | }, 151 | }); 152 | }; 153 | 154 | export const useComments = () => { 155 | return useMutation({ 156 | mutationFn: async (id) => { 157 | const { data } = await axios.get(`${API_URI}/posts/comments/` + id); 158 | 159 | return data; 160 | }, 161 | }); 162 | }; 163 | 164 | export const useDeleteComment = (token) => { 165 | return useMutation({ 166 | mutationFn: async ({ id, postId }) => { 167 | const { data } = await axios.delete( 168 | `${API_URI}/posts/comment/${id}/${postId}`, 169 | { 170 | headers: { 171 | Authorization: `Bearer ${token}`, 172 | }, 173 | } 174 | ); 175 | 176 | return data; 177 | }, 178 | }); 179 | }; 180 | -------------------------------------------------------------------------------- /admin/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | body { 6 | margin: 0; 7 | padding: 0; 8 | box-sizing: border-box; 9 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 10 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 11 | sans-serif; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | } 15 | 16 | .stickyNavbar { 17 | position: fixed; 18 | top: 0px; 19 | z-index: 49; 20 | } 21 | 22 | .page-item.active .page-link { 23 | z-index: 30; 24 | color: #fff !important; 25 | background-color: blue !important; 26 | } 27 | 28 | /* Custom Scrollbar Styles */ 29 | /* Track */ 30 | ::-webkit-scrollbar { 31 | width: 8px; 32 | } 33 | 34 | /* Handle */ 35 | ::-webkit-scrollbar-thumb { 36 | background-color: #807c7c; 37 | border-radius: 6px; 38 | } 39 | 40 | /* Handle on hover */ 41 | ::-webkit-scrollbar-thumb:hover { 42 | background-color: #302f2f; 43 | } 44 | 45 | /* Track */ 46 | ::-webkit-scrollbar-track { 47 | background: transparent; 48 | } 49 | -------------------------------------------------------------------------------- /admin/src/index.js: -------------------------------------------------------------------------------- 1 | import { MantineProvider } from "@mantine/core"; 2 | import "@mantine/core/styles.css"; 3 | import "@mantine/tiptap/styles.css"; 4 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 5 | import React from "react"; 6 | import ReactDOM from "react-dom/client"; 7 | import { BrowserRouter } from "react-router-dom"; 8 | 9 | import App from "./App"; 10 | import "./index.css"; 11 | 12 | const root = ReactDOM.createRoot(document.getElementById("root")); 13 | const queryClient = new QueryClient(); 14 | 15 | root.render( 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | ); 26 | -------------------------------------------------------------------------------- /admin/src/pages/Analytics.jsx: -------------------------------------------------------------------------------- 1 | import { Select, useMantineColorScheme } from "@mantine/core"; 2 | import { useDisclosure } from "@mantine/hooks"; 3 | import { useEffect, useState } from "react"; 4 | import { Toaster, toast } from "sonner"; 5 | import Loading from "../components/Loading"; 6 | import { useAnalytics } from "../hooks/post-hook"; 7 | import useStore from "../store"; 8 | import clsx from "clsx"; 9 | import Stats from "../components/Stats"; 10 | import Graph from "../components/Graph"; 11 | 12 | const Analytics = () => { 13 | const { colorScheme } = useMantineColorScheme(); 14 | 15 | const { user } = useStore(); 16 | const [numOfDays, setNumberOfDays] = useState(28); 17 | const [visible, { toggle }] = useDisclosure(false); 18 | const { data, isPending, mutate } = useAnalytics(toast, toggle, user?.token); 19 | 20 | const theme = colorScheme === "dark"; 21 | 22 | useEffect(() => { 23 | mutate(numOfDays); 24 | }, [numOfDays]); 25 | 26 | return ( 27 |
28 |
29 |

35 | Analytics 36 |

37 | 38 | setCategory(val)} 99 | /> 100 | 101 | 116 |
117 | 118 | {editor && ( 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | )} 127 | 128 | 129 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 |
204 | 210 |
211 | 212 | 213 | 214 | 215 | ); 216 | }; 217 | 218 | export default WritePost; 219 | -------------------------------------------------------------------------------- /admin/src/store/commentStore.js: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | 3 | const useCommentStore = create((set) => ({ 4 | openComment: false, 5 | 6 | commentId: null, 7 | 8 | setOpen: (val) => set((state) => ({ openComment: val })), 9 | setCommentId: (val) => set((state) => ({ commentId: val })), 10 | })); 11 | 12 | export default useCommentStore; 13 | -------------------------------------------------------------------------------- /admin/src/store/index.js: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | 3 | const useStore = create((set) => ({ 4 | user: JSON.parse(localStorage.getItem("user")) || {}, 5 | isOTPLevel: false, 6 | otpData: JSON.parse(localStorage.getItem("otp_data")), 7 | signInModal: false, 8 | 9 | signIn: (data) => 10 | set((state) => ({ 11 | user: data, 12 | })), 13 | 14 | setPT: (val) => set((state) => ({ isOTPLevel: val })), 15 | 16 | signOut: () => set({ user: {} }), 17 | setSignInModal: (val) => set((state) => ({ signInModal: val })), 18 | })); 19 | 20 | export default useStore; 21 | -------------------------------------------------------------------------------- /admin/src/utils/firebase.js: -------------------------------------------------------------------------------- 1 | import { initializeApp } from "firebase/app"; 2 | 3 | const firebaseConfig = { 4 | apiKey: process.env.REACT_APP_FIREBASE_APIKEY, 5 | authDomain: "fullstack-blog-app-b0ac2.firebaseapp.com", 6 | projectId: "fullstack-blog-app-b0ac2", 7 | storageBucket: "fullstack-blog-app-b0ac2.appspot.com", 8 | messagingSenderId: "1038252361509", 9 | appId: "1:1038252361509:web:eb8b16ce44e3dbd3407ccc", 10 | }; 11 | 12 | export const app = initializeApp(firebaseConfig); 13 | -------------------------------------------------------------------------------- /admin/src/utils/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | getDownloadURL, 3 | getStorage, 4 | ref, 5 | uploadBytesResumable, 6 | } from "firebase/storage"; 7 | import { app } from "./firebase"; 8 | 9 | export const API_URI = "http://localhost:8800"; 10 | 11 | export const uploadFile = (setFileURL, file) => { 12 | const storage = getStorage(app); 13 | 14 | const name = new Date().getTime() + file.name; 15 | const storageRef = ref(storage, name); 16 | 17 | const uploadTask = uploadBytesResumable(storageRef, file); 18 | 19 | uploadTask.on( 20 | "state_changed", 21 | (snapshot) => { 22 | const progress = (snapshot.bytesTransferred / snapshot.totalBytes) * 100; 23 | console.log("Upload is " + progress + "% done"); 24 | 25 | switch (snapshot.state) { 26 | case "paused": 27 | console.log("Upload is paused"); 28 | break; 29 | case "running": 30 | console.log("Upload is running"); 31 | break; 32 | } 33 | }, 34 | (error) => { 35 | console.log(error); 36 | }, 37 | () => { 38 | getDownloadURL(uploadTask.snapshot.ref).then((downloadURL) => { 39 | console.log("Successfully uploaded"); 40 | setFileURL(downloadURL); 41 | }); 42 | } 43 | ); 44 | }; 45 | 46 | export function formatNumber(num) { 47 | if (num >= 1000000) { 48 | return (num / 1000000).toFixed(1) + "M"; 49 | } else if (num >= 1000) { 50 | return (num / 1000).toFixed(1) + "K"; 51 | } 52 | 53 | return num.toString(); 54 | } 55 | 56 | export function getInitials(fullName) { 57 | const names = fullName.split(" "); //code wave asante 58 | 59 | const initials = names.slice(0, 2).map((name) => name[0].toUpperCase()); 60 | 61 | const initialsStr = initials.join(""); 62 | 63 | return initialsStr; 64 | } 65 | 66 | export function createSlug(title) { 67 | return title 68 | .toLowerCase() 69 | .replace(/\s+/g, "-") // Replace spaces with - 70 | .replace(/[^\w-]+/g, "") // Remove non-word characters 71 | .replace(/--+/g, "-") // Replace multiple - with single - 72 | .replace(/^-+/, "") // Trim - from start of text 73 | .replace(/-+$/, ""); // Trim - from end of text 74 | } 75 | 76 | export const updateURL = ({ page, navigate, location }) => { 77 | const params = new URLSearchParams(); 78 | 79 | if (page && page > 1) { 80 | params.set("page", page); 81 | } 82 | 83 | const newURL = `${location.pathname}?${params.toString()}`; 84 | navigate(newURL, { replace: true }); 85 | 86 | return newURL; 87 | }; 88 | -------------------------------------------------------------------------------- /admin/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ["./src/**/*.{html,js,jsx}"], 4 | theme: { 5 | extend: {}, 6 | }, 7 | plugins: [], 8 | }; 9 | -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in your browser. 13 | 14 | The page will reload when you make changes.\ 15 | You may also see any lint errors in the console. 16 | 17 | ### `npm test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `npm run build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `npm run eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can't go back!** 35 | 36 | If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own. 39 | 40 | You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | 48 | ### Code Splitting 49 | 50 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) 51 | 52 | ### Analyzing the Bundle Size 53 | 54 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) 55 | 56 | ### Making a Progressive Web App 57 | 58 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) 59 | 60 | ### Advanced Configuration 61 | 62 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) 63 | 64 | ### Deployment 65 | 66 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) 67 | 68 | ### `npm run build` fails to minify 69 | 70 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) 71 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@react-oauth/google": "^0.11.1", 7 | "@testing-library/jest-dom": "^5.17.0", 8 | "@testing-library/react": "^13.4.0", 9 | "@testing-library/user-event": "^13.5.0", 10 | "axios": "^1.6.2", 11 | "firebase": "^10.6.0", 12 | "markdown-to-jsx": "^7.3.2", 13 | "react": "^18.2.0", 14 | "react-dom": "^18.2.0", 15 | "react-icons": "^4.11.0", 16 | "react-router-dom": "^6.16.0", 17 | "react-scripts": "5.0.1", 18 | "sonner": "^1.2.0", 19 | "web-vitals": "^2.1.4", 20 | "zustand": "^4.4.6" 21 | }, 22 | "scripts": { 23 | "start": "react-scripts start", 24 | "build": "react-scripts build", 25 | "test": "react-scripts test", 26 | "eject": "react-scripts eject" 27 | }, 28 | "eslintConfig": { 29 | "extends": [ 30 | "react-app", 31 | "react-app/jest" 32 | ] 33 | }, 34 | "browserslist": { 35 | "production": [ 36 | ">0.2%", 37 | "not dead", 38 | "not op_mini all" 39 | ], 40 | "development": [ 41 | "last 1 chrome version", 42 | "last 1 firefox version", 43 | "last 1 safari version" 44 | ] 45 | }, 46 | "devDependencies": { 47 | "tailwindcss": "^3.3.5" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeWaveWithAsante/fullstack_blog_app/657972bddcee6bb7d4eedbddabf9cd1988a3ccee/client/public/favicon.ico -------------------------------------------------------------------------------- /client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /client/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeWaveWithAsante/fullstack_blog_app/657972bddcee6bb7d4eedbddabf9cd1988a3ccee/client/public/logo192.png -------------------------------------------------------------------------------- /client/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeWaveWithAsante/fullstack_blog_app/657972bddcee6bb7d4eedbddabf9cd1988a3ccee/client/public/logo512.png -------------------------------------------------------------------------------- /client/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /client/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /client/src/App.js: -------------------------------------------------------------------------------- 1 | import { Routes, Route, Outlet } from "react-router-dom"; 2 | import { 3 | BlogDetails, 4 | CategoriesPage, 5 | Home, 6 | LoginPage, 7 | SignupPage, 8 | WriterPage, 9 | } from "./pages"; 10 | import Loading from "./components/Loading"; 11 | import { Footer, Navbar } from "./components"; 12 | import useStore from "./store"; 13 | 14 | function Layout() { 15 | return ( 16 |
17 | 18 |
19 | 20 |
21 |
22 |
23 | ); 24 | } 25 | 26 | function App() { 27 | const { theme, isLoading } = useStore(); 28 | 29 | return ( 30 |
31 |
32 | 33 | }> 34 | } /> 35 | } /> 36 | } /> 37 | } /> 38 | 39 | 40 | } /> 41 | } /> 42 | 43 | 44 | {isLoading && } 45 |
46 |
47 | ); 48 | } 49 | 50 | export default App; 51 | -------------------------------------------------------------------------------- /client/src/assets/profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeWaveWithAsante/fullstack_blog_app/657972bddcee6bb7d4eedbddabf9cd1988a3ccee/client/src/assets/profile.png -------------------------------------------------------------------------------- /client/src/components/Banner.jsx: -------------------------------------------------------------------------------- 1 | import Markdown from "markdown-to-jsx"; 2 | import { Link } from "react-router-dom"; 3 | 4 | const Banner = ({ post }) => { 5 | return ( 6 |
7 |
8 | 9 | Banner 14 | 15 | 16 |
17 | 18 |

19 | {post?.title.slice(0, 60) + "..."} 20 |

21 | 22 | 23 |
24 | 25 | {post?.desc?.slice(0, 160) + "..."} 26 | 27 |
28 | 32 | Read more... 33 | 34 | 38 | User profile 43 | 44 | {post?.user?.name} 45 | 46 | 47 | {new Date(post?.createdAt).toDateString()} 48 | 49 | 50 |
51 |
52 |
53 | ); 54 | }; 55 | 56 | export default Banner; 57 | -------------------------------------------------------------------------------- /client/src/components/Button.jsx: -------------------------------------------------------------------------------- 1 | const Button = ({ label, styles, icon, type, onClick }) => { 2 | return ( 3 | 12 | ); 13 | }; 14 | 15 | export default Button; 16 | -------------------------------------------------------------------------------- /client/src/components/Card.jsx: -------------------------------------------------------------------------------- 1 | import Markdown from "markdown-to-jsx"; 2 | import React from "react"; 3 | import { AiOutlineArrowRight } from "react-icons/ai"; 4 | import { Link } from "react-router-dom"; 5 | 6 | const Card = ({ post, index }) => { 7 | return ( 8 |
15 | 19 | {post?.title} 24 | 25 | 26 |
27 |
28 | 29 | {new Date(post?.createdAt).toDateString()} 30 | 31 | 32 | {post?.cat} 33 | 34 |
35 | 36 |
37 | {post?.title} 38 |
39 | 40 |
41 | 42 | {post?.desc?.slice(0, 250) + "..."} 43 | 44 |
45 | 46 | 50 | Read More 51 | 52 |
53 |
54 | ); 55 | }; 56 | 57 | export default Card; 58 | -------------------------------------------------------------------------------- /client/src/components/Divider.jsx: -------------------------------------------------------------------------------- 1 | const Divider = ({ label }) => { 2 | return ( 3 |
4 |
5 |
{label}
6 |
7 |
8 | ); 9 | }; 10 | 11 | export default Divider; 12 | -------------------------------------------------------------------------------- /client/src/components/Footer.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-router-dom"; 3 | 4 | const Footer = () => { 5 | return ( 6 |
7 |

© 2023 CodeWaveWithAsante. All rights reserved.

8 | 9 | Contact 10 | Terms of Service 11 | 12 | Privacy Policy 13 | 14 | 15 |
16 | ); 17 | }; 18 | 19 | export default Footer; 20 | -------------------------------------------------------------------------------- /client/src/components/Inputbox.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Inputbox = ({ 4 | label, 5 | name, 6 | type, 7 | isRequired = false, 8 | placeholder, 9 | value, 10 | onChange, 11 | }) => { 12 | return ( 13 |
14 | 17 | 26 |
27 | ); 28 | }; 29 | 30 | export default Inputbox; 31 | -------------------------------------------------------------------------------- /client/src/components/Loading.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Loading = () => { 4 | return ( 5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | ); 14 | }; 15 | 16 | export default Loading; 17 | -------------------------------------------------------------------------------- /client/src/components/Logo.jsx: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router-dom"; 2 | 3 | const Logo = ({ type }) => { 4 | return ( 5 |
6 | 12 | Blog 13 | 16 | Wave 17 | 18 | 19 |
20 | ); 21 | }; 22 | 23 | export default Logo; 24 | -------------------------------------------------------------------------------- /client/src/components/Navbar.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { AiOutlineClose } from "react-icons/ai"; 3 | import { 4 | FaFacebook, 5 | FaInstagram, 6 | FaTwitterSquare, 7 | FaYoutube, 8 | } from "react-icons/fa"; 9 | import { Link } from "react-router-dom"; 10 | import useStore from "../store"; 11 | import Button from "./Button"; 12 | import Logo from "./Logo"; 13 | import ThemeSwitch from "./Switch"; 14 | 15 | function getInitials(fullName) { 16 | const names = fullName.split(" "); 17 | 18 | const initials = names.slice(0, 2).map((name) => name[0].toUpperCase()); 19 | 20 | const initialsStr = initials.join(""); 21 | 22 | return initialsStr; 23 | } 24 | 25 | const MobileMenu = ({ user, signOut }) => { 26 | const [isMenuOpen, setIsMenuOpen] = useState(false); 27 | 28 | const toggleMenu = () => { 29 | setIsMenuOpen(!isMenuOpen); 30 | }; 31 | 32 | return ( 33 |
34 | 53 | {isMenuOpen && ( 54 |
55 | 56 |
    57 |
  • 58 | Home 59 |
  • 60 |
  • 61 | Contact 62 |
  • 63 |
  • 64 | About 65 |
  • 66 |
67 |
68 | {user?.token ? ( 69 |
70 |
71 | {user?.user.image ? ( 72 | Profile 77 | ) : ( 78 | 79 | {getInitials(user?.user.name)} 80 | 81 | )} 82 | 83 | {user?.user.name} 84 | 85 |
86 | 87 | 93 |
94 | ) : ( 95 | 96 |
103 | 104 | {/* theme switch */} 105 | 106 | 107 | 111 | 112 | 113 |
114 | )} 115 |
116 | ); 117 | }; 118 | 119 | const Navbar = () => { 120 | const { user, signOut } = useStore(); 121 | const [showProfile, setShowProfile] = useState(false); 122 | 123 | const handleSignOut = () => { 124 | localStorage.removeItem("user"); 125 | signOut(); 126 | }; 127 | 128 | console.log(user); 129 | return ( 130 | 206 | ); 207 | }; 208 | 209 | export default Navbar; 210 | -------------------------------------------------------------------------------- /client/src/components/Pagination.jsx: -------------------------------------------------------------------------------- 1 | const Pagination = ({ totalPages, onPageChange }) => { 2 | const searchParams = new URLSearchParams(window.location.search); 3 | const currentPage = parseInt(searchParams.get("page")) || 1; 4 | 5 | const range = (start, end) => 6 | Array.from({ length: end - start + 1 }, (_, i) => start + i); 7 | 8 | const showEllipses = totalPages > 8; 9 | 10 | return ( 11 |
12 | 19 | 20 | {showEllipses && currentPage > 4 && ( 21 | <> 22 | 25 | ... 26 | 27 | )} 28 | 29 | {range( 30 | Math.max(1, currentPage - 3), 31 | Math.min(totalPages, currentPage + 4) 32 | ).map((page) => ( 33 | 40 | ))} 41 | 42 | {showEllipses && currentPage < totalPages - 3 && ( 43 | <> 44 | ... 45 | 51 | 52 | )} 53 | 54 | 61 |
62 | ); 63 | }; 64 | 65 | export default Pagination; 66 | -------------------------------------------------------------------------------- /client/src/components/PopularPosts.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-router-dom"; 3 | import { CATEGORIES } from "../utils/dummyData"; 4 | 5 | const PopularPosts = ({ posts }) => { 6 | const Card = ({ post }) => { 7 | let catColor = ""; 8 | CATEGORIES.map((cat) => { 9 | if (cat.label === post?.cat) { 10 | catColor = cat?.color; 11 | } 12 | return null; 13 | }); 14 | 15 | return ( 16 |
17 | {post?.user?.name} 22 |
23 | 26 | {post?.cat} 27 | 28 | 32 | {post?.title} 33 | 34 |
35 | {post?.user?.name} 36 | 37 | {new Date(post?.createdAt).toDateString()} 38 | 39 |
40 |
41 |
42 | ); 43 | }; 44 | 45 | return ( 46 |
47 |

48 | Popular Articles 49 |

50 | {posts?.map((post, id) => ( 51 | 52 | ))} 53 |
54 | ); 55 | }; 56 | 57 | export default PopularPosts; 58 | -------------------------------------------------------------------------------- /client/src/components/PopularWriters.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-router-dom"; 3 | import Profile from "../assets/profile.png"; 4 | import { formatNumber } from "../utils"; 5 | 6 | const PopularWriters = ({ data }) => { 7 | return ( 8 |
9 |

10 | Popular Writers 11 |

12 | {data?.map((el, id) => ( 13 | 18 | {el?.name} 23 |
24 | 25 | {el?.name} 26 | 27 | 28 | {formatNumber(el?.followers)}{" "} 29 | Folloers 30 | 31 |
32 | 33 | ))} 34 |
35 | ); 36 | }; 37 | 38 | export default PopularWriters; 39 | -------------------------------------------------------------------------------- /client/src/components/PostComments.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import useStore from "../store"; 3 | import { COMMENTS } from "../utils/dummyData"; 4 | import Button from "./Button"; 5 | import { Link } from "react-router-dom"; 6 | import Profile from "../assets/profile.png"; 7 | import { Toaster } from "sonner"; 8 | 9 | const PostComments = ({ postId }) => { 10 | const { user } = useStore(); 11 | const [comments, setComments] = useState(COMMENTS); 12 | const [desc, setDesc] = useState(""); 13 | 14 | const handleDeleteComment = async (id) => {}; 15 | return ( 16 |
17 |

18 | Post Comments 19 |

20 | 21 | {user?.token ? ( 22 |
23 | 31 | 32 |
33 |
40 |
41 | ) : ( 42 | 43 |
93 | ); 94 | }; 95 | 96 | export default PostComments; 97 | -------------------------------------------------------------------------------- /client/src/components/Switch.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import useStore from "../store"; 3 | 4 | const ThemeSwitch = () => { 5 | const { theme, setTheme } = useStore(); 6 | const [isDarkMode, setIsDarkMode] = useState(theme === "dark"); 7 | 8 | // console.log(isDarkMode); 9 | 10 | const toggleTheme = () => { 11 | const newTheme = isDarkMode ? "light" : "dark"; 12 | setIsDarkMode(!isDarkMode); 13 | setTheme(newTheme); 14 | localStorage.setItem("theme", newTheme); 15 | }; 16 | 17 | return ( 18 |
22 |
23 |
24 | ); 25 | }; 26 | 27 | export default ThemeSwitch; 28 | -------------------------------------------------------------------------------- /client/src/components/index.js: -------------------------------------------------------------------------------- 1 | import Banner from "./Banner"; 2 | import Button from "./Button"; 3 | import Card from "./Card"; 4 | import Divider from "./Divider"; 5 | import Footer from "./Footer"; 6 | import Inputbox from "./Inputbox"; 7 | import Loading from "./Loading"; 8 | import Logo from "./Logo"; 9 | import Navbar from "./Navbar"; 10 | import Pagination from "./Pagination"; 11 | import PostComments from "./PostComments"; 12 | import PopularPosts from "./PopularPosts"; 13 | import PopularWriters from "./PopularWriters"; 14 | 15 | export { 16 | Banner, 17 | Button, 18 | Card, 19 | Divider, 20 | Footer, 21 | Inputbox, 22 | Loading, 23 | Logo, 24 | Navbar, 25 | Pagination, 26 | PopularPosts, 27 | PopularWriters, 28 | PostComments, 29 | }; 30 | -------------------------------------------------------------------------------- /client/src/index.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=Poppins:wght@100;200;300;400;500;600;700;800;900&display=swap"); 2 | 3 | @tailwind base; 4 | @tailwind components; 5 | @tailwind utilities; 6 | 7 | body { 8 | margin: 0; 9 | font-family: "Poppins", sans-serif; 10 | } 11 | 12 | /* Custom Scrollbar Styles */ 13 | /* Track */ 14 | ::-webkit-scrollbar { 15 | width: 8px; 16 | } 17 | 18 | /* Handle */ 19 | ::-webkit-scrollbar-thumb { 20 | background-color: #c0c0c0; 21 | border-radius: 6px; 22 | } 23 | 24 | /* Handle on hover */ 25 | ::-webkit-scrollbar-thumb:hover { 26 | background-color: #a0a0a0; 27 | } 28 | 29 | /* Track */ 30 | ::-webkit-scrollbar-track { 31 | background: #f5f5f5; 32 | } 33 | 34 | /* Pagination.css */ 35 | .pagination { 36 | display: flex; 37 | justify-content: center; 38 | margin-top: 1rem; 39 | } 40 | 41 | .pagination-btn { 42 | padding: 0.5rem 1rem; 43 | margin: 0 0.25rem; 44 | border: 1px solid #ddd; 45 | cursor: pointer; 46 | background-color: #fff; 47 | transition: background-color 0.3s; 48 | 49 | &:hover { 50 | background-color: #f0f0f0; 51 | color: #9f1239; 52 | } 53 | 54 | &:disabled { 55 | cursor: not-allowed; 56 | opacity: 0.6; 57 | } 58 | } 59 | 60 | .active { 61 | background-color: #9f1239; 62 | color: #fff; 63 | } 64 | 65 | .pagination-ellipsis { 66 | padding: 0.5rem 1rem; 67 | margin: 0 0.25rem; 68 | cursor: not-allowed; 69 | color: #ccc; 70 | } 71 | 72 | /* Loading CSS */ 73 | .loading-container { 74 | width: 100%; 75 | height: 100vh; 76 | background-color: #00000087; 77 | display: flex; 78 | flex-direction: column; 79 | align-items: center; 80 | justify-content: center; 81 | position: absolute; 82 | z-index: 999; 83 | top: 0; 84 | left: 0; 85 | right: 0; 86 | bottom: 0; 87 | } 88 | 89 | .loading-inner { 90 | display: flex; 91 | flex-direction: column; 92 | align-items: center; 93 | justify-content: center; 94 | background-color: #ffffff70; 95 | } 96 | 97 | .loading-wave { 98 | width: 300px; 99 | height: 100px; 100 | display: flex; 101 | justify-content: center; 102 | align-items: flex-end; 103 | } 104 | 105 | .loading-bar { 106 | width: 20px; 107 | height: 10px; 108 | margin: 0 5px; 109 | background-color: rgb(225 29 72); 110 | /* #3498db; */ 111 | border-radius: 5px; 112 | animation: loading-wave-animation 1s ease-in-out infinite; 113 | } 114 | 115 | .loading-bar:nth-child(2) { 116 | animation-delay: 0.1s; 117 | } 118 | 119 | .loading-bar:nth-child(3) { 120 | animation-delay: 0.2s; 121 | } 122 | 123 | .loading-bar:nth-child(4) { 124 | animation-delay: 0.3s; 125 | } 126 | 127 | @keyframes loading-wave-animation { 128 | 0% { 129 | height: 10px; 130 | } 131 | 132 | 50% { 133 | height: 50px; 134 | } 135 | 136 | 100% { 137 | height: 10px; 138 | } 139 | } 140 | 141 | /* ThemeSwitch.css */ 142 | .switch { 143 | position: relative; 144 | width: 55px; 145 | height: 25px; 146 | background-color: #ddd; 147 | border-radius: 15px; 148 | cursor: pointer; 149 | transition: background-color 0.3s; 150 | } 151 | 152 | .switch .ball { 153 | position: absolute; 154 | top: 2px; 155 | left: 2px; 156 | width: 21px; 157 | height: 21px; 158 | background-color: #fff; 159 | border-radius: 50%; 160 | transition: transform 0.5s; 161 | } 162 | 163 | .switch.dark { 164 | background-color: #333; 165 | } 166 | 167 | .ball.dark { 168 | background-color: #333; 169 | } 170 | 171 | .switch.dark .ball { 172 | transform: translateX(30px); 173 | } 174 | 175 | .switch.light .ball { 176 | transform: translateX(2px); 177 | } 178 | -------------------------------------------------------------------------------- /client/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import App from "./App"; 4 | import "./index.css"; 5 | import { BrowserRouter } from "react-router-dom"; 6 | import { GoogleOAuthProvider } from "@react-oauth/google"; 7 | 8 | const clientId = process.env.REACT_APP_GOOGLE_CLIENT_ID; 9 | 10 | const root = ReactDOM.createRoot(document.getElementById("root")); 11 | 12 | root.render( 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | ); 21 | -------------------------------------------------------------------------------- /client/src/pages/BlogDetails.jsx: -------------------------------------------------------------------------------- 1 | import Markdown from "markdown-to-jsx"; 2 | import { useEffect, useState } from "react"; 3 | import { Link, useParams } from "react-router-dom"; 4 | import { PopularPosts, PopularWriters, PostComments } from "../components"; 5 | import useStore from "../store"; 6 | import { popular, posts } from "../utils/dummyData"; 7 | 8 | const BlogDetails = () => { 9 | const { setIsLoading } = useStore(); 10 | 11 | const { id } = useParams(); 12 | const [post, setPost] = useState(posts[1]); 13 | 14 | useEffect(() => { 15 | if (id) { 16 | // fetch post 17 | window.scrollTo({ top: 0, left: 0, behavior: "smooth" }); 18 | } 19 | }, [id]); 20 | 21 | if (!post) 22 | return ( 23 |
24 | Loading... 25 |
26 | ); 27 | 28 | return ( 29 |
30 |
31 |
32 |

33 | {post?.title} 34 |

35 | 36 |
37 | 38 | {post?.cat} 39 | 40 | 41 | 42 | {post?.views?.length} 43 | Views 44 | 45 |
46 | 47 | 48 | {post?.user?.name} 53 |
54 |

55 | {post?.user?.name} 56 |

57 | 58 | {new Date(post?.createdAt).toDateString()} 59 | 60 |
61 | 62 |
63 | {post?.title} 68 |
69 | 70 |
71 | {/* LEFT */} 72 |
73 | {post?.desc && ( 74 | 78 | {post?.desc} 79 | 80 | )} 81 | 82 | {/* COMMENTS SECTION */} 83 |
{}
84 |
85 | 86 | {/* RIGHT */} 87 |
88 | {/* POPULAR POSTS */} 89 | 90 | 91 | {/* POPULAR WRITERS */} 92 | 93 |
94 |
95 |
96 | ); 97 | }; 98 | 99 | export default BlogDetails; 100 | -------------------------------------------------------------------------------- /client/src/pages/CategoriesPage.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { Card, Pagination, PopularPosts, PopularWriters } from "../components"; 3 | import { popular, posts } from "../utils/dummyData"; 4 | 5 | const CategoriesPage = () => { 6 | const query = new URLSearchParams(window.location.search).get("cat"); 7 | const numOfPages = 4; 8 | const [page, setPage] = useState(0); 9 | 10 | const handlePageChange = (val) => { 11 | setPage(val); 12 | 13 | console.log(val); 14 | }; 15 | 16 | return ( 17 |
18 |
19 |

20 | {query} 21 |

22 |
23 | 24 |
25 | {/* LEFT */} 26 |
27 | {posts?.length === 0 ? ( 28 |
29 | 30 | No Post Available for this category 31 | 32 |
33 | ) : ( 34 | <> 35 | {posts?.map((post) => ( 36 | 37 | ))} 38 | 39 |
40 | 44 |
45 | 46 | )} 47 |
48 | 49 | {/* RIGHT */} 50 |
51 | {/* POPULAR POSTS */} 52 | 53 | 54 | {/* POPULAR WRITERS */} 55 | 56 |
57 |
58 |
59 | ); 60 | }; 61 | 62 | export default CategoriesPage; 63 | -------------------------------------------------------------------------------- /client/src/pages/Home.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { Link } from "react-router-dom"; 3 | import { 4 | Banner, 5 | Card, 6 | Pagination, 7 | PopularPosts, 8 | PopularWriters, 9 | } from "../components"; 10 | 11 | import { CATEGORIES, popular, posts } from "../utils/dummyData"; 12 | 13 | const Home = () => { 14 | const numOfPages = 4; 15 | const [page, setPage] = useState(0); 16 | 17 | const randomIndex = Math.floor(Math.random() * posts.length); 18 | 19 | const handlePageChange = (val) => { 20 | setPage(val); 21 | 22 | console.log(val); 23 | }; 24 | 25 | if (posts?.length < 1) 26 | return ( 27 |
28 | No Post Available 29 |
30 | ); 31 | 32 | return ( 33 |
34 | 35 | 36 |
37 | {/* Categories */} 38 |
39 |

40 | Popular Categories 41 |

42 |
43 | {CATEGORIES.map((cat) => ( 44 | 49 | {cat?.icon} 50 | {cat.label} 51 | 52 | ))} 53 |
54 |
55 | 56 | {/* Blog Post */} 57 |
58 | {/* LEFT */} 59 |
60 | {posts?.map((post, index) => ( 61 | 62 | ))} 63 | 64 |
65 | 69 |
70 |
71 | 72 | {/* RIGHT */} 73 |
74 | {/* POPULAR POSTS */} 75 | 76 | 77 | {/* POPULAR WRITERS */} 78 | 79 |
80 |
81 |
82 |
83 | ); 84 | }; 85 | 86 | export default Home; 87 | -------------------------------------------------------------------------------- /client/src/pages/LoginPage.jsx: -------------------------------------------------------------------------------- 1 | import { useGoogleLogin } from "@react-oauth/google"; 2 | import React, { useState } from "react"; 3 | import { FcGoogle } from "react-icons/fc"; 4 | import { Toaster, toast } from "sonner"; 5 | import { Link } from "react-router-dom"; 6 | import { Button, Divider, Inputbox, Logo } from "../components"; 7 | 8 | const LoginPage = () => { 9 | const user = {}; 10 | 11 | const [data, setData] = useState({ 12 | email: "", 13 | password: "", 14 | }); 15 | 16 | const handleChange = (e) => { 17 | // const [name, value] = e.target; change to one below 18 | const { name, value } = e.target; 19 | setData({ 20 | ...data, 21 | [name]: value, 22 | }); 23 | }; 24 | 25 | const googleLogin = async () => {}; 26 | 27 | const handleSubmit = async () => {}; 28 | 29 | if (user.token) window.location.replace("/"); 30 | return ( 31 |
32 |
33 | 34 | Welcome, back! 35 |
36 | 37 |
38 |
39 |
40 | 41 |
42 |
43 |
44 |

45 | Sign in to your account 46 |

47 |
48 | 49 |
97 |
98 |
99 | 100 | 101 |
102 | ); 103 | }; 104 | 105 | export default LoginPage; 106 | -------------------------------------------------------------------------------- /client/src/pages/SignupPage.1.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { Logo } from "../components"; 3 | 4 | export const SignupPage = () => { 5 | const user = {}; 6 | const [showForm, setShowForm] = useState(false); 7 | const [data, setData] = useState({ 8 | firstName: "", 9 | lastName: "", 10 | email: "", 11 | password: "", 12 | }); 13 | const [file, setFile] = useState(""); 14 | const [fileURL, setFileURL] = useState(""); 15 | 16 | if (user.token) window.location.replace("/"); 17 | 18 | return ( 19 |
20 |
21 | {fileURL && ( 22 | 27 | )} 28 | 29 | Welcome! 30 |
31 | 32 |
33 |
34 |
35 | 36 |
37 | 38 |
39 |
40 | {showFo} 41 | rm} 42 |
43 |
44 |
45 |
46 |
47 | ); 48 | }; 49 | -------------------------------------------------------------------------------- /client/src/pages/SignupPage.jsx: -------------------------------------------------------------------------------- 1 | import { useGoogleLogin } from "@react-oauth/google"; 2 | import React, { useEffect, useState } from "react"; 3 | import { BiImages } from "react-icons/bi"; 4 | import { FcGoogle } from "react-icons/fc"; 5 | import { IoArrowBackCircleSharp } from "react-icons/io5"; 6 | import { Link } from "react-router-dom"; 7 | import { Toaster, toast } from "sonner"; 8 | import { Button, Divider, Inputbox, Logo } from "../components"; 9 | 10 | const SignupPage = () => { 11 | const user = {}; 12 | const [showForm, setShowForm] = useState(false); 13 | const [data, setData] = useState({ 14 | firstName: "", 15 | lastName: "", 16 | email: "", 17 | password: "", 18 | }); 19 | const [file, setFile] = useState(""); 20 | const [fileURL, setFileURL] = useState(""); 21 | 22 | const handleChange = (e) => { 23 | // const [name, value] = e.target; 24 | const { name, value } = e.target; 25 | setData({ 26 | ...data, 27 | [name]: value, 28 | }); 29 | }; 30 | 31 | const googleLogin = async () => {}; 32 | 33 | const handleSubmit = async () => {}; 34 | 35 | if (user.token) window.location.replace("/"); 36 | 37 | return ( 38 |
39 | {/* LEFT */} 40 |
41 | {fileURL && ( 42 | 47 | )} 48 | 49 | Welcome! 50 |
51 | 52 | {/* RIGHT */} 53 |
54 |
55 |
56 | 57 |
58 | 59 |
60 |
61 | {showForm && ( 62 | setShowForm(false)} 65 | /> 66 | )} 67 |

68 | Sign up for an account 69 |

70 |
71 | {showForm ? ( 72 |
76 |
77 |
78 | 87 | 96 |
97 | 98 | 107 | 116 | 117 |
118 | 133 |
134 |
135 | 136 |
159 | 160 | )} 161 | 162 |

163 | Already has an account?{" "} 164 | 165 | Sign in 166 | 167 |

168 |
169 |
170 |
171 | 172 | 173 |
174 | ); 175 | }; 176 | 177 | export default SignupPage; 178 | -------------------------------------------------------------------------------- /client/src/pages/WriterPage.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { FaUserCheck } from "react-icons/fa"; 3 | import { useParams } from "react-router-dom"; 4 | import NoProfile from "../assets/profile.png"; 5 | import { 6 | Button, 7 | Card, 8 | Pagination, 9 | PopularPosts, 10 | PopularWriters, 11 | } from "../components"; 12 | import useStore from "../store"; 13 | import { formatNumber } from "../utils"; 14 | import { popular, posts, writer } from "../utils/dummyData"; 15 | 16 | const WriterPage = () => { 17 | const { user } = useStore(); 18 | 19 | const { id } = useParams(); 20 | const numOfPages = 4; 21 | const [page, setPage] = useState(0); 22 | 23 | const handlePageChange = (val) => { 24 | setPage(val); 25 | 26 | console.log(val); 27 | }; 28 | // const [writer, setWriter] = useState(null); 29 | 30 | const followerIds = writer.followers.map((f) => fetch.followerId); 31 | 32 | if (!writer) 33 | return ( 34 |
35 | No Writer Found 36 |
37 | ); 38 | 39 | return ( 40 |
41 |
42 | Writer 47 |
48 |

49 | {writer?.name} 50 |

51 | 52 |
53 |
54 |

55 | {formatNumber(writer?.followers?.length ?? 0)} 56 |

57 | Followers 58 |
59 | 60 |
61 |

62 | {formatNumber(posts?.length ?? 0)} 63 |

64 | Posts 65 |
66 |
67 | 68 | {user?.token && ( 69 |
70 | {!followerIds?.includes(user?.user?._id) ? ( 71 |
83 | )} 84 |
85 |
86 | 87 |
88 | {/* LEFT */} 89 |
90 | {posts?.length === 0 ? ( 91 |
92 | No Post Available 93 |
94 | ) : ( 95 | <> 96 | {posts?.map((post, index) => ( 97 | 98 | ))} 99 | 100 |
101 | 105 |
106 | 107 | )} 108 |
109 | 110 | {/* RIGHT */} 111 |
112 | {/* POPULAR POSTS */} 113 | 114 | 115 | {/* POPULAR WRITERS */} 116 | 117 |
118 |
119 |
120 | ); 121 | }; 122 | 123 | export default WriterPage; 124 | -------------------------------------------------------------------------------- /client/src/pages/index.js: -------------------------------------------------------------------------------- 1 | import Home from "./Home"; 2 | import BlogDetails from "./BlogDetails"; 3 | import CategoriesPage from "./CategoriesPage"; 4 | import LoginPage from "./LoginPage"; 5 | import SignupPage from "./SignupPage"; 6 | import WriterPage from "./WriterPage"; 7 | 8 | export { Home, LoginPage, SignupPage, WriterPage, CategoriesPage, BlogDetails }; 9 | -------------------------------------------------------------------------------- /client/src/store/index.js: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | 3 | const useStore = create((set) => ({ 4 | user: JSON.parse(localStorage.getItem("user")) || {}, 5 | isLoading: false, 6 | 7 | theme: localStorage.getItem("theme") ?? "light", 8 | 9 | signIn: (data) => set((state) => ({ user: data })), 10 | 11 | setTheme: (value) => set({ theme: value }), 12 | 13 | signOut: () => set({ user: {} }), 14 | 15 | setIsLoading: (val) => set((state) => ({ isLoading: val })), 16 | })); 17 | 18 | export default useStore; 19 | -------------------------------------------------------------------------------- /client/src/utils/index.js: -------------------------------------------------------------------------------- 1 | export function formatNumber(num) { 2 | if (num >= 1000000) { 3 | return (num / 1000000).toFixed(1) + "M"; 4 | } else if (num >= 1000) { 5 | return (num / 1000).toFixed(1) + "K"; 6 | } 7 | return num.toString(); 8 | } 9 | -------------------------------------------------------------------------------- /client/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ["./src/**/*.{html,js,jsx}"], 4 | darkMode: "class", 5 | theme: { 6 | extend: {}, 7 | }, 8 | plugins: [], 9 | }; 10 | -------------------------------------------------------------------------------- /server/controllers/authController.js: -------------------------------------------------------------------------------- 1 | import Users from "../models/userModel.js"; 2 | import { compareString, createJWT, hashString } from "../utils/index.js"; 3 | import { sendVerificationEmail } from "../utils/sendEmail.js"; 4 | 5 | export const register = async (req, res, next) => { 6 | try { 7 | const { 8 | firstName, 9 | lastName, 10 | email, 11 | password, 12 | image, 13 | accountType, 14 | provider, 15 | } = req.body; 16 | 17 | //validate fileds 18 | if (!(firstName || lastName || email || password)) { 19 | return next("Provide Required Fields!"); 20 | } 21 | 22 | if (accountType === "Writer" && !image) 23 | return next("Please provide profile picture"); 24 | 25 | const userExist = await Users.findOne({ email }); 26 | 27 | if (userExist) { 28 | return next("Email Address already exists. Try Login"); 29 | } 30 | 31 | const hashedPassword = await hashString(password); 32 | 33 | const user = await Users.create({ 34 | name: firstName + " " + lastName, 35 | email, 36 | password: !provider ? hashedPassword : "", 37 | image, 38 | accountType, 39 | provider, 40 | }); 41 | 42 | user.password = undefined; 43 | 44 | const token = createJWT(user?._id); 45 | 46 | //send email verification if account type is writer 47 | if (accountType === "Writer") { 48 | sendVerificationEmail(user, res, token); 49 | } else { 50 | res.status(201).json({ 51 | success: true, 52 | message: "Account created successfully", 53 | user, 54 | token, 55 | }); 56 | } 57 | } catch (error) { 58 | console.log(error); 59 | res.status(404).json({ message: error.message }); 60 | } 61 | }; 62 | 63 | export const googleSignUp = async (req, res, next) => { 64 | try { 65 | const { name, email, image, emailVerified } = req.body; 66 | 67 | const userExist = await Users.findOne({ email }); 68 | 69 | if (userExist) { 70 | next("Email Address already exists. Try Login"); 71 | return; 72 | } 73 | 74 | const user = await Users.create({ 75 | name, 76 | email, 77 | image, 78 | provider: "Google", 79 | emailVerified, 80 | }); 81 | 82 | user.password = undefined; 83 | 84 | const token = createJWT(user?._id); 85 | 86 | res.status(201).json({ 87 | success: true, 88 | message: "Account created successfully", 89 | user, 90 | token, 91 | }); 92 | } catch (error) { 93 | console.log(error); 94 | res.status(404).json({ message: error.message }); 95 | } 96 | }; 97 | 98 | export const login = async (req, res, next) => { 99 | try { 100 | const { email, password } = req.body; 101 | 102 | //validation 103 | if (!email) { 104 | return next("Please Provide User Credentials"); 105 | } 106 | 107 | // find user by email 108 | const user = await Users.findOne({ email }).select("+password"); 109 | 110 | if (!user) { 111 | return next("Invalid email or password"); 112 | } 113 | 114 | // Google account signed in 115 | if (!password && user?.provider === "Google") { 116 | const token = createJWT(user?._id); 117 | 118 | return res.status(201).json({ 119 | success: true, 120 | message: "Login successfully", 121 | user, 122 | token, 123 | }); 124 | } 125 | 126 | // compare password 127 | const isMatch = await compareString(password, user?.password); 128 | 129 | if (!isMatch) { 130 | return next("Invalid email or password"); 131 | } 132 | 133 | if (user?.accountType === "Writer" && !user?.emailVerified) { 134 | return next("Please verify your email address."); 135 | } 136 | 137 | user.password = undefined; 138 | 139 | const token = createJWT(user?._id); 140 | 141 | res.status(201).json({ 142 | success: true, 143 | message: "Login successfully", 144 | user, 145 | token, 146 | }); 147 | } catch (error) { 148 | console.log(error); 149 | res.status(404).json({ success: "failed", message: error.message }); 150 | } 151 | }; 152 | -------------------------------------------------------------------------------- /server/controllers/postController.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | import Posts from "../models/postModel.js"; 3 | import Users from "../models/userModel.js"; 4 | import Views from "../models/viewsModel.js"; 5 | import Followers from "../models/followersModel.js"; 6 | import Comments from "../models/commentModel.js"; 7 | 8 | export const stats = async (req, res, next) => { 9 | try { 10 | const { query } = req.query; 11 | const { userId } = req.body.user; 12 | 13 | const numofDays = Number(query) || 28; 14 | 15 | const currentDate = new Date(); 16 | const startDate = new Date(); 17 | startDate.setDate(currentDate.getDate() - numofDays); 18 | 19 | const totalPosts = await Posts.find({ 20 | user: userId, 21 | createdAt: { $gte: startDate, $lte: currentDate }, 22 | }).countDocuments(); 23 | 24 | const totalViews = await Views.find({ 25 | user: userId, 26 | createdAt: { $gte: startDate, $lte: currentDate }, 27 | }).countDocuments(); 28 | 29 | const totalWriters = await Users.find({ 30 | accountType: "Writer", 31 | }).countDocuments(); 32 | 33 | const totalFollowers = await Users.findById(userId); 34 | 35 | const viewStats = await Views.aggregate([ 36 | { 37 | $match: { 38 | user: new mongoose.Types.ObjectId(userId), 39 | createdAt: { $gte: startDate, $lte: currentDate }, 40 | }, 41 | }, 42 | { 43 | $group: { 44 | _id: { 45 | $dateToString: { format: "%Y-%m-%d", date: "$createdAt" }, 46 | }, 47 | Total: { $sum: 1 }, 48 | }, 49 | }, 50 | { $sort: { _id: 1 } }, 51 | ]); 52 | 53 | const followersStats = await Followers.aggregate([ 54 | { 55 | $match: { 56 | writerId: new mongoose.Types.ObjectId(userId), 57 | createdAt: { $gte: startDate, $lte: currentDate }, 58 | }, 59 | }, 60 | { 61 | $group: { 62 | _id: { 63 | $dateToString: { format: "%Y-%m-%d", date: "$createdAt" }, 64 | }, 65 | Total: { $sum: 1 }, 66 | }, 67 | }, 68 | { $sort: { _id: 1 } }, 69 | ]); 70 | 71 | const last5Followers = await Users.findById(userId).populate({ 72 | path: "followers", 73 | options: { sort: { _id: -1 } }, 74 | perDocumentLimit: 5, 75 | populate: { 76 | path: "followerId", 77 | select: "name email image accountType followers -password", 78 | }, 79 | }); 80 | 81 | const last5Posts = await Posts.find({ user: userId }) 82 | .limit(5) 83 | .sort({ _id: -1 }); 84 | 85 | res.status(200).json({ 86 | success: true, 87 | message: "Data loaded successfully", 88 | totalPosts, 89 | totalViews, 90 | totalWriters, 91 | followers: totalFollowers?.followers?.length, 92 | viewStats, 93 | followersStats, 94 | last5Followers: last5Followers?.followers, 95 | last5Posts, 96 | }); 97 | } catch (error) { 98 | console.log(error); 99 | res.status(404).json({ message: error.message }); 100 | } 101 | }; 102 | 103 | export const getFollowers = async (req, res, next) => { 104 | try { 105 | const { userId } = req.body.user; 106 | 107 | // pagination 108 | const page = Number(req.query.page) || 1; 109 | const limit = Number(req.query.limit) || 8; 110 | const skip = (page - 1) * limit; //2-1 * 8 = 8 111 | 112 | const result = await Users.findById(userId).populate({ 113 | path: "followers", 114 | options: { sort: { _id: -1 }, limit: limit, skip: skip }, 115 | populate: { 116 | path: "followerId", 117 | select: "name email image accountType followers -password", 118 | }, 119 | }); 120 | 121 | const totalFollowers = await Users.findById(userId); 122 | 123 | const numOfPages = Math.ceil(totalFollowers?.followers?.length / limit); 124 | 125 | res.status(200).json({ 126 | data: result?.followers, 127 | total: totalFollowers?.followers?.length, 128 | numOfPages, 129 | page, 130 | }); 131 | } catch (error) { 132 | console.log(error); 133 | res.status(404).json({ message: error.message }); 134 | } 135 | }; 136 | 137 | export const getPostContent = async (req, res, next) => { 138 | try { 139 | const { userId } = req.body.user; 140 | 141 | let queryResult = Posts.find({ user: userId }).sort({ 142 | _id: -1, 143 | }); 144 | 145 | // pagination 146 | const page = Number(req.query.page) || 1; 147 | const limit = Number(req.query.limit) || 8; 148 | const skip = (page - 1) * limit; 149 | 150 | //records count 151 | const totalPost = await Posts.countDocuments({ user: userId }); 152 | const numOfPage = Math.ceil(totalPost / limit); 153 | 154 | queryResult = queryResult.skip(skip).limit(limit); 155 | 156 | const posts = await queryResult; 157 | 158 | res.status(200).json({ 159 | success: true, 160 | message: "Content Loaded successfully", 161 | totalPost, 162 | data: posts, 163 | page, 164 | numOfPage, 165 | }); 166 | } catch (error) { 167 | console.log(error); 168 | res.status(404).json({ message: error.message }); 169 | } 170 | }; 171 | 172 | export const createPost = async (req, res, next) => { 173 | try { 174 | const { userId } = req.body.user; 175 | const { desc, img, title, slug, cat } = req.body; 176 | 177 | if (!(desc || img || title || cat)) { 178 | return next( 179 | "All fields are required. Please enter a description, title, category and select image." 180 | ); 181 | } 182 | 183 | const post = await Posts.create({ 184 | user: userId, 185 | desc, 186 | img, 187 | title, 188 | slug, 189 | cat, 190 | }); 191 | 192 | res.status(200).json({ 193 | sucess: true, 194 | message: "Post created successfully", 195 | data: post, 196 | }); 197 | } catch (error) { 198 | console.log(error); 199 | res.status(404).json({ message: error.message }); 200 | } 201 | }; 202 | 203 | export const commentPost = async (req, res, next) => { 204 | try { 205 | const { desc } = req.body; 206 | const { userId } = req.body.user; 207 | const { id } = req.params; 208 | 209 | if (desc === null) { 210 | return res.status(404).json({ message: "Comment is required." }); 211 | } 212 | 213 | const newComment = new Comments({ desc, user: userId, post: id }); 214 | 215 | await newComment.save(); 216 | 217 | //updating the post with the comments id 218 | const post = await Posts.findById(id); 219 | 220 | post.comments.push(newComment._id); 221 | 222 | await Posts.findByIdAndUpdate(id, post, { 223 | new: true, 224 | }); 225 | 226 | res.status(201).json({ 227 | success: true, 228 | message: "Comment published successfully", 229 | newComment, 230 | }); 231 | } catch (error) { 232 | console.log(error); 233 | res.status(404).json({ message: error.message }); 234 | } 235 | }; 236 | 237 | export const updatePost = async (req, res, next) => { 238 | try { 239 | const { id } = req.params; 240 | const { status } = req.body; 241 | 242 | const post = await Posts.findByIdAndUpdate(id, { status }, { new: true }); 243 | 244 | res.status(200).json({ 245 | sucess: true, 246 | message: "Action performed successfully", 247 | data: post, 248 | }); 249 | } catch (error) { 250 | console.log(error); 251 | res.status(404).json({ message: error.message }); 252 | } 253 | }; 254 | 255 | export const getPosts = async (req, res, next) => { 256 | try { 257 | const { cat, writerId } = req.query; 258 | 259 | let query = { status: true }; 260 | 261 | if (cat) { 262 | query.cat = cat; 263 | } else if (writerId) { 264 | query.user = writerId; 265 | } 266 | 267 | let queryResult = Posts.find(query) 268 | .populate({ 269 | path: "user", 270 | select: "name image -password", 271 | }) 272 | .sort({ _id: -1 }); 273 | console.log(queryResult); 274 | // pagination 275 | const page = Number(req.query.page) || 1; 276 | const limit = Number(req.query.limit) || 5; 277 | const skip = (page - 1) * limit; 278 | 279 | //records count 280 | const totalPost = await Posts.countDocuments(queryResult); 281 | 282 | const numOfPage = Math.ceil(totalPost / limit); 283 | 284 | queryResult = queryResult.skip(skip).limit(limit); 285 | 286 | const posts = await queryResult; 287 | 288 | res.status(200).json({ 289 | success: true, 290 | totalPost, 291 | data: posts, 292 | page, 293 | numOfPage, 294 | }); 295 | } catch (error) { 296 | console.log(error); 297 | res.status(404).json({ message: error.message }); 298 | } 299 | }; 300 | 301 | export const getPopularContents = async (req, res, next) => { 302 | try { 303 | const posts = await Posts.aggregate([ 304 | { 305 | $match: { 306 | status: true, 307 | }, 308 | }, 309 | { 310 | $project: { 311 | title: 1, 312 | slug: 1, 313 | img: 1, 314 | cat: 1, 315 | views: { $size: "$views" }, 316 | createdAt: 1, 317 | }, 318 | }, 319 | { 320 | $sort: { views: -1 }, 321 | }, 322 | { 323 | $limit: 5, 324 | }, 325 | ]); 326 | 327 | const writers = await Users.aggregate([ 328 | { 329 | $match: { 330 | accountType: { $ne: "User" }, 331 | }, 332 | }, 333 | { 334 | $project: { 335 | name: 1, 336 | image: 1, 337 | followers: { $size: "$followers" }, 338 | }, 339 | }, 340 | { 341 | $sort: { followers: -1 }, 342 | }, 343 | { 344 | $limit: 5, 345 | }, 346 | ]); 347 | 348 | res.status(200).json({ 349 | success: true, 350 | message: "Successful", 351 | data: { posts, writers }, 352 | }); 353 | } catch (error) { 354 | console.log(error); 355 | res.status(404).json({ message: error.message }); 356 | } 357 | }; 358 | 359 | export const getPost = async (req, res, next) => { 360 | try { 361 | const { postId } = req.params; 362 | 363 | const post = await Posts.findById(postId).populate({ 364 | path: "user", 365 | select: "name image -password", 366 | }); 367 | 368 | const newView = await Views.create({ 369 | user: post?.user, 370 | post: postId, 371 | }); 372 | 373 | post.views.push(newView?._id); 374 | 375 | await Posts.findByIdAndUpdate(postId, post); 376 | 377 | res.status(200).json({ 378 | success: true, 379 | message: "Successful", 380 | data: post, 381 | }); 382 | } catch (error) { 383 | console.log(error); 384 | res.status(404).json({ message: error.message }); 385 | } 386 | }; 387 | 388 | export const getComments = async (req, res, next) => { 389 | try { 390 | const { postId } = req.params; 391 | 392 | const postComments = await Comments.find({ post: postId }) 393 | .populate({ 394 | path: "user", 395 | select: "name image -password", 396 | }) 397 | .sort({ _id: -1 }); 398 | 399 | res.status(200).json({ 400 | sucess: true, 401 | message: "successfully", 402 | data: postComments, 403 | }); 404 | } catch (error) { 405 | console.log(error); 406 | res.status(404).json({ message: error.message }); 407 | } 408 | }; 409 | 410 | export const deletePost = async (req, res, next) => { 411 | try { 412 | const { userId } = req.body.user; 413 | const { id } = req.params; 414 | 415 | await Posts.findOneAndDelete({ _id: id, user: userId }); 416 | 417 | res.status(200).json({ 418 | success: true, 419 | message: "Deleted successfully", 420 | }); 421 | } catch (error) { 422 | console.log(error); 423 | res.status(404).json({ message: error.message }); 424 | } 425 | }; 426 | 427 | export const deleteComment = async (req, res, next) => { 428 | try { 429 | const { id, postId } = req.params; 430 | 431 | await Comments.findByIdAndDelete(id); 432 | 433 | //removing commetn id from post 434 | const result = await Posts.updateOne( 435 | { _id: postId }, 436 | { $pull: { comments: id } } 437 | ); 438 | 439 | if (result.modifiedCount > 0) { 440 | res 441 | .status(200) 442 | .json({ success: true, message: "Comment removed successfully" }); 443 | } else { 444 | res.status(404).json({ message: "Post or comment not found" }); 445 | } 446 | } catch (error) { 447 | console.log(error); 448 | res.status(404).json({ message: error.message }); 449 | } 450 | }; 451 | -------------------------------------------------------------------------------- /server/controllers/userController.js: -------------------------------------------------------------------------------- 1 | import Verification from "../models/emailVerification.js"; 2 | import Followers from "../models/followersModel.js"; 3 | import Users from "../models/userModel.js"; 4 | import { compareString, createJWT } from "../utils/index.js"; 5 | import { sendVerificationEmail } from "../utils/sendEmail.js"; 6 | 7 | export const OPTVerification = async (req, res, next) => { 8 | try { 9 | const { userId, otp } = req.params; 10 | 11 | const result = await Verification.findOne({ userId }); 12 | 13 | const { expiresAt, token } = result; 14 | 15 | // token has expired, delete token 16 | if (expiresAt < Date.now()) { 17 | await Verification.findOneAndDelete({ userId }); 18 | 19 | const message = "Verification token has expired."; 20 | res.status(404).json({ message }); 21 | } else { 22 | const isMatch = await compareString(otp, token); 23 | 24 | if (isMatch) { 25 | await Promise.all([ 26 | Users.findOneAndUpdate({ _id: userId }, { emailVerified: true }), 27 | Verification.findOneAndDelete({ userId }), 28 | ]); 29 | 30 | const message = "Email verified successfully"; 31 | res.status(200).json({ message }); 32 | } else { 33 | const message = "Verification failed or link is invalid"; 34 | res.status(404).json({ message }); 35 | } 36 | } 37 | } catch (error) { 38 | console.log(error); 39 | res.status(404).json({ message: "Something went wrong" }); 40 | } 41 | }; 42 | 43 | export const resendOTP = async (req, res, next) => { 44 | try { 45 | const { id } = req.params; 46 | 47 | await Verification.findOneAndDelete({ userId: id }); 48 | 49 | const user = await Users.findById(id); 50 | 51 | user.password = undefined; 52 | 53 | const token = createJWT(user?._id); 54 | 55 | if (user?.accountType === "Writer") { 56 | sendVerificationEmail(user, res, token); 57 | } else res.status(404).json({ message: "Something went wrong" }); 58 | } catch (error) { 59 | console.log(error); 60 | res.status(404).json({ message: "Something went wrong" }); 61 | } 62 | }; 63 | 64 | export const followWritter = async (req, res, next) => { 65 | try { 66 | const followerId = req.body.user.userId; 67 | const { id } = req.params; 68 | 69 | const checks = await Followers.findOne({ followerId }); 70 | 71 | if (checks) 72 | return res.status(201).json({ 73 | success: false, 74 | message: "You're already following this writer.", 75 | }); 76 | 77 | const writer = await Users.findById(id); 78 | 79 | const newFollower = await Followers.create({ 80 | followerId, 81 | writerId: id, 82 | }); 83 | 84 | writer?.followers?.push(newFollower?._id); 85 | 86 | await Users.findByIdAndUpdate(id, writer, { new: true }); 87 | 88 | res.status(201).json({ 89 | success: true, 90 | message: "You're now following writer " + writer?.name, 91 | }); 92 | } catch (error) { 93 | console.log(error); 94 | res.status(404).json({ message: error.message }); 95 | } 96 | }; 97 | 98 | export const updateUser = async (req, res, next) => { 99 | try { 100 | const { userId } = req.body.user; 101 | const { firstName, lastName, image } = req.body; 102 | 103 | if (!(firstName || lastName)) { 104 | return next("Please provide all required fields"); 105 | } 106 | 107 | const updateUser = { 108 | name: firstName + " " + lastName, 109 | image, 110 | _id: userId, 111 | }; 112 | 113 | const user = await Users.findByIdAndUpdate(userId, updateUser, { 114 | new: true, 115 | }); 116 | 117 | const token = createJWT(user?._id); 118 | 119 | user.password = undefined; 120 | 121 | res.status(200).json({ 122 | sucess: true, 123 | message: "User updated successfully", 124 | user, 125 | token, 126 | }); 127 | } catch (error) { 128 | console.log(error); 129 | res.status(404).json({ message: error.message }); 130 | } 131 | }; 132 | 133 | export const getWriter = async (req, res, next) => { 134 | try { 135 | const { id } = req.params; 136 | 137 | const user = await Users.findById(id).populate({ 138 | path: "followers", 139 | select: "followerId", 140 | }); 141 | 142 | if (!user) { 143 | return res.status(200).send({ 144 | success: false, 145 | message: "Writer Not Found", 146 | }); 147 | } 148 | 149 | user.password = undefined; 150 | 151 | res.status(200).json({ 152 | success: true, 153 | data: user, 154 | }); 155 | } catch (error) { 156 | console.log(error); 157 | res.status(404).json({ message: "Something went wrong" }); 158 | } 159 | }; 160 | -------------------------------------------------------------------------------- /server/dbConfig/index.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | const dbConnection = async () => { 4 | try { 5 | await mongoose.connect(process.env.MONGODB_URL); 6 | 7 | console.log("DB Connected"); 8 | } catch (error) { 9 | console.log("DB Error: " + error); 10 | } 11 | }; 12 | 13 | export default dbConnection; 14 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | import bodyParser from "body-parser"; 2 | import cors from "cors"; 3 | import dotenv from "dotenv"; 4 | import express from "express"; 5 | import helmet from "helmet"; 6 | import morgan from "morgan"; 7 | import dbConnection from "./dbConfig/index.js"; 8 | 9 | import errorMiddleware from "./middleware/errorMiddleware.js"; 10 | import routes from "./routes/index.js"; 11 | 12 | dotenv.config(); 13 | 14 | const app = express(); 15 | const PORT = process.env.PORT || 8800; 16 | 17 | dbConnection(); 18 | 19 | app.use(helmet()); 20 | app.use(cors()); 21 | app.use(bodyParser.json()); 22 | app.use(bodyParser.urlencoded({ extended: true })); 23 | app.use(express.json({ limit: "10mb" })); 24 | app.use(express.urlencoded({ extended: true })); 25 | 26 | app.use(morgan("dev")); 27 | 28 | app.use(routes); 29 | 30 | app.use(errorMiddleware); 31 | 32 | app.listen(PORT, () => { 33 | console.log("Server running of port " + PORT); 34 | }); 35 | -------------------------------------------------------------------------------- /server/middleware/authMiddleware.js: -------------------------------------------------------------------------------- 1 | import JWT from "jsonwebtoken"; 2 | 3 | const authMiddleware = async (req, res, next) => { 4 | const authHeader = req?.headers?.authorization; 5 | 6 | if (!authHeader || !authHeader?.startsWith("Bearer")) { 7 | next("Authentication failed"); 8 | } 9 | 10 | // Bearer djhdgfghdjkgdfh 11 | 12 | const token = authHeader?.split(" ")[1]; 13 | 14 | try { 15 | const userToken = JWT.verify(token, process.env.JWT_SECRET_KEY); 16 | 17 | req.body.user = { 18 | userId: userToken.userId, 19 | }; 20 | 21 | next(); 22 | } catch (error) { 23 | console.log(error); 24 | next("Authentication failed"); 25 | } 26 | }; 27 | 28 | export default authMiddleware; 29 | -------------------------------------------------------------------------------- /server/middleware/errorMiddleware.js: -------------------------------------------------------------------------------- 1 | // ERROR MIDDLEWARE | NEXT FUNCTION 2 | 3 | const errorMiddleware = (err, req, res, next) => { 4 | const defaultError = { 5 | statusCode: 500, // Default to Internal Server Error 6 | success: "failed", 7 | message: err, 8 | }; 9 | 10 | // Log the error for debugging 11 | console.error(err); 12 | 13 | if (err?.name === "ValidationError") { 14 | defaultError.statusCode = 400; // Bad Request 15 | 16 | defaultError.message = Object.values(err.errors) 17 | .map((el) => el.message) 18 | .join(","); 19 | } 20 | 21 | // Duplicate key error 22 | if (err?.code && err?.code === 11000) { 23 | defaultError.statusCode = 409; // Conflict 24 | defaultError.message = `${Object.keys(err.keyPattern).join( 25 | ", " 26 | )} must be unique`; 27 | } 28 | 29 | res?.status(defaultError.statusCode).json({ 30 | success: defaultError.success, 31 | message: defaultError.message, 32 | }); 33 | }; 34 | 35 | export default errorMiddleware; 36 | -------------------------------------------------------------------------------- /server/models/commentModel.js: -------------------------------------------------------------------------------- 1 | import mongoose, { Schema } from "mongoose"; 2 | 3 | const commentSchema = new mongoose.Schema( 4 | { 5 | user: { type: Schema.Types.ObjectId, ref: "Users" }, 6 | post: { type: Schema.Types.ObjectId, ref: "Posts" }, 7 | desc: { type: String }, 8 | }, 9 | { timestamps: true } 10 | ); 11 | 12 | const Comments = mongoose.model("Comments", commentSchema); 13 | 14 | export default Comments; 15 | -------------------------------------------------------------------------------- /server/models/emailVerification.js: -------------------------------------------------------------------------------- 1 | import mongoose, { Schema } from "mongoose"; 2 | 3 | const emailVerificationSchema = Schema({ 4 | userId: String, 5 | token: String, 6 | createdAt: Date, 7 | expiresAt: Date, 8 | }); 9 | 10 | const Verification = mongoose.model("Verification", emailVerificationSchema); 11 | 12 | export default Verification; 13 | -------------------------------------------------------------------------------- /server/models/followersModel.js: -------------------------------------------------------------------------------- 1 | import mongoose, { Schema } from "mongoose"; 2 | 3 | const followersSchema = new mongoose.Schema( 4 | { 5 | followerId: { type: Schema.Types.ObjectId, ref: "Users" }, 6 | writerId: { type: Schema.Types.ObjectId, ref: "Users" }, 7 | }, 8 | { timestamps: true } 9 | ); 10 | 11 | const Followers = mongoose.model("Followers", followersSchema); 12 | 13 | export default Followers; 14 | -------------------------------------------------------------------------------- /server/models/postModel.js: -------------------------------------------------------------------------------- 1 | import mongoose, { Schema } from "mongoose"; 2 | 3 | const postSchema = new mongoose.Schema( 4 | { 5 | title: { type: String, required: true }, 6 | slug: { type: String, unique: true }, 7 | desc: { type: String }, 8 | img: { type: String }, 9 | cat: { type: String }, 10 | views: [{ type: Schema.Types.ObjectId, ref: "Views" }], 11 | user: { type: Schema.Types.ObjectId, ref: "Users" }, 12 | comments: [{ type: Schema.Types.ObjectId, ref: "Comments" }], 13 | status: { type: Boolean, default: true }, 14 | }, 15 | { timestamps: true } 16 | ); 17 | 18 | const Posts = mongoose.model("Posts", postSchema); 19 | 20 | export default Posts; 21 | -------------------------------------------------------------------------------- /server/models/userModel.js: -------------------------------------------------------------------------------- 1 | import mongoose, { Schema } from "mongoose"; 2 | 3 | const userSchema = new mongoose.Schema( 4 | { 5 | name: { type: String, required: true }, 6 | email: { type: String, required: true, unique: true }, 7 | emailVerified: { type: Boolean, default: false }, 8 | accountType: { type: String, default: "User" }, 9 | image: { type: String }, 10 | password: { type: String, select: true }, 11 | provider: { type: String, default: "Codewave" }, 12 | followers: [{ type: Schema.Types.ObjectId, ref: "Followers" }], 13 | }, 14 | { timestamps: true } 15 | ); 16 | 17 | const Users = mongoose.model("Users", userSchema); 18 | 19 | export default Users; 20 | -------------------------------------------------------------------------------- /server/models/viewsModel.js: -------------------------------------------------------------------------------- 1 | import mongoose, { Schema } from "mongoose"; 2 | 3 | const viewsSchema = new mongoose.Schema( 4 | { 5 | user: { type: Schema.Types.ObjectId, ref: "Users" }, 6 | post: { type: Schema.Types.ObjectId, ref: "Posts" }, 7 | }, 8 | { timestamps: true } 9 | ); 10 | 11 | const Views = mongoose.model("Views", viewsSchema); 12 | 13 | export default Views; 14 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "type": "module", 7 | "scripts": { 8 | "start": "nodemon index.js", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "bcryptjs": "^2.4.3", 15 | "body-parser": "^1.20.2", 16 | "cors": "^2.8.5", 17 | "dotenv": "^16.3.1", 18 | "express": "^4.18.2", 19 | "helmet": "^7.1.0", 20 | "jsonwebtoken": "^9.0.2", 21 | "mongoose": "^8.0.2", 22 | "morgan": "^1.10.0", 23 | "nodemailer": "^6.9.7", 24 | "nodemon": "^3.0.2" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /server/routes/authRoute.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import { 3 | googleSignUp, 4 | login, 5 | register, 6 | } from "../controllers/authController.js"; 7 | 8 | const router = express.Router(); 9 | 10 | router.post("/register", register); 11 | router.post("/google-signup", googleSignUp); 12 | router.post("/login", login); 13 | 14 | export default router; 15 | -------------------------------------------------------------------------------- /server/routes/index.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import authRoute from "./authRoute.js"; 3 | import userRoute from "./userRoute.js"; 4 | import postRoute from "./postRoute.js"; 5 | 6 | const router = express.Router(); 7 | 8 | router.use("/auth", authRoute); //auth/register 9 | router.use("/users", userRoute); 10 | router.use("/posts", postRoute); 11 | 12 | export default router; 13 | -------------------------------------------------------------------------------- /server/routes/postRoute.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import userAuth from "../middleware/authMiddleware.js"; 3 | import { 4 | commentPost, 5 | createPost, 6 | deleteComment, 7 | deletePost, 8 | getComments, 9 | getFollowers, 10 | getPopularContents, 11 | getPost, 12 | getPostContent, 13 | getPosts, 14 | stats, 15 | updatePost, 16 | } from "../controllers/postController.js"; 17 | 18 | const router = express.Router(); 19 | 20 | // ADMIN ROUTES 21 | router.post("/admin-analytics", userAuth, stats); 22 | router.post("/admin-followers", userAuth, getFollowers); 23 | router.post("/admin-content", userAuth, getPostContent); 24 | router.post("/create-post", userAuth, createPost); 25 | 26 | // LIKE & COMMENT ON POST 27 | router.post("/comment/:id", userAuth, commentPost); 28 | 29 | // UPDATE POST 30 | router.patch("/update/:id", userAuth, updatePost); 31 | 32 | // GET POSTS ROUTES 33 | router.get("/", getPosts); 34 | router.get("/popular", getPopularContents); 35 | router.get("/:postId", getPost); 36 | router.get("/comments/:postId", getComments); 37 | 38 | // DELETE POSTS ROUTES 39 | router.delete("/:id", userAuth, deletePost); 40 | router.delete("/comment/:id/:postId", userAuth, deleteComment); 41 | 42 | export default router; 43 | -------------------------------------------------------------------------------- /server/routes/userRoute.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import userAuth from "../middleware/authMiddleware.js"; 3 | import { 4 | OPTVerification, 5 | followWritter, 6 | getWriter, 7 | resendOTP, 8 | updateUser, 9 | } from "../controllers/userController.js"; 10 | 11 | const router = express.Router(); 12 | 13 | router.post("/verify/:userId/:otp", OPTVerification); 14 | router.post("/resend-link/:id", resendOTP); 15 | 16 | // user routes 17 | router.post("/follower/:id", userAuth, followWritter); 18 | router.put("/update-user", userAuth, updateUser); 19 | 20 | router.get("/get-user/:id?", getWriter); 21 | 22 | export default router; 23 | -------------------------------------------------------------------------------- /server/utils/index.js: -------------------------------------------------------------------------------- 1 | import bcrypt from "bcryptjs"; 2 | import JWT from "jsonwebtoken"; 3 | 4 | export const hashString = async (userValue) => { 5 | const salt = await bcrypt.genSalt(10); 6 | 7 | const hashedpassword = await bcrypt.hash(userValue, salt); 8 | return hashedpassword; 9 | }; 10 | 11 | export const compareString = async (userPassword, password) => { 12 | try { 13 | const isMatch = await bcrypt?.compare(userPassword, password); 14 | return isMatch; 15 | } catch (error) { 16 | console.log(error); 17 | } 18 | }; 19 | 20 | //JSON WEBTOKEN 21 | export function createJWT(id) { 22 | return JWT.sign({ userId: id }, process.env.JWT_SECRET_KEY, { 23 | expiresIn: "1d", 24 | }); 25 | } 26 | 27 | export function generateOTP() { 28 | const min = 100000; // Minimum 6-digit number 29 | const max = 999999; // Maximum 6-digit number 30 | 31 | let randomNumber; 32 | 33 | randomNumber = Math.floor(Math.random() * (max - min + 1)) + min; 34 | 35 | return randomNumber; 36 | } 37 | -------------------------------------------------------------------------------- /server/utils/sendEmail.js: -------------------------------------------------------------------------------- 1 | import dotenv from "dotenv"; 2 | import nodemailer from "nodemailer"; 3 | import Verification from "../models/emailVerification.js"; 4 | import { generateOTP, hashString } from "./index.js"; 5 | 6 | dotenv.config(); 7 | 8 | const { AUTH_EMAIL, AUTH_PASSWORD } = process.env; 9 | 10 | let transporter = nodemailer.createTransport({ 11 | host: "smtp-mail.outlook.com", 12 | auth: { 13 | user: AUTH_EMAIL, 14 | pass: AUTH_PASSWORD, 15 | }, 16 | }); 17 | 18 | export const sendVerificationEmail = async (user, res, token) => { 19 | const { _id, email, name } = user; 20 | const otp = generateOTP(); 21 | 22 | // mail options 23 | const mailOptions = { 24 | from: AUTH_EMAIL, 25 | to: email, 26 | subject: "Email Verification", 27 | html: `
29 |

Please verify your email address

30 |
31 |

Hi, ${name},

32 |

33 | Please verify your email address with the OTP. 34 |
35 |

${otp}

36 |

This OTP expires in 2 mins

37 |

38 |
39 |
Regards
40 |
BlogWave
41 |
42 |
`, 43 | }; 44 | 45 | try { 46 | const hashedToken = await hashString(String(otp)); 47 | 48 | const newVerifiedEmail = await Verification.create({ 49 | userId: _id, 50 | token: hashedToken, 51 | createdAt: Date.now(), 52 | expiresAt: Date.now() + 120000, 53 | }); 54 | 55 | if (newVerifiedEmail) { 56 | transporter 57 | .sendMail(mailOptions) 58 | .then(() => { 59 | res.status(201).send({ 60 | success: "PENDING", 61 | message: 62 | "OTP has been sent to your account. Check your email and verify your email.", 63 | user, 64 | token, 65 | }); 66 | }) 67 | .catch((err) => { 68 | console.log(err); 69 | res.status(404).json({ message: "Something went wrong" }); 70 | }); 71 | } 72 | } catch (error) { 73 | console.log(error); 74 | res.status(404).json({ message: "Something went wrong" }); 75 | } 76 | }; 77 | --------------------------------------------------------------------------------