├── images
├── error-bg.png
├── register-bg.webp
├── Threads-logo-white-bg.png
└── Threads-logo-black-bg.webp
├── client
├── public
│ ├── error-bg.png
│ ├── register-bg.webp
│ ├── Threads-logo-white-bg.png
│ └── Threads-logo-black-bg.webp
├── Dockerfile
├── vite.config.js
├── .gitignore
├── index.html
├── .dockerignore
├── src
│ ├── redux
│ │ ├── store.js
│ │ ├── slice.js
│ │ └── service.js
│ ├── components
│ │ ├── common
│ │ │ ├── Loading.jsx
│ │ │ ├── Header.jsx
│ │ │ └── Navbar.jsx
│ │ ├── home
│ │ │ ├── Input.jsx
│ │ │ ├── Post.jsx
│ │ │ └── post
│ │ │ │ ├── PostOne.jsx
│ │ │ │ ├── Comments.jsx
│ │ │ │ └── PostTwo.jsx
│ │ ├── menu
│ │ │ ├── MyMenu.jsx
│ │ │ └── MainMenu.jsx
│ │ ├── search
│ │ │ ├── ProfileBar.jsx
│ │ │ └── SearchInput.jsx
│ │ └── modals
│ │ │ ├── AddPost.jsx
│ │ │ └── EditProfile.jsx
│ ├── index.css
│ ├── main.jsx
│ ├── pages
│ │ ├── Protected
│ │ │ ├── ProtectedLayout.jsx
│ │ │ ├── Search.jsx
│ │ │ ├── profile
│ │ │ │ ├── Replies.jsx
│ │ │ │ ├── Threads.jsx
│ │ │ │ ├── Repost.jsx
│ │ │ │ └── ProfileLayout.jsx
│ │ │ ├── Home.jsx
│ │ │ └── SinglePost.jsx
│ │ ├── Error.jsx
│ │ └── Register.jsx
│ └── App.jsx
├── README.md
├── .eslintrc.cjs
└── package.json
├── server
├── Dockerfile
├── config
│ ├── db.js
│ └── cloudinary.js
├── .env.sample
├── .dockerignore
├── models
│ ├── comment-model.js
│ ├── post-model.js
│ └── user-model.js
├── index.js
├── package.json
├── middleware
│ └── auth.js
├── routes.js
└── controllers
│ ├── comment-controller.js
│ ├── post-controller.js
│ └── user-controller.js
├── .gitignore
├── nginx.conf
├── compose.yaml
└── .github
└── workflows
└── main.yml
/images/error-bg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meabhisingh/thread/HEAD/images/error-bg.png
--------------------------------------------------------------------------------
/images/register-bg.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meabhisingh/thread/HEAD/images/register-bg.webp
--------------------------------------------------------------------------------
/client/public/error-bg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meabhisingh/thread/HEAD/client/public/error-bg.png
--------------------------------------------------------------------------------
/client/public/register-bg.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meabhisingh/thread/HEAD/client/public/register-bg.webp
--------------------------------------------------------------------------------
/images/Threads-logo-white-bg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meabhisingh/thread/HEAD/images/Threads-logo-white-bg.png
--------------------------------------------------------------------------------
/images/Threads-logo-black-bg.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meabhisingh/thread/HEAD/images/Threads-logo-black-bg.webp
--------------------------------------------------------------------------------
/client/public/Threads-logo-white-bg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meabhisingh/thread/HEAD/client/public/Threads-logo-white-bg.png
--------------------------------------------------------------------------------
/client/public/Threads-logo-black-bg.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/meabhisingh/thread/HEAD/client/public/Threads-logo-black-bg.webp
--------------------------------------------------------------------------------
/server/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:20
2 |
3 | WORKDIR /usr/src/app
4 |
5 | COPY package*.json ./
6 |
7 | RUN npm install
8 |
9 | COPY . .
10 |
11 | CMD [ "npm", "start" ]
--------------------------------------------------------------------------------
/client/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:20
2 |
3 | WORKDIR /usr/src/app
4 |
5 | COPY package*.json ./
6 |
7 | RUN npm install
8 |
9 | COPY . .
10 |
11 | RUN npm run build
12 |
13 | CMD [ "npm", "run","preview" ]
--------------------------------------------------------------------------------
/server/config/db.js:
--------------------------------------------------------------------------------
1 | const mongoose = require("mongoose");
2 |
3 | const connectDB = async () => {
4 | await mongoose.connect(process.env.MONGO_URI);
5 | console.log("DB Connected...");
6 | };
7 |
8 | module.exports = connectDB;
9 |
--------------------------------------------------------------------------------
/server/.env.sample:
--------------------------------------------------------------------------------
1 | PORT=5000
2 |
3 | MONGO_URI=URI TO MONGO DB
4 |
5 | JWT_SECRET=SECRET FOR JWT
6 |
7 | CLOUD_NAME=CLOUDINARY CLOUD NAME
8 |
9 | API_KEY=CLOUDINARY API KEY
10 |
11 | API_SECRET=CLOUDINARY API SECRET
12 |
13 | CLIENT_URL=URL TO CLIENT
--------------------------------------------------------------------------------
/client/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vite";
2 | import react from "@vitejs/plugin-react";
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 | plugins: [react()],
7 | define: {
8 | SERVER_URL: JSON.stringify("https://threadapi.6packprogrammer.store"),
9 | },
10 | });
11 |
--------------------------------------------------------------------------------
/server/config/cloudinary.js:
--------------------------------------------------------------------------------
1 | const cloudinary = require("cloudinary").v2;
2 | const dotenv = require("dotenv");
3 |
4 | dotenv.config();
5 |
6 | cloudinary.config({
7 | cloud_name: process.env.CLOUD_NAME,
8 | api_key: process.env.API_KEY,
9 | api_secret: process.env.API_SECRET,
10 | });
11 |
12 | module.exports = cloudinary;
13 |
--------------------------------------------------------------------------------
/client/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/client/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Threads Clone | App by Aditya Jawanjal
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 | *node_modules
26 | npm-debug.log
27 | .git
28 | .idea
29 | .vscode
30 | *.env
31 | *dist
32 |
33 |
--------------------------------------------------------------------------------
/client/.dockerignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 | *node_modules
26 | npm-debug.log
27 | .git
28 | .idea
29 | .vscode
30 | *.env
31 | *dist
32 |
33 |
--------------------------------------------------------------------------------
/client/src/redux/store.js:
--------------------------------------------------------------------------------
1 | import { configureStore } from "@reduxjs/toolkit";
2 | import serviceReducer from "./slice";
3 | import { serviceApi } from "./service";
4 |
5 | export default configureStore({
6 | reducer: {
7 | service: serviceReducer,
8 | [serviceApi.reducerPath]: serviceApi.reducer,
9 | },
10 | middleware: (getDefaultMiddleware) =>
11 | getDefaultMiddleware({ serializableCheck: false }).concat(
12 | serviceApi.middleware
13 | ),
14 | });
15 |
--------------------------------------------------------------------------------
/server/.dockerignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 | *node_modules
26 | npm-debug.log
27 | .git
28 | .idea
29 | .vscode
30 | *.env
31 | *dist
32 |
33 |
--------------------------------------------------------------------------------
/client/README.md:
--------------------------------------------------------------------------------
1 | # React + Vite
2 |
3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4 |
5 | Currently, two official plugins are available:
6 |
7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
9 |
--------------------------------------------------------------------------------
/client/src/components/common/Loading.jsx:
--------------------------------------------------------------------------------
1 | import { Stack, CircularProgress } from "@mui/material";
2 |
3 | const Loading = () => {
4 | return (
5 |
14 |
15 |
16 | );
17 | };
18 |
19 | export default Loading;
20 |
--------------------------------------------------------------------------------
/server/models/comment-model.js:
--------------------------------------------------------------------------------
1 | const mongoose = require("mongoose");
2 |
3 | const commentSchema = new mongoose.Schema(
4 | {
5 | admin: {
6 | type: mongoose.Schema.Types.ObjectId,
7 | ref: "user",
8 | },
9 | post: {
10 | type: mongoose.Schema.Types.ObjectId,
11 | ref: "post",
12 | },
13 | text: {
14 | type: String,
15 | },
16 | },
17 | { timestamps: true }
18 | );
19 |
20 | module.exports = mongoose.model("comment", commentSchema);
21 |
--------------------------------------------------------------------------------
/nginx.conf:
--------------------------------------------------------------------------------
1 |
2 | server {
3 |
4 | listen 80;
5 |
6 | server_name thread.6packprogrammer.store threadapi.6packprogrammer.store;
7 |
8 | location / {
9 |
10 | if ($host = thread.6packprogrammer.store) {
11 | proxy_pass http://localhost:4173;
12 | }
13 |
14 | if ($host = threadapi.6packprogrammer.store) {
15 | proxy_pass http://localhost:5000;
16 | }
17 |
18 | proxy_http_version 1.1;
19 | proxy_set_header Upgrade $http_upgrade;
20 | proxy_set_header Connection 'upgrade';
21 | proxy_set_header Host $host;
22 | proxy_cache_bypass $http_upgrade;
23 | }
24 |
25 |
26 | }
--------------------------------------------------------------------------------
/server/models/post-model.js:
--------------------------------------------------------------------------------
1 | const mongoose = require("mongoose");
2 |
3 | const postSchema = new mongoose.Schema(
4 | {
5 | admin: {
6 | type: mongoose.Schema.Types.ObjectId,
7 | ref: "user",
8 | },
9 | text: {
10 | type: String,
11 | },
12 | media: {
13 | type: String,
14 | },
15 | public_id: {
16 | type: String,
17 | },
18 | likes: [{ type: mongoose.Schema.Types.ObjectId, ref: "user" }],
19 | comments: [{ type: mongoose.Schema.Types.ObjectId, ref: "comment" }],
20 | },
21 | { timestamps: true }
22 | );
23 |
24 | module.exports = mongoose.model("post", postSchema);
25 |
--------------------------------------------------------------------------------
/client/src/index.css:
--------------------------------------------------------------------------------
1 | * {
2 | margin: 0;
3 | padding: 0;
4 | }
5 | .login-link {
6 | text-decoration: underline;
7 | cursor: pointer;
8 | color: blue;
9 | }
10 | .link {
11 | text-decoration: none;
12 | color: black;
13 | }
14 | .image-icon {
15 | cursor: pointer;
16 | }
17 | .text1 {
18 | border: none;
19 | outline: none;
20 | padding: 5px 10px;
21 | position: relative;
22 | left: -10px;
23 | color: gray;
24 | font-size: 1rem;
25 | margin-bottom: 20px;
26 | }
27 | .file-input {
28 | display: none;
29 | }
30 | #url-img {
31 | margin: 0 0 10px;
32 | }
33 | .mode {
34 | color: whitesmoke;
35 | background-color: rgb(18, 18, 18);
36 | }
37 |
--------------------------------------------------------------------------------
/server/index.js:
--------------------------------------------------------------------------------
1 | const express = require("express");
2 | const dotenv = require("dotenv");
3 | const connectDB = require("./config/db");
4 | const router = require("./routes");
5 | const cookieParser = require("cookie-parser");
6 | const cors = require("cors");
7 |
8 | dotenv.config();
9 | const app = express();
10 | connectDB();
11 |
12 | app.use(
13 | cors({
14 | origin: [process.env.CLIENT_URL],
15 | credentials: true,
16 | })
17 | );
18 | app.use(express.json());
19 | app.use(cookieParser());
20 | app.use("/api", router);
21 |
22 | const port = process.env.PORT || 5000;
23 | app.listen(port, () => {
24 | console.log(`App is listening on PORT : ${port}`);
25 | });
26 |
--------------------------------------------------------------------------------
/client/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | global: {
4 | SERVER_URL: "readonly",
5 | },
6 | env: { browser: true, es2020: true },
7 | extends: [
8 | "eslint:recommended",
9 | "plugin:react/recommended",
10 | "plugin:react/jsx-runtime",
11 | "plugin:react-hooks/recommended",
12 | ],
13 | ignorePatterns: ["dist", ".eslintrc.cjs"],
14 | parserOptions: { ecmaVersion: "latest", sourceType: "module" },
15 | settings: { react: { version: "18.2" } },
16 | plugins: ["react-refresh"],
17 | rules: {
18 | "react/jsx-no-target-blank": "off",
19 | "react-refresh/only-export-components": [
20 | "warn",
21 | { allowConstantExport: true },
22 | ],
23 | },
24 | };
25 |
--------------------------------------------------------------------------------
/client/src/main.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom/client";
3 | import App from "./App.jsx";
4 | import "./index.css";
5 | import { Provider } from "react-redux";
6 | import store from "./redux/store.js";
7 | import "react-toastify/dist/ReactToastify.css";
8 | import { ToastContainer } from "react-toastify";
9 | import { HelmetProvider } from "react-helmet-async";
10 |
11 | const helmetContext = {};
12 |
13 | ReactDOM.createRoot(document.getElementById("root")).render(
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | );
23 |
--------------------------------------------------------------------------------
/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "threads-clone",
3 | "version": "1.0.0",
4 | "description": "This is a Threads Clone .",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1",
8 | "dev": "nodemon index.js",
9 | "start": "node index.js"
10 | },
11 | "author": "Aditya Jawanjal",
12 | "license": "ISC",
13 | "dependencies": {
14 | "bcrypt": "^5.1.1",
15 | "cloudinary": "^2.0.3",
16 | "cookie-parser": "^1.4.6",
17 | "cors": "^2.8.5",
18 | "dotenv": "^16.4.5",
19 | "express": "^4.19.2",
20 | "formidable": "^2.1.2",
21 | "jsonwebtoken": "^9.0.2",
22 | "mongoose": "^8.2.3"
23 | },
24 | "devDependencies": {
25 | "nodemon": "^3.1.0"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/client/src/pages/Protected/ProtectedLayout.jsx:
--------------------------------------------------------------------------------
1 | import { Stack, useMediaQuery } from "@mui/material";
2 | import { Outlet } from "react-router-dom";
3 | import Header from "../../components/common/Header";
4 | import AddPost from "../../components/modals/AddPost";
5 | import EditProfile from "../../components/modals/EditProfile";
6 | import MainMenu from "../../components/menu/MainMenu";
7 | import MyMenu from "../../components/menu/MyMenu";
8 |
9 | const ProtectedLayout = () => {
10 | const _700 = useMediaQuery("(min-width:700px)");
11 |
12 | return (
13 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | );
28 | };
29 |
30 | export default ProtectedLayout;
31 |
--------------------------------------------------------------------------------
/compose.yaml:
--------------------------------------------------------------------------------
1 | services:
2 | client:
3 | image: meabhisingh/thread-client
4 | container_name: client
5 | ports:
6 | - "4173:4173"
7 | depends_on:
8 | - server
9 |
10 | server:
11 | image: meabhisingh/thread-server
12 | container_name: server
13 | ports:
14 | - "5000:5000"
15 | environment:
16 | MONGO_URI: mongodb://db/Thread
17 | PORT: 5000
18 | JWT_SECRET: mysecret
19 | CLOUD_NAME: mycloudname
20 | API_KEY: myapikey
21 | API_SECRET: myapisecret
22 | CLIENT_URL: http://localhost:4173
23 | depends_on:
24 | - db
25 |
26 | db:
27 | image: mongo
28 | container_name: mongodb
29 | ports:
30 | - "27017:27017"
31 | # environment:
32 | # MONGO_INITDB_ROOT_USERNAME: admin
33 | # MONGO_INITDB_ROOT_PASSWORD: mypassword
34 | volumes:
35 | - mongodbdata:/data/db
36 |
37 | volumes:
38 | mongodbdata:
39 | driver: local
40 |
--------------------------------------------------------------------------------
/client/src/pages/Protected/Search.jsx:
--------------------------------------------------------------------------------
1 | import { Stack, Typography } from "@mui/material";
2 | import ProfileBar from "../../components/search/ProfileBar";
3 | import SearchInput from "../../components/search/SearchInput";
4 | import { useSelector } from "react-redux";
5 |
6 | const Search = () => {
7 | const { searchedUsers } = useSelector((state) => state.service);
8 |
9 | return (
10 | <>
11 |
12 |
13 | {searchedUsers ? (
14 | searchedUsers.length > 0 ? (
15 | searchedUsers.map((e) => {
16 | return ;
17 | })
18 | ) : (
19 | ""
20 | )
21 | ) : (
22 |
23 | Start searching...
24 |
25 | )}
26 |
27 | >
28 | );
29 | };
30 | export default Search;
31 |
--------------------------------------------------------------------------------
/server/middleware/auth.js:
--------------------------------------------------------------------------------
1 | const User = require("../models/user-model");
2 | const jwt = require("jsonwebtoken");
3 |
4 | const auth = async (req, res, next) => {
5 | try {
6 | const token = req.cookies.token;
7 | if (!token) {
8 | return res.status(400).json({ msg: "No token in auth !" });
9 | }
10 | const decodedToken = jwt.verify(token, process.env.JWT_SECRET);
11 | if (!decodedToken) {
12 | return res
13 | .status(400)
14 | .json({ msg: "Error while decoding token in auth !" });
15 | }
16 | const user = await User.findById(decodedToken.token)
17 | .populate("followers")
18 | .populate("threads")
19 | .populate("replies")
20 | .populate("reposts");
21 | if (!user) {
22 | return res.status(400).json({ msg: "No user found !" });
23 | }
24 | req.user = user;
25 | next();
26 | } catch (err) {
27 | return res.status(400).json({ msg: "Error in auth !", err: err.message });
28 | }
29 | };
30 |
31 | module.exports = auth;
32 |
--------------------------------------------------------------------------------
/client/src/pages/Protected/profile/Replies.jsx:
--------------------------------------------------------------------------------
1 | import { Stack, Typography, useMediaQuery } from "@mui/material";
2 | import Comments from "../../../components/home/post/Comments";
3 | import { useSelector } from "react-redux";
4 |
5 | const Replies = () => {
6 | const { user } = useSelector((state) => state.service);
7 | const _700 = useMediaQuery("(min-width:700px)");
8 | return (
9 | <>
10 |
16 | {user ? (
17 | user.user ? (
18 | user.user.replies.length > 0 ? (
19 | user.user.replies.map((e) => {
20 | return ;
21 | })
22 | ) : (
23 |
24 | No Replies yet !
25 |
26 | )
27 | ) : null
28 | ) : null}
29 |
30 | >
31 | );
32 | };
33 |
34 | export default Replies;
35 |
--------------------------------------------------------------------------------
/server/models/user-model.js:
--------------------------------------------------------------------------------
1 | const mongoose = require("mongoose");
2 |
3 | const userSchema = new mongoose.Schema(
4 | {
5 | userName: {
6 | type: String,
7 | required: true,
8 | trim: true,
9 | },
10 | email: {
11 | type: String,
12 | required: true,
13 | trim: true,
14 | unique: true,
15 | },
16 | password: {
17 | type: String,
18 | required: true,
19 | },
20 | bio: {
21 | type: String,
22 | },
23 | profilePic: {
24 | type: String,
25 | default:
26 | "https://www.pngall.com/wp-content/uploads/5/User-Profile-PNG-Clipart.png",
27 | },
28 | public_id: {
29 | type: String,
30 | },
31 | followers: [{ type: mongoose.Schema.Types.ObjectId, ref: "user" }],
32 | threads: [{ type: mongoose.Schema.Types.ObjectId, ref: "post" }],
33 | replies: [{ type: mongoose.Schema.Types.ObjectId, ref: "comment" }],
34 | reposts: [{ type: mongoose.Schema.Types.ObjectId, ref: "post" }],
35 | },
36 | { timestamps: true }
37 | );
38 |
39 | module.exports = mongoose.model("user", userSchema);
40 |
--------------------------------------------------------------------------------
/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "client",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite --host",
8 | "build": "vite build",
9 | "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
10 | "preview": "vite preview --host"
11 | },
12 | "dependencies": {
13 | "@emotion/react": "^11.11.3",
14 | "@emotion/styled": "^11.11.0",
15 | "@mui/material": "^5.15.11",
16 | "@reduxjs/toolkit": "^2.2.2",
17 | "react": "^18.2.0",
18 | "react-dom": "^18.2.0",
19 | "react-helmet-async": "^2.0.4",
20 | "react-icons": "^5.0.1",
21 | "react-redux": "^9.1.0",
22 | "react-router-dom": "^6.22.1",
23 | "react-toastify": "^10.0.5"
24 | },
25 | "devDependencies": {
26 | "@types/react": "^18.2.56",
27 | "@types/react-dom": "^18.2.19",
28 | "@vitejs/plugin-react": "^4.2.1",
29 | "eslint": "^8.56.0",
30 | "eslint-plugin-react": "^7.33.2",
31 | "eslint-plugin-react-hooks": "^4.6.0",
32 | "eslint-plugin-react-refresh": "^0.4.5",
33 | "vite": "^5.1.4"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/client/src/pages/Protected/profile/Threads.jsx:
--------------------------------------------------------------------------------
1 | import { Stack, Typography, useMediaQuery } from "@mui/material";
2 | import Post from "../../../components/home/Post";
3 | import { useSelector } from "react-redux";
4 |
5 | const Threads = () => {
6 | const { user } = useSelector((state) => state.service);
7 | const _700 = useMediaQuery("(min-width:700px)");
8 | return (
9 | <>
10 | {user ? (
11 | user.user ? (
12 | user.user.threads.length > 0 ? (
13 |
20 | {user.user.threads.map((e) => {
21 | return ;
22 | })}
23 |
24 | ) : (
25 |
26 | No Thread yet !
27 |
28 | )
29 | ) : (
30 | ""
31 | )
32 | ) : (
33 | ""
34 | )}
35 | >
36 | );
37 | };
38 |
39 | export default Threads;
40 |
--------------------------------------------------------------------------------
/client/src/pages/Protected/profile/Repost.jsx:
--------------------------------------------------------------------------------
1 | import { Stack, Typography, useMediaQuery } from "@mui/material";
2 | import Post from "../../../components/home/Post";
3 | import { useSelector } from "react-redux";
4 |
5 | const Repost = () => {
6 | const { user } = useSelector((state) => state.service);
7 | const _700 = useMediaQuery("(min-width:700px)");
8 | return (
9 | <>
10 | {user ? (
11 | user.user ? (
12 | user.user.reposts.length > 0 ? (
13 |
20 | {user.user.reposts.map((e) => {
21 | return ;
22 | })}
23 |
24 | ) : (
25 |
26 | No Repost yet !
27 |
28 | )
29 | ) : (
30 |
31 | No Repost yet !
32 |
33 | )
34 | ) : (
35 |
36 | No Repost yet !
37 |
38 | )}
39 | >
40 | );
41 | };
42 |
43 | export default Repost;
44 |
--------------------------------------------------------------------------------
/server/routes.js:
--------------------------------------------------------------------------------
1 | const express = require("express");
2 | const {
3 | signin,
4 | login,
5 | userDetails,
6 | followUser,
7 | updateProfile,
8 | searchUser,
9 | logout,
10 | myInfo,
11 | } = require("./controllers/user-controller");
12 | const auth = require("./middleware/auth");
13 | const {
14 | addPost,
15 | allPost,
16 | deletePost,
17 | likePost,
18 | repost,
19 | singlePost,
20 | } = require("./controllers/post-controller");
21 | const {
22 | addComment,
23 | deleteComment,
24 | } = require("./controllers/comment-controller");
25 |
26 | const router = express.Router();
27 |
28 | router.post("/signin", signin);
29 | router.post("/login", login);
30 |
31 | router.get("/user/:id", auth, userDetails);
32 | router.put("/user/follow/:id", auth, followUser);
33 | router.put("/update", auth, updateProfile);
34 | router.get("/users/search/:query", auth, searchUser);
35 | router.post("/logout", auth, logout);
36 | router.get("/me", auth, myInfo);
37 |
38 | router.post("/post", auth, addPost);
39 | router.get("/post", auth, allPost);
40 | router.delete("/post/:id", auth, deletePost);
41 | router.put("/post/like/:id", auth, likePost);
42 | router.put("/repost/:id", auth, repost);
43 | router.get("/post/:id", auth, singlePost);
44 |
45 | router.post("/comment/:id", auth, addComment);
46 | router.delete("/comment/:postId/:id", auth, deleteComment);
47 |
48 | module.exports = router;
49 |
--------------------------------------------------------------------------------
/client/src/pages/Error.jsx:
--------------------------------------------------------------------------------
1 | import { Button, Stack, Typography } from "@mui/material";
2 | import { useNavigate } from "react-router-dom";
3 |
4 | const Error = () => {
5 | const navigate = useNavigate();
6 |
7 | return (
8 | <>
9 |
21 |
31 | OOP`s
32 | You entered wrong location !
33 |
49 |
50 |
51 | >
52 | );
53 | };
54 | export default Error;
55 |
--------------------------------------------------------------------------------
/client/src/components/home/Input.jsx:
--------------------------------------------------------------------------------
1 | import {
2 | Avatar,
3 | Button,
4 | Stack,
5 | Typography,
6 | useMediaQuery,
7 | } from "@mui/material";
8 | import { useDispatch, useSelector } from "react-redux";
9 | import { addPostModal } from "../../redux/slice";
10 |
11 | const Input = () => {
12 | const { myInfo } = useSelector((state) => state.service);
13 | const _700 = useMediaQuery("(min-width:700px)");
14 |
15 | const dispatch = useDispatch();
16 |
17 | const handleAddPost = () => {
18 | dispatch(addPostModal(true));
19 | };
20 |
21 | return (
22 | <>
23 | {_700 ? (
24 |
36 |
37 |
41 | Start a thread...
42 |
43 |
56 |
57 | ) : null}
58 | >
59 | );
60 | };
61 |
62 | export default Input;
63 |
--------------------------------------------------------------------------------
/client/src/pages/Protected/Home.jsx:
--------------------------------------------------------------------------------
1 | import { Button, Stack, Typography } from "@mui/material";
2 | import Input from "../../components/home/Input";
3 | import Post from "../../components/home/Post";
4 | import Loading from "../../components/common/Loading";
5 | import { useAllPostQuery } from "../../redux/service";
6 | import { useEffect, useState } from "react";
7 | import { useSelector } from "react-redux";
8 |
9 | const Home = () => {
10 | const [page, setPage] = useState(1);
11 | const [showMore, setShowMore] = useState(true);
12 | const { data, isLoading } = useAllPostQuery(page);
13 | const { allPosts } = useSelector((state) => state.service);
14 |
15 | const handleClick = () => {
16 | setPage((pre) => pre + 1);
17 | };
18 |
19 | useEffect(() => {
20 | if (data) {
21 | if (data.posts.length < 3) {
22 | setShowMore(false);
23 | }
24 | }
25 | }, [data]);
26 |
27 | return (
28 | <>
29 |
30 |
31 | {allPosts ? (
32 | allPosts.length > 0 ? (
33 | allPosts.map((e) => {
34 | return ;
35 | })
36 | ) : (
37 |
38 | No post yet !
39 |
40 | )
41 | ) : isLoading ? (
42 |
43 | ) : null}
44 |
45 | {showMore ? (
46 |
53 | ) : (
54 | allPosts?.length > 0 && (
55 |
56 | You have reached the end !
57 |
58 | )
59 | )}
60 | >
61 | );
62 | };
63 |
64 | export default Home;
65 |
--------------------------------------------------------------------------------
/client/src/components/menu/MyMenu.jsx:
--------------------------------------------------------------------------------
1 | import { Menu, MenuItem } from "@mui/material";
2 | import { useDispatch, useSelector } from "react-redux";
3 | import { toggleMyMenu } from "../../redux/slice";
4 | import { useDeletePostMutation } from "../../redux/service";
5 | import { useEffect } from "react";
6 | import { Bounce, toast } from "react-toastify";
7 |
8 | const MyMenu = () => {
9 | const { anchorE2, postId } = useSelector((state) => state.service);
10 |
11 | const dispatch = useDispatch();
12 |
13 | const [deletePost, deletePostData] = useDeletePostMutation();
14 |
15 | const handleClose = () => {
16 | dispatch(toggleMyMenu(null));
17 | };
18 |
19 | const handleDeletePost = async () => {
20 | handleClose();
21 | await deletePost(postId);
22 | };
23 |
24 | useEffect(() => {
25 | if (deletePostData.isSuccess) {
26 | toast.warning(deletePostData.data.msg, {
27 | position: "top-center",
28 | autoClose: 2500,
29 | hideProgressBar: false,
30 | closeOnClick: true,
31 | pauseOnHover: true,
32 | draggable: true,
33 | theme: "colored",
34 | transition: Bounce,
35 | });
36 | }
37 | if (deletePostData.isError) {
38 | toast.error(deletePostData.error.data.msg, {
39 | position: "top-center",
40 | autoClose: 2500,
41 | hideProgressBar: false,
42 | closeOnClick: true,
43 | pauseOnHover: true,
44 | draggable: true,
45 | theme: "colored",
46 | transition: Bounce,
47 | });
48 | }
49 | }, [deletePostData.isSuccess, deletePostData.isError]);
50 |
51 | return (
52 | <>
53 |
62 | >
63 | );
64 | };
65 |
66 | export default MyMenu;
67 |
--------------------------------------------------------------------------------
/client/src/App.jsx:
--------------------------------------------------------------------------------
1 | import { Box } from "@mui/material";
2 | import { useSelector } from "react-redux";
3 | import { BrowserRouter, Route, Routes } from "react-router-dom";
4 | import Error from "./pages/Error";
5 | import Home from "./pages/Protected/Home";
6 | import ProfileLayout from "./pages/Protected/profile/ProfileLayout";
7 | import Replies from "./pages/Protected/profile/Replies";
8 | import Repost from "./pages/Protected/profile/Repost";
9 | import Threads from "./pages/Protected/profile/Threads";
10 | import ProtectedLayout from "./pages/Protected/ProtectedLayout";
11 | import Search from "./pages/Protected/Search";
12 | import Register from "./pages/Register";
13 | import SinglePost from "./pages/Protected/SinglePost";
14 | import { useMyInfoQuery } from "./redux/service";
15 |
16 | const App = () => {
17 | const { darkMode } = useSelector((state) => state.service);
18 | const { data, isError } = useMyInfoQuery();
19 |
20 | if (isError || !data) {
21 | return (
22 |
23 |
24 | } />
25 |
26 |
27 | );
28 | }
29 |
30 | return (
31 | <>
32 |
33 |
34 |
35 | }>
36 | } />
37 | } />
38 | } />
39 | }>
40 | } />
41 | } />
42 | } />
43 |
44 |
45 | } />
46 |
47 |
48 |
49 | >
50 | );
51 | };
52 |
53 | export default App;
54 |
--------------------------------------------------------------------------------
/client/src/components/search/ProfileBar.jsx:
--------------------------------------------------------------------------------
1 | import {
2 | Avatar,
3 | Button,
4 | Stack,
5 | Typography,
6 | useMediaQuery,
7 | } from "@mui/material";
8 | import { useSelector } from "react-redux";
9 | import { Link } from "react-router-dom";
10 |
11 | const ProfileBar = ({ e }) => {
12 | const { darkMode } = useSelector((state) => state.service);
13 | const _700 = useMediaQuery("(min-width:700px)");
14 |
15 | return (
16 | <>
17 |
29 |
30 |
31 |
32 |
33 |
38 | {e ? e.userName : ""}
39 |
40 |
41 |
46 | {e ? e.bio : ""}
47 |
48 |
49 | {e ? e.followers.length : 0} followers
50 |
51 |
52 |
53 |
54 |
66 |
67 |
68 | >
69 | );
70 | };
71 | export default ProfileBar;
72 |
--------------------------------------------------------------------------------
/client/src/components/menu/MainMenu.jsx:
--------------------------------------------------------------------------------
1 | import { Menu, MenuItem } from "@mui/material";
2 | import { useDispatch, useSelector } from "react-redux";
3 | import { Link } from "react-router-dom";
4 | import { toggleColorMode, toggleMainMenu } from "../../redux/slice";
5 | import { useLogoutMeMutation } from "../../redux/service";
6 | import { useEffect } from "react";
7 | import { Bounce, toast } from "react-toastify";
8 |
9 | const MainMenu = () => {
10 | const { anchorE1, myInfo } = useSelector((state) => state.service);
11 |
12 | const [logoutMe, logoutMeData] = useLogoutMeMutation();
13 |
14 | const dispatch = useDispatch();
15 |
16 | const handleClose = () => {
17 | dispatch(toggleMainMenu(null));
18 | };
19 |
20 | const handleToggleTheme = () => {
21 | handleClose();
22 | dispatch(toggleColorMode());
23 | };
24 |
25 | const handleLogout = async () => {
26 | handleClose();
27 | await logoutMe();
28 | };
29 |
30 | useEffect(() => {
31 | if (logoutMeData.isSuccess) {
32 | toast.warning(logoutMeData.data.msg, {
33 | position: "top-center",
34 | autoClose: 2500,
35 | hideProgressBar: false,
36 | closeOnClick: true,
37 | pauseOnHover: true,
38 | draggable: true,
39 | theme: "colored",
40 | transition: Bounce,
41 | });
42 | }
43 | if (logoutMeData.isError) {
44 | toast.error(logoutMeData.error.data.msg, {
45 | position: "top-center",
46 | autoClose: 2500,
47 | hideProgressBar: false,
48 | closeOnClick: true,
49 | pauseOnHover: true,
50 | draggable: true,
51 | theme: "colored",
52 | transition: Bounce,
53 | });
54 | }
55 | }, [logoutMeData.isSuccess, logoutMeData.isError]);
56 |
57 | return (
58 | <>
59 |
72 | >
73 | );
74 | };
75 |
76 | export default MainMenu;
77 |
--------------------------------------------------------------------------------
/client/src/components/home/Post.jsx:
--------------------------------------------------------------------------------
1 | import { Stack, Typography, useMediaQuery } from "@mui/material";
2 | import { IoIosMore } from "react-icons/io";
3 | import PostOne from "./post/PostOne";
4 | import PostTwo from "./post/PostTwo";
5 | import { useDispatch, useSelector } from "react-redux";
6 | import { addPostId, toggleMyMenu } from "../../redux/slice";
7 | import { useEffect, useState } from "react";
8 |
9 | const Post = ({ e }) => {
10 | const { darkMode, myInfo } = useSelector((state) => state.service);
11 |
12 | const [isAdmin, setIsAdmin] = useState();
13 |
14 | const _300 = useMediaQuery("(min-width:300px)");
15 | const _400 = useMediaQuery("(min-width:400px)");
16 | const _700 = useMediaQuery("(min-width:700px)");
17 |
18 | const dispatch = useDispatch();
19 |
20 | const handleOpenMenu = (event) => {
21 | dispatch(addPostId(e._id));
22 | dispatch(toggleMyMenu(event.currentTarget));
23 | };
24 |
25 | const checkIsAdmin = () => {
26 | if (e?.admin._id === myInfo._id) {
27 | setIsAdmin(true);
28 | return;
29 | }
30 | setIsAdmin(false);
31 | };
32 |
33 | useEffect(() => {
34 | if (e && myInfo) {
35 | checkIsAdmin();
36 | }
37 | }, [e, myInfo]);
38 |
39 | return (
40 | <>
41 |
56 |
57 |
58 |
59 |
60 |
66 |
73 | 24h
74 |
75 | {isAdmin ? (
76 |
77 | ) : (
78 |
79 | )}
80 |
81 |
82 | >
83 | );
84 | };
85 |
86 | export default Post;
87 |
--------------------------------------------------------------------------------
/client/src/pages/Protected/SinglePost.jsx:
--------------------------------------------------------------------------------
1 | import { Stack, TextField } from "@mui/material";
2 | import Post from "../../components/home/Post";
3 | import Comments from "../../components/home/post/Comments";
4 | import { useEffect, useState } from "react";
5 | import { useParams } from "react-router-dom";
6 | import { useAddCommentMutation, useSinglePostQuery } from "../../redux/service";
7 | import { Bounce, toast } from "react-toastify";
8 |
9 | const SinglePost = () => {
10 | const params = useParams();
11 |
12 | const [comment, setComment] = useState("");
13 |
14 | const { data, refetch } = useSinglePostQuery(params?.id);
15 | const [addComment, addCommentData] = useAddCommentMutation();
16 |
17 | const handleAddComment = async (e) => {
18 | if (data && e.key === "Enter") {
19 | const info = {
20 | id: data.post._id,
21 | text: comment,
22 | };
23 | await addComment(info);
24 | }
25 | };
26 |
27 | useEffect(() => {
28 | if (addCommentData.isSuccess) {
29 | setComment();
30 | refetch();
31 | toast.success(addCommentData.data.msg, {
32 | position: "top-center",
33 | autoClose: 2500,
34 | hideProgressBar: false,
35 | closeOnClick: true,
36 | pauseOnHover: true,
37 | draggable: true,
38 | theme: "colored",
39 | transition: Bounce,
40 | });
41 | }
42 | if (addCommentData.isError) {
43 | toast.error(addCommentData.error.data.msg, {
44 | position: "top-center",
45 | autoClose: 2500,
46 | hideProgressBar: false,
47 | closeOnClick: true,
48 | pauseOnHover: true,
49 | draggable: true,
50 | theme: "colored",
51 | transition: Bounce,
52 | });
53 | }
54 | }, [addCommentData.isSuccess, addCommentData.isError]);
55 |
56 | return (
57 | <>
58 |
59 |
60 |
61 | {data
62 | ? data.post?.comments?.length > 0
63 | ? data.post.comments.map((e) => {
64 | return ;
65 | })
66 | : null
67 | : null}
68 |
69 |
80 | >
81 | );
82 | };
83 |
84 | export default SinglePost;
85 |
--------------------------------------------------------------------------------
/client/src/components/search/SearchInput.jsx:
--------------------------------------------------------------------------------
1 | import { InputAdornment, TextField } from "@mui/material";
2 | import { useEffect, useState } from "react";
3 | import { FaSearch } from "react-icons/fa";
4 | import { useDispatch, useSelector } from "react-redux";
5 | import { useLazySearchUsersQuery } from "../../redux/service";
6 | import { addToSearchedUsers } from "../../redux/slice";
7 | import { Bounce, toast } from "react-toastify";
8 |
9 | const SearchInput = () => {
10 | const { darkMode } = useSelector((state) => state.service);
11 |
12 | const [query, setQuery] = useState();
13 |
14 | const [searchUser, searchUserData] = useLazySearchUsersQuery();
15 |
16 | const dispatch = useDispatch();
17 |
18 | const handleSearch = async (e) => {
19 | if (query && e.key === "Enter") {
20 | await searchUser(query);
21 | }
22 | };
23 |
24 | useEffect(() => {
25 | if (searchUserData.isSuccess) {
26 | dispatch(addToSearchedUsers(searchUserData.data.users));
27 | toast.success(searchUserData.data.msg, {
28 | position: "top-center",
29 | autoClose: 2500,
30 | hideProgressBar: false,
31 | closeOnClick: true,
32 | pauseOnHover: true,
33 | draggable: true,
34 | theme: "colored",
35 | transition: Bounce,
36 | });
37 | }
38 | if (searchUserData.isError) {
39 | toast.success(searchUserData.error.data.msg, {
40 | position: "top-center",
41 | autoClose: 2500,
42 | hideProgressBar: false,
43 | closeOnClick: true,
44 | pauseOnHover: true,
45 | draggable: true,
46 | theme: "colored",
47 | transition: Bounce,
48 | });
49 | }
50 | }, [searchUserData.isSuccess, searchUserData.isError]);
51 |
52 | return (
53 | <>
54 |
78 |
79 |
80 | ),
81 | }}
82 | onChange={(e) => setQuery(e.target.value)}
83 | onKeyUp={handleSearch}
84 | />
85 | >
86 | );
87 | };
88 | export default SearchInput;
89 |
--------------------------------------------------------------------------------
/client/src/components/common/Header.jsx:
--------------------------------------------------------------------------------
1 | import { Grid, Stack, useMediaQuery } from "@mui/material";
2 | import Navbar from "./Navbar";
3 | import { IoMenu } from "react-icons/io5";
4 | import { useDispatch, useSelector } from "react-redux";
5 | import { toggleMainMenu } from "../../redux/slice";
6 |
7 | const Header = () => {
8 | const { darkMode } = useSelector((state) => state.service);
9 | const _700 = useMediaQuery("(min-width:700px)");
10 |
11 | const dispatch = useDispatch();
12 |
13 | const handleOpenMenu = (e) => {
14 | dispatch(toggleMainMenu(e.currentTarget));
15 | };
16 |
17 | return (
18 | <>
19 | {_700 ? (
20 |
29 | {darkMode ? (
30 |
36 | ) : (
37 |
43 | )}
44 |
51 |
52 |
53 |
59 |
60 | ) : (
61 | <>
62 |
72 |
73 |
74 |
81 |
82 |
88 |
89 |
90 |
91 | >
92 | )}
93 | >
94 | );
95 | };
96 |
97 | export default Header;
98 |
--------------------------------------------------------------------------------
/client/src/components/common/Navbar.jsx:
--------------------------------------------------------------------------------
1 | import { Stack, useMediaQuery } from "@mui/material";
2 | import { GoHome } from "react-icons/go";
3 | import { IoIosSearch } from "react-icons/io";
4 | import { TbEdit } from "react-icons/tb";
5 | import { CiHeart } from "react-icons/ci";
6 | import { RxAvatar } from "react-icons/rx";
7 | import { FiArrowLeft } from "react-icons/fi";
8 | import { Link, useNavigate } from "react-router-dom";
9 | import { useDispatch, useSelector } from "react-redux";
10 | import { addPostModal } from "../../redux/slice";
11 | import { useEffect, useState } from "react";
12 |
13 | const Navbar = () => {
14 | const { darkMode, myInfo } = useSelector((state) => state.service);
15 |
16 | const _300 = useMediaQuery("(min-width:300px)");
17 | const _700 = useMediaQuery("(min-width:700px)");
18 |
19 | const dispatch = useDispatch();
20 | const navigate = useNavigate();
21 |
22 | const [showArrow, setShowArrow] = useState(false);
23 |
24 | const checkArrow = () => {
25 | if (window.location.pathname.includes("/post/") && _700) {
26 | setShowArrow(true);
27 | return;
28 | }
29 | setShowArrow(false);
30 | };
31 |
32 | const handleAddPost = () => {
33 | dispatch(addPostModal(true));
34 | };
35 |
36 | const handleNavigate = () => {
37 | navigate(-1);
38 | };
39 |
40 | useEffect(() => {
41 | checkArrow();
42 | }, [window.location.pathname]);
43 |
44 | return (
45 | <>
46 |
52 | {showArrow ? (
53 |
59 | ) : null}
60 |
61 |
62 |
63 |
64 |
68 |
69 |
75 |
76 |
77 |
81 |
82 |
83 | >
84 | );
85 | };
86 |
87 | export default Navbar;
88 |
--------------------------------------------------------------------------------
/server/controllers/comment-controller.js:
--------------------------------------------------------------------------------
1 | const User = require("../models/user-model");
2 | const Post = require("../models/post-model");
3 | const Comment = require("../models/comment-model");
4 | const mongoose = require("mongoose");
5 |
6 | exports.addComment = async (req, res) => {
7 | try {
8 | const { id } = req.params;
9 | const { text } = req.body;
10 | if (!id) {
11 | return res.status(400).json({ msg: "id is required !" });
12 | }
13 | if (!text) {
14 | return res.status(400).json({ msg: "No comment is added !" });
15 | }
16 | const postExists = await Post.findById(id);
17 | if (!postExists) {
18 | return res.status(400).json({ msg: "No such post !" });
19 | }
20 | const comment = new Comment({
21 | text,
22 | admin: req.user._id,
23 | post: postExists._id,
24 | });
25 | const newComment = await comment.save();
26 | await Post.findByIdAndUpdate(
27 | id,
28 | {
29 | $push: { comments: newComment._id },
30 | },
31 | { new: true }
32 | );
33 | await User.findByIdAndUpdate(
34 | req.user._id,
35 | {
36 | $push: { replies: newComment._id },
37 | },
38 | { new: true }
39 | );
40 | res.status(201).json({ msg: "Commented !" });
41 | } catch (err) {
42 | res.status(400).json({ msg: "Error in addComment !", err: err.message });
43 | }
44 | };
45 |
46 | exports.deleteComment = async (req, res) => {
47 | try {
48 | const { postId, id } = req.params;
49 | if (!postId || !id) {
50 | return res.status(400).json({ msg: "Error in deleteComment !" });
51 | }
52 | const postExists = await Post.findById(postId);
53 | if (!postExists) {
54 | return res.status(400).json({ msg: "No such post !" });
55 | }
56 | const commentExists = await Comment.findById(id);
57 | if (!commentExists) {
58 | return res.status(400).json({ msg: "No such comment !" });
59 | }
60 | const newId = new mongoose.Types.ObjectId(id);
61 | if (postExists.comments.includes(newId)) {
62 | const id1 = commentExists.admin._id.toString();
63 | const id2 = req.user._id.toString();
64 | if (id1 !== id2) {
65 | return res
66 | .status(400)
67 | .json({ msg: "You are not authorized to delete the comment !" });
68 | }
69 | await Post.findByIdAndUpdate(
70 | postId,
71 | {
72 | $pull: { comments: id },
73 | },
74 | { new: true }
75 | );
76 | await User.findByIdAndUpdate(
77 | req.user._id,
78 | {
79 | $pull: { replies: id },
80 | },
81 | { new: true }
82 | );
83 | await Comment.findByIdAndDelete(id);
84 | return res.status(201).json({ msg: "Comment deleted !" });
85 | }
86 | res.status(201).json({ msg: "This post does not include the comment !" });
87 | } catch (err) {
88 | res.status(400).json({ msg: "Error in deleteComment", err: err.message });
89 | }
90 | };
91 |
--------------------------------------------------------------------------------
/client/src/redux/slice.js:
--------------------------------------------------------------------------------
1 | import { createSlice } from "@reduxjs/toolkit";
2 |
3 | export const serviceSlice = createSlice({
4 | name: "service",
5 | initialState: {
6 | openAddPostModal: false,
7 | openEditProfileModal: false,
8 | anchorE1: null,
9 | anchorE2: null,
10 | darkMode: false,
11 | myInfo: null,
12 | user: {},
13 | allPosts: [],
14 | postId: null,
15 | searchedUsers: [],
16 | },
17 | reducers: {
18 | addPostModal: (state, action) => {
19 | state.openAddPostModal = action.payload;
20 | },
21 | editProfileModal: (state, action) => {
22 | state.openEditProfileModal = action.payload;
23 | },
24 | toggleMainMenu: (state, action) => {
25 | state.anchorE1 = action.payload;
26 | },
27 | toggleMyMenu: (state, action) => {
28 | state.anchorE2 = action.payload;
29 | },
30 | toggleColorMode: (state) => {
31 | state.darkMode = !state.darkMode;
32 | },
33 | addMyInfo: (state, action) => {
34 | state.myInfo = action.payload.me;
35 | },
36 | addUser: (state, action) => {
37 | state.user = action.payload;
38 | },
39 |
40 | addSingle: (state, action) => {
41 | let newArr = [...state.allPosts];
42 | let updatedArr = [action.payload.newPost, ...newArr];
43 | let uniqueArr = new Set();
44 | let uniquePosts = updatedArr.filter((e) => {
45 | if (!uniqueArr.has(e._id)) {
46 | uniqueArr.add(e);
47 | return true;
48 | }
49 | return false;
50 | });
51 | state.allPosts = [...uniquePosts];
52 | },
53 | addToAllPost: (state, action) => {
54 | const newPostArr = [...action.payload.posts];
55 | if (state.allPosts.length === 0) {
56 | state.allPosts = newPostArr;
57 | return;
58 | }
59 | const existingPosts = [...state.allPosts];
60 | newPostArr.forEach((e) => {
61 | const existingIndex = existingPosts.findIndex((i) => {
62 | return i._id === e._id;
63 | });
64 | if (existingIndex !== -1) {
65 | existingPosts[existingIndex] = e;
66 | } else {
67 | existingPosts.push(e);
68 | }
69 | });
70 | state.allPosts = existingPosts;
71 | },
72 | deleteThePost: (state, action) => {
73 | let postArr = [...state.allPosts];
74 | let newArr = postArr.filter((e) => e._id !== state.postId);
75 | state.allPosts = newArr;
76 | },
77 | addPostId:(state,action)=>{
78 | state.postId = action.payload;
79 | },
80 |
81 | addToSearchedUsers: (state, action) => {
82 | state.searchedUsers = action.payload;
83 | },
84 | },
85 | });
86 |
87 | export const {
88 | addPostId,
89 | addPostModal,
90 | editProfileModal,
91 | toggleMainMenu,
92 | toggleMyMenu,
93 | toggleColorMode,
94 | addMyInfo,
95 | addUser,
96 | addSingle,
97 | addToAllPost,
98 | deleteThePost,
99 | addToSearchedUsers,
100 | } = serviceSlice.actions;
101 |
102 | export default serviceSlice.reducer;
103 |
--------------------------------------------------------------------------------
/client/src/components/home/post/PostOne.jsx:
--------------------------------------------------------------------------------
1 | import {
2 | Avatar,
3 | AvatarGroup,
4 | Badge,
5 | Stack,
6 | Stepper,
7 | useMediaQuery,
8 | } from "@mui/material";
9 | import { Link } from "react-router-dom";
10 |
11 | const PostOne = ({ e }) => {
12 | const _700 = useMediaQuery("(min-width:700px)");
13 |
14 | return (
15 | <>
16 |
21 |
22 |
38 | {" "}
39 | +{" "}
40 |
41 | }
42 | >
43 |
48 |
49 |
50 |
56 |
65 | {e ? (
66 | e.comments.length > 0 ? (
67 |
77 |
81 | {e.comments.length > 1 ? (
82 |
86 | ) : null}
87 |
88 | ) : (
89 | ""
90 | )
91 | ) : (
92 | ""
93 | )}
94 |
95 |
96 | >
97 | );
98 | };
99 |
100 | export default PostOne;
101 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: Thread Workflow
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 |
8 | jobs:
9 | check-changes:
10 | runs-on: ubuntu-latest
11 | outputs:
12 | client_changed: ${{ steps.client_changed.outputs.client }}
13 | server_changed: ${{ steps.server_changed.outputs.server }}
14 | steps:
15 | - name: Checkout code
16 | uses: actions/checkout@v4
17 |
18 | - name: Check for changes in client
19 | id: client_changed
20 | uses: dorny/paths-filter@v3
21 | with:
22 | filters: |
23 | client:
24 | - 'client/**'
25 |
26 | - name: Check for changes in server
27 | id: server_changed
28 | uses: dorny/paths-filter@v3
29 | with:
30 | filters: |
31 | server:
32 | - 'server/**'
33 |
34 | client:
35 | needs: check-changes
36 | runs-on: ubuntu-latest
37 | if: needs.check-changes.outputs.client_changed == 'true'
38 | steps:
39 | - name: Checkout code
40 | uses: actions/checkout@v4
41 |
42 | - name: Creating Docker Image
43 | run: |
44 | cd client
45 | docker build -t meabhisingh/thread-client .
46 |
47 | - name: Pushing Docker Image
48 | env:
49 | DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
50 | DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
51 | run: |
52 | echo $DOCKER_PASSWORD | docker login -u $DOCKER_USERNAME --password-stdin
53 | docker push meabhisingh/thread-client
54 | docker logout
55 |
56 | - name: SSH login and Deploy
57 | env:
58 | SSH_USERNAME: ${{ secrets.SSH_USERNAME }}
59 | SSH_PASSWORD: ${{ secrets.SSH_PASSWORD }}
60 | SSH_HOST: ${{ secrets.SSH_HOST }}
61 | run: |
62 | sudo apt-get update -y
63 | sudo apt-get install sshpass
64 | sudo sshpass -p $SSH_PASSWORD ssh -o StrictHostKeyChecking=no $SSH_USERNAME@$SSH_HOST '
65 | sudo docker pull meabhisingh/thread-client &&
66 | cd /root/app &&
67 | sudo docker compose up -d --no-deps --build client && exit
68 | '
69 |
70 | server:
71 | needs: check-changes
72 | runs-on: ubuntu-latest
73 | if: needs.check-changes.outputs.server_changed == 'true'
74 | steps:
75 | - name: Checkout code
76 | uses: actions/checkout@v4
77 |
78 | - name: Creating Docker Image
79 | run: |
80 | cd client
81 | docker build -t meabhisingh/thread-server .
82 |
83 | - name: Pushing Docker Image
84 | env:
85 | DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
86 | DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
87 | run: |
88 | echo $DOCKER_PASSWORD | docker login -u $DOCKER_USERNAME --password-stdin
89 | docker push meabhisingh/thread-server
90 | docker logout
91 |
92 | - name: SSH login and Deploy
93 | env:
94 | SSH_USERNAME: ${{ secrets.SSH_USERNAME }}
95 | SSH_PASSWORD: ${{ secrets.SSH_PASSWORD }}
96 | SSH_HOST: ${{ secrets.SSH_HOST }}
97 | run: |
98 | sudo apt-get update -y
99 | sudo apt-get install sshpass
100 | sudo sshpass -p $SSH_PASSWORD ssh -o StrictHostKeyChecking=no $SSH_USERNAME@$SSH_HOST '
101 | sudo docker pull meabhisingh/thread-server &&
102 | cd /root/app &&
103 | sudo docker compose up -d --no-deps --build server && exit
104 | '
105 |
--------------------------------------------------------------------------------
/client/src/components/home/post/Comments.jsx:
--------------------------------------------------------------------------------
1 | import {
2 | Avatar,
3 | Menu,
4 | MenuItem,
5 | Stack,
6 | Typography,
7 | useMediaQuery,
8 | } from "@mui/material";
9 | import { useEffect, useState } from "react";
10 | import { IoIosMore } from "react-icons/io";
11 | import { useSelector } from "react-redux";
12 | import {
13 | useDeleteCommentMutation,
14 | useSinglePostQuery,
15 | } from "../../../redux/service";
16 | import { Bounce, toast } from "react-toastify";
17 | import { FaBedPulse } from "react-icons/fa6";
18 |
19 | const Comments = ({ e, postId }) => {
20 | const { darkMode, myInfo } = useSelector((state) => state.service);
21 |
22 | const [anchorEl, setAnchorEl] = useState(null);
23 | const [isAdmin, setIsAdmin] = useState();
24 |
25 | const _700 = useMediaQuery("(min-width:700px)");
26 |
27 | const [deleteComment, deleteCommentData] = useDeleteCommentMutation();
28 | const { refetch } = useSinglePostQuery(postId);
29 |
30 | const handleClose = () => {
31 | setAnchorEl(null);
32 | };
33 |
34 | const handleDeleteComment = async () => {
35 | const info = {
36 | postId,
37 | id: e?._id,
38 | };
39 | await deleteComment(info);
40 | handleClose();
41 | refetch();
42 | };
43 |
44 | const checkIsAdmin = () => {
45 | if (e && myInfo) {
46 | if (e.admin._id === myInfo._id) {
47 | setIsAdmin(true);
48 | return;
49 | }
50 | }
51 | setIsAdmin(false);
52 | };
53 |
54 | useEffect(() => {
55 | checkIsAdmin();
56 | }, []);
57 |
58 | useEffect(() => {
59 | if (deleteCommentData.isSuccess) {
60 | toast.success(deleteCommentData.data.msg, {
61 | position: "top-center",
62 | autoClose: 2500,
63 | hideProgressBar: false,
64 | closeOnClick: true,
65 | pauseOnHover: true,
66 | draggable: true,
67 | theme: "colored",
68 | transition: Bounce,
69 | });
70 | }
71 | if (deleteCommentData.isError) {
72 | toast.error(deleteCommentData.error.data.msg, {
73 | position: "top-center",
74 | autoClose: 2500,
75 | hideProgressBar: false,
76 | closeOnClick: true,
77 | pauseOnHover: true,
78 | draggable: true,
79 | theme: "colored",
80 | transition: Bounce,
81 | });
82 | }
83 | }, [deleteCommentData.isSuccess, deleteCommentData.isError]);
84 |
85 | return (
86 | <>
87 |
96 |
97 |
101 |
102 |
103 | {e ? e.admin.userName : ""}
104 |
105 |
106 | {e ? e.text : ""}
107 |
108 |
109 |
110 |
117 | 24min
118 | {isAdmin ? (
119 | setAnchorEl(e.currentTarget)}
123 | />
124 | ) : (
125 |
126 | )}
127 |
128 |
129 |
138 | >
139 | );
140 | };
141 | export default Comments;
142 |
--------------------------------------------------------------------------------
/client/src/pages/Register.jsx:
--------------------------------------------------------------------------------
1 | import {
2 | Button,
3 | Stack,
4 | TextField,
5 | Typography,
6 | useMediaQuery,
7 | } from "@mui/material";
8 | import { useEffect, useState } from "react";
9 | import { useLoginMutation, useSigninMutation } from "../redux/service";
10 | import { Bounce, toast } from "react-toastify";
11 | import Loading from "../components/common/Loading";
12 |
13 | const Register = () => {
14 | const _700 = useMediaQuery("(min-width:700px)");
15 |
16 | const [signinUser, signinUserData] = useSigninMutation();
17 | const [loginUser, loginUserData] = useLoginMutation();
18 |
19 | const [login, setLogin] = useState(false);
20 | const [userName, setUserName] = useState("");
21 | const [email, setEmail] = useState("");
22 | const [password, setPassword] = useState("");
23 |
24 | const toggleLogin = () => {
25 | setLogin((pre) => !pre);
26 | };
27 |
28 | const handleLogin = async () => {
29 | const data = {
30 | email,
31 | password,
32 | };
33 | await loginUser(data);
34 | };
35 |
36 | const handleRegister = async () => {
37 | const data = {
38 | userName,
39 | email,
40 | password,
41 | };
42 | await signinUser(data);
43 | };
44 |
45 | useEffect(() => {
46 | if (signinUserData.isSuccess) {
47 | toast.success(signinUserData.data.msg, {
48 | position: "top-center",
49 | autoClose: 2500,
50 | hideProgressBar: false,
51 | closeOnClick: true,
52 | pauseOnHover: true,
53 | draggable: true,
54 | theme: "colored",
55 | transition: Bounce,
56 | });
57 | }
58 | if (signinUserData.isError) {
59 | toast.error(signinUserData.error.data.msg, {
60 | position: "top-center",
61 | autoClose: 2500,
62 | hideProgressBar: false,
63 | closeOnClick: true,
64 | pauseOnHover: true,
65 | draggable: true,
66 | theme: "colored",
67 | transition: Bounce,
68 | });
69 | }
70 | }, [signinUserData.isSuccess, signinUserData.isError]);
71 |
72 | useEffect(() => {
73 | if (loginUserData.isSuccess) {
74 | toast.success(loginUserData.data.msg, {
75 | position: "top-center",
76 | autoClose: 2500,
77 | hideProgressBar: false,
78 | closeOnClick: true,
79 | pauseOnHover: true,
80 | draggable: true,
81 | theme: "colored",
82 | transition: Bounce,
83 | });
84 | }
85 | if (loginUserData.isError) {
86 | toast.error(loginUserData.error.data.msg, {
87 | position: "top-center",
88 | autoClose: 2500,
89 | hideProgressBar: false,
90 | closeOnClick: true,
91 | pauseOnHover: true,
92 | draggable: true,
93 | theme: "colored",
94 | transition: Bounce,
95 | });
96 | }
97 | }, [loginUserData.isSuccess, loginUserData.isError]);
98 |
99 | if (signinUserData.isLoading || loginUserData.isLoading) {
100 | return (
101 |
102 |
103 |
104 | );
105 | }
106 |
107 | return (
108 | <>
109 |
125 |
131 |
137 | {login ? " Login with email" : " Register with email"}
138 |
139 | {login ? null : (
140 | setUserName(e.target.value)}
144 | />
145 | )}
146 | setEmail(e.target.value)}
150 | />
151 | setPassword(e.target.value)}
155 | />
156 |
173 |
178 | {login ? "Don`t have an account ?" : " Already have an accout ?"}
179 |
180 | {" "}
181 | {login ? "Sign up" : "Login"}
182 |
183 |
184 |
185 |
186 | >
187 | );
188 | };
189 |
190 | export default Register;
191 |
--------------------------------------------------------------------------------
/client/src/redux/service.js:
--------------------------------------------------------------------------------
1 | import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
2 | import {
3 | addMyInfo,
4 | addSingle,
5 | addToAllPost,
6 | addUser,
7 | deleteThePost,
8 | } from "./slice";
9 |
10 | export const serviceApi = createApi({
11 | reducerPath: "serviceApi",
12 | baseQuery: fetchBaseQuery({
13 | baseUrl: `${SERVER_URL}/api/`,
14 | credentials: "include",
15 | }),
16 | keepUnusedDataFor: 60 * 60 * 24 * 7,
17 | tagTypes: ["Post", "User", "Me"],
18 | endpoints: (builder) => ({
19 | signin: builder.mutation({
20 | query: (data) => ({
21 | url: "signin",
22 | method: "POST",
23 | body: data,
24 | }),
25 | invalidateTags: ["Me"],
26 | }),
27 | login: builder.mutation({
28 | query: (data) => ({
29 | url: "login",
30 | method: "POST",
31 | body: data,
32 | }),
33 | invalidatesTags: ["Me"],
34 | }),
35 | myInfo: builder.query({
36 | query: () => ({
37 | url: "me",
38 | method: "GET",
39 | }),
40 | providesTags: ["Me"],
41 | async onQueryStarted(params, { dispatch, queryFulfilled }) {
42 | try {
43 | const { data } = await queryFulfilled;
44 | dispatch(addMyInfo(data));
45 | } catch (err) {
46 | console.log(err);
47 | }
48 | },
49 | }),
50 | logoutMe: builder.mutation({
51 | query: () => ({
52 | url: "logout",
53 | method: "POST",
54 | }),
55 | invalidatesTags: ["Me"],
56 | }),
57 | userDetails: builder.query({
58 | query: (id) => ({
59 | url: `user/${id}`,
60 | method: "GET",
61 | }),
62 | providesTags: ["User"],
63 | async onQueryStarted(params, { dispatch, queryFulfilled }) {
64 | try {
65 | const { data } = await queryFulfilled;
66 | dispatch(addUser(data));
67 | } catch (err) {
68 | console.log(err);
69 | }
70 | },
71 | }),
72 | searchUsers: builder.query({
73 | query: (query) => ({
74 | url: `users/search/${query}`,
75 | method: "GET",
76 | }),
77 | }),
78 | followUser: builder.mutation({
79 | query: (id) => ({
80 | url: `user/follow/${id}`,
81 | method: "PUT",
82 | }),
83 | invalidatesTags: (result, error, { id }) => [{ type: "User", id }],
84 | }),
85 | updateProfile: builder.mutation({
86 | query: (data) => ({
87 | url: "update",
88 | method: "PUT",
89 | body: data,
90 | }),
91 | invalidatesTags: ["Me"],
92 | }),
93 |
94 | addPost: builder.mutation({
95 | query: (data) => ({
96 | url: `post`,
97 | method: "POST",
98 | body: data,
99 | }),
100 | invalidatesTags: ["Post"],
101 | async onQueryStarted(params, { dispatch, queryFulfilled }) {
102 | try {
103 | const { data } = await queryFulfilled;
104 | dispatch(addSingle(data));
105 | } catch (err) {
106 | console.log(err);
107 | }
108 | },
109 | }),
110 | allPost: builder.query({
111 | query: (page) => ({
112 | url: `post?page=${page}`,
113 | method: "GET",
114 | }),
115 | providesTags: (result) => {
116 | return result
117 | ? [
118 | ...result.posts.map(({ _id }) => ({ type: "Post", id: _id })),
119 | { type: "Post", id: "LIST" },
120 | ]
121 | : [{ type: "Post", id: "LIST" }];
122 | },
123 | async onQueryStarted(params, { dispatch, queryFulfilled }) {
124 | try {
125 | const { data } = await queryFulfilled;
126 | dispatch(addToAllPost(data));
127 | } catch (err) {
128 | console.log(err);
129 | }
130 | },
131 | }),
132 | deletePost: builder.mutation({
133 | query: (id) => ({
134 | url: `post/${id}`,
135 | method: "DELETE",
136 | }),
137 | async onQueryStarted(params, { dispatch, queryFulfilled }) {
138 | try {
139 | const { data } = await queryFulfilled;
140 | dispatch(deleteThePost(data));
141 | } catch (err) {
142 | console.log(err);
143 | }
144 | },
145 | }),
146 | likePost: builder.mutation({
147 | query: (id) => ({
148 | url: `post/like/${id}`,
149 | method: "PUT",
150 | }),
151 | invalidatesTags: (result, error, { id }) => [{ type: "Post", id }],
152 | }),
153 | singlePost: builder.query({
154 | query: (id) => ({
155 | url: `post/${id}`,
156 | method: "GET",
157 | }),
158 | providesTags: (result, error, { id }) => [{ type: "Post", id }],
159 | }),
160 | repost: builder.mutation({
161 | query: (id) => ({
162 | url: `repost/${id}`,
163 | method: "PUT",
164 | }),
165 | invalidatesTags: ["User"],
166 | }),
167 |
168 | addComment: builder.mutation({
169 | query: ({ id, ...data }) => ({
170 | url: `comment/${id}`,
171 | method: "POST",
172 | body: data,
173 | }),
174 | invalidatesTags: ["User"],
175 | }),
176 | deleteComment: builder.mutation({
177 | query: ({ postId, id }) => ({
178 | url: `comment/${postId}/${id}`,
179 | method: "DELETE",
180 | }),
181 | invalidatesTags: (result, error, { postId }) => [
182 | { type: "Post", id: postId },
183 | ],
184 | }),
185 | }),
186 | });
187 |
188 | export const {
189 | useSigninMutation,
190 | useLoginMutation,
191 | useMyInfoQuery,
192 | useLogoutMeMutation,
193 | useUserDetailsQuery,
194 | useLazySearchUsersQuery,
195 | useAllPostQuery,
196 | useFollowUserMutation,
197 | useAddCommentMutation,
198 | useAddPostMutation,
199 | useDeleteCommentMutation,
200 | useDeletePostMutation,
201 | useLikePostMutation,
202 | useRepostMutation,
203 | useSinglePostQuery,
204 | useUpdateProfileMutation,
205 | } = serviceApi;
206 |
--------------------------------------------------------------------------------
/client/src/components/home/post/PostTwo.jsx:
--------------------------------------------------------------------------------
1 | import { Stack, Typography, useMediaQuery } from "@mui/material";
2 | import { FaRegHeart, FaRegComment, FaRetweet, FaHeart } from "react-icons/fa6";
3 | import { IoMdSend } from "react-icons/io";
4 | import { useSelector } from "react-redux";
5 | import { Link } from "react-router-dom";
6 | import { useLikePostMutation, useRepostMutation } from "../../../redux/service";
7 | import { useEffect, useState } from "react";
8 | import { Bounce, toast } from "react-toastify";
9 |
10 | const PostTwo = ({ e }) => {
11 | const { darkMode, myInfo } = useSelector((state) => state.service);
12 |
13 | const [likePost] = useLikePostMutation();
14 | const [repost, repostData] = useRepostMutation();
15 |
16 | const [isLiked, setIsLiked] = useState();
17 |
18 | const _300 = useMediaQuery("(min-width:300px)");
19 | const _400 = useMediaQuery("(min-width:400px)");
20 | const _500 = useMediaQuery("(min-width:500px)");
21 | const _700 = useMediaQuery("(min-width:700px)");
22 |
23 | const handleLike = async () => {
24 | await likePost(e?._id);
25 | };
26 |
27 | const checkIsLiked = () => {
28 | if (e?.likes.length > 0) {
29 | const variable = e.likes.filter((ele) => ele._id === myInfo._id);
30 | if (variable.length > 0) {
31 | setIsLiked(true);
32 | return;
33 | }
34 | }
35 | setIsLiked(false);
36 | };
37 |
38 | const handleRepost = async () => {
39 | await repost(e?._id);
40 | };
41 |
42 | useEffect(() => {
43 | checkIsLiked();
44 | }, [e]);
45 |
46 | useEffect(() => {
47 | if (repostData.isSuccess) {
48 | toast.success(repostData.data.msg, {
49 | position: "top-center",
50 | autoClose: 2500,
51 | hideProgressBar: false,
52 | closeOnClick: true,
53 | pauseOnHover: true,
54 | draggable: true,
55 | theme: "colored",
56 | transition: Bounce,
57 | });
58 | }
59 | if (repostData.isError) {
60 | toast.success(repostData.error.data.msg, {
61 | position: "top-center",
62 | autoClose: 2500,
63 | hideProgressBar: false,
64 | closeOnClick: true,
65 | pauseOnHover: true,
66 | draggable: true,
67 | theme: "colored",
68 | transition: Bounce,
69 | });
70 | }
71 | }, [repostData.isSuccess, repostData.isError]);
72 |
73 | return (
74 | <>
75 |
76 |
77 |
78 |
83 | {e ? e.admin.userName : ""}
84 |
85 |
86 |
93 | {e ? e.text : ""}
94 |
95 |
96 |
97 | {e ? (
98 | e.media ? (
99 |
116 | ) : null
117 | ) : null}
118 |
119 |
120 |
121 | {isLiked ? (
122 |
123 | ) : (
124 |
128 | )}
129 |
130 |
131 |
132 |
133 |
137 |
138 |
139 |
146 | {e ? (
147 | e.likes.length > 0 ? (
148 |
153 | {e.likes.length} likes .
154 |
155 | ) : (
156 | ""
157 | )
158 | ) : (
159 | ""
160 | )}
161 | {e ? (
162 | e.comments.length > 0 ? (
163 |
168 | {e.comments.length} comment{" "}
169 |
170 | ) : (
171 | ""
172 | )
173 | ) : (
174 | ""
175 | )}
176 |
177 |
178 |
179 | >
180 | );
181 | };
182 | export default PostTwo;
183 |
--------------------------------------------------------------------------------
/client/src/components/modals/AddPost.jsx:
--------------------------------------------------------------------------------
1 | import {
2 | Avatar,
3 | Box,
4 | Button,
5 | Dialog,
6 | DialogContent,
7 | DialogTitle,
8 | Stack,
9 | Typography,
10 | useMediaQuery,
11 | } from "@mui/material";
12 | import { RxCross2 } from "react-icons/rx";
13 | import { FaImages } from "react-icons/fa";
14 | import { useEffect, useRef, useState } from "react";
15 | import { useDispatch, useSelector } from "react-redux";
16 | import { addPostModal } from "../../redux/slice";
17 | import { useAddPostMutation } from "../../redux/service";
18 | import Loading from "../common/Loading";
19 | import { Bounce, toast } from "react-toastify";
20 |
21 | const AddPost = () => {
22 | const { openAddPostModal, myInfo } = useSelector((state) => state.service);
23 |
24 | const [addNewPost, addNewPostData] = useAddPostMutation();
25 |
26 | const _700 = useMediaQuery("(min-width:700px)");
27 | const _500 = useMediaQuery("(min-width:500px)");
28 | const _300 = useMediaQuery("(min-width:300px)");
29 |
30 | const [text, setText] = useState();
31 | const [media, setMedia] = useState();
32 |
33 | const mediaRef = useRef();
34 | const dispatch = useDispatch();
35 |
36 | const handleClose = () => {
37 | dispatch(addPostModal(false));
38 | };
39 |
40 | const handleMediaRef = () => {
41 | mediaRef.current.click();
42 | };
43 |
44 | const handlePost = async () => {
45 | const data = new FormData();
46 | if (text) {
47 | data.append("text", text);
48 | }
49 | if (media) {
50 | data.append("media", media);
51 | }
52 | await addNewPost(data);
53 | };
54 |
55 | useEffect(() => {
56 | if (addNewPostData.isSuccess) {
57 | setText();
58 | setMedia();
59 | dispatch(addPostModal(false));
60 | toast.success(addNewPostData.data.msg, {
61 | position: "top-center",
62 | autoClose: 2500,
63 | hideProgressBar: false,
64 | closeOnClick: true,
65 | pauseOnHover: true,
66 | draggable: true,
67 | theme: "colored",
68 | transition: Bounce,
69 | });
70 | }
71 | if (addNewPostData.isError) {
72 | toast.error(addNewPostData.error.data.msg, {
73 | position: "top-center",
74 | autoClose: 2500,
75 | hideProgressBar: false,
76 | closeOnClick: true,
77 | pauseOnHover: true,
78 | draggable: true,
79 | theme: "colored",
80 | transition: Bounce,
81 | });
82 | }
83 | }, [addNewPostData.isSuccess, addNewPostData.isError]);
84 |
85 | return (
86 | <>
87 |
180 | >
181 | );
182 | };
183 |
184 | export default AddPost;
185 |
--------------------------------------------------------------------------------
/server/controllers/post-controller.js:
--------------------------------------------------------------------------------
1 | const User = require("../models/user-model");
2 | const Post = require("../models/post-model");
3 | const Comment = require("../models/comment-model");
4 | const cloudinary = require("../config/cloudinary");
5 | const formidable = require("formidable");
6 | const mongoose = require("mongoose");
7 |
8 | exports.addPost = async (req, res) => {
9 | try {
10 | const form = formidable({});
11 | form.parse(req, async (err, fields, files) => {
12 | if (err) {
13 | return res.status(400).json({ msg: "Error in form parse !" });
14 | }
15 | const post = new Post();
16 | if (fields.text) {
17 | post.text = fields.text;
18 | }
19 | if (files.media) {
20 | const uploadedImage = await cloudinary.uploader.upload(
21 | files.media.filepath,
22 | { folder: "Threads_clone_youtube/Posts" }
23 | );
24 | if (!uploadedImage) {
25 | return res.status(400).json({ msg: "Error while uploading Image !" });
26 | }
27 | post.media = uploadedImage.secure_url;
28 | post.public_id = uploadedImage.public_id;
29 | }
30 | post.admin = req.user._id;
31 | const newPost = await post.save();
32 | await User.findByIdAndUpdate(
33 | req.user._id,
34 | {
35 | $push: { threads: newPost._id },
36 | },
37 | { new: true }
38 | );
39 | res.status(201).json({ msg: "Post created !", newPost });
40 | });
41 | } catch (err) {
42 | res.status(400).json({ msg: "Error in addPost !", err: err.message });
43 | }
44 | };
45 |
46 | exports.allPost = async (req, res) => {
47 | try {
48 | const { page } = req.query;
49 | let pageNumber = page;
50 | if (!page || page === undefined) {
51 | pageNumber = 1;
52 | }
53 | const posts = await Post.find({})
54 | .sort({ createdAt: -1 })
55 | .skip((pageNumber - 1) * 3)
56 | .limit(3)
57 | .populate({ path: "admin", select: "-password" })
58 | .populate({ path: "likes", select: "-password" })
59 | .populate({
60 | path: "comments",
61 | populate: {
62 | path: "admin",
63 | model: "user",
64 | },
65 | });
66 | res.status(200).json({ msg: "Post Fetched !", posts });
67 | } catch (err) {
68 | res.status(400).json({ msg: "Error in allPost !", err: err.message });
69 | }
70 | };
71 |
72 | exports.deletePost = async (req, res) => {
73 | try {
74 | const { id } = req.params;
75 | if (!id) {
76 | return res.status(400).json({ msg: "Id is required !" });
77 | }
78 | const postExists = await Post.findById(id);
79 | if (!postExists) {
80 | return res.status(400).json({ msg: "Post not found !" });
81 | }
82 | const userId = req.user._id.toString();
83 | const adminId = postExists.admin._id.toString();
84 | if (userId !== adminId) {
85 | return res
86 | .status(400)
87 | .json({ msg: "You are not authorized to delete this post !" });
88 | }
89 | if (postExists.media) {
90 | await cloudinary.uploader.destroy(
91 | postExists.public_id,
92 | (error, result) => {
93 | console.log({ error, result });
94 | }
95 | );
96 | }
97 | await Comment.deleteMany({ _id: { $in: postExists.comments } });
98 | await User.updateMany(
99 | {
100 | $or: [{ threads: id }, { reposts: id }, { replies: id }],
101 | },
102 | {
103 | $pull: {
104 | threads: id,
105 | reposts: id,
106 | replies: id,
107 | },
108 | },
109 | { new: true }
110 | );
111 | await Post.findByIdAndDelete(id);
112 | res.status(400).json({ msg: "Post deleted !" });
113 | } catch (err) {
114 | res.status(400).json({ msg: "Error in deletePost !", err: err.message });
115 | }
116 | };
117 |
118 | exports.likePost = async (req, res) => {
119 | try {
120 | const { id } = req.params;
121 | if (!id) {
122 | return res.status(400).json({ msg: "Id is required !" });
123 | }
124 | const post = await Post.findById(id);
125 | if (!post) {
126 | return res.status(400).json({ msg: "No such Post !" });
127 | }
128 | if (post.likes.includes(req.user._id)) {
129 | await Post.findByIdAndUpdate(
130 | id,
131 | { $pull: { likes: req.user._id } },
132 | { new: true }
133 | );
134 | return res.status(201).json({ msg: "Post unliked !" });
135 | }
136 | await Post.findByIdAndUpdate(
137 | id,
138 | { $push: { likes: req.user._id } },
139 | { new: true }
140 | );
141 | return res.status(201).json({ msg: "Post liked !" });
142 | } catch (err) {
143 | res.status(400).json({ msg: "Error in likePost !", err: err.message });
144 | }
145 | };
146 |
147 | exports.repost = async (req, res) => {
148 | try {
149 | const { id } = req.params;
150 | if (!id) {
151 | return res.status(400).json({ msg: "Id is needed !" });
152 | }
153 | const post = await Post.findById(id);
154 | if (!post) {
155 | return res.status(400).json({ msg: "No such post !" });
156 | }
157 | const newId = new mongoose.Types.ObjectId(id);
158 | if (req.user.reposts.includes(newId)) {
159 | return res.status(400).json({ msg: "This post is already reposted !" });
160 | }
161 | await User.findByIdAndUpdate(
162 | req.user._id,
163 | {
164 | $push: { reposts: post._id },
165 | },
166 | { new: true }
167 | );
168 | res.status(201).json({ msg: "Reposted !" });
169 | } catch (err) {
170 | res.status(400).json({ msg: "Error in repost !", err: err.message });
171 | }
172 | };
173 |
174 | exports.singlePost = async (req, res) => {
175 | try {
176 | const { id } = req.params;
177 | if (!id) {
178 | return res.status(400).json({ msg: "Id is required !" });
179 | }
180 | const post = await Post.findById(id)
181 | .populate({
182 | path: "admin",
183 | select: "-password",
184 | })
185 | .populate({ path: "likes",select:'-password' })
186 | .populate({
187 | path: "comments",
188 | populate: {
189 | path: "admin",
190 | },
191 | });
192 | res.status(200).json({ msg: "Post Fetched !", post });
193 | } catch (err) {
194 | res.status(400).json({ msg: "Error in singlePost !", err: err.message });
195 | }
196 | };
197 |
--------------------------------------------------------------------------------
/client/src/pages/Protected/profile/ProfileLayout.jsx:
--------------------------------------------------------------------------------
1 | import {
2 | Avatar,
3 | Button,
4 | Chip,
5 | Stack,
6 | Typography,
7 | useMediaQuery,
8 | } from "@mui/material";
9 | import { FaInstagram } from "react-icons/fa";
10 | import { Link, Outlet, useParams } from "react-router-dom";
11 | import { useDispatch, useSelector } from "react-redux";
12 | import { editProfileModal } from "../../../redux/slice";
13 | import {
14 | useFollowUserMutation,
15 | useUserDetailsQuery,
16 | } from "../../../redux/service";
17 | import { useEffect, useState } from "react";
18 | import EditProfile from "../../../components/modals/EditProfile";
19 | import { Bounce, toast } from "react-toastify";
20 | import { Helmet } from "react-helmet-async";
21 |
22 | const ProfileLayout = () => {
23 | const dispatch = useDispatch();
24 | const params = useParams();
25 |
26 | const { data } = useUserDetailsQuery(params?.id);
27 | const [followUser, followUserData] = useFollowUserMutation();
28 |
29 | const { darkMode, myInfo } = useSelector((state) => state.service);
30 |
31 | const [myAccount, setMyAccount] = useState();
32 | const [isFollowing, setIsFollowing] = useState();
33 |
34 | const _300 = useMediaQuery("(min-width:300px)");
35 | const _500 = useMediaQuery("(min-width:500px)");
36 | const _700 = useMediaQuery("(min-width:700px)");
37 |
38 | const checkIsFollowing = () => {
39 | if (data && myInfo) {
40 | const isTrue = data.user.followers.filter((e) => e._id === myInfo._id);
41 | if (isTrue.length > 0) {
42 | setIsFollowing(true);
43 | return;
44 | }
45 | setIsFollowing(false);
46 | }
47 | };
48 |
49 | const checkIsMyAccount = () => {
50 | if (data && myInfo) {
51 | const isTrue = data.user._id === myInfo._id;
52 | setMyAccount(isTrue);
53 | }
54 | };
55 |
56 | const handleFollow = async () => {
57 | if (data) {
58 | await followUser(data.user._id);
59 | }
60 | };
61 |
62 | const handleOpenEditModal = () => {
63 | dispatch(editProfileModal(true));
64 | };
65 |
66 | useEffect(() => {
67 | if (followUserData.isSuccess) {
68 | toast.success(followUserData.data.msg, {
69 | position: "top-center",
70 | autoClose: 2500,
71 | hideProgressBar: false,
72 | closeOnClick: true,
73 | pauseOnHover: true,
74 | draggable: true,
75 | theme: "colored",
76 | transition: Bounce,
77 | });
78 | }
79 | if (followUserData.isError) {
80 | toast.error(followUserData.error.data.msg, {
81 | position: "top-center",
82 | autoClose: 2500,
83 | hideProgressBar: false,
84 | closeOnClick: true,
85 | pauseOnHover: true,
86 | draggable: true,
87 | theme: "colored",
88 | transition: Bounce,
89 | });
90 | }
91 | }, [followUserData.isSuccess, followUserData.isError]);
92 |
93 | useEffect(() => {
94 | checkIsFollowing();
95 | checkIsMyAccount();
96 | }, [data]);
97 |
98 | return (
99 | <>
100 |
101 |
102 | {data
103 | ? data.user
104 | ? data.user.userName + " | Threads Clone"
105 | : "Threads Clone | App by Aditya Jawanjal"
106 | : "Threads Clone | App by Aditya Jawanjal"}
107 |
108 |
109 |
117 |
122 |
123 |
128 | {data ? (data.user ? data.user.userName : "") : ""}
129 |
130 |
131 |
132 | {data ? (data.user ? data.user.email : "") : ""}
133 |
134 |
139 |
140 |
141 |
146 |
147 |
148 | {data ? (data.user ? data.user.bio : "") : ""}
149 |
150 |
155 |
156 | {data
157 | ? data.user
158 | ? data.user.followers.length > 0
159 | ? `${data.user.followers.length} followers`
160 | : "No Followers"
161 | : ""
162 | : ""}
163 |
164 |
165 |
166 |
167 |
184 |
194 |
198 | Threads
199 |
200 |
204 | Replies
205 |
206 |
210 | Reposts
211 |
212 |
213 |
214 |
215 | >
216 | );
217 | };
218 |
219 | export default ProfileLayout;
220 |
--------------------------------------------------------------------------------
/client/src/components/modals/EditProfile.jsx:
--------------------------------------------------------------------------------
1 | import {
2 | Avatar,
3 | Box,
4 | Button,
5 | Dialog,
6 | DialogContent,
7 | DialogTitle,
8 | Stack,
9 | Typography,
10 | useMediaQuery,
11 | } from "@mui/material";
12 | import { useEffect, useRef, useState } from "react";
13 | import { RxCross2 } from "react-icons/rx";
14 | import { useDispatch, useSelector } from "react-redux";
15 | import { editProfileModal } from "../../redux/slice";
16 | import { useParams } from "react-router-dom";
17 | import {
18 | useUpdateProfileMutation,
19 | useUserDetailsQuery,
20 | } from "../../redux/service";
21 | import Loading from "../common/Loading";
22 | import { Bounce, toast } from "react-toastify";
23 |
24 | const EditProfile = () => {
25 | const { openEditProfileModal, myInfo } = useSelector(
26 | (state) => state.service
27 | );
28 | const _700 = useMediaQuery("(min-width:700px)");
29 |
30 | const [pic, setPic] = useState();
31 | const [bio, setBio] = useState();
32 |
33 | const params = useParams();
34 | const imgRef = useRef();
35 | const dispatch = useDispatch();
36 |
37 | const [updateProfile, updateProfileData] = useUpdateProfileMutation();
38 | const { refetch } = useUserDetailsQuery(params?.id);
39 |
40 | const handlePhoto = () => {
41 | imgRef.current.click();
42 | };
43 |
44 | const handleClose = () => {
45 | dispatch(editProfileModal(false));
46 | };
47 |
48 | const handleUpdate = async () => {
49 | if (pic || bio) {
50 | const data = new FormData();
51 | if (bio) {
52 | data.append("text", bio);
53 | }
54 | if (pic) {
55 | data.append("media", pic);
56 | }
57 | await updateProfile(data);
58 | }
59 | dispatch(editProfileModal(false));
60 | };
61 |
62 | useEffect(() => {
63 | if (updateProfileData.isSuccess) {
64 | refetch();
65 | toast.success(updateProfileData.data.msg, {
66 | position: "top-center",
67 | autoClose: 2500,
68 | hideProgressBar: false,
69 | closeOnClick: true,
70 | pauseOnHover: true,
71 | draggable: true,
72 | theme: "colored",
73 | transition: Bounce,
74 | });
75 | }
76 | if (updateProfileData.isError) {
77 | toast.error(updateProfileData.error.data.msg, {
78 | position: "top-center",
79 | autoClose: 2500,
80 | hideProgressBar: false,
81 | closeOnClick: true,
82 | pauseOnHover: true,
83 | draggable: true,
84 | theme: "colored",
85 | transition: Bounce,
86 | });
87 | }
88 | }, [updateProfileData.isError, updateProfileData.isSuccess]);
89 |
90 | return (
91 | <>
92 |
219 | >
220 | );
221 | };
222 |
223 | export default EditProfile;
224 |
--------------------------------------------------------------------------------
/server/controllers/user-controller.js:
--------------------------------------------------------------------------------
1 | const User = require("../models/user-model");
2 | const bcrypt = require("bcrypt");
3 | const jwt = require("jsonwebtoken");
4 | const formidable = require("formidable");
5 | const cloudinary = require("../config/cloudinary");
6 |
7 | exports.signin = async (req, res) => {
8 | try {
9 | const { userName, email, password } = req.body;
10 | if (!userName || !email || !password) {
11 | return res
12 | .status(400)
13 | .json({ msg: "userName , email and password are required !" });
14 | }
15 | const userExists = await User.findOne({ email });
16 | if (userExists) {
17 | return res
18 | .status(400)
19 | .json({ msg: "User is already registerd ! Please Login ." });
20 | }
21 | const hashedPassword = await bcrypt.hash(password, 10);
22 | if (!hashedPassword) {
23 | return res.status(400).json({ msg: "Error in password hashing !" });
24 | }
25 | const user = new User({
26 | userName,
27 | email,
28 | password: hashedPassword,
29 | });
30 | const result = await user.save();
31 | if (!result) {
32 | return res.status(400).json({ msg: "Error while saving user !" });
33 | }
34 | const accesToken = jwt.sign({ token: result._id }, process.env.JWT_SECRET, {
35 | expiresIn: "30d",
36 | });
37 | if (!accesToken) {
38 | return res.status(400).json({ msg: "Error while generating token !" });
39 | }
40 | res.cookie("token", accesToken, {
41 | maxAge: 1000 * 60 * 60 * 24 * 30,
42 | httpOnly: true,
43 | sameSite: "none",
44 | secure: true,
45 | partitioned: true,
46 | });
47 | res
48 | .status(201)
49 | .json({ msg: `User Signed in successfully ! hello ${result?.userName}` });
50 | } catch (err) {
51 | res.status(400).json({ msg: "Error in signin !", err: err.message });
52 | }
53 | };
54 |
55 | exports.login = async (req, res) => {
56 | try {
57 | const { email, password } = req.body;
58 | if (!email || !password) {
59 | return res.status(400).json({ msg: "Email and Password are required !" });
60 | }
61 | const userExists = await User.findOne({ email });
62 | if (!userExists) {
63 | return res.status(400).json({ msg: "Please Signin first !" });
64 | }
65 | console.log(userExists);
66 | const passwordMatched = await bcrypt.compare(password, userExists.password);
67 | if (!passwordMatched) {
68 | return res.status(400).json({ msg: "Incorrect credentials !" });
69 | }
70 | const accessToken = jwt.sign(
71 | { token: userExists._id },
72 | process.env.JWT_SECRET,
73 | { expiresIn: "30d" }
74 | );
75 | if (!accessToken) {
76 | return res.status(400).json({ msg: "Token not gemnerated in login !" });
77 | }
78 | res.cookie("token", accessToken, {
79 | maxAge: 1000 * 60 * 60 * 24 * 30,
80 | httpOnly: true,
81 | secure: true,
82 | sameSite: "none",
83 | partitioned: true,
84 | });
85 | res.status(200).json({ msg: "User logged in succcessfully !" });
86 | } catch (err) {
87 | res.status(400).json({ msg: "Error in login !", err: err.message });
88 | }
89 | };
90 |
91 | exports.userDetails = async (req, res) => {
92 | try {
93 | const { id } = req.params;
94 | if (!id) {
95 | return res.status(400).json({ msg: "id is required !" });
96 | }
97 | const user = await User.findById(id)
98 | .select("-password")
99 | .populate("followers")
100 | .populate({
101 | path: "threads",
102 | populate: [{ path: "likes" }, { path: "comments" }, { path: "admin" }],
103 | })
104 | .populate({ path: "replies", populate: { path: "admin" } })
105 | .populate({
106 | path: "reposts",
107 | populate: [{ path: "likes" }, { path: "comments" }, { path: "admin" }],
108 | });
109 | res.status(200).json({ msg: "User Details Fetched !", user });
110 | } catch (err) {
111 | res.status(400).json({ msg: "Error in userDetails !", err: err.message });
112 | }
113 | };
114 |
115 | exports.followUser = async (req, res) => {
116 | try {
117 | const { id } = req.params;
118 | if (!id) {
119 | return res.status(400).json({ msg: "Id is required !" });
120 | }
121 | const userExists = await User.findById(id);
122 | if (!userExists) {
123 | return res.status(400).json({ msg: "User don`t Exist !" });
124 | }
125 | if (userExists.followers.includes(req.user._id)) {
126 | await User.findByIdAndUpdate(
127 | userExists._id,
128 | {
129 | $pull: { followers: req.user._id },
130 | },
131 | { new: true }
132 | );
133 | return res.status(201).json({ msg: `Unfollowed ${userExists.userName}` });
134 | }
135 | await User.findByIdAndUpdate(
136 | userExists._id,
137 | {
138 | $push: { followers: req.user._id },
139 | },
140 | { new: true }
141 | );
142 | return res.status(201).json({ msg: `Following ${userExists.userName}` });
143 | } catch (err) {
144 | res.status(400).json({ msg: "Error in followUser !", err: err.message });
145 | }
146 | };
147 |
148 | exports.updateProfile = async (req, res) => {
149 | try {
150 | const userExists = await User.findById(req.user._id);
151 | if (!userExists) {
152 | return res.status(400).json({ msg: "No such user !" });
153 | }
154 | const form = formidable({});
155 | form.parse(req, async (err, fields, files) => {
156 | if (err) {
157 | return res.status(400).json({ msg: "Error in formidable !", err: err });
158 | }
159 | if (fields.text) {
160 | await User.findByIdAndUpdate(
161 | req.user._id,
162 | { bio: fields.text },
163 | { new: true }
164 | );
165 | }
166 | if (files.media) {
167 | if (userExists.public_id) {
168 | await cloudinary.uploader.destroy(
169 | userExists.public_id,
170 | (error, result) => {
171 | console.log({ error, result });
172 | }
173 | );
174 | }
175 | const uploadedImage = await cloudinary.uploader.upload(
176 | files.media.filepath,
177 | { folder: "Threads_clone_youtube/Profiles" }
178 | );
179 | if (!uploadedImage) {
180 | return res.status(400).json({ msg: "Error while uploading pic !" });
181 | }
182 | await User.findByIdAndUpdate(
183 | req.user._id,
184 | {
185 | profilePic: uploadedImage.secure_url,
186 | public_id: uploadedImage.public_id,
187 | },
188 | { new: true }
189 | );
190 | }
191 | });
192 | res.status(201).json({ msg: "Profile updated successfully !" });
193 | } catch (err) {
194 | res.status(400).json({ msg: "Error in updateProfile !", err: err.message });
195 | }
196 | };
197 |
198 | exports.searchUser = async (req, res) => {
199 | try {
200 | const { query } = req.params;
201 | const users = await User.find({
202 | $or: [
203 | { userName: { $regex: query, $options: "i" } },
204 | { email: { $regex: query, $options: "i" } },
205 | ],
206 | });
207 | res.status(200).json({ msg: "Searched !", users });
208 | } catch (err) {
209 | res.status(400).json({ msg: "Error in searchUser !", err: err.message });
210 | }
211 | };
212 |
213 | exports.logout = async (req, res) => {
214 | try {
215 | res.cookie("token", "", {
216 | maxAge: 0,
217 | httpOnly: true,
218 | sameSite: "none",
219 | secure: true,
220 | partitioned: true,
221 | });
222 |
223 | res.status(201).json({ msg: "You logged out !" });
224 | } catch (err) {
225 | res.status(400).json({ msg: "Error in logout" });
226 | }
227 | };
228 |
229 | exports.myInfo = async (req, res) => {
230 | try {
231 | res.status(200).json({ me: req.user });
232 | } catch (err) {
233 | res.status(400).json({ msg: "Error in myInfo !" });
234 | }
235 | };
236 |
--------------------------------------------------------------------------------