├── 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 | 60 | Delete 61 | 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 | 66 | Toggle Theme 67 | 68 | My Profile 69 | 70 | Logout 71 | 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 | setComment(e.target.value)} 76 | onKeyUp={handleAddComment} 77 | value={comment ? comment : ""} 78 | /> 79 | 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 | logo 36 | ) : ( 37 | logo 43 | )} 44 | 51 | 52 | 53 | 59 | 60 | ) : ( 61 | <> 62 | 72 | 73 | 74 | 81 | 82 | logo 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 | 136 | Delete 137 | 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 | {e?.media} 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 | 93 | {addNewPostData?.isLoading ? ( 94 | 95 | 96 | 97 | ) : ( 98 | <> 99 | 105 | 106 | 107 | 108 | New Thread... 109 | 110 | 111 | 112 | 116 | 117 | 122 | {myInfo ? myInfo.userName : ""} 123 | 124 |