└── Social-master ├── .gitignore ├── README.MD ├── backend ├── .gitignore ├── @types │ └── express │ │ └── index.d.ts ├── package-lock.json ├── package.json ├── src │ ├── config │ │ ├── users.ts │ │ └── validators.ts │ ├── controllers │ │ ├── auth.ts │ │ ├── conversation.ts │ │ ├── friendRequests.ts │ │ ├── message.ts │ │ ├── messageNotification.ts │ │ ├── post.ts │ │ ├── upload.ts │ │ └── user.ts │ ├── db │ │ └── connectDB.ts │ ├── errors │ │ ├── badRequest.ts │ │ ├── error.ts │ │ ├── index.ts │ │ ├── notFound.ts │ │ └── unauthorized.ts │ ├── index.ts │ ├── middleware │ │ ├── auth.ts │ │ ├── error.ts │ │ └── notFound.ts │ ├── models │ │ ├── comment.ts │ │ ├── conversation.ts │ │ ├── friendRequest.ts │ │ ├── message.ts │ │ ├── messageNotification.ts │ │ ├── post.ts │ │ ├── postNotification.ts │ │ └── user.ts │ └── routes │ │ ├── auth.ts │ │ ├── conversation.ts │ │ ├── friendRequests.ts │ │ ├── message.ts │ │ ├── messageNotification.ts │ │ ├── posts.ts │ │ ├── upload.ts │ │ └── users.ts └── tsconfig.json └── frontend ├── README.md ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── _redirects ├── favicon.ico ├── images │ ├── bricks.jpeg │ └── social-logo.png └── index.html ├── src ├── App.tsx ├── animations │ └── typing.json ├── app │ ├── hooks.ts │ └── store.ts ├── components │ ├── ChatOnline.tsx │ ├── Comment.tsx │ ├── Conversation.tsx │ ├── Feed.tsx │ ├── Friend.tsx │ ├── FriendRequest.tsx │ ├── Message.tsx │ ├── MessageNotification.tsx │ ├── Navbar.tsx │ ├── Online.tsx │ ├── Post.tsx │ ├── PostNotification.tsx │ ├── ProtectedRoute.tsx │ ├── Rightbar.tsx │ ├── RightbarFriend.tsx │ ├── Share.tsx │ ├── SharedLayout.tsx │ ├── Sidebar.tsx │ └── index.ts ├── config │ ├── createRipple.ts │ └── utils.ts ├── context.tsx ├── features │ ├── conversations │ │ └── conversationsSlice.ts │ ├── posts │ │ └── postsSlice.ts │ └── user │ │ └── userSlice.ts ├── friendRequest.css ├── hooks.ts ├── images │ ├── 404.webp │ ├── bricks.jpeg │ ├── confirm.png │ ├── social-logo.png │ └── verify.png ├── index.css ├── index.tsx ├── interfaces.ts ├── messageNotification.css ├── pages │ ├── Confirm.tsx │ ├── Home.tsx │ ├── Login.tsx │ ├── Messanger.tsx │ ├── NotFound.tsx │ ├── Profile.tsx │ ├── ResetPassword.tsx │ ├── SinglePost.tsx │ ├── UpdatePassword.tsx │ ├── Verify.tsx │ └── index.ts ├── react-app-env.d.ts └── sounds │ └── notification.mp3 ├── tailwind.config.js └── tsconfig.json /Social-master/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | ./backend/.env 6 | .env 7 | /.pnp 8 | .pnp.js 9 | build 10 | # testing 11 | /coverage 12 | 13 | # production 14 | /build 15 | 16 | # misc 17 | .DS_Store 18 | .env.local 19 | .env.development.local 20 | .env.test.local 21 | .env.production.local 22 | 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | -------------------------------------------------------------------------------- /Social-master/README.MD: -------------------------------------------------------------------------------- 1 | # Social 2 | 3 | Social Media FullStack Applciation build with MERN 4 | 5 | ## Stack 6 | 7 | 1. Frontend: React with Typescript + Tailwind CSS + ReduxToolkit as a state manager 8 | 2. Backend: Node.js REST api + MongoDB with Mongoose ODM 9 | 3. Backend API hosted on Railway 10 | 4. Frontend hosted on Netlify 11 | 12 | ### Features 13 | 14 | 1. JWT cookies auth. 15 | 2. Create posts, comment and like them. 16 | 3. Search users, add to friends, edit profile info. 17 | 4. Real time chat with other people, realtime notifications, friends requests and online friends. 18 | 5. Reset password & confirm email. 19 | 20 | ### Screenshots 21 | 22 | ![alt text](https://imgur.com/sqajNRq.png 'App Photo') 23 | ![alt text](https://imgur.com/eJZTz24.png 'App Photo') 24 | ![alt text](https://imgur.com/AdDH0pv.png 'App Photo') 25 | ![alt text](https://imgur.com/OICMSzt.png 'App Photo') 26 | ![alt text](https://imgur.com/kCosdUW.png 'App Photo') 27 | ![alt text](https://imgur.com/ewhDDc2.png 'App Photo') 28 | ![alt text](https://imgur.com/nmodIJz.png 'App Photo') 29 | ![alt text](https://imgur.com/svDlCXs.png 'App Photo') 30 | ![alt text](https://imgur.com/rpP37fC.png 'App Photo') 31 | -------------------------------------------------------------------------------- /Social-master/backend/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | -------------------------------------------------------------------------------- /Social-master/backend/@types/express/index.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace Express { 2 | export interface Request { 3 | user: { 4 | id: string; 5 | }; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Social-master/backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "social", 3 | "version": "1.0.0", 4 | "description": "social media app mern stack", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node src/index.js", 8 | "start:dev": "nodemon --watch './**/*.ts' --exec ts-node src/index.ts" 9 | }, 10 | "keywords": [ 11 | "social-media", 12 | "mern", 13 | "react", 14 | "typescript", 15 | "node.js" 16 | ], 17 | "author": "lifeisbeautiful", 18 | "license": "MIT", 19 | "dependencies": { 20 | "@types/socket.io": "^3.0.2", 21 | "bcryptjs": "^2.4.3", 22 | "cloudinary": "^1.30.1", 23 | "colors": "^1.4.0", 24 | "cookie": "^0.5.0", 25 | "cookie-parser": "^1.4.6", 26 | "cors": "^2.8.5", 27 | "dotenv": "^16.0.1", 28 | "express": "^4.18.1", 29 | "express-async-errors": "^3.1.1", 30 | "http-status-codes": "^2.2.0", 31 | "jsonwebtoken": "^8.5.1", 32 | "mailgun-js": "^0.22.0", 33 | "mongoose": "^6.4.6", 34 | "morgan": "^1.10.0", 35 | "multer": "^1.4.5-lts.1", 36 | "socket.io": "^4.5.1", 37 | "streamifier": "^0.1.1" 38 | }, 39 | "devDependencies": { 40 | "@types/bcryptjs": "^2.4.2", 41 | "@types/cookie": "^0.5.1", 42 | "@types/cookie-parser": "^1.4.3", 43 | "@types/express": "^4.17.13", 44 | "@types/jsonwebtoken": "^8.5.8", 45 | "@types/mailgun-js": "^0.22.12", 46 | "@types/morgan": "^1.9.3", 47 | "@types/multer": "^1.4.7", 48 | "@types/node": "^18.6.4", 49 | "@types/streamifier": "^0.1.0", 50 | "nodemon": "^2.0.19", 51 | "ts-node-dev": "^2.0.0", 52 | "typescript": "^4.7.4" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Social-master/backend/src/config/users.ts: -------------------------------------------------------------------------------- 1 | export let users: { 2 | socketId: string; 3 | userId: string; 4 | }[] = []; 5 | 6 | export const addUser = (userId: string, socketId: string) => { 7 | !users.some((user) => user.userId === userId) && 8 | users.push({ userId, socketId }); 9 | }; 10 | 11 | export const removeUser = (socketId: string) => { 12 | users = users.filter((user) => user.socketId !== socketId); 13 | }; 14 | 15 | export const getUser = (userId: string) => { 16 | return users.find((user) => user.userId === userId); 17 | }; 18 | -------------------------------------------------------------------------------- /Social-master/backend/src/config/validators.ts: -------------------------------------------------------------------------------- 1 | interface IErrors { 2 | username?: string; 3 | email?: string; 4 | password?: string; 5 | confirmPassword?: string; 6 | } 7 | 8 | export const validateRegisterInput = ( 9 | username: string, 10 | email: string, 11 | password: string, 12 | confirmPassword: string 13 | ) => { 14 | const errors: IErrors = {}; 15 | if (username.trim() === '') { 16 | errors.username = 'Username must not be empty'; 17 | } 18 | if (email.trim() === '') { 19 | errors.email = 'Email must not be empty'; 20 | } else { 21 | const regEx = 22 | /^([0-9a-zA-Z]([-.\w]*[0-9a-zA-Z])*@([0-9a-zA-Z][-\w]*[0-9a-zA-Z]\.)+[a-zA-Z]{2,9})$/; 23 | if (!email.match(regEx)) { 24 | errors.email = 'Email must be a valid email address'; 25 | } 26 | } 27 | if (password.trim() === '') { 28 | errors.password = 'Password must not be empty'; 29 | } else if (password.length < 6) { 30 | errors.password = 'Password is too short'; 31 | } else if (password !== confirmPassword) { 32 | errors.confirmPassword = 'Passwords do not match'; 33 | } 34 | return { 35 | errors, 36 | valid: Object.keys(errors).length < 1, 37 | }; 38 | }; 39 | 40 | export const validateLoginInput = (username: string, password: string) => { 41 | const errors: IErrors = {}; 42 | if (username.trim() === '') { 43 | errors.username = 'Username must not be empty'; 44 | } 45 | if (password.trim() === '') { 46 | errors.password = 'Password must not be empty'; 47 | } 48 | return { 49 | errors, 50 | valid: Object.keys(errors).length < 1, 51 | }; 52 | }; 53 | -------------------------------------------------------------------------------- /Social-master/backend/src/controllers/auth.ts: -------------------------------------------------------------------------------- 1 | import { StatusCodes } from 'http-status-codes'; 2 | import { 3 | validateRegisterInput, 4 | validateLoginInput, 5 | } from '../config/validators'; 6 | import { Request, Response } from 'express'; 7 | import bcrypt from 'bcryptjs'; 8 | import jwt from 'jsonwebtoken'; 9 | import cookie from 'cookie'; 10 | import mailgun from 'mailgun-js'; 11 | 12 | import User from '../models/user'; 13 | 14 | export const register = async (req: Request, res: Response) => { 15 | let { username, email, password, confirmPassword } = req.body; 16 | 17 | const { errors, valid } = validateRegisterInput( 18 | username, 19 | email, 20 | password, 21 | confirmPassword 22 | ); 23 | if (valid) { 24 | let exist = await User.findOne({ username }); 25 | if (exist) { 26 | return res.status(StatusCodes.BAD_REQUEST).json({ 27 | errors: { 28 | username: `User with username ${username} already exist`, 29 | }, 30 | }); 31 | } 32 | exist = await User.findOne({ email }); 33 | if (exist) { 34 | return res.status(StatusCodes.BAD_REQUEST).json({ 35 | errors: { 36 | email: `User with email ${email} already exist`, 37 | }, 38 | }); 39 | } 40 | const salt = await bcrypt.genSalt(10); 41 | const password = await bcrypt.hash(req.body.password, salt); 42 | 43 | const token = jwt.sign( 44 | { username, email, password }, 45 | process.env.JWT_SECRET as string, 46 | { expiresIn: '20m' } 47 | ); 48 | 49 | const mg = mailgun({ 50 | apiKey: process.env.MAILGUN_API_KEY as string, 51 | domain: process.env.MAILGUN_DOMAIN as string, 52 | }); 53 | 54 | const data = { 55 | from: 'noreply@hello.com', 56 | to: email, 57 | subject: 'Email verification', 58 | html: `

Thank you!

59 |

In order to verify email, please proceed to the following link: 60 | Confirm email 61 |

