Commit a2181d2b authored by Цой Данил's avatar Цой Данил 💬

Rewrote back on js

parent 41373fad
APP_PORT=8000
PG_HOST=localhost
MONGO_CLIENT_URL=mongodb://localhost/TsoyDanilChatDB
ENV_SALT=10
SECRET_KEY=key
\ No newline at end of file
/node_modules
\ No newline at end of file
node_modules
.idea
.vscode
\ No newline at end of file
import * as path from "path";
import { dirname } from 'path';
import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const rootPath = __dirname;
export default {
rootPath,
db: {
name: 'chat',
url: 'mongodb://localhost'
}
};
\ No newline at end of file
import mongoose from "mongoose";
const Scheme = mongoose.Schema;
const MessageScheme = new Scheme({
user: {
type: Scheme.Types.ObjectId,
ref: 'User',
required: true
},
message: {
type: String,
required: true
},
datetime: {
type: Date,
default: Date.now()
}
});
const Message = mongoose.model('Message', MessageScheme);
export default Message;
\ No newline at end of file
import mongoose from "mongoose";
import bcrypt from 'bcrypt';
import { nanoid } from "nanoid";
const SALT_WORK_FACTOR = 10;
const Scheme = mongoose.Schema;
const UserScheme = new Scheme({
username: {
type: String,
required: true,
unique: true,
validate: {
validator: async value => {
const user = await User.findOne({username: value});
if(user) return false;
},
message: 'This user is already registered'
}
},
password: {
type: String,
required: true
},
token: {
type: String,
required: true
}
});
UserScheme.pre('save', async function(next) {
if(!this.isModified('password')) return next();
const salt = await bcrypt.genSalt(SALT_WORK_FACTOR);
const hash = await bcrypt.hash(this.password, salt);
this.password = hash;
next();
});
UserScheme.set('toJSON', {
transform: (doc, ret, options) => {
delete ret.password;
return ret;
}
});
UserScheme.methods.checkPassword = function(password) {
return bcrypt.compare(password, this.password);
};
UserScheme.methods.generateToken = function() {
this.token = nanoid();
};
const User = mongoose.model('User', UserScheme);
export default User;
\ No newline at end of file
This source diff could not be displayed because it is too large. You can view the blob instead.
{
"name": "homework",
"name": "backend",
"version": "1.0.0",
"description": "",
"main": "index.js",
"type": "module",
"main": "server.js",
"scripts": {
"dev": "ts-node-dev --respawn --trace-warnings --transpile-only src/index.ts",
"seed": "ts-node-dev --trace-warnings --transpile-only src/fixtures.ts",
"test": "echo \"Error: no test specified\" && exit 1"
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node server.js",
"dev": "nodemon server.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"nodemon": "^2.0.20"
},
"dependencies": {
"@types/cors": "^2.8.13",
"@types/express-ws": "^3.0.1",
"@types/jsonwebtoken": "^9.0.1",
"@types/shortid": "^0.0.29",
"bcrypt": "^5.1.0",
"cors": "^2.8.5",
"dotenv": "^16.0.3",
"express": "^4.18.2",
"express-ws": "^5.0.2",
"jsonwebtoken": "^9.0.0",
"mongodb": "^5.1.0",
"mongoose": "^7.0.3",
"multer": "^1.4.5-lts.1",
"pg": "^8.10.0",
"pg-hstore": "^2.3.4",
"sequelize": "^6.29.3",
"sequelize-typescript": "^2.1.5",
"shortid": "^2.2.16",
"uuid": "^9.0.0"
},
"devDependencies": {
"@types/bcrypt": "^5.0.0",
"@types/dotenv": "^8.2.0",
"@types/express": "^4.17.17",
"@types/mongodb": "^4.0.7",
"@types/mongoose": "^5.11.97",
"@types/multer": "^1.4.7",
"@types/uuid": "^9.0.1",
"@types/validator": "^13.7.14",
"@types/ws": "^8.5.4",
"ts-node-dev": "^2.0.0"
"mongoose": "^6.6.5",
"mongoose-sequence": "^5.3.1",
"nanoid": "^3.3.4"
}
}
import express, { Router } from "express";
import expressWs from "express-ws";
import shortid from "shortid";
import { User } from "../models/User";
import { Message } from "../models/Message";
import express from 'express';
import expressWs from 'express-ws';
import { nanoid } from 'nanoid';
import User from '../models/User.js';
import Message from '../models/Message.js';
const router = express.Router();
expressWs(router);
const messagesRouter: any = express.Router()
expressWs(messagesRouter)
const activeConnections = {};
const users = [];
const activeConnections: any= {};
const users: any[] = [];
messagesRouter.ws('/', (ws: any, req: any) => {
const id = shortid.generate()
router.ws('/', (ws, req) => {
const id = nanoid();
console.log(`Client connected id=${id}`);
activeConnections[id] = ws
activeConnections[id] = ws;
ws.on('close', async (msg: any) => {
ws.on('close', async (msg) => {
console.log(`Client disconnected id = ${id}`);
delete activeConnections[id];
})
});
ws.on('message', async (msg: any) => {
ws.on('message', async (msg) => {
const decodedMessage = JSON.parse(msg);
switch (decodedMessage.type) {
case 'OPEN_CHAT':
......@@ -36,12 +35,12 @@ messagesRouter.ws('/', (ws: any, req: any) => {
});
break;
case 'CLOSED_CHAT':
const offlineUser = await User.find({_id: decodedMessage.user._id});
const oflineUser = await User.find({_id: decodedMessage.user._id});
Object.keys(activeConnections).forEach(connId => {
const conn = activeConnections[connId];
conn.send(JSON.stringify({
type: "CLOSED_CHAT_USER",
id: offlineUser[0]._id
id: oflineUser[0]._id
}));
});
break;
......@@ -49,7 +48,6 @@ messagesRouter.ws('/', (ws: any, req: any) => {
const message = new Message({user: decodedMessage.user.id, message: decodedMessage.user.text});
await message.save();
const user = await User.findOne({_id: decodedMessage.user.id});
// @ts-ignore
const newMessage = {...message._doc, user: user.username};
Object.keys(activeConnections).forEach(connId => {
const conn = activeConnections[connId];
......@@ -72,5 +70,9 @@ messagesRouter.ws('/', (ws: any, req: any) => {
default:
break;
}
})
})
\ No newline at end of file
});
});
export default router;
\ No newline at end of file
import express from 'express';
import User from '../models/User.js';
const router = express.Router();
router.post('/', async (req, res) => {
try{
const {username,password} = req.body;
const user = new User({
username,
password
});
user.generateToken();
await user.save();
res.send(user);
}catch(error){
return res.status(400).send(error);
};
});
router.post('/sessions', async (req, res) => {
const user = await User.findOne({username: req.body.username});
if(!user) return res.status(400).send({error: 'User not found'});
const isMatch = await user.checkPassword(req.body.password);
if(!isMatch) return res.status(400).send({error: 'Password is wrong!'});
return res.send(user);
});
router.delete('/sessions', async (req, res) => {
const token = req.get('Authorization');
const successMessage = {message: 'Success'};
if(!token) return res.send(successMessage);
const user = await User.findOne({token});
if(!user) return res.send(successMessage);
user.token = nanoid();
await user.save({ validateBeforeSave: false });
return res.send(successMessage);
});
export default router;
\ No newline at end of file
import mongoose from 'mongoose';
import express from "express";
import cors from 'cors';
import config from './config.js';
import expressWs from "express-ws";
import Users from './routes/Users.js';
import ChatWs from './routes/ChatWs.js';
const app = express();
const PORT = 8000;
expressWs(app);
app.use(cors());
app.use(express.json());
app.use('/users', Users);
app.use('/chat', ChatWs);
const run = async() => {
mongoose.connect(`${config.db.url}/${config.db.name}`, {useNewUrlParser: true});
app.listen(PORT, () => {
console.log(`Server started at http://localhost:${PORT}/`);
});
process.on("exit", () => {
mongoose.disconnect();
});
};
run().catch(console.error);
\ No newline at end of file
import { Request, Response } from 'express';
export const healthCheckController = async (req: Request, res: Response) => {
res.status(200).send('Server is ok')
}
\ No newline at end of file
// import express, { Request, Response, Router } from "express"
// import { MessagesService, messagesService } from "../services/messagesService"
// import { auth } from "../middlewares/auth"
// import IResponse from "../interfaces/IResponse"
// import IMessage from "../interfaces/IMessage"
// import { EStatuses } from "../enum/EStatuses"
// import IModifiedRequest from "../interfaces/IModifiedRequest"
// import IMessageDto from "../interfaces/IMessageDto"
// export class MessagesController {
// private service: MessagesService
// private router: Router
// constructor(){
// this.service = messagesService
// this.router = express.Router()
// this.router.get('/', auth, this.getMessages)
// this.router.post('/', auth, this.addMessage)
// }
// public getRouter = () => {
// return this.router
// }
// private getMessages = async (req: Request, res: Response): Promise<void> => {
// const response: IResponse<IMessage[] | null> = await this.service.getMessages()
// if (response.status === EStatuses.SUCCESS){
// res.status(200).send(response)
// } else{
// res.status(418).send(response)
// }
// }
// private addMessage = async (req: Request, res: Response): Promise<void> => {
// const modifiedReq = req as IModifiedRequest
// if (typeof modifiedReq.verifiedData === 'object'){
// const {message} = req.body
// const {_id} = modifiedReq.verifiedData
// const messageDto: IMessageDto = {
// user: _id,
// message: message
// }
// const response: IResponse<IMessage | null> = await this.service.addMessage(messageDto)
// if (response.status === EStatuses.SUCCESS){
// res.status(200).send(response)
// } else{
// res.status(418).send(response)
// }
// } else{
// res.status(418).send('error in request. Some invalid field appeared')
// }
// }
// }
// export const messagesController = new MessagesController()
\ No newline at end of file
import { Request, Response } from 'express';
import IResponse from '../interfaces/IResponse';
import { EStatuses } from '../enum/EStatuses';
import IUserGetDto from '../interfaces/IUserGetDto';
import { User } from '../models/User';
import { generateJWT } from '../helpers/generateJWT';
import IModifiedRequest from '../interfaces/IModifiedRequest';
import IUserCreateDto from '../interfaces/IUserCreateDto';
export const createUser = async(req: Request, res: Response) => {
try{
const userDto: IUserGetDto = req.body
const user = new User(userDto)
await user.save()
const responseData: IUserGetDto = {
_id: user._id,
username: user.username,
token: generateJWT({_id: user._id, username: user.username})
}
const response: IResponse<IUserGetDto> = {
status: EStatuses.SUCCESS,
result: responseData,
message: 'User added'
}
res.status(200).send(response)
} catch(err: unknown){
const error = err as Error
const response: IResponse<null> = {
status: EStatuses.FAILURE,
result: null,
message: error.message
}
res.status(418).send(response)
}
}
export const loginUser = async(req: Request, res: Response): Promise<void> => {
try{
const userDto: IUserCreateDto = req.body
const user = await User.findOne({username: userDto.username})
if (!user) throw new Error('User not found')
const isMatch: boolean = await user.checkPassword(userDto.password)
if (!isMatch) throw new Error('Wrong password')
await user.save()
const responseData = {
_id: user._id,
username: user.username,
token: generateJWT({_id: user._id, username: user.username})
}
const response: IResponse<IUserGetDto> = {
status: EStatuses.SUCCESS,
result: responseData,
message: 'Access granted'
}
res.status(200).send(response)
} catch(err: unknown){
const error = err as Error
const response: IResponse<null> = {
status: EStatuses.FAILURE,
result: null,
message: error.message
}
res.status(418).send(response)
}
}
export const checkToken = async (req: Request, res: Response): Promise<void> => {
const modifiedReq = req as IModifiedRequest
const response: IResponse<IUserGetDto | null> = {
status: EStatuses.SUCCESS,
result: modifiedReq.verifiedData as IUserGetDto,
message: 'Token is ok'
}
res.status(200).send(response)
}
export const enum EStatuses {
SUCCESS = 'Success',
FAILURE = 'Failure'
}
\ No newline at end of file
import dotenv from 'dotenv'
import mongoose from 'mongoose'
import { User } from './models/User'
import { Message } from './models/Message'
dotenv.config()
mongoose.connect(process.env.MONGO_CLIENT_URL || 'mongodb://localhost/TsoyDanilChatDB')
const db = mongoose.connection
export default db.once('open', async () => {
try{
await db.dropCollection('messages')
await db.dropCollection('users')
} catch(err: unknown){
console.log(`Skipped db drop because of: ${err}`);
}
const [userOne, userTwo, userThree, userFour, userFive] = await User.create(
{
username: 'user1',
password: 'pass'
},
{
username: 'user2',
password: 'pass'
},
{
username: 'user3',
password: 'pass'
},
{
username: 'user4',
password: 'pass'
},
{
username: 'user5',
password: 'pass'
}
)
await Message.create(
{
user: userOne._id,
message: 'mess1'
},
{
user: userOne._id,
message: 'mess2'
},
{
user: userOne._id,
message: 'mess3'
},
{
user: userOne._id,
message: 'mess4'
},
{
user: userOne._id,
message: 'mess5'
},
{
user: userOne._id,
message: 'mess6'
},
{
user: userOne._id,
message: 'mess7'
},
{
user: userOne._id,
message: 'mess8'
},
{
user: userOne._id,
message: 'mess9'
},
{
user: userOne._id,
message: 'mess10'
},
{
user: userOne._id,
message: 'mess11'
},
{
user: userOne._id,
message: 'mess12'
},
{
user: userOne._id,
message: 'mess13'
},
{
user: userOne._id,
message: 'mess14'
},
{
user: userOne._id,
message: 'mess15'
},
{
user: userOne._id,
message: 'mess16'
},
{
user: userOne._id,
message: 'mess17'
},
{
user: userOne._id,
message: 'mess19'
},
{
user: userOne._id,
message: 'mess20'
},
{
user: userOne._id,
message: 'mess21'
},
{
user: userOne._id,
message: 'mess22'
},
{
user: userOne._id,
message: 'mess23'
},
{
user: userOne._id,
message: 'mess24'
},
{
user: userOne._id,
message: 'mess25'
},
{
user: userOne._id,
message: 'mess26'
},
{
user: userOne._id,
message: 'mess27'
},
{
user: userOne._id,
message: 'mess28'
},
{
user: userOne._id,
message: 'mess29'
},
{
user: userOne._id,
message: 'mess30'
},
{
user: userOne._id,
message: 'mess31'
},
{
user: userOne._id,
message: 'mess32'
},
{
user: userOne._id,
message: 'mess33'
},
{
user: userOne._id,
message: 'mess34'
},
{
user: userOne._id,
message: 'mess35'
}
)
db.close()
})
\ No newline at end of file
import jwt from 'jsonwebtoken'
import { Types } from 'mongoose'
export const generateJWT = (payload: {[key: string]: number | string | boolean | Types.ObjectId}) => {
return jwt.sign(payload, process.env.SECRET_KEY || '', {expiresIn: '24h'})
}
import path from 'path'
export const config = {
port: 8000,
filePath: path.join(__dirname, '../public/uploads')
}
import mongoose from 'mongoose';
import express, {Express} from "express";
import cors from 'cors';
import expressWs from "express-ws";
import usersRouter from './routes/usersRoute';
import dotenv from 'dotenv'
import healthCheckRouter from './routes/healthCheckRouter';
dotenv.config()
const app: Express = express()
app.use(cors())
app.use(express.json())
app.use('/health-check', healthCheckRouter)
app.use('/users', usersRouter)
const run = async(): Promise<void> => {
try{
mongoose.connect(process.env.MONGO_CLIENT_URL || 'mongodb://localhost/TsoyDanilChatDB')
app.listen(process.env.APP_PORT, () => {
console.log(`Server started at http://localhost:${process.env.APP_PORT}`);
})
process.on('exit', () => {
mongoose.disconnect()
})
} catch(err: unknown){
console.log(err);
}
}
run()
\ No newline at end of file
import { Types } from "mongoose";
import IUser from "./IUser";
export default interface IMessage{
_id: Types.ObjectId
user: IUser
message: string
}
\ No newline at end of file
import IMessage from "./IMessage";
export default interface IMessageDto{
user: IMessage['_id']
message: IMessage['message']
}
\ No newline at end of file
import { Request } from "express";
import { JwtPayload } from "jsonwebtoken";
export default interface IModifiedRequest extends Request {
verifiedData: string | JwtPayload
}
\ No newline at end of file
import { EStatuses } from "../enum/EStatuses";
export default interface IResponse<T = null> {
status: EStatuses
result: T
message: string
}
\ No newline at end of file
import {Document, Types} from "mongoose"
export default interface IUser extends Document {
_id: Types.ObjectId
username: string
password: string
checkPassword: (pass: string) => boolean
}
\ No newline at end of file
import IUser from "./IUser";
export default interface IUserCreateDto {
username: IUser['username']
password: IUser['password']
}
import { Types } from "mongoose";
import IUSer from "./IUser";
export default interface IUserGetDto {
_id: Types.ObjectId
username: IUSer['username']
token: string
}
\ No newline at end of file
import { NextFunction, Request, Response } from "express";
import { EStatuses } from "../enum/EStatuses";
import IResponse from "../interfaces/IResponse";
import jwt from 'jsonwebtoken'
import IModifiedRequest from "../interfaces/IModifiedRequest";
export const auth = (expressReq: Request, res: Response, next: NextFunction) => {
const req = expressReq as IModifiedRequest
if (req.method === 'OPTIONS') {
next()
}
try{
const token = req.header('Authorization')?.replace('Bearer ', '');
if (!token){
throw new Error('Token not provided')
}
const verifiedData = jwt.verify(token, process.env.SECRET_KEY as string);
req.verifiedData = verifiedData
next()
} catch(err: unknown){
const error = err as Error
const response: IResponse<null> = {
status: EStatuses.FAILURE,
result: null,
message: error.message
}
res.status(418).send(response)
}
}
\ No newline at end of file
import mongoose, { Schema } from "mongoose";
import IMessage from "../interfaces/IMessage";
const MessageSchema: Schema = new Schema<IMessage>({
user: {
type: Schema.Types.ObjectId,
ref: 'User',
minlength: 1,
trim: true,
required: [true, 'User id has to be provided']
},
message: {
type: String,
trim: true,
minlength: 1,
required: [true, 'Username should exist']
}
}, {versionKey: false})
export const Message = mongoose.model<IMessage>('Message', MessageSchema)
\ No newline at end of file
import mongoose, { Schema } from "mongoose";
import bcrypt from "bcrypt";
import IUser from "../interfaces/IUser";
const UserSchema: Schema = new Schema<IUser>({
username: {
type: String,
unique: true,
trim: true,
minlength: 1,
required: [true, 'Username should exist']
},
password: {
type: String,
trim: true,
minlength: 1,
required: [true, 'Password should exist']
}
},{versionKey: false})
UserSchema.pre('save', async function(next){
if (!this.isModified('password')) return next()
const salt = await bcrypt.genSalt()
const hash = await bcrypt.hash(this.password, salt)
this.password = hash
next()
})
UserSchema.set('toJSON', {transform(doc, ret, options) {
delete ret.password
return ret
}})
UserSchema.methods.checkPassword = async function(password: string) {
return await bcrypt.compare(password, this.password);
}
export const User = mongoose.model<IUser>('User', UserSchema)
\ No newline at end of file
import dotenv from 'dotenv'
import mongoose, { Mongoose } from 'mongoose'
import IResponse from '../interfaces/IResponse'
import { EStatuses } from '../enum/EStatuses'
import IUserGetDto from '../interfaces/IUserGetDto'
import { generateJWT } from '../helpers/generateJWT'
import IUserCreateDto from '../interfaces/IUserCreateDto'
import { User } from '../models/User'
import IMessage from '../interfaces/IMessage'
import { Message } from '../models/Message'
import IMessageDto from '../interfaces/IMessageDto'
dotenv.config()
export class MongooseDB {
private client: Mongoose | null = null
public close = async() => {
if (!this.client) return
await this.client.disconnect();
}
public init = async (): Promise<void> => {
try {
this.client = await mongoose.connect(process.env.MONGO_CLIENT_URL || '')
console.log('Server connected to MongoDB');
} catch (err) {
const error = err as Error;
console.error('Connected error MongooseDB:', error);
}
}
public createUser = async (userDto: IUserCreateDto): Promise<IResponse<IUserGetDto | null>> => {
try {
const user = new User(userDto)
await user.save()
const data: IUserGetDto = {
_id: user._id,
username: user.username,
token: generateJWT({_id: user._id, username: user.username})
}
const response: IResponse<IUserGetDto> = {
status: EStatuses.SUCCESS,
result: data,
message: 'User added'
}
return response
} catch(err: unknown){
const error = err as Error
const response: IResponse<null> = {
status: EStatuses.FAILURE,
result: null,
message: error.message
}
return response
}
}
public loginUser = async(userDto: IUserCreateDto): Promise<IResponse<IUserGetDto | null>> => {
try{
const user = await User.findOne({username: userDto.username})
if (!user) throw new Error('User not found')
const isMatch: boolean = await user.checkPassword(userDto.password)
if (!isMatch) throw new Error('Wrong password')
const data = {
_id: user._id,
username: user.username,
token: generateJWT({_id: user._id, username: user.username})
}
await user.save()
const response: IResponse<IUserGetDto> = {
status: EStatuses.SUCCESS,
result: data,
message: 'Access granted'
}
return response
} catch(err: unknown){
const error = err as Error
const response: IResponse<null> = {
status: EStatuses.FAILURE,
result: null,
message: error.message
}
return response
}
}
public getMessages = async(): Promise<IResponse<IMessage[] | null>> => {
try{
const data = (await Message.find().populate('user').sort({$natural:-1}).limit(30)).reverse()
const response: IResponse<IMessage[] | null> = {
status: EStatuses.SUCCESS,
result: data,
message: 'Messages found'
}
return response
} catch(err: unknown){
const error = err as Error
const response: IResponse<null> = {
status: EStatuses.FAILURE,
result: null,
message: error.message
}
return response
}
}
public addMessage = async(messageDto: IMessageDto): Promise<IResponse<IMessage | null>> => {
try{
const message = new Message(messageDto)
const responseData = await message.save()
const response: IResponse<IMessage> = {
status: EStatuses.SUCCESS,
result: responseData,
message: 'Artist added'
}
return response
} catch (err: unknown){
const error = err as Error
const response: IResponse<null> = {
status: EStatuses.FAILURE,
result: null,
message: error.message
}
return response
}
}
}
export const mongooseDB = new MongooseDB()
\ No newline at end of file
import express, { Router } from 'express';
import { healthCheckController } from '../controllers/healthCheck';
const healthCheckRouter: Router = express.Router()
healthCheckRouter.get('/', healthCheckController)
export default healthCheckRouter
\ No newline at end of file
import express, { Router } from 'express';
import { checkToken, createUser, loginUser } from '../controllers/usersController';
import { auth } from '../middlewares/auth';
const usersRouter: Router = express.Router();
usersRouter.post('/', createUser)
usersRouter.post('/sessions', loginUser)
usersRouter.get('/token', auth, checkToken)
export default usersRouter
\ No newline at end of file
import IMessage from "../interfaces/IMessage";
import IMessageDto from "../interfaces/IMessageDto";
import IResponse from "../interfaces/IResponse";
import { MongooseDB, mongooseDB } from "../repository/mongooseDB";
export class MessagesService {
private repository: MongooseDB
constructor(){
this.repository = mongooseDB
}
public getMessages = async(): Promise<IResponse<IMessage[] | null>> => {
return await this.repository.getMessages()
}
public addMessage = async(messageDto: IMessageDto): Promise<IResponse<IMessage | null>> => {
return await this.repository.addMessage(messageDto)
}
}
export const messagesService = new MessagesService()
\ No newline at end of file
import IResponse from "../interfaces/IResponse";
import IUser from "../interfaces/IUser";
import IUserCreateDto from "../interfaces/IUserCreateDto";
import IUserGetDto from "../interfaces/IUserGetDto";
import { mongooseDB, MongooseDB } from "../repository/mongooseDB";
export class UsersService {
private repository: MongooseDB
constructor(){
this.repository = mongooseDB
}
public createUser = async (userDto: IUserCreateDto): Promise<IResponse<IUserGetDto | null>> => {
return await this.repository.createUser(userDto)
}
public loginUser = async (userDto: IUserCreateDto): Promise<IResponse<IUserGetDto | null>> => {
return await this.repository.loginUser(userDto)
}
}
export const usersService = new UsersService()
\ No newline at end of file
This diff is collapsed.
VITE_BASE_URL=http://localhost:8000/
\ No newline at end of file
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
node_modules
dist
dist-ssr
*.local
# production
/build
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
# misc
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Getting Started with Create React App
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
## Available Scripts
In the project directory, you can run:
### `npm start`
Runs the app in the development mode.\
Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
The page will reload when you make changes.\
You may also see any lint errors in the console.
### `npm test`
Launches the test runner in the interactive watch mode.\
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
### `npm run build`
Builds the app for production to the `build` folder.\
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.\
Your app is ready to be deployed!
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
### `npm run eject`
**Note: this is a one-way operation. Once you `eject`, you can't go back!**
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.
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.
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.
## Learn More
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).
### Code Splitting
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)
### Analyzing the Bundle Size
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)
### Making a Progressive Web App
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)
### Advanced Configuration
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)
### Deployment
This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
### `npm run build` fails to minify
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)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
This diff is collapsed.
{
"name": "frontend",
"version": "0.1.0",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview"
},
"dependencies": {
"@emotion/react": "^11.10.4",
"@emotion/styled": "^11.10.4",
"@mui/material": "^5.10.9",
"@mui/styled-engine-sc": "^5.10.6",
"@reduxjs/toolkit": "^1.8.6",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"axios": "^1.1.2",
"react": "^18.2.0",
"react-dom": "^18.2.0"
"react-dom": "^18.2.0",
"react-redux": "^8.0.4",
"react-router-dom": "^6.4.2",
"react-scripts": "5.0.1",
"styled-components": "^5.3.6",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"devDependencies": {
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",
"@vitejs/plugin-react": "^3.1.0",
"typescript": "^4.9.3",
"vite": "^4.2.0"
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
\ No newline at end of file
#root {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
.App {
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
background-image: url('./Assets/Images/app-bg.jpg');
background-size: 100% 100%;
width: 100%;
height: 100vh;
box-sizing: border-box;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafbaa);
}
@keyframes logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@media (prefers-reduced-motion: no-preference) {
a:nth-of-type(2) .logo {
animation: logo-spin infinite 20s linear;
}
}
.card {
padding: 2em;
}
.read-the-docs {
color: #888;
}
.main {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: calc(100vh - 10vh);
box-sizing: border-box;
}
\ No newline at end of file
import React from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { useSelector, shallowEqual } from 'react-redux';
import Layout from './Components/Layout/Layout';
import ProtectedRoute from './Components/ProtectedRoute/ProtectedRoute';
import Authentication from './Container/Authentication/Authentication';
import Chat from './Container/Chat/Chat';
import './App.css';
function App() {
const {user} = useSelector(state => state.users, shallowEqual);
return (
<div className="App">
<BrowserRouter>
<Routes>
<Route path='/' element={<Layout/>}>
<Route index element={<Authentication/>}/>
<Route path='/chat' element={
<ProtectedRoute isAllowed={user?.token} redirectedPath='/'>
<Chat/>
</ProtectedRoute>}
/>
</Route>
</Routes>
</BrowserRouter>
</div>
);
}
export default App;
import { useState } from 'react'
import reactLogo from './assets/react.svg'
import viteLogo from '/vite.svg'
import './App.css'
function App() {
const [count, setCount] = useState(0)
return (
<div className="App">
<div>
<a href="https://vitejs.dev" target="_blank">
<img src={viteLogo} className="logo" alt="Vite logo" />
</a>
<a href="https://reactjs.org" target="_blank">
<img src={reactLogo} className="logo react" alt="React logo" />
</a>
</div>
<h1>Vite + React</h1>
<div className="card">
<button onClick={() => setCount((count) => count + 1)}>
count is {count}
</button>
<p>
Edit <code>src/App.tsx</code> and save to test HMR
</p>
</div>
<p className="read-the-docs">
Click on the Vite and React logos to learn more
</p>
</div>
)
}
export default App
import axios from "axios";
import apiURL from "./config";
export default axios.create({
baseURL: apiURL
});
.window {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
box-sizing: border-box;
}
.ChatWindow {
width: 100%;
height: 87%;
border: 1px solid blueviolet;
border-radius: 20px;
padding: 20px;
box-sizing: border-box;
overflow-y: auto;
}
.ChatWindow::-webkit-scrollbar {
display: none;
}
.ChatTitle {
color: cyan;
font-size: 40px;
margin: 0;
border-bottom: 1px solid green;
text-shadow: 0px 0px 5px greenyellow;
}
.formMessage {
display: flex;
align-items: center;
justify-content: space-between;
border: 1px solid blueviolet;
border-radius: 10px;
padding: 10px;
height: 5%;
}
.formMessage input {
width: 80%;
border-radius: 5px;
border: none;
color: aqua;
background: rgba(138, 43, 226, 0.3);
height: 30px;
}
.formMessage input:focus,
.formMessage input:focus {
outline: 0;
box-shadow: 0px 0px 10px darkcyan;
border: none;
border-color: transparent;
}
.sendMessageBtn {
width: 18%;
height: 30px;
border-radius: 5px;
}
\ No newline at end of file
import React from 'react';
import Button from '@mui/material/Button';
import Message from '../Message/Message';
import './ChatWindow.css';
const ChatWindow = ({onSubmit, value, onChange, messages}) => {
return (
<div className='window'>
<div className='ChatWindow'>
<p className='ChatTitle'>Chat Room</p>
{
messages.map((message, idx) => {
return (
<Message key={message?._id + idx} message={message?.message} author={message?.user.username || message.user}/>
)
})
}
</div>
<form onSubmit={onSubmit} className='formMessage'>
<input type="text" name="message" value={value} onChange={onChange}/>
<Button type='submit' className='sendMessageBtn' color="secondary" variant="text">Send</Button>
</form>
</div>
);
};
export default ChatWindow;
\ No newline at end of file
.header {
margin: 0 auto;
display: flex;
justify-content: space-around;
width: 100%;
height: 5vh;
padding: 15px 0;
background-color: rgba(18, 18, 18, 0.7);
}
.headerTitle {
color: blueviolet;
text-shadow: 0px 0px 20px blueviolet;
}
.header:hover {
background-color: rgba(18, 18, 18, 1);
}
.pagination_header {
display: flex;
align-items: center;
justify-content: center;
}
.btn_btnGrp:hover {
box-shadow: 0px 0px 20px blueviolet;
text-shadow: 0px 0px 20px blueviolet;
}
.avatar {
margin-right: 20px;
}
.noAuthenticated {
color: blueviolet;
}
\ No newline at end of file
import React from "react";
import { userLogout } from "../../Store/Services/usersSlice";
import { useSelector, shallowEqual, useDispatch } from "react-redux";
import { useNavigate } from 'react-router-dom';
import Navigation from "../Navigation/Navigation";
import ButtonGroup from '@mui/material/ButtonGroup';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Avatar from '@mui/material/Avatar';
import './Header.css';
const Header = () => {
const navigate = useNavigate();
const dispatch = useDispatch();
const {user} = useSelector(state => state.users, shallowEqual);
const onClickLogoutHandler = () => {
dispatch(userLogout());
navigate({
pathname: '/'
});
};
return (
<header className="header">
<nav>
<Navigation to={'/chat'} end>
<h1 className="headerTitle">Chat</h1>
</Navigation>
</nav>
<nav className="pagination_header">
{
user ?
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center','& > *': { m: 1,},}}>
<ButtonGroup variant="text" color="secondary" size="large" aria-label="text button group">
<Button className="btn_btnGrp"><Avatar sx={{ bgcolor: 'blueviolet'}} className='avatar'></Avatar>{user.username}</Button>
<Button className="btn_btnGrp" onClick={onClickLogoutHandler}>Logout</Button>
</ButtonGroup>
</Box> : <h2 className="noAuthenticated">Need to be authenticated</h2>
}
</nav>
</header>
);
};
export default Header;
\ No newline at end of file
import React from "react";
import Header from '../Header/Header';
import { Outlet } from 'react-router-dom';
const Layout = () => {
return (
<>
<Header/>
<main className="main">
<Outlet/>
</main>
</>
);
};
export default Layout;
.active {
display: block;
}
.no-active {
display: none;
}
\ No newline at end of file
import React, {useState} from "react";
import { useSelector, shallowEqual, useDispatch } from "react-redux";
import { useNavigate } from 'react-router-dom';
import { loginUser } from "../../Store/Services/usersSlice";
import Form from "../UI/Form/Form";
import MyAlert from "../UI/MyAlert/MyAlert";
import Spinner from "../UI/Spinner/Spinner";
import './Login.css';
const Login = ({loginState}) => {
const {loginError, isLoading} = useSelector(state => state.users, shallowEqual);
const navigate = useNavigate('/');
const dispatch = useDispatch();
const [state, setState] = useState({
username: '',
password: ''
});
const inputChangeHandler = (e) => {
const {name, value} = e.target;
setState(prevState => ({...prevState, [name]: value}));
};
const submitHandler = async (e) => {
e.preventDefault();
await dispatch(loginUser({
userData: {...state},
navigate
}));
};
return(
<div className={loginState}>
{isLoading ? <Spinner/> : null}
{loginError && <MyAlert alertText={loginError.error}/>}
<Form
btnText={'Login'}
formTitle={'Please Login'}
onSubmit={submitHandler}
username={state.username}
password={state.password}
onChange={inputChangeHandler}
/>
</div>
);
};
export default Login;
\ No newline at end of file
.Message {
display: flex;
align-items: center;
}
.author {
color: cyan;
text-shadow: 0px 0px 5px greenyellow;
font-size: 25px;
margin: 0;
}
.authorMessage {
margin-left: 20px;
font-size: 20px;
color: blueviolet;
text-shadow: 0px 0px 20px black;
font-weight: 600;
}
\ No newline at end of file
import React from "react";
import './Message.css';
const Message = ({author, message}) => {
return (
<div className="Message">
<h3 className="author">{author}:</h3>
<p className="authorMessage">{message}</p>
</div>
);
};
export default Message;
\ No newline at end of file
.NavLink {
margin: 0;
display: flex;
height: 100%;
align-items: center;
}
.NavLink a {
color: whitesmoke;
font-size: 15px;
text-decoration: none;
display: block;
}
.NavLink a:hover,
.NavLink a:active,
.NavLink a.active {
color: blueviolet;
text-shadow: 0px 0px 20px blueviolet;
}
\ No newline at end of file
import React from "react";
import { NavLink } from "react-router-dom";
import './Navigation.css';
const Navigation = ({to, end, children}) => {
return (
<span className="NavLink">
<NavLink to={to} end={end}>{children}</NavLink>
</span>
);
};
export default Navigation;
\ No newline at end of file
import React from 'react';
import { Navigate, Outlet } from 'react-router-dom';
const ProtectedRoute = ({ isAllowed, redirectedPath, children }) => {
if(!isAllowed) {
return <Navigate to={redirectedPath} replace />
}
return (
<>
{children || <Outlet/>}
</>
);
};
export default ProtectedRoute;
\ No newline at end of file
.active {
display: block;
}
.no-active {
display: none;
}
\ No newline at end of file
import React, {useState} from "react";
import { useDispatch, useSelector, shallowEqual } from "react-redux";
import { registerUser } from "../../Store/Services/usersSlice";
import Form from "../UI/Form/Form";
import MyAlert from "../UI/MyAlert/MyAlert";
import Spinner from "../UI/Spinner/Spinner";
import './Register.css';
const Register = ({registerState}) => {
const {registerError, isLoading} = useSelector(state => state.users, shallowEqual);
const dispatch = useDispatch();
const [state, setState] = useState({
username: '',
password: ''
});
const inputChangeHandler = (e) => {
const {name, value} = e.target;
setState(prevState => ({...prevState, [name]: value}));
};
const submitHandler = async (e) => {
e.preventDefault();
await dispatch(registerUser({
userData: {...state}
}));
};
return(
<div className={registerState}>
{isLoading ? <Spinner/> : null}
{registerError && <MyAlert alertText={registerError.errors['username'].message}/>}
<Form
btnText={'Registration'}
formTitle={'Please register'}
onSubmit={submitHandler}
username={state.username}
password={state.password}
onChange={inputChangeHandler}
/>
</div>
);
};
export default Register;
\ No newline at end of file
.form_block {
background: rgba(3, 3, 55, 1);
width: 35rem;
height: 20rem;
padding: 20px;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.form_block h3 {
text-align: center;
vertical-align: middle;
line-height: 3rem;
height: 3rem;
background: rgba(3, 3, 55, 0.7);
font-size: 1.4rem;
color: #d3d3d3;
-webkit-animation: pulsate 1.2s linear infinite;
animation: pulsate 1.2s linear infinite;
}
@keyframes pulsate {
50% {color:#fff; text-shadow:0 -1px rgba(0,0,0,.3), 0 0 5px #f03000, 0 0 8px #f80000;}
}
.register_form {
display: flex;
height: 100%;
justify-content: space-around;
flex-direction: column;
}
.inputSubmit {
background: rgba(235, 30, 54, 1);
border: 0;
display: block;
width: 70%;
margin: 0 auto;
color: white;
padding: 0.7rem;
cursor: pointer;
}
.inputForm {
background: transparent;
border: 0;
border-left: 4px solid;
border-color: #FF0000;
padding: 10px;
color: white;
}
.inputForm:focus,
.inputForm:focus {
outline: 0;
background: rgba(235, 30, 54, 0.3);
border-radius: 1.2rem;
border-color: transparent;
}
::placeholder {
color: #d3d3d3;
}
@media all and (min-width: 481px) and (max-width: 568px) {
#container {
margin-top: 10%;
margin-bottom: 10%;
}
}
@media all and (min-width: 569px) and (max-width: 768px) {
#container {
margin-top: 5%;
margin-bottom: 5%;
}
}
\ No newline at end of file
import React from 'react';
import './Form.css';
const Form = ({onSubmit, username, password, onChange, formTitle, btnText}) => {
return (
<div className='form_block'>
<h3>{formTitle}</h3>
<form onSubmit={onSubmit} className='register_form'>
<input type="text" className='inputForm' name="username" value={username} onChange={onChange} autoComplete='off' placeholder="Username" required autoFocus/>
<input type="password" className='inputForm' name='password' value={password} onChange={onChange} autoComplete='off' placeholder="Password" required/>
<input className='inputSubmit' type="submit" name="submit" value={btnText}/>
</form>
</div>
);
};
export default Form;
\ No newline at end of file
.myAlert {
position: absolute!important;
top: 60px;
left: 60px;
z-index: 200;
}
\ No newline at end of file
import React, {useState} from 'react';
import Alert from '@mui/material/Alert';
import './MyAlert.css';
const MyAlert = ({alertText, alertTitle}) => {
const [show, setShow] = useState(true);
if (show) {
return (
<Alert severity="error" onClick={() => setShow(false)}>{alertText}</Alert>
);
};
};
export default MyAlert;
\ No newline at end of file
import React from "react";
import Backdrop from '@mui/material/Backdrop';
import CircularProgress from '@mui/material/CircularProgress';
import Box from '@mui/material/Box';
const Spinner = () => {
return (
<Box sx={{ display: 'flex' }}>
<Backdrop sx={{ color: '#fff', zIndex: (theme) => theme.zIndex.drawer + 1 }} open={true}>
<CircularProgress color="secondary"/>
</Backdrop>
</Box>
);
};
export default Spinner;
\ No newline at end of file
.User {
display: flex;
align-items: center;
width: 100px;
}
.username {
margin-left: 15px;
color: darkcyan;
font-size: 20px;
font-weight: bold;
}
\ No newline at end of file
import React from "react";
import { styled } from '@mui/material/styles';
import Badge from '@mui/material/Badge';
import Avatar from '@mui/material/Avatar';
import './User.css';
const User = ({user}) => {
const StyledBadge = styled(Badge)(({ theme }) => ({
'& .MuiBadge-badge': {
backgroundColor: '#44b700',
color: '#44b700',
boxShadow: `0 0 0 2px ${theme.palette.background.paper}`,
'&::after': {
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
borderRadius: '50%',
animation: 'ripple 1.2s infinite ease-in-out',
border: '1px solid currentColor',
content: '""',
},
},
'@keyframes ripple': {
'0%': {
transform: 'scale(.8)',
opacity: 1,
},
'100%': {
transform: 'scale(2.4)',
opacity: 0,
},
},
}));
return (
<div className="User">
<StyledBadge
overlap="circular"
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
variant="dot"
>
<Avatar sx={{ bgcolor: 'blueviolet'}}>{user}</Avatar>
</StyledBadge>
<p className="username">{user}</p>
</div>
);
};
export default User;
\ No newline at end of file
.UserList {
display: flex;
flex-direction: column;
border: 1px solid blueviolet;
border-radius: 20px;
width: 100%;
height: 100%;
padding: 20px;
overflow-y: auto;
box-sizing: border-box;
}
.onlineUser {
color: cyan;
font-size: 40px;
margin: 0;
border-bottom: 1px solid green;
text-shadow: 0px 0px 5px greenyellow;
}
.UserList::-webkit-scrollbar {
display: none;
}
\ No newline at end of file
import React from "react";
import User from "../User/User";
import './UserList.css';
const UserList = ({users}) => {
return (
<div className="UserList">
<p className="onlineUser">Online users</p>
{
users.map((user, idx) => {
return (
<User key={user._id + idx} user={user.username}/>
)
})
}
</div>
);
};
export default UserList;
\ No newline at end of file
import React, {useState} from "react";
import Register from "../../Components/Register/Register";
import Login from "../../Components/Login/Login";
import Radio from '@mui/material/Radio';
import RadioGroup from '@mui/material/RadioGroup';
import FormControlLabel from '@mui/material/FormControlLabel';
import FormControl from '@mui/material/FormControl';
import './Authentication.css';
const Authentication = () => {
const [state, setState] = useState({
isRegister: 'active',
isLogin: 'no-active'
});
const handleChange = (e) => {
if(e.target.value === 'register') {
setState(prevState => ({...prevState, isLogin: 'no-active', isRegister: 'active'}));
} else {
setState(prevState => ({...prevState, isLogin: 'active', isRegister: 'no-active'}));
};
};
return(
<div className="Authentication">
<div className="Authentication_pagination">
<FormControl>
<RadioGroup aria-labelledby="demo-radio-buttons-group-label" defaultValue="register" name="radio-buttons-group">
<FormControlLabel onClick={handleChange} sx={{color: 'blueviolet', textShadow: '0px 0px 20px blueviolet'}} value="register" control={<Radio color="secondary"/>} label="Register" />
<FormControlLabel onClick={handleChange} sx={{color: 'blueviolet', textShadow: '0px 0px 20px blueviolet'}} value="login" control={<Radio color="secondary"/>} label="Login" />
</RadioGroup>
</FormControl>
</div>
<Register registerState={state.isRegister}/>
<Login loginState={state.isLogin}/>
</div>
);
};
export default Authentication;
\ No newline at end of file
.Chat {
display: flex;
justify-content: space-between;
width: 95%;
height: 90%;
}
.Chat_users {
width: 25%;
height: 100%;
}
.Chat_window {
width: 70%;
height: 100%;
}
import React, {useState, useRef, useEffect} from "react";
import { useSelector, shallowEqual, useDispatch } from "react-redux";
import { getAllMessages, getMessages, getOnlineUser, deleteOflineUser } from "../../Store/Services/messagesSlice";
import UserList from "../../Components/UserList/UserList";
import ChatWindow from "../../Components/ChatWindow/ChatWindow";
import './Chat.css';
const Chat = () => {
const ws = useRef(null);
const dispatch = useDispatch();
const [message, setMessage] = useState('');
const {user} = useSelector(state => state.users, shallowEqual);
const {messages, users} = useSelector(state => state.messages, shallowEqual);
useEffect(() => {
ws.current = new WebSocket('ws://localhost:8000/chat');
ws.current.onopen = async () => {
await ws.current.send(JSON.stringify({
type:"OPEN_CHAT",
user: user
}));
await ws.current.send(JSON.stringify({
type:"GET_ALL_MESSAGES"
}));
};
ws.current.onmessage = async (event) => {
const decodedMessage = await JSON.parse(event.data);
if(decodedMessage.type === "NEW_MESSAGES"){
dispatch(getMessages(decodedMessage.message));
};
if(decodedMessage.type === "ALL_MESSAGES"){
dispatch(getAllMessages(decodedMessage.allMessages));
};
if(decodedMessage.type === "OPEN_CHAT_USER"){
dispatch(getOnlineUser(decodedMessage.user));
};
if(decodedMessage.type === 'CLOSED_CHAT_USER') {
dispatch(deleteOflineUser(decodedMessage.id));
};
};
return () => {
ws.current.send(JSON.stringify({
type:"CLOSED_CHAT",
user: user
}));
};
}, [user, dispatch]);
const onChangeHandler = (e) => {
setMessage(e.target.value);
};
const onSubmitHandler = async (e) => {
e.preventDefault();
await ws.current.send(JSON.stringify({
type:"CREATE_MESSAGE",
user: {
text: message,
id: user._id
}
}));
};
return (
<div className="Chat">
<div className="Chat_users">
<UserList users={users}/>
</div>
<div className="Chat_window">
<ChatWindow
messages={messages}
onSubmit={onSubmitHandler}
value={message}
onChange={onChangeHandler}
/>
</div>
</div>
);
};
export default Chat;
\ No newline at end of file
import {createSlice} from "@reduxjs/toolkit";
const initialState = {
messages: [],
users: []
};
const messagesSlice = createSlice({
name: 'messages',
initialState,
reducers: {
getMessages: (state, action) => {
state.messages.push(action.payload);
},
getAllMessages: (state, action) => {
state.messages = action.payload;
},
getOnlineUser: (state, action) => {
state.users = action.payload;
},
deleteOflineUser: (state, action) => {
const index = state.users.findIndex(user => user._id === action.payload);
state.users.splice(index, 1);
}
}
});
export const {getMessages, getAllMessages, getOnlineUser, deleteOflineUser} = messagesSlice.actions;
export default messagesSlice.reducer;
\ No newline at end of file
import {createSlice, createAsyncThunk} from "@reduxjs/toolkit";
import axios from "../../AxiosBaseUrl";
const initialState = {
user: null,
isLoading: false,
registerStatus: null,
error: false,
registerError: null,
global: null,
loginError: null,
};
export const loginUser = createAsyncThunk(
'users/login',
async (payload, thunkApi) => {
try {
const res = await axios.post('/users/sessions', payload.userData);
payload.navigate('/chat');
return res.data;
} catch (e) {
if (e.response && e.response.data) {
thunkApi.dispatch(usersSlice.actions.catchLoginError(e.response.data));
} else {
thunkApi.dispatch(usersSlice.actions.globalError(e));
};
};
}
);
export const registerUser = createAsyncThunk(
'users/register',
async (payload, thunkApi) => {
try {
return await axios.post('/users', payload.userData).then(res => res.data);
} catch (e) {
if (e.response && e.response.data) {
thunkApi.dispatch(usersSlice.actions.catchRegisterError(e.response.data));
} else {
thunkApi.dispatch(usersSlice.actions.globalError(e));
};
};
}
);
export const userLogout = createAsyncThunk(
'logout/users',
async (payload, thunkAPI) => {
try {
const token = thunkAPI.getState().users.token;
const headers = {'Authorization': token};
await axios.delete('users/sessions', {headers});
thunkAPI.dispatch(usersSlice.actions.logoutUser());
} catch (error) {
console.log(error);
};
}
);
const usersSlice = createSlice({
name: 'users',
initialState,
reducers: {
getMessages: (state, action) => {
state.messages = action.payload;
},
catchRegisterError: (state, action) => {
state.registerError = action.payload;
},
catchLoginError: (state, action) => {
state.loginError = action.payload;
},
globalError: (state, action) => {
state.global = action.payload;
},
logoutUser: (state) => {
state.user = null;
}
},
extraReducers: {
[registerUser.pending]: (state) => {
state.isLoading = true;
state.loginError = null;
state.global = null;
state.registerError = null;
},
[registerUser.rejected]: (state, action) => {
state.isLoading = false;
state.error = action.error;
},
[registerUser.fulfilled]: (state, action) => {
state.registerStatus = action.payload.user;
state.isLoading = false;
},
[loginUser.pending]: (state) => {
state.isLoading = true;
state.loginError = null;
state.global = null;
state.registerError = null;
},
[loginUser.rejected]: (state, action) => {
state.isLoading = false;
state.error = action.error;
},
[loginUser.fulfilled]: (state, action) => {
state.user = action.payload;
state.isLoading = false;
}
}
});
export const {catchLoginError, catchRegisterError, globalError, logoutUser, getMessages} = usersSlice.actions;
export default usersSlice.reducer;
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
\ No newline at end of file
const apiURL = 'http://localhost:8000';
export default apiURL;
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
box-sizing: border-box;
text-transform: capitalize;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import { Provider } from 'react-redux';
import { configureStore } from '@reduxjs/toolkit';
import usersReducer from './Store/Services/usersSlice.js';
import messagesReducer from './Store/Services/messagesSlice.js';
import reportWebVitals from './reportWebVitals';
const store = configureStore({
reducer: {
users: usersReducer,
messages: messagesReducer
}
});
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<Provider store={store}>
<App />
</Provider>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import './index.css'
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)
const reportWebVitals = onPerfEntry => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};
export default reportWebVitals;
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';
/// <reference types="vite/client" />
{
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"allowJs": false,
"skipLibCheck": true,
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "Node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}
{
"compilerOptions": {
"composite": true,
"module": "ESNext",
"moduleResolution": "Node",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
})
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment