├── .DS_Store
├── .dockerignore
├── .gitignore
├── .idea
├── codershouse.iml
├── modules.xml
├── vcs.xml
└── workspace.xml
├── Dockerfile.dev
├── Dockerfile.prod
├── README.md
├── api.rest
├── backend
├── .DS_Store
├── .dockerignore
├── .env.dev
├── .env.example
├── .gitignore
├── Dockerfile
├── Dockerfile.dev
├── Dockerfile.prod
├── actions.js
├── controllers
│ ├── activate-controller.js
│ ├── auth-controller.js
│ └── rooms-controller.js
├── database.js
├── dtos
│ ├── room.dto.js
│ └── user-dto.js
├── middlewares
│ └── auth-middleware.js
├── models
│ ├── refresh-model.js
│ ├── room-model.js
│ └── user-model.js
├── package-lock.json
├── package.json
├── routes.js
├── server.js
├── services
│ ├── hash-service.js
│ ├── otp-service.js
│ ├── room-service.js
│ ├── token-service.js
│ └── user-service.js
├── storage
│ ├── 1642526066430-314666215.png
│ ├── 1642527794110-994306863.png
│ ├── 1642531123517-736224419.png
│ ├── 1642582147799-92575190.png
│ ├── 1642585725835-327940772.png
│ ├── 1642585751584-121589683.png
│ ├── 1642781556250-61708629.png
│ ├── 1642782686194-929282973.png
│ └── 1642785881695-39789856.png
└── yarn.lock
├── deploy
└── default.conf
├── docker-compose.dev.yml
├── docker-compose.prod.yml
└── frontend
├── .env
├── .env.dev
├── .gitignore
├── README.md
├── package-lock.json
├── package.json
├── public
├── favicon.ico
├── images
│ ├── add-room-icon.png
│ ├── arrow-forward.png
│ ├── arrow-left.png
│ ├── celebration.png
│ ├── chat-bubble.png
│ ├── close.png
│ ├── email-emoji.png
│ ├── globe.png
│ ├── goggle-emoji.png
│ ├── lock-emoji.png
│ ├── lock.png
│ ├── logo.png
│ ├── logout.png
│ ├── mail-white.png
│ ├── mic-mute.png
│ ├── mic.png
│ ├── monkey-avatar.png
│ ├── monkey-emoji.png
│ ├── palm.png
│ ├── phone-white.png
│ ├── phone.png
│ ├── search-icon.png
│ ├── social.png
│ ├── user-icon.png
│ └── win.png
├── index.html
├── manifest.json
└── robots.txt
├── src
├── App.css
├── App.js
├── App.module.css
├── App.test.js
├── actions.js
├── components
│ ├── AddRoomModal
│ │ ├── AddRoomModal.jsx
│ │ └── AddRoomModal.module.css
│ ├── RoomCard
│ │ ├── RoomCard.jsx
│ │ └── RoomCard.module.css
│ └── shared
│ │ ├── Button
│ │ ├── Button.jsx
│ │ └── Button.module.css
│ │ ├── Card
│ │ ├── Card.jsx
│ │ └── Card.module.css
│ │ ├── Loader
│ │ ├── Loader.js
│ │ └── Loader.module.css
│ │ ├── Navigation
│ │ ├── Navigation.jsx
│ │ └── Navigation.module.css
│ │ └── TextInput
│ │ ├── TextInput.jsx
│ │ └── TextInput.module.css
├── hooks
│ ├── useLoadingWithRefresh.js
│ ├── useStateWithCallback.js
│ ├── useWebRTC.js
│ ├── useWebRTC.old.js
│ └── useWebRTC.old2.js
├── http
│ └── index.js
├── index.js
├── logo.svg
├── pages
│ ├── Activate
│ │ └── Activate.jsx
│ ├── Authenticate
│ │ └── Authenticate.jsx
│ ├── Home
│ │ ├── Home.jsx
│ │ └── Home.module.css
│ ├── Room
│ │ ├── Room.jsx
│ │ └── Room.module.css
│ ├── Rooms
│ │ ├── Rooms.jsx
│ │ └── Rooms.module.css
│ └── Steps
│ │ ├── StepAvatar
│ │ ├── StepAvatar.jsx
│ │ └── StepAvatar.module.css
│ │ ├── StepName
│ │ ├── StepName.jsx
│ │ └── StepName.module.css
│ │ ├── StepOtp
│ │ ├── StepOtp.jsx
│ │ └── StepOtp.module.css
│ │ ├── StepPhoneEmail
│ │ ├── Email
│ │ │ └── Email.jsx
│ │ ├── Phone
│ │ │ └── Phone.jsx
│ │ ├── StepPhoneEmail.jsx
│ │ └── StepPhoneEmail.module.css
│ │ └── StepUsername
│ │ └── StepUsername.jsx
├── reportWebVitals.js
├── setupTests.js
├── socket
│ └── index.js
└── store
│ ├── activateSlice.js
│ ├── authSlice.js
│ └── index.js
├── webpack.config.js
└── yarn.lock
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codersgyan/codershouse-mern/674dc78830401c0b6eff29fe099cb70407637ec2/.DS_Store
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | Dockerfile.*
2 | docker-compose.*
3 | .dockerignore
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | .DS_Store
3 |
--------------------------------------------------------------------------------
/.idea/codershouse.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/workspace.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | 1629308908173
30 |
31 |
32 | 1629308908173
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/Dockerfile.dev:
--------------------------------------------------------------------------------
1 | FROM node:16
2 | WORKDIR /frontend
3 | COPY ./frontend/package*.json ./
4 | RUN npm install
5 | COPY ./frontend/ ./
6 | CMD ["npm", "run", "start"]
7 |
--------------------------------------------------------------------------------
/Dockerfile.prod:
--------------------------------------------------------------------------------
1 | FROM node:16 as build
2 | WORKDIR /frontend
3 | COPY ./frontend/package*.json ./
4 | RUN npm install
5 | COPY ./frontend/ ./
6 | RUN npm run build
7 |
8 |
9 | FROM ubuntu:18.04
10 | RUN apt update -y \
11 | && apt install nginx curl vim -y \
12 | && apt-get install software-properties-common -y \
13 | && add-apt-repository ppa:certbot/certbot -y \
14 | && apt-get update -y \
15 | && apt-get install python-certbot-nginx -y \
16 | && apt-get clean
17 |
18 | EXPOSE 80
19 | STOPSIGNAL SIGTERM
20 |
21 | COPY --from=build /frontend/build /var/www/html
22 |
23 | CMD ["nginx", "-g", "daemon off;"]
24 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # codershouse-mern
2 |
3 | Final code: deployment branch
4 |
--------------------------------------------------------------------------------
/api.rest:
--------------------------------------------------------------------------------
1 | POST http://localhost:5500/api/send-otp HTTP/1.1
2 | Content-Type: application/json
3 |
4 | {
5 | "phone": "+915657565753"
6 | }
7 |
8 | ###
9 | POST http://localhost:5500/api/verify-otp HTTP/1.1
10 | Content-Type: application/json
11 |
12 | {
13 | "phone": "+915657565753",
14 | "otp": 1247,
15 | "hash": "37532b00c431ab139ae7e36d9f3b7887bd7575bfaf4ba64afe2a37a0f8771a7d.1626567458744"
16 | }
17 |
18 |
--------------------------------------------------------------------------------
/backend/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codersgyan/codershouse-mern/674dc78830401c0b6eff29fe099cb70407637ec2/backend/.DS_Store
--------------------------------------------------------------------------------
/backend/.dockerignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | Dockerfile.dev
3 | Dockerfile.prod
4 | Dockerfile
5 | .dockerignore
6 |
7 |
--------------------------------------------------------------------------------
/backend/.env.dev:
--------------------------------------------------------------------------------
1 | HASH_SECRET=9c11ad54ca524e091f669997d8b415d37984685d88ac31ee91c562000a3c14fe27af5213a5b4581541515efb17a2d3f6e1703f97eb16ca163463c079c08d189c
2 | SMS_SID=ACcc42d73ac9a27fa52b7e749e218e2fee
3 | SMS_AUTH_TOKEN=93da0b9bd5137f2bc93cf62d1d1ceb27
4 | SMS_FROM_NUMBER="+19378835038"
5 | DB_URL=mongodb://mongodb:27017/codershouse
6 | # DB_URL=mongodb://superAdmin:secret@localhost:27017/codershouse?authSource=admin&w=1
7 | JWT_ACCESS_TOKEN_SECRET=9e03fddc477f8dddf89ca6b608d1c6cccdc882ccd104dbafcdb02ff8edd419296937b1b6562db403c0be150a0a432f70c5e13cea0b572d9ac143ac7ab0cddc3a
8 | JWT_REFRESH_TOKEN_SECRET=3be8aba5089a46db14d7ac02cf37b0eba85bf403b767af6d04cb9cea3a7832450e13be06c40652df6b6d4decd828c395e4cf06d0f082bd29764f760e7ee8737a
9 | BASE_URL=http://localhost:5500
10 | FRONT_URL=http://localhost:3000
11 | NODE_ENV=development
12 |
--------------------------------------------------------------------------------
/backend/.env.example:
--------------------------------------------------------------------------------
1 | HASH_SECRET=9c11ad54ca524e091f669997d8b415d37984685d88ac31ee91c562000a3c14fe27af5213a5b4581541515efb17a2d3f6e1703f97eb16ca163463c079c08d189c
2 | SMS_SID=
3 | SMS_AUTH_TOKEN=
4 | SMS_FROM_NUMBER=
5 | DB_URL=
6 | JWT_ACCESS_TOKEN_SECRET=9e03fddc477f8dddf89ca6b608d1c6cccdc882ccd104dbafcdb02ff8edd419296937b1b6562db403c0be150a0a432f70c5e13cea0b572d9ac143ac7ab0cddc3a
7 | JWT_REFRESH_TOKEN_SECRET=3be8aba5089a46db14d7ac02cf37b0eba85bf403b767af6d04cb9cea3a7832450e13be06c40652df6b6d4decd828c395e4cf06d0f082bd29764f760e7ee8737a
8 | BASE_URL=http://localhost:5500
--------------------------------------------------------------------------------
/backend/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules
2 | .env
3 | .env.dev
4 | .env.prod
--------------------------------------------------------------------------------
/backend/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:14
2 | WORKDIR /backend/
3 | COPY package*.json /backend/
4 | RUN npm install
5 | COPY . .
6 | EXPOSE 5500
7 | CMD [ "npm", "run", "start" ]
8 |
--------------------------------------------------------------------------------
/backend/Dockerfile.dev:
--------------------------------------------------------------------------------
1 | FROM node:14
2 | WORKDIR /backend
3 | COPY package*.json ./
4 | RUN npm install
5 | COPY . ./
6 | EXPOSE 5500
7 | CMD [ "npm", "run", "dev" ]
8 |
--------------------------------------------------------------------------------
/backend/Dockerfile.prod:
--------------------------------------------------------------------------------
1 | FROM node:14
2 | WORKDIR /backend
3 | COPY package*.json ./
4 | RUN npm install --only=prod
5 | COPY . ./
6 | EXPOSE 5500
7 | CMD [ "npm", "run", "start" ]
8 |
--------------------------------------------------------------------------------
/backend/actions.js:
--------------------------------------------------------------------------------
1 | const ACTIONS = {
2 | JOIN: 'join',
3 | LEAVE: 'leave',
4 | ADD_PEER: 'add-peer',
5 | REMOVE_PEER: 'remove-peer',
6 | RELAY_ICE: 'relay-ice',
7 | RELAY_SDP: 'relay-sdp',
8 | SESSION_DESCRIPTION: 'session-description',
9 | ICE_CANDIDATE: 'ice-candidate',
10 | MUTE: 'mute',
11 | UNMUTE: 'unmute',
12 | MUTE_INFO: 'mute-info',
13 | };
14 |
15 | module.exports = ACTIONS;
16 |
--------------------------------------------------------------------------------
/backend/controllers/activate-controller.js:
--------------------------------------------------------------------------------
1 | const Jimp = require('jimp');
2 | const path = require('path');
3 | const userService = require('../services/user-service');
4 | const UserDto = require('../dtos/user-dto');
5 |
6 | class ActivateController {
7 | async activate(req, res) {
8 | // Activation logic
9 | const { name, avatar } = req.body;
10 | if (!name || !avatar) {
11 | res.status(400).json({ message: 'All fields are required!' });
12 | }
13 |
14 | // Image Base64
15 | const buffer = Buffer.from(
16 | avatar.replace(/^data:image\/(png|jpg|jpeg);base64,/, ''),
17 | 'base64'
18 | );
19 | const imagePath = `${Date.now()}-${Math.round(
20 | Math.random() * 1e9
21 | )}.png`;
22 | // 32478362874-3242342342343432.png
23 |
24 | try {
25 | const jimResp = await Jimp.read(buffer);
26 | jimResp
27 | .resize(150, Jimp.AUTO)
28 | .write(path.resolve(__dirname, `../storage/${imagePath}`));
29 | } catch (err) {
30 | res.status(500).json({ message: 'Could not process the image' });
31 | }
32 |
33 | const userId = req.user._id;
34 | // Update user
35 | try {
36 | const user = await userService.findUser({ _id: userId });
37 | if (!user) {
38 | res.status(404).json({ message: 'User not found!' });
39 | }
40 | user.activated = true;
41 | user.name = name;
42 | user.avatar = `/storage/${imagePath}`;
43 | user.save();
44 | res.json({ user: new UserDto(user), auth: true });
45 | } catch (err) {
46 | res.status(500).json({ message: 'Something went wrong!' });
47 | }
48 | }
49 | }
50 |
51 | module.exports = new ActivateController();
52 |
--------------------------------------------------------------------------------
/backend/controllers/auth-controller.js:
--------------------------------------------------------------------------------
1 | const otpService = require('../services/otp-service');
2 | const hashService = require('../services/hash-service');
3 | const userService = require('../services/user-service');
4 | const tokenService = require('../services/token-service');
5 | const UserDto = require('../dtos/user-dto');
6 |
7 | class AuthController {
8 | async sendOtp(req, res) {
9 | const { phone } = req.body;
10 | if (!phone) {
11 | res.status(400).json({ message: 'Phone field is required!' });
12 | }
13 |
14 | const otp = await otpService.generateOtp();
15 | // const otp = 7777;
16 |
17 | const ttl = 1000 * 60 * 2; // 2 min
18 | const expires = Date.now() + ttl;
19 | const data = `${phone}.${otp}.${expires}`;
20 | const hash = hashService.hashOtp(data);
21 |
22 | // send OTP
23 | try {
24 | // await otpService.sendBySms(phone, otp);
25 | res.json({
26 | hash: `${hash}.${expires}`,
27 | phone,
28 | });
29 | } catch (err) {
30 | console.log(err);
31 | res.status(500).json({ message: 'message sending failed' });
32 | }
33 | }
34 |
35 | async verifyOtp(req, res) {
36 | const { otp, hash, phone } = req.body;
37 | if (!otp || !hash || !phone) {
38 | res.status(400).json({ message: 'All fields are required!' });
39 | }
40 |
41 | const [hashedOtp, expires] = hash.split('.');
42 | if (Date.now() > +expires) {
43 | res.status(400).json({ message: 'OTP expired!' });
44 | }
45 |
46 | const data = `${phone}.${otp}.${expires}`;
47 | const isValid = otpService.verifyOtp(hashedOtp, data);
48 | if (!isValid) {
49 | res.status(400).json({ message: 'Invalid OTP' });
50 | }
51 |
52 | let user;
53 | try {
54 | user = await userService.findUser({ phone });
55 | if (!user) {
56 | user = await userService.createUser({ phone });
57 | }
58 | } catch (err) {
59 | console.log(err);
60 | res.status(500).json({ message: 'Db error' });
61 | }
62 |
63 | const { accessToken, refreshToken } = tokenService.generateTokens({
64 | _id: user._id,
65 | activated: false,
66 | });
67 |
68 | await tokenService.storeRefreshToken(refreshToken, user._id);
69 |
70 | res.cookie('refreshToken', refreshToken, {
71 | maxAge: 1000 * 60 * 60 * 24 * 30,
72 | httpOnly: true,
73 | });
74 |
75 | res.cookie('accessToken', accessToken, {
76 | maxAge: 1000 * 60 * 60 * 24 * 30,
77 | httpOnly: true,
78 | });
79 |
80 | const userDto = new UserDto(user);
81 | res.json({ user: userDto, auth: true });
82 | }
83 |
84 | async refresh(req, res) {
85 | // get refresh token from cookie
86 | const { refreshToken: refreshTokenFromCookie } = req.cookies;
87 | // check if token is valid
88 | let userData;
89 | try {
90 | userData = await tokenService.verifyRefreshToken(
91 | refreshTokenFromCookie
92 | );
93 | } catch (err) {
94 | return res.status(401).json({ message: 'Invalid Token' });
95 | }
96 | // Check if token is in db
97 | try {
98 | const token = await tokenService.findRefreshToken(
99 | userData._id,
100 | refreshTokenFromCookie
101 | );
102 | if (!token) {
103 | return res.status(401).json({ message: 'Invalid token' });
104 | }
105 | } catch (err) {
106 | return res.status(500).json({ message: 'Internal error' });
107 | }
108 | // check if valid user
109 | const user = await userService.findUser({ _id: userData._id });
110 | if (!user) {
111 | return res.status(404).json({ message: 'No user' });
112 | }
113 | // Generate new tokens
114 | const { refreshToken, accessToken } = tokenService.generateTokens({
115 | _id: userData._id,
116 | });
117 |
118 | // Update refresh token
119 | try {
120 | await tokenService.updateRefreshToken(userData._id, refreshToken);
121 | } catch (err) {
122 | return res.status(500).json({ message: 'Internal error' });
123 | }
124 | // put in cookie
125 | res.cookie('refreshToken', refreshToken, {
126 | maxAge: 1000 * 60 * 60 * 24 * 30,
127 | httpOnly: true,
128 | });
129 |
130 | res.cookie('accessToken', accessToken, {
131 | maxAge: 1000 * 60 * 60 * 24 * 30,
132 | httpOnly: true,
133 | });
134 | // response
135 | const userDto = new UserDto(user);
136 | res.json({ user: userDto, auth: true });
137 | }
138 |
139 | async logout(req, res) {
140 | const { refreshToken } = req.cookies;
141 | // delete refresh token from db
142 | await tokenService.removeToken(refreshToken);
143 | // delete cookies
144 | res.clearCookie('refreshToken');
145 | res.clearCookie('accessToken');
146 | res.json({ user: null, auth: false });
147 | }
148 | }
149 |
150 | module.exports = new AuthController();
151 |
--------------------------------------------------------------------------------
/backend/controllers/rooms-controller.js:
--------------------------------------------------------------------------------
1 | const RoomDto = require('../dtos/room.dto');
2 | const roomService = require('../services/room-service');
3 |
4 | class RoomsController {
5 | async create(req, res) {
6 | // room
7 | const { topic, roomType } = req.body;
8 |
9 | if (!topic || !roomType) {
10 | return res
11 | .status(400)
12 | .json({ message: 'All fields are required!' });
13 | }
14 |
15 | const room = await roomService.create({
16 | topic,
17 | roomType,
18 | ownerId: req.user._id,
19 | });
20 |
21 | return res.json(new RoomDto(room));
22 | }
23 |
24 | async index(req, res) {
25 | const rooms = await roomService.getAllRooms(['open']);
26 | const allRooms = rooms.map((room) => new RoomDto(room));
27 | return res.json(allRooms);
28 | }
29 |
30 | async show(req, res) {
31 | const room = await roomService.getRoom(req.params.roomId);
32 |
33 | return res.json(room);
34 | }
35 | }
36 |
37 | module.exports = new RoomsController();
38 |
--------------------------------------------------------------------------------
/backend/database.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 | function DbConnect() {
3 | console.log('coming here...', process.env.DB_URL);
4 | const DB_URL = process.env.DB_URL;
5 | // Database connection
6 | mongoose.connect(DB_URL, {
7 | useNewUrlParser: true,
8 | useUnifiedTopology: true,
9 | useFindAndModify: false,
10 | });
11 | const db = mongoose.connection;
12 | db.on('error', console.error.bind(console, 'connection error:'));
13 | db.once('open', () => {
14 | console.log('DB connected...');
15 | });
16 | }
17 |
18 | module.exports = DbConnect;
19 |
--------------------------------------------------------------------------------
/backend/dtos/room.dto.js:
--------------------------------------------------------------------------------
1 | class RoomDto {
2 | id;
3 | topic;
4 | roomType;
5 | speakers;
6 | ownerId;
7 | createdAt;
8 |
9 | constructor(room) {
10 | this.id = room._id;
11 | this.topic = room.topic;
12 | this.roomType = room.roomType;
13 | this.ownerId = room.ownerId;
14 | this.speakers = room.speakers;
15 | this.createdAt = room.createdAt;
16 | }
17 | }
18 | module.exports = RoomDto;
19 |
--------------------------------------------------------------------------------
/backend/dtos/user-dto.js:
--------------------------------------------------------------------------------
1 | class UserDto {
2 | id;
3 | phone;
4 | name;
5 | avatar;
6 | activated;
7 | createdAt;
8 |
9 | constructor(user) {
10 | this.id = user._id;
11 | this.phone = user.phone;
12 | this.name = user.name;
13 | this.avatar = user.avatar;
14 | this.activated = user.activated;
15 | this.createdAt = user.createdAt;
16 | }
17 | }
18 |
19 | module.exports = UserDto;
20 |
--------------------------------------------------------------------------------
/backend/middlewares/auth-middleware.js:
--------------------------------------------------------------------------------
1 | const tokenService = require('../services/token-service');
2 |
3 | module.exports = async function (req, res, next) {
4 | try {
5 | const { accessToken } = req.cookies;
6 | if (!accessToken) {
7 | throw new Error();
8 | }
9 | const userData = await tokenService.verifyAccessToken(accessToken);
10 | if (!userData) {
11 | throw new Error();
12 | }
13 | req.user = userData;
14 | next();
15 | } catch (err) {
16 | res.status(401).json({ message: 'Invalid token' });
17 | }
18 | };
19 |
--------------------------------------------------------------------------------
/backend/models/refresh-model.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 | const Schema = mongoose.Schema;
3 |
4 | const refreshSchema = new Schema(
5 | {
6 | token: { type: String, required: true },
7 | userId: { type: Schema.Types.ObjectId, ref: 'User' },
8 | },
9 | {
10 | timestamps: true,
11 | }
12 | );
13 |
14 | module.exports = mongoose.model('Refresh', refreshSchema, 'tokens');
15 |
--------------------------------------------------------------------------------
/backend/models/room-model.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 | const Schema = mongoose.Schema;
3 |
4 | const roomSchema = new Schema(
5 | {
6 | topic: { type: String, required: true },
7 | roomType: { type: String, required: true },
8 | ownerId: { type: Schema.Types.ObjectId, ref: 'User' },
9 | speakers: {
10 | type: [
11 | {
12 | type: Schema.Types.ObjectId,
13 | ref: 'User',
14 | },
15 | ],
16 | required: false,
17 | },
18 | },
19 | {
20 | timestamps: true,
21 | }
22 | );
23 |
24 | module.exports = mongoose.model('Room', roomSchema, 'rooms');
25 |
--------------------------------------------------------------------------------
/backend/models/user-model.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 | const Schema = mongoose.Schema;
3 |
4 | const userSchema = new Schema(
5 | {
6 | phone: { type: String, required: true },
7 | name: { type: String, required: false },
8 | avatar: {
9 | type: String,
10 | required: false,
11 | get: (avatar) => {
12 | if (avatar) {
13 | return `${process.env.BASE_URL}${avatar}`;
14 | }
15 | return avatar;
16 | },
17 | },
18 | activated: { type: Boolean, required: false, default: false },
19 | },
20 | {
21 | timestamps: true,
22 | toJSON: { getters: true },
23 | }
24 | );
25 |
26 | module.exports = mongoose.model('User', userSchema, 'users');
27 |
--------------------------------------------------------------------------------
/backend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "backend",
3 | "version": "1.0.0",
4 | "main": "index.js",
5 | "license": "MIT",
6 | "scripts": {
7 | "dev": "nodemon server.js",
8 | "start": "node server.js"
9 | },
10 | "dependencies": {
11 | "cookie-parser": "^1.4.5",
12 | "cors": "^2.8.5",
13 | "dotenv": "^10.0.0",
14 | "express": "^4.17.1",
15 | "jimp": "^0.16.1",
16 | "jsonwebtoken": "^8.5.1",
17 | "mongoose": "^5.13.3",
18 | "socket.io": "^4.4.0",
19 | "twilio": "^3.66.0"
20 | },
21 | "devDependencies": {
22 | "nodemon": "^2.0.12"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/backend/routes.js:
--------------------------------------------------------------------------------
1 | const router = require('express').Router();
2 | const authController = require('./controllers/auth-controller');
3 | const activateController = require('./controllers/activate-controller');
4 | const authMiddleware = require('./middlewares/auth-middleware');
5 | const roomsController = require('./controllers/rooms-controller');
6 |
7 | router.post('/api/send-otp', authController.sendOtp);
8 | router.post('/api/verify-otp', authController.verifyOtp);
9 | router.post('/api/activate', authMiddleware, activateController.activate);
10 | router.get('/api/refresh', authController.refresh);
11 | router.post('/api/logout', authMiddleware, authController.logout);
12 | router.post('/api/rooms', authMiddleware, roomsController.create);
13 | router.get('/api/rooms', authMiddleware, roomsController.index);
14 | router.get('/api/rooms/:roomId', authMiddleware, roomsController.show);
15 | router.get('/api/test', (req, res) => res.json({ msg: 'OK' }));
16 |
17 | module.exports = router;
18 |
--------------------------------------------------------------------------------
/backend/server.js:
--------------------------------------------------------------------------------
1 | // require('dotenv').config();
2 | const express = require('express');
3 | const app = express();
4 | const server = require('http').createServer(app);
5 | const DbConnect = require('./database');
6 | const router = require('./routes');
7 | const cors = require('cors');
8 | const cookieParser = require('cookie-parser');
9 |
10 | const ACTIONS = require('./actions');
11 |
12 | const io = require('socket.io')(server, {
13 | cors: {
14 | origin: process.env.FRONT_URL,
15 | methods: ['GET', 'POST'],
16 | },
17 | });
18 |
19 | app.use(cookieParser());
20 | const corsOption = {
21 | credentials: true,
22 | origin: [process.env.FRONT_URL],
23 | };
24 | app.use(cors(corsOption));
25 | app.use('/storage', express.static('storage'));
26 |
27 | const PORT = process.env.PORT || 5500;
28 | DbConnect();
29 | app.use(express.json({ limit: '8mb' }));
30 | app.use(router);
31 |
32 | app.get('/', (req, res) => {
33 | res.send('Hello from express Js');
34 | });
35 |
36 | // Sockets
37 | const socketUserMap = {};
38 |
39 | io.on('connection', (socket) => {
40 | console.log('New connection', socket.id);
41 | socket.on(ACTIONS.JOIN, ({ roomId, user }) => {
42 | socketUserMap[socket.id] = user;
43 | const clients = Array.from(io.sockets.adapter.rooms.get(roomId) || []);
44 | clients.forEach((clientId) => {
45 | io.to(clientId).emit(ACTIONS.ADD_PEER, {
46 | peerId: socket.id,
47 | createOffer: false,
48 | user,
49 | });
50 | socket.emit(ACTIONS.ADD_PEER, {
51 | peerId: clientId,
52 | createOffer: true,
53 | user: socketUserMap[clientId],
54 | });
55 | });
56 | socket.join(roomId);
57 | });
58 |
59 | socket.on(ACTIONS.RELAY_ICE, ({ peerId, icecandidate }) => {
60 | io.to(peerId).emit(ACTIONS.ICE_CANDIDATE, {
61 | peerId: socket.id,
62 | icecandidate,
63 | });
64 | });
65 |
66 | socket.on(ACTIONS.RELAY_SDP, ({ peerId, sessionDescription }) => {
67 | io.to(peerId).emit(ACTIONS.SESSION_DESCRIPTION, {
68 | peerId: socket.id,
69 | sessionDescription,
70 | });
71 | });
72 |
73 | socket.on(ACTIONS.MUTE, ({ roomId, userId }) => {
74 | const clients = Array.from(io.sockets.adapter.rooms.get(roomId) || []);
75 | clients.forEach((clientId) => {
76 | io.to(clientId).emit(ACTIONS.MUTE, {
77 | peerId: socket.id,
78 | userId,
79 | });
80 | });
81 | });
82 |
83 | socket.on(ACTIONS.UNMUTE, ({ roomId, userId }) => {
84 | const clients = Array.from(io.sockets.adapter.rooms.get(roomId) || []);
85 | clients.forEach((clientId) => {
86 | io.to(clientId).emit(ACTIONS.UNMUTE, {
87 | peerId: socket.id,
88 | userId,
89 | });
90 | });
91 | });
92 |
93 | socket.on(ACTIONS.MUTE_INFO, ({ userId, roomId, isMute }) => {
94 | const clients = Array.from(io.sockets.adapter.rooms.get(roomId) || []);
95 | clients.forEach((clientId) => {
96 | if (clientId !== socket.id) {
97 | console.log('mute info');
98 | io.to(clientId).emit(ACTIONS.MUTE_INFO, {
99 | userId,
100 | isMute,
101 | });
102 | }
103 | });
104 | });
105 |
106 | const leaveRoom = () => {
107 | const { rooms } = socket;
108 | Array.from(rooms).forEach((roomId) => {
109 | const clients = Array.from(
110 | io.sockets.adapter.rooms.get(roomId) || []
111 | );
112 | clients.forEach((clientId) => {
113 | io.to(clientId).emit(ACTIONS.REMOVE_PEER, {
114 | peerId: socket.id,
115 | userId: socketUserMap[socket.id]?.id,
116 | });
117 |
118 | // socket.emit(ACTIONS.REMOVE_PEER, {
119 | // peerId: clientId,
120 | // userId: socketUserMap[clientId]?.id,
121 | // });
122 | });
123 | socket.leave(roomId);
124 | });
125 | delete socketUserMap[socket.id];
126 | };
127 |
128 | socket.on(ACTIONS.LEAVE, leaveRoom);
129 |
130 | socket.on('disconnecting', leaveRoom);
131 | });
132 |
133 | server.listen(PORT, () => console.log(`Listening on port ${PORT}`));
134 |
--------------------------------------------------------------------------------
/backend/services/hash-service.js:
--------------------------------------------------------------------------------
1 | const crypto = require('crypto');
2 |
3 | class HashService {
4 | hashOtp(data) {
5 | return crypto
6 | .createHmac('sha256', process.env.HASH_SECRET)
7 | .update(data)
8 | .digest('hex');
9 | }
10 | }
11 |
12 | module.exports = new HashService();
13 |
--------------------------------------------------------------------------------
/backend/services/otp-service.js:
--------------------------------------------------------------------------------
1 | const crypto = require('crypto');
2 | const hashService = require('./hash-service');
3 |
4 | const smsSid = process.env.SMS_SID;
5 | const smsAuthToken = process.env.SMS_AUTH_TOKEN;
6 | const twilio = require('twilio')(smsSid, smsAuthToken, {
7 | lazyLoading: true,
8 | });
9 |
10 | class OtpService {
11 | async generateOtp() {
12 | const otp = crypto.randomInt(1000, 9999);
13 | return otp;
14 | }
15 |
16 | async sendBySms(phone, otp) {
17 | return await twilio.messages.create({
18 | to: phone,
19 | from: process.env.SMS_FROM_NUMBER,
20 | body: `Your codershouse OTP is ${otp}`,
21 | });
22 | }
23 |
24 | verifyOtp(hashedOtp, data) {
25 | let computedHash = hashService.hashOtp(data);
26 | return computedHash === hashedOtp;
27 | }
28 | }
29 |
30 | module.exports = new OtpService();
31 |
--------------------------------------------------------------------------------
/backend/services/room-service.js:
--------------------------------------------------------------------------------
1 | const RoomModel = require('../models/room-model');
2 | class RoomService {
3 | async create(payload) {
4 | const { topic, roomType, ownerId } = payload;
5 | const room = await RoomModel.create({
6 | topic,
7 | roomType,
8 | ownerId,
9 | speakers: [ownerId],
10 | });
11 | return room;
12 | }
13 |
14 | async getAllRooms(types) {
15 | const rooms = await RoomModel.find({ roomType: { $in: types } })
16 | .populate('speakers')
17 | .populate('ownerId')
18 | .exec();
19 | return rooms;
20 | }
21 |
22 | async getRoom(roomId) {
23 | const room = await RoomModel.findOne({ _id: roomId });
24 | return room;
25 | }
26 | }
27 | module.exports = new RoomService();
28 |
--------------------------------------------------------------------------------
/backend/services/token-service.js:
--------------------------------------------------------------------------------
1 | const jwt = require('jsonwebtoken');
2 | const accessTokenSecret = process.env.JWT_ACCESS_TOKEN_SECRET;
3 | const refreshTokenSecret = process.env.JWT_REFRESH_TOKEN_SECRET;
4 | const refreshModel = require('../models/refresh-model');
5 | class TokenService {
6 | generateTokens(payload) {
7 | const accessToken = jwt.sign(payload, accessTokenSecret, {
8 | expiresIn: '1m',
9 | });
10 | const refreshToken = jwt.sign(payload, refreshTokenSecret, {
11 | expiresIn: '1y',
12 | });
13 | return { accessToken, refreshToken };
14 | }
15 |
16 | async storeRefreshToken(token, userId) {
17 | try {
18 | await refreshModel.create({
19 | token,
20 | userId,
21 | });
22 | } catch (err) {
23 | console.log(err.message);
24 | }
25 | }
26 |
27 | async verifyAccessToken(token) {
28 | return jwt.verify(token, accessTokenSecret);
29 | }
30 |
31 | async verifyRefreshToken(refreshToken) {
32 | return jwt.verify(refreshToken, refreshTokenSecret);
33 | }
34 |
35 | async findRefreshToken(userId, refreshToken) {
36 | return await refreshModel.findOne({
37 | userId: userId,
38 | token: refreshToken,
39 | });
40 | }
41 |
42 | async updateRefreshToken(userId, refreshToken) {
43 | return await refreshModel.updateOne(
44 | { userId: userId },
45 | { token: refreshToken }
46 | );
47 | }
48 |
49 | async removeToken(refreshToken) {
50 | return await refreshModel.deleteOne({ token: refreshToken });
51 | }
52 | }
53 |
54 | module.exports = new TokenService();
55 |
--------------------------------------------------------------------------------
/backend/services/user-service.js:
--------------------------------------------------------------------------------
1 | const UserModel = require('../models/user-model');
2 | class UserService {
3 | async findUser(filter) {
4 | const user = await UserModel.findOne(filter);
5 | return user;
6 | }
7 |
8 | async createUser(data) {
9 | const user = await UserModel.create(data);
10 | return user;
11 | }
12 | }
13 |
14 | module.exports = new UserService();
15 |
--------------------------------------------------------------------------------
/backend/storage/1642526066430-314666215.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codersgyan/codershouse-mern/674dc78830401c0b6eff29fe099cb70407637ec2/backend/storage/1642526066430-314666215.png
--------------------------------------------------------------------------------
/backend/storage/1642527794110-994306863.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codersgyan/codershouse-mern/674dc78830401c0b6eff29fe099cb70407637ec2/backend/storage/1642527794110-994306863.png
--------------------------------------------------------------------------------
/backend/storage/1642531123517-736224419.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codersgyan/codershouse-mern/674dc78830401c0b6eff29fe099cb70407637ec2/backend/storage/1642531123517-736224419.png
--------------------------------------------------------------------------------
/backend/storage/1642582147799-92575190.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codersgyan/codershouse-mern/674dc78830401c0b6eff29fe099cb70407637ec2/backend/storage/1642582147799-92575190.png
--------------------------------------------------------------------------------
/backend/storage/1642585725835-327940772.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codersgyan/codershouse-mern/674dc78830401c0b6eff29fe099cb70407637ec2/backend/storage/1642585725835-327940772.png
--------------------------------------------------------------------------------
/backend/storage/1642585751584-121589683.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codersgyan/codershouse-mern/674dc78830401c0b6eff29fe099cb70407637ec2/backend/storage/1642585751584-121589683.png
--------------------------------------------------------------------------------
/backend/storage/1642781556250-61708629.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codersgyan/codershouse-mern/674dc78830401c0b6eff29fe099cb70407637ec2/backend/storage/1642781556250-61708629.png
--------------------------------------------------------------------------------
/backend/storage/1642782686194-929282973.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codersgyan/codershouse-mern/674dc78830401c0b6eff29fe099cb70407637ec2/backend/storage/1642782686194-929282973.png
--------------------------------------------------------------------------------
/backend/storage/1642785881695-39789856.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codersgyan/codershouse-mern/674dc78830401c0b6eff29fe099cb70407637ec2/backend/storage/1642785881695-39789856.png
--------------------------------------------------------------------------------
/deploy/default.conf:
--------------------------------------------------------------------------------
1 | server {
2 | listen 80 default_server;
3 | listen [::]:80 default_server;
4 | root /var/www/html;
5 | index index.html index.htm index.nginx-debian.html;
6 |
7 | server_name codersvoice.xyz www.codersvoice.xyz;
8 |
9 | location / {
10 | root /var/www/html;
11 | try_files $uri /index.html;
12 | }
13 |
14 | location /socket.io {
15 | proxy_pass http://backend:5500;
16 | proxy_http_version 1.1;
17 | proxy_set_header Upgrade $http_upgrade;
18 | proxy_set_header Connection 'upgrade';
19 | proxy_cache_bypass $http_upgrade;
20 | }
21 |
22 | location /api {
23 | proxy_pass http://backend:5500;
24 | }
25 |
26 | location /storage {
27 | proxy_pass http://backend:5500;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/docker-compose.dev.yml:
--------------------------------------------------------------------------------
1 | version: "3"
2 | services:
3 | mongodb:
4 | image: mongo
5 | container_name: mongodb
6 | restart: always
7 | ports:
8 | - "27017:27017"
9 | volumes:
10 | - /Users/codersgyan/Documents/databases/mongodb/codershouse-data:/data/db
11 | backend:
12 | build:
13 | dockerfile: Dockerfile.dev
14 | context: ./backend/
15 | container_name: backend
16 | restart: always
17 | ports:
18 | - "5500:5500"
19 | env_file:
20 | - ./backend/.env.dev
21 | volumes:
22 | - ./backend:/backend
23 | - /backend/node_modules
24 | depends_on:
25 | - mongodb
26 | frontend:
27 | build:
28 | dockerfile: Dockerfile.dev
29 | context: ./
30 | container_name: frontend
31 | ports:
32 | - "3000:3000"
33 | env_file:
34 | - ./frontend/.env.dev
35 | volumes:
36 | - ./frontend:/frontend
37 | - /frontend/node_modules
38 | environment:
39 | - CHOKIDAR_USEPOLLING=true
40 | depends_on:
41 | - backend
42 |
--------------------------------------------------------------------------------
/docker-compose.prod.yml:
--------------------------------------------------------------------------------
1 | version: "3"
2 | services:
3 | mongodb:
4 | image: mongo
5 | container_name: mongodb
6 | restart: always
7 | ports:
8 | - "27017:27017"
9 | volumes:
10 | - /var/databases/mongodb/codershouse-data:/data/db
11 | backend:
12 | build:
13 | dockerfile: Dockerfile.prod
14 | context: ./backend/
15 | container_name: backend
16 | restart: always
17 | ports:
18 | - "5500:5500"
19 | env_file:
20 | - ./backend/.env
21 | volumes:
22 | - ./backend:/backend
23 | - /backend/node_modules
24 | depends_on:
25 | - mongodb
26 | frontend:
27 | build:
28 | dockerfile: Dockerfile.prod
29 | context: ./
30 | container_name: frontend
31 | ports:
32 | - "80:80"
33 | - "443:443"
34 | env_file:
35 | - ./frontend/.env
36 | volumes:
37 | - ./deploy/default.conf:/etc/nginx/sites-available/default
38 | - /var/certs/etc-letsencrypt:/etc/letsencrypt
39 | depends_on:
40 | - backend
41 |
--------------------------------------------------------------------------------
/frontend/.env:
--------------------------------------------------------------------------------
1 | REACT_APP_API_URL=https://codersvoice.xyz
2 | SOCKET_SERVER_URL=https://codersvoice.xyz
3 | NODE_ENV=production
--------------------------------------------------------------------------------
/frontend/.env.dev:
--------------------------------------------------------------------------------
1 | REACT_APP_API_URL=http://localhost:5500
2 | REACT_APP_SOCKET_SERVER_URL=http://localhost:5500
3 | NODE_ENV=development
--------------------------------------------------------------------------------
/frontend/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
25 | .env
26 | .env.dev
27 | .env.prod
28 |
--------------------------------------------------------------------------------
/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 | ### `yarn 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 | ### `yarn 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 | ### `yarn 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 | ### `yarn eject`
33 |
34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!**
35 |
36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
37 |
38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
39 |
40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
41 |
42 | ## Learn More
43 |
44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
45 |
46 | To learn React, check out the [React documentation](https://reactjs.org/).
47 |
48 | ### Code Splitting
49 |
50 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
51 |
52 | ### Analyzing the Bundle Size
53 |
54 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
55 |
56 | ### Making a Progressive Web App
57 |
58 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
59 |
60 | ### Advanced Configuration
61 |
62 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
63 |
64 | ### Deployment
65 |
66 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
67 |
68 | ### `yarn build` fails to minify
69 |
70 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
71 |
--------------------------------------------------------------------------------
/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "frontend",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@reduxjs/toolkit": "^1.6.1",
7 | "@testing-library/jest-dom": "^5.11.4",
8 | "@testing-library/react": "^11.1.0",
9 | "@testing-library/user-event": "^12.1.10",
10 | "axios": "^0.21.1",
11 | "freeice": "^2.2.2",
12 | "react": "^17.0.2",
13 | "react-dom": "^17.0.2",
14 | "react-redux": "^7.2.4",
15 | "react-router-dom": "^5.2.0",
16 | "react-scripts": "4.0.3",
17 | "socket.io-client": "^4.4.0",
18 | "web-vitals": "^1.0.1"
19 | },
20 | "scripts": {
21 | "start": "react-scripts start",
22 | "build": "react-scripts build",
23 | "test": "react-scripts test",
24 | "eject": "react-scripts eject"
25 | },
26 | "eslintConfig": {
27 | "extends": [
28 | "react-app",
29 | "react-app/jest"
30 | ]
31 | },
32 | "browserslist": {
33 | "production": [
34 | ">0.2%",
35 | "not dead",
36 | "not op_mini all"
37 | ],
38 | "development": [
39 | "last 1 chrome version",
40 | "last 1 firefox version",
41 | "last 1 safari version"
42 | ]
43 | },
44 | "main": "index.js",
45 | "license": "MIT"
46 | }
47 |
--------------------------------------------------------------------------------
/frontend/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codersgyan/codershouse-mern/674dc78830401c0b6eff29fe099cb70407637ec2/frontend/public/favicon.ico
--------------------------------------------------------------------------------
/frontend/public/images/add-room-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codersgyan/codershouse-mern/674dc78830401c0b6eff29fe099cb70407637ec2/frontend/public/images/add-room-icon.png
--------------------------------------------------------------------------------
/frontend/public/images/arrow-forward.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codersgyan/codershouse-mern/674dc78830401c0b6eff29fe099cb70407637ec2/frontend/public/images/arrow-forward.png
--------------------------------------------------------------------------------
/frontend/public/images/arrow-left.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codersgyan/codershouse-mern/674dc78830401c0b6eff29fe099cb70407637ec2/frontend/public/images/arrow-left.png
--------------------------------------------------------------------------------
/frontend/public/images/celebration.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codersgyan/codershouse-mern/674dc78830401c0b6eff29fe099cb70407637ec2/frontend/public/images/celebration.png
--------------------------------------------------------------------------------
/frontend/public/images/chat-bubble.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codersgyan/codershouse-mern/674dc78830401c0b6eff29fe099cb70407637ec2/frontend/public/images/chat-bubble.png
--------------------------------------------------------------------------------
/frontend/public/images/close.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codersgyan/codershouse-mern/674dc78830401c0b6eff29fe099cb70407637ec2/frontend/public/images/close.png
--------------------------------------------------------------------------------
/frontend/public/images/email-emoji.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codersgyan/codershouse-mern/674dc78830401c0b6eff29fe099cb70407637ec2/frontend/public/images/email-emoji.png
--------------------------------------------------------------------------------
/frontend/public/images/globe.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codersgyan/codershouse-mern/674dc78830401c0b6eff29fe099cb70407637ec2/frontend/public/images/globe.png
--------------------------------------------------------------------------------
/frontend/public/images/goggle-emoji.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codersgyan/codershouse-mern/674dc78830401c0b6eff29fe099cb70407637ec2/frontend/public/images/goggle-emoji.png
--------------------------------------------------------------------------------
/frontend/public/images/lock-emoji.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codersgyan/codershouse-mern/674dc78830401c0b6eff29fe099cb70407637ec2/frontend/public/images/lock-emoji.png
--------------------------------------------------------------------------------
/frontend/public/images/lock.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codersgyan/codershouse-mern/674dc78830401c0b6eff29fe099cb70407637ec2/frontend/public/images/lock.png
--------------------------------------------------------------------------------
/frontend/public/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codersgyan/codershouse-mern/674dc78830401c0b6eff29fe099cb70407637ec2/frontend/public/images/logo.png
--------------------------------------------------------------------------------
/frontend/public/images/logout.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codersgyan/codershouse-mern/674dc78830401c0b6eff29fe099cb70407637ec2/frontend/public/images/logout.png
--------------------------------------------------------------------------------
/frontend/public/images/mail-white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codersgyan/codershouse-mern/674dc78830401c0b6eff29fe099cb70407637ec2/frontend/public/images/mail-white.png
--------------------------------------------------------------------------------
/frontend/public/images/mic-mute.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codersgyan/codershouse-mern/674dc78830401c0b6eff29fe099cb70407637ec2/frontend/public/images/mic-mute.png
--------------------------------------------------------------------------------
/frontend/public/images/mic.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codersgyan/codershouse-mern/674dc78830401c0b6eff29fe099cb70407637ec2/frontend/public/images/mic.png
--------------------------------------------------------------------------------
/frontend/public/images/monkey-avatar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codersgyan/codershouse-mern/674dc78830401c0b6eff29fe099cb70407637ec2/frontend/public/images/monkey-avatar.png
--------------------------------------------------------------------------------
/frontend/public/images/monkey-emoji.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codersgyan/codershouse-mern/674dc78830401c0b6eff29fe099cb70407637ec2/frontend/public/images/monkey-emoji.png
--------------------------------------------------------------------------------
/frontend/public/images/palm.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codersgyan/codershouse-mern/674dc78830401c0b6eff29fe099cb70407637ec2/frontend/public/images/palm.png
--------------------------------------------------------------------------------
/frontend/public/images/phone-white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codersgyan/codershouse-mern/674dc78830401c0b6eff29fe099cb70407637ec2/frontend/public/images/phone-white.png
--------------------------------------------------------------------------------
/frontend/public/images/phone.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codersgyan/codershouse-mern/674dc78830401c0b6eff29fe099cb70407637ec2/frontend/public/images/phone.png
--------------------------------------------------------------------------------
/frontend/public/images/search-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codersgyan/codershouse-mern/674dc78830401c0b6eff29fe099cb70407637ec2/frontend/public/images/search-icon.png
--------------------------------------------------------------------------------
/frontend/public/images/social.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codersgyan/codershouse-mern/674dc78830401c0b6eff29fe099cb70407637ec2/frontend/public/images/social.png
--------------------------------------------------------------------------------
/frontend/public/images/user-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codersgyan/codershouse-mern/674dc78830401c0b6eff29fe099cb70407637ec2/frontend/public/images/user-icon.png
--------------------------------------------------------------------------------
/frontend/public/images/win.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codersgyan/codershouse-mern/674dc78830401c0b6eff29fe099cb70407637ec2/frontend/public/images/win.png
--------------------------------------------------------------------------------
/frontend/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
14 |
15 |
16 |
20 |
21 |
30 | React App
31 |
32 |
33 | You need to enable JavaScript to run this app.
34 |
35 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/frontend/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "Codershouse",
3 | "name": "Codershouse",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": ".",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/frontend/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/frontend/src/App.css:
--------------------------------------------------------------------------------
1 | html {
2 | font-size: 16px;
3 | }
4 | * {
5 | box-sizing: border-box;
6 | }
7 |
8 | body,
9 | h1,
10 | h2,
11 | h3,
12 | h4,
13 | h5,
14 | h6,
15 | p,
16 | ol,
17 | ul {
18 | margin: 0;
19 | padding: 0;
20 | font-weight: normal;
21 | -webkit-font-smoothing: antialiased;
22 | }
23 |
24 | ol,
25 | ul {
26 | list-style: none;
27 | }
28 |
29 | img {
30 | max-width: 100%;
31 | height: auto;
32 | }
33 |
34 | button {
35 | border: none;
36 | outline: none;
37 | cursor: pointer;
38 | font-weight: bold;
39 | }
40 |
41 | body {
42 | background: #121212;
43 | font-family: 'Nunito', sans-serif;
44 | color: #fff;
45 | }
46 |
47 | .container {
48 | width: 1200px;
49 | max-width: 90%;
50 | margin: 0 auto;
51 | }
52 |
53 | .cardWrapper {
54 | display: flex;
55 | align-items: center;
56 | justify-content: center;
57 | min-height: calc(100vh - 140px);
58 | }
59 |
--------------------------------------------------------------------------------
/frontend/src/App.js:
--------------------------------------------------------------------------------
1 | import './App.css';
2 | import { BrowserRouter, Switch, Route, Redirect } from 'react-router-dom';
3 | import Home from './pages/Home/Home';
4 | import Navigation from './components/shared/Navigation/Navigation';
5 | import Authenticate from './pages/Authenticate/Authenticate';
6 | import Activate from './pages/Activate/Activate';
7 | import Rooms from './pages/Rooms/Rooms';
8 | import Room from './pages/Room/Room';
9 | import { useSelector } from 'react-redux';
10 | import { useLoadingWithRefresh } from './hooks/useLoadingWithRefresh';
11 | import Loader from './components/shared/Loader/Loader';
12 |
13 | function App() {
14 | // call refresh endpoint
15 | const { loading } = useLoadingWithRefresh();
16 |
17 | return loading ? (
18 |
19 | ) : (
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | );
41 | }
42 |
43 | const GuestRoute = ({ children, ...rest }) => {
44 | const { isAuth } = useSelector((state) => state.auth);
45 | return (
46 | {
49 | return isAuth ? (
50 |
56 | ) : (
57 | children
58 | );
59 | }}
60 | >
61 | );
62 | };
63 |
64 | const SemiProtectedRoute = ({ children, ...rest }) => {
65 | const { user, isAuth } = useSelector((state) => state.auth);
66 | return (
67 | {
70 | return !isAuth ? (
71 |
77 | ) : isAuth && !user.activated ? (
78 | children
79 | ) : (
80 |
86 | );
87 | }}
88 | >
89 | );
90 | };
91 |
92 | const ProtectedRoute = ({ children, ...rest }) => {
93 | const { user, isAuth } = useSelector((state) => state.auth);
94 | return (
95 | {
98 | return !isAuth ? (
99 |
105 | ) : isAuth && !user.activated ? (
106 |
112 | ) : (
113 | children
114 | );
115 | }}
116 | >
117 | );
118 | };
119 |
120 | export default App;
121 |
--------------------------------------------------------------------------------
/frontend/src/App.module.css:
--------------------------------------------------------------------------------
1 | .cardWrapper {
2 | display: flex;
3 | align-items: center;
4 | justify-content: center;
5 | margin-top: 6rem;
6 | }
7 |
--------------------------------------------------------------------------------
/frontend/src/App.test.js:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/react';
2 | import App from './App';
3 |
4 | test('renders learn react link', () => {
5 | render( );
6 | const linkElement = screen.getByText(/learn react/i);
7 | expect(linkElement).toBeInTheDocument();
8 | });
9 |
--------------------------------------------------------------------------------
/frontend/src/actions.js:
--------------------------------------------------------------------------------
1 | export const ACTIONS = {
2 | JOIN: 'join',
3 | LEAVE: 'leave',
4 | ADD_PEER: 'add-peer',
5 | REMOVE_PEER: 'remove-peer',
6 | RELAY_ICE: 'relay-ice',
7 | RELAY_SDP: 'relay-sdp',
8 | SESSION_DESCRIPTION: 'session-description',
9 | ICE_CANDIDATE: 'ice-candidate',
10 | MUTE: 'mute',
11 | UNMUTE: 'unmute',
12 | MUTE_INFO: 'mute-info',
13 | };
14 |
--------------------------------------------------------------------------------
/frontend/src/components/AddRoomModal/AddRoomModal.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import styles from './AddRoomModal.module.css';
3 | import TextInput from '../shared/TextInput/TextInput';
4 | import { createRoom as create } from '../../http';
5 | import { useHistory } from 'react-router-dom';
6 | const AddRoomModal = ({ onClose }) => {
7 | const history = useHistory();
8 |
9 | const [roomType, setRoomType] = useState('open');
10 | const [topic, setTopic] = useState('');
11 |
12 | async function createRoom() {
13 | try {
14 | if (!topic) return;
15 | const { data } = await create({ topic, roomType });
16 | history.push(`/room/${data.id}`);
17 | } catch (err) {
18 | console.log(err.message);
19 | }
20 | }
21 |
22 | return (
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | Enter the topic to be disscussed
31 |
32 |
setTopic(e.target.value)}
36 | />
37 | Room types
38 |
39 |
setRoomType('open')}
41 | className={`${styles.typeBox} ${
42 | roomType === 'open' ? styles.active : ''
43 | }`}
44 | >
45 |
46 |
Open
47 |
48 |
setRoomType('social')}
50 | className={`${styles.typeBox} ${
51 | roomType === 'social' ? styles.active : ''
52 | }`}
53 | >
54 |
55 |
Social
56 |
57 |
setRoomType('private')}
59 | className={`${styles.typeBox} ${
60 | roomType === 'private' ? styles.active : ''
61 | }`}
62 | >
63 |
64 |
Private
65 |
66 |
67 |
68 |
69 |
Start a room, open to everyone
70 |
74 |
75 | Let's go
76 |
77 |
78 |
79 |
80 | );
81 | };
82 |
83 | export default AddRoomModal;
84 |
--------------------------------------------------------------------------------
/frontend/src/components/AddRoomModal/AddRoomModal.module.css:
--------------------------------------------------------------------------------
1 | .modalMask {
2 | position: fixed;
3 | top: 0;
4 | right: 0;
5 | bottom: 0;
6 | left: 0;
7 | background: rgba(0, 0, 0, 0.6);
8 | display: flex;
9 | align-items: center;
10 | justify-content: center;
11 | }
12 |
13 | .modalBody {
14 | width: 50%;
15 | max-width: 500px;
16 | background: #1d1d1d;
17 | border-radius: 20px;
18 | position: relative;
19 | }
20 |
21 | .modalHeader {
22 | padding: 30px;
23 | border-bottom: 2px solid #262626;
24 | }
25 |
26 | .roomTypes {
27 | display: grid;
28 | grid-template-columns: repeat(3, 1fr);
29 | gap: 30px;
30 | }
31 |
32 | .typeBox {
33 | display: flex;
34 | flex-direction: column;
35 | align-items: center;
36 | padding: 10px;
37 | border-radius: 10px;
38 | cursor: pointer;
39 | }
40 |
41 | .typeBox.active {
42 | background: #262626;
43 | }
44 |
45 | .heading {
46 | margin-bottom: 5px;
47 | }
48 |
49 | .subHeading {
50 | font-size: 18px;
51 | margin: 10px 0;
52 | font-weight: bold;
53 | }
54 |
55 | .modalFooter {
56 | padding: 30px;
57 | text-align: center;
58 | }
59 | .modalFooter h2 {
60 | margin-bottom: 20px;
61 | font-weight: bold;
62 | font-size: 20px;
63 | }
64 |
65 | .footerButton {
66 | background: #20bd5f;
67 | color: #fff;
68 | display: flex;
69 | align-items: center;
70 | width: 200px;
71 | justify-content: center;
72 | padding: 7px 10px;
73 | border-radius: 20px;
74 | margin: 0 auto;
75 | transition: all 0.3s ease-in-out;
76 | }
77 |
78 | .footerButton:hover {
79 | background: #13763b;
80 | }
81 |
82 | .footerButton span {
83 | margin-left: 5px;
84 | font-weight: bold;
85 | }
86 |
87 | .closeButton {
88 | position: absolute;
89 | right: 3px;
90 | top: 8px;
91 | background: none;
92 | }
93 |
--------------------------------------------------------------------------------
/frontend/src/components/RoomCard/RoomCard.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styles from './RoomCard.module.css';
3 | import { useHistory } from 'react-router-dom';
4 |
5 | const RoomCard = ({ room }) => {
6 | const history = useHistory();
7 | return (
8 | {
10 | history.push(`/room/${room.id}`);
11 | }}
12 | className={styles.card}
13 | >
14 |
{room.topic}
15 |
20 |
21 | {room.speakers.map((speaker) => (
22 |
27 | ))}
28 |
29 |
30 | {room.speakers.map((speaker) => (
31 |
32 |
{speaker.name}
33 |
37 |
38 | ))}
39 |
40 |
41 |
42 |
{room.totalPeople}
43 |
44 |
45 |
46 | );
47 | };
48 |
49 | export default RoomCard;
50 |
--------------------------------------------------------------------------------
/frontend/src/components/RoomCard/RoomCard.module.css:
--------------------------------------------------------------------------------
1 | .card {
2 | background: #1d1d1d;
3 | padding: 20px;
4 | border-radius: 10px;
5 | cursor: pointer;
6 | }
7 |
8 | .speakers {
9 | display: flex;
10 | align-items: center;
11 | position: relative;
12 | margin: 20px 0;
13 | }
14 |
15 | .avatars img {
16 | width: 40px;
17 | height: 40px;
18 | border-radius: 50%;
19 | object-fit: cover;
20 | border: 2px solid #20bd5f;
21 | position: absolute;
22 | top: 0;
23 | left: 0;
24 | background: #1d1d1d;
25 | }
26 |
27 | .avatars img:last-child {
28 | top: 20px;
29 | left: 20px;
30 | }
31 |
32 | .names {
33 | margin-left: 100px;
34 | }
35 |
36 | .names span {
37 | padding-bottom: 5px;
38 | display: inline-block;
39 | margin-right: 5px;
40 | }
41 |
42 | .peopleCount {
43 | text-align: right;
44 | }
45 |
46 | .peopleCount span {
47 | font-weight: bold;
48 | margin-right: 5px;
49 | }
50 |
51 | .singleSpeaker .avatars img {
52 | position: initial;
53 | }
54 |
55 | .singleSpeaker .names {
56 | margin-left: 20px;
57 | }
58 |
--------------------------------------------------------------------------------
/frontend/src/components/shared/Button/Button.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styles from './Button.module.css';
3 | const Button = ({ text, onClick }) => {
4 | return (
5 |
6 | {text}
7 |
12 |
13 | );
14 | };
15 | export default Button;
16 |
--------------------------------------------------------------------------------
/frontend/src/components/shared/Button/Button.module.css:
--------------------------------------------------------------------------------
1 | .button {
2 | background: #0077ff;
3 | padding: 10px 20px;
4 | border: none;
5 | outline: none;
6 | display: flex;
7 | align-items: center;
8 | margin: 0 auto;
9 | color: #fff;
10 | font-size: 18px;
11 | font-weight: bold;
12 | border-radius: 50px;
13 | cursor: pointer;
14 | transition: all 0.3s ease-in-out;
15 | }
16 | .button:hover {
17 | background: #014a9c;
18 | }
19 | .arrow {
20 | margin-left: 10px;
21 | }
22 |
--------------------------------------------------------------------------------
/frontend/src/components/shared/Card/Card.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styles from './Card.module.css';
3 |
4 | const Card = ({ title, icon, children }) => {
5 | return (
6 |
7 |
8 | {icon &&
}
9 | {title &&
{title} }
10 |
11 | {children}
12 |
13 | );
14 | };
15 |
16 | export default Card;
17 |
--------------------------------------------------------------------------------
/frontend/src/components/shared/Card/Card.module.css:
--------------------------------------------------------------------------------
1 | .card {
2 | width: 500px;
3 | max-width: 90%;
4 | min-height: 300px;
5 | background: #1d1d1d;
6 | padding: 30px;
7 | border-radius: 20px;
8 | display: flex;
9 | flex-direction: column;
10 | justify-content: center;
11 | align-items: center;
12 | }
13 | .headingWrapper {
14 | display: flex;
15 | align-items: center;
16 | justify-content: center;
17 | margin-bottom: 30px;
18 | }
19 | .heading {
20 | font-size: 22px;
21 | font-weight: bold;
22 | margin-left: 10px;
23 | }
24 |
--------------------------------------------------------------------------------
/frontend/src/components/shared/Loader/Loader.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Card from '../Card/Card';
3 | import styles from './Loader.module.css';
4 |
5 | const Loader = ({ message }) => {
6 | return (
7 |
8 |
9 |
16 |
23 |
27 |
28 | {message}
29 |
30 |
31 | );
32 | };
33 |
34 | export default Loader;
35 |
--------------------------------------------------------------------------------
/frontend/src/components/shared/Loader/Loader.module.css:
--------------------------------------------------------------------------------
1 | .message {
2 | font-weight: bold;
3 | font-size: 22px;
4 | }
5 |
6 | .spinner {
7 | margin-bottom: 20px;
8 | animation: spin 1s ease-in-out infinite;
9 | }
10 |
11 | @keyframes spin {
12 | 100% {
13 | transform: rotate(360deg);
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/frontend/src/components/shared/Navigation/Navigation.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link } from 'react-router-dom';
3 | import { logout } from '../../../http';
4 | import styles from './Navigation.module.css';
5 | import { useDispatch, useSelector } from 'react-redux';
6 | import { setAuth } from '../../../store/authSlice';
7 |
8 | const Navigation = () => {
9 | const brandStyle = {
10 | color: '#fff',
11 | textDecoration: 'none',
12 | fontWeight: 'bold',
13 | fontSize: '22px',
14 | display: 'flex',
15 | alignItems: 'center',
16 | };
17 |
18 | const logoText = {
19 | marginLeft: '10px',
20 | };
21 | const dispatch = useDispatch();
22 | const { isAuth, user } = useSelector((state) => state.auth);
23 | async function logoutUser() {
24 | try {
25 | const { data } = await logout();
26 | dispatch(setAuth(data));
27 | } catch (err) {
28 | console.log(err);
29 | }
30 | }
31 |
32 | return (
33 |
34 |
35 |
36 | Codershouse
37 |
38 | {isAuth && (
39 |
40 |
{user?.name}
41 |
42 |
53 |
54 |
58 |
59 |
60 |
61 | )}
62 |
63 | );
64 | };
65 |
66 | export default Navigation;
67 |
--------------------------------------------------------------------------------
/frontend/src/components/shared/Navigation/Navigation.module.css:
--------------------------------------------------------------------------------
1 | .navbar {
2 | padding: 20px 0;
3 | display: flex;
4 | align-items: center;
5 | justify-content: space-between;
6 | }
7 |
8 | .logoutButton {
9 | background: none;
10 | cursor: pointer;
11 | }
12 |
13 | .navRight {
14 | display: flex;
15 | align-items: center;
16 | }
17 |
18 | .avatar {
19 | border-radius: 50%;
20 | object-fit: cover;
21 | border: 3px solid #0077ff;
22 | margin: 0 20px;
23 | margin-right: 10px;
24 | }
25 |
--------------------------------------------------------------------------------
/frontend/src/components/shared/TextInput/TextInput.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styles from './TextInput.module.css';
3 |
4 | const TextInput = (props) => {
5 | return (
6 |
7 |
15 |
16 | );
17 | };
18 |
19 | export default TextInput;
20 |
--------------------------------------------------------------------------------
/frontend/src/components/shared/TextInput/TextInput.module.css:
--------------------------------------------------------------------------------
1 | .input {
2 | background: #323232;
3 | border: none;
4 | padding: 10px 20px;
5 | width: 200px;
6 | color: #fff;
7 | font-size: 18px;
8 | border-radius: 10px;
9 | outline: none;
10 | }
11 |
--------------------------------------------------------------------------------
/frontend/src/hooks/useLoadingWithRefresh.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 | import axios from 'axios';
3 | import { useDispatch } from 'react-redux';
4 | import { setAuth } from '../store/authSlice';
5 | export function useLoadingWithRefresh() {
6 | const [loading, setLoading] = useState(true);
7 | const dispatch = useDispatch();
8 | useEffect(() => {
9 | (async () => {
10 | try {
11 | const { data } = await axios.get(
12 | `${process.env.REACT_APP_API_URL}/api/refresh`,
13 | {
14 | withCredentials: true,
15 | }
16 | );
17 | dispatch(setAuth(data));
18 | setLoading(false);
19 | } catch (err) {
20 | console.log(err);
21 | setLoading(false);
22 | }
23 | })();
24 | }, []);
25 |
26 | return { loading };
27 | }
28 |
--------------------------------------------------------------------------------
/frontend/src/hooks/useStateWithCallback.js:
--------------------------------------------------------------------------------
1 | import { useState, useRef, useEffect, useCallback } from 'react';
2 | export const useStateWithCallback = (intialState) => {
3 | const [state, setState] = useState(intialState);
4 | const cbRef = useRef(null);
5 |
6 | const updateState = useCallback((newState, cb) => {
7 | cbRef.current = cb;
8 |
9 | setState((prev) =>
10 | typeof newState === 'function' ? newState(prev) : newState
11 | );
12 | }, []);
13 |
14 | useEffect(() => {
15 | if (cbRef.current) {
16 | cbRef.current(state);
17 | cbRef.current = null;
18 | }
19 | }, [state]);
20 |
21 | return [state, updateState];
22 | };
23 |
--------------------------------------------------------------------------------
/frontend/src/hooks/useWebRTC.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useState, useRef, useCallback } from 'react';
2 | import { ACTIONS } from '../actions';
3 | import socketInit from '../socket';
4 | import freeice from 'freeice';
5 | import { useStateWithCallback } from './useStateWithCallback';
6 |
7 | export const useWebRTC = (roomId, user) => {
8 | const [clients, setClients] = useStateWithCallback([]);
9 | const audioElements = useRef({});
10 | const connections = useRef({});
11 | const socket = useRef(null);
12 | const localMediaStream = useRef(null);
13 | const clientsRef = useRef(null);
14 |
15 | const addNewClient = useCallback(
16 | (newClient, cb) => {
17 | const lookingFor = clients.find(
18 | (client) => client.id === newClient.id
19 | );
20 |
21 | if (lookingFor === undefined) {
22 | setClients(
23 | (existingClients) => [...existingClients, newClient],
24 | cb
25 | );
26 | }
27 | },
28 | [clients, setClients]
29 | );
30 |
31 | useEffect(() => {
32 | clientsRef.current = clients;
33 | }, [clients]);
34 |
35 | useEffect(() => {
36 | const initChat = async () => {
37 | socket.current = socketInit();
38 | await captureMedia();
39 | addNewClient({ ...user, muted: true }, () => {
40 | const localElement = audioElements.current[user.id];
41 | if (localElement) {
42 | localElement.volume = 0;
43 | localElement.srcObject = localMediaStream.current;
44 | }
45 | });
46 | socket.current.on(ACTIONS.MUTE_INFO, ({ userId, isMute }) => {
47 | handleSetMute(isMute, userId);
48 | });
49 |
50 | socket.current.on(ACTIONS.ADD_PEER, handleNewPeer);
51 | socket.current.on(ACTIONS.REMOVE_PEER, handleRemovePeer);
52 | socket.current.on(ACTIONS.ICE_CANDIDATE, handleIceCandidate);
53 | socket.current.on(ACTIONS.SESSION_DESCRIPTION, setRemoteMedia);
54 | socket.current.on(ACTIONS.MUTE, ({ peerId, userId }) => {
55 | handleSetMute(true, userId);
56 | });
57 | socket.current.on(ACTIONS.UNMUTE, ({ peerId, userId }) => {
58 | handleSetMute(false, userId);
59 | });
60 | socket.current.emit(ACTIONS.JOIN, {
61 | roomId,
62 | user,
63 | });
64 |
65 | async function captureMedia() {
66 | // Start capturing local audio stream.
67 | localMediaStream.current =
68 | await navigator.mediaDevices.getUserMedia({
69 | audio: true,
70 | });
71 | }
72 | async function handleNewPeer({
73 | peerId,
74 | createOffer,
75 | user: remoteUser,
76 | }) {
77 | if (peerId in connections.current) {
78 | return console.warn(
79 | `You are already connected with ${peerId} (${user.name})`
80 | );
81 | }
82 |
83 | // Store it to connections
84 | connections.current[peerId] = new RTCPeerConnection({
85 | iceServers: freeice(),
86 | });
87 |
88 | // Handle new ice candidate on this peer connection
89 | connections.current[peerId].onicecandidate = (event) => {
90 | socket.current.emit(ACTIONS.RELAY_ICE, {
91 | peerId,
92 | icecandidate: event.candidate,
93 | });
94 | };
95 |
96 | // Handle on track event on this connection
97 | connections.current[peerId].ontrack = ({
98 | streams: [remoteStream],
99 | }) => {
100 | addNewClient({ ...remoteUser, muted: true }, () => {
101 | // get current users mute info
102 | const currentUser = clientsRef.current.find(
103 | (client) => client.id === user.id
104 | );
105 | if (currentUser) {
106 | socket.current.emit(ACTIONS.MUTE_INFO, {
107 | userId: user.id,
108 | roomId,
109 | isMute: currentUser.muted,
110 | });
111 | }
112 | if (audioElements.current[remoteUser.id]) {
113 | audioElements.current[remoteUser.id].srcObject =
114 | remoteStream;
115 | } else {
116 | let settled = false;
117 | const interval = setInterval(() => {
118 | if (audioElements.current[remoteUser.id]) {
119 | audioElements.current[
120 | remoteUser.id
121 | ].srcObject = remoteStream;
122 | settled = true;
123 | }
124 |
125 | if (settled) {
126 | clearInterval(interval);
127 | }
128 | }, 300);
129 | }
130 | });
131 | };
132 |
133 | // Add connection to peer connections track
134 | localMediaStream.current.getTracks().forEach((track) => {
135 | connections.current[peerId].addTrack(
136 | track,
137 | localMediaStream.current
138 | );
139 | });
140 |
141 | // Create an offer if required
142 | if (createOffer) {
143 | const offer = await connections.current[
144 | peerId
145 | ].createOffer();
146 |
147 | // Set as local description
148 | await connections.current[peerId].setLocalDescription(
149 | offer
150 | );
151 |
152 | // send offer to the server
153 | socket.current.emit(ACTIONS.RELAY_SDP, {
154 | peerId,
155 | sessionDescription: offer,
156 | });
157 | }
158 | }
159 | async function handleRemovePeer({ peerId, userId }) {
160 | // Correction: peerID to peerId
161 | if (connections.current[peerId]) {
162 | connections.current[peerId].close();
163 | }
164 |
165 | delete connections.current[peerId];
166 | delete audioElements.current[peerId];
167 | setClients((list) => list.filter((c) => c.id !== userId));
168 | }
169 | async function handleIceCandidate({ peerId, icecandidate }) {
170 | if (icecandidate) {
171 | connections.current[peerId].addIceCandidate(icecandidate);
172 | }
173 | }
174 | async function setRemoteMedia({
175 | peerId,
176 | sessionDescription: remoteSessionDescription,
177 | }) {
178 | connections.current[peerId].setRemoteDescription(
179 | new RTCSessionDescription(remoteSessionDescription)
180 | );
181 |
182 | // If session descrition is offer then create an answer
183 | if (remoteSessionDescription.type === 'offer') {
184 | const connection = connections.current[peerId];
185 |
186 | const answer = await connection.createAnswer();
187 | connection.setLocalDescription(answer);
188 |
189 | socket.current.emit(ACTIONS.RELAY_SDP, {
190 | peerId,
191 | sessionDescription: answer,
192 | });
193 | }
194 | }
195 | async function handleSetMute(mute, userId) {
196 | const clientIdx = clientsRef.current
197 | .map((client) => client.id)
198 | .indexOf(userId);
199 | const allConnectedClients = JSON.parse(
200 | JSON.stringify(clientsRef.current)
201 | );
202 | if (clientIdx > -1) {
203 | allConnectedClients[clientIdx].muted = mute;
204 | setClients(allConnectedClients);
205 | }
206 | }
207 | };
208 |
209 | initChat();
210 | return () => {
211 | localMediaStream.current
212 | .getTracks()
213 | .forEach((track) => track.stop());
214 | socket.current.emit(ACTIONS.LEAVE, { roomId });
215 | for (let peerId in connections.current) {
216 | connections.current[peerId].close();
217 | delete connections.current[peerId];
218 | delete audioElements.current[peerId];
219 | }
220 | socket.current.off(ACTIONS.ADD_PEER);
221 | socket.current.off(ACTIONS.REMOVE_PEER);
222 | socket.current.off(ACTIONS.ICE_CANDIDATE);
223 | socket.current.off(ACTIONS.SESSION_DESCRIPTION);
224 | socket.current.off(ACTIONS.MUTE);
225 | socket.current.off(ACTIONS.UNMUTE);
226 | };
227 | }, []);
228 |
229 | const provideRef = (instance, userId) => {
230 | audioElements.current[userId] = instance;
231 | };
232 |
233 | const handleMute = (isMute, userId) => {
234 | let settled = false;
235 |
236 | if (userId === user.id) {
237 | let interval = setInterval(() => {
238 | if (localMediaStream.current) {
239 | localMediaStream.current.getTracks()[0].enabled = !isMute;
240 | if (isMute) {
241 | socket.current.emit(ACTIONS.MUTE, {
242 | roomId,
243 | userId: user.id,
244 | });
245 | } else {
246 | socket.current.emit(ACTIONS.UNMUTE, {
247 | roomId,
248 | userId: user.id,
249 | });
250 | }
251 | settled = true;
252 | }
253 | if (settled) {
254 | clearInterval(interval);
255 | }
256 | }, 200);
257 | }
258 | };
259 |
260 | return {
261 | clients,
262 | provideRef,
263 | handleMute,
264 | };
265 | };
266 |
--------------------------------------------------------------------------------
/frontend/src/hooks/useWebRTC.old.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useState, useRef, useCallback } from 'react';
2 | import { ACTIONS } from '../actions';
3 | import socketInit from '../socket';
4 | import freeice from 'freeice';
5 | import { useStateWithCallback } from './useStateWithCallback';
6 |
7 | export const useWebRTC = (roomId, user) => {
8 | const [clients, setClients] = useStateWithCallback([]);
9 | const audioElements = useRef({});
10 | const connections = useRef({});
11 | const socket = useRef(null);
12 | const localMediaStream = useRef(null);
13 | const clientsRef = useRef(null);
14 |
15 | useEffect(() => {
16 | console.log('render socketInit', 2);
17 | socket.current = socketInit();
18 | }, []);
19 |
20 | const addNewClient = useCallback(
21 | (newClient, cb) => {
22 | const lookingFor = clients.find(
23 | (client) => client.id === newClient.id
24 | );
25 |
26 | if (lookingFor === undefined) {
27 | setClients(
28 | (existingClients) => [...existingClients, newClient],
29 | cb
30 | );
31 | }
32 | },
33 | [clients, setClients]
34 | );
35 |
36 | useEffect(() => {
37 | console.log('render clientsRef.current = clients', 3);
38 | clientsRef.current = clients;
39 | }, [clients]);
40 |
41 | useEffect(() => {
42 | console.log('render startCapture', 4);
43 | const startCapture = async () => {
44 | // Start capturing local audio stream.
45 | localMediaStream.current =
46 | await navigator.mediaDevices.getUserMedia({
47 | audio: true,
48 | });
49 | };
50 |
51 | startCapture().then(() => {
52 | // add user to clients list
53 | console.log('render startCapture then', 5);
54 | addNewClient({ ...user, muted: true }, () => {
55 | console.log('render add new client me', 6);
56 | const localElement = audioElements.current[user.id];
57 | if (localElement) {
58 | localElement.volume = 0;
59 | localElement.srcObject = localMediaStream.current;
60 | }
61 | });
62 | console.log('render before ACTIONS.JOIN', 7);
63 |
64 | // Emit the action to join
65 | socket.current.emit(ACTIONS.JOIN, {
66 | roomId,
67 | user,
68 | });
69 | });
70 |
71 | // Leaving the room
72 | return () => {
73 | localMediaStream.current
74 | .getTracks()
75 | .forEach((track) => track.stop());
76 | socket.current.emit(ACTIONS.LEAVE, { roomId });
77 | };
78 | }, []);
79 | // Handle new peer
80 |
81 | useEffect(() => {
82 | console.log('render handle new peer useEffect', 8);
83 | const handleNewPeer = async ({
84 | peerId,
85 | createOffer,
86 | user: remoteUser,
87 | }) => {
88 | // If already connected then prevent connecting again
89 | console.log('render inside handle new peer', 8);
90 | if (peerId in connections.current) {
91 | return console.warn(
92 | `You are already connected with ${peerId} (${user.name})`
93 | );
94 | }
95 |
96 | // Store it to connections
97 | connections.current[peerId] = new RTCPeerConnection({
98 | iceServers: freeice(),
99 | });
100 |
101 | // Handle new ice candidate on this peer connection
102 | connections.current[peerId].onicecandidate = (event) => {
103 | socket.current.emit(ACTIONS.RELAY_ICE, {
104 | peerId,
105 | icecandidate: event.candidate,
106 | });
107 | };
108 |
109 | // Handle on track event on this connection
110 | connections.current[peerId].ontrack = ({
111 | streams: [remoteStream],
112 | }) => {
113 | addNewClient({ ...remoteUser, muted: true }, () => {
114 | console.log('render add new client remote', 9);
115 | if (audioElements.current[remoteUser.id]) {
116 | audioElements.current[remoteUser.id].srcObject =
117 | remoteStream;
118 | } else {
119 | let settled = false;
120 | const interval = setInterval(() => {
121 | if (audioElements.current[remoteUser.id]) {
122 | audioElements.current[remoteUser.id].srcObject =
123 | remoteStream;
124 | settled = true;
125 | }
126 |
127 | if (settled) {
128 | clearInterval(interval);
129 | }
130 | }, 300);
131 | }
132 | });
133 | };
134 |
135 | // Add connection to peer connections track
136 | localMediaStream.current.getTracks().forEach((track) => {
137 | connections.current[peerId].addTrack(
138 | track,
139 | localMediaStream.current
140 | );
141 | });
142 |
143 | // Create an offer if required
144 | if (createOffer) {
145 | const offer = await connections.current[peerId].createOffer();
146 |
147 | // Set as local description
148 | await connections.current[peerId].setLocalDescription(offer);
149 |
150 | // send offer to the server
151 | socket.current.emit(ACTIONS.RELAY_SDP, {
152 | peerId,
153 | sessionDescription: offer,
154 | });
155 | }
156 | };
157 |
158 | // Listen for add peer event from ws
159 | socket.current.on(ACTIONS.ADD_PEER, handleNewPeer);
160 | return () => {
161 | socket.current.off(ACTIONS.ADD_PEER);
162 | };
163 | }, []);
164 |
165 | // Handle ice candidate
166 | useEffect(() => {
167 | console.log('render handle ice candidate out', 10);
168 | socket.current.on(ACTIONS.ICE_CANDIDATE, ({ peerId, icecandidate }) => {
169 | if (icecandidate) {
170 | connections.current[peerId].addIceCandidate(icecandidate);
171 | }
172 | });
173 |
174 | return () => {
175 | socket.current.off(ACTIONS.ICE_CANDIDATE);
176 | };
177 | }, []);
178 |
179 | // Handle session description
180 |
181 | useEffect(() => {
182 | console.log('render set remote media', 11);
183 | const setRemoteMedia = async ({
184 | peerId,
185 | sessionDescription: remoteSessionDescription,
186 | }) => {
187 | connections.current[peerId].setRemoteDescription(
188 | new RTCSessionDescription(remoteSessionDescription)
189 | );
190 |
191 | // If session descrition is offer then create an answer
192 | if (remoteSessionDescription.type === 'offer') {
193 | const connection = connections.current[peerId];
194 |
195 | const answer = await connection.createAnswer();
196 | connection.setLocalDescription(answer);
197 |
198 | socket.current.emit(ACTIONS.RELAY_SDP, {
199 | peerId,
200 | sessionDescription: answer,
201 | });
202 | }
203 | };
204 |
205 | socket.current.on(ACTIONS.SESSION_DESCRIPTION, setRemoteMedia);
206 | return () => {
207 | socket.current.off(ACTIONS.SESSION_DESCRIPTION);
208 | };
209 | }, []);
210 |
211 | useEffect(() => {
212 | console.log('render handle remove peer out', 12);
213 | const handleRemovePeer = ({ peerId, userId }) => {
214 | console.log('render inside handle remove peer out', 13);
215 | // Correction: peerID to peerId
216 | if (connections.current[peerId]) {
217 | connections.current[peerId].close();
218 | }
219 |
220 | delete connections.current[peerId];
221 | delete audioElements.current[peerId];
222 | setClients((list) => list.filter((c) => c.id !== userId));
223 | };
224 |
225 | socket.current.on(ACTIONS.REMOVE_PEER, handleRemovePeer);
226 |
227 | return () => {
228 | for (let peerId in connections.current) {
229 | connections.current[peerId].close();
230 | delete connections.current[peerId];
231 | delete audioElements.current[peerId];
232 | console.log('removing', connections.current);
233 | }
234 | socket.current.off(ACTIONS.REMOVE_PEER);
235 | };
236 | }, []);
237 |
238 | useEffect(() => {
239 | // handle mute and unmute
240 | console.log('render inside mute useEffect', 14);
241 | socket.current.on(ACTIONS.MUTE, ({ peerId, userId }) => {
242 | setMute(true, userId);
243 | });
244 |
245 | socket.current.on(ACTIONS.UNMUTE, ({ peerId, userId }) => {
246 | setMute(false, userId);
247 | });
248 |
249 | const setMute = (mute, userId) => {
250 | const clientIdx = clientsRef.current
251 | .map((client) => client.id)
252 | .indexOf(userId);
253 | const allConnectedClients = JSON.parse(
254 | JSON.stringify(clientsRef.current)
255 | );
256 | if (clientIdx > -1) {
257 | allConnectedClients[clientIdx].muted = mute;
258 | setClients(allConnectedClients);
259 | }
260 | };
261 | }, []);
262 |
263 | const provideRef = (instance, userId) => {
264 | audioElements.current[userId] = instance;
265 | };
266 |
267 | const handleMute = (isMute, userId) => {
268 | let settled = false;
269 |
270 | if (userId === user.id) {
271 | let interval = setInterval(() => {
272 | if (localMediaStream.current) {
273 | localMediaStream.current.getTracks()[0].enabled = !isMute;
274 | if (isMute) {
275 | socket.current.emit(ACTIONS.MUTE, {
276 | roomId,
277 | userId: user.id,
278 | });
279 | } else {
280 | socket.current.emit(ACTIONS.UNMUTE, {
281 | roomId,
282 | userId: user.id,
283 | });
284 | }
285 | settled = true;
286 | }
287 | if (settled) {
288 | clearInterval(interval);
289 | }
290 | }, 200);
291 | }
292 | };
293 |
294 | // useEffect(() => {
295 | // socket.current.emit(ACTIONS.MUTE_INFO, {
296 | // roomId,
297 | // });
298 | // console.log('hello');
299 | // socket.current.on(ACTIONS.MUTE_INFO, (muteMap) => {
300 | // console.log('mute map', muteMap);
301 | // setClients(
302 | // (list) => {
303 | // return list.map((client) => {
304 | // console.log('client map', client);
305 | // return {
306 | // ...client,
307 | // muted: muteMap[client.id],
308 | // };
309 | // });
310 | // },
311 | // (prev) => {
312 | // console.log('prev', prev);
313 | // }
314 | // );
315 | // });
316 | // }, []);
317 |
318 | return {
319 | clients,
320 | provideRef,
321 | handleMute,
322 | };
323 | };
324 |
--------------------------------------------------------------------------------
/frontend/src/hooks/useWebRTC.old2.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useState, useRef, useCallback } from 'react';
2 | import { ACTIONS } from '../actions';
3 | import socketInit from '../socket';
4 | import freeice from 'freeice';
5 | import { useStateWithCallback } from './useStateWithCallback';
6 |
7 | export const useWebRTC = (roomId, user) => {
8 | const [clients, setClients] = useStateWithCallback([]);
9 | const audioElements = useRef({});
10 | const connections = useRef({});
11 | const socket = useRef(null);
12 | const localMediaStream = useRef(null);
13 | const clientsRef = useRef(null);
14 |
15 | useEffect(() => {
16 | console.log('render socketInit', 2);
17 | socket.current = socketInit();
18 | }, []);
19 |
20 | const addNewClient = useCallback(
21 | (newClient, cb) => {
22 | const lookingFor = clients.find(
23 | (client) => client.id === newClient.id
24 | );
25 |
26 | if (lookingFor === undefined) {
27 | setClients(
28 | (existingClients) => [...existingClients, newClient],
29 | cb
30 | );
31 | }
32 | },
33 | [clients, setClients]
34 | );
35 |
36 | useEffect(() => {
37 | console.log('render clientsRef.current = clients', 3);
38 | clientsRef.current = clients;
39 | }, [clients]);
40 |
41 | useEffect(() => {
42 | console.log('render startCapture', 4);
43 | const initChat = async () => {
44 | const startCapture = async () => {
45 | // Start capturing local audio stream.
46 | localMediaStream.current =
47 | await navigator.mediaDevices.getUserMedia({
48 | audio: true,
49 | });
50 | };
51 |
52 | await startCapture();
53 | // =============================== Handle new peer ================================
54 | const handleNewPeer = async ({
55 | peerId,
56 | createOffer,
57 | user: remoteUser,
58 | }) => {
59 | // If already connected then prevent connecting again
60 | console.log('render inside handle new peer', 8);
61 | if (peerId in connections.current) {
62 | return console.warn(
63 | `You are already connected with ${peerId} (${user.name})`
64 | );
65 | }
66 |
67 | // Store it to connections
68 | connections.current[peerId] = new RTCPeerConnection({
69 | iceServers: freeice(),
70 | });
71 |
72 | // Handle new ice candidate on this peer connection
73 | connections.current[peerId].onicecandidate = (event) => {
74 | socket.current.emit(ACTIONS.RELAY_ICE, {
75 | peerId,
76 | icecandidate: event.candidate,
77 | });
78 | };
79 |
80 | // Handle on track event on this connection
81 | connections.current[peerId].ontrack = ({
82 | streams: [remoteStream],
83 | }) => {
84 | addNewClient({ ...remoteUser, muted: true }, () => {
85 | console.log('render add new client remote', 9);
86 | if (audioElements.current[remoteUser.id]) {
87 | audioElements.current[remoteUser.id].srcObject =
88 | remoteStream;
89 | } else {
90 | let settled = false;
91 | const interval = setInterval(() => {
92 | if (audioElements.current[remoteUser.id]) {
93 | audioElements.current[
94 | remoteUser.id
95 | ].srcObject = remoteStream;
96 | settled = true;
97 | }
98 |
99 | if (settled) {
100 | clearInterval(interval);
101 | }
102 | }, 300);
103 | }
104 | });
105 | };
106 |
107 | // Add connection to peer connections track
108 | localMediaStream.current.getTracks().forEach((track) => {
109 | connections.current[peerId].addTrack(
110 | track,
111 | localMediaStream.current
112 | );
113 | });
114 |
115 | // Create an offer if required
116 | if (createOffer) {
117 | const offer = await connections.current[
118 | peerId
119 | ].createOffer();
120 |
121 | // Set as local description
122 | await connections.current[peerId].setLocalDescription(
123 | offer
124 | );
125 |
126 | // send offer to the server
127 | socket.current.emit(ACTIONS.RELAY_SDP, {
128 | peerId,
129 | sessionDescription: offer,
130 | });
131 | }
132 | };
133 |
134 | // Listen for add peer event from ws
135 | socket.current.on(ACTIONS.ADD_PEER, handleNewPeer);
136 |
137 | // =============================== Handle add new client me ================================
138 | addNewClient({ ...user, muted: true }, () => {
139 | console.log('render add new client me', 6);
140 | const localElement = audioElements.current[user.id];
141 | if (localElement) {
142 | localElement.volume = 0;
143 | localElement.srcObject = localMediaStream.current;
144 | }
145 | });
146 | console.log('render before ACTIONS.JOIN', 7);
147 |
148 | // Emit the action to join
149 | socket.current.emit(ACTIONS.JOIN, {
150 | roomId,
151 | user,
152 | });
153 |
154 | // =============================== handle ice candidate ================================
155 |
156 | console.log('render handle ice candidate out', 10);
157 | socket.current.on(
158 | ACTIONS.ICE_CANDIDATE,
159 | ({ peerId, icecandidate }) => {
160 | if (icecandidate) {
161 | connections.current[peerId].addIceCandidate(
162 | icecandidate
163 | );
164 | }
165 | }
166 | );
167 |
168 | // =============================== handle session description ================================
169 | console.log('render set remote media', 11);
170 | const setRemoteMedia = async ({
171 | peerId,
172 | sessionDescription: remoteSessionDescription,
173 | }) => {
174 | connections.current[peerId].setRemoteDescription(
175 | new RTCSessionDescription(remoteSessionDescription)
176 | );
177 |
178 | // If session descrition is offer then create an answer
179 | if (remoteSessionDescription.type === 'offer') {
180 | const connection = connections.current[peerId];
181 |
182 | const answer = await connection.createAnswer();
183 | connection.setLocalDescription(answer);
184 |
185 | socket.current.emit(ACTIONS.RELAY_SDP, {
186 | peerId,
187 | sessionDescription: answer,
188 | });
189 | }
190 | };
191 |
192 | socket.current.on(ACTIONS.SESSION_DESCRIPTION, setRemoteMedia);
193 |
194 | // =============================== handle remove peer ================================
195 | console.log('render handle remove peer out', 12);
196 | const handleRemovePeer = ({ peerId, userId }) => {
197 | console.log('render inside handle remove peer out', 13);
198 | // Correction: peerID to peerId
199 | if (connections.current[peerId]) {
200 | connections.current[peerId].close();
201 | }
202 |
203 | delete connections.current[peerId];
204 | delete audioElements.current[peerId];
205 | setClients((list) => list.filter((c) => c.id !== userId));
206 | };
207 |
208 | socket.current.on(ACTIONS.REMOVE_PEER, handleRemovePeer);
209 |
210 | // =============================== handle mute ================================
211 | console.log('render inside mute useEffect', 14);
212 | socket.current.on(ACTIONS.MUTE, ({ peerId, userId }) => {
213 | setMute(true, userId);
214 | });
215 |
216 | socket.current.on(ACTIONS.UNMUTE, ({ peerId, userId }) => {
217 | setMute(false, userId);
218 | });
219 |
220 | const setMute = (mute, userId) => {
221 | const clientIdx = clientsRef.current
222 | .map((client) => client.id)
223 | .indexOf(userId);
224 | const allConnectedClients = JSON.parse(
225 | JSON.stringify(clientsRef.current)
226 | );
227 | if (clientIdx > -1) {
228 | allConnectedClients[clientIdx].muted = mute;
229 | setClients(allConnectedClients);
230 | }
231 | };
232 | };
233 |
234 | initChat();
235 |
236 | // Leaving the room
237 | return () => {
238 | localMediaStream.current
239 | .getTracks()
240 | .forEach((track) => track.stop());
241 | socket.current.emit(ACTIONS.LEAVE, { roomId });
242 |
243 | for (let peerId in connections.current) {
244 | connections.current[peerId].close();
245 | delete connections.current[peerId];
246 | delete audioElements.current[peerId];
247 | console.log('removing', connections.current);
248 | }
249 | socket.current.off(ACTIONS.REMOVE_PEER);
250 | };
251 | }, []);
252 | // Handle new peer
253 |
254 | // useEffect(() => {
255 | // console.log('render handle new peer useEffect', 8);
256 | // const handleNewPeer = async ({
257 | // peerId,
258 | // createOffer,
259 | // user: remoteUser,
260 | // }) => {
261 | // // If already connected then prevent connecting again
262 | // console.log('render inside handle new peer', 8);
263 | // if (peerId in connections.current) {
264 | // return console.warn(
265 | // `You are already connected with ${peerId} (${user.name})`
266 | // );
267 | // }
268 |
269 | // // Store it to connections
270 | // connections.current[peerId] = new RTCPeerConnection({
271 | // iceServers: freeice(),
272 | // });
273 |
274 | // // Handle new ice candidate on this peer connection
275 | // connections.current[peerId].onicecandidate = (event) => {
276 | // socket.current.emit(ACTIONS.RELAY_ICE, {
277 | // peerId,
278 | // icecandidate: event.candidate,
279 | // });
280 | // };
281 |
282 | // // Handle on track event on this connection
283 | // connections.current[peerId].ontrack = ({
284 | // streams: [remoteStream],
285 | // }) => {
286 | // addNewClient({ ...remoteUser, muted: true }, () => {
287 | // console.log('render add new client remote', 9);
288 | // if (audioElements.current[remoteUser.id]) {
289 | // audioElements.current[remoteUser.id].srcObject =
290 | // remoteStream;
291 | // } else {
292 | // let settled = false;
293 | // const interval = setInterval(() => {
294 | // if (audioElements.current[remoteUser.id]) {
295 | // audioElements.current[remoteUser.id].srcObject =
296 | // remoteStream;
297 | // settled = true;
298 | // }
299 |
300 | // if (settled) {
301 | // clearInterval(interval);
302 | // }
303 | // }, 300);
304 | // }
305 | // });
306 | // };
307 |
308 | // // Add connection to peer connections track
309 | // localMediaStream.current.getTracks().forEach((track) => {
310 | // connections.current[peerId].addTrack(
311 | // track,
312 | // localMediaStream.current
313 | // );
314 | // });
315 |
316 | // // Create an offer if required
317 | // if (createOffer) {
318 | // const offer = await connections.current[peerId].createOffer();
319 |
320 | // // Set as local description
321 | // await connections.current[peerId].setLocalDescription(offer);
322 |
323 | // // send offer to the server
324 | // socket.current.emit(ACTIONS.RELAY_SDP, {
325 | // peerId,
326 | // sessionDescription: offer,
327 | // });
328 | // }
329 | // };
330 |
331 | // // Listen for add peer event from ws
332 | // socket.current.on(ACTIONS.ADD_PEER, handleNewPeer);
333 | // return () => {
334 | // socket.current.off(ACTIONS.ADD_PEER);
335 | // };
336 | // }, []);
337 |
338 | // Handle ice candidate
339 | // useEffect(() => {
340 | // console.log('render handle ice candidate out', 10);
341 | // socket.current.on(ACTIONS.ICE_CANDIDATE, ({ peerId, icecandidate }) => {
342 | // if (icecandidate) {
343 | // connections.current[peerId].addIceCandidate(icecandidate);
344 | // }
345 | // });
346 |
347 | // return () => {
348 | // socket.current.off(ACTIONS.ICE_CANDIDATE);
349 | // };
350 | // }, []);
351 |
352 | // Handle session description
353 |
354 | // useEffect(() => {
355 | // console.log('render set remote media', 11);
356 | // const setRemoteMedia = async ({
357 | // peerId,
358 | // sessionDescription: remoteSessionDescription,
359 | // }) => {
360 | // connections.current[peerId].setRemoteDescription(
361 | // new RTCSessionDescription(remoteSessionDescription)
362 | // );
363 |
364 | // // If session descrition is offer then create an answer
365 | // if (remoteSessionDescription.type === 'offer') {
366 | // const connection = connections.current[peerId];
367 |
368 | // const answer = await connection.createAnswer();
369 | // connection.setLocalDescription(answer);
370 |
371 | // socket.current.emit(ACTIONS.RELAY_SDP, {
372 | // peerId,
373 | // sessionDescription: answer,
374 | // });
375 | // }
376 | // };
377 |
378 | // socket.current.on(ACTIONS.SESSION_DESCRIPTION, setRemoteMedia);
379 | // return () => {
380 | // socket.current.off(ACTIONS.SESSION_DESCRIPTION);
381 | // };
382 | // }, []);
383 |
384 | // useEffect(() => {
385 | // console.log('render handle remove peer out', 12);
386 | // const handleRemovePeer = ({ peerId, userId }) => {
387 | // console.log('render inside handle remove peer out', 13);
388 | // // Correction: peerID to peerId
389 | // if (connections.current[peerId]) {
390 | // connections.current[peerId].close();
391 | // }
392 |
393 | // delete connections.current[peerId];
394 | // delete audioElements.current[peerId];
395 | // setClients((list) => list.filter((c) => c.id !== userId));
396 | // };
397 |
398 | // socket.current.on(ACTIONS.REMOVE_PEER, handleRemovePeer);
399 |
400 | // return () => {
401 | // for (let peerId in connections.current) {
402 | // connections.current[peerId].close();
403 | // delete connections.current[peerId];
404 | // delete audioElements.current[peerId];
405 | // console.log('removing', connections.current);
406 | // }
407 | // socket.current.off(ACTIONS.REMOVE_PEER);
408 | // };
409 | // }, []);
410 |
411 | // useEffect(() => {
412 | // // handle mute and unmute
413 | // console.log('render inside mute useEffect', 14);
414 | // socket.current.on(ACTIONS.MUTE, ({ peerId, userId }) => {
415 | // setMute(true, userId);
416 | // });
417 |
418 | // socket.current.on(ACTIONS.UNMUTE, ({ peerId, userId }) => {
419 | // setMute(false, userId);
420 | // });
421 |
422 | // const setMute = (mute, userId) => {
423 | // const clientIdx = clientsRef.current
424 | // .map((client) => client.id)
425 | // .indexOf(userId);
426 | // const allConnectedClients = JSON.parse(
427 | // JSON.stringify(clientsRef.current)
428 | // );
429 | // if (clientIdx > -1) {
430 | // allConnectedClients[clientIdx].muted = mute;
431 | // setClients(allConnectedClients);
432 | // }
433 | // };
434 | // }, []);
435 |
436 | const provideRef = (instance, userId) => {
437 | audioElements.current[userId] = instance;
438 | };
439 |
440 | const handleMute = (isMute, userId) => {
441 | let settled = false;
442 |
443 | if (userId === user.id) {
444 | let interval = setInterval(() => {
445 | if (localMediaStream.current) {
446 | localMediaStream.current.getTracks()[0].enabled = !isMute;
447 | if (isMute) {
448 | socket.current.emit(ACTIONS.MUTE, {
449 | roomId,
450 | userId: user.id,
451 | });
452 | } else {
453 | socket.current.emit(ACTIONS.UNMUTE, {
454 | roomId,
455 | userId: user.id,
456 | });
457 | }
458 | settled = true;
459 | }
460 | if (settled) {
461 | clearInterval(interval);
462 | }
463 | }, 200);
464 | }
465 | };
466 |
467 | // useEffect(() => {
468 | // socket.current.emit(ACTIONS.MUTE_INFO, {
469 | // roomId,
470 | // });
471 | // console.log('hello');
472 | // socket.current.on(ACTIONS.MUTE_INFO, (muteMap) => {
473 | // console.log('mute map', muteMap);
474 | // setClients(
475 | // (list) => {
476 | // return list.map((client) => {
477 | // console.log('client map', client);
478 | // return {
479 | // ...client,
480 | // muted: muteMap[client.id],
481 | // };
482 | // });
483 | // },
484 | // (prev) => {
485 | // console.log('prev', prev);
486 | // }
487 | // );
488 | // });
489 | // }, []);
490 |
491 | return {
492 | clients,
493 | provideRef,
494 | handleMute,
495 | };
496 | };
497 |
--------------------------------------------------------------------------------
/frontend/src/http/index.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 |
3 | const api = axios.create({
4 | baseURL: process.env.REACT_APP_API_URL,
5 | withCredentials: true,
6 | headers: {
7 | 'Content-type': 'application/json',
8 | Accept: 'application/json',
9 | },
10 | });
11 |
12 | // List of all the endpoints
13 | export const sendOtp = (data) => api.post('/api/send-otp', data);
14 | export const verifyOtp = (data) => api.post('/api/verify-otp', data);
15 | export const activate = (data) => api.post('/api/activate', data);
16 | export const logout = () => api.post('/api/logout');
17 | export const createRoom = (data) => api.post('/api/rooms', data);
18 | export const getAllRooms = () => api.get('/api/rooms');
19 | export const getRoom = (roomId) => api.get(`/api/rooms/${roomId}`);
20 |
21 | // Interceptors
22 | api.interceptors.response.use(
23 | (config) => {
24 | return config;
25 | },
26 | async (error) => {
27 | const originalRequest = error.config;
28 | if (
29 | error.response.status === 401 &&
30 | originalRequest &&
31 | !originalRequest._isRetry
32 | ) {
33 | originalRequest.isRetry = true;
34 | try {
35 | await axios.get(
36 | `${process.env.REACT_APP_API_URL}/api/refresh`,
37 | {
38 | withCredentials: true,
39 | }
40 | );
41 |
42 | return api.request(originalRequest);
43 | } catch (err) {
44 | console.log(err.message);
45 | }
46 | }
47 | throw error;
48 | }
49 | );
50 |
51 | export default api;
52 |
--------------------------------------------------------------------------------
/frontend/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './App';
4 | import reportWebVitals from './reportWebVitals';
5 | import { store } from './store';
6 | import { Provider } from 'react-redux';
7 |
8 | ReactDOM.render(
9 |
10 |
11 |
12 |
13 | ,
14 | document.getElementById('root')
15 | );
16 |
17 | reportWebVitals();
18 |
--------------------------------------------------------------------------------
/frontend/src/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/src/pages/Activate/Activate.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import StepName from '../Steps/StepName/StepName';
3 | import StepAvatar from '../Steps/StepAvatar/StepAvatar';
4 |
5 | const steps = {
6 | 1: StepName,
7 | 2: StepAvatar,
8 | };
9 |
10 | const Activate = () => {
11 | const [step, setStep] = useState(1);
12 | const Step = steps[step];
13 |
14 | function onNext() {
15 | setStep(step + 1);
16 | }
17 | return (
18 |
19 |
20 |
21 | );
22 | };
23 |
24 | export default Activate;
25 |
--------------------------------------------------------------------------------
/frontend/src/pages/Authenticate/Authenticate.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import StepPhoneEmail from '../Steps/StepPhoneEmail/StepPhoneEmail';
3 | import StepOtp from '../Steps/StepOtp/StepOtp';
4 |
5 | const steps = {
6 | 1: StepPhoneEmail,
7 | 2: StepOtp,
8 | };
9 |
10 | const Authenticate = () => {
11 | const [step, setStep] = useState(1);
12 | const Step = steps[step];
13 |
14 | function onNext() {
15 | setStep(step + 1);
16 | }
17 |
18 | return ;
19 | };
20 |
21 | export default Authenticate;
22 |
--------------------------------------------------------------------------------
/frontend/src/pages/Home/Home.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styles from './Home.module.css';
3 | import { Link, useHistory } from 'react-router-dom';
4 | import Card from '../../components/shared/Card/Card';
5 | import Button from '../../components/shared/Button/Button';
6 |
7 | const Home = () => {
8 | const signInLinkStyle = {
9 | color: '#0077ff',
10 | fontWeight: 'bold',
11 | textDecoration: 'none',
12 | marginLeft: '10px',
13 | };
14 | const history = useHistory();
15 | function startRegister() {
16 | history.push('/authenticate');
17 | }
18 | return (
19 |
20 |
21 |
22 | We’re working hard to get Codershouse ready for everyone!
23 | While we wrap up the finishing youches, we’re adding people
24 | gradually to make sure nothing breaks
25 |
26 |
27 |
28 |
29 |
30 |
31 | Have an invite text?
32 |
33 |
34 |
35 |
36 | );
37 | };
38 |
39 | export default Home;
40 |
--------------------------------------------------------------------------------
/frontend/src/pages/Home/Home.module.css:
--------------------------------------------------------------------------------
1 | .text {
2 | font-size: 22px;
3 | line-height: 1.6;
4 | color: #c4c5c5;
5 | text-align: center;
6 | margin-bottom: 30px;
7 | }
8 |
9 | .cardWrapper {
10 | display: flex;
11 | align-items: center;
12 | justify-content: center;
13 | margin-top: 6rem;
14 | }
15 |
16 | .signinWrapper {
17 | margin-top: 20px;
18 | }
19 |
20 | .hasInvite {
21 | color: #0077ff;
22 | }
23 |
--------------------------------------------------------------------------------
/frontend/src/pages/Room/Room.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import { useSelector } from 'react-redux';
3 | import { useWebRTC } from '../../hooks/useWebRTC';
4 | import { useParams, useHistory } from 'react-router-dom';
5 | import { getRoom } from '../../http';
6 |
7 | import styles from './Room.module.css';
8 |
9 | const Room = () => {
10 | const user = useSelector((state) => state.auth.user);
11 | const { id: roomId } = useParams();
12 | const [room, setRoom] = useState(null);
13 |
14 | const { clients, provideRef, handleMute } = useWebRTC(roomId, user);
15 |
16 | const history = useHistory();
17 |
18 | const [isMuted, setMuted] = useState(true);
19 |
20 | useEffect(() => {
21 | const fetchRoom = async () => {
22 | const { data } = await getRoom(roomId);
23 | setRoom((prev) => data);
24 | };
25 |
26 | fetchRoom();
27 | }, [roomId]);
28 |
29 | useEffect(() => {
30 | handleMute(isMuted, user.id);
31 | }, [isMuted]);
32 |
33 | const handManualLeave = () => {
34 | history.push('/rooms');
35 | };
36 |
37 | const handleMuteClick = (clientId) => {
38 | if (clientId !== user.id) {
39 | return;
40 | }
41 | setMuted((prev) => !prev);
42 | };
43 |
44 | return (
45 |
46 |
47 |
48 |
49 | All voice rooms
50 |
51 |
52 |
53 |
54 | {room &&
{room.topic} }
55 |
56 |
57 |
58 |
59 |
63 |
64 | Leave quietly
65 |
66 |
67 |
68 |
69 | {clients.map((client) => {
70 | return (
71 |
72 |
73 |
78 |
{
81 | provideRef(instance, client.id);
82 | }}
83 | />
84 |
86 | handleMuteClick(client.id)
87 | }
88 | className={styles.micBtn}
89 | >
90 | {client.muted ? (
91 |
96 | ) : (
97 |
102 | )}
103 |
104 |
105 |
{client.name}
106 |
107 | );
108 | })}
109 |
110 |
111 |
112 | );
113 | };
114 |
115 | export default Room;
116 |
--------------------------------------------------------------------------------
/frontend/src/pages/Room/Room.module.css:
--------------------------------------------------------------------------------
1 | .client {
2 | display: flex;
3 | flex-direction: column;
4 | align-items: center;
5 | }
6 | .userHead {
7 | width: 90px;
8 | height: 90px;
9 | border-radius: 50%;
10 | border: 3px solid #5453e0;
11 | position: relative;
12 | }
13 |
14 | .userHead + h4 {
15 | font-weight: bold;
16 | margin-top: 1rem;
17 | }
18 |
19 | .userAvatar {
20 | width: 100%;
21 | height: 100%;
22 | border-radius: 50%;
23 | }
24 |
25 | .goBack {
26 | display: flex;
27 | align-items: center;
28 | background: none;
29 | outline: none;
30 | margin-top: 2rem;
31 | }
32 |
33 | .goBack span {
34 | margin-left: 1rem;
35 | font-weight: bold;
36 | color: #fff;
37 | font-size: 16px;
38 | position: relative;
39 | }
40 |
41 | .goBack span::after {
42 | content: '';
43 | position: absolute;
44 | bottom: -16px;
45 | left: 0;
46 | width: 60%;
47 | height: 4px;
48 | background: #0077ff;
49 | border-radius: 10px;
50 | }
51 |
52 | .clientsWrap {
53 | background: #1d1d1d;
54 | margin-top: 4rem;
55 | border-top-right-radius: 20px;
56 | border-top-left-radius: 20px;
57 | min-height: calc(100vh - 205px);
58 | padding: 2rem;
59 | }
60 |
61 | .topic {
62 | font-size: 18px;
63 | font-weight: bold;
64 | }
65 |
66 | .clientsList {
67 | margin-top: 2rem;
68 | display: flex;
69 | align-items: center;
70 | flex-wrap: wrap;
71 | gap: 30px;
72 | }
73 |
74 | .actionBtn {
75 | background: #262626;
76 | outline: none;
77 | margin-left: 2rem;
78 | display: flex;
79 | align-items: center;
80 | padding: 0.7rem 1rem;
81 | border-radius: 20px;
82 | color: #fff;
83 | transition: all 0.3s ease-in-out;
84 | }
85 |
86 | .actionBtn:hover {
87 | background: #333333;
88 | }
89 |
90 | .actionBtn span {
91 | font-weight: bold;
92 | margin-left: 1rem;
93 | }
94 |
95 | .header {
96 | display: flex;
97 | align-items: center;
98 | justify-content: space-between;
99 | }
100 |
101 | .actions {
102 | display: flex;
103 | align-items: center;
104 | }
105 |
106 | .micBtn {
107 | background: #212121;
108 | position: absolute;
109 | bottom: 0;
110 | right: 0;
111 | width: 30px;
112 | height: 30px;
113 | box-sizing: content-box;
114 | border-radius: 30px;
115 | padding: 5px;
116 | box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25);
117 | }
118 |
119 | .micImg {
120 | }
121 |
--------------------------------------------------------------------------------
/frontend/src/pages/Rooms/Rooms.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import AddRoomModal from '../../components/AddRoomModal/AddRoomModal';
3 | import RoomCard from '../../components/RoomCard/RoomCard';
4 | import styles from './Rooms.module.css';
5 | import { getAllRooms } from '../../http';
6 |
7 | // const rooms = [
8 | // {
9 | // id: 1,
10 | // topic: 'Which framework best for frontend ?',
11 | // speakers: [
12 | // {
13 | // id: 1,
14 | // name: 'John Doe',
15 | // avatar: '/images/monkey-avatar.png',
16 | // },
17 | // {
18 | // id: 2,
19 | // name: 'Jane Doe',
20 | // avatar: '/images/monkey-avatar.png',
21 | // },
22 | // ],
23 | // totalPeople: 40,
24 | // },
25 | // {
26 | // id: 3,
27 | // topic: 'What’s new in machine learning?',
28 | // speakers: [
29 | // {
30 | // id: 1,
31 | // name: 'John Doe',
32 | // avatar: '/images/monkey-avatar.png',
33 | // },
34 | // {
35 | // id: 2,
36 | // name: 'Jane Doe',
37 | // avatar: '/images/monkey-avatar.png',
38 | // },
39 | // ],
40 | // totalPeople: 40,
41 | // },
42 | // {
43 | // id: 4,
44 | // topic: 'Why people use stack overflow?',
45 | // speakers: [
46 | // {
47 | // id: 1,
48 | // name: 'John Doe',
49 | // avatar: '/images/monkey-avatar.png',
50 | // },
51 | // {
52 | // id: 2,
53 | // name: 'Jane Doe',
54 | // avatar: '/images/monkey-avatar.png',
55 | // },
56 | // ],
57 | // totalPeople: 40,
58 | // },
59 | // {
60 | // id: 5,
61 | // topic: 'Artificial inteligence is the future?',
62 | // speakers: [
63 | // {
64 | // id: 1,
65 | // name: 'John Doe',
66 | // avatar: '/images/monkey-avatar.png',
67 | // },
68 | // {
69 | // id: 2,
70 | // name: 'Jane Doe',
71 | // avatar: '/images/monkey-avatar.png',
72 | // },
73 | // ],
74 | // totalPeople: 40,
75 | // },
76 | // ];
77 |
78 | const Rooms = () => {
79 | const [showModal, setShowModal] = useState(false);
80 | const [rooms, setRooms] = useState([]);
81 |
82 | useEffect(() => {
83 | const fetchRooms = async () => {
84 | const { data } = await getAllRooms();
85 | setRooms(data);
86 | };
87 | fetchRooms();
88 | }, []);
89 | function openModal() {
90 | setShowModal(true);
91 | }
92 | return (
93 | <>
94 |
95 |
96 |
97 |
All voice rooms
98 |
99 |
100 |
101 |
102 |
103 |
104 |
108 |
112 | Start a room
113 |
114 |
115 |
116 |
117 |
118 | {rooms.map((room) => (
119 |
120 | ))}
121 |
122 |
123 | {showModal && setShowModal(false)} />}
124 | >
125 | );
126 | };
127 |
128 | export default Rooms;
129 |
--------------------------------------------------------------------------------
/frontend/src/pages/Rooms/Rooms.module.css:
--------------------------------------------------------------------------------
1 | .roomsHeader {
2 | display: flex;
3 | align-items: center;
4 | justify-content: space-between;
5 | margin: 20px 0;
6 | }
7 |
8 | .left {
9 | display: flex;
10 | align-items: center;
11 | }
12 |
13 | .heading {
14 | font-size: 20px;
15 | font-weight: bold;
16 | }
17 |
18 | .searchBox {
19 | background: #262626;
20 | margin-left: 20px;
21 | display: flex;
22 | align-items: center;
23 | padding: 0 10px;
24 | min-width: 300px;
25 | border-radius: 20px;
26 | }
27 |
28 | .searchInput {
29 | background: transparent;
30 | border: none;
31 | outline: none;
32 | padding: 10px;
33 | color: #fff;
34 | width: 100%;
35 | }
36 |
37 | .heading {
38 | position: relative;
39 | }
40 |
41 | .heading::after {
42 | content: '';
43 | position: absolute;
44 | bottom: -10px;
45 | left: 0;
46 | width: 60%;
47 | height: 4px;
48 | background: #0077ff;
49 | }
50 |
51 | .startRoomButton {
52 | display: flex;
53 | align-items: center;
54 | background: #20bd5f;
55 | padding: 5px 20px;
56 | border-radius: 20px;
57 | color: #fff;
58 | transition: all 0.3s ease-in-out;
59 | }
60 |
61 | .startRoomButton span {
62 | font-size: 16px;
63 | margin-left: 10px;
64 | }
65 |
66 | .startRoomButton:hover {
67 | background: #0f6632;
68 | }
69 |
70 | .roomList {
71 | display: grid;
72 | grid-template-columns: repeat(4, 1fr);
73 | gap: 20px;
74 | margin-top: 60px;
75 | }
76 |
--------------------------------------------------------------------------------
/frontend/src/pages/Steps/StepAvatar/StepAvatar.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import Card from '../../../components/shared/Card/Card';
3 | import Button from '../../../components/shared/Button/Button';
4 | import styles from './StepAvatar.module.css';
5 | import { useSelector, useDispatch } from 'react-redux';
6 | import { setAvatar } from '../../../store/activateSlice';
7 | import { activate } from '../../../http';
8 | import { setAuth } from '../../../store/authSlice';
9 | import Loader from '../../../components/shared/Loader/Loader';
10 |
11 | const StepAvatar = ({ onNext }) => {
12 | const dispatch = useDispatch();
13 | const { name, avatar } = useSelector((state) => state.activate);
14 | const [image, setImage] = useState('/images/monkey-avatar.png');
15 | const [loading, setLoading] = useState(false);
16 | const [unMounted, setUnMounted] = useState(false);
17 |
18 | function captureImage(e) {
19 | const file = e.target.files[0];
20 | const reader = new FileReader();
21 | reader.readAsDataURL(file);
22 | reader.onloadend = function () {
23 | setImage(reader.result);
24 | dispatch(setAvatar(reader.result));
25 | };
26 | }
27 | async function submit() {
28 | if (!name || !avatar) return;
29 | setLoading(true);
30 | try {
31 | const { data } = await activate({ name, avatar });
32 | if (data.auth) {
33 | if (!unMounted) {
34 | dispatch(setAuth(data));
35 | }
36 | }
37 | } catch (err) {
38 | console.log(err);
39 | } finally {
40 | setLoading(false);
41 | }
42 | }
43 |
44 | useEffect(() => {
45 | return () => {
46 | setUnMounted(true);
47 | };
48 | }, []);
49 |
50 | if (loading) return ;
51 | return (
52 | <>
53 |
54 | How’s this photo?
55 |
56 |
61 |
62 |
63 |
69 |
70 | Choose a different photo
71 |
72 |
73 |
74 |
75 |
76 |
77 | >
78 | );
79 | };
80 |
81 | export default StepAvatar;
82 |
--------------------------------------------------------------------------------
/frontend/src/pages/Steps/StepAvatar/StepAvatar.module.css:
--------------------------------------------------------------------------------
1 | .subHeading {
2 | color: #c4c5c5;
3 | text-align: center;
4 | margin-bottom: 20px;
5 | }
6 |
7 | .avatarWrapper {
8 | width: 110px;
9 | height: 110px;
10 | border: 6px solid #0077ff;
11 | border-radius: 50%;
12 | display: flex;
13 | align-items: center;
14 | justify-content: center;
15 | overflow: hidden;
16 | }
17 |
18 | .avatarImage {
19 | height: 90%;
20 | width: 90%;
21 | object-fit: cover;
22 | }
23 |
24 | .avatarInput {
25 | display: none;
26 | }
27 |
28 | .avatarLabel {
29 | color: #0077ff;
30 | margin: 30px 0;
31 | display: inline-block;
32 | cursor: pointer;
33 | }
34 |
--------------------------------------------------------------------------------
/frontend/src/pages/Steps/StepName/StepName.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import Card from '../../../components/shared/Card/Card';
3 | import Button from '../../../components/shared/Button/Button';
4 | import TextInput from '../../../components/shared/TextInput/TextInput';
5 | import { useDispatch, useSelector } from 'react-redux';
6 | import { setName } from '../../../store/activateSlice';
7 | import styles from './StepName.module.css';
8 | const StepName = ({ onNext }) => {
9 | const { name } = useSelector((state) => state.activate);
10 | const dispatch = useDispatch();
11 | const [fullname, setFullname] = useState(name);
12 |
13 | function nextStep() {
14 | if (!fullname) {
15 | return;
16 | }
17 | dispatch(setName(fullname));
18 | onNext();
19 | }
20 | return (
21 | <>
22 |
23 | setFullname(e.target.value)}
26 | />
27 |
28 | People use real names at codershouse :) !
29 |
30 |
31 |
32 |
33 |
34 | >
35 | );
36 | };
37 |
38 | export default StepName;
39 |
--------------------------------------------------------------------------------
/frontend/src/pages/Steps/StepName/StepName.module.css:
--------------------------------------------------------------------------------
1 | .paragraph {
2 | width: 70%;
3 | text-align: center;
4 | margin: 20px auto;
5 | }
6 |
--------------------------------------------------------------------------------
/frontend/src/pages/Steps/StepOtp/StepOtp.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import Card from '../../../components/shared/Card/Card';
3 | import TextInput from '../../../components/shared/TextInput/TextInput';
4 | import Button from '../../../components/shared/Button/Button';
5 | import styles from './StepOtp.module.css';
6 | import { verifyOtp } from '../../../http';
7 | import { useSelector } from 'react-redux';
8 | import { setAuth } from '../../../store/authSlice';
9 | import { useDispatch } from 'react-redux';
10 |
11 | const StepOtp = () => {
12 | const [otp, setOtp] = useState('');
13 | const dispatch = useDispatch();
14 | const { phone, hash } = useSelector((state) => state.auth.otp);
15 | async function submit() {
16 | if (!otp || !phone || !hash) return;
17 | try {
18 | const { data } = await verifyOtp({ otp, phone, hash });
19 | dispatch(setAuth(data));
20 | } catch (err) {
21 | console.log(err);
22 | }
23 | }
24 | return (
25 | <>
26 |
27 |
31 | setOtp(e.target.value)}
34 | />
35 |
36 |
37 |
38 |
39 | By entering your number, you’re agreeing to our Terms of
40 | Service and Privacy Policy. Thanks!
41 |
42 |
43 |
44 | >
45 | );
46 | };
47 |
48 | export default StepOtp;
49 |
--------------------------------------------------------------------------------
/frontend/src/pages/Steps/StepOtp/StepOtp.module.css:
--------------------------------------------------------------------------------
1 | .cardWrapper {
2 | composes: cardWrapper from '../../../App.module.css';
3 | }
4 |
5 | .bottomParagraph {
6 | composes: bottomParagraph from '../StepPhoneEmail/StepPhoneEmail.module.css';
7 | }
8 |
9 | .actionButtonWrap {
10 | composes: actionButtonWrap from '../StepPhoneEmail/StepPhoneEmail.module.css';
11 | }
12 |
--------------------------------------------------------------------------------
/frontend/src/pages/Steps/StepPhoneEmail/Email/Email.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import Card from '../../../../components/shared/Card/Card';
3 | import Button from '../../../../components/shared/Button/Button';
4 | import TextInput from '../../../../components/shared/TextInput/TextInput';
5 | import styles from '../StepPhoneEmail.module.css';
6 |
7 | const Email = ({ onNext }) => {
8 | const [email, setEmail] = useState('');
9 | return (
10 |
11 | setEmail(e.target.value)}
14 | />
15 |
16 |
17 |
18 |
19 |
20 | By entering your number, you’re agreeing to our Terms of
21 | Service and Privacy Policy. Thanks!
22 |
23 |
24 |
25 | );
26 | };
27 |
28 | export default Email;
29 |
--------------------------------------------------------------------------------
/frontend/src/pages/Steps/StepPhoneEmail/Phone/Phone.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import Card from '../../../../components/shared/Card/Card';
3 | import Button from '../../../../components/shared/Button/Button';
4 | import TextInput from '../../../../components/shared/TextInput/TextInput';
5 | import styles from '../StepPhoneEmail.module.css';
6 | import { sendOtp } from '../../../../http/index';
7 | import { useDispatch } from 'react-redux';
8 | import { setOtp } from '../../../../store/authSlice';
9 |
10 | const Phone = ({ onNext }) => {
11 | const [phoneNumber, setPhoneNumber] = useState('');
12 | const dispatch = useDispatch();
13 |
14 | async function submit() {
15 | if (!phoneNumber) return;
16 | const { data } = await sendOtp({ phone: phoneNumber });
17 | console.log(data);
18 | dispatch(setOtp({ phone: data.phone, hash: data.hash }));
19 | onNext();
20 | }
21 |
22 | return (
23 |
24 | setPhoneNumber(e.target.value)}
27 | />
28 |
29 |
30 |
31 |
32 |
33 | By entering your number, you’re agreeing to our Terms of
34 | Service and Privacy Policy. Thanks!
35 |
36 |
37 |
38 | );
39 | };
40 |
41 | export default Phone;
42 |
--------------------------------------------------------------------------------
/frontend/src/pages/Steps/StepPhoneEmail/StepPhoneEmail.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import Phone from './Phone/Phone';
3 | import Email from './Email/Email';
4 | import styles from './StepPhoneEmail.module.css';
5 |
6 | const phoneEmailMap = {
7 | phone: Phone,
8 | email: Email,
9 | };
10 |
11 | const StepPhoneEmail = ({ onNext }) => {
12 | const [type, setType] = useState('phone');
13 | const Component = phoneEmailMap[type];
14 |
15 | return (
16 | <>
17 |
18 |
19 |
20 |
setType('phone')}
25 | >
26 |
27 |
28 |
setType('email')}
33 | >
34 |
35 |
36 |
37 |
38 |
39 |
40 | >
41 | );
42 | };
43 |
44 | export default StepPhoneEmail;
45 |
--------------------------------------------------------------------------------
/frontend/src/pages/Steps/StepPhoneEmail/StepPhoneEmail.module.css:
--------------------------------------------------------------------------------
1 | .cardWrapper {
2 | display: flex;
3 | align-items: center;
4 | justify-content: center;
5 | margin-top: 6rem;
6 | }
7 | .buttonWrap {
8 | margin-bottom: 20px;
9 | display: flex;
10 | align-items: center;
11 | justify-content: flex-end;
12 | }
13 |
14 | .tabButton {
15 | width: 60px;
16 | height: 60px;
17 | background: #262626;
18 | border: none;
19 | outline: none;
20 | border-radius: 10px;
21 | cursor: pointer;
22 | }
23 |
24 | .tabButton:last-child {
25 | margin-left: 20px;
26 | }
27 |
28 | .active {
29 | background: #0077ff;
30 | }
31 |
32 | .actionButtonWrap {
33 | margin-top: 40px;
34 | }
35 |
36 | .bottomParagraph {
37 | color: #c4c5c5;
38 | width: 70%;
39 | margin: 0 auto;
40 | margin-top: 20px;
41 | }
42 |
--------------------------------------------------------------------------------
/frontend/src/pages/Steps/StepUsername/StepUsername.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const StepUsername = ({ onNext }) => {
4 | return (
5 | <>
6 | username component
7 | Next
8 | >
9 | );
10 | };
11 |
12 | export default StepUsername;
13 |
--------------------------------------------------------------------------------
/frontend/src/reportWebVitals.js:
--------------------------------------------------------------------------------
1 | const reportWebVitals = onPerfEntry => {
2 | if (onPerfEntry && onPerfEntry instanceof Function) {
3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
4 | getCLS(onPerfEntry);
5 | getFID(onPerfEntry);
6 | getFCP(onPerfEntry);
7 | getLCP(onPerfEntry);
8 | getTTFB(onPerfEntry);
9 | });
10 | }
11 | };
12 |
13 | export default reportWebVitals;
14 |
--------------------------------------------------------------------------------
/frontend/src/setupTests.js:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom';
6 |
--------------------------------------------------------------------------------
/frontend/src/socket/index.js:
--------------------------------------------------------------------------------
1 | import { io } from 'socket.io-client';
2 |
3 | const socketInit = () => {
4 | const options = {
5 | 'force new connection': true,
6 | reconnectionAttempts: 'Infinity',
7 | timeout: 10000,
8 | transports: ['websocket'],
9 | };
10 | return io(process.env.REACT_APP_SOCKET_SERVER_URL, options);
11 | };
12 |
13 | export default socketInit;
14 |
--------------------------------------------------------------------------------
/frontend/src/store/activateSlice.js:
--------------------------------------------------------------------------------
1 | import { createSlice } from '@reduxjs/toolkit';
2 |
3 | const initialState = {
4 | name: '',
5 | avatar: '',
6 | };
7 |
8 | export const activateSlice = createSlice({
9 | name: 'activate',
10 | initialState,
11 | reducers: {
12 | setName: (state, action) => {
13 | state.name = action.payload;
14 | },
15 | setAvatar: (state, action) => {
16 | state.avatar = action.payload;
17 | },
18 | },
19 | });
20 |
21 | export const { setName, setAvatar } = activateSlice.actions;
22 |
23 | export default activateSlice.reducer;
24 |
--------------------------------------------------------------------------------
/frontend/src/store/authSlice.js:
--------------------------------------------------------------------------------
1 | import { createSlice } from '@reduxjs/toolkit';
2 |
3 | const initialState = {
4 | isAuth: false,
5 | user: null,
6 | otp: {
7 | phone: '',
8 | hash: '',
9 | },
10 | };
11 |
12 | export const authSlice = createSlice({
13 | name: 'auth',
14 | initialState,
15 | reducers: {
16 | setAuth: (state, action) => {
17 | const { user } = action.payload;
18 | state.user = user;
19 | if (user === null) {
20 | state.isAuth = false;
21 | } else {
22 | state.isAuth = true;
23 | }
24 | },
25 | setOtp: (state, action) => {
26 | const { phone, hash } = action.payload;
27 | state.otp.phone = phone;
28 | state.otp.hash = hash;
29 | },
30 | },
31 | });
32 |
33 | export const { setAuth, setOtp } = authSlice.actions;
34 |
35 | export default authSlice.reducer;
36 |
--------------------------------------------------------------------------------
/frontend/src/store/index.js:
--------------------------------------------------------------------------------
1 | import { configureStore } from '@reduxjs/toolkit';
2 | import auth from './authSlice';
3 | import activate from './activateSlice';
4 |
5 | export const store = configureStore({
6 | reducer: {
7 | auth,
8 | activate,
9 | },
10 | });
11 |
--------------------------------------------------------------------------------
/frontend/webpack.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | devServer: {
3 | disableHostCheck: true,
4 | historyApiFallback: true,
5 | },
6 | };
7 |
--------------------------------------------------------------------------------