62 | `, 63 | }; 64 | 65 | mg.messages().send(data, function (error, body) { 66 | if (error) { 67 | console.log(error); 68 | return res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ error }); 69 | } 70 | console.log(body); 71 | return res 72 | .status(StatusCodes.OK) 73 | .json({ message: 'Please verify email' }); 74 | }); 75 | } else { 76 | res.status(StatusCodes.BAD_REQUEST).json({ errors }); 77 | } 78 | }; 79 | 80 | export const login = async (req: Request, res: Response) => { 81 | const { username, password } = req.body; 82 | 83 | const { errors, valid } = validateLoginInput(username, password); 84 | 85 | if (valid) { 86 | let user = await User.findOne({ 87 | username, 88 | }) 89 | .populate('friends', 'username profilePicture') 90 | .populate('friendRequests', 'from to createdAt') 91 | .populate('messageNotifications', 'createdAt conversation from to') 92 | .populate('postNotifications', 'post createdAt user type'); 93 | user = await User.populate(user, { 94 | path: 'friendRequests.from', 95 | select: 'username profilePicture', 96 | }); 97 | user = await User.populate(user, { 98 | path: 'friendRequests.to', 99 | select: 'username profilePicture', 100 | }); 101 | user = await User.populate(user, { 102 | path: 'messageNotifications.from', 103 | select: 'username profilePicture', 104 | }); 105 | user = await User.populate(user, { 106 | path: 'postNotifications.user', 107 | select: 'username profilePicture', 108 | }); 109 | if (!user) { 110 | return res.status(StatusCodes.BAD_REQUEST).json({ 111 | errors: { 112 | username: `User with username ${username} not found`, 113 | }, 114 | }); 115 | } 116 | 117 | const isPasswordMatch = await bcrypt.compare(password, user.password); 118 | 119 | if (!isPasswordMatch) { 120 | return res.status(StatusCodes.BAD_REQUEST).json({ 121 | errors: { 122 | password: `Password is incorrect`, 123 | }, 124 | }); 125 | } 126 | 127 | const token = jwt.sign({ id: user._id }, process.env.JWT_SECRET as string, { 128 | expiresIn: process.env.JWT_EXPIRES_IN, 129 | }); 130 | 131 | res.set( 132 | 'Set-Cookie', 133 | cookie.serialize('token', token, { 134 | httpOnly: true, 135 | sameSite: 'none', 136 | secure: true, 137 | maxAge: 3600 * 24 * 7, 138 | path: '/', 139 | }) 140 | ); 141 | 142 | res.status(StatusCodes.OK).json({ 143 | // @ts-ignore 144 | ...user._doc, 145 | }); 146 | } else { 147 | res.status(StatusCodes.BAD_REQUEST).json({ errors }); 148 | } 149 | }; 150 | 151 | export const logout = (req: Request, res: Response) => { 152 | res.set( 153 | 'Set-Cookie', 154 | cookie.serialize('token', '', { 155 | httpOnly: true, 156 | secure: true, 157 | maxAge: +new Date(0), 158 | sameSite: 'none', 159 | path: '/', 160 | }) 161 | ); 162 | res.json({ message: 'Logout' }); 163 | }; 164 | 165 | export const verifyAccount = async (req: Request, res: Response) => { 166 | let { token } = req.body; 167 | 168 | const { email, username, password }: any = jwt.verify( 169 | token, 170 | process.env.JWT_SECRET as string 171 | ); 172 | 173 | await User.create({ 174 | username, 175 | email, 176 | password, 177 | }); 178 | 179 | res.status(StatusCodes.OK).json({ 180 | message: 'success', 181 | }); 182 | }; 183 | 184 | export const resetPassword = async (req: Request, res: Response) => { 185 | const { email } = req.body; 186 | 187 | const user = await User.findOne({ email }); 188 | 189 | if (!user) 190 | return res.status(StatusCodes.BAD_REQUEST).json({ 191 | errors: { 192 | email: `User with email ${email} not found.`, 193 | }, 194 | }); 195 | 196 | const token = jwt.sign({ id: user._id }, process.env.JWT_SECRET as string, { 197 | expiresIn: '20m', 198 | }); 199 | 200 | const mg = mailgun({ 201 | apiKey: process.env.MAILGUN_API_KEY as string, 202 | domain: process.env.MAILGUN_DOMAIN as string, 203 | }); 204 | 205 | const data = { 206 | from: 'noreply@hello.com', 207 | to: email, 208 | subject: 'Reset Password', 209 | html: ` 210 |

To reset your password please visit the following page: 211 | Reset Password 212 |

