└── 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 | 
23 | 
24 | 
25 | 
26 | 
27 | 
28 | 
29 | 
30 | 
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 |

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 |
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 |

44 | {ids.includes(otherUser?._id!) && (
45 |
46 | )}
47 | {/*
*/}
48 |
49 | {/*

*/}
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 |
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 |

52 |
53 |
{fr.from.username}
54 |
55 | {formatDistanceToNow(new Date(fr.createdAt), {
56 | addSuffix: true,
57 | })}
58 |
59 |
60 |
61 |
74 | {/*
*/}
89 |
102 | {/*
*/}
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 |

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 |

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 |

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 |

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 |
109 | )}
110 |
111 |
112 |
113 |
{post?.desc}
114 | {post?.img && (
115 |

116 | )}
117 |
118 |
119 |
120 |

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 |
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 |

124 |
125 | Pola Foster and 3 others have a birthday today.
126 |
127 |
128 | {/*
*/}
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 |
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 |

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 |
163 |
164 |
165 | );
166 | };
167 |
168 | export default Share;
169 |
--------------------------------------------------------------------------------
/Social-master/frontend/src/components/SharedLayout.tsx:
--------------------------------------------------------------------------------
1 | import { Outlet } from 'react-router-dom';
2 | import { Navbar } from './';
3 | import { Socket } from 'socket.io-client';
4 | import { ServerToClientEvents, ClientToServerEvents } from '../interfaces';
5 |
6 | type SharedLayoutProps = {
7 | socket?: React.MutableRefObject | null>;
11 | };
12 |
13 | const SharedLayout: React.FC = ({ socket }) => {
14 | return (
15 | <>
16 |
17 |
18 | >
19 | );
20 | };
21 |
22 | export default SharedLayout;
23 |
--------------------------------------------------------------------------------
/Social-master/frontend/src/components/Sidebar.tsx:
--------------------------------------------------------------------------------
1 | import { Link } from 'react-router-dom';
2 | import { useAppSelector } from '../hooks';
3 | import { onlyUniqueNotifications } from '../config/utils';
4 | import { IoPeopleOutline } from 'react-icons/io5';
5 | import { useEffect } from 'react';
6 | import { useAppDispatch } from '../app/hooks';
7 | import { setOnlineFriends } from '../features/user/userSlice';
8 | // import { IUser } from '../interfaces';
9 | import { Online } from './';
10 | // import { FaUsers } from 'react-icons/fa';
11 |
12 | const Sidebar = () => {
13 | const { user, onlineUsers, onlineFriends } = useAppSelector(
14 | (state) => state.user
15 | );
16 | const dispatch = useAppDispatch();
17 | // const [onlineFriends, setOnlineFriends] = useState([]);
18 |
19 | useEffect(() => {
20 | // @ts-ignore
21 | const onlineUsersId = onlineUsers?.map((onlineUser) => onlineUser.userId);
22 |
23 | dispatch(
24 | setOnlineFriends(
25 | // @ts-ignore
26 | user?.friends.filter((friend) => onlineUsersId.includes(friend._id))
27 | )
28 | );
29 | }, [onlineUsers, user?.friends, dispatch]);
30 | // console.log(onlineFriends);
31 | return (
32 |
33 |
34 |
35 |
36 | -
37 |
51 |
52 | My Profile
53 |
54 |
55 |
56 | -
57 |
71 | News
72 |
73 |
74 |
75 | -
76 |
90 | Messanger
91 | {user?.messageNotifications?.length > 0 && (
92 |
93 | {
94 | user?.messageNotifications?.filter(onlyUniqueNotifications)
95 | .length
96 | }
97 |
98 | )}
99 |
100 |
101 |
102 |
103 | -
104 |
105 | Friends
106 |
107 |
108 |
109 | -
110 |
123 | Communities
124 |
125 |
126 |
127 |
128 | -
129 |
142 | Files
143 |
144 |
145 |
146 |
147 | -
148 |
162 |
163 | Online friends
164 |
165 |
166 | {onlineFriends.map((friend: any) => (
167 |
168 | ))}
169 |
170 |
171 |
172 |
173 | );
174 | };;;;;;;;;;
175 |
176 | export default Sidebar;
177 |
--------------------------------------------------------------------------------
/Social-master/frontend/src/components/index.ts:
--------------------------------------------------------------------------------
1 | export { default as Navbar } from './Navbar';
2 | export { default as Sidebar } from './Sidebar';
3 | export { default as Feed } from './Feed';
4 | export { default as Rightbar } from './Rightbar';
5 | export { default as Share } from './Share';
6 | export { default as Post } from './Post';
7 | export { default as Online } from './Online';
8 | export { default as Friend } from './Friend';
9 | export { default as Conversation } from './Conversation';
10 | export { default as Message } from './Message';
11 | export { default as Comment } from './Comment';
12 | export { default as ChatOnline } from './ChatOnline';
13 | export { default as SharedLayout } from './SharedLayout';
14 | export { default as RightbarFriend } from './RightbarFriend';
15 | export { default as ProtectedRoute } from './ProtectedRoute';
16 | export { default as FriendRequest } from './FriendRequest';
17 | export { default as MessageNotification } from './MessageNotification';
18 | export { default as PostNotification } from './PostNotification';
19 |
--------------------------------------------------------------------------------
/Social-master/frontend/src/config/createRipple.ts:
--------------------------------------------------------------------------------
1 | export const createRipple = (e: any) => {
2 | const button = e.currentTarget;
3 | const circle = document.createElement('span');
4 | const diameter = Math.max(button.clientWidth, button.clientHeight);
5 | const radius = diameter / 2;
6 |
7 | circle.style.width = circle.style.height = `${diameter}px`;
8 | circle.style.left = `${e.clientX - (button.offsetLeft + radius)}px`;
9 | circle.style.top = `${e.clientY - (button.offsetTop + radius)}px`;
10 | circle.classList.add('ripple');
11 |
12 | const ripple = button.getElementsByClassName('ripple')[0];
13 | if (ripple) ripple.remove();
14 | button.appendChild(circle);
15 | };
16 |
--------------------------------------------------------------------------------
/Social-master/frontend/src/config/utils.ts:
--------------------------------------------------------------------------------
1 | import { useLocation } from 'react-router-dom';
2 | import { IMessageNotification } from '../interfaces';
3 | export const useQuery = () => new URLSearchParams(useLocation().search);
4 |
5 | export function onlyUniqueNotifications(
6 | value: IMessageNotification,
7 | index: number,
8 | self: IMessageNotification[]
9 | ): boolean {
10 | return self?.map((n) => n?.from?._id).indexOf(value?.from?._id) === index;
11 | }
12 |
--------------------------------------------------------------------------------
/Social-master/frontend/src/context.tsx:
--------------------------------------------------------------------------------
1 | import React, { useContext, createContext, useState } from 'react';
2 |
3 | interface IProfileData {
4 | username: string;
5 | desc: string;
6 | city: string;
7 | from: string;
8 | relationship: number;
9 | }
10 |
11 | interface IProfileInfoContext {
12 | isEdit: boolean;
13 | setIsEdit: React.Dispatch>;
14 | refetch: boolean;
15 | setRefetch: React.Dispatch>;
16 | profileData: IProfileData;
17 | setProfileData: React.Dispatch>;
18 | handleChange: (
19 | e: React.ChangeEvent
20 | ) => void;
21 | }
22 |
23 | export const ProfileInfoContext = createContext(
24 | {} as IProfileInfoContext
25 | );
26 |
27 | interface ProfileInfoContextProviderProps {
28 | children: React.ReactNode;
29 | }
30 |
31 | export const ProfileInfoContextProvider: React.FC<
32 | ProfileInfoContextProviderProps
33 | > = ({ children }) => {
34 | const [refetch, setRefetch] = useState(false);
35 | const [isEdit, setIsEdit] = useState(false);
36 | const [profileData, setProfileData] = useState({
37 | username: '',
38 | desc: '',
39 | city: '',
40 | from: '',
41 | relationship: 1,
42 | });
43 | const handleChange = (
44 | e: React.ChangeEvent
45 | ) => {
46 | setProfileData({
47 | ...profileData,
48 | [e.target.name]: e.target.value,
49 | });
50 | };
51 | return (
52 |
63 | {children}
64 |
65 | );
66 | };
67 |
68 | export const useProfileInfoContext = () => useContext(ProfileInfoContext);
69 |
--------------------------------------------------------------------------------
/Social-master/frontend/src/features/conversations/conversationsSlice.ts:
--------------------------------------------------------------------------------
1 | import { createSlice, PayloadAction, createAsyncThunk } from '@reduxjs/toolkit';
2 | import { IConversation, IMessage } from '../../interfaces';
3 | import axios from 'axios';
4 |
5 | type initialStateType = {
6 | conversations: IConversation[];
7 | selectedConversation: IConversation | null;
8 | messages: IMessage[];
9 | refetchMessages: boolean;
10 | isTyping: boolean;
11 | };
12 |
13 | const initialState: initialStateType = {
14 | conversations: [],
15 | selectedConversation: null,
16 | messages: [],
17 | refetchMessages: false,
18 | isTyping: false,
19 | };
20 |
21 | export const fetchAndSetConversation = createAsyncThunk(
22 | '/conversations/fetchAndSetConversation',
23 | async (conversationId: any, thunkAPI) => {
24 | try {
25 | const { data } = await axios.get('/conversations/' + conversationId);
26 | return data;
27 | } catch (error) {
28 | thunkAPI.rejectWithValue(error);
29 | }
30 | }
31 | );
32 |
33 | export const conversationsSlice = createSlice({
34 | name: 'conversations',
35 | initialState,
36 | reducers: {
37 | setConversations: (state, action) => {
38 | state.conversations = action.payload;
39 | },
40 | setIsTyping: (state, action: PayloadAction) => {
41 | state.isTyping = action.payload;
42 | },
43 | selectConversation: (state, action) => {
44 | state.selectedConversation = action.payload;
45 | },
46 | setMessages: (state, action) => {
47 | state.messages = action.payload;
48 | },
49 | addMessage: (state, action) => {
50 | state.messages.unshift(action.payload);
51 | },
52 | setRefetchMessages: (state) => {
53 | state.refetchMessages = !state.refetchMessages;
54 | },
55 | },
56 | extraReducers: (builder) => {
57 | builder.addCase(fetchAndSetConversation.fulfilled, (state, action) => {
58 | state.selectedConversation = action.payload;
59 | });
60 | },
61 | });
62 |
63 | export const {
64 | setConversations,
65 | selectConversation,
66 | setMessages,
67 | addMessage,
68 | setRefetchMessages,
69 | setIsTyping,
70 | } = conversationsSlice.actions;
71 |
72 | export default conversationsSlice.reducer;
73 |
--------------------------------------------------------------------------------
/Social-master/frontend/src/features/posts/postsSlice.ts:
--------------------------------------------------------------------------------
1 | import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
2 | import axios from 'axios';
3 | import { IPost } from '../../interfaces';
4 |
5 | type initialStateType = {
6 | posts: IPost[];
7 | selectedPost: IPost | null;
8 | numberOfPages: number;
9 | };
10 |
11 | const initialState: initialStateType = {
12 | posts: [],
13 | selectedPost: null,
14 | numberOfPages: 1,
15 | };
16 |
17 | export const deletePost = createAsyncThunk(
18 | '/posts/deletePost',
19 | async (post: IPost, thunkAPI) => {
20 | try {
21 |
22 | await axios.delete('/posts/' + post._id);
23 | return post;
24 | } catch (error) {
25 | thunkAPI.rejectWithValue(error);
26 | }
27 | }
28 | );
29 |
30 | export const postsSlice = createSlice({
31 | name: 'posts',
32 | initialState,
33 | reducers: {
34 | setNumberOfPages: (state, action: PayloadAction) => {
35 | state.numberOfPages = action.payload;
36 | },
37 | init: (state, action: PayloadAction) => {
38 | state.posts = action.payload;
39 | },
40 | addPost: (state, action: PayloadAction) => {
41 | state.posts.unshift(action.payload);
42 | },
43 | addPosts: (state, action: PayloadAction) => {
44 | state.posts = [...state.posts, ...action.payload];
45 | },
46 | updatePost: (state, action: PayloadAction) => {
47 | state.posts = state.posts.map((post) => {
48 | if (post._id === action.payload._id) {
49 | return action.payload;
50 | } else return post;
51 | });
52 | },
53 | updateSelectedPost: (state, action: PayloadAction) => {
54 | state.selectedPost = action.payload;
55 | },
56 | },
57 | extraReducers: (builder) => {
58 | builder
59 | .addCase(deletePost.pending, (state, action) => {
60 | console.log('delete post pendig');
61 | })
62 | .addCase(deletePost.fulfilled, (state, action) => {
63 | state.posts = state.posts.filter(
64 | (post) => post._id !== action?.payload?._id
65 | );
66 | })
67 | .addCase(deletePost.rejected, (state, action) => {
68 | console.log('delete post rejected', action.payload);
69 | });
70 | },
71 | });
72 |
73 | export const {
74 | init,
75 | addPost,
76 | updatePost,
77 | updateSelectedPost,
78 | addPosts,
79 | setNumberOfPages,
80 | } = postsSlice.actions;
81 |
82 | export default postsSlice.reducer;
83 |
--------------------------------------------------------------------------------
/Social-master/frontend/src/features/user/userSlice.ts:
--------------------------------------------------------------------------------
1 | import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit';
2 | import { IFriendRequest, IPostNotification, IUser } from '../../interfaces';
3 | import axios from 'axios';
4 |
5 | let user: any = null;
6 |
7 | if (localStorage.getItem('user')) {
8 | user = JSON.parse(localStorage.getItem('user')!);
9 | }
10 |
11 | const initialState = {
12 | user,
13 | refetch: false,
14 | onlineUsers: [],
15 | onlineFriends: [],
16 | friendRequests: [],
17 | };
18 |
19 | export const updateNotifications = createAsyncThunk(
20 | '/user/updateNotifications',
21 | async (_, thunkAPI) => {
22 | try {
23 | const { data } = await axios.get('/messageNotifications/');
24 | return data;
25 | } catch (error) {
26 | thunkAPI.rejectWithValue(error);
27 | }
28 | }
29 | );
30 |
31 | export const deleteNotifications = createAsyncThunk(
32 | '/user/deleteNotifications',
33 | async (from: string, thunkAPI) => {
34 | try {
35 | await axios.delete('/messageNotifications/' + from);
36 | return from;
37 | } catch (error) {
38 | thunkAPI.rejectWithValue(error);
39 | }
40 | }
41 | );
42 |
43 | export const logout = createAsyncThunk('/user/logout', async (_, thunkAPI) => {
44 | try {
45 | const { data } = await axios.get('/auth/logout');
46 | return data;
47 | } catch (error) {
48 | thunkAPI.rejectWithValue(error);
49 | }
50 | });
51 |
52 | export const userSlice = createSlice({
53 | name: 'user',
54 | initialState,
55 | reducers: {
56 | login: (state, action) => {
57 | state.user = action.payload;
58 | state.friendRequests = action.payload.friendRequests?.filter(
59 | (fr: IFriendRequest) => fr.from._id !== action.payload._id
60 | );
61 | localStorage.setItem('user', JSON.stringify(action.payload));
62 |
63 | state.user.postNotifications = action.payload.postNotifications.filter(
64 | (p: IPostNotification) => p.user._id !== state.user._id
65 | );
66 | },
67 | setOnlineUsers: (state, action) => {
68 | state.onlineUsers = action.payload;
69 | },
70 | setOnlineFriends: (state, action) => {
71 | state.onlineFriends = action.payload;
72 | },
73 | setRefetch: (state) => {
74 | state.refetch = !state.refetch;
75 | },
76 | updateUser: (state, action) => {
77 | state.user = action.payload;
78 | state.user.postNotifications = action.payload.postNotifications.filter(
79 | (p: IPostNotification) => p.user._id !== state.user._id
80 | );
81 | state.friendRequests = action.payload.friendRequests?.filter(
82 | (fr: IFriendRequest) => fr.from._id !== action.payload._id
83 | );
84 | localStorage.setItem('user', JSON.stringify(action.payload));
85 | },
86 | addFriendRequest: (state, action: PayloadAction) => {
87 | state.user?.friendRequests.push(action.payload);
88 | },
89 | removeFriendRequest: (state, action: PayloadAction) => {
90 | state.user.friendRequests = state.user?.friendRequests.filter(
91 | (fr: IFriendRequest) => fr._id !== action.payload._id
92 | );
93 | state.friendRequests = state.friendRequests?.filter(
94 | (fr: IFriendRequest) => fr._id !== action.payload._id
95 | );
96 | },
97 | removeNotifications: (state, action: PayloadAction) => {
98 | state.user.postNotifications = state.user.postNotifications.filter(
99 | (n: IPostNotification) => n.post !== action.payload
100 | );
101 | },
102 | addFriend: (state, action: PayloadAction) => {
103 | state.user?.friends.push(action.payload);
104 | },
105 | removeFriend: (state, action: PayloadAction) => {
106 | state.user.friends = state.user?.friends.filter(
107 | (user: IUser) => user._id !== action.payload._id
108 | );
109 | },
110 | },
111 | extraReducers: (builder) => {
112 | builder
113 | .addCase(updateNotifications.fulfilled, (state, action) => {
114 | state.user.messageNotifications = action.payload;
115 | })
116 | .addCase(deleteNotifications.fulfilled, (state, action) => {
117 | state.user.messageNotifications =
118 | state.user.messageNotifications.filter(
119 | (n: any) => n.from._id !== action.payload
120 | );
121 | })
122 | .addCase(logout.fulfilled, (state, action) => {
123 | state.user = null;
124 | state.friendRequests = [];
125 | state.onlineUsers = [];
126 | localStorage.removeItem('user');
127 | });
128 | },
129 | });
130 |
131 | export const {
132 | login,
133 | setOnlineUsers,
134 | updateUser,
135 | addFriendRequest,
136 | removeFriendRequest,
137 | removeNotifications,
138 | removeFriend,
139 | addFriend,
140 | setRefetch,
141 | setOnlineFriends,
142 | } = userSlice.actions;
143 |
144 | export default userSlice.reducer;
145 |
--------------------------------------------------------------------------------
/Social-master/frontend/src/friendRequest.css:
--------------------------------------------------------------------------------
1 | .friend-request {
2 | display: flex;
3 | justify-content: space-between;
4 | padding: 1rem;
5 | gap: 0.625rem;
6 | }
7 |
8 | .friend-request__image {
9 | width: 3.125rem;
10 | height: 3.125rem;
11 | object-fit: cover;
12 | border-radius: 50%;
13 | }
14 |
15 | .friend-request__info {
16 | display: flex;
17 | flex-direction: column;
18 | align-items: flex-start;
19 | justify-content: center;
20 | font-size: 0.75rem;
21 | font-weight: bold;
22 | border-bottom: 0.5px solid #f1f1f1;
23 | }
24 |
25 | .friend-request__info p:nth-child(2) {
26 | font-size: 0.7rem;
27 | color: rgb(190, 190, 190);
28 | }
29 |
30 | .friend-request__actions {
31 | display: flex;
32 | justify-content: center;
33 | gap: 0.625rem;
34 | align-items: center;
35 | }
36 |
37 | .friend-request__button {
38 | font-size: 2rem;
39 | transition: transform 350ms;
40 | }
41 |
42 | .friend-request__button:first-child {
43 | color: #4ade80;
44 | }
45 |
46 | .friend-request__button:last-child {
47 | color: #f43f5e;
48 | }
49 |
50 | .friend-request__button:hover {
51 | transform: scale(0.9);
52 | }
53 |
--------------------------------------------------------------------------------
/Social-master/frontend/src/hooks.ts:
--------------------------------------------------------------------------------
1 | import { TypedUseSelectorHook, useSelector } from 'react-redux';
2 | import { RootState } from './app/store';
3 |
4 | export const useAppSelector: TypedUseSelectorHook = useSelector;
5 |
--------------------------------------------------------------------------------
/Social-master/frontend/src/images/404.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/max0700/Web-Development/d119a79429359c0edf23c035727d81f336e5e649/Social-master/frontend/src/images/404.webp
--------------------------------------------------------------------------------
/Social-master/frontend/src/images/bricks.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/max0700/Web-Development/d119a79429359c0edf23c035727d81f336e5e649/Social-master/frontend/src/images/bricks.jpeg
--------------------------------------------------------------------------------
/Social-master/frontend/src/images/confirm.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/max0700/Web-Development/d119a79429359c0edf23c035727d81f336e5e649/Social-master/frontend/src/images/confirm.png
--------------------------------------------------------------------------------
/Social-master/frontend/src/images/social-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/max0700/Web-Development/d119a79429359c0edf23c035727d81f336e5e649/Social-master/frontend/src/images/social-logo.png
--------------------------------------------------------------------------------
/Social-master/frontend/src/images/verify.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/max0700/Web-Development/d119a79429359c0edf23c035727d81f336e5e649/Social-master/frontend/src/images/verify.png
--------------------------------------------------------------------------------
/Social-master/frontend/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom/client';
3 | import { BrowserRouter } from 'react-router-dom';
4 | import { Provider } from 'react-redux';
5 | import { store } from './app/store';
6 | import './index.css';
7 | import App from './App';
8 |
9 | const root = ReactDOM.createRoot(
10 | document.getElementById('root') as HTMLElement
11 | );
12 | root.render(
13 |
14 |
15 |
16 |
17 |
18 | );
19 |
20 |
21 |
--------------------------------------------------------------------------------
/Social-master/frontend/src/interfaces.ts:
--------------------------------------------------------------------------------
1 | export interface IUser {
2 | _id: string;
3 | profilePicture: string;
4 | coverPicture: string;
5 | username: string;
6 | desc: string;
7 | city: string;
8 | from: string;
9 | relationship: 1 | 2 | 3;
10 | messageNotifications: IMessageNotification[];
11 | postNotifications: IPostNotification[];
12 | following: IUser[];
13 | followers: IUser[];
14 | friends: IUser[];
15 | friendRequests: IFriendRequest[];
16 | }
17 |
18 | export interface IFriendRequest {
19 | _id: string;
20 | from: IUser;
21 | to: IUser;
22 | createdAt: string;
23 | }
24 |
25 | export interface IPost {
26 | _id: string;
27 | desc: string;
28 | img: string;
29 | updatedAt: string;
30 | createdAt: string;
31 | author: IUser;
32 | likes: string[];
33 | comments: IComment[];
34 | }
35 |
36 | export interface IComment {
37 | _id: string;
38 | author: IUser;
39 | body: string;
40 | postId: string;
41 | createdAt: string;
42 | }
43 |
44 | export interface IConversation {
45 | _id: string;
46 | members: IUser[];
47 | }
48 |
49 | export interface IMessageNotification {
50 | _id: string;
51 | conversation: IConversation;
52 | from: IUser;
53 | to: IUser;
54 | createdAt: string;
55 | }
56 |
57 | export interface IPostNotification {
58 | _id: string;
59 | createdAt: string;
60 | user: IUser;
61 | post: string;
62 | type: string;
63 | }
64 |
65 | export interface IMessage {
66 | _id: string;
67 | conversationId: string;
68 | text: string;
69 | sender: IUser;
70 | createdAt: string;
71 | }
72 |
73 | export interface IOnlineUser {
74 | userId: string;
75 | socketId: string;
76 | }
77 |
78 | export interface IError {
79 | username?: string;
80 | email?: string;
81 | password?: string;
82 | confirmPassword?: string;
83 | }
84 |
85 | export interface ServerToClientEvents {
86 | getUsers: (users: IOnlineUser[]) => void;
87 | getMessage: () => void;
88 | getRequest: () => void;
89 | typing: () => void;
90 | stopTyping: () => void;
91 | }
92 |
93 | export interface ClientToServerEvents {
94 | addUser: (userId: string) => void;
95 | sendMessage: (receiverId: string) => void;
96 | sendRequest: (receiverId: string) => void;
97 | typing: (receiverId: string) => void;
98 | stopTyping: (receiverId: string) => void;
99 | }
100 |
101 |
--------------------------------------------------------------------------------
/Social-master/frontend/src/messageNotification.css:
--------------------------------------------------------------------------------
1 | .message-notification {
2 | display: flex;
3 | padding: 1rem;
4 | gap: 0.625rem;
5 | overflow: hidden;
6 | transition: background-color 0.3s;
7 | }
8 |
9 | .message-notification:hover {
10 | background-color: rgb(240, 240, 240);
11 | }
12 |
13 | .message-notification__image {
14 | width: 3.125rem;
15 | height: 3.125rem;
16 | object-fit: cover;
17 | border-radius: 50%;
18 | }
19 |
20 | .message-notification__info {
21 | display: flex;
22 | flex-direction: column;
23 | align-items: flex-start;
24 | justify-content: center;
25 | font-size: 0.75rem;
26 | font-weight: bold;
27 | border-bottom: 0.5px solid #f1f1f1;
28 | }
29 |
30 | .message-notification__info p:nth-child(2) {
31 | font-size: 0.7rem;
32 | color: rgb(190, 190, 190);
33 | }
34 |
--------------------------------------------------------------------------------
/Social-master/frontend/src/pages/Confirm.tsx:
--------------------------------------------------------------------------------
1 | import { useParams, Link } from 'react-router-dom';
2 | import { useEffect, useState } from 'react';
3 | import axios from 'axios';
4 |
5 | import confirm from '../images/confirm.png';
6 |
7 | const Confirm = () => {
8 | const [isLoading, setIsLoading] = useState(true);
9 | const [isError, setIsError] = useState(false);
10 |
11 | const { token } = useParams();
12 |
13 | useEffect(() => {
14 | const confirmEmail = async () => {
15 | try {
16 | setIsLoading(true);
17 | await axios.post('/auth/verify', {
18 | token,
19 | });
20 | setIsLoading(false);
21 | } catch (error) {
22 | console.log(error);
23 | setIsError(true);
24 | setIsLoading(false);
25 | }
26 | };
27 | confirmEmail();
28 | }, [token]);
29 | if (isLoading) {
30 | return (
31 |
32 |
33 |
49 |
Loading...
50 |
51 |
52 | );
53 | }
54 |
55 | if (isError) {
56 | return (
57 |
58 |
59 |
60 | There was an error, please double check your verification link
61 |
62 |
63 |
64 | );
65 | } else {
66 | return (
67 |
68 |
69 |
70 |

75 |
76 |
77 | You have successfully created account!
78 |
79 |
80 | Go to Login Page
81 |
82 |
83 |
84 | );
85 | }
86 | };
87 |
88 | export default Confirm;
89 |
--------------------------------------------------------------------------------
/Social-master/frontend/src/pages/Home.tsx:
--------------------------------------------------------------------------------
1 | import { Sidebar, Feed } from '../components';
2 | import { Socket } from 'socket.io-client';
3 | import { ServerToClientEvents, ClientToServerEvents } from '../interfaces';
4 |
5 | type HomeProps = {
6 | socket: React.MutableRefObject | null>;
10 | };
11 |
12 | const Home: React.FC = ({ socket }) => {
13 | return (
14 |
15 |
16 |
17 |
18 | {/* */}
19 |
20 |
21 | );
22 | };
23 |
24 | export default Home;
25 |
--------------------------------------------------------------------------------
/Social-master/frontend/src/pages/Messanger.tsx:
--------------------------------------------------------------------------------
1 | import { Conversation, Message } from '../components';
2 | import { useAppSelector } from '../hooks';
3 | import { useDispatch } from 'react-redux';
4 | import React, { useEffect, useState, useRef } from 'react';
5 | import Picker from 'emoji-picker-react';
6 | import {
7 | ServerToClientEvents,
8 | ClientToServerEvents,
9 | IUser,
10 | IConversation,
11 | } from '../interfaces';
12 | import {
13 | setOnlineUsers,
14 | deleteNotifications,
15 | } from '../features/user/userSlice';
16 | import {
17 | setConversations,
18 | setMessages,
19 | addMessage,
20 | } from '../features/conversations/conversationsSlice';
21 | import axios from 'axios';
22 | import { Socket } from 'socket.io-client';
23 | import animationData from '../animations/typing.json';
24 | import Lottie from 'react-lottie';
25 |
26 | const defaultOptions = {
27 | loop: true,
28 | autoplay: true,
29 | animationData,
30 | rendererSettings: {
31 | preserveAspectRatio: 'xMidYMid slice',
32 | },
33 | };
34 |
35 | type MessagerProps = {
36 | socket: React.MutableRefObject | null>;
40 | };
41 |
42 | const Messanger: React.FC = ({ socket }) => {
43 | const dispatch = useDispatch();
44 | const [showPicker, setShowPicker] = useState(false);
45 | const { user } = useAppSelector((state) => state.user);
46 | const {
47 | conversations,
48 | selectedConversation,
49 | messages,
50 | refetchMessages,
51 | isTyping,
52 | } = useAppSelector((state) => state.conversations);
53 |
54 | const textareaRef = useRef(null);
55 | const [newMessage, setNewMessage] = useState('');
56 | const [receiver, setReceiver] = useState(null);
57 | const [filter, setFilter] = useState('');
58 |
59 | const [typing, setTyping] = useState(false);
60 |
61 | const onEmojiClick = (event: any, emojiObject: any) => {
62 | setNewMessage(newMessage + emojiObject.emoji);
63 | };
64 |
65 | const [filteredConversations, setFilteredConversations] = useState<
66 | IConversation[]
67 | >([]);
68 |
69 | useEffect(() => {
70 | socket?.current?.emit('addUser', user._id);
71 | socket?.current?.on('getUsers', (users) => {
72 | dispatch(setOnlineUsers(users));
73 | });
74 | }, [user, socket, dispatch]);
75 |
76 | useEffect(() => {
77 | const fetchConversations = async () => {
78 | try {
79 | const { data } = await axios.get('/conversations');
80 | dispatch(setConversations(data));
81 | setFilteredConversations(data);
82 | } catch (error) {
83 | console.log(error);
84 | }
85 | };
86 | fetchConversations();
87 | }, [dispatch, user._id]);
88 |
89 | useEffect(() => {
90 | const fetchMessages = async () => {
91 | try {
92 | if (selectedConversation) {
93 | const { data } = await axios.get(
94 | '/messages/' + selectedConversation?._id
95 | );
96 |
97 | dispatch(setMessages(data));
98 | const otherUser = selectedConversation.members.find(
99 | (m) => m._id !== user._id
100 | );
101 | // @ts-ignore
102 | dispatch(deleteNotifications(otherUser._id));
103 | setReceiver(
104 | selectedConversation?.members.find((m) => user._id !== m._id)!
105 | );
106 | }
107 | } catch (error) {
108 | console.log(error);
109 | }
110 | };
111 | fetchMessages();
112 | textareaRef?.current?.focus();
113 | }, [selectedConversation, dispatch, refetchMessages, user._id]);
114 |
115 | const handleSend = async () => {
116 | try {
117 | const { data } = await axios.post('/messages/', {
118 | sender: user._id,
119 | receiver: receiver?._id!,
120 | text: newMessage,
121 | conversationId: selectedConversation?._id,
122 | });
123 | dispatch(addMessage(data));
124 |
125 | socket?.current?.emit('sendMessage', receiver?._id!);
126 | socket?.current?.emit('stopTyping', receiver?._id!);
127 | } catch (error) {
128 | console.log(error);
129 | }
130 | setNewMessage('');
131 | };
132 | const handleFilter = async () => {
133 | if (filter) {
134 | setFilteredConversations(
135 | conversations.filter((c) => {
136 | const otherUser = c.members.find(
137 | (member) => member.username !== user.username
138 | );
139 | console.log(otherUser);
140 | if (otherUser) {
141 | const exp = new RegExp(filter, 'i');
142 |
143 | return otherUser.username.match(exp);
144 | }
145 | return true;
146 | })
147 | );
148 | } else {
149 | setFilteredConversations(conversations);
150 | }
151 | };
152 | useEffect(() => {
153 | handleFilter();
154 | }, [filter]);
155 | const handleTyping = (e: React.ChangeEvent) => {
156 | setNewMessage(e.target.value);
157 | if (!typing) {
158 | setTyping(true);
159 | socket?.current?.emit('typing', receiver?._id!);
160 | }
161 | const lastTypingTime = new Date().getTime();
162 | const timerLength = 3000;
163 | setTimeout(() => {
164 | const timeNow = new Date().getTime();
165 | const timeDiff = timeNow - lastTypingTime;
166 | if (timeDiff >= timerLength && typing) {
167 | setTyping(false);
168 | socket?.current?.emit('stopTyping', receiver?._id!);
169 | }
170 | }, timerLength);
171 | };
172 | return (
173 |
174 |
175 |
176 | setFilter(e.target.value)}
180 | placeholder="Search dialogs"
181 | className="hidden md:block w-full my-4 p-2 rounded text-sm border border-gray-200 outline-none shadow-inner"
182 | />
183 |
184 | {filteredConversations &&
185 | filteredConversations.map((c) => (
186 |
187 | ))}
188 |
189 |
190 | {selectedConversation ? (
191 | <>
192 |
193 | {messages &&
194 | messages.map((m) => (
195 |
200 | ))}
201 |
202 | {isTyping && (
203 |
204 |
205 |
206 | )}
207 |
{' '}
274 | >
275 | ) : (
276 |
277 | Select chat to send messages
278 |
279 | )}
280 |
281 | {/*
282 |
283 |
*/}
284 |
285 | );
286 | };;;;
287 |
288 | export default Messanger;
289 |
--------------------------------------------------------------------------------
/Social-master/frontend/src/pages/NotFound.tsx:
--------------------------------------------------------------------------------
1 | import notFound from '../images/404.webp';
2 |
3 | const NotFound = () => {
4 | return (
5 |
6 |
7 |

12 |
13 |
14 | );
15 | };
16 |
17 | export default NotFound;
18 |
--------------------------------------------------------------------------------
/Social-master/frontend/src/pages/Profile.tsx:
--------------------------------------------------------------------------------
1 | import { Sidebar, Feed, Rightbar } from '../components';
2 | import { useState, useEffect } from 'react';
3 | import { useParams, useNavigate } from 'react-router-dom';
4 | import { useAppSelector } from '../hooks';
5 | import { useDispatch } from 'react-redux';
6 | import { IUser } from '../interfaces';
7 | import { FiEdit2 } from 'react-icons/fi';
8 | import axios from 'axios';
9 | import { updateUser } from '../features/user/userSlice';
10 | import { useProfileInfoContext } from '../context';
11 | import { Socket } from 'socket.io-client';
12 | import { ServerToClientEvents, ClientToServerEvents } from '../interfaces';
13 | import { createRipple } from '../config/createRipple';
14 |
15 | type ProfileProps = {
16 | socket: React.MutableRefObject | null>;
20 | };
21 |
22 | const Profile: React.FC = ({ socket }) => {
23 | const { userId } = useParams();
24 | const { user: currentUser } = useAppSelector((state) => state.user);
25 | const [user, setUser] = useState({} as IUser);
26 | const {
27 | refetch,
28 | setRefetch,
29 | isEdit,
30 | setIsEdit,
31 | profileData,
32 | setProfileData,
33 | handleChange,
34 | } = useProfileInfoContext();
35 |
36 | const dispatch = useDispatch();
37 |
38 | const navigate = useNavigate();
39 |
40 | const handleImageChange = async (
41 | e: React.ChangeEvent,
42 | photo: string
43 | ) => {
44 | // @ts-ignore
45 | const file = e.target.files[0];
46 | if (file) {
47 | const formData = new FormData();
48 | formData.append('file', file);
49 | try {
50 | if (currentUser.profilePicture) {
51 | const id = currentUser.profilePicture.split('/').at(-1).split('.')[0];
52 | await axios.delete('/upload/' + id);
53 | }
54 | const { data: imageData } = await axios.post('/upload', formData, {
55 | headers: {
56 | 'Content-Type': 'multipart/form-data',
57 | },
58 | });
59 | const url = imageData.secure_url;
60 | try {
61 | const { data } = await axios.patch('/users/' + currentUser._id, {
62 | [photo]: url,
63 | });
64 | dispatch(updateUser(data));
65 | setRefetch(!refetch);
66 | } catch (error) {
67 | console.log(error);
68 | }
69 | } catch (error) {
70 | console.log(error);
71 | }
72 | }
73 | };
74 |
75 | useEffect(() => {
76 | const fetchUser = async () => {
77 | try {
78 | const { data } = await axios.get('/users/' + userId);
79 | setUser(data);
80 | setProfileData({
81 | ...profileData,
82 | username: data.username,
83 | desc: data.desc,
84 | });
85 | } catch (error) {
86 | console.log(error);
87 | navigate('/');
88 | }
89 | };
90 | fetchUser();
91 | }, [userId, navigate, refetch]);
92 |
93 | const handleUpdateSubmit = async (e: React.FormEvent) => {
94 | e.preventDefault();
95 | try {
96 | const { data } = await axios.patch('/users/' + currentUser._id, {
97 | ...profileData,
98 | });
99 | dispatch(updateUser(data));
100 | setIsEdit(false);
101 | setRefetch(!refetch);
102 | } catch (error) {
103 | console.log(error);
104 | }
105 | };
106 |
107 | return (
108 | <>
109 |
110 |
111 |
112 |
113 |
114 |
120 |

129 | {currentUser._id === userId && (
130 |
140 | )}
141 |
142 |
147 |

156 | {currentUser._id === userId && (
157 |
170 | )}
171 |
172 |
173 |
174 | {!isEdit ? (
175 | <>
176 | {' '}
177 |
{user?.username}
178 | {user?.desc}
179 | >
180 | ) : (
181 |
206 | )}
207 | {currentUser._id === userId && !isEdit && (
208 | <>
209 | setIsEdit(true)}
212 | />
213 | Edit Profile
214 | >
215 | )}
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 | >
225 | );
226 | };
227 |
228 | export default Profile;
229 |
--------------------------------------------------------------------------------
/Social-master/frontend/src/pages/ResetPassword.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Link } from 'react-router-dom';
3 | import axios from 'axios';
4 |
5 | import { createRipple } from '../config/createRipple';
6 |
7 | const ResetPassword = () => {
8 | const [errors, setErrors] = useState({});
9 | const [reset, setReset] = useState(false);
10 | const [email, setEmail] = useState('');
11 | const handleSubmit = async (e: React.FormEvent) => {
12 | e.preventDefault();
13 | if (email.trim()) {
14 | try {
15 | await axios.post('/auth/reset/password', {
16 | email,
17 | });
18 | setReset(true);
19 | setErrors({});
20 | } catch (error: any) {
21 | console.log(error);
22 | setErrors(error?.response?.data?.errors);
23 | }
24 | }
25 | };
26 | return (
27 |
28 |
35 |
36 |
37 |
Reset Passowrd
38 |
Please enter email adress
39 |
80 | {reset && (
81 |
Please check your email
82 | )}
83 |
84 |
85 |
89 | Sign Up
90 |
91 |
92 |
93 |
97 | Login
98 |
99 |
100 |
101 |
102 |
103 | );
104 | };
105 |
106 | export default ResetPassword;
107 |
--------------------------------------------------------------------------------
/Social-master/frontend/src/pages/SinglePost.tsx:
--------------------------------------------------------------------------------
1 | import { Sidebar, Post, Comment } from '../components';
2 | import { useParams, useNavigate } from 'react-router-dom';
3 | import React, { useEffect, useState } from 'react';
4 | import { useAppSelector, useAppDispatch } from '../app/hooks';
5 | import { updateSelectedPost } from '../features/posts/postsSlice';
6 | import { Socket } from 'socket.io-client';
7 | import { ServerToClientEvents, ClientToServerEvents } from '../interfaces';
8 | import axios from 'axios';
9 | import Picker from 'emoji-picker-react';
10 | import { removeNotifications } from '../features/user/userSlice';
11 | import { createRipple } from '../config/createRipple';
12 |
13 | type SinglePostProps = {
14 | socket: React.MutableRefObject | null>;
18 | };
19 |
20 | const SinglePost: React.FC = ({ socket }) => {
21 | const { id } = useParams();
22 | const navigate = useNavigate();
23 | const { user } = useAppSelector((state) => state.user);
24 | const dispatch = useAppDispatch();
25 | const [body, setBody] = useState('');
26 |
27 | const [showPicker, setShowPicker] = useState(false);
28 |
29 | useEffect(() => {
30 | const fetchPost = async () => {
31 | const { data } = await axios.get('/posts/' + id, {
32 | headers: {
33 | Authorization: `Bearer ${user?.token}`,
34 | },
35 | });
36 | dispatch(updateSelectedPost(data));
37 | dispatch(removeNotifications(data._id));
38 | };
39 | fetchPost();
40 | }, [id, dispatch, user.token]);
41 | const { selectedPost } = useAppSelector((state) => state.posts);
42 |
43 | const callback = () => navigate('/');
44 |
45 | const handleSubmit = async (e: React.FormEvent) => {
46 | e.preventDefault();
47 |
48 | if (body.trim()) {
49 | try {
50 | const { data } = await axios.post(
51 | `/posts/${id}/comment`,
52 | {
53 | body,
54 | },
55 | {
56 | headers: {
57 | Authorization: `Bearer ${user.token}`,
58 | },
59 | }
60 | );
61 | dispatch(updateSelectedPost(data));
62 | if (selectedPost?.author?._id !== user._id)
63 | socket?.current?.emit('sendRequest', selectedPost?.author?._id!);
64 | setBody('');
65 | } catch (error) {
66 | console.log(error);
67 | }
68 | }
69 | };
70 | const onEmojiClick = (event: any, emojiObject: any) => {
71 | // console.log(emojiObject.emoji);
72 | setBody(body + emojiObject.emoji);
73 | };
74 |
75 | return (
76 |
77 |
78 |
79 | {selectedPost && (
80 |
81 | )}
82 |
83 |
Add a new comment
84 |
129 |
130 |
131 | {selectedPost?.comments.map((c) => (
132 |
133 | ))}
134 |
135 |
136 | {/*
*/}
137 |
138 | );
139 | };
140 |
141 | export default SinglePost;
142 |
--------------------------------------------------------------------------------
/Social-master/frontend/src/pages/UpdatePassword.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Link, useParams } from 'react-router-dom';
3 | import axios from 'axios';
4 |
5 | import { createRipple } from '../config/createRipple';
6 |
7 | const UpdatePassword = () => {
8 | const [errors, setErrors] = useState({});
9 | const { token } = useParams();
10 | const [showPassword, setShowPassword] = useState(false);
11 | const [updated, setUpdated] = useState(false);
12 | const [message, setMessage] = useState('');
13 | const [password, setPassword] = useState('');
14 | const [confirmPassword, setConfirmPassword] = useState('');
15 | const handleSubmit = async (e: React.FormEvent) => {
16 | e.preventDefault();
17 | try {
18 | await axios.patch('/auth/update/password', {
19 | password,
20 | confirmPassword,
21 | token,
22 | });
23 | setErrors({});
24 | setUpdated(true);
25 | } catch (error: any) {
26 | console.log(error);
27 | if (error?.response?.data?.message) {
28 | setMessage(error?.response?.data?.message);
29 | setErrors({});
30 | } else {
31 | setErrors(error?.response?.data?.errors);
32 | }
33 | }
34 | };
35 | return (
36 |
37 |
43 |
44 |
45 |
Update Passowrd
46 |
Please enter new password
47 |
226 | {updated && (
227 |
228 | Password successfully updated
229 |
230 | )}
231 | {message && (
232 |
233 | Something went wrong... Please check the link.
234 |
235 | )}
236 |
237 |
238 |
242 | Sign Up
243 |
244 |
245 |
246 |
250 | Login
251 |
252 |
253 |
254 |
255 |
256 | );
257 | };
258 |
259 | export default UpdatePassword;
260 |
--------------------------------------------------------------------------------
/Social-master/frontend/src/pages/Verify.tsx:
--------------------------------------------------------------------------------
1 | import verify from '../images/verify.png';
2 |
3 | const Verify = () => {
4 | return (
5 |
6 |
7 |
8 |

13 |
14 |
15 | In order to create account, please confirm your email.
16 |
17 |
18 |
19 | );
20 | };
21 |
22 | export default Verify;
23 |
--------------------------------------------------------------------------------
/Social-master/frontend/src/pages/index.ts:
--------------------------------------------------------------------------------
1 | export { default as Home } from './Home';
2 | export { default as Profile } from './Profile';
3 | export { default as SinglePost } from './SinglePost';
4 | export { default as Login } from './Login';
5 | export { default as Verify } from './Verify';
6 | export { default as Confirm } from './Confirm';
7 | export { default as NotFound } from './NotFound';
8 | export { default as Messanger } from './Messanger';
9 | export { default as ResetPassword } from './ResetPassword';
10 | export { default as UpdatePassword } from './UpdatePassword';
11 |
--------------------------------------------------------------------------------
/Social-master/frontend/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | declare module 'react-notification-badge';
3 |
--------------------------------------------------------------------------------
/Social-master/frontend/src/sounds/notification.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/max0700/Web-Development/d119a79429359c0edf23c035727d81f336e5e649/Social-master/frontend/src/sounds/notification.mp3
--------------------------------------------------------------------------------
/Social-master/frontend/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | content: ['./src/**/*.{js,jsx,ts,tsx}'],
4 | theme: {
5 | extend: {},
6 | },
7 | plugins: [],
8 | };
9 |
--------------------------------------------------------------------------------
/Social-master/frontend/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "strict": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "noFallthroughCasesInSwitch": true,
16 | "module": "esnext",
17 | "moduleResolution": "node",
18 | "resolveJsonModule": true,
19 | "isolatedModules": true,
20 | "noEmit": true,
21 | "jsx": "react-jsx"
22 | },
23 | "include": [
24 | "src"
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------