Complete project

This commit is contained in:
2025-03-17 10:01:32 +04:00
parent bcc5d1daf4
commit 46c040c642
41 changed files with 1951 additions and 127 deletions

View File

@@ -1,3 +1,4 @@
PORT=8000
DATABASE_TYPE=postgres
DATABASE_HOST=localhost
DATABASE_PORT=3306

View File

@@ -9,12 +9,15 @@
"version": "0.0.1",
"license": "UNLICENSED",
"dependencies": {
"@nestjs/common": "^11.0.1",
"@nestjs/common": "^11.0.11",
"@nestjs/config": "^4.0.1",
"@nestjs/core": "^11.0.1",
"@nestjs/core": "^11.0.11",
"@nestjs/platform-express": "^11.0.1",
"@nestjs/platform-socket.io": "^11.0.11",
"@nestjs/platform-ws": "^11.0.11",
"@nestjs/swagger": "^11.0.6",
"@nestjs/typeorm": "^11.0.0",
"@nestjs/websockets": "^11.0.11",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"mysql2": "^3.13.0",
@@ -2407,6 +2410,65 @@
"@nestjs/core": "^11.0.0"
}
},
"node_modules/@nestjs/platform-socket.io": {
"version": "11.0.11",
"resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-11.0.11.tgz",
"integrity": "sha512-+bLrtTSPDX6AxrL9PbR5lEgcEnn6oFzkGpLUcm3Xs9x5OBejzJh1tiWgBGJRQIh3l9iIG8/mQ8hNwufAt8SIcA==",
"license": "MIT",
"dependencies": {
"socket.io": "4.8.1",
"tslib": "2.8.1"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/nest"
},
"peerDependencies": {
"@nestjs/common": "^11.0.0",
"@nestjs/websockets": "^11.0.0",
"rxjs": "^7.1.0"
}
},
"node_modules/@nestjs/platform-ws": {
"version": "11.0.11",
"resolved": "https://registry.npmjs.org/@nestjs/platform-ws/-/platform-ws-11.0.11.tgz",
"integrity": "sha512-aIQCEJVHwlRwJPczFShsWEJq7w+VqyAFe1EjG8oxSihJ7sPqT+1jOLgqXabZSAKZNl2sF4++NtxwGZG++5/hlQ==",
"license": "MIT",
"dependencies": {
"tslib": "2.8.1",
"ws": "8.18.1"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/nest"
},
"peerDependencies": {
"@nestjs/common": "^11.0.0",
"@nestjs/websockets": "^11.0.0",
"rxjs": "^7.1.0"
}
},
"node_modules/@nestjs/platform-ws/node_modules/ws": {
"version": "8.18.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz",
"integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/@nestjs/schematics": {
"version": "11.0.2",
"resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-11.0.2.tgz",
@@ -2579,6 +2641,29 @@
"typeorm": "^0.3.0"
}
},
"node_modules/@nestjs/websockets": {
"version": "11.0.11",
"resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-11.0.11.tgz",
"integrity": "sha512-9sNNT/kYA534iaFyZ9MrOXKwQFuJArsMXhT6ywVxaWKQ84lVbV/sDmdmJUe9mzUGLPiHMn+m3oDUO9MiLTEKPA==",
"license": "MIT",
"dependencies": {
"iterare": "1.2.1",
"object-hash": "3.0.0",
"tslib": "2.8.1"
},
"peerDependencies": {
"@nestjs/common": "^11.0.0",
"@nestjs/core": "^11.0.0",
"@nestjs/platform-socket.io": "^11.0.0",
"reflect-metadata": "^0.1.12 || ^0.2.0",
"rxjs": "^7.1.0"
},
"peerDependenciesMeta": {
"@nestjs/platform-socket.io": {
"optional": true
}
}
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -2710,6 +2795,12 @@
"@sinonjs/commons": "^3.0.0"
}
},
"node_modules/@socket.io/component-emitter": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
"license": "MIT"
},
"node_modules/@sqltools/formatter": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/@sqltools/formatter/-/formatter-1.2.5.tgz",
@@ -3134,6 +3225,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/cors": {
"version": "2.8.17",
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz",
"integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==",
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/eslint": {
"version": "9.6.1",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz",
@@ -3276,7 +3376,6 @@
"version": "22.13.10",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz",
"integrity": "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.20.0"
@@ -4372,6 +4471,15 @@
],
"license": "MIT"
},
"node_modules/base64id": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
"integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==",
"license": "MIT",
"engines": {
"node": "^4.5.0 || >= 5.9"
}
},
"node_modules/bin-version": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/bin-version/-/bin-version-6.0.0.tgz",
@@ -5486,6 +5594,104 @@
"node": ">= 0.8"
}
},
"node_modules/engine.io": {
"version": "6.6.4",
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz",
"integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==",
"license": "MIT",
"dependencies": {
"@types/cors": "^2.8.12",
"@types/node": ">=10.0.0",
"accepts": "~1.3.4",
"base64id": "2.0.0",
"cookie": "~0.7.2",
"cors": "~2.8.5",
"debug": "~4.3.1",
"engine.io-parser": "~5.2.1",
"ws": "~8.17.1"
},
"engines": {
"node": ">=10.2.0"
}
},
"node_modules/engine.io-parser": {
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
"integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/engine.io/node_modules/accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
"license": "MIT",
"dependencies": {
"mime-types": "~2.1.34",
"negotiator": "0.6.3"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/engine.io/node_modules/cookie": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/engine.io/node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/engine.io/node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/engine.io/node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/engine.io/node_modules/negotiator": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/enhanced-resolve": {
"version": "5.18.1",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz",
@@ -8725,6 +8931,15 @@
"node": ">=0.10.0"
}
},
"node_modules/object-hash": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
"integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
"license": "MIT",
"engines": {
"node": ">= 6"
}
},
"node_modules/object-inspect": {
"version": "1.13.4",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
@@ -10098,6 +10313,141 @@
"node": ">=8"
}
},
"node_modules/socket.io": {
"version": "4.8.1",
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz",
"integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==",
"license": "MIT",
"dependencies": {
"accepts": "~1.3.4",
"base64id": "~2.0.0",
"cors": "~2.8.5",
"debug": "~4.3.2",
"engine.io": "~6.6.0",
"socket.io-adapter": "~2.5.2",
"socket.io-parser": "~4.2.4"
},
"engines": {
"node": ">=10.2.0"
}
},
"node_modules/socket.io-adapter": {
"version": "2.5.5",
"resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz",
"integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==",
"license": "MIT",
"dependencies": {
"debug": "~4.3.4",
"ws": "~8.17.1"
}
},
"node_modules/socket.io-adapter/node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/socket.io-parser": {
"version": "4.2.4",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
"integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
"license": "MIT",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/socket.io-parser/node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/socket.io/node_modules/accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
"license": "MIT",
"dependencies": {
"mime-types": "~2.1.34",
"negotiator": "0.6.3"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/socket.io/node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/socket.io/node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/socket.io/node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/socket.io/node_modules/negotiator": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/sort-keys": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz",
@@ -11414,7 +11764,6 @@
"version": "6.20.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
"devOptional": true,
"license": "MIT"
},
"node_modules/universalify": {
@@ -11910,6 +12259,27 @@
"dev": true,
"license": "ISC"
},
"node_modules/ws": {
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",

View File

@@ -20,12 +20,15 @@
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@nestjs/common": "^11.0.1",
"@nestjs/common": "^11.0.11",
"@nestjs/config": "^4.0.1",
"@nestjs/core": "^11.0.1",
"@nestjs/core": "^11.0.11",
"@nestjs/platform-express": "^11.0.1",
"@nestjs/platform-socket.io": "^11.0.11",
"@nestjs/platform-ws": "^11.0.11",
"@nestjs/swagger": "^11.0.6",
"@nestjs/typeorm": "^11.0.0",
"@nestjs/websockets": "^11.0.11",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"mysql2": "^3.13.0",

View File

@@ -1,10 +1,14 @@
import Message, { CreateMessageDTO } from '@/entities/message';
import MessageGateway from '@/shared/gateways/message';
import MessageService from '@/shared/services/message';
import { Body, Controller, Get, Post } from '@nestjs/common';
@Controller('message')
export class MessageController {
constructor(private messageService: MessageService) {}
constructor(
private messageService: MessageService,
private messageGateway: MessageGateway,
) {}
@Get()
async getTopOfHistory(): Promise<Message[]> {
@@ -13,6 +17,8 @@ export class MessageController {
@Post()
async send(@Body() message: CreateMessageDTO): Promise<Message> {
return await this.messageService.addMessage(message);
const newMessage = await this.messageService.addMessage(message);
this.messageGateway.sendMessage(newMessage);
return newMessage;
}
}

View File

@@ -5,6 +5,9 @@ export class Message {
@PrimaryGeneratedColumn()
id: number;
@Column()
text: string;
@Column()
timeOfSend: string;

View File

@@ -1,12 +1,15 @@
import { NestFactory } from '@nestjs/core';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { ValidationPipe } from '@nestjs/common';
import { WsAdapter } from '@nestjs/platform-ws';
import AppModule from './modules/app';
import AppModule from '@/modules/app';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe());
app.useWebSocketAdapter(new WsAdapter(app));
const options = new DocumentBuilder()
.setTitle('Chat')

View File

@@ -4,13 +4,14 @@ import { ConfigModule } from '@nestjs/config';
import config from '@/configuration/configuration';
import databaseConfig from '@/configuration/database';
import MessageModule from '../message';
import MessageModule from '@/modules/message';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
load: [config],
load: [config, databaseConfig],
}),
TypeOrmModule.forRoot(databaseConfig()),

View File

@@ -1,5 +1,6 @@
import MessageController from '@/controllers/message';
import Message from '@/entities/message';
import MessageGateway from '@/shared/gateways/message';
import MessageService from '@/shared/services/message';
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
@@ -7,6 +8,6 @@ import { TypeOrmModule } from '@nestjs/typeorm';
@Module({
imports: [TypeOrmModule.forFeature([Message])],
controllers: [MessageController],
providers: [MessageService],
providers: [MessageService, MessageGateway],
})
export class MessageModule {}

View File

@@ -0,0 +1,3 @@
import { MessageGateway } from './message.gateway';
export default MessageGateway;

View File

@@ -0,0 +1,17 @@
import Message from '@/entities/message';
import { OnGatewayConnection, WebSocketGateway } from '@nestjs/websockets';
import { Socket } from 'socket.io';
@WebSocketGateway(8002)
export class MessageGateway implements OnGatewayConnection {
private clients: Socket[] = [];
handleConnection(client: Socket) {
this.clients.push(client);
}
sendMessage = (message: Message) => {
console.log(message, this.clients.length);
this.clients.forEach((client) => client.send(JSON.stringify(message)));
};
}

50
docker-compose-all.yml Normal file
View File

@@ -0,0 +1,50 @@
services:
postgres:
container_name: postgres_container
image: postgres
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: pass2postgres
PGDATA: /data/postgres
volumes:
- postgres:/data/postgres
- ./create_database.sql:/docker-entrypoint-initdb.d/init.sql
# ports:
# - "5432:5432"
networks:
- postgres
restart: unless-stopped
backend:
build: ./backend
environment:
DATABASE_HOST: postgres
DATABASE_PORT: 5432
DATABASE_USER: postgres
DATABASE_PASSWORD: pass2postgres
DATABASE_DATABASE: chat
WEBSOCKETS_PORT: 8001
# ports:
# - "5000:5000"
depends_on:
- postgres
frontend:
build: ./frontend
environment:
NEXT_PUBLIC_BASE_PROTOCOL: http
NEXT_PUBLIC_BASE_DOMAIN: backend
NEXT_PUBLIC_BASE_PORT: 3000
NEXT_PUBLIC_WS_URL: ws://backend:8002/
NEXT_PUBLIC_API_PATTERN: /
ports:
- "3000:3000"
depends_on:
- backend
networks:
postgres:
driver: bridge
volumes:
postgres:

View File

@@ -1 +0,0 @@
NEXT_PUBLIC_API_PATTERN=/api

View File

@@ -1,9 +1,11 @@
BACKEND_PROTOCOL=http
BACKEND_DOMAIN=127.0.0.1
BACKEND_PORT=8000
BACKEND_API_PROTOCOL=http
BACKEND_API_DOMAIN=localhost
BACKEND_API_PORT=8000
BASE_PROTOCOL=http
BASE_DOMAIN=127.0.0.1
BASE_PORT=3000
NEXT_PUBLIC_BASE_PROTOCOL=http
NEXT_PUBLIC_BASE_DOMAIN=localhost
NEXT_PUBLIC_BASE_PORT=3000
NEXT_PUBLIC_BASE_URL=http://127.0.0.1:3000
NEXT_PUBLIC_WS_URL=ws://127.0.0.1:8002/
NEXT_PUBLIC_API_PATTERN=/api/

View File

@@ -4,20 +4,21 @@ const nextConfig = {
{
source: "/api/:path*",
destination:
`${process.env.BACKEND_PROTOCOL}://` +
`${process.env.BACKEND_DOMAIN}:${process.env.BACKEND_PORT}/:path*`,
`${process.env.BACKEND_API_PROTOCOL}://` +
`${process.env.BACKEND_API_DOMAIN}:${process.env.BACKEND_API_PORT}/:path*`,
},
];
},
images: {
remotePatterns: [
{
protocol: process.env.BASE_PROTOCOL,
hostname: process.env.BASE_DOMAIN,
port: process.env.BASE_PORT,
protocol: process.env.NEXT_PUBLIC_BASE_PROTOCOL,
hostname: process.env.NEXT_PUBLIC_BASE_DOMAIN,
port: process.env.NEXT_PUBLIC_BASE_PORT,
},
],
},
devIndicators: false,
};
export default nextConfig;