213 | `, 214 | }; 215 | 216 | const body = await mg.messages().send(data); 217 | 218 | console.log(body); 219 | 220 | res.status(StatusCodes.OK).json({ message: 'success' }); 221 | }; 222 | 223 | export const updatePassword = async (req: Request, res: Response) => { 224 | const { password, confirmPassword, token } = req.body; 225 | 226 | if (!password.trim()) 227 | return res.status(StatusCodes.BAD_REQUEST).json({ 228 | errors: { 229 | password: 'New password must not be empty', 230 | }, 231 | }); 232 | if (password.length < 6) 233 | return res.status(StatusCodes.BAD_REQUEST).json({ 234 | errors: { 235 | password: 'New password must be at least 6 characters long.', 236 | }, 237 | }); 238 | if (confirmPassword !== password) 239 | return res.status(StatusCodes.BAD_REQUEST).json({ 240 | errors: { 241 | confirmPassword: 'Passwords do not match', 242 | }, 243 | }); 244 | 245 | const { id }: any = jwt.verify(token, process.env.JWT_SECRET as string); 246 | 247 | const hashedPassword = await bcrypt.hash(password, 10); 248 | 249 | await User.findByIdAndUpdate(id, { 250 | password: hashedPassword, 251 | }); 252 | 253 | res.status(StatusCodes.OK).json({ 254 | message: 'Password successfully been updated', 255 | }); 256 | }; -------------------------------------------------------------------------------- /Social-master/backend/src/controllers/conversation.ts: -------------------------------------------------------------------------------- 1 | import Conversation from '../models/conversation'; 2 | import { StatusCodes } from 'http-status-codes'; 3 | import { Request, Response } from 'express'; 4 | 5 | export const createConversation = async (req: Request, res: Response) => { 6 | const exist = await Conversation.findOne({ 7 | $or: [ 8 | { members: [res.locals.user.id, req.params.id] }, 9 | { members: [req.params.id, res.locals.user.id] }, 10 | ], 11 | }); 12 | if (!exist) { 13 | const conversation = await Conversation.create({ 14 | members: [res.locals.user.id, req.params.id], 15 | }); 16 | return res.status(StatusCodes.OK).json(conversation); 17 | } else { 18 | return res 19 | .status(StatusCodes.OK) 20 | .json({ message: 'Conversation alerady exist' }); 21 | } 22 | }; 23 | 24 | export const getConversations = async (req: Request, res: Response) => { 25 | const conversations = await Conversation.find({ 26 | members: { 27 | $in: [res.locals.user.id], 28 | }, 29 | }).populate('members', 'profilePicture username'); 30 | res.status(StatusCodes.OK).json(conversations); 31 | }; 32 | 33 | export const getConversation = async (req: Request, res: Response) => { 34 | const conversation = await Conversation.findById(req.params.id).populate( 35 | 'members', 36 | 'profilePicture username' 37 | ); 38 | res.status(StatusCodes.OK).json(conversation); 39 | }; 40 | -------------------------------------------------------------------------------- /Social-master/backend/src/controllers/friendRequests.ts: -------------------------------------------------------------------------------- 1 | import FriendRequest from '../models/friendRequest'; 2 | import { StatusCodes } from 'http-status-codes'; 3 | import User from '../models/user'; 4 | import { Request, Response } from 'express'; 5 | import { constants } from 'perf_hooks'; 6 | 7 | export const createRequest = async (req: Request, res: Response) => { 8 | const { to } = req.body; 9 | const newFriendRequest = await FriendRequest.create({ 10 | from: res.locals.user.id, 11 | to, 12 | }); 13 | await User.findByIdAndUpdate(to, { 14 | $push: { 15 | // @ts-ignore 16 | friendRequests: newFriendRequest, 17 | }, 18 | }); 19 | await User.findByIdAndUpdate(res.locals.user.id, { 20 | $push: { 21 | // @ts-ignore 22 | friendRequests: newFriendRequest, 23 | }, 24 | }); 25 | const friendRequest = await FriendRequest.findById(newFriendRequest) 26 | .populate('from', 'username profilePicture') 27 | .populate('to', 'username profilePicture'); 28 | res.status(StatusCodes.OK).json(friendRequest); 29 | }; 30 | 31 | export const closeRequest = async (req: Request, res: Response) => { 32 | const { id: requestId } = req.params; 33 | const { status } = req.body; 34 | const friendRequest = await FriendRequest.findById(requestId); 35 | if (status.toLowerCase() === 'accept') { 36 | const user = await User.findByIdAndUpdate( 37 | friendRequest?.to, 38 | { 39 | $push: { 40 | // @ts-ignore 41 | friends: friendRequest?.from, 42 | }, 43 | $pull: { 44 | // @ts-ignore 45 | friendRequests: friendRequest?._id, 46 | }, 47 | }, 48 | { 49 | new: true, 50 | runValidators: true, 51 | } 52 | ); 53 | await User.findByIdAndUpdate(friendRequest?.from, { 54 | $push: { 55 | // @ts-ignore 56 | friends: friendRequest?.to, 57 | }, 58 | $pull: { 59 | // @ts-ignore 60 | friendRequests: friendRequest?._id, 61 | }, 62 | }); 63 | const newFriend = await User.findById(friendRequest?.from).select( 64 | 'username profilePicture' 65 | ); 66 | await friendRequest?.remove(); 67 | res.status(StatusCodes.OK).json(newFriend); 68 | } else { 69 | const user = await User.findByIdAndUpdate(friendRequest?.to, { 70 | $pull: { 71 | // @ts-ignore 72 | friendRequests: friendRequest?._id, 73 | }, 74 | }); 75 | await User.findByIdAndUpdate(friendRequest?.from, { 76 | $pull: { 77 | // @ts-ignore 78 | friendRequests: friendRequest?._id, 79 | }, 80 | }); 81 | await friendRequest?.remove(); 82 | res.status(StatusCodes.OK).json(user); 83 | } 84 | }; 85 | -------------------------------------------------------------------------------- /Social-master/backend/src/controllers/message.ts: -------------------------------------------------------------------------------- 1 | import Message from '../models/message'; 2 | import MessageNotification from '../models/messageNotification'; 3 | import User from '../models/user'; 4 | import { StatusCodes } from 'http-status-codes'; 5 | import { Request, Response } from 'express'; 6 | 7 | export const createMessage = async (req: Request, res: Response) => { 8 | const { conversationId, sender, receiver, text } = req.body; 9 | const newMessage = await Message.create({ 10 | conversationId, 11 | text, 12 | sender, 13 | }); 14 | const message = await Message.findById(newMessage._id).populate( 15 | 'sender', 16 | 'username profilePicture' 17 | ); 18 | const notification = await MessageNotification.create({ 19 | from: sender, 20 | conversation: conversationId, 21 | to: receiver, 22 | }); 23 | await User.findByIdAndUpdate(receiver, { 24 | $push: { 25 | // @ts-ignore 26 | messageNotifications: notification, 27 | }, 28 | }); 29 | res.status(StatusCodes.OK).json(message); 30 | }; 31 | 32 | export const getMessages = async (req: Request, res: Response) => { 33 | const messages = await Message.find({ 34 | conversationId: req.params.conversationId, 35 | }) 36 | .populate('sender', 'username profilePicture') 37 | .sort({ createdAt: -1 }); 38 | res.status(StatusCodes.OK).json(messages); 39 | }; 40 | -------------------------------------------------------------------------------- /Social-master/backend/src/controllers/messageNotification.ts: -------------------------------------------------------------------------------- 1 | import User from '../models/user'; 2 | import MessageNotification from '../models/messageNotification'; 3 | import { Request, Response } from 'express'; 4 | import { StatusCodes } from 'http-status-codes'; 5 | 6 | export const getNotifications = async (req: Request, res: Response) => { 7 | let user = await User.findById(res.locals.user.id).populate( 8 | 'messageNotifications', 9 | 'createdAt from to conversation' 10 | ); 11 | user = await User.populate(user, { 12 | path: 'messageNotifications.from', 13 | select: 'profilePicture username', 14 | }); 15 | res.status(StatusCodes.OK).json(user.messageNotifications); 16 | }; 17 | 18 | export const deleteNotifications = async (req: Request, res: Response) => { 19 | const { id } = req.params; 20 | const notifications = await MessageNotification.find({ from: id }); 21 | let notificationsId = notifications.map((n) => n._id); 22 | await User.findByIdAndUpdate(res.locals.user.id, { 23 | $pull: { 24 | messageNotifications: { 25 | // @ts-ignore 26 | $in: notificationsId, 27 | }, 28 | }, 29 | }); 30 | await MessageNotification.deleteMany({ 31 | from: id, 32 | }); 33 | res.status(StatusCodes.OK).json({ message: 'OK' }); 34 | }; 35 | -------------------------------------------------------------------------------- /Social-master/backend/src/controllers/post.ts: -------------------------------------------------------------------------------- 1 | import User from '../models/user'; 2 | import Post from '../models/post'; 3 | import Comment from '../models/comment'; 4 | import PostNotification from '../models/postNotification'; 5 | import { BadRequestError, NotFoundError } from '../errors'; 6 | import { StatusCodes } from 'http-status-codes'; 7 | import { Request, Response } from 'express'; 8 | 9 | export const updatePost = async (req: Request, res: Response) => { 10 | let post = await Post.findById(req.params.id); 11 | if (!post) { 12 | throw new NotFoundError(`Post with id ${req.params.id} doesnt exist`); 13 | } else { 14 | if (post.author == res.locals.user.id) { 15 | post = await post.updateOne(req.body, { 16 | new: true, 17 | runValidators: true, 18 | }); 19 | return res.status(StatusCodes.OK).json(post); 20 | } else { 21 | throw new BadRequestError('You can only update your posts'); 22 | } 23 | } 24 | }; 25 | 26 | export const createPost = async (req: Request, res: Response) => { 27 | const post = await Post.create(req.body); 28 | const fullPost = await User.populate(post, { 29 | path: 'author', 30 | select: 'username profilePicture', 31 | }); 32 | res.status(StatusCodes.OK).json(fullPost); 33 | }; 34 | 35 | export const deletePost = async (req: Request, res: Response) => { 36 | const post = await Post.findById(req.params.id); 37 | if (post) { 38 | if (post.author == res.locals.user.id) { 39 | await Comment.deleteMany({ postId: post._id }); 40 | await post.deleteOne(); 41 | return res 42 | .status(StatusCodes.OK) 43 | .json({ message: 'The post has been deleted' }); 44 | } else { 45 | throw new BadRequestError('You can only delete your posts'); 46 | } 47 | } else { 48 | throw new NotFoundError(`Post with id ${req.params.id} doesnt exist`); 49 | } 50 | }; 51 | 52 | export const likePost = async (req: Request, res: Response) => { 53 | let post = await Post.findById(req.params.id); 54 | if (post) { 55 | // @ts-ignore 56 | if (post.likes.includes(res.locals.user.id)) { 57 | // @ts-ignore 58 | post.likes = post.likes.filter((id) => id != res.locals.user.id); 59 | } else { 60 | post.likes.push(res.locals.user.id); 61 | if (post?.author != res.locals.user.id) { 62 | // @ts-ignore 63 | const notification = await PostNotification.create({ 64 | user: res.locals.user.id, 65 | post: post?._id, 66 | type: 'Like', 67 | }); 68 | await User.findByIdAndUpdate(post?.author!, { 69 | $push: { 70 | // @ts-ignore 71 | postNotifications: notification?._id, 72 | }, 73 | }); 74 | } 75 | } 76 | await post.save(); 77 | post = await Post.findById(req.params.id).populate( 78 | 'author', 79 | 'username profilePicture' 80 | ); 81 | 82 | res.status(StatusCodes.OK).json(post); 83 | } else { 84 | throw new NotFoundError(`Post with id ${req.params.id} not found`); 85 | } 86 | }; 87 | 88 | export const getPost = async (req: Request, res: Response) => { 89 | const post = await Post.findById(req.params.id) 90 | .populate('author', 'username profilePicture') 91 | .populate('comments', 'author body postId createdAt'); 92 | const fullPost = await User.populate(post, { 93 | path: 'comments.author', 94 | select: 'username profilePicture desc', 95 | }); 96 | const notifications = await PostNotification.find({ 97 | post: fullPost?._id, 98 | }); 99 | const notificationId = notifications.map((n) => n._id); 100 | await User.findByIdAndUpdate(post?.author?._id, { 101 | $pull: { 102 | postNotifications: { 103 | // @ts-ignore 104 | $in: notificationId, 105 | }, 106 | }, 107 | }); 108 | await PostNotification.deleteMany({ post: post?._id }); 109 | res.status(StatusCodes.OK).json(fullPost); 110 | }; 111 | 112 | export const getPosts = async (req: Request, res: Response) => { 113 | const page = Number(req.query.page) || 1; 114 | const limit = Number(req.query.limit) || 5; 115 | const skip = (page - 1) * limit; 116 | const currentUser = await User.findById(req.query.userId); 117 | const userPosts = await Post.find({ author: req.query.userId }).populate( 118 | 'author', 119 | 'username profilePicture' 120 | ); 121 | const friendPosts = await Post.find({ 122 | author: { 123 | $in: currentUser?.friends, 124 | }, 125 | }).populate('author', 'username profilePicture'); 126 | // @ts-ignore 127 | let allPosts = userPosts.concat(...friendPosts).sort((p1, p2) => { 128 | return ( 129 | // @ts-ignore 130 | new Date(p2.createdAt).getTime() - new Date(p1.createdAt).getTime() 131 | ); 132 | }); 133 | const numberOfPages = Math.ceil(allPosts.length / limit); 134 | allPosts = allPosts.slice(skip).slice(0, limit); 135 | 136 | res.status(StatusCodes.OK).json({ 137 | posts: allPosts, 138 | numberOfPages, 139 | }); 140 | }; 141 | 142 | export const getUsersPosts = async (req: Request, res: Response) => { 143 | const posts = await Post.find({ author: req.params.userId }) 144 | .populate('author', 'username profilePicture') 145 | .sort({ 146 | createdAt: -1, 147 | }); 148 | res.status(StatusCodes.OK).json(posts); 149 | }; 150 | 151 | export const addComment = async (req: Request, res: Response) => { 152 | const { id } = req.params; 153 | const { body } = req.body; 154 | const comment = await Comment.create({ 155 | postId: id, 156 | author: res.locals.user.id, 157 | body, 158 | }); 159 | const post = await Post.findByIdAndUpdate( 160 | id, 161 | { 162 | $push: { 163 | comments: comment._id, 164 | }, 165 | }, 166 | { 167 | new: true, 168 | runValidators: true, 169 | } 170 | ) 171 | .populate('author', 'username profilePicture desc') 172 | .populate('comments', 'author body postId createdAt'); 173 | const updatedPost = await User.populate(post, { 174 | path: 'comments.author', 175 | select: 'username profilePicture desc', 176 | }); 177 | if (post?.author?._id != res.locals.user.id) { 178 | const notification = await PostNotification.create({ 179 | user: res.locals.user.id, 180 | post: post?._id, 181 | type: 'Comment', 182 | }); 183 | 184 | await User.findByIdAndUpdate(post?.author?._id!, { 185 | $push: { 186 | // @ts-ignore 187 | postNotifications: notification?._id, 188 | }, 189 | }); 190 | } 191 | res.status(StatusCodes.OK).json(updatedPost); 192 | }; 193 | 194 | export const deleteComment = async (req: Request, res: Response) => { 195 | const { postId, commentId } = req.params; 196 | await Comment.findByIdAndDelete(commentId); 197 | const post = await Post.findByIdAndUpdate( 198 | postId, 199 | { 200 | $pull: { 201 | comments: commentId, 202 | }, 203 | }, 204 | { 205 | new: true, 206 | runValidators: true, 207 | } 208 | ) 209 | .populate('author', 'username profilePicture') 210 | .populate('comments', 'author body postId createdAt'); 211 | const updatedPost = await User.populate(post, { 212 | path: 'comments.author', 213 | select: 'username profilePicture', 214 | }); 215 | res.status(StatusCodes.OK).json(updatedPost); 216 | }; 217 | -------------------------------------------------------------------------------- /Social-master/backend/src/controllers/upload.ts: -------------------------------------------------------------------------------- 1 | import { v2 as cloudinary } from 'cloudinary'; 2 | import { Request, Response } from 'express'; 3 | import { StatusCodes } from 'http-status-codes'; 4 | import streamifier from 'streamifier'; 5 | 6 | cloudinary.config({ 7 | cloud_name: process.env.CLOUDINARY_CLOUD_NAME, 8 | api_key: process.env.CLOUDINARY_API_KEY, 9 | api_secret: process.env.CLOUDINARY_API_SECRET, 10 | }); 11 | 12 | export const uploadImage = async (req: Request, res: Response) => { 13 | const streamUpload = (req: Request) => { 14 | return new Promise((resolve, reject) => { 15 | const stream = cloudinary.uploader.upload_stream((error, result) => { 16 | if (result) resolve(result); 17 | else reject(error); 18 | }); 19 | streamifier.createReadStream(req?.file?.buffer!).pipe(stream); 20 | }); 21 | }; 22 | const result = await streamUpload(req); 23 | res.status(200).send(result); 24 | }; 25 | 26 | export const deleteImage = async (req: Request, res: Response) => { 27 | let id: string = req.params.id; 28 | cloudinary.uploader.destroy(id, function (result) { 29 | console.log(result); 30 | }); 31 | res.status(StatusCodes.OK).json({ message: 'Deleted' }); 32 | }; 33 | -------------------------------------------------------------------------------- /Social-master/backend/src/controllers/user.ts: -------------------------------------------------------------------------------- 1 | import User from '../models/user'; 2 | import { BadRequestError, NotFoundError } from '../errors/index'; 3 | import { StatusCodes } from 'http-status-codes'; 4 | import { Request, Response } from 'express'; 5 | 6 | export const getUser = async (req: Request, res: Response) => { 7 | const user = await User.findById(req.params.id).populate( 8 | 'friends', 9 | 'username profilePicture' 10 | ); 11 | // @ts-ignore 12 | const { password, updatedAt, ...other } = user._doc; 13 | res.status(StatusCodes.OK).json(other); 14 | }; 15 | 16 | export const getUserInfo = async (req: Request, res: Response) => { 17 | const user = await User.findById(res.locals.user.id) 18 | .populate('friends', 'username profilePicture') 19 | .populate('friendRequests', 'from to createdAt') 20 | .populate('messageNotifications', 'createdAt from to conversation') 21 | .populate('postNotifications', 'post createdAt user type') 22 | .select('-password'); 23 | let fullUser = await User.populate(user, { 24 | path: 'friendRequests.from', 25 | select: 'username profilePicture', 26 | }); 27 | fullUser = await User.populate(user, { 28 | path: 'friendRequests.to', 29 | select: 'username profilePicture', 30 | }); 31 | fullUser = await User.populate(user, { 32 | path: 'messageNotifications.from', 33 | select: 'username profilePicture', 34 | }); 35 | fullUser = await User.populate(user, { 36 | path: 'postNotifications.user', 37 | select: 'profilePicture username', 38 | }); 39 | 40 | res.status(StatusCodes.OK).json(fullUser); 41 | }; 42 | 43 | export const updateUser = async (req: Request, res: Response) => { 44 | const updatedUser = await User.findByIdAndUpdate(req.params.id, req.body, { 45 | new: true, 46 | runValidators: true, 47 | }) 48 | .populate('friends', 'profilePicture username') 49 | .populate('friendRequests', 'from to createdAt') 50 | .populate('messageNotifications', 'createdAt from to conversation') 51 | .populate('postNotifications', 'post createdAt user type') 52 | .select('-password'); 53 | 54 | let fullUser = await User.populate(updatedUser, { 55 | path: 'friendRequests.from', 56 | select: 'username profilePicture', 57 | }); 58 | fullUser = await User.populate(updatedUser, { 59 | path: 'friendRequests.to', 60 | select: 'username profilePicture', 61 | }); 62 | fullUser = await User.populate(updatedUser, { 63 | path: 'messageNotifications.from', 64 | select: 'username profilePicture', 65 | }); 66 | fullUser = await User.populate(updatedUser, { 67 | path: 'postNotifications.user', 68 | select: 'profilePicture username', 69 | }); 70 | 71 | res.status(StatusCodes.OK).json(updatedUser); 72 | }; 73 | 74 | export const deleteUser = async (req: Request, res: Response) => { 75 | if (res.locals.user.id === req.params.id || req.body.isAdmin) { 76 | await User.findByIdAndDelete(req.params.id); 77 | return res 78 | .status(StatusCodes.OK) 79 | .json({ message: 'Account has been deleted' }); 80 | } else { 81 | throw new BadRequestError('You can only delete your account'); 82 | } 83 | }; 84 | 85 | // export const followUser = async (req: Request, res: Response) => { 86 | // if (res.locals.user.id !== req.params.id) { 87 | // const user = await User.findById(req.params.id); 88 | // let currentUser = await User.findById(res.locals.user.id); 89 | // if (user) { 90 | // if (currentUser) { 91 | // // @ts-ignore 92 | // if (!user?.followers.includes(res.locals.user.id)) { 93 | // // @ts-ignore 94 | // await user.updateOne({ $push: { followers: res.locals.user.id } }); 95 | // // @ts-ignore 96 | // currentUser = await User.findByIdAndUpdate( 97 | // currentUser._id, 98 | // { 99 | // // @ts-ignore 100 | // $push: { following: req.params.id }, 101 | // }, 102 | // { 103 | // new: true, 104 | // runValidators: true, 105 | // } 106 | // ).populate('following', 'username profilePicture'); 107 | // return res.status(StatusCodes.OK).json(currentUser?.following); 108 | // } else { 109 | // throw new NotFoundError(`User with id ${req.params.id} not found`); 110 | // } 111 | // } else { 112 | // throw new NotFoundError(`User with id ${res.locals.user.id} not found`); 113 | // } 114 | // } else { 115 | // throw new BadRequestError('You already follow this user'); 116 | // } 117 | // } else { 118 | // throw new BadRequestError('You cant follow yourself'); 119 | // } 120 | // }; 121 | 122 | // export const unfollowUser = async (req: Request, res: Response) => { 123 | // if (res.locals.user.id !== req.params.id) { 124 | // const user = await User.findById(req.params.id); 125 | // let currentUser = await User.findById(res.locals.user.id); 126 | // // @ts-ignore 127 | // if (user?.followers.includes(res.locals.user.id)) { 128 | // // @ts-ignore 129 | // await user.updateOne({ $pull: { followers: res.locals.user.id } }); 130 | // // @ts-ignore 131 | // currentUser = await User.findByIdAndUpdate( 132 | // currentUser?._id, 133 | // { 134 | // // @ts-ignore 135 | // $pull: { following: req.params.id }, 136 | // }, 137 | // { 138 | // new: true, 139 | // runValidators: true, 140 | // } 141 | // ).populate('following', 'username profilePicture'); 142 | // return res.status(StatusCodes.OK).json(currentUser?.following); 143 | // } else { 144 | // throw new BadRequestError('You dont follow this user'); 145 | // } 146 | // } else { 147 | // throw new BadRequestError('You cant unfollow yourself'); 148 | // } 149 | // }; 150 | 151 | export const getFriends = async (req: Request, res: Response) => { 152 | const user = await User.findById(req.params.id).populate( 153 | 'friends', 154 | 'username profilePicture' 155 | ); 156 | if (!user) { 157 | throw new NotFoundError(`User with id ${req.params.id} doesnt exist`); 158 | } else { 159 | res.status(StatusCodes.OK).json(user.friends); 160 | } 161 | }; 162 | 163 | export const searchUsers = async (req: Request, res: Response) => { 164 | const { search } = req.query; 165 | const username = new RegExp(search as string, 'i'); 166 | const users = await User.find({ 167 | username, 168 | }).find({ _id: { $ne: res.locals.user.id } }); 169 | res.status(StatusCodes.OK).json(users); 170 | }; 171 | 172 | export const removeFriend = async (req: Request, res: Response) => { 173 | const user = await User.findByIdAndUpdate( 174 | res.locals.user.id, 175 | { 176 | $pull: { 177 | // @ts-ignore 178 | friends: req.params.id, 179 | }, 180 | }, 181 | { 182 | new: true, 183 | runValidators: true, 184 | } 185 | ); 186 | await User.findByIdAndUpdate(req.params.id, { 187 | $pull: { 188 | // @ts-ignore 189 | friends: res.locals.user.id, 190 | }, 191 | }); 192 | res.status(StatusCodes.OK).json(user); 193 | }; 194 | -------------------------------------------------------------------------------- /Social-master/backend/src/db/connectDB.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | 3 | const connectDB = async () => { 4 | return mongoose.connect(process.env.MONGO_URI as string); 5 | }; 6 | 7 | export default connectDB; 8 | -------------------------------------------------------------------------------- /Social-master/backend/src/errors/badRequest.ts: -------------------------------------------------------------------------------- 1 | import { StatusCodes } from 'http-status-codes'; 2 | import CustomApiError from './error'; 3 | 4 | class BadRequestError extends CustomApiError { 5 | constructor(message: string) { 6 | super(message); 7 | this.statusCode = StatusCodes.BAD_REQUEST; 8 | } 9 | } 10 | 11 | export default BadRequestError; 12 | -------------------------------------------------------------------------------- /Social-master/backend/src/errors/error.ts: -------------------------------------------------------------------------------- 1 | class CustomApiError extends Error { 2 | statusCode: number; 3 | constructor(message: string) { 4 | super(message); 5 | this.statusCode = 500; 6 | } 7 | } 8 | export default CustomApiError; 9 | -------------------------------------------------------------------------------- /Social-master/backend/src/errors/index.ts: -------------------------------------------------------------------------------- 1 | export { default as CustomApiError } from './error'; 2 | export { default as BadRequestError } from './badRequest'; 3 | export { default as NotFoundError } from './notFound'; 4 | export { default as UnauthorizedError } from './unauthorized'; 5 | -------------------------------------------------------------------------------- /Social-master/backend/src/errors/notFound.ts: -------------------------------------------------------------------------------- 1 | import { StatusCodes } from 'http-status-codes'; 2 | import CustomApiError from './error'; 3 | 4 | class NotFoundError extends CustomApiError { 5 | constructor(message: string) { 6 | super(message); 7 | this.statusCode = StatusCodes.NOT_FOUND; 8 | } 9 | } 10 | 11 | export default NotFoundError; 12 | -------------------------------------------------------------------------------- /Social-master/backend/src/errors/unauthorized.ts: -------------------------------------------------------------------------------- 1 | import { StatusCodes } from 'http-status-codes'; 2 | import CustomApiError from './error'; 3 | 4 | class UnauthorizedError extends CustomApiError { 5 | constructor(message: string) { 6 | super(message); 7 | this.statusCode = StatusCodes.UNAUTHORIZED; 8 | } 9 | } 10 | 11 | export default UnauthorizedError; 12 | -------------------------------------------------------------------------------- /Social-master/backend/src/index.ts: -------------------------------------------------------------------------------- 1 | import express, { Request, Response } from 'express'; 2 | import 'express-async-errors'; 3 | import 'colors'; 4 | import 'dotenv/config'; 5 | import { Server } from 'socket.io'; 6 | 7 | import connectDB from './db/connectDB'; 8 | 9 | import notFound from './middleware/notFound'; 10 | import errorHandler from './middleware/error'; 11 | import authMiddleware from './middleware/auth'; 12 | import morgan from 'morgan'; 13 | import cookieParser from 'cookie-parser'; 14 | import cors from 'cors'; 15 | 16 | import auth from './routes/auth'; 17 | import user from './routes/users'; 18 | import post from './routes/posts'; 19 | import conversation from './routes/conversation'; 20 | import message from './routes/message'; 21 | import upload from './routes/upload'; 22 | import friendRequests from './routes/friendRequests'; 23 | import messageNotifications from './routes/messageNotification'; 24 | 25 | // Chats 26 | import { addUser, users, removeUser, getUser } from './config/users'; 27 | 28 | const app = express(); 29 | 30 | // const __dirname = path.resolve( 31 | // path.dirname(decodeURI(new URL(import.meta.url).pathname)) 32 | // ); 33 | 34 | // app.use('/images', express.static(path.join(__dirname + '/public/images'))); 35 | 36 | const corsOptions = { 37 | origin: true, 38 | credentials: true, 39 | }; 40 | 41 | app.use(function (req, res, next) { 42 | res.header('Access-Control-Allow-Origin', '*'); 43 | res.header('Access-Control-Allow-Methods', 'GET, PUT, PATCH, POST, DELETE'); 44 | res.header( 45 | 'Access-Control-Allow-Headers', 46 | 'Origin, X-Requested-With, Content-Type, Accept' 47 | ); 48 | next(); 49 | }); 50 | 51 | app.enable('trust proxy'); 52 | app.use(cors(corsOptions)); 53 | app.use(cookieParser()); 54 | app.use(morgan('dev')); 55 | app.use(cookieParser()); 56 | app.use( 57 | cors({ 58 | origin: 'https://project-social.netlify.app', 59 | credentials: true, 60 | }) 61 | ); 62 | app.use(express.json()); 63 | app.use(express.urlencoded({ extended: true })); 64 | 65 | // File uploading part 66 | 67 | // const storage = multer.diskStorage({ 68 | // destination: (req, file, cb) => { 69 | // cb(null, 'public/images'); 70 | // }, 71 | // filename: (req, file, cb) => { 72 | // cb(null, req.body.name); 73 | // }, 74 | // }); 75 | 76 | // const upload = multer({ storage }); 77 | 78 | // app.post('/api/upload', upload.single('file'), (req, res) => { 79 | // try { 80 | // res.status(200).json({ message: 'File uploaded successfully' }); 81 | // } catch (error) { 82 | // console.log(error); 83 | // } 84 | // }); 85 | 86 | // File uploading part ended 87 | 88 | app.get('/', (req: Request, res: Response) => 89 | res.status(200).send('Hello world!') 90 | ); 91 | 92 | app.use('/api/auth', auth); 93 | app.use('/api/users', authMiddleware, user); 94 | app.use('/api/posts', authMiddleware, post); 95 | app.use('/api/conversations', authMiddleware, conversation); 96 | app.use('/api/messages', authMiddleware, message); 97 | app.use('/api/upload', authMiddleware, upload); 98 | app.use('/api/friendRequests', authMiddleware, friendRequests); 99 | app.use('/api/messageNotifications', authMiddleware, messageNotifications); 100 | 101 | app.use(errorHandler); 102 | app.use(notFound); 103 | 104 | const PORT = process.env.PORT || 5000; 105 | 106 | const start = async () => { 107 | try { 108 | await connectDB(); 109 | const server = app.listen(PORT, () => 110 | console.log(`Server runnnig on port ${PORT}`.green.bold) 111 | ); 112 | const io = new Server(server, { 113 | cors: { 114 | origin: '*', 115 | }, 116 | }); 117 | 118 | io.on('connection', (socket) => { 119 | console.log('User connected'); 120 | 121 | socket.on('addUser', (userId) => { 122 | addUser(userId, socket.id); 123 | io.emit('getUsers', users); 124 | }); 125 | 126 | socket.on('disconnect', () => { 127 | console.log('User disconnected'); 128 | removeUser(socket.id); 129 | io.emit('getUsers', users); 130 | }); 131 | 132 | socket.on('sendMessage', (receiverId) => { 133 | // console.log(users, receiverId); 134 | const receiver = getUser(receiverId); 135 | // console.log('yoo bro you got message wake up', receiver); 136 | if (receiver) { 137 | socket.to(receiver.socketId).emit('getMessage'); 138 | } 139 | }); 140 | 141 | socket.on('typing', (receiverId) => { 142 | // console.log('start typing'); 143 | const receiver = getUser(receiverId); 144 | if (receiver) { 145 | socket.to(receiver.socketId).emit('typing'); 146 | } 147 | }); 148 | 149 | socket.on('stopTyping', (receiverId) => { 150 | // console.log('stop typing'); 151 | const receiver = getUser(receiverId); 152 | if (receiver) { 153 | socket.to(receiver.socketId).emit('stopTyping'); 154 | } 155 | }); 156 | socket.on('sendRequest', (receiverId) => { 157 | const receiver = getUser(receiverId); 158 | if (receiver) { 159 | socket.to(receiver.socketId).emit('getRequest'); 160 | } 161 | }); 162 | }); 163 | } catch (error) { 164 | console.log(`${error}`.red.bold); 165 | process.exit(1); 166 | } 167 | }; 168 | 169 | start(); 170 | -------------------------------------------------------------------------------- /Social-master/backend/src/middleware/auth.ts: -------------------------------------------------------------------------------- 1 | import { UnauthorizedError } from '../errors'; 2 | import { Request, Response, NextFunction } from 'express'; 3 | import jwt from 'jsonwebtoken'; 4 | 5 | const auth = (req: Request, res: Response, next: NextFunction) => { 6 | const token = req.cookies.token; 7 | 8 | if (!token) throw new UnauthorizedError('Unauthenticated'); 9 | 10 | const { id }: any = jwt.verify(token, process.env.JWT_SECRET as string); 11 | 12 | res.locals.user = { id }; 13 | 14 | next(); 15 | // const { authorization } = req.headers; 16 | // if (!authorization || !authorization.startsWith('Bearer')) { 17 | // throw new UnauthorizedError('Token not provided'); 18 | // } else { 19 | // const token = authorization.split(' ')[1]; 20 | // try { 21 | // const { id } = jwt.verify(token, process.env.JWT_SECRET as string) as { 22 | // id: string; 23 | // }; 24 | // req.user = { 25 | // id, 26 | // }; 27 | // next(); 28 | // } catch (error) { 29 | // throw new UnauthorizedError('Invalid/Expired token'); 30 | // } 31 | // } 32 | }; 33 | 34 | export default auth; 35 | -------------------------------------------------------------------------------- /Social-master/backend/src/middleware/error.ts: -------------------------------------------------------------------------------- 1 | import { StatusCodes } from 'http-status-codes'; 2 | import { CustomApiError } from '../errors'; 3 | import { Request, Response, NextFunction } from 'express'; 4 | 5 | const errorMiddleware = ( 6 | err: Error, 7 | req: Request, 8 | res: Response, 9 | next: NextFunction 10 | ) => { 11 | if (err instanceof CustomApiError) { 12 | return res.status(err.statusCode).json({ message: err.message }); 13 | } 14 | res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ message: err.message }); 15 | }; 16 | 17 | export default errorMiddleware; 18 | -------------------------------------------------------------------------------- /Social-master/backend/src/middleware/notFound.ts: -------------------------------------------------------------------------------- 1 | import { StatusCodes } from 'http-status-codes'; 2 | import { Request, Response } from 'express'; 3 | 4 | const notFoundMiddleware = (req: Request, res: Response) => { 5 | res 6 | .status(StatusCodes.NOT_FOUND) 7 | .json({ message: 'The page you are looking for does not exist.' }); 8 | }; 9 | 10 | export default notFoundMiddleware; 11 | -------------------------------------------------------------------------------- /Social-master/backend/src/models/comment.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | 3 | const CommentSchema = new mongoose.Schema( 4 | { 5 | author: { 6 | type: mongoose.Schema.Types.ObjectId, 7 | ref: 'User', 8 | }, 9 | postId: { 10 | type: mongoose.Schema.Types.ObjectId, 11 | ref: 'Post', 12 | }, 13 | body: { 14 | type: String, 15 | required: [true, 'Please provide comment body'], 16 | trim: true, 17 | }, 18 | }, 19 | { 20 | timestamps: true, 21 | } 22 | ); 23 | 24 | const Comment = mongoose.model('Comment', CommentSchema); 25 | 26 | export default Comment; 27 | -------------------------------------------------------------------------------- /Social-master/backend/src/models/conversation.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | 3 | const ConversationSchema = new mongoose.Schema( 4 | { 5 | members: [ 6 | { 7 | type: mongoose.Schema.Types.ObjectId, 8 | ref: 'User', 9 | default: [], 10 | }, 11 | ], 12 | }, 13 | { 14 | timestamps: true, 15 | } 16 | ); 17 | 18 | const Conversation = mongoose.model('Conversation', ConversationSchema); 19 | 20 | export default Conversation; 21 | -------------------------------------------------------------------------------- /Social-master/backend/src/models/friendRequest.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | 3 | const FriendRequestSchema = new mongoose.Schema( 4 | { 5 | from: { 6 | type: mongoose.Schema.Types.ObjectId, 7 | ref: 'User', 8 | required: true, 9 | }, 10 | to: { 11 | type: mongoose.Schema.Types.ObjectId, 12 | ref: 'User', 13 | required: true, 14 | }, 15 | }, 16 | { 17 | timestamps: true, 18 | } 19 | ); 20 | 21 | const FriendRequest = mongoose.model('FriendRequest', FriendRequestSchema); 22 | 23 | export default FriendRequest; 24 | -------------------------------------------------------------------------------- /Social-master/backend/src/models/message.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | 3 | const MessageSchema = new mongoose.Schema( 4 | { 5 | conversationId: { 6 | type: mongoose.Schema.Types.ObjectId, 7 | ref: 'Conversation', 8 | }, 9 | sender: { 10 | type: mongoose.Schema.Types.ObjectId, 11 | ref: 'User', 12 | }, 13 | text: { 14 | type: String, 15 | required: [true, 'Please provide message'], 16 | }, 17 | }, 18 | { 19 | timestamps: true, 20 | } 21 | ); 22 | 23 | const Message = mongoose.model('Message', MessageSchema); 24 | 25 | export default Message; 26 | -------------------------------------------------------------------------------- /Social-master/backend/src/models/messageNotification.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | 3 | const MessageNotificationSchema = new mongoose.Schema( 4 | { 5 | conversation: { 6 | type: mongoose.Schema.Types.ObjectId, 7 | ref: 'Conversation', 8 | }, 9 | from: { 10 | type: mongoose.Schema.Types.ObjectId, 11 | ref: 'User', 12 | }, 13 | to: { 14 | type: mongoose.Schema.Types.ObjectId, 15 | ref: 'User', 16 | }, 17 | }, 18 | { 19 | timestamps: true, 20 | } 21 | ); 22 | 23 | const MessageNotification = mongoose.model( 24 | 'MessageNotification', 25 | MessageNotificationSchema 26 | ); 27 | 28 | export default MessageNotification; 29 | -------------------------------------------------------------------------------- /Social-master/backend/src/models/post.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | 3 | const PostSchema = new mongoose.Schema( 4 | { 5 | author: { 6 | type: mongoose.Schema.Types.ObjectId, 7 | ref: 'User', 8 | required: true, 9 | }, 10 | desc: { 11 | type: String, 12 | maxLength: 500, 13 | }, 14 | img: { 15 | type: String, 16 | default: '', 17 | }, 18 | likes: [ 19 | { 20 | type: mongoose.Schema.Types.ObjectId, 21 | ref: 'User', 22 | }, 23 | ], 24 | comments: [ 25 | { 26 | type: mongoose.Schema.Types.ObjectId, 27 | ref: 'Comment', 28 | }, 29 | ], 30 | }, 31 | { 32 | timestamps: true, 33 | } 34 | ); 35 | 36 | const Post = mongoose.model('Post', PostSchema); 37 | 38 | export default Post; 39 | -------------------------------------------------------------------------------- /Social-master/backend/src/models/postNotification.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | 3 | const PostNotificationSchema = new mongoose.Schema( 4 | { 5 | type: { 6 | type: String, 7 | required: true, 8 | }, 9 | post: { 10 | type: mongoose.Schema.Types.ObjectId, 11 | ref: 'Post', 12 | }, 13 | user: { 14 | type: mongoose.Schema.Types.ObjectId, 15 | ref: 'User', 16 | }, 17 | }, 18 | { 19 | timestamps: true, 20 | } 21 | ); 22 | 23 | const PostNotification = mongoose.model( 24 | 'PostNotification', 25 | PostNotificationSchema 26 | ); 27 | 28 | export default PostNotification; 29 | -------------------------------------------------------------------------------- /Social-master/backend/src/models/user.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | 3 | 4 | const UserSchema = new mongoose.Schema( 5 | { 6 | username: { 7 | type: String, 8 | required: [true, 'Please provide username'], 9 | unique: true, 10 | }, 11 | email: { 12 | type: String, 13 | unique: true, 14 | required: [true, 'Please provide email'], 15 | }, 16 | password: { 17 | type: String, 18 | required: [true, 'Please provide password'], 19 | minLength: 8, 20 | }, 21 | messageNotifications: [ 22 | { 23 | type: mongoose.Schema.Types.ObjectId, 24 | ref: 'MessageNotification', 25 | }, 26 | ], 27 | postNotifications: [ 28 | { 29 | type: mongoose.Schema.Types.ObjectId, 30 | ref: 'PostNotification', 31 | }, 32 | ], 33 | profilePicture: { 34 | type: String, 35 | default: 36 | 'https://res.cloudinary.com/dxf7urmsh/image/upload/v1660727184/dquestion_app_widget_1_b_i7bjjo.png', 37 | }, 38 | coverPicture: { 39 | type: String, 40 | default: 41 | 'https://res.cloudinary.com/dxf7urmsh/image/upload/v1659264833/16588370472353_vy1sjr.jpg', 42 | }, 43 | desc: { 44 | type: String, 45 | default: '', 46 | }, 47 | city: { 48 | type: String, 49 | default: 'City ...', 50 | }, 51 | from: { 52 | type: String, 53 | default: 'From ...', 54 | }, 55 | relationship: { 56 | type: Number, 57 | default: 1, 58 | }, 59 | isAdmin: { 60 | type: Boolean, 61 | default: false, 62 | }, 63 | friendRequests: [ 64 | { 65 | type: mongoose.Schema.Types.ObjectId, 66 | ref: 'FriendRequest', 67 | }, 68 | ], 69 | friends: [ 70 | { 71 | type: mongoose.Schema.Types.ObjectId, 72 | ref: 'User', 73 | }, 74 | ], 75 | following: [ 76 | { 77 | type: mongoose.Schema.Types.ObjectId, 78 | ref: 'User', 79 | default: [], 80 | }, 81 | ], 82 | followers: [ 83 | { 84 | type: mongoose.Schema.Types.ObjectId, 85 | ref: 'User', 86 | default: [], 87 | }, 88 | ], 89 | }, 90 | { 91 | timestamps: true, 92 | } 93 | ); 94 | 95 | const User = mongoose.model('User', UserSchema); 96 | 97 | export default User; 98 | -------------------------------------------------------------------------------- /Social-master/backend/src/routes/auth.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | 3 | const router = express.Router(); 4 | 5 | import { 6 | register, 7 | login, 8 | logout, 9 | verifyAccount, 10 | resetPassword, 11 | updatePassword, 12 | } from '../controllers/auth'; 13 | 14 | router.post('/register', register); 15 | 16 | router.post('/login', login); 17 | 18 | router.get('/logout', logout); 19 | 20 | router.post('/verify', verifyAccount); 21 | 22 | router.post('/reset/password', resetPassword); 23 | 24 | router.patch('/update/password', updatePassword); 25 | 26 | export default router; 27 | -------------------------------------------------------------------------------- /Social-master/backend/src/routes/conversation.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | 3 | const router = express.Router(); 4 | 5 | import { 6 | createConversation, 7 | getConversations, 8 | getConversation, 9 | } from '../controllers/conversation'; 10 | 11 | router.post('/:id', createConversation); 12 | 13 | router.get('/', getConversations); 14 | 15 | router.get('/:id', getConversation); 16 | 17 | export default router; 18 | -------------------------------------------------------------------------------- /Social-master/backend/src/routes/friendRequests.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | 3 | const router = express.Router(); 4 | 5 | import { createRequest, closeRequest } from '../controllers/friendRequests'; 6 | 7 | router.post('/', createRequest); 8 | 9 | router.patch('/:id', closeRequest); 10 | 11 | export default router; 12 | -------------------------------------------------------------------------------- /Social-master/backend/src/routes/message.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | 3 | const router = express(); 4 | 5 | import { createMessage, getMessages } from '../controllers/message'; 6 | 7 | router.post('/', createMessage); 8 | 9 | router.get('/:conversationId', getMessages); 10 | 11 | export default router; 12 | -------------------------------------------------------------------------------- /Social-master/backend/src/routes/messageNotification.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | 3 | import { 4 | getNotifications, 5 | deleteNotifications, 6 | } from '../controllers/messageNotification'; 7 | 8 | const router = express.Router(); 9 | 10 | router.get('/', getNotifications); 11 | 12 | router.delete('/:id', deleteNotifications); 13 | 14 | export default router; 15 | -------------------------------------------------------------------------------- /Social-master/backend/src/routes/posts.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | 3 | const router = express.Router(); 4 | 5 | import { 6 | getPost, 7 | getPosts, 8 | getUsersPosts, 9 | createPost, 10 | updatePost, 11 | deletePost, 12 | likePost, 13 | addComment, 14 | deleteComment, 15 | } from '../controllers/post'; 16 | 17 | router.post('/', createPost); 18 | 19 | router.get('/timeline', getPosts); 20 | 21 | router.get('/:id', getPost); 22 | 23 | router.get('/all/:userId', getUsersPosts); 24 | 25 | router.patch('/:id', updatePost); 26 | 27 | router.delete('/:id', deletePost); 28 | 29 | router.post('/:id/like', likePost); 30 | 31 | router.post('/:id/comment', addComment); 32 | 33 | router.delete('/:postId/comment/:commentId', deleteComment); 34 | 35 | export default router; 36 | -------------------------------------------------------------------------------- /Social-master/backend/src/routes/upload.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import multer from 'multer'; 3 | const upload = multer(); 4 | 5 | const router = express.Router(); 6 | 7 | import { uploadImage, deleteImage } from '../controllers/upload'; 8 | 9 | router.post('/', upload.single('file'), uploadImage); 10 | 11 | router.delete('/:id', deleteImage); 12 | 13 | export default router; 14 | -------------------------------------------------------------------------------- /Social-master/backend/src/routes/users.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | 3 | const router = express.Router(); 4 | 5 | import { 6 | updateUser, 7 | getUser, 8 | getFriends, 9 | getUserInfo, 10 | deleteUser, 11 | searchUsers, 12 | removeFriend, 13 | } from '../controllers/user'; 14 | 15 | router.get('/find', searchUsers); 16 | 17 | router.get('/me', getUserInfo); 18 | 19 | router.get('/:id', getUser); 20 | 21 | router.get('/friends/:id', getFriends); 22 | 23 | router.patch('/:id', updateUser); 24 | 25 | router.delete('/:id/friend', removeFriend); 26 | 27 | router.delete('/:id', deleteUser); 28 | 29 | 30 | export default router; 31 | -------------------------------------------------------------------------------- /Social-master/backend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 22 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 26 | 27 | /* Modules */ 28 | "module": "commonjs" /* Specify what module code is generated. */, 29 | // "rootDir": "./", /* Specify the root folder within your source files. */ 30 | // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 31 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 33 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 34 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 35 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 36 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 37 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 38 | // "resolveJsonModule": true, /* Enable importing .json files. */ 39 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 40 | 41 | /* JavaScript Support */ 42 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 43 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 44 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 45 | 46 | /* Emit */ 47 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 48 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 49 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 50 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 51 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 52 | // "outDir": "./", /* Specify an output folder for all emitted files. */ 53 | // "removeComments": true, /* Disable emitting comments. */ 54 | // "noEmit": true, /* Disable emitting files from a compilation. */ 55 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 56 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 57 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 58 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 59 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 60 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 61 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 62 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 63 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 64 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 65 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 66 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 67 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 68 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 69 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 70 | 71 | /* Interop Constraints */ 72 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 73 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 74 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, 75 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 76 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 77 | 78 | /* Type Checking */ 79 | "strict": true /* Enable all strict type-checking options. */, 80 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 81 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 82 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 83 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 84 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 85 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 86 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 87 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 88 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 89 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 90 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 91 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 92 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 93 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 94 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 95 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 96 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 97 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 98 | 99 | /* Completeness */ 100 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 101 | "skipLibCheck": true /* Skip type checking all .d.ts files. */, 102 | "typeRoots": ["@types", "./node_modules/@types"] 103 | }, 104 | "include": [ 105 | "src/**/*" 106 | ], 107 | "exclude": [ 108 | "node_modules", 109 | "../frontend" 110 | ] 111 | } 112 | -------------------------------------------------------------------------------- /Social-master/frontend/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 13 | 14 | The page will reload if you make edits.\ 15 | You will also see any lint errors in the console. 16 | 17 | ### `npm test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `npm run build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `npm run eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 35 | 36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 39 | 40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | -------------------------------------------------------------------------------- /Social-master/frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "social", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@reduxjs/toolkit": "^1.8.3", 7 | "@testing-library/jest-dom": "^5.16.4", 8 | "@testing-library/react": "^13.3.0", 9 | "@testing-library/user-event": "^13.5.0", 10 | "@types/jest": "^27.5.2", 11 | "@types/node": "^16.11.45", 12 | "@types/react": "^18.0.15", 13 | "@types/react-dom": "^18.0.6", 14 | "axios": "^0.27.2", 15 | "date-fns": "^2.29.1", 16 | "emoji-picker-react": "^3.6.1", 17 | "framer-motion": "^6.5.1", 18 | "react": "^18.2.0", 19 | "react-dom": "^18.2.0", 20 | "react-icons": "^4.4.0", 21 | "react-lottie": "^1.2.3", 22 | "react-redux": "^8.0.2", 23 | "react-router-dom": "^6.3.0", 24 | "react-scripts": "5.0.1", 25 | "react-spinners": "^0.13.4", 26 | "socket.io-client": "^4.5.1", 27 | "typescript": "^4.7.4", 28 | "web-vitals": "^2.1.4" 29 | }, 30 | "scripts": { 31 | "start": "react-scripts start", 32 | "build": "react-scripts build", 33 | "test": "react-scripts test", 34 | "eject": "react-scripts eject" 35 | }, 36 | "eslintConfig": { 37 | "extends": [ 38 | "react-app", 39 | "react-app/jest" 40 | ] 41 | }, 42 | "browserslist": { 43 | "production": [ 44 | ">0.2%", 45 | "not dead", 46 | "not op_mini all" 47 | ], 48 | "development": [ 49 | "last 1 chrome version", 50 | "last 1 firefox version", 51 | "last 1 safari version" 52 | ] 53 | }, 54 | "devDependencies": { 55 | "@types/react-lottie": "^1.2.6", 56 | "autoprefixer": "^10.4.8", 57 | "postcss": "^8.4.16", 58 | "tailwindcss": "^3.1.8" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Social-master/frontend/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /Social-master/frontend/public/_redirects: -------------------------------------------------------------------------------- 1 | /* /index.html 200. -------------------------------------------------------------------------------- /Social-master/frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/max0700/Web-Development/d119a79429359c0edf23c035727d81f336e5e649/Social-master/frontend/public/favicon.ico -------------------------------------------------------------------------------- /Social-master/frontend/public/images/bricks.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/max0700/Web-Development/d119a79429359c0edf23c035727d81f336e5e649/Social-master/frontend/public/images/bricks.jpeg -------------------------------------------------------------------------------- /Social-master/frontend/public/images/social-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/max0700/Web-Development/d119a79429359c0edf23c035727d81f336e5e649/Social-master/frontend/public/images/social-logo.png -------------------------------------------------------------------------------- /Social-master/frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 13 | Social 14 | 15 | 16 | 17 |
18 | 19 | 20 | -------------------------------------------------------------------------------- /Social-master/frontend/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Home, 3 | Profile, 4 | Login, 5 | Messanger, 6 | SinglePost, 7 | Verify, 8 | Confirm, 9 | NotFound, 10 | ResetPassword, 11 | UpdatePassword, 12 | } from './pages'; 13 | import { ProtectedRoute, SharedLayout } from './components'; 14 | import { Routes, Route } from 'react-router-dom'; 15 | import { io, Socket } from 'socket.io-client'; 16 | import { useRef, useEffect } from 'react'; 17 | import { 18 | login, 19 | setRefetch, 20 | updateNotifications, 21 | } from './features/user/userSlice'; 22 | import { useAppDispatch } from './app/hooks'; 23 | import { setOnlineUsers, logout } from './features/user/userSlice'; 24 | import { 25 | setRefetchMessages, 26 | setIsTyping, 27 | } from './features/conversations/conversationsSlice'; 28 | import { useAppSelector } from './hooks'; 29 | import { ServerToClientEvents, ClientToServerEvents } from './interfaces'; 30 | import { ProfileInfoContextProvider } from './context'; 31 | // import useSound from 'use-sound'; 32 | // @ts-ignore 33 | import sound from './sounds/notification.mp3'; 34 | import axios from 'axios'; 35 | 36 | axios.defaults.baseURL = 'https://social-backend-production.up.railway.app/api'; 37 | axios.defaults.withCredentials = true; 38 | 39 | const App = () => { 40 | const dispatch = useAppDispatch(); 41 | const socket = useRef | null>(null); 45 | const { user, refetch } = useAppSelector((state) => state.user); 46 | 47 | useEffect(() => { 48 | const updateUser = async () => { 49 | try { 50 | const { data } = await axios.get('/users/me'); 51 | dispatch(login({ ...data })); 52 | } catch (error) { 53 | dispatch(logout()); 54 | console.log(error); 55 | } 56 | }; 57 | updateUser(); 58 | }, [refetch, dispatch]); 59 | 60 | useEffect(() => { 61 | if (user) { 62 | socket.current = io('https://social-backend-production.up.railway.app'); 63 | socket?.current?.emit('addUser', user?._id); 64 | socket?.current?.on('getUsers', (users) => { 65 | dispatch(setOnlineUsers(users)); 66 | }); 67 | socket?.current?.on('getMessage', () => { 68 | // console.log('i got new message'); 69 | // @ts-ignore 70 | dispatch(updateNotifications()); 71 | dispatch(setRefetchMessages()); 72 | const audio = new Audio(sound); 73 | 74 | audio.play(); 75 | }); 76 | socket?.current?.on('getRequest', () => { 77 | const audio = new Audio(sound); 78 | dispatch(setRefetch()); 79 | audio.play(); 80 | }); 81 | socket?.current?.on('typing', () => dispatch(setIsTyping(true))); 82 | socket?.current?.on('stopTyping', () => dispatch(setIsTyping(false))); 83 | } else { 84 | // socket?.current?.disconnect(); 85 | } 86 | }, [user, dispatch]); 87 | 88 | return ( 89 | 90 | }> 91 | 95 | 96 | 97 | } 98 | /> 99 | 103 | 104 | 105 | } 106 | /> 107 | 111 | 112 | 113 | 114 | 115 | } 116 | /> 117 | 121 | 122 | 123 | } 124 | /> 125 | 126 | } /> 127 | } /> 128 | } /> 129 | } /> 130 | } /> 131 | } /> 132 | } /> 133 | 134 | ); 135 | }; 136 | 137 | export default App; 138 | -------------------------------------------------------------------------------- /Social-master/frontend/src/animations/typing.json: -------------------------------------------------------------------------------- 1 | {"v":"5.5.2","fr":60,"ip":0,"op":104,"w":84,"h":40,"nm":"Typing-Indicator","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Oval 3","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.643],"y":[1]},"o":{"x":[1],"y":[0]},"t":18,"s":[35],"e":[100]},{"i":{"x":[0.099],"y":[1]},"o":{"x":[0.129],"y":[0]},"t":33,"s":[100],"e":[35]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":65,"s":[35],"e":[35]},{"t":71}],"ix":11,"x":"var $bm_rt;\n$bm_rt = loopOut('cycle', 0);"},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[61,20,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[1,1,0.333],"y":[0,0,0]},"t":18,"s":[100,100,100],"e":[140,140,100]},{"i":{"x":[0.032,0.032,0.667],"y":[1,1,1]},"o":{"x":[0.217,0.217,0.333],"y":[0,0,0]},"t":33,"s":[140,140,100],"e":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":65,"s":[100,100,100],"e":[100,100,100]},{"t":71}],"ix":6,"x":"var $bm_rt;\n$bm_rt = loopOut('cycle', 0);"}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[12,12],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.847000002861,0.847000002861,0.847000002861,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Oval 3","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":3600,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Oval 2","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[1],"y":[0]},"t":9,"s":[35],"e":[98]},{"i":{"x":[0.023],"y":[1]},"o":{"x":[0.179],"y":[0]},"t":24,"s":[98],"e":[35]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":56,"s":[35],"e":[35]},{"t":62}],"ix":11,"x":"var $bm_rt;\n$bm_rt = loopOut('cycle', 0);"},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[41,20,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.654,0.654,0.667],"y":[1,1,1]},"o":{"x":[1,1,0.333],"y":[0,0,0]},"t":9,"s":[100,100,100],"e":[140,140,100]},{"i":{"x":[0.11,0.11,0.667],"y":[1,1,1]},"o":{"x":[0.205,0.205,0.333],"y":[0,0,0]},"t":24,"s":[140,140,100],"e":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":56,"s":[100,100,100],"e":[100,100,100]},{"t":62}],"ix":6,"x":"var $bm_rt;\n$bm_rt = loopOut('cycle', 0);"}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[12,12],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.847000002861,0.847000002861,0.847000002861,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Oval 2","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":3600,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Oval 1","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[1],"y":[0]},"t":0,"s":[35],"e":[100]},{"i":{"x":[0.067],"y":[1]},"o":{"x":[0.125],"y":[0]},"t":15,"s":[100],"e":[35]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":47,"s":[35],"e":[35]},{"t":53}],"ix":11,"x":"var $bm_rt;\n$bm_rt = loopOut('cycle', 0);"},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[21,20,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.673,0.673,0.667],"y":[1,1,1]},"o":{"x":[1,1,0.333],"y":[0,0,0]},"t":0,"s":[100,100,100],"e":[140,140,100]},{"i":{"x":[0.049,0.049,0.667],"y":[1,1,1]},"o":{"x":[0.198,0.198,0.333],"y":[0,0,0]},"t":15,"s":[140,140,100],"e":[100,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[1,1,1]},"o":{"x":[0.167,0.167,0.167],"y":[0,0,0]},"t":47,"s":[100,100,100],"e":[100,100,100]},{"t":53}],"ix":6,"x":"var $bm_rt;\n$bm_rt = loopOut('cycle', 0);"}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[12,12],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.847000002861,0.847000002861,0.847000002861,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Oval 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":3600,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"BG","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[42,20,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[-42,-20],[42,-20],[42,20],[-42,20]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"rd","nm":"Round Corners 1","r":{"a":0,"k":20,"ix":1},"ix":2,"mn":"ADBE Vector Filter - RC","hd":false},{"ty":"fl","c":{"a":0,"k":[0.96078401804,0.96078401804,0.96078401804,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"BG","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":3600,"st":0,"bm":0}],"markers":[]} -------------------------------------------------------------------------------- /Social-master/frontend/src/app/hooks.ts: -------------------------------------------------------------------------------- 1 | import { TypedUseSelectorHook, useSelector, useDispatch } from 'react-redux'; 2 | import type { RootState, AppDispatch } from './store'; 3 | 4 | export const useAppDispatch = () => useDispatch(); 5 | export const useAppSelector: TypedUseSelectorHook = useSelector; 6 | -------------------------------------------------------------------------------- /Social-master/frontend/src/app/store.ts: -------------------------------------------------------------------------------- 1 | import { configureStore, ThunkAction, Action } from '@reduxjs/toolkit'; 2 | import userReducer from '../features/user/userSlice'; 3 | import postsReducer from '../features/posts/postsSlice'; 4 | import conversationsReducer from '../features/conversations/conversationsSlice'; 5 | 6 | export const store = configureStore({ 7 | reducer: { 8 | user: userReducer, 9 | posts: postsReducer, 10 | conversations: conversationsReducer, 11 | }, 12 | }); 13 | 14 | export type AppDispatch = typeof store.dispatch; 15 | export type RootState = ReturnType; 16 | export type AppThunk = ThunkAction< 17 | ReturnType, 18 | RootState, 19 | unknown, 20 | Action 21 | >; 22 | -------------------------------------------------------------------------------- /Social-master/frontend/src/components/ChatOnline.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useAppSelector } from '../hooks'; 3 | import { IOnlineUser } from '../interfaces'; 4 | 5 | type ChatOnlineProps = { 6 | onlineUsers: IOnlineUser[]; 7 | }; 8 | 9 | const ChatOnline: React.FC = () => { 10 | const { onlineFriends } = useAppSelector((state) => state.user); 11 | // const [onlineFriends, setOnlineFriends] = useState([]); 12 | 13 | // useEffect(() => { 14 | // const onlineUsersId = onlineUsers.map((onlineUser) => onlineUser.userId); 15 | 16 | // setOnlineFriends( 17 | // // @ts-ignore 18 | // user.following.filter((friend) => onlineUsersId.includes(friend._id)) 19 | // ); 20 | // }, [onlineUsers, user.following]); 21 | 22 | return ( 23 |
24 | {onlineFriends && 25 | onlineFriends.map((friend: any) => ( 26 |
27 |
28 | friend online 37 |
38 |
39 | {friend.username} 40 |
41 | ))} 42 |
43 | ); 44 | }; 45 | 46 | export default ChatOnline; 47 | -------------------------------------------------------------------------------- /Social-master/frontend/src/components/Comment.tsx: -------------------------------------------------------------------------------- 1 | import { IComment } from '../interfaces'; 2 | import { Link } from 'react-router-dom'; 3 | import { formatDistanceToNow } from 'date-fns'; 4 | import { updateSelectedPost } from '../features/posts/postsSlice'; 5 | import axios from 'axios'; 6 | import { useAppSelector, useAppDispatch } from '../app/hooks'; 7 | 8 | interface CommentProps { 9 | comment: IComment; 10 | } 11 | 12 | const Comment: React.FC = ({ comment }) => { 13 | const { user } = useAppSelector((state) => state.user); 14 | const dispatch = useAppDispatch(); 15 | const handleDelete = async () => { 16 | try { 17 | const { data } = await axios.delete( 18 | `/posts/${comment.postId}/comment/${comment._id}` 19 | ); 20 | dispatch(updateSelectedPost(data)); 21 | } catch (error) { 22 | console.log(error); 23 | } 24 | }; 25 | // console.log(comment.author); 26 | return ( 27 |
28 | 29 |
30 | 38 |
39 | 40 |
41 |
42 |

43 | {comment.author.username} 44 |

45 |
46 |

47 | {formatDistanceToNow(new Date(comment.createdAt), { 48 | addSuffix: true, 49 | })} 50 |

51 | {user._id === comment.author._id && ( 52 | 56 | 64 | 69 | 70 | 71 | )} 72 |
73 |
74 |

{comment.author.desc}

75 |

{comment.body}

76 |
77 | 78 | {/*
{comment.body}
*/} 79 |
80 | ); 81 | }; 82 | 83 | export default Comment; 84 | -------------------------------------------------------------------------------- /Social-master/frontend/src/components/Conversation.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { IConversation } from '../interfaces'; 3 | import { useDispatch } from 'react-redux'; 4 | import { deleteNotifications } from '../features/user/userSlice'; 5 | import { selectConversation } from '../features/conversations/conversationsSlice'; 6 | import { useAppSelector } from '../hooks'; 7 | 8 | type ConversationProps = { 9 | conversation: IConversation; 10 | }; 11 | 12 | const Conversation: React.FC = ({ conversation }) => { 13 | const { user, onlineUsers } = useAppSelector((state) => state.user); 14 | const { selectedConversation } = useAppSelector( 15 | (state) => state.conversations 16 | ); 17 | const otherUser = conversation.members.find((m) => m._id !== user._id); 18 | const dispatch = useDispatch(); 19 | const ids = onlineUsers.map((o: { userId: string }) => o.userId); 20 | 21 | return ( 22 |
{ 29 | dispatch(selectConversation(conversation)); 30 | // @ts-ignore 31 | dispatch(deleteNotifications(otherUser._id)); 32 | }} 33 | > 34 |
35 | friend online 44 | {ids.includes(otherUser?._id!) && ( 45 |
46 | )} 47 | {/*
*/} 48 |
49 | {/* conversation */} 58 | 59 | {otherUser?.username} 60 | 61 |
62 | ); 63 | }; 64 | 65 | export default Conversation; 66 | -------------------------------------------------------------------------------- /Social-master/frontend/src/components/Feed.tsx: -------------------------------------------------------------------------------- 1 | import { Share, Post } from './'; 2 | import { useEffect, useState } from 'react'; 3 | import { useAppDispatch, useAppSelector } from '../app/hooks'; 4 | import { addPosts, setNumberOfPages } from '../features/posts/postsSlice'; 5 | import { init } from '../features/posts/postsSlice'; 6 | import { useQuery } from '../config/utils'; 7 | import axios from 'axios'; 8 | import { ClientToServerEvents, ServerToClientEvents } from '../interfaces'; 9 | import { Socket } from 'socket.io-client'; 10 | import ClockLoader from 'react-spinners/ClockLoader'; 11 | 12 | type FeedProps = { 13 | profile?: boolean; 14 | userId?: string; 15 | scrollable?: boolean; 16 | socket: React.MutableRefObject | null>; 20 | }; 21 | 22 | const Feed: React.FC = ({ profile, userId, scrollable, socket }) => { 23 | const { user } = useAppSelector((state) => state.user); 24 | const { posts, numberOfPages } = useAppSelector((state) => state.posts); 25 | const [loading, setLoading] = useState(false); 26 | const dispatch = useAppDispatch(); 27 | const query = useQuery(); 28 | const page = query.get('page') || 1; 29 | 30 | const [currentPage, setCurrentPage] = useState(+page); 31 | 32 | useEffect(() => { 33 | const fetchPosts = async () => { 34 | setLoading(true); 35 | try { 36 | const { data } = await axios.get( 37 | profile 38 | ? `/posts/all/${userId}` 39 | : `/posts/timeline/?userId=${user?._id}&page=${page}` 40 | ); 41 | if (profile) { 42 | dispatch(init(data)); 43 | } else { 44 | dispatch(init(data.posts)); 45 | dispatch(setNumberOfPages(data.numberOfPages)); 46 | } 47 | setLoading(false); 48 | } catch (error) { 49 | console.log(error); 50 | setLoading(false); 51 | } 52 | }; 53 | fetchPosts(); 54 | }, [userId, profile, dispatch, user, page]); 55 | 56 | const handleScroll = async (e: any) => { 57 | let triggerHeight = e.target.scrollTop + e.target.offsetHeight; 58 | // console.log(Math.floor(triggerHeight), e.target.scrollHeight); 59 | if (Math.floor(triggerHeight) + 1 >= +e.target.scrollHeight) { 60 | if (currentPage + 1 <= numberOfPages) { 61 | setCurrentPage(currentPage + 1); 62 | try { 63 | const { data } = await axios.get( 64 | `/posts/timeline?userId=${user?._id}&page=${currentPage + 1}` 65 | ); 66 | // console.log(data.posts); 67 | dispatch(addPosts(data.posts)); 68 | } catch (error) { 69 | console.log(error); 70 | } 71 | } 72 | } 73 | }; 74 | 75 | return ( 76 |
80 |
81 | {!profile && } 82 | {/* {user._id === userId && } */} 83 | 84 |
85 | {loading ? ( 86 | 87 | ) : ( 88 | posts.map((p) => ) 89 | )} 90 |
91 |
92 |
93 | ); 94 | }; 95 | 96 | export default Feed; 97 | 98 | -------------------------------------------------------------------------------- /Social-master/frontend/src/components/Friend.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from 'react-router-dom'; 2 | import { IUser } from '../interfaces'; 3 | 4 | type FriendProps = { 5 | user: IUser; 6 | }; 7 | 8 | const Friend: React.FC = ({ user }) => { 9 | return ( 10 | 11 |
  • 12 | friend 21 | {user?.username} 22 |
  • 23 | 24 | ); 25 | }; 26 | 27 | export default Friend; 28 | -------------------------------------------------------------------------------- /Social-master/frontend/src/components/FriendRequest.tsx: -------------------------------------------------------------------------------- 1 | import '../friendRequest.css'; 2 | import { IFriendRequest } from '../interfaces'; 3 | import { formatDistanceToNow } from 'date-fns'; 4 | // import { BsCheck2Circle } from 'react-icons/bs'; 5 | // import { MdOutlineCancel } from 'react-icons/md'; 6 | import { useAppDispatch } from '../app/hooks'; 7 | import axios from 'axios'; 8 | import { addFriend, removeFriendRequest } from '../features/user/userSlice'; 9 | import { Socket } from 'socket.io-client'; 10 | import { ServerToClientEvents, ClientToServerEvents } from '../interfaces'; 11 | 12 | interface FriendRequestProps { 13 | fr: IFriendRequest; 14 | socket?: React.MutableRefObject | null>; 18 | } 19 | 20 | const FriendRequest: React.FC = ({ fr, socket }) => { 21 | const dispatch = useAppDispatch(); 22 | const closeFriendRequest = async (status: string) => { 23 | try { 24 | if (status === 'Accept') { 25 | const { data } = await axios.patch(`/friendRequests/${fr._id}`, { 26 | status, 27 | }); 28 | dispatch(addFriend(data)); 29 | dispatch(removeFriendRequest(fr)); 30 | } else { 31 | await axios.patch(`/friendRequests/${fr._id}`, { 32 | status, 33 | }); 34 | dispatch(removeFriendRequest(fr)); 35 | } 36 | socket?.current?.emit('sendRequest', fr.from._id); 37 | } catch (error) { 38 | console.log(error); 39 | } 40 | }; 41 | return ( 42 |
    43 | {fr.from.username} 52 |
    53 |

    {fr.from.username}

    54 |

    55 | {formatDistanceToNow(new Date(fr.createdAt), { 56 | addSuffix: true, 57 | })} 58 |

    59 |
    60 |
    61 | closeFriendRequest('Accept')} 64 | className="h-8 w-8 friend-request__button" 65 | viewBox="0 0 20 20" 66 | fill="currentColor" 67 | > 68 | 73 | 74 | {/* closeFriendRequest('Accept')} 77 | className="h-8 w-8 friend-request__button" 78 | fill="none" 79 | viewBox="0 0 24 24" 80 | stroke="currentColor" 81 | strokeWidth={2} 82 | > 83 | 88 | */} 89 | closeFriendRequest('Decline')} 92 | className="h-8 w-8 friend-request__button" 93 | viewBox="0 0 20 20" 94 | fill="currentColor" 95 | > 96 | 101 | 102 | {/* closeFriendRequest('Decline')} 105 | className="h-8 w-8 friend-request__button" 106 | fill="none" 107 | viewBox="0 0 24 24" 108 | stroke="currentColor" 109 | strokeWidth={2} 110 | > 111 | 116 | */} 117 | {/* closeFriendRequest('Accept')} 119 | className="friend-request__button" 120 | /> */} 121 | {/* closeFriendRequest('Decline')} 123 | className="friend-request__button" 124 | /> */} 125 |
    126 |
    127 | ); 128 | }; 129 | 130 | export default FriendRequest; 131 | -------------------------------------------------------------------------------- /Social-master/frontend/src/components/Message.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { IMessage } from '../interfaces'; 3 | import { formatDistanceToNow } from 'date-fns'; 4 | 5 | type MessageProps = { 6 | own?: boolean; 7 | message: IMessage; 8 | }; 9 | 10 | const Message: React.FC = ({ own, message }) => { 11 | return ( 12 |
    13 |
    14 | message 23 |

    {message.text}

    24 |
    25 |
    26 | {formatDistanceToNow(new Date(message.createdAt), { 27 | addSuffix: true, 28 | })} 29 |
    30 |
    31 | ); 32 | }; 33 | 34 | export default Message; 35 | -------------------------------------------------------------------------------- /Social-master/frontend/src/components/MessageNotification.tsx: -------------------------------------------------------------------------------- 1 | import { IMessageNotification } from '../interfaces'; 2 | import { formatDistanceToNow } from 'date-fns'; 3 | import { useAppDispatch } from '../app/hooks'; 4 | import { fetchAndSetConversation } from '../features/conversations/conversationsSlice'; 5 | import { useNavigate } from 'react-router-dom'; 6 | import '../messageNotification.css'; 7 | 8 | interface MessageNotificationProps { 9 | notification: IMessageNotification; 10 | } 11 | 12 | const MessageNotification: React.FC = ({ 13 | notification, 14 | }) => { 15 | const dispatch = useAppDispatch(); 16 | const navigate = useNavigate(); 17 | return ( 18 |
    { 21 | navigate('/messanger'); 22 | dispatch(fetchAndSetConversation(notification.conversation)); 23 | }} 24 | > 25 | {notification.from.username} 34 |
    35 |

    {notification.from.username} send you message

    36 |

    37 | {formatDistanceToNow(new Date(notification.createdAt), { 38 | addSuffix: true, 39 | })} 40 |

    41 |
    42 |
    43 | ); 44 | }; 45 | 46 | export default MessageNotification; 47 | -------------------------------------------------------------------------------- /Social-master/frontend/src/components/Online.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { IUser } from '../interfaces'; 4 | 5 | type OnlineProps = { 6 | user: IUser; 7 | }; 8 | 9 | const Online: React.FC = ({ user }) => { 10 | return ( 11 | 15 |
    16 | friend 25 | {/* */} 26 |
    27 | {user?.username} 28 | 29 | ); 30 | }; 31 | 32 | export default Online; 33 | -------------------------------------------------------------------------------- /Social-master/frontend/src/components/Post.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { useAppSelector } from '../hooks'; 3 | import { 4 | ClientToServerEvents, 5 | IPost, 6 | ServerToClientEvents, 7 | } from '../interfaces'; 8 | import { useDispatch } from 'react-redux'; 9 | import { deletePost, updatePost } from '../features/posts/postsSlice'; 10 | import { Link } from 'react-router-dom'; 11 | import { motion } from 'framer-motion'; 12 | import { formatDistanceToNow } from 'date-fns'; 13 | import { Socket } from 'socket.io-client'; 14 | import axios from 'axios'; 15 | 16 | type PostProps = { 17 | post: IPost; 18 | callback?: () => void; 19 | socket: React.MutableRefObject | null>; 23 | }; 24 | 25 | const Post: React.FC = ({ post, callback, socket }) => { 26 | const [likes, setLikes] = useState(post.likes?.length!); 27 | const dispatch = useDispatch(); 28 | 29 | const { user: currentUser } = useAppSelector((state) => state.user); 30 | 31 | const [isLiked, setIsLiked] = useState( 32 | post?.likes?.includes(currentUser._id) 33 | ); 34 | 35 | const handleLike = async () => { 36 | setLikes(isLiked ? likes - 1 : likes + 1); 37 | setIsLiked((prevState) => !prevState); 38 | try { 39 | const { data } = await axios.post(`/posts/${post._id}/like`); 40 | if (!isLiked && post.author._id !== currentUser._id) { 41 | socket?.current?.emit('sendRequest', post.author._id); 42 | } 43 | dispatch(updatePost(data)); 44 | } catch (error) { 45 | console.log(error); 46 | } 47 | }; 48 | 49 | const handleDelete = async () => { 50 | if (post) { 51 | if (post.img) { 52 | //@ts-ignore 53 | const id = post.img.split('/').at(-1).split('.')[0]; 54 | await axios.delete('/upload/' + id); 55 | } 56 | } 57 | // @ts-ignore 58 | dispatch(deletePost(post)); 59 | callback && callback(); 60 | }; 61 | 62 | return ( 63 | 68 |
    69 |
    70 |
    71 |
    72 | 73 | profile 82 | 83 |
    84 | {post.author?.username} 85 | 86 | {formatDistanceToNow(new Date(post?.createdAt!), { 87 | addSuffix: true, 88 | })} 89 | 90 |
    91 |
    92 | {currentUser._id === post.author._id && ( 93 |
    94 | 102 | 107 | 108 |
    109 | )} 110 |
    111 | 112 |
    113 | {post?.desc} 114 | {post?.img && ( 115 | post 116 | )} 117 |
    118 |
    119 |
    120 | heart 126 | 127 | {likes} people like it 128 | 129 |
    130 |
    131 | 135 | {' '} 136 | {post.comments.length} comments 137 | 138 |
    139 |
    140 |
    141 |
    142 |
    143 | ); 144 | }; 145 | 146 | export default Post; 147 | -------------------------------------------------------------------------------- /Social-master/frontend/src/components/PostNotification.tsx: -------------------------------------------------------------------------------- 1 | import { IPostNotification } from '../interfaces'; 2 | import { formatDistanceToNow } from 'date-fns'; 3 | import { Link } from 'react-router-dom'; 4 | import '../messageNotification.css'; 5 | 6 | interface PostNotificationProps { 7 | notification: IPostNotification; 8 | } 9 | 10 | const PostNotification: React.FC = ({ 11 | notification, 12 | }) => { 13 | return ( 14 | 15 | {notification.user.username} 24 |
    25 |

    26 | {notification.user.username}{' '} 27 | {notification.type === 'Like' 28 | ? 'liked your post' 29 | : 'commented on your post'} 30 |

    31 |

    32 | {formatDistanceToNow(new Date(notification?.createdAt), { 33 | addSuffix: true, 34 | })} 35 |

    36 |
    37 | 38 | ); 39 | }; 40 | 41 | export default PostNotification; 42 | -------------------------------------------------------------------------------- /Social-master/frontend/src/components/ProtectedRoute.tsx: -------------------------------------------------------------------------------- 1 | import { Navigate } from 'react-router-dom'; 2 | import { useAppSelector } from '../app/hooks'; 3 | 4 | type ProtectedRouteProps = { 5 | children?: React.ReactNode; 6 | }; 7 | 8 | const ProtectedRoute: React.FC = ({ children }) => { 9 | const { user } = useAppSelector((state) => state.user); 10 | return <>{user ? children : }; 11 | }; 12 | 13 | export default ProtectedRoute; 14 | -------------------------------------------------------------------------------- /Social-master/frontend/src/components/Rightbar.tsx: -------------------------------------------------------------------------------- 1 | import { RightbarFriend } from './'; 2 | import { IFriendRequest, IUser } from '../interfaces'; 3 | import { useAppSelector } from '../app/hooks'; 4 | import { useDispatch } from 'react-redux'; 5 | import { useNavigate } from 'react-router-dom'; 6 | import { addFriendRequest, removeFriend } from '../features/user/userSlice'; 7 | import axios from 'axios'; 8 | import React, { useState, useEffect } from 'react'; 9 | import { Socket } from 'socket.io-client'; 10 | import { ServerToClientEvents, ClientToServerEvents } from '../interfaces'; 11 | import { useProfileInfoContext } from '../context'; 12 | 13 | type RightbarProps = { 14 | user?: IUser; 15 | socket?: React.MutableRefObject | null>; 19 | }; 20 | 21 | const Rightbar: React.FC = ({ user, socket }) => { 22 | const [friends, setFriends] = useState([]); 23 | const { user: currentUser } = useAppSelector((state) => state.user); 24 | const navigate = useNavigate(); 25 | 26 | const dispatch = useDispatch(); 27 | 28 | const createConversation = async () => { 29 | try { 30 | await axios.post('/conversations/' + user?._id); 31 | } catch (error) { 32 | console.log(error); 33 | } 34 | navigate('/messanger'); 35 | }; 36 | 37 | const [isFriend, setIsFriend] = useState( 38 | currentUser?.friends?.map((user: IUser) => user._id).includes(user?._id) 39 | ); 40 | 41 | const hide = 42 | currentUser?.friendRequests 43 | ?.map((fr: IFriendRequest) => fr.from?._id) 44 | .includes(user?._id) || 45 | currentUser?.friendRequests 46 | ?.map((fr: IFriendRequest) => fr.from?._id) 47 | .includes(currentUser?._id); 48 | 49 | const { isEdit, profileData, setProfileData, handleChange } = 50 | useProfileInfoContext(); 51 | 52 | useEffect(() => { 53 | if (setProfileData) { 54 | setProfileData({ 55 | ...profileData, 56 | city: user?.city!, 57 | from: user?.from!, 58 | relationship: user?.relationship!, 59 | }); 60 | } 61 | }, [user]); 62 | 63 | // const [onlineFriends, setOnlineFriends] = useState([]); 64 | 65 | // useEffect(() => { 66 | // // @ts-ignore 67 | // const onlineUsersId = onlineUsers?.map((onlineUser) => onlineUser.userId); 68 | 69 | // setOnlineFriends( 70 | // // @ts-ignore 71 | // currentUser?.friends.filter((friend) => 72 | // onlineUsersId.includes(friend._id) 73 | // ) 74 | // ); 75 | // }, [onlineUsers, currentUser?.friends]); 76 | 77 | const handleClick = async () => { 78 | try { 79 | if (isFriend) { 80 | await axios.delete(`/users/${user?._id}/friend`); 81 | dispatch(removeFriend(user!)); 82 | setIsFriend(!isFriend); 83 | } else { 84 | const { data } = await axios.post('/friendRequests/', { 85 | to: user?._id, 86 | }); 87 | dispatch(addFriendRequest(data)); 88 | } 89 | socket?.current?.emit('sendRequest', user?._id!); 90 | } catch (error) { 91 | console.log(error); 92 | } 93 | }; 94 | 95 | useEffect(() => { 96 | const fetchFriends = async () => { 97 | try { 98 | if (user?._id) { 99 | const { data } = await axios.get('/users/friends/' + user?._id); 100 | setFriends(data); 101 | const friendsId = currentUser?.friends?.map( 102 | // @ts-ignore 103 | (friend) => friend._id 104 | ); 105 | setIsFriend(friendsId?.includes(user._id)); 106 | } 107 | } catch (error) { 108 | console.log(error); 109 | } 110 | }; 111 | 112 | fetchFriends(); 113 | }, [user?._id, currentUser?.friends]); 114 | 115 | const RightbarHome = () => { 116 | return ( 117 | <> 118 |
    119 | birthday 124 | 125 | Pola Foster and 3 others have a birthday today. 126 | 127 |
    128 | {/* ad */} 133 |

    Online Friends

    134 | {/*
      135 | {onlineFriends?.map((u) => ( 136 | 137 | ))} 138 |
    */} 139 | 140 | ); 141 | }; 142 | 143 | const RightbarProfile = () => { 144 | return ( 145 | <> 146 | {currentUser?._id !== user?._id && ( 147 |
    148 | {!hide && ( 149 | 155 | )} 156 | 162 |
    163 | )} 164 |

    165 | {isEdit ? 'Update user information' : 'User Information'} 166 |

    167 | {!isEdit ? ( 168 |
    169 |
    170 | City: 171 | {user?.city} 172 |
    173 |
    174 | From: 175 | {user?.from} 176 |
    177 |
    178 | Relationship: 179 | 180 | {user?.relationship === 1 181 | ? 'Single' 182 | : user?.relationship === 2 183 | ? 'Married' 184 | : 'Married with children'} 185 | 186 |
    187 |
    188 | ) : ( 189 |
    190 |
    191 | 201 | 207 |
    208 | {/*
    209 | 210 | 219 |
    */} 220 |
    221 | 231 | 237 |
    238 | {/*
    239 | 240 | 249 |
    */} 250 |
    251 | 252 | 264 |
    265 |
    266 | )} 267 |

    User friends

    268 |
    269 | {friends?.map((friend) => ( 270 | 271 | ))} 272 |
    273 | 274 | ); 275 | }; 276 | 277 | return ( 278 |
    279 |
    {user ? RightbarProfile() : RightbarHome()}
    280 |
    281 | ); 282 | }; 283 | 284 | export default Rightbar; 285 | -------------------------------------------------------------------------------- /Social-master/frontend/src/components/RightbarFriend.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { IUser } from '../interfaces'; 3 | import { Link } from 'react-router-dom'; 4 | 5 | type RightbarFriendProps = { 6 | friend: IUser; 7 | }; 8 | 9 | const RightbarFriend: React.FC = ({ friend }) => { 10 | return ( 11 | 12 |
    13 | following user 22 | {friend?.username} 23 |
    24 | 25 | ); 26 | }; 27 | 28 | export default RightbarFriend; 29 | -------------------------------------------------------------------------------- /Social-master/frontend/src/components/Share.tsx: -------------------------------------------------------------------------------- 1 | import { MdCancel } from 'react-icons/md'; 2 | import { useAppSelector } from '../hooks'; 3 | import { useDispatch } from 'react-redux'; 4 | import { addPost } from '../features/posts/postsSlice'; 5 | import React, { useState } from 'react'; 6 | import { createRipple } from '../config/createRipple'; 7 | import axios from 'axios'; 8 | import Picker from 'emoji-picker-react'; 9 | 10 | const Share = () => { 11 | const { user } = useAppSelector((state) => state.user); 12 | const [desc, setDesc] = useState(''); 13 | const [file, setFile] = useState(null); 14 | 15 | const [showPicker, setShowPicker] = useState(false); 16 | 17 | const onEmojiClick = (event: any, emojiObject: any) => { 18 | setDesc(desc + emojiObject.emoji); 19 | }; 20 | 21 | const dispatch = useDispatch(); 22 | 23 | const handleSubmit = async (e: React.FormEvent) => { 24 | e.preventDefault(); 25 | if (!desc && !file) return; 26 | const newPost = { 27 | author: user._id, 28 | desc, 29 | img: '', 30 | }; 31 | if (file) { 32 | const formData = new FormData(); 33 | // @ts-ignore 34 | // const fileName = Date.now() + file.name; 35 | // data.append('name', fileName); 36 | formData.append('file', file); 37 | // newPost.img = fileName; 38 | try { 39 | const { data: imageData } = await axios.post('/upload', formData); 40 | newPost.img = imageData.secure_url; 41 | } catch (error) { 42 | console.log(error); 43 | } 44 | } 45 | try { 46 | const { data } = await axios.post('/posts', newPost); 47 | setDesc(''); 48 | setFile(null); 49 | dispatch(addPost(data)); 50 | } catch (error) { 51 | console.log(error); 52 | } 53 | }; 54 | return ( 55 |
    56 |
    57 |

    Create new post

    58 | 59 |
    60 |
    61 | profile 70 |