├── .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 | 10 | 11 | 13 | 14 | 15 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 1629308908173 30 | 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 | 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 | 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 | globe 46 | Open 47 |
48 |
setRoomType('social')} 50 | className={`${styles.typeBox} ${ 51 | roomType === 'social' ? styles.active : '' 52 | }`} 53 | > 54 | social 55 | Social 56 |
57 |
setRoomType('private')} 59 | className={`${styles.typeBox} ${ 60 | roomType === 'private' ? styles.active : '' 61 | }`} 62 | > 63 | lock 64 | Private 65 |
66 |
67 |
68 |
69 |

Start a room, open to everyone

70 | 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 | speaker-avatar 27 | ))} 28 |
29 |
30 | {room.speakers.map((speaker) => ( 31 |
32 | {speaker.name} 33 | chat-bubble 37 |
38 | ))} 39 |
40 |
41 |
42 | {room.totalPeople} 43 | user-icon 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 | 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 && logo} 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 | 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 |
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 | 51 |
52 |
53 |
54 | {room &&

{room.topic}

} 55 |
56 | 59 | 66 |
67 |
68 |
69 | {clients.map((client) => { 70 | return ( 71 |
72 |
73 | 78 |
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 | search 100 | 101 |
102 |
103 |
104 | 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 | avatar 61 |
62 |
63 | 69 | 72 |
73 |
74 |
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 |
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 |
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 |
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 |
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 | 28 | 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 | 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 | --------------------------------------------------------------------------------