File diff suppressed because it is too large Load Diff

View File

@@ -9,10 +9,14 @@
"lint": "next lint"
},
"dependencies": {
"@tailwindcss/vite": "^4.0.0",
"next": "15.2.2",
"next-themes": "^0.4.6",
"react": "^19.0.0",
"react-dom": "^19.0.0"
"react-dom": "^19.0.0",
"swr": "^2.3.3",
"uuid": "^11.1.0",
"yup": "^1.6.1"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
@@ -22,7 +26,7 @@
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "15.2.2",
"tailwindcss": "^4",
"tailwindcss": "^4.0.0",
"typescript": "^5"
}
}

View File

@@ -1,5 +1,7 @@
const config = {
plugins: ["@tailwindcss/postcss"],
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;

View File

@@ -1,14 +1,32 @@
@tailwind base;
/* @tailwind base;
@tailwind components;
@tailwind utilities;
@tailwind utilities; */
@import "tailwindcss";
:root {
font-size: calc((100vw / 1920) * 20);
--color-col1: #fb4934;
--color-col2: #b8bb26;
--color-col3: #fabd2f;
--color-col4: #83a598;
--color-col5: #d3869b;
--color-col6: #8ec07c;
--color-col7: #a89986;
--color-col8: #fe8019;
}
@theme {
--color-bg0: #fbf1c7;
--color-bg1: #ebdbb2;
--color-bg2: #d5c4a1;
--color-bg3: #bdae93;
--color-bg4: #a89984;
--color-fg0: #282828;
--color-fg1: #3c3836;
--color-fg2: #504945;
--color-fg3: #665c54;
--color-fg4: #7c6f64;
--color-ac0: #83a598;
@@ -16,18 +34,19 @@
--color-ac2: #8ec07c;
--color-err: #cc241d;
--app-width: 70%;
font-size: calc((100vw / 1920) * 20);
}
[data-theme="dark"] {
--color-bg0: #282828;
--color-bg1: #3c3836;
--color-bg2: #504945;
--color-bg3: #665c54;
--color-bg4: #7c6f64;
--color-fg0: #fbf1c7;
--color-fg1: #ebdbb2;
--color-fg2: #d5c4a1;
--color-fg3: #bdae93;
--color-fg4: #a89984;
--color-ac0: #076678;
@@ -35,8 +54,19 @@
--color-ac2: #427b58;
--color-err: #cc241d;
--color-col1: #9d0006;
--color-col2: #79740e;
--color-col3: #b57614;
--color-col4: #076678;
--color-col5: #8f3f71;
--color-col6: #427b58;
--color-col7: #7c6f65;
--color-col8: #af3a03;
}
@custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *));
html,
body {
padding: 0;
@@ -59,10 +89,6 @@ body * {
scrollbar-width: thin;
}
audio::-webkit-media-controls-panel {
background-color: var(--color-bg1);
}
@media (max-width: 1024px) {
:root {
font-size: calc((100vw / 1920) * 56);
@@ -74,4 +100,4 @@ audio::-webkit-media-controls-panel {
:root {
font-size: calc((100vw / 1920) * 64);
}
}
}

View File

@@ -1,6 +1,6 @@
import { Roboto } from "next/font/google";
import "./globals.css";
import { ThemeProvider } from "next-themes";
import "./globals.css";
const roboto = Roboto({ subsets: ["latin"] });
@@ -12,7 +12,7 @@ export default function RootLayout({
return (
<html lang="ru" suppressHydrationWarning>
<body className={roboto.className}>
<ThemeProvider enableSystem={false} defaultTheme="light">
<ThemeProvider enableSystem={false} defaultTheme="dark">
{children}
</ThemeProvider>
</body>

View File

@@ -1,3 +1,4 @@
import ChatWindow from "@/widgets/chat-window";
import { Metadata } from "next";
export const metadata: Metadata = {
@@ -6,5 +7,9 @@ export const metadata: Metadata = {
};
export default async function Root() {
return <></>;
return (
<main className="w-full h-full flex items-center justify-center p-8">
<ChatWindow />
</main>
);
}

View File

@@ -0,0 +1,5 @@
import { MessageService } from "./message.service";
export { type IMessage, messageSchema, messagesSchema } from "./message.schema";
export default MessageService;

View File

@@ -0,0 +1,11 @@
import { object, string, number, InferType, array } from "yup";
export const messageSchema = object({
id: number().required().positive().integer(),
text: string().required(),
timeOfSend: string().required(),
sender: string().required(),
});
export const messagesSchema = array(messageSchema);
export type IMessage = InferType<typeof messageSchema>;

View File

@@ -0,0 +1,16 @@
import HTTPService from "@/shared/services/http";
import { messageSchema, messagesSchema } from "./message.schema";
export abstract class MessageService {
public static getTopOfHistory = () =>
HTTPService.get(`message`, messagesSchema);
public static sendMessage = (message: string, senderUUID: string) =>
HTTPService.post(`message`, messageSchema, {
body: {
text: message,
timeOfSend: new Date().toISOString(),
sender: senderUUID,
},
});
}

View File

@@ -0,0 +1,5 @@
import { UserService } from "./user.service";
export { type IUser, userSchema } from "./user.schema";
export default UserService;

View File

@@ -0,0 +1,8 @@
import { object, string, number, InferType } from "yup";
export const userSchema = object({
uuid: string().uuid().required(),
color: number().min(1).max(8).required(),
});
export type IUser = InferType<typeof userSchema>;

View File

@@ -0,0 +1,25 @@
import { v1 as uuidV1 } from "uuid";
import { IUser } from "./user.schema";
export abstract class UserService {
private static pickedColors: IUser["color"][] = [];
public static generateMe = (): IUser => ({
uuid: uuidV1(),
color: 8,
});
public static generateUser = (uuid: IUser["uuid"]): IUser => {
const newColor = Array.from(Array(7).keys())
.map((i) => i + 1)
.reduce(
(p, c) => (!p && !this.pickedColors.includes(c) ? c : p),
undefined as number | undefined
);
this.pickedColors.push(newColor ?? 1);
return {
uuid: uuid,
color: newColor ?? 1,
};
};
}

View File

@@ -0,0 +1,17 @@
"use client";
import { SunIcon } from "@/shared/assets/icons";
import { useTheme } from "next-themes";
export const ColorSchemeSwitch = () => {
const { theme, setTheme } = useTheme();
return (
<>
<SunIcon
className="h-10 min-w-10 max-w-10 cursor-pointer bg-bg3 p-1 rounded-lg hover:bg-bg4 transition"
onClick={() => setTheme(theme == "light" ? "dark" : "light")}
/>
</>
);
};

View File

@@ -0,0 +1,3 @@
import { ColorSchemeSwitch } from "./colorSchemeSwitch";
export default ColorSchemeSwitch;

View File

@@ -0,0 +1,3 @@
import { MessagesList } from "./messagesList";
export default MessagesList;

View File

@@ -0,0 +1,28 @@
import { IMessage } from "@/entities/message";
import { IUser } from "@/entities/user";
export const Message = ({
message,
color,
align = "right",
}: {
message: IMessage;
color: IUser["color"] | undefined;
align?: "left" | "right";
}) => {
return (
<div
key={message.id}
style={{
background: `var(--color-col${color})`,
borderRadius: `var(--radius-xl)`,
[align === "right"
? "borderBottomRightRadius"
: "borderBottomLeftRadius"]: 0,
}}
className={`max-w-20 p-2`}
>
{message.text}
</div>
);
};

View File

@@ -0,0 +1,73 @@
"use client";
import MessageService from "@/entities/message";
import UserService, { IUser } from "@/entities/user";
import { useEffect, useRef, useState } from "react";
import useSWR from "swr";
import { Message } from "./message";
export const MessagesList = () => {
const { data: users, mutate: mutateUsers } = useSWR<IUser[]>("users");
const { data: messages, mutate: mutateMessages } = useSWR(
"messages",
MessageService.getTopOfHistory,
{ revalidateOnFocus: false }
);
const { data: me } = useSWR<IUser>("me");
const [websocket, updateWebSocket] = useState<WebSocket>();
const bottomRef = useRef<HTMLDivElement>(null);
const getUserColor = (userUUID: IUser["uuid"]) =>
users?.find((user) => user.uuid === userUUID)?.color;
useEffect(() => {
bottomRef.current?.scrollIntoView({ behavior: "smooth" });
const newUsers: IUser[] = [];
messages?.forEach((message) => {
if (
!users?.concat(newUsers).find((user) => user.uuid === message.sender)
) {
const newUser = UserService.generateUser(message.sender);
newUsers.push(newUser);
}
});
mutateUsers([...(users ?? []), ...newUsers]);
}, [messages]);
useEffect(() => {
if (!websocket && messages && process.env.NEXT_PUBLIC_WS_URL) {
const websocket = new WebSocket(process.env.NEXT_PUBLIC_WS_URL);
websocket.onmessage = (event) => {
mutateMessages([...(messages ?? []), JSON.parse(event.data)], {
revalidate: false,
});
};
websocket.onclose = function () {
setTimeout(() => updateWebSocket(undefined), 1000);
};
updateWebSocket(websocket);
}
return () => websocket?.close();
}, [websocket, messages]);
return (
<div className="max-w-full h-full overflow-auto flex flex-col gap-2">
{messages?.map((message) => (
<div
key={message.id}
className="w-full flex"
style={{
justifyContent: message.sender === me?.uuid ? "end" : "start",
}}
>
<Message
message={message}
color={getUserColor(message.sender)}
align={message.sender === me?.uuid ? "right" : "left"}
/>
</div>
))}
<div ref={bottomRef}></div>
</div>
);
};

View File

@@ -0,0 +1,3 @@
import { SendMessage } from "./sendMessage";
export default SendMessage;

View File

@@ -0,0 +1,41 @@
"use client";
import MessageService from "@/entities/message";
import UserService from "@/entities/user";
import { useState } from "react";
import useSWR from "swr";
export const SendMessage = () => {
const [message, updateMessage] = useState<string>("");
const { data: me } = useSWR(`me`, UserService.generateMe);
return (
<div className="w-full h-full flex items-center gap-4">
<input
type="text"
className={
"w-full h-full px-4 py-1 rounded-lg" +
" focus:outline-1 outline-fg4 focus:shadow-md"
}
placeholder="Напиши сообщение..."
value={message}
onChange={(e) => updateMessage(e.target.value)}
/>
<button
className={
"h-full px-4 rounded-lg bg-ac2 disabled:bg-bg3" +
" cursor-pointer hover:shadow-md text-fg0 disabled:text-fg4"
}
disabled={!message || !me}
onClick={() => {
if (!!me) {
MessageService.sendMessage(message, me.uuid);
updateMessage("");
}
}}
>
Отправить
</button>
</div>
);
};

View File

@@ -0,0 +1,2 @@
export { SunIcon } from "./sunIcon";
export { SendIcon } from "./sendIcon";

View File

@@ -0,0 +1,33 @@
export const SendIcon = ({
className,
onClick,
}: {
className?: string;
onClick?: () => void;
}) => {
return (
<svg
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
onClick={onClick}
>
<g id="SVGRepo_bgCarrier" stroke-width="0"></g>
<g
id="SVGRepo_tracerCarrier"
stroke-linecap="round"
stroke-linejoin="round"
></g>
<g id="SVGRepo_iconCarrier">
{" "}
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M1.265 4.42619C1.04293 2.87167 2.6169 1.67931 4.05323 2.31397L21.8341 10.1706C23.423 10.8727 23.423 13.1273 21.8341 13.8294L4.05323 21.686C2.6169 22.3207 1.04293 21.1283 1.265 19.5738L1.99102 14.4917C2.06002 14.0087 2.41458 13.6156 2.88791 13.4972L8.87688 12L2.88791 10.5028C2.41458 10.3844 2.06002 9.99129 1.99102 9.50829L1.265 4.42619ZM21.0257 12L3.2449 4.14335L3.89484 8.69294L12.8545 10.9328C13.9654 11.2106 13.9654 12.7894 12.8545 13.0672L3.89484 15.3071L3.2449 19.8566L21.0257 12Z"
fill="var(--color-fg0)"
></path>{" "}
</g>
</svg>
);
};

View File

@@ -0,0 +1,31 @@
export const SunIcon = ({
className,
onClick,
}: {
className?: string;
onClick?: () => void;
}) => {
return (
<svg
width="800px"
height="800px"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
onClick={onClick}
>
<path
d="M12 3V4M12 20V21M4 12H3M6.31412 6.31412L5.5 5.5M17.6859
6.31412L18.5 5.5M6.31412 17.69L5.5 18.5001M17.6859
17.69L18.5 18.5001M21 12H20M16 12C16 14.2091 14.2091
16 12 16C9.79086 16 8 14.2091 8 12C8 9.79086 9.79086
8 12 8C14.2091 8 16 9.79086 16 12Z"
stroke="var(--color-fg1)"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
};

View File

@@ -0,0 +1,95 @@
import { AnySchema } from "yup";
export type RequestCacheOptions = {
cache?: RequestCache;
next?: NextFetchRequestConfig;
};
type GetRequestOptions = RequestCacheOptions & {
headers?: HeadersInit;
};
type RequestOptions = GetRequestOptions & {
body?: BodyInit | object;
stringify?: boolean;
};
export abstract class HTTPService {
private static deepUndefinedToNull(o?: object): object | undefined {
if (Array.isArray(o)) return o;
if (o)
return Object.fromEntries(
Object.entries(o).map(([k, v]) => {
if (v === undefined) return [k, null];
if (typeof v === "object") return [k, this.deepUndefinedToNull(v)];
return [k, v];
})
);
}
public static async request<Y extends AnySchema>(
method: "GET" | "POST" | "PUT" | "DELETE",
url: string,
schema: Y,
options?: RequestOptions
) {
return await fetch(
`${process.env.NEXT_PUBLIC_BASE_PROTOCOL}://` +
`${process.env.NEXT_PUBLIC_BASE_DOMAIN}:` +
`${process.env.NEXT_PUBLIC_BASE_PORT}` +
`${process.env.NEXT_PUBLIC_API_PATTERN}${url}`,
{
method: method,
headers: {
accept: "application/json",
...((options?.stringify ?? true) != true
? {}
: { "Content-Type": "application/json" }),
...options?.headers,
},
body:
(options?.stringify ?? true) != true
? (options?.body as BodyInit)
: JSON.stringify(
this.deepUndefinedToNull(options?.body as object | undefined)
),
cache: options?.cache ?? options?.next ? undefined : "no-cache",
next: options?.next ?? {},
}
)
.then((r) => {
if (r && r.ok) return r;
else throw Error("Response ok = false");
})
.then((r) => r.json())
.then(async (d) => await schema.validate(d))
.catch((e) => {
console.error(e);
return null;
});
}
public static async get<Y extends AnySchema>(
url: string,
schema: Y,
options?: GetRequestOptions
) {
return await this.request<Y>("GET", url, schema, options);
}
public static async post<Y extends AnySchema>(
url: string,
schema: Y,
options?: RequestOptions
) {
return await this.request<Y>("POST", url, schema, options);
}
public static async put<Y extends AnySchema>(
url: string,
schema: Y,
options?: RequestOptions
) {
return await this.request<Y>("PUT", url, schema, options);
}
}

View File

@@ -0,0 +1,3 @@
import { HTTPService } from "./http";
export default HTTPService;

View File

@@ -0,0 +1,3 @@
import { ChatWindow } from "./window";
export default ChatWindow;

View File

@@ -0,0 +1,25 @@
import ColorSchemeSwitch from "@/features/colorSchemeSwitch";
import MessagesList from "@/features/messagesList";
import SendMessage from "@/features/sendMessage";
export const ChatWindow = () => {
return (
<div
className={
"w-lg h-full md:size-120 bg-bg1 p-4 rounded-lg " +
"drop-shadow-2xl flex flex-col gap-4 justify-between"
}
>
<MessagesList />
<div
className={
"w-full size-14 bg-bg2 p-2 rounded-lg drop-shadow-md " +
"flex flex-row gap-4 items-center justify-between"
}
>
<ColorSchemeSwitch />
<SendMessage />
</div>
</div>
);
};

View File

@@ -1,47 +0,0 @@
import type { Config } from "tailwindcss";
const config: Config = {
content: [
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
"./src/**/*.{js,ts,jsx,tsx,mdx}",
],
theme: {
extend: {
colors: {
bg0: "var(--color-bg0)",
bg1: "var(--color-bg1)",
bg4: "var(--color-bg4)",
fg0: "var(--color-fg0)",
fg1: "var(--color-fg1)",
fg4: "var(--color-fg4)",
ac0: "var(--color-ac0)",
ac1: "var(--color-ac1)",
ac2: "var(--color-ac2)",
err: "var(--color-err)",
},
animation: {
fadeIn: "fadeIn 0.25s ease-in-out",
fadeOut: "fadeOut 0.25s ease-in-out",
},
keyframes: () => ({
fadeIn: {
"0%": { opacity: "0" },
"100%": { opacity: "1" },
},
fadeOut: {
"0%": { opacity: "1" },
"100%": { opacity: "0" },
},
}),
},
screens: {
tb: "640px",
lp: "1024px",
dsk: "1280px",
},
},
plugins: [],
};
export default config;