├── Readme.md ├── backend ├── .dockerignore ├── .env ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── Dockerfile ├── README.md ├── nest-cli.json ├── package.json ├── prisma │ ├── migrations │ │ ├── 20230107004908_final │ │ │ └── migration.sql │ │ └── migration_lock.toml │ └── schema.prisma ├── run.sh ├── src │ ├── app.module.ts │ ├── auth │ │ ├── auth.controller.ts │ │ ├── auth.module.ts │ │ ├── auth.service.ts │ │ ├── dto │ │ │ ├── auth.dto.ts │ │ │ └── index.ts │ │ ├── twoFactor-Auth │ │ │ ├── dto │ │ │ │ └── 2faCode.dto.ts │ │ │ ├── twoFactorAuthentication.controller.ts │ │ │ ├── twoFactorAuthentication.module.ts │ │ │ ├── twoFactorAuthentication.service.ts │ │ │ └── utils │ │ │ │ └── check-jwt.strategy.ts │ │ └── utils │ │ │ ├── FortyTwoStrategy.ts │ │ │ ├── Guards.ts │ │ │ ├── http-exception.filter.ts │ │ │ └── jwt.strategy.ts │ ├── chat │ │ ├── chat.controller.ts │ │ ├── chat.gateway.ts │ │ ├── chat.module.ts │ │ ├── chat.service.ts │ │ └── dto │ │ │ ├── createRoom.dto.ts │ │ │ ├── joinRoom.dto.ts │ │ │ ├── muteuser.dto.ts │ │ │ ├── rid_uid.dto.ts │ │ │ └── roomid.dto.ts │ ├── game │ │ ├── data │ │ │ ├── board.ts │ │ │ └── rooms.ts │ │ ├── game.controller.ts │ │ ├── game.gateway.ts │ │ ├── game.module.ts │ │ └── game.service.ts │ ├── global-error-hanlder.ts │ ├── main.ts │ ├── prisma │ │ ├── prisma.module.ts │ │ └── prisma.service.ts │ └── users │ │ ├── dto │ │ ├── country.dto.ts │ │ ├── email.dto.ts │ │ ├── fullname.dto.ts │ │ ├── phonenumber.dto.ts │ │ └── username.dto.ts │ │ ├── users.controller.ts │ │ ├── users.gateway.ts │ │ ├── users.module.ts │ │ ├── users.service.ts │ │ └── utils │ │ ├── helper.ts │ │ ├── type.ts │ │ └── validate_country.ts ├── test │ ├── app.e2e-spec.ts │ └── jest-e2e.json ├── tsconfig.build.json ├── tsconfig.json └── yarn.lock ├── docker-compose.yml ├── frontend ├── .dockerignore ├── .env ├── .eslintrc.json ├── .gitignore ├── Dockerfile ├── README.md ├── middleware.ts ├── next.config.js ├── package.json ├── pages │ ├── 404.tsx │ ├── [profile] │ │ ├── blockList.tsx │ │ ├── friends.tsx │ │ ├── index.tsx │ │ ├── matchHistory.tsx │ │ └── settings.tsx │ ├── _app.tsx │ ├── api │ │ ├── logout.ts │ │ ├── twoFactor │ │ │ ├── authenticate.ts │ │ │ ├── turnOff.ts │ │ │ └── turnOn.ts │ │ └── updates │ │ │ ├── settings.ts │ │ │ └── username.ts │ ├── components.tsx │ ├── game │ │ ├── [id].tsx │ │ └── index.tsx │ ├── index.tsx │ ├── login │ │ ├── index.tsx │ │ ├── qrcode.tsx │ │ └── username.tsx │ └── messages │ │ ├── [id].tsx │ │ └── index.ts ├── public │ ├── fonts │ │ └── Poppins │ │ │ ├── Poppins-Bold.ttf │ │ │ ├── Poppins-Light.ttf │ │ │ ├── Poppins-Medium.ttf │ │ │ ├── Poppins-Regular.ttf │ │ │ └── Poppins-SemiBold.ttf │ ├── images │ │ ├── achievements │ │ │ ├── chart.svg │ │ │ ├── first.svg │ │ │ ├── hands.svg │ │ │ ├── midalia.svg │ │ │ ├── mountain.svg │ │ │ ├── pingpong.svg │ │ │ ├── rocket.svg │ │ │ ├── spider.svg │ │ │ └── unlock.svg │ │ ├── backgrounds │ │ │ ├── 404.svg │ │ │ ├── bg.svg │ │ │ ├── forest-back.svg │ │ │ ├── forest-front.svg │ │ │ ├── forest-mid.svg │ │ │ ├── gradient.jpg │ │ │ └── mountains.svg │ │ ├── brand │ │ │ ├── 42.svg │ │ │ ├── brand-lg.svg │ │ │ ├── brand.svg │ │ │ ├── favicon.ico │ │ │ ├── joroh.jpg │ │ │ └── logo.svg │ │ ├── icons │ │ │ ├── check.svg │ │ │ ├── close.svg │ │ │ ├── copy.svg │ │ │ ├── diamond.svg │ │ │ ├── dots.svg │ │ │ ├── friend.svg │ │ │ ├── gameOver.svg │ │ │ ├── losses.svg │ │ │ ├── plus.svg │ │ │ ├── refresh.svg │ │ │ ├── views.svg │ │ │ └── wins.svg │ │ ├── illustrations │ │ │ ├── desktop.svg │ │ │ └── winner.svg │ │ ├── levels │ │ │ ├── 3ankoob-lg.svg │ │ │ ├── 3ankoob-md.svg │ │ │ ├── 3ankoob-sm.svg │ │ │ ├── agent-lg.svg │ │ │ ├── agent-md.svg │ │ │ ├── agent-sm.svg │ │ │ ├── epic-lg.svg │ │ │ ├── epic-md.svg │ │ │ ├── epic-sm.svg │ │ │ ├── legend-lg.svg │ │ │ ├── legend-md.svg │ │ │ ├── legend-sm.svg │ │ │ ├── nadi-lg.svg │ │ │ ├── nadi-md.svg │ │ │ ├── nadi-sm.svg │ │ │ ├── newbie-lg.svg │ │ │ ├── newbie-md.svg │ │ │ └── newbie-sm.svg │ │ └── maps │ │ │ ├── 1337.svg │ │ │ ├── defaultMap.jpeg │ │ │ ├── map_2.jpg │ │ │ ├── map_3.jpg │ │ │ └── map_4.png │ └── sounds │ │ ├── 9bi7.mp3 │ │ ├── achievement.mp3 │ │ ├── d3if.mp3 │ │ └── message.mp3 ├── run.sh ├── sass │ ├── base │ │ ├── reset.sass │ │ └── typography.sass │ ├── components │ │ ├── button.sass │ │ ├── input.sass │ │ ├── modal.sass │ │ ├── ping-pong-animation.sass │ │ └── tooltip.sass │ ├── style.sass │ └── tools │ │ ├── helpers.sass │ │ ├── main.sass │ │ └── variables.sass ├── src │ ├── achievements.tsx │ ├── components │ │ ├── Achievements.tsx │ │ ├── AddUnfriend.tsx │ │ ├── Avatar.tsx │ │ ├── AvatarUpload.tsx │ │ ├── CallToAction.tsx │ │ ├── Canvas.tsx │ │ ├── Chats.tsx │ │ ├── Checkbox.tsx │ │ ├── DeleteAccount.tsx │ │ ├── Dropdown.tsx │ │ ├── Friends.tsx │ │ ├── GridEffect.tsx │ │ ├── GroupDefaultAvatar.tsx │ │ ├── Level.tsx │ │ ├── LiveGames │ │ │ ├── Card.tsx │ │ │ ├── index.tsx │ │ │ └── styles.ts │ │ ├── Map.tsx │ │ ├── MapData.tsx │ │ ├── NavbarSearch.tsx │ │ ├── PlayCard.tsx │ │ ├── Players.tsx │ │ ├── Rooms.tsx │ │ ├── SearchInput.tsx │ │ ├── TopPlayers.tsx │ │ ├── TwoFactor.tsx │ │ ├── boardPlayers.tsx │ │ ├── dropdowns │ │ │ ├── Notifications.tsx │ │ │ ├── Notifications │ │ │ │ └── Request.tsx │ │ │ ├── ParticipantsOptions.tsx │ │ │ ├── Profile.tsx │ │ │ └── UserOptions.tsx │ │ ├── modals │ │ │ ├── AchievementModal.tsx │ │ │ ├── BanUser.tsx │ │ │ ├── BlockUser.tsx │ │ │ ├── CancelFriendReq.tsx │ │ │ ├── CreateChatRoom.tsx │ │ │ ├── DeleteAccount.tsx │ │ │ ├── Feedback.tsx │ │ │ ├── GameOver.tsx │ │ │ ├── InvitePlayer.tsx │ │ │ ├── MuteUser.tsx │ │ │ ├── PlayGame.tsx │ │ │ ├── TwoFactor.tsx │ │ │ ├── Unfriend.tsx │ │ │ └── interface.ts │ │ └── toasts │ │ │ ├── FriendRequest.tsx │ │ │ ├── MessageToast.tsx │ │ │ └── PlayRequest.tsx │ ├── context │ │ ├── auth.tsx │ │ ├── chat.tsx │ │ ├── gameSocket.tsx │ │ └── socket.tsx │ ├── hooks │ │ ├── useBlockList.ts │ │ ├── useConversations.ts │ │ ├── useFriends.ts │ │ ├── useInvitePlayer.ts │ │ ├── useNotifications.ts │ │ ├── useOnClickOutside.ts │ │ ├── useProfile.ts │ │ ├── useRooms.ts │ │ └── useWindowSize.tsx │ ├── layout │ │ ├── Head.tsx │ │ ├── Modal.tsx │ │ ├── Navbar.tsx │ │ ├── messenger │ │ │ ├── ChatRoomParticipants.tsx │ │ │ ├── Conversations.tsx │ │ │ ├── Informations.tsx │ │ │ ├── Layout.tsx │ │ │ └── interfaces.ts │ │ └── profile │ │ │ ├── Layout.tsx │ │ │ └── Sidebar.tsx │ └── tools.ts ├── tsconfig.json └── yarn.lock └── screenshots ├── Create room modal.png ├── Enter username.png ├── Friends list.png ├── Game.png ├── Home.png ├── Match history.png ├── Profile.png ├── Settings.png └── chat.png /Readme.md: -------------------------------------------------------------------------------- 1 | # ft_transcendence 2 | 3 | Welcome to ft_transcendence, a real-time multiplayer online pong game, chat application, live games and more. 4 | 5 | ## Authors: 6 | 7 | * [hfadyl](https://github.com/hfadyl) 8 | * [Fatima Zahra Sarbout](https://github.com/fsarbout) 9 | * [Joroh](https://github.com/0xJoroh) 10 | 11 | ## Key Features: 12 | 13 | - Chat function with the ability to create public/private channels or channels protected by a password 14 | - Direct messaging between users and the option to block other users 15 | - User profiles visible through the chat interface 16 | - Real-time multiplayer online pong games with a matchmaking system 17 | - User accounts with OAuth integration, unique display names, avatar uploads, and two-factor authentication 18 | - Ability to add other users as friends and view their current status 19 | - Stats tracking, including victories, losses, and other stats 20 | 21 | ## Technologies Used: 22 | 23 | . Frontend: Next.js 24 | . Backend: Nest.js 25 | . Database: PostgreSQL 26 | . Containerization: Docker 27 | 28 | ## Conclusion: 29 | 30 | ft_transcendence is a fun and interactive way to play pong and chat with other users. We hope you enjoy using it as much as we enjoyed building it! 31 | If you have any questions or issues, please feel free to reach out to us on GitHub or contact us on any of this linkedin accounts: [hfadyl](https://www.linkedin.com/in/hicham-fadyl-6058b5198/), [Joroh](https://www.linkedin.com/in/0x10000/), [Fatima Zahra Sarbout](https://www.linkedin.com/in/fatima-zahra-sarbout/) 32 | 33 | ## Getting Started: 34 | 35 | - Clone the repository and navigate to the project directory: 36 | 37 | ``` 38 | git clone https://github.com/hfadyl/Transcendence-.git 39 | cd Transcendence- 40 | ``` 41 | 42 | - Do Not Forgot to add client Id and secret of your 42 app, and change IP to your machine IP in .env 43 | 44 | - Run the docker-compose command to start the application: 45 | 46 | ``` 47 | docker-compose up --build 48 | ``` 49 | 50 | ## Screenshots 51 | 52 | ![image](./screenshots/Enter%20username.png) 53 | 54 | ![image](./screenshots/Home.png) 55 | 56 | ![image](./screenshots/Profile.png) 57 | 58 | ![image](./screenshots/Friends%20list.png) 59 | 60 | ![image](./screenshots/Match%20history.png) 61 | 62 | ![image](./screenshots/Settings.png) 63 | 64 | ![image](./screenshots/Game.png) 65 | 66 | ![image](./screenshots/chat.png) 67 | -------------------------------------------------------------------------------- /backend/.dockerignore: -------------------------------------------------------------------------------- 1 | Dockerfile 2 | .dockerignore 3 | node_modules 4 | npm-debug.log 5 | dist 6 | -------------------------------------------------------------------------------- /backend/.env: -------------------------------------------------------------------------------- 1 | 2 | DATABASE_URL="postgresql://hfadyl:admin@postgress_db:5432/transcendence-db?schema=public" 3 | 4 | #You should pass your client Id and secret of your app in intranet 5 | clientID="" 6 | clientSecret="" 7 | 8 | #Change your machine IP 9 | IP_ADDRESS=localhost 10 | 11 | #do not forgot to pass callbackurl and change to your machine IP 12 | callbackURL=http://${IP_ADDRESS}:8000/api/auth/42/redirect 13 | route_upload=http://${IP_ADDRESS}:8000/api/users/pictures/ 14 | 15 | route_frontend=http://${IP_ADDRESS}:3000 16 | route_frontend_login=http://${IP_ADDRESS}:3000/login 17 | route_frontend_updateusername=http://${IP_ADDRESS}:3000/login/username 18 | route_qrcode=http://${IP_ADDRESS}:3000/login/qrcode 19 | 20 | #those stay like this 21 | jwt_secret=test 22 | checkjwt_secret=testing 23 | 24 | jwt_expiresIn='15d' 25 | 26 | TWO_FACTOR_AUTHENTICATION_APP_NAME=Transcendence 27 | -------------------------------------------------------------------------------- /backend/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | tsconfigRootDir : __dirname, 6 | sourceType: 'module', 7 | }, 8 | plugins: ['@typescript-eslint/eslint-plugin'], 9 | extends: [ 10 | 'plugin:@typescript-eslint/recommended', 11 | 'plugin:prettier/recommended', 12 | ], 13 | root: true, 14 | env: { 15 | node: true, 16 | jest: true, 17 | }, 18 | ignorePatterns: ['.eslintrc.js'], 19 | rules: { 20 | '@typescript-eslint/interface-name-prefix': 'off', 21 | '@typescript-eslint/explicit-function-return-type': 'off', 22 | '@typescript-eslint/explicit-module-boundary-types': 'off', 23 | '@typescript-eslint/no-explicit-any': 'off', 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | dist 3 | node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | pnpm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | lerna-debug.log* 13 | 14 | database 15 | 16 | # OS 17 | .DS_Store 18 | file.txt 19 | 20 | # Tests 21 | /coverage 22 | /.nyc_output 23 | 24 | # IDEs and editors 25 | /.idea 26 | .project 27 | .classpath 28 | .c9/ 29 | *.launch 30 | .settings/ 31 | *.sublime-workspace 32 | 33 | # IDE - VSCode 34 | .vscode/* 35 | !.vscode/settings.json 36 | !.vscode/tasks.json 37 | !.vscode/launch.json 38 | !.vscode/extensions.json 39 | 40 | /uploads/* -------------------------------------------------------------------------------- /backend/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /backend/Dockerfile: -------------------------------------------------------------------------------- 1 | # Base image 2 | FROM node:18-alpine 3 | 4 | # Create app directory 5 | WORKDIR /app 6 | COPY . /app 7 | 8 | # Install app dependencies 9 | RUN yarn install 10 | 11 | RUN yarn global add prisma 12 | 13 | 14 | EXPOSE 8000 15 | -------------------------------------------------------------------------------- /backend/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nest-cli", 3 | "collection": "@nestjs/schematics", 4 | "sourceRoot": "src" 5 | } 6 | -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "auth", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "", 6 | "private": true, 7 | "license": "UNLICENSED", 8 | "scripts": { 9 | "prisma:dev:deploy": "prisma migrate deploy", 10 | "db:dev:rm": "docker compose rm postgress_db -s -f -v", 11 | "db:dev:up": "docker compose up postgress_db -d", 12 | "db:dev:restart": "yarn run db:dev:rm && yarn run db:dev:up && sleep 2 && yarn run prisma:dev:deploy", 13 | "prebuild": "rimraf dist", 14 | "build": "nest build", 15 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 16 | "start": "nest start", 17 | "start:dev": "nest start --watch", 18 | "start:debug": "nest start --debug --watch", 19 | "start:prod": "node dist/main", 20 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 21 | "test": "jest", 22 | "test:watch": "jest --watch", 23 | "test:cov": "jest --coverage", 24 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 25 | "test:e2e": "jest --config ./test/jest-e2e.json" 26 | }, 27 | "dependencies": { 28 | "@nestjs/common": "^9.0.0", 29 | "@nestjs/config": "^2.2.0", 30 | "@nestjs/core": "^9.0.0", 31 | "@nestjs/jwt": "^9.0.0", 32 | "@nestjs/passport": "^9.0.0", 33 | "@nestjs/platform-express": "^9.0.0", 34 | "@nestjs/platform-socket.io": "^9.1.5", 35 | "@nestjs/swagger": "^6.1.2", 36 | "@nestjs/websockets": "^9.1.5", 37 | "@prisma/client": "^4.3.1", 38 | "argon2": "^0.30.2", 39 | "class-transformer": "^0.5.1", 40 | "class-validator": "^0.13.2", 41 | "cookie-parser": "^1.4.6", 42 | "express-session": "^1.17.3", 43 | "moment": "^2.29.4", 44 | "otplib": "^12.0.1", 45 | "passport": "^0.6.0", 46 | "passport-42": "^1.2.6", 47 | "passport-jwt": "^4.0.0", 48 | "qrcode": "^1.5.1", 49 | "reflect-metadata": "^0.1.13", 50 | "rimraf": "^3.0.2", 51 | "rxjs": "^7.2.0" 52 | }, 53 | "devDependencies": { 54 | "@nestjs/cli": "^9.0.0", 55 | "@nestjs/schematics": "^9.0.0", 56 | "@nestjs/testing": "^9.0.0", 57 | "@types/cookie-parser": "^1.4.3", 58 | "@types/express": "^4.17.13", 59 | "@types/express-session": "^1.17.5", 60 | "@types/jest": "28.1.4", 61 | "@types/multer": "^1.4.7", 62 | "@types/node": "^16.0.0", 63 | "@types/passport-jwt": "^3.0.6", 64 | "@types/supertest": "^2.0.11", 65 | "@typescript-eslint/eslint-plugin": "^5.0.0", 66 | "@typescript-eslint/parser": "^5.0.0", 67 | "eslint": "^8.0.1", 68 | "eslint-config-prettier": "^8.3.0", 69 | "eslint-plugin-prettier": "^4.0.0", 70 | "jest": "28.1.2", 71 | "prettier": "^2.3.2", 72 | "prisma": "^4.3.1", 73 | "source-map-support": "^0.5.20", 74 | "supertest": "^6.1.3", 75 | "ts-jest": "28.0.5", 76 | "ts-loader": "^9.2.3", 77 | "ts-node": "^10.0.0", 78 | "tsconfig-paths": "4.0.0", 79 | "typescript": "^4.3.5" 80 | }, 81 | "jest": { 82 | "moduleFileExtensions": [ 83 | "js", 84 | "json", 85 | "ts" 86 | ], 87 | "rootDir": "src", 88 | "testRegex": ".*\\.spec\\.ts$", 89 | "transform": { 90 | "^.+\\.(t|j)s$": "ts-jest" 91 | }, 92 | "collectCoverageFrom": [ 93 | "**/*.(t|j)s" 94 | ], 95 | "coverageDirectory": "../coverage", 96 | "testEnvironment": "node" 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /backend/prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "postgresql" -------------------------------------------------------------------------------- /backend/run.sh: -------------------------------------------------------------------------------- 1 | sleep 30 2 | 3 | prisma migrate deploy 4 | 5 | yarn start 6 | -------------------------------------------------------------------------------- /backend/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ConfigModule } from '@nestjs/config'; 3 | import { AuthModule } from './auth/auth.module'; 4 | import { PrismaModule } from './prisma/prisma.module'; 5 | import { twoFactorAuthentication } from './auth/twoFactor-Auth/twoFactorAuthentication.module'; 6 | import { UsersModule } from './users/users.module'; 7 | import { JwtService } from '@nestjs/jwt'; 8 | import { ChatModule } from './chat/chat.module'; 9 | import { GameModule } from './game/game.module'; 10 | import { UserGateway } from './users/users.gateway'; 11 | 12 | @Module({ 13 | imports: [ 14 | AuthModule, 15 | PrismaModule, 16 | ConfigModule.forRoot({ isGlobal: true }), 17 | twoFactorAuthentication, 18 | UsersModule, 19 | ChatModule, 20 | GameModule, 21 | ], 22 | controllers: [], 23 | providers: [JwtService], 24 | }) 25 | export class AppModule {} 26 | -------------------------------------------------------------------------------- /backend/src/auth/auth.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Controller, 3 | Get, 4 | UseGuards, 5 | Req, 6 | HttpStatus, 7 | Delete, 8 | UseFilters, 9 | } from '@nestjs/common'; 10 | import { Request } from 'express'; 11 | import { FortyTwoGuard } from './utils/Guards'; 12 | import { AuthGuard } from '@nestjs/passport'; 13 | import { UsersService } from 'src/users/users.service'; 14 | import { ConfigService } from '@nestjs/config'; 15 | import { HttpExceptionFilter } from './utils/http-exception.filter'; 16 | 17 | @Controller('auth') 18 | export class AuthController { 19 | constructor( 20 | private userservice: UsersService, 21 | private config: ConfigService, 22 | ) {} 23 | //Route: "http://localhost:8000/api/auth/42/login" to login with 42 24 | @UseGuards(FortyTwoGuard) 25 | @Get('42/login') 26 | handleLogin() { 27 | return; 28 | } 29 | 30 | //Route: "http://localhost:8000/api/auth/42/redirect" 42-passport redirect from login to this route, then it will redirect to the frontend 31 | @UseFilters(new HttpExceptionFilter()) 32 | @UseGuards(FortyTwoGuard) 33 | @Get('42/redirect') 34 | async handleRedirect(@Req() req: Request) { 35 | if (req.user['twofactor'] == true) { 36 | const checkCookie = await this.userservice.signCheckToken(req.user['id']); 37 | req.res.cookie('checkJwt', checkCookie, { path: '/', httpOnly: true }); 38 | req.res.redirect(this.config.get('route_qrcode')); 39 | } else { 40 | const cookie = await this.userservice.signToken(req.user['id']); 41 | req.res.cookie('jwt', cookie, { path: '/', httpOnly: true }); 42 | if (req.user['firstLogin'] == true) { 43 | req.res.redirect(this.config.get('route_frontend_updateusername')); 44 | } else { 45 | req.res.redirect(this.config.get('route_frontend')); 46 | } 47 | } 48 | return; 49 | } 50 | 51 | //Route: "http://localhost:8000/api/auth/42/logout" to logout and redirect to the frontend 52 | @UseGuards(AuthGuard('jwt')) 53 | @Delete('42/logout') 54 | async handleLogout(@Req() req: Request) { 55 | req.res.cookie('jwt', '', { path: '/', httpOnly: false }); 56 | req.res.redirect(this.config.get('route_frontend_login')); 57 | return; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /backend/src/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AuthController } from './auth.controller'; 3 | import { FortyTwoStrategy } from './utils/FortyTwoStrategy'; 4 | import { AuthService } from './auth.service'; 5 | import { JwtModule } from '@nestjs/jwt'; 6 | import { PassportModule, PassportSerializer } from '@nestjs/passport'; 7 | import { JwtStrategy } from './utils/jwt.strategy'; 8 | import { UsersModule } from 'src/users/users.module'; 9 | 10 | @Module({ 11 | imports: [ 12 | JwtModule.register({}), 13 | PassportModule.register({ defaultStrategy: '42' }), 14 | UsersModule, 15 | ], 16 | controllers: [AuthController], 17 | providers: [FortyTwoStrategy, AuthService, JwtStrategy], 18 | }) 19 | export class AuthModule {} 20 | -------------------------------------------------------------------------------- /backend/src/auth/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { PrismaService } from 'src/prisma/prisma.service'; 3 | import { AuthDto } from './dto'; 4 | 5 | @Injectable() 6 | export class AuthService { 7 | constructor(private prisma: PrismaService) {} 8 | async validateUser(details: AuthDto): Promise { 9 | const user = await this.prisma.user.findUnique({ 10 | where: { 11 | checkID: details.id, 12 | }, 13 | }); 14 | if (user) return { ...user, firstLogin: false }; 15 | const checkUserName = await this.prisma.user.findUnique({ 16 | where: { 17 | login: details.username, 18 | }, 19 | }); 20 | let data = { 21 | checkID: details.id, 22 | login: details.username, 23 | fullName: details.displayName, 24 | firstName: details.firstName, 25 | lastName: details.lastName, 26 | email: details.email, 27 | avatarUrl: details.avatarUrl, 28 | twoFactorAuthenticationSecret: '', 29 | }; 30 | if (checkUserName) { 31 | data = { 32 | ...data, 33 | login: details.username + '_', 34 | }; 35 | } 36 | 37 | const newUser = await this.prisma.user.create({ 38 | data, 39 | }); 40 | return { ...newUser, firstLogin: true }; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /backend/src/auth/dto/auth.dto.ts: -------------------------------------------------------------------------------- 1 | export interface AuthDto { 2 | id: string; 3 | username: string; 4 | displayName: string; 5 | lastName: string; 6 | firstName: string; 7 | email: string; 8 | avatarUrl: string; 9 | } 10 | -------------------------------------------------------------------------------- /backend/src/auth/dto/index.ts: -------------------------------------------------------------------------------- 1 | export * from './auth.dto'; -------------------------------------------------------------------------------- /backend/src/auth/twoFactor-Auth/dto/2faCode.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsString } from 'class-validator'; 2 | 3 | export class twofacCode { 4 | @IsString() 5 | readonly code: string; 6 | } 7 | -------------------------------------------------------------------------------- /backend/src/auth/twoFactor-Auth/twoFactorAuthentication.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Body, 3 | Controller, 4 | Get, 5 | HttpCode, 6 | Post, 7 | Req, 8 | Res, 9 | ForbiddenException, 10 | UseGuards, 11 | UsePipes, 12 | ValidationPipe, 13 | } from '@nestjs/common'; 14 | import { ConfigService } from '@nestjs/config'; 15 | import { AuthGuard } from '@nestjs/passport'; 16 | import { Request, Response } from 'express'; 17 | import { UsersService } from 'src/users/users.service'; 18 | import { twofacCode } from './dto/2faCode.dto'; 19 | import { TwoFactorAuthenticationService } from './twoFactorAuthentication.service'; 20 | 21 | @Controller('2fa') 22 | export class TwoFactorAuthenticationController { 23 | constructor( 24 | private readonly twofac: TwoFactorAuthenticationService, 25 | private readonly userService: UsersService, 26 | private config: ConfigService, 27 | ) {} 28 | //Route: "http://localhost:8000/api/2fa/generate" to generate the secret and the qr code 29 | @UseGuards(AuthGuard('jwt')) 30 | @Get('generate') 31 | async register(@Res() response: Response, @Req() request: Request) { 32 | const { otpauthUrl } = await this.twofac.generateSecret(request.user); 33 | return this.twofac.pipeQrCodeStream(response, otpauthUrl); 34 | } 35 | 36 | isNumber(str: string) { 37 | const pattern = /^\d+$/; 38 | return pattern.test(str); 39 | } 40 | 41 | validateData(code: string) { 42 | if (!code) { 43 | throw new ForbiddenException('Code is required'); 44 | } 45 | if (!this.isNumber(code)) { 46 | throw new ForbiddenException('Code must be a number'); 47 | } 48 | if (code.length !== 6) { 49 | throw new ForbiddenException('Code must be 6 digits'); 50 | } 51 | } 52 | 53 | //Route: "http://localhost:8000/api/2fa/turn-on" to validate the code and turn on the 2fa 54 | @UseGuards(AuthGuard('jwt')) 55 | @Post('turn-on') 56 | @HttpCode(200) 57 | async turnOnTwoFactorAuth(@Req() request, @Body() twofacCode: twofacCode) { 58 | this.validateData(twofacCode.code); 59 | const isCodeValid = this.twofac.isTwoFactorAuthenticationValid( 60 | twofacCode.code, 61 | request.user, 62 | ); 63 | if (!isCodeValid) { 64 | throw new ForbiddenException('Wrong Authentication Code'); 65 | } 66 | await this.userService.turnOnTwoFactorAuthentication(request.user.email); 67 | return { statusCode: 200, message: 'Authenticated' }; 68 | } 69 | 70 | //Route: "http://localhost:8000/api/2fa/authenticate" to validate the code and authenticate the user 71 | @UseGuards(AuthGuard('checkJwt')) 72 | @Post('authenticate') 73 | @HttpCode(200) 74 | async authenticate(@Req() request, @Body() twofacCode: twofacCode) { 75 | this.validateData(twofacCode.code); 76 | const isCodeValid = this.twofac.isTwoFactorAuthenticationValid( 77 | twofacCode.code, 78 | request.user, 79 | ); 80 | if (!isCodeValid) { 81 | throw new ForbiddenException('Wrong Authentication Code'); 82 | } 83 | const cookie = await this.userService.signToken(request.user['id']); 84 | return { statusCode: 200, message: 'Authenticated', jwt: cookie }; 85 | } 86 | 87 | //Route: "http://localhost:8000/api/2fa/disable2fa" to disable the 2fa 88 | @UseGuards(AuthGuard('jwt')) 89 | @Post('disable2fa') 90 | @HttpCode(200) 91 | async disable2fa(@Req() request) { 92 | await this.twofac.disableTwoFactorAuthentication(request.user); 93 | return { statusCode: 200, message: '2fa disabled' }; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /backend/src/auth/twoFactor-Auth/twoFactorAuthentication.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { UsersModule } from 'src/users/users.module'; 3 | import { TwoFactorAuthenticationController } from './twoFactorAuthentication.controller'; 4 | import { TwoFactorAuthenticationService } from './twoFactorAuthentication.service'; 5 | import { checkJwtStrategy } from './utils/check-jwt.strategy'; 6 | 7 | @Module({ 8 | imports: [UsersModule], 9 | controllers: [TwoFactorAuthenticationController], 10 | providers: [TwoFactorAuthenticationService, checkJwtStrategy], 11 | }) 12 | export class twoFactorAuthentication {} 13 | -------------------------------------------------------------------------------- /backend/src/auth/twoFactor-Auth/twoFactorAuthentication.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Req } from '@nestjs/common'; 2 | import { ConfigService } from '@nestjs/config'; 3 | import { Response } from 'express'; 4 | import { authenticator } from 'otplib'; 5 | import { PrismaService } from 'src/prisma/prisma.service'; 6 | import { toFileStream } from 'qrcode'; 7 | import { User } from '@prisma/client'; 8 | 9 | @Injectable() 10 | export class TwoFactorAuthenticationService { 11 | constructor(private prisma: PrismaService, private config: ConfigService) {} 12 | 13 | public async generateSecret(user: any) { 14 | const secret = authenticator.generateSecret(); 15 | const otpauthUrl = authenticator.keyuri( 16 | user.email, 17 | this.config.get('TWO_FACTOR_AUTHENTICATION_APP_NAME'), 18 | secret, 19 | ); 20 | await this.prisma.user.update({ 21 | where: { 22 | email: user.email, 23 | }, 24 | data: { 25 | twoFactorAuthenticationSecret: secret, 26 | }, 27 | }); 28 | return { secret, otpauthUrl }; 29 | } 30 | 31 | public async pipeQrCodeStream(stream: Response, otpauthUrl: string) { 32 | return toFileStream(stream, otpauthUrl); 33 | } 34 | 35 | public isTwoFactorAuthenticationValid( 36 | twoFactorAuthenticationCode: string, 37 | user: User, 38 | ) { 39 | return authenticator.verify({ 40 | token: twoFactorAuthenticationCode, 41 | secret: user['twoFactorAuthenticationSecret'], 42 | }); 43 | } 44 | 45 | public async disableTwoFactorAuthentication(user: User) { 46 | await this.prisma.user.update({ 47 | where: { 48 | email: user.email, 49 | }, 50 | data: { 51 | twoFactorAuthenticationSecret: '', 52 | twofactor: false, 53 | }, 54 | }); 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /backend/src/auth/twoFactor-Auth/utils/check-jwt.strategy.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { PassportStrategy } from '@nestjs/passport'; 3 | import { ExtractJwt, Strategy } from 'passport-jwt'; 4 | import { ConfigService } from '@nestjs/config'; 5 | import { Request } from 'express'; 6 | import { PrismaService } from 'src/prisma/prisma.service'; 7 | 8 | @Injectable() 9 | export class checkJwtStrategy extends PassportStrategy(Strategy, 'checkJwt') { 10 | constructor(config: ConfigService, private prisma: PrismaService) { 11 | super({ 12 | jwtFromRequest: ExtractJwt.fromExtractors([ 13 | (request: Request) => { 14 | // This is a hack to get the token from the cookie 15 | let data = request?.headers.cookie; 16 | if (data) { 17 | if (data.includes('jwt')) { 18 | data = data.split('=')[1]; 19 | return data; 20 | } 21 | } else { 22 | return null; 23 | } 24 | }, 25 | ]), 26 | secretOrKey: config.get('checkjwt_secret'), 27 | }); 28 | } 29 | 30 | async validate(payload: { sub: string }) { 31 | return this.prisma.user.findUnique({ 32 | where: { 33 | id: payload.sub, 34 | }, 35 | }); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /backend/src/auth/utils/FortyTwoStrategy.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { ConfigService } from '@nestjs/config'; 3 | import { PassportStrategy } from '@nestjs/passport'; 4 | import { Request, Response } from 'express'; 5 | import { Strategy, Profile } from 'passport-42'; 6 | import { AuthService } from '../auth.service'; 7 | 8 | @Injectable() 9 | export class FortyTwoStrategy extends PassportStrategy(Strategy, '42') { 10 | constructor(private authService: AuthService, private config: ConfigService) { 11 | super({ 12 | clientID: config.get('clientID'), 13 | clientSecret: config.get('clientSecret'), 14 | callbackURL: config.get('callbackURL'), 15 | }); 16 | } 17 | 18 | async validate( 19 | accesToken: string, 20 | refreshToken: string, 21 | profile: Profile, 22 | cb: any, 23 | ) { 24 | const user = await this.authService.validateUser({ 25 | id: profile.id, 26 | username: profile.username, 27 | displayName: profile.displayName, 28 | lastName: profile.name.familyName, 29 | firstName: profile.name.givenName, 30 | email: profile.emails[0].value, 31 | avatarUrl: profile._json.image.link, 32 | }); 33 | if (user) cb(null, user); 34 | else cb(null, false); 35 | // return user || null; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /backend/src/auth/utils/Guards.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { AuthGuard } from '@nestjs/passport'; 3 | 4 | @Injectable() 5 | export class FortyTwoGuard extends AuthGuard('42') { 6 | constructor() { 7 | super(); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /backend/src/auth/utils/http-exception.filter.ts: -------------------------------------------------------------------------------- 1 | import { Catch, HttpException } from '@nestjs/common'; 2 | 3 | @Catch(HttpException) 4 | export class HttpExceptionFilter { 5 | catch(exception, host) { 6 | const ctx = host.switchToHttp(); 7 | const response = ctx.getResponse(); 8 | const request = ctx.getRequest(); 9 | const status = exception.getStatus(); 10 | 11 | response.redirect(process.env.route_frontend); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /backend/src/auth/utils/jwt.strategy.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { PassportStrategy } from '@nestjs/passport'; 3 | import { ExtractJwt, Strategy } from 'passport-jwt'; 4 | import { ConfigService } from '@nestjs/config'; 5 | import { Request } from 'express'; 6 | import { PrismaService } from 'src/prisma/prisma.service'; 7 | 8 | @Injectable() 9 | export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') { 10 | constructor(config: ConfigService, private prisma: PrismaService) { 11 | super({ 12 | jwtFromRequest: ExtractJwt.fromExtractors([ 13 | (request: Request) => { 14 | // This is a hack to get the token from the cookie 15 | let data = request?.headers.cookie; 16 | if (data) { 17 | let token = data.split('; ').find((row) => row.startsWith('jwt=')); 18 | if (token) { 19 | token = token.split('=')[1]; 20 | return token; 21 | } 22 | } else { 23 | return null; 24 | } 25 | }, 26 | ]), 27 | secretOrKey: config.get('jwt_secret'), 28 | }); 29 | } 30 | 31 | async validate(payload: { sub: string }) { 32 | return this.prisma.user.findUnique({ 33 | where: { 34 | id: payload.sub, 35 | }, 36 | }); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /backend/src/chat/chat.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ChatService } from './chat.service'; 3 | import { ChatController } from './chat.controller'; 4 | import { ChatGateway } from './chat.gateway'; 5 | import { JwtService } from '@nestjs/jwt'; 6 | 7 | @Module({ 8 | providers: [ChatService, ChatGateway, JwtService], 9 | controllers: [ChatController], 10 | exports: [ChatGateway], 11 | }) 12 | export class ChatModule {} 13 | -------------------------------------------------------------------------------- /backend/src/chat/dto/createRoom.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty, IsOptional, IsString } from 'class-validator'; 2 | 3 | export class createRoom { 4 | @IsString() 5 | @IsNotEmpty() 6 | readonly name: string; 7 | 8 | @IsString() 9 | @IsOptional() 10 | readonly password?: string; 11 | 12 | @IsOptional() 13 | participants?: []; 14 | 15 | @IsString() 16 | @IsNotEmpty() 17 | readonly state: string; 18 | } 19 | -------------------------------------------------------------------------------- /backend/src/chat/dto/joinRoom.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty, IsOptional, IsString, IsUUID } from 'class-validator'; 2 | export class joinRoom { 3 | @IsString() 4 | @IsUUID() 5 | @IsNotEmpty() 6 | readonly roomId: string; 7 | 8 | @IsString() 9 | @IsOptional() 10 | readonly password?: string; 11 | } 12 | -------------------------------------------------------------------------------- /backend/src/chat/dto/muteuser.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty, IsString, IsUUID } from 'class-validator'; 2 | 3 | export class muteUser { 4 | @IsString() 5 | @IsNotEmpty() 6 | @IsUUID() 7 | readonly roomId: string; 8 | 9 | @IsString() 10 | @IsNotEmpty() 11 | @IsUUID() 12 | readonly userId: string; 13 | 14 | @IsString() 15 | @IsNotEmpty() 16 | readonly duration: string; 17 | } 18 | -------------------------------------------------------------------------------- /backend/src/chat/dto/rid_uid.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty, IsString, IsUUID } from 'class-validator'; 2 | 3 | export class rid_uid { 4 | @IsString() 5 | @IsNotEmpty() 6 | @IsUUID() 7 | readonly roomId: string; 8 | 9 | @IsString() 10 | @IsNotEmpty() 11 | @IsUUID() 12 | readonly userId: string; 13 | } 14 | -------------------------------------------------------------------------------- /backend/src/chat/dto/roomid.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty, IsString, IsUUID } from 'class-validator'; 2 | 3 | export class roomId { 4 | @IsString() 5 | @IsNotEmpty() 6 | @IsUUID() 7 | readonly roomId: string; 8 | } 9 | -------------------------------------------------------------------------------- /backend/src/game/data/board.ts: -------------------------------------------------------------------------------- 1 | export interface board { 2 | width: number; 3 | height: number; 4 | frontWidth: number; 5 | frontHeight: number; 6 | coefficient: number; 7 | paddleHeight: number; 8 | paddleWidth: number; 9 | ballRadius: number; 10 | ballSpeed_x: number; 11 | ballSpeed_y: number; 12 | ball_x: number; 13 | ball_y: number; 14 | player1_x: number; 15 | player1_y: number; 16 | player2_x: number; 17 | player2_y: number; 18 | leftScore: number; 19 | rightScore: number; 20 | player1points: number; 21 | player2points: number; 22 | views: number; 23 | } 24 | export interface Player { 25 | username: string; 26 | socketId: string; 27 | roomId?: string; 28 | playerType: string; 29 | avatar?: any; 30 | score?: number; 31 | points?: number; 32 | } 33 | export interface Viewer { 34 | username: string; 35 | viewerInterval?: any; 36 | socketId: string; 37 | } 38 | export interface Room { 39 | index: number; 40 | id: string; 41 | players: Player[]; 42 | board?: any; 43 | viewers: Viewer[]; 44 | interval?: any; 45 | reserved?: boolean; 46 | gameOn: boolean; 47 | maxViews: number; 48 | } 49 | -------------------------------------------------------------------------------- /backend/src/game/game.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Body, 3 | Controller, 4 | ForbiddenException, 5 | Get, 6 | Post, 7 | Req, 8 | UseGuards, 9 | } from '@nestjs/common'; 10 | import { GameService } from './game.service'; 11 | import { AuthGuard } from '@nestjs/passport'; 12 | 13 | @Controller('game') 14 | @UseGuards(AuthGuard('jwt')) 15 | export class GameController { 16 | constructor(private readonly gameService: GameService) {} 17 | 18 | validatename(name: string) { 19 | if (name.length < 4) 20 | throw new ForbiddenException('Name must be at least 4 characters long'); 21 | if (name.length > 20) 22 | throw new ForbiddenException('Name must be at most 20 characters long'); 23 | if (!/^[a-zA-Z0-9_-]+$/.test(name)) 24 | throw new ForbiddenException( 25 | 'Name must only contain letters and numbers', 26 | ); 27 | } 28 | 29 | @Get('getMatchHistory') 30 | async getMatchHistory(@Req() req) { 31 | if (!req.query.username) { 32 | throw new ForbiddenException('Username not provided'); 33 | } 34 | this.validatename(req.query.username); 35 | return await this.gameService.getMatchHistory(req.query.username); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /backend/src/game/game.module.ts: -------------------------------------------------------------------------------- 1 | import { UsersModule } from 'src/users/users.module'; 2 | import { Module } from '@nestjs/common'; 3 | import { GameService } from './game.service'; 4 | import { GameController } from './game.controller'; 5 | import { GameGateway } from './game.gateway'; 6 | 7 | @Module({ 8 | imports: [UsersModule], 9 | providers: [GameService, GameGateway], 10 | controllers: [GameController], 11 | }) 12 | export class GameModule {} 13 | -------------------------------------------------------------------------------- /backend/src/global-error-hanlder.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ExceptionFilter, 3 | Catch, 4 | ArgumentsHost, 5 | HttpException, 6 | HttpStatus, 7 | } from '@nestjs/common'; 8 | import { Request, Response } from 'express'; 9 | 10 | @Catch() 11 | export class GlobalErrorHanlder implements ExceptionFilter { 12 | catch(exception: unknown, host: ArgumentsHost) { 13 | const ctx = host.switchToHttp(); 14 | const response = ctx.getResponse(); 15 | const request = ctx.getRequest(); 16 | if (exception instanceof HttpException && exception.getStatus() !== 500) { 17 | const status = 18 | exception instanceof HttpException 19 | ? exception.getStatus() 20 | : HttpStatus.BAD_REQUEST; 21 | 22 | response.status(status).json({ 23 | statusCode: status, 24 | message: exception.message, 25 | }); 26 | } else { 27 | const status = 28 | exception instanceof HttpException 29 | ? exception.getStatus() 30 | : HttpStatus.BAD_REQUEST; 31 | 32 | response.status(status).json({ 33 | statusCode: status, 34 | timestamp: new Date().toISOString(), 35 | path: request.url, 36 | }); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /backend/src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { AppModule } from './app.module'; 3 | import * as cookieParser from 'cookie-parser'; 4 | import { NestExpressApplication } from '@nestjs/platform-express'; 5 | import { join } from 'path'; 6 | import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; 7 | import { ValidationPipe } from '@nestjs/common'; 8 | import { GlobalErrorHanlder } from './global-error-hanlder'; 9 | 10 | async function bootstrap() { 11 | const app = await NestFactory.create(AppModule); 12 | // Set the ValidationPipe as a global pipe to validate the request body or query parameters of all routes 13 | app.useGlobalPipes(new ValidationPipe({ transform: true })); 14 | // app.useStaticAssets(join(__dirname, '..', 'static')); 15 | app.setGlobalPrefix('api'); 16 | app.use(cookieParser()); 17 | 18 | // adds the swagger documentation to the backend 19 | const config = new DocumentBuilder() 20 | .setTitle('FT_TRANSCENDENCE') 21 | .setDescription('The API description of auth') 22 | .setVersion('1.0') 23 | .build(); 24 | const document = SwaggerModule.createDocument(app, config); 25 | SwaggerModule.setup('docs', app, document); 26 | app.enableCors({ 27 | origin: process.env.route_frontend, 28 | credentials: true, 29 | }); 30 | 31 | app.useGlobalFilters(new GlobalErrorHanlder()); 32 | await app.listen(8000); 33 | } 34 | bootstrap(); 35 | -------------------------------------------------------------------------------- /backend/src/prisma/prisma.module.ts: -------------------------------------------------------------------------------- 1 | import { Global, Module } from '@nestjs/common'; 2 | import { PrismaService } from './prisma.service'; 3 | 4 | @Global() 5 | @Module({ 6 | providers: [PrismaService], 7 | exports: [PrismaService], 8 | }) 9 | export class PrismaModule {} 10 | -------------------------------------------------------------------------------- /backend/src/prisma/prisma.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { ConfigService } from '@nestjs/config'; 3 | import { PrismaClient } from '@prisma/client'; 4 | 5 | @Injectable() 6 | export class PrismaService extends PrismaClient { 7 | constructor(private config: ConfigService) { 8 | super({ 9 | datasources: { 10 | db: { 11 | url: config.get('DATABASE_URL'), 12 | }, 13 | }, 14 | }); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /backend/src/users/dto/country.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty, IsString } from 'class-validator'; 2 | 3 | export class country { 4 | @IsString() 5 | @IsNotEmpty() 6 | country: string; 7 | } 8 | -------------------------------------------------------------------------------- /backend/src/users/dto/email.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsEmail, IsNotEmpty, IsString } from 'class-validator'; 2 | 3 | export class Email { 4 | @IsString() 5 | @IsNotEmpty() 6 | @IsEmail() 7 | email: string; 8 | } 9 | -------------------------------------------------------------------------------- /backend/src/users/dto/fullname.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty, IsString } from 'class-validator'; 2 | 3 | export class fullname { 4 | @IsString() 5 | @IsNotEmpty() 6 | fullName: string; 7 | } 8 | -------------------------------------------------------------------------------- /backend/src/users/dto/phonenumber.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty, IsPhoneNumber, IsString } from 'class-validator'; 2 | 3 | export class phonenumber { 4 | @IsString() 5 | @IsNotEmpty() 6 | phoneNumber: string; 7 | } 8 | -------------------------------------------------------------------------------- /backend/src/users/dto/username.dto.ts: -------------------------------------------------------------------------------- 1 | import { IsNotEmpty, IsString } from 'class-validator'; 2 | 3 | export class username { 4 | @IsString() 5 | @IsNotEmpty() 6 | username: string; 7 | } 8 | -------------------------------------------------------------------------------- /backend/src/users/users.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { JwtModule, JwtService } from '@nestjs/jwt'; 3 | import { UsersController } from './users.controller'; 4 | import { UsersService } from './users.service'; 5 | import { UserGateway } from './users.gateway'; 6 | import { MulterModule } from '@nestjs/platform-express'; 7 | import { ChatModule } from 'src/chat/chat.module'; 8 | 9 | @Module({ 10 | imports: [ 11 | JwtModule.register({}), 12 | MulterModule.register({ 13 | dest: './uploads', 14 | }), 15 | ChatModule, 16 | ], 17 | controllers: [UsersController], 18 | providers: [UsersService, UserGateway, JwtService], 19 | exports: [UsersService, UserGateway], 20 | }) 21 | export class UsersModule {} 22 | -------------------------------------------------------------------------------- /backend/src/users/utils/helper.ts: -------------------------------------------------------------------------------- 1 | import { ForbiddenException, Req } from '@nestjs/common'; 2 | import * as fs from 'fs'; 3 | 4 | export class Helper { 5 | static customFileName(@Req() req, file, cb) { 6 | if (file.originalname.length > 100) { 7 | return cb( 8 | new ForbiddenException( 9 | 'File Name Should Contain Only Letters, Numbers, Underscore, Dash and Dot', 10 | ), 11 | false, 12 | ); 13 | } 14 | let fileExtension = ''; 15 | if (file.mimetype.match(/\/(jpg|jpeg|png)$/)) { 16 | fileExtension = file.mimetype.split('/')[1]; 17 | } else { 18 | return cb( 19 | new ForbiddenException( 20 | 'We Support This types : [jpg, jpeg, png] for now!', 21 | ), 22 | false, 23 | ); 24 | } 25 | const originalName = file.originalname.split('.')[0]; 26 | cb(null, req.user['login'] + '.' + fileExtension); 27 | } 28 | 29 | static destinationPath(req, file, cb) { 30 | if (!fs.existsSync('./uploads')) { 31 | fs.mkdirSync('./uploads'); 32 | } 33 | if ( 34 | fs.existsSync( 35 | './uploads/' + req.user['login'] + '.' + file.mimetype.split('/')[1], 36 | ) 37 | ) { 38 | fs.unlink( 39 | './uploads/' + req.user['login'] + '.' + file.mimetype.split('/')[1], 40 | (err) => { 41 | if (err) { 42 | // console.log(err); 43 | throw new ForbiddenException('Error While Uploading File!'); 44 | } 45 | }, 46 | ); 47 | } 48 | cb(null, 'uploads/'); 49 | } 50 | 51 | static async checkFile(filename) { 52 | try { 53 | await fs.promises.access('./uploads/' + filename); 54 | return true; 55 | } catch (error) { 56 | return false; 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /backend/src/users/utils/type.ts: -------------------------------------------------------------------------------- 1 | import { Socket } from 'socket.io'; 2 | export interface activeUser { 3 | id: string; 4 | login: string; 5 | inGame: Boolean; 6 | socket: Socket; 7 | } 8 | 9 | export interface Notification { 10 | type: 'request' | 'message' | 'game'; 11 | reciver: string; 12 | reciverId: string; 13 | sender: string; 14 | senderId: string; 15 | message: string; 16 | image: string; 17 | createdAt: Date; 18 | seen: Boolean; 19 | id?: string; 20 | } 21 | -------------------------------------------------------------------------------- /backend/test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { INestApplication } from '@nestjs/common'; 3 | import * as request from 'supertest'; 4 | import { AppModule } from './../src/app.module'; 5 | 6 | describe('AppController (e2e)', () => { 7 | let app: INestApplication; 8 | 9 | beforeEach(async () => { 10 | const moduleFixture: TestingModule = await Test.createTestingModule({ 11 | imports: [AppModule], 12 | }).compile(); 13 | 14 | app = moduleFixture.createNestApplication(); 15 | await app.init(); 16 | }); 17 | 18 | it('/ (GET)', () => { 19 | return request(app.getHttpServer()) 20 | .get('/') 21 | .expect(200) 22 | .expect('Hello World!'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /backend/test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /backend/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /backend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "es2017", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true, 14 | "skipLibCheck": true, 15 | "strictNullChecks": false, 16 | "noImplicitAny": false, 17 | "strictBindCallApply": false, 18 | "forceConsistentCasingInFileNames": false, 19 | "noFallthroughCasesInSwitch": false 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | services: 3 | postgress_db: 4 | image: postgres 5 | networks: 6 | - database 7 | environment: 8 | POSTGRES_USER: hfadyl 9 | POSTGRES_PASSWORD: admin 10 | POSTGRES_DB: transcendence-db 11 | restart: always 12 | volumes: 13 | - ./backend/database/prod:/var/lib/postgresql/data 14 | container_name: postgress_db 15 | backend: 16 | build: 17 | context: ./backend 18 | dockerfile: Dockerfile 19 | networks: 20 | - database 21 | ports: 22 | - 8000:8000 23 | - 5555:5555 24 | # volumes: 25 | # - ./backend/auth:/usr/src/app 26 | depends_on: 27 | - postgress_db 28 | 29 | command: sh run.sh 30 | container_name: backend 31 | # restart: always 32 | frontend: 33 | build: 34 | context: ./frontend 35 | dockerfile: Dockerfile 36 | # networks: 37 | # - database 38 | ports: 39 | - 3000:3000 40 | volumes: 41 | - ./frontend:/usr/src/app 42 | depends_on: 43 | - backend 44 | command: sh run.sh 45 | container_name: frontend 46 | # restart: always 47 | 48 | networks: 49 | database: 50 | driver: bridge -------------------------------------------------------------------------------- /frontend/.dockerignore: -------------------------------------------------------------------------------- 1 | Dockerfile 2 | .dockerignore 3 | node_modules 4 | npm-debug.log 5 | 6 | -------------------------------------------------------------------------------- /frontend/.env: -------------------------------------------------------------------------------- 1 | 2 | JWT_SECRET= "test" 3 | CHECK_JWT_SECRET= "testing" 4 | 5 | #Change your machine IP 6 | IP_ADDRESS=localhost 7 | 8 | SERVER_URL=http://${IP_ADDRESS}:8000 9 | CLIENT_URL=http://${IP_ADDRESS}:3000 10 | 11 | AUTH=http://${IP_ADDRESS}:8000/api/auth/42 12 | USERS=http://${IP_ADDRESS}:8000/api/users 13 | CHAT=http://${IP_ADDRESS}:8000/api/chat 14 | GAME=http://${IP_ADDRESS}:8000/api/game 15 | TWO_FACTOR_AUTH=http://${IP_ADDRESS}:8000/api/2fa 16 | -------------------------------------------------------------------------------- /frontend/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | # Base image 2 | FROM node:18-alpine 3 | 4 | # Create app directory 5 | WORKDIR /app 6 | COPY . /app 7 | 8 | 9 | 10 | # Install app dependencies 11 | RUN yarn install 12 | RUN yarn add sharp 13 | 14 | 15 | EXPOSE 3000 16 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | ``` 12 | 13 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 14 | 15 | You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file. 16 | 17 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/avatar](http://localhost:3000/api/avatar). This endpoint can be edited in `pages/api/avatar.ts`. 18 | 19 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. 20 | 21 | ## Learn More 22 | 23 | To learn more about Next.js, take a look at the following resources: 24 | 25 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 26 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 27 | 28 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 29 | 30 | ## Deploy on Vercel 31 | 32 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 33 | 34 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 35 | -------------------------------------------------------------------------------- /frontend/middleware.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | import type { NextRequest } from "next/server"; 3 | import { jwtVerify } from "jose"; 4 | 5 | const pages = [ 6 | "/game", 7 | "/messages", 8 | "/friends", 9 | "/settings", 10 | "/blockList", 11 | "/matchHistory", 12 | "/login/username", 13 | ]; 14 | 15 | function allowedPages(name: string): boolean { 16 | if ("/" === name) return true; 17 | let result = false; 18 | pages.forEach((page) => { 19 | if (name.includes(page)) { 20 | result = true; 21 | return; 22 | } 23 | }); 24 | return result; 25 | } 26 | 27 | export default async function middleware(req: NextRequest) { 28 | const jwt = req.cookies.get("jwt"); 29 | const checkJwt = req.cookies.get("checkJwt"); 30 | 31 | if (req.nextUrl.pathname === "/login") { 32 | if (checkJwt) { 33 | try { 34 | await jwtVerify(checkJwt?.value, new TextEncoder().encode(process.env.CHECK_JWT_SECRET)); 35 | return NextResponse.redirect(`${process.env.CLIENT_URL}/login/qrcode`); 36 | } catch (error) { 37 | return NextResponse.next(); 38 | } 39 | } else if (jwt) { 40 | try { 41 | await jwtVerify(jwt?.value, new TextEncoder().encode(process.env.JWT_SECRET)); 42 | return NextResponse.redirect(`${process.env.CLIENT_URL}`); 43 | } catch (error) { 44 | return NextResponse.next(); 45 | } 46 | } 47 | } 48 | if (req.nextUrl.pathname === "/login/qrcode") { 49 | if (checkJwt) { 50 | try { 51 | await jwtVerify(checkJwt?.value, new TextEncoder().encode(process.env.CHECK_JWT_SECRET)); 52 | return NextResponse.next(); 53 | } catch (error) { 54 | return NextResponse.redirect(`${process.env.CLIENT_URL}/login`); 55 | } 56 | } else if (jwt) { 57 | try { 58 | await jwtVerify(jwt?.value, new TextEncoder().encode(process.env.JWT_SECRET)); 59 | return NextResponse.redirect(`${process.env.CLIENT_URL}`); 60 | } catch (error) { 61 | return NextResponse.next(); 62 | } 63 | } else return NextResponse.redirect(`${process.env.CLIENT_URL}/login`); 64 | } 65 | if (allowedPages(req.nextUrl.pathname)) { 66 | if (jwt === undefined) return NextResponse.redirect(`${process.env.CLIENT_URL}/login`); 67 | try { 68 | await jwtVerify(jwt?.value, new TextEncoder().encode(process.env.JWT_SECRET)); 69 | return NextResponse.next(); 70 | } catch (error) { 71 | return NextResponse.redirect(`${process.env.CLIENT_URL}/login`); 72 | } 73 | } 74 | return NextResponse.next(); 75 | } 76 | -------------------------------------------------------------------------------- /frontend/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: false, 4 | swcMinify: true, 5 | i18n: { 6 | locales: ["en"], 7 | defaultLocale: "en", 8 | }, 9 | compiler: { 10 | styledComponents: true, 11 | }, 12 | webpack(config) { 13 | config.module.rules.push({ 14 | test: /\.svg$/, 15 | use: ["@svgr/webpack"], 16 | }); 17 | return config; 18 | }, 19 | images: { 20 | domains: ["cdn.intra.42.fr", "localhost", process.env.IP_ADDRESS], 21 | }, 22 | env: { 23 | USERS: process.env.USERS, 24 | CHAT: process.env.CHAT, 25 | AUTH: process.env.AUTH, 26 | GAME: process.env.GAME, 27 | TWO_FACTOR_AUTH: process.env.TWO_FACTOR_AUTH, 28 | CLIENT_URL: process.env.CLIENT_URL, 29 | SERVER_URL: process.env.SERVER_URL, 30 | IP_ADDRESS: process.env.IP_ADDRESS, 31 | }, 32 | }; 33 | 34 | module.exports = nextConfig; 35 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "NODE_OPTIONS='--max-old-space-size=8192' next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@svgr/webpack": "^6.3.1", 13 | "@tippyjs/react": "^4.2.6", 14 | "atropos": "^1.0.2", 15 | "axios": "^0.27.2", 16 | "jose": "^4.9.2", 17 | "moment": "^2.29.4", 18 | "next": "^13.1.1", 19 | "nprogress": "^0.2.0", 20 | "p5": "^1.5.0", 21 | "react": "^18.2.0", 22 | "react-dom": "^18.2.0", 23 | "react-iconly": "^2.2.5", 24 | "react-modal": "^3.15.1", 25 | "react-p5": "^1.3.33", 26 | "react-toastify": "^9.0.8", 27 | "sass": "^1.54.8", 28 | "socket.io-client": "^4.5.3", 29 | "styled-components": "^5.3.5", 30 | "swiper": "^8.3.2", 31 | "use-sound": "^4.0.1" 32 | }, 33 | "devDependencies": { 34 | "@types/jsonwebtoken": "^8.5.9", 35 | "@types/node": "18.7.2", 36 | "@types/nprogress": "^0.2.0", 37 | "@types/react": "18.0.17", 38 | "@types/react-dom": "18.0.6", 39 | "@types/react-modal": "^3.13.1", 40 | "@types/styled-components": "^5.1.26", 41 | "eslint": "8.21.0", 42 | "eslint-config-next": "^13.1.1", 43 | "npm": "^8.19.1", 44 | "path": "^0.12.7", 45 | "typescript": "4.7.4" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /frontend/pages/404.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import Link from "next/link"; 3 | import { Home as HomeIcon } from "react-iconly"; 4 | import { PageWithNoLayout } from "@/pages/login"; 5 | 6 | import Head from "@/layout/Head"; 7 | import _404 from "@/images/backgrounds/404.svg"; 8 | 9 | const Home: PageWithNoLayout = () => { 10 | return ( 11 | <> 12 | 13 | 21 | 22 | ); 23 | }; 24 | 25 | export default Home; 26 | 27 | Home.noLayout = true; 28 | 29 | const Style = styled.main` 30 | display: flex; 31 | justify-content: center; 32 | align-items: center; 33 | flex-direction: column; 34 | gap: 2rem; 35 | `; 36 | -------------------------------------------------------------------------------- /frontend/pages/[profile]/blockList.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { ProfileLayout, ProfilePageWithLayout } from "@/layout/profile/Layout"; 3 | import { GetServerSideProps } from "next"; 4 | import { ShieldFail } from "react-iconly"; 5 | import Avatar from "@/components/Avatar"; 6 | import styled from "styled-components"; 7 | 8 | interface User { 9 | id: string; 10 | avatarUrl: string; 11 | login: string; 12 | } 13 | 14 | const BlockList: ProfilePageWithLayout = ({ list }: { list: User[] }) => { 15 | const [listUser, setListUser] = useState(list); 16 | 17 | const handleUnblock = async (id: string) => { 18 | await fetch(`${process.env.USERS}/unblockUser?id=${id}`, { 19 | method: "DELETE", 20 | credentials: "include", 21 | }).then(() => setListUser((prev) => prev.filter((user) => user.id !== id))); 22 | }; 23 | 24 | return ( 25 | <> 26 |

27 | 28 | Block List 29 |

30 |
31 | {listUser?.length === 0 &&

There is no user in your block list

} 32 | {listUser?.map((user, key) => ( 33 | 34 | 35 |

{user.login}

36 | 39 |
40 | ))} 41 |
42 | 43 | ); 44 | }; 45 | 46 | export default BlockList; 47 | 48 | BlockList.getLayout = (page) => ProfileLayout(page, "Block List"); 49 | 50 | const Row = styled.div` 51 | display: flex; 52 | align-items: center; 53 | padding: 10px; 54 | border-radius: 5px; 55 | :hover { 56 | background-color: var(--background-200); 57 | } 58 | > p { 59 | margin-left: 10px; 60 | } 61 | > button { 62 | margin-left: auto; 63 | } 64 | `; 65 | 66 | export const getServerSideProps: GetServerSideProps = async (context) => { 67 | const { jwt } = context.req.cookies; 68 | let list: User[] = []; 69 | 70 | await fetch(`${process.env.USERS}/getblockedUsers`, { 71 | method: "GET", 72 | headers: { 73 | "Content-Type": "application/json", 74 | Cookie: `jwt=${jwt}`, 75 | }, 76 | }) 77 | .then((res) => res.json()) 78 | .then((data) => { 79 | list = data; 80 | }); 81 | return { 82 | props: { 83 | list, 84 | }, 85 | }; 86 | }; 87 | -------------------------------------------------------------------------------- /frontend/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import { ReactElement, ReactNode } from "react"; 2 | import { ToastContainer } from "react-toastify"; 3 | import type { AppProps } from "next/app"; 4 | import NProgress from "nprogress"; 5 | import Router from "next/router"; 6 | import { NextPage } from "next"; 7 | 8 | import Navbar from "@/layout/Navbar"; 9 | import { AuthProvider } from "@/src/context/auth"; 10 | import { SocketProvider } from "@/src/context/socket"; 11 | import { GameSocketProvider } from "@/src/context/gameSocket"; 12 | import { ChatSocketProvider } from "@/src/context/chat"; 13 | 14 | import "nprogress/nprogress.css"; 15 | import "tippy.js/dist/tippy.css"; 16 | import "react-toastify/dist/ReactToastify.css"; 17 | import "swiper/css"; 18 | import "swiper/css/effect-cards"; 19 | import "../sass/style.sass"; 20 | 21 | //Binding events. 22 | Router.events.on("routeChangeStart", () => NProgress.start()); 23 | Router.events.on("routeChangeComplete", () => NProgress.done()); 24 | Router.events.on("routeChangeError", () => NProgress.done()); 25 | 26 | type NextPageWithLayout = NextPage & { 27 | getLayout?: (page: ReactElement) => ReactNode; 28 | noLayout?: boolean; 29 | }; 30 | type AppPropsWithLayout = AppProps & { 31 | Component: NextPageWithLayout; 32 | }; 33 | 34 | export default function App({ Component, pageProps }: AppPropsWithLayout) { 35 | if (Component.noLayout === true) return ; 36 | if (Component.getLayout) 37 | return Component.getLayout( 38 | <> 39 | 40 | 41 | 42 | 43 | ); 44 | return ( 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | ); 57 | } 58 | -------------------------------------------------------------------------------- /frontend/pages/api/logout.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from "next"; 2 | 3 | const handler = async (req: NextApiRequest, res: NextApiResponse) => { 4 | if (req.method === "DELETE") { 5 | res.setHeader( 6 | "Set-Cookie", 7 | Object.keys(req.cookies).map((key) => `${key}=; Path=/; Max-Age=0`) 8 | ); 9 | res.status(200).json({ message: "OK" }); 10 | } else res.status(405).json({ error: "Method not allowed" }); 11 | }; 12 | export default handler; 13 | -------------------------------------------------------------------------------- /frontend/pages/api/twoFactor/authenticate.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from "next"; 2 | 3 | export default async function handler(req: NextApiRequest, res: NextApiResponse) { 4 | if (req.method === "POST") { 5 | await fetch(`${process.env.TWO_FACTOR_AUTH}/authenticate`, { 6 | method: "POST", 7 | body: JSON.stringify({ code: req.body }), 8 | headers: { 9 | "Content-Type": "application/json", 10 | Cookie: `jwt=${req.cookies.checkJwt}`, 11 | }, 12 | }) 13 | .then((res) => res.json()) 14 | .then((data) => { 15 | if (data.statusCode === 200) 16 | res.setHeader("Set-Cookie", [ 17 | `checkJwt=; Path=/; HttpOnly; Max-Age=0`, 18 | `jwt=${data.jwt}; Path=/; HttpOnly; Max-Age=86400`, 19 | ]); 20 | res.status(data.statusCode).json({ message: data.message, statusCode: data.statusCode }); 21 | }) 22 | .catch((err) => { 23 | res.status(err.statusCode).json({ message: err.message, statusCode: err.statusCode }); 24 | }); 25 | } else res.status(405).json({ error: "Method not allowed" }); 26 | } 27 | -------------------------------------------------------------------------------- /frontend/pages/api/twoFactor/turnOff.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from "next"; 2 | 3 | export default async function handler(req: NextApiRequest, res: NextApiResponse) { 4 | if (req.method === "POST") { 5 | await fetch(`${process.env.TWO_FACTOR_AUTH}/disable2fa`, { 6 | method: "POST", 7 | headers: { 8 | "Content-Type": "application/json", 9 | Cookie: `jwt=${req.cookies.jwt}`, 10 | }, 11 | }) 12 | .then((res) => res.json()) 13 | .then((data) => { 14 | res.status(data.statusCode).json({ message: data.message, statusCode: data.statusCode }); 15 | }) 16 | .catch((err) => { 17 | res.status(err.statusCode).json({ message: err.message, statusCode: err.statusCode }); 18 | }); 19 | } else res.status(405).json({ error: "Method not allowed" }); 20 | } 21 | -------------------------------------------------------------------------------- /frontend/pages/api/twoFactor/turnOn.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from "next"; 2 | 3 | export default async function handler(req: NextApiRequest, res: NextApiResponse) { 4 | if (req.method === "POST") { 5 | await fetch(`${process.env.TWO_FACTOR_AUTH}/turn-on`, { 6 | method: "POST", 7 | body: JSON.stringify({ code: req.body }), 8 | headers: { 9 | "Content-Type": "application/json", 10 | Cookie: `jwt=${req.cookies.jwt}`, 11 | }, 12 | }) 13 | .then((res) => res.json()) 14 | .then((data) => { 15 | res.status(data.statusCode).json({ message: data.message, statusCode: data.statusCode }); 16 | }) 17 | .catch((err) => { 18 | res.status(err.statusCode).json({ message: err.message, statusCode: err.statusCode }); 19 | }); 20 | } else res.status(405).json({ error: "Method not allowed" }); 21 | } 22 | -------------------------------------------------------------------------------- /frontend/pages/api/updates/settings.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from "next"; 2 | import axios from "axios"; 3 | 4 | export default async function handler(req: NextApiRequest, res: NextApiResponse) { 5 | const { jwt } = req.cookies; 6 | const { field, value } = req.body; 7 | 8 | let route = ""; 9 | if (field === "username") route = "/updateUsername"; 10 | else if (field === "email") route = "/updateEmail"; 11 | else if (field === "fullName") route = "/updatefullName"; 12 | else if (field === "phoneNumber") route = "/updatePhoneNumber"; 13 | else if (field === "country") route = "/updateCountry"; 14 | 15 | if (req.method === "PUT" && field && value) { 16 | await axios 17 | .put( 18 | process.env.USERS + route, 19 | { [field]: value }, 20 | { 21 | headers: { 22 | "Content-Type": "application/json", 23 | Cookie: `jwt=${jwt}`, 24 | }, 25 | } 26 | ) 27 | .then((response) => { 28 | res.status(200).json(response.data); 29 | }) 30 | .catch((error) => { 31 | res.status(error.response.data.statusCode).json({ message: error.response.data.message }); 32 | }); 33 | } else res.status(405).json({ error: "Method not allowed" }); 34 | } 35 | -------------------------------------------------------------------------------- /frontend/pages/api/updates/username.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from "next"; 2 | import axios from "axios"; 3 | 4 | type Data = { 5 | message: string; 6 | }; 7 | 8 | export default async function handler(req: NextApiRequest, res: NextApiResponse) { 9 | const { jwt } = req.cookies; 10 | 11 | if (req.method === "PUT") { 12 | await axios 13 | .put( 14 | process.env.USERS + "/updateusername", 15 | { username: req.body.data }, 16 | { 17 | headers: { 18 | "Content-Type": "application/json", 19 | Cookie: `jwt=${jwt}`, 20 | }, 21 | } 22 | ) 23 | .then((response) => { 24 | res.status(200).json(response.data); 25 | }) 26 | .catch((error) => { 27 | res.status(error.response.data.statusCode).json({ message: error.response.data.message }); 28 | }); 29 | } else res.status(405).json({ message: "Method not allowed" }); 30 | } 31 | -------------------------------------------------------------------------------- /frontend/pages/components.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from "next"; 2 | 3 | import Head from "@/layout/Head"; 4 | import Navbar from "@/layout/Navbar"; 5 | import Avatar from "@/components/Avatar"; 6 | import Link from "next/link"; 7 | 8 | const Home: NextPage = () => { 9 | return ( 10 | <> 11 | 12 | 13 |
14 |

heading 1

15 |

heading 2

16 |

heading 3

17 |

heading 4

18 |
heading 5
19 |
heading 6
20 |

21 | Lorem ipsum dolor sit amet consectetur adipisicing elit. Sint nisi obcaecati vel rerum 22 | numquam pariatur fuga officiis molestias repellendus ducimus! 23 |

24 | link 25 | 26 | 27 | 28 | 29 |
30 | 31 | 32 | 33 |
34 | 35 |
36 | 37 | ); 38 | }; 39 | 40 | export default Home; 41 | -------------------------------------------------------------------------------- /frontend/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import type { NextPage } from "next"; 3 | import styled from "styled-components"; 4 | import { toast } from "react-toastify"; 5 | 6 | import Head from "@/layout/Head"; 7 | import LiveGames from "@/components/LiveGames"; 8 | import TopPlayers from "@/components/TopPlayers"; 9 | import Rooms from "@/components/Rooms"; 10 | import Friends from "@/components/Friends"; 11 | import CallToAction from "@/components/CallToAction"; 12 | import { useGame } from "@/src/context/gameSocket"; 13 | import AchievementModal from "@/components/modals/AchievementModal"; 14 | 15 | const Home: NextPage = () => { 16 | const { achievements, setAchievements } = useGame(); 17 | const [modal, setModal] = useState(false); 18 | 19 | useEffect(() => { 20 | if (achievements?.length > 0) setModal(true); 21 | return () => { 22 | setAchievements([]); 23 | }; 24 | }, []); 25 | 26 | return ( 27 | <> 28 | 29 | 44 | 45 | ); 46 | }; 47 | 48 | export default Home; 49 | 50 | const Style = styled.main` 51 | margin-bottom: 50px; 52 | .container { 53 | display: flex; 54 | flex-direction: column; 55 | gap: 50px; 56 | } 57 | `; 58 | -------------------------------------------------------------------------------- /frontend/pages/login/qrcode.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { Login } from "react-iconly"; 3 | import { useRouter } from "next/router"; 4 | import { PageWithNoLayout } from "@/pages/login"; 5 | 6 | import Head from "@/layout/Head"; 7 | import { Style } from "@/pages/login/username"; 8 | 9 | import Logo from "@/images/brand/brand.svg"; 10 | 11 | const QRCodeLogin: PageWithNoLayout = () => { 12 | const [code, setCode] = useState(""); 13 | const [error, setError] = useState(""); 14 | const router = useRouter(); 15 | 16 | const handleSubmit = async (e: React.FormEvent) => { 17 | e.preventDefault(); 18 | if (code.length < 6) { 19 | setError("Invalid code"); 20 | return; 21 | } 22 | await fetch("/api/twoFactor/authenticate", { 23 | method: "POST", 24 | body: code, 25 | }) 26 | .then((res) => res.json()) 27 | .then((data) => { 28 | if (data?.statusCode === 200) router.push("/").then(() => window.location.reload()); 29 | else setError(data.message); 30 | }) 31 | .catch((err) => setError(err.message)); 32 | }; 33 | return ( 34 | <> 35 | 36 | 61 | 62 | ); 63 | }; 64 | export default QRCodeLogin; 65 | 66 | QRCodeLogin.noLayout = true; 67 | -------------------------------------------------------------------------------- /frontend/pages/messages/index.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import type { NextPage } from "next"; 3 | import { useRouter } from "next/router"; 4 | import { toast } from "react-toastify"; 5 | import { MessagesLayout, MessagesPageWithLayout } from "@/layout/messenger/Layout"; 6 | import axios from "axios"; 7 | 8 | type MessagePageProps = MessagesPageWithLayout & NextPage; 9 | 10 | const Messenger: MessagePageProps = () => { 11 | const router = useRouter(); 12 | 13 | useEffect(() => { 14 | const getLastActiveRoom = async () => { 15 | try { 16 | await axios 17 | .get(`${process.env.CHAT}/getLastActiveRoom`, { 18 | withCredentials: true, 19 | }) 20 | .then(({ data }) => { 21 | if (data) router.push(`/messages/${data}`); 22 | else { 23 | toast.info("You have no active rooms"); 24 | setTimeout(() => { 25 | router.push("/"); 26 | }, 1000); 27 | } 28 | }) 29 | .catch(() => {}); 30 | } catch (err) {} 31 | }; 32 | getLastActiveRoom(); 33 | }, [router]); 34 | return null; 35 | }; 36 | export default Messenger; 37 | 38 | Messenger.getLayout = (page) => MessagesLayout(page); 39 | -------------------------------------------------------------------------------- /frontend/public/fonts/Poppins/Poppins-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hfadyl/ft_transcendence/b6a90753015261eaeefbf6e188a224db250792d9/frontend/public/fonts/Poppins/Poppins-Bold.ttf -------------------------------------------------------------------------------- /frontend/public/fonts/Poppins/Poppins-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hfadyl/ft_transcendence/b6a90753015261eaeefbf6e188a224db250792d9/frontend/public/fonts/Poppins/Poppins-Light.ttf -------------------------------------------------------------------------------- /frontend/public/fonts/Poppins/Poppins-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hfadyl/ft_transcendence/b6a90753015261eaeefbf6e188a224db250792d9/frontend/public/fonts/Poppins/Poppins-Medium.ttf -------------------------------------------------------------------------------- /frontend/public/fonts/Poppins/Poppins-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hfadyl/ft_transcendence/b6a90753015261eaeefbf6e188a224db250792d9/frontend/public/fonts/Poppins/Poppins-Regular.ttf -------------------------------------------------------------------------------- /frontend/public/fonts/Poppins/Poppins-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hfadyl/ft_transcendence/b6a90753015261eaeefbf6e188a224db250792d9/frontend/public/fonts/Poppins/Poppins-SemiBold.ttf -------------------------------------------------------------------------------- /frontend/public/images/achievements/chart.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /frontend/public/images/achievements/midalia.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /frontend/public/images/achievements/mountain.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /frontend/public/images/achievements/pingpong.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /frontend/public/images/achievements/unlock.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /frontend/public/images/backgrounds/gradient.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hfadyl/ft_transcendence/b6a90753015261eaeefbf6e188a224db250792d9/frontend/public/images/backgrounds/gradient.jpg -------------------------------------------------------------------------------- /frontend/public/images/brand/42.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /frontend/public/images/brand/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hfadyl/ft_transcendence/b6a90753015261eaeefbf6e188a224db250792d9/frontend/public/images/brand/favicon.ico -------------------------------------------------------------------------------- /frontend/public/images/brand/joroh.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hfadyl/ft_transcendence/b6a90753015261eaeefbf6e188a224db250792d9/frontend/public/images/brand/joroh.jpg -------------------------------------------------------------------------------- /frontend/public/images/brand/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /frontend/public/images/icons/check.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /frontend/public/images/icons/close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /frontend/public/images/icons/copy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /frontend/public/images/icons/diamond.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /frontend/public/images/icons/dots.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /frontend/public/images/icons/friend.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /frontend/public/images/icons/gameOver.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /frontend/public/images/icons/plus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /frontend/public/images/icons/refresh.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /frontend/public/images/icons/views.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /frontend/public/images/illustrations/winner.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /frontend/public/images/maps/1337.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /frontend/public/images/maps/defaultMap.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hfadyl/ft_transcendence/b6a90753015261eaeefbf6e188a224db250792d9/frontend/public/images/maps/defaultMap.jpeg -------------------------------------------------------------------------------- /frontend/public/images/maps/map_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hfadyl/ft_transcendence/b6a90753015261eaeefbf6e188a224db250792d9/frontend/public/images/maps/map_2.jpg -------------------------------------------------------------------------------- /frontend/public/images/maps/map_3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hfadyl/ft_transcendence/b6a90753015261eaeefbf6e188a224db250792d9/frontend/public/images/maps/map_3.jpg -------------------------------------------------------------------------------- /frontend/public/images/maps/map_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hfadyl/ft_transcendence/b6a90753015261eaeefbf6e188a224db250792d9/frontend/public/images/maps/map_4.png -------------------------------------------------------------------------------- /frontend/public/sounds/9bi7.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hfadyl/ft_transcendence/b6a90753015261eaeefbf6e188a224db250792d9/frontend/public/sounds/9bi7.mp3 -------------------------------------------------------------------------------- /frontend/public/sounds/achievement.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hfadyl/ft_transcendence/b6a90753015261eaeefbf6e188a224db250792d9/frontend/public/sounds/achievement.mp3 -------------------------------------------------------------------------------- /frontend/public/sounds/d3if.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hfadyl/ft_transcendence/b6a90753015261eaeefbf6e188a224db250792d9/frontend/public/sounds/d3if.mp3 -------------------------------------------------------------------------------- /frontend/public/sounds/message.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hfadyl/ft_transcendence/b6a90753015261eaeefbf6e188a224db250792d9/frontend/public/sounds/message.mp3 -------------------------------------------------------------------------------- /frontend/run.sh: -------------------------------------------------------------------------------- 1 | yarn build && yarn start -------------------------------------------------------------------------------- /frontend/sass/base/reset.sass: -------------------------------------------------------------------------------- 1 | *, 2 | *::before, 3 | *::after 4 | box-sizing: border-box // Use a more-intuitive box-sizing model. 5 | 6 | * 7 | margin: 0 8 | font: inherit 9 | 10 | html, 11 | body, 12 | #__next 13 | height: 100% // Allow percentage-based heights in the application 14 | 15 | html 16 | font-size: 100% // Correct text resizing oddly in IE 6/7 when body `font-size` is set using `em` units. 17 | -webkit-text-size-adjust: 100% // Prevent iOS text size adjust after orientation change, without disabling user zoom. 18 | -ms-text-size-adjust: 100% 19 | scroll-behavior: smooth 20 | 21 | body 22 | line-height: 1.5 // Add accessible line-height 23 | -webkit-font-smoothing: antialiased // Improve text rendering 24 | overflow-x: hidden 25 | 26 | img, 27 | picture, 28 | video, 29 | canvas, 30 | svg 31 | display: block 32 | max-width: 100% 33 | 34 | input, 35 | button, 36 | textarea, 37 | select 38 | font: inherit // Remove built-in form typography styles 39 | 40 | #__next 41 | isolation: isolate // Create a root stacking context 42 | 43 | a, 44 | a:visited 45 | cursor: pointer 46 | text-decoration: none 47 | display: inline-block 48 | transition: color 300ms ease-in-out 49 | &:active, 50 | &:hover 51 | transition: color 300ms ease-in-out 52 | &:focus-within 53 | outline: none 54 | box-shadow: 0 0 0 2px #fff 55 | 56 | svg * 57 | transition: stroke 300ms ease-in-out, fill 300ms ease-in-out 58 | &:hover * 59 | transition: stroke 300ms ease-in-out, fill 300ms ease-in-out 60 | 61 | button, .btn 62 | transition: background-color 300ms ease-in-out, color 300ms ease-in-out, transform 300ms ease-in-out, border-color 300ms ease-in-out 63 | &:hover 64 | transition: background-color 300ms ease-in-out, color 300ms ease-in-out, transform 300ms ease-in-out, border-color 300ms ease-in-out 65 | 66 | ul 67 | padding: 0 68 | li 69 | list-style: none 70 | 71 | label 72 | display: block 73 | margin-bottom: 10px 74 | -------------------------------------------------------------------------------- /frontend/sass/base/typography.sass: -------------------------------------------------------------------------------- 1 | $font-name: "Poppins" 2 | $font-weight: "Regular", "SemiBold", "Bold", "Medium", "Light" 3 | 4 | @if ($font-name != "" and nth($font-weight,1) != "") 5 | @each $i in $font-weight 6 | @font-face 7 | font-family: "#{$font-name nth($i,1)}" 8 | font-display: swap 9 | src: url("../../public/fonts/#{$font-name}/#{$font-name}-#{nth($i,1)}.ttf") format("truetype") 10 | 11 | body 12 | font-family: $font-regular 13 | font-stretch: normal 14 | font-style: normal 15 | font-weight: normal 16 | letter-spacing: normal 17 | font-size: 1rem 18 | 19 | h1, 20 | h2, 21 | h3, 22 | h4, 23 | h5, 24 | h6 25 | font-family: $font-medium 26 | font-weight: 500 27 | line-height: 1.5 28 | &:first-letter 29 | text-transform: uppercase 30 | 31 | h1 32 | font-size: 2rem 33 | 34 | h2 35 | font-size: 1.8rem 36 | 37 | h3 38 | font-size: 1.6rem 39 | 40 | h4 41 | font-size: 1.5rem 42 | 43 | h5 44 | font-weight: 500 45 | font-size: 1.2rem 46 | 47 | h6 48 | font-weight: 500 49 | font-size: 1rem 50 | 51 | p 52 | font-size: 1rem 53 | line-height: 1.4 54 | font-weight: 400 55 | &.error 56 | color: $danger 57 | &::first-letter 58 | text-transform: uppercase 59 | 60 | a 61 | font-family: $font-medium 62 | font-size: 1rem 63 | line-height: 1.4 64 | font-weight: 500 65 | color: $primary 66 | 67 | .btn, 68 | button 69 | font-family: var(--font-regular) 70 | font-size: 1rem 71 | line-height: 1.4 72 | font-weight: 400 73 | text-transform: capitalize 74 | &.lg 75 | font-family: $font-medium 76 | 77 | label 78 | line-height: 1.4 79 | font-weight: 400 80 | color: $text-100 81 | text-transform: capitalize 82 | 83 | input 84 | font-size: 1rem 85 | line-height: 1.4 86 | font-weight: 400 87 | 88 | b.time 89 | font-size: 12px 90 | color: $text-200 91 | font-weight: 400 92 | font-family: $font-medium 93 | -------------------------------------------------------------------------------- /frontend/sass/components/button.sass: -------------------------------------------------------------------------------- 1 | .btn, button 2 | display: inline-flex 3 | align-items: center 4 | justify-content: center 5 | text-align: center 6 | border-radius: 5px 7 | background-color: $primary 8 | color: $text-100 9 | padding: 6px 12px 10 | border: 1px solid $primary 11 | cursor: pointer 12 | text-decoration: none 13 | vertical-align: middle 14 | user-select: none 15 | white-space: nowrap 16 | gap: 10px 17 | span 18 | vertical-align: middle 19 | &:hover 20 | transform: translateY(-2px) 21 | &:active 22 | transform: translateY(0) 23 | transition: transform 100ms ease-in 24 | &:focus-within 25 | outline: none 26 | box-shadow: 0 0 0 2px $text-100 27 | &.lg 28 | padding: 8px 12px 29 | &.secondary 30 | background-color: $background-200 31 | border-color: $background-200 32 | color: $text-300 33 | &:hover 34 | background-color: $background-300 35 | border-color: $background-300 36 | &.outline 37 | background-color: $background-200 38 | border-color: $border 39 | &:hover 40 | background-color: $primary 41 | color: $text-100 42 | border-color: $primary 43 | 44 | &.danger 45 | background-color: $danger 46 | border-color: $danger 47 | &.outline 48 | background-color: transparent 49 | border-color: $text-200 50 | color: $text-200 51 | &:hover 52 | color: $danger 53 | border-color: $danger 54 | &.none 55 | background: none 56 | border: none 57 | padding: 0 58 | transition: none 59 | display: inline-block 60 | text-align: inherit 61 | border-radius: 0 62 | white-space: inherit 63 | &:hover 64 | transform: none 65 | &.icon 66 | @extend .none 67 | background-color: #342E59 68 | border-radius: 100px 69 | width: 30px 70 | height: 30px 71 | display: inline-flex 72 | align-items: center 73 | justify-content: center 74 | transition: background-color 300ms ease-in-out 75 | user-select: none 76 | &:hover 77 | transition: background-color 300ms ease-in-out 78 | background-color: $border 79 | &:active 80 | transition: background-color 100ms ease-in 81 | background-color: #342E59 82 | &:focus-within 83 | box-shadow: 0 0 0 2px $border 84 | &.primary 85 | background-color: $primary 86 | &.md 87 | width: 40px 88 | height: 40px 89 | &.disabled, &:disabled, &.disabled:hover, &:disabled:hover 90 | cursor: not-allowed 91 | user-select: none 92 | opacity: 0.5 93 | position: relative 94 | transform: none 95 | -------------------------------------------------------------------------------- /frontend/sass/components/input.sass: -------------------------------------------------------------------------------- 1 | input 2 | background-color: $background-300 3 | border: 1px solid $border 4 | border-radius: 5px 5 | color: $text-100 6 | padding: 10px 15px 7 | &::placeholder 8 | color: $text-300 9 | &:focus-within 10 | outline: none 11 | box-shadow: 0 0 0 2px $text-100 12 | &.error 13 | border: 1px solid $danger 14 | &[type="text"], &[type="email"], &[type="password"], &[type="number"] 15 | width: 100% 16 | &[type="search"] 17 | &::-webkit-search-decoration, 18 | &::-webkit-search-cancel-button, 19 | &::-webkit-search-results-button, 20 | &::-webkit-search-results-decoration 21 | -webkit-appearance: none 22 | display: none 23 | 24 | select 25 | background-color: $background-300 26 | background-image: url("data:image/svg+xml,%3Csvg width='12' height='10' viewBox='0 0 12 10' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M4.86724 0.214335L9.08645 7.06244C9.18046 7.21582 9.17146 7.41218 9.06045 7.55578C8.55943 8.2064 8.0514 8.78766 7.62438 9.17549C7.62438 9.17549 7.28236 9.50959 7.06535 9.65418C6.76934 9.88766 6.38232 10 6.0063 10C5.58428 10 5.18526 9.87691 4.86624 9.63171C4.80924 9.57602 4.55823 9.36403 4.35322 9.16377C3.07715 7.99539 0.969048 4.94549 0.330016 3.34141C0.227011 3.10793 0.0120006 2.48466 0 2.16131C0 1.84967 0.0680034 1.54879 0.217011 1.25962C0.422021 0.90403 0.740037 0.625612 1.11706 0.469307C1.37907 0.368686 2.16511 0.213358 2.18811 0.213358C2.74914 0.112736 3.53718 0.0375146 4.45722 0.00039219C4.62223 -0.00644615 4.78224 0.0765909 4.86724 0.214335Z' fill='%23EDE9F9'/%3E%3Cpath opacity='0.4' d='M7.13972 0.672521C6.95271 0.370657 7.19173 -0.00935965 7.55074 0.00529394C8.39279 0.0414395 9.13482 0.103961 9.68685 0.18016C9.69885 0.191883 10.6779 0.347211 11.0089 0.525985C11.6239 0.837618 12 1.44916 12 2.10662V2.16133C11.989 2.5853 11.6129 3.48699 11.5899 3.48699C11.4009 3.94125 11.0809 4.53423 10.6959 5.17215C10.5219 5.45936 10.0949 5.4662 9.91786 5.17899L7.13972 0.672521Z' fill='%23EDE9F9'/%3E%3C/svg%3E%0A") 27 | background-repeat: no-repeat 28 | background-position-x: 97% 29 | background-position-y: 50% 30 | border: 1px solid $border 31 | border-radius: 5px 32 | color: $text-100 33 | padding: 10px 15px 34 | width: 100% 35 | -moz-appearance: none 36 | -webkit-appearance: none 37 | appearance: none 38 | &:focus-within 39 | outline: none 40 | box-shadow: 0 0 0 2px $text-100 41 | &.error 42 | border: 1px solid $danger 43 | 44 | input[type="checkbox"] 45 | display: none 46 | &:checked + label 47 | .checkbox 48 | background-color: var(--primary) 49 | svg 50 | opacity: 1 51 | .checkbox 52 | width: 18px 53 | height: 18px 54 | border-radius: 5px 55 | background-color: var(--background-300) 56 | border: 1px solid $border 57 | display: flex 58 | align-items: center 59 | justify-content: center 60 | svg 61 | opacity: 0 62 | -------------------------------------------------------------------------------- /frontend/sass/components/modal.sass: -------------------------------------------------------------------------------- 1 | .ReactModal__Overlay 2 | z-index: 100 3 | background-color: rgb(0 0 0 / 50%) !important 4 | backdrop-filter: blur(4px) 5 | 6 | .ReactModal__Content 7 | position: absolute 8 | top: 50% 9 | left: 50% 10 | transform: translate(-50%, -50%) 11 | background-color: $background-100 12 | outline: 1px solid $border 13 | border-radius: 5px 14 | padding: 40px 30px 20px 15 | width: 55vw 16 | max-height: 97% 17 | max-width: 850px 18 | overflow: auto 19 | overflow-x: hidden 20 | @media (max-width: 992px) 21 | min-width: 90% 22 | .close 23 | position: absolute 24 | padding: 0 25 | top: 10px 26 | right: 10px 27 | cursor: pointer 28 | svg 29 | opacity: .3 30 | transition: opacity .2s 31 | &:hover 32 | svg 33 | opacity: 1 34 | transition: opacity .2s 35 | -------------------------------------------------------------------------------- /frontend/sass/components/tooltip.sass: -------------------------------------------------------------------------------- 1 | .tippy-content 2 | background-color: $border 3 | border-radius: 5px 4 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2) 5 | color: $text-100 6 | padding: 9px 10px 7 | .tippy-arrow 8 | color: $border 9 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2) 10 | -------------------------------------------------------------------------------- /frontend/sass/style.sass: -------------------------------------------------------------------------------- 1 | @import "./tools/variables" 2 | 3 | @import "./base/reset" 4 | @import "./base/typography" 5 | 6 | @import "./tools/helpers" 7 | @import "./tools/main" 8 | 9 | @import "./components/button" 10 | @import "./components/input" 11 | @import "./components/modal" 12 | @import "./components/tooltip" 13 | @import "./components/ping-pong-animation" 14 | -------------------------------------------------------------------------------- /frontend/sass/tools/helpers.sass: -------------------------------------------------------------------------------- 1 | .container, 2 | .container-fluid, 3 | .container-xxl, 4 | .container-xl, 5 | .container-lg, 6 | .container-md, 7 | .container-sm 8 | width: 100% 9 | padding-right: var(--bs-gutter-x, 0.75rem) 10 | padding-left: var(--bs-gutter-x, 0.75rem) 11 | margin-right: auto 12 | margin-left: auto 13 | 14 | @media (min-width: 576px) 15 | .container-sm, .container 16 | max-width: 540px 17 | 18 | @media (min-width: 768px) 19 | .container-md, .container-sm, .container 20 | max-width: 720px 21 | 22 | @media (min-width: 992px) 23 | .container-lg, .container-md, .container-sm, .container 24 | max-width: 960px 25 | 26 | @media (min-width: 1200px) 27 | .container-xl, .container-lg, .container-md, .container-sm, .container 28 | max-width: 1140px 29 | 30 | @media (min-width: 1400px) 31 | .container-xxl, .container-xl, .container-lg, .container-md, .container-sm, .container 32 | max-width: 1320px 33 | 34 | #nprogress .bar 35 | background-color: $primary !important 36 | 37 | .friend-request-toast 38 | margin: 0 39 | padding-left: 10px 40 | padding-right: 10px 41 | background-color: $background-200 42 | color: $text-100 43 | 44 | .toast 45 | padding: 0 46 | --toastify-color-light: $background-200 47 | 48 | .no-content 49 | font-size: 300% 50 | text-transform: uppercase 51 | font-weight: bold 52 | color: var(--text-300) 53 | border: 7px solid 54 | padding: 40px 20px 55 | text-align: center 56 | -------------------------------------------------------------------------------- /frontend/sass/tools/main.sass: -------------------------------------------------------------------------------- 1 | main, body, div, section, aside 2 | &::-webkit-scrollbar 3 | height: 7px 4 | width: 7px 5 | &-track 6 | background: $background-200 7 | &-thumb 8 | border-radius: 5px 9 | background: $primary 10 | &:hover 11 | background: $border 12 | #__next 13 | padding-top: 62.4px 14 | body 15 | background-color: $background-100 16 | color: $text-100 17 | main 18 | overflow-y: auto 19 | overflow-x: hidden 20 | height: 100% 21 | -------------------------------------------------------------------------------- /frontend/sass/tools/variables.sass: -------------------------------------------------------------------------------- 1 | :root 2 | --primary: #6647BF 3 | --danger: #F65164 4 | --active: #43FF83 5 | 6 | --text-100: #EDE9F9 7 | --text-200: #847E9D 8 | --text-300: #7B73AE 9 | 10 | --background-100: #2C254A 11 | --background-200: #272042 12 | --background-300: #332E59 13 | 14 | --gradient: linear-gradient(90deg, #4200FF 0%, #5E26FF 0.01%, #3104B5 100%) 15 | --border: #40386B 16 | --shadow: 0px 2px 5px rgba(0, 0, 0, 0.1) 17 | 18 | --font-light: 'Poppins Light', sans-serif 19 | --font-regular: 'Poppins Regular', sans-serif 20 | --font-medium: 'Poppins Medium', sans-serif 21 | --font-bold: 'Poppins Bold', sans-serif 22 | --font-semi-bold: 'Poppins SemiBold', sans-serif 23 | 24 | --toastify-color-error: #F65164 25 | 26 | $primary: var(--primary) 27 | $danger: var(--danger) 28 | $active: var(--active) 29 | 30 | $text-100: var(--text-100) 31 | $text-200: var(--text-200) 32 | $text-300: var(--text-300) 33 | 34 | $background-100: var(--background-100) 35 | $background-200: var(--background-200) 36 | $background-300: var(--background-300) 37 | 38 | $gradient: var(--gradient) 39 | $border: var(--border) 40 | $shadow: var(--shadow) 41 | 42 | $font-light: var(--font-light) 43 | $font-regular: var(--font-regular) 44 | $font-medium: var(--font-medium) 45 | $font-bold: var(--font-bold) 46 | $font-semi-bold: var(--font-semi-bold) 47 | -------------------------------------------------------------------------------- /frontend/src/achievements.tsx: -------------------------------------------------------------------------------- 1 | import A1 from "@/images/achievements/hands.svg"; 2 | import A2 from "@/images/achievements/mountain.svg"; 3 | import A3 from "@/images/achievements/first.svg"; 4 | import A4 from "@/images/achievements/unlock.svg"; 5 | import A5 from "@/images/achievements/chart.svg"; 6 | import A6 from "@/images/achievements/rocket.svg"; 7 | import A7 from "@/images/achievements/midalia.svg"; 8 | import A8 from "@/images/achievements/spider.svg"; 9 | 10 | export const achievementsList = [ 11 | { 12 | title: "3 Matches", 13 | desc: "win the matches in a row", 14 | img: , 15 | }, 16 | { 17 | title: "5 Matches", 18 | desc: "win the matches in a row", 19 | img: , 20 | }, 21 | { 22 | title: "First match", 23 | desc: "win the first match ever", 24 | img: , 25 | }, 26 | { 27 | title: "Unlock level 1", 28 | desc: "You become Naaadi", 29 | img: , 30 | }, 31 | { 32 | title: "5 Total wins", 33 | desc: "pretty simple achivements", 34 | img: , 35 | }, 36 | { 37 | title: "Clean sheet", 38 | desc: "win with top score", 39 | img: , 40 | }, 41 | { 42 | title: "15 games", 43 | desc: "total played games", 44 | img: , 45 | }, 46 | { 47 | title: "Leader crush", 48 | desc: "you beat a Pong Champ team leader", 49 | img: , 50 | }, 51 | ]; 52 | -------------------------------------------------------------------------------- /frontend/src/components/Achievements.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import GridEffect from "@/components/GridEffect"; 3 | import styled from "styled-components"; 4 | import { achievementsList } from "@/src/achievements"; 5 | 6 | interface Props { 7 | achievements: string[]; 8 | } 9 | 10 | const Achievements: FC = ({ achievements }) => { 11 | return ( 12 |
13 |

14 | Achievements {achievements?.length}/9 15 |

16 | 17 | {achievementsList?.map((achievement, index) => ( 18 |
19 |
20 | 23 | {achievement.img} 24 |

{achievement.title}

25 |

{achievement.desc}

26 |
27 |
28 |
29 | ))} 30 |
31 |
32 | ); 33 | }; 34 | 35 | export default Achievements; 36 | 37 | export const CardStyle = styled.div` 38 | display: flex; 39 | flex-direction: column; 40 | align-items: center; 41 | justify-content: center; 42 | flex: 1; 43 | text-align: center; 44 | h4 { 45 | margin-top: 25px; 46 | margin-bottom: 0; 47 | text-transform: uppercase; 48 | } 49 | p { 50 | color: var(--text-300); 51 | max-width: 240px; 52 | margin-top: 5px; 53 | } 54 | 55 | > * { 56 | opacity: 0.5; 57 | } 58 | svg { 59 | mix-blend-mode: luminosity; 60 | } 61 | &.unlock { 62 | > * { 63 | opacity: 1; 64 | } 65 | svg { 66 | mix-blend-mode: normal; 67 | } 68 | } 69 | `; 70 | -------------------------------------------------------------------------------- /frontend/src/components/Avatar.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import styled from "styled-components"; 3 | import GroupDefaultAvatar from "@/components/GroupDefaultAvatar"; 4 | import Link from "next/link"; 5 | import Image from "next/image"; 6 | import { useSocket } from "@/src/context/socket"; 7 | 8 | interface AvatarProps { 9 | src?: string | undefined; 10 | size?: number; 11 | alt?: string; 12 | radius?: number; 13 | username?: string; 14 | link?: boolean; 15 | } 16 | 17 | interface StyleProps { 18 | size: number; 19 | radius: number; 20 | status?: "online" | "offline" | "inGame"; 21 | } 22 | 23 | const Avatar: FC = ({ 24 | src = undefined, 25 | alt = "", 26 | size = 50, 27 | radius = size, 28 | username, 29 | link = true, 30 | }) => { 31 | const { getStatus } = useSocket(); 32 | 33 | return !src ? ( 34 | 35 | ) : ( 36 | 50 | ); 51 | }; 52 | export default Avatar; 53 | 54 | const Style = styled.figure` 55 | display: inline-block; 56 | width: ${(p) => p.size}px; 57 | height: ${(p) => p.size}px; 58 | min-width: ${(p) => p.size}px; 59 | max-width: ${(p) => p.size}px; 60 | min-height: ${(p) => p.size}px; 61 | max-height: ${(p) => p.size}px; 62 | border-radius: ${(p) => p.radius}px; 63 | background-color: #111331; 64 | position: relative; 65 | &::after { 66 | content: ""; 67 | position: absolute; 68 | width: ${(p) => (p.size > 70 ? "14" : "12")}px; 69 | height: ${(p) => (p.size > 70 ? "14" : "12")}px; 70 | border-radius: 50%; 71 | bottom: ${(p) => (p.size > 70 ? "5" : "0")}px; 72 | right: ${(p) => (p.size > 70 ? "5" : "0")}px; 73 | 74 | ${({ status }) => status && "border: 2px solid #111331"}; 75 | background-color: ${({ status }) => { 76 | switch (status) { 77 | case "online": 78 | return "#43FF83"; 79 | case "inGame": 80 | return "#E17A00"; 81 | case "offline": 82 | return "#847E9D"; 83 | default: 84 | return "transparent"; 85 | } 86 | }}; 87 | } 88 | 89 | img { 90 | height: 100%; 91 | width: 100%; 92 | object-fit: cover; 93 | border-radius: ${(p) => p.radius}px; 94 | } 95 | a { 96 | height: 100%; 97 | width: 100%; 98 | :focus-within { 99 | box-shadow: none; 100 | } 101 | } 102 | `; 103 | -------------------------------------------------------------------------------- /frontend/src/components/Checkbox.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import styled from "styled-components"; 3 | import CheckIcon from "@/images/icons/check.svg"; 4 | 5 | interface Props { 6 | name: string; 7 | checked: boolean; 8 | setState?: React.Dispatch>; 9 | children?: React.ReactNode; 10 | id: string; 11 | onClick?: () => void; 12 | } 13 | 14 | const Checkbox: FC = ({ name, checked, setState, children, id, onClick }) => { 15 | return ( 16 | 32 | ); 33 | }; 34 | 35 | export default Checkbox; 36 | 37 | const Style = styled.div` 38 | input[type="checkbox"] { 39 | display: none; 40 | } 41 | label { 42 | cursor: pointer; 43 | margin-bottom: 0; 44 | display: flex; 45 | width: 100%; 46 | align-items: center; 47 | gap: 5px; 48 | } 49 | input[type="checkbox"]:checked + label { 50 | div svg { 51 | opacity: 1; 52 | } 53 | } 54 | 55 | label > div { 56 | width: 18px; 57 | height: 18px; 58 | border-radius: 5px; 59 | background-color: var(--background-200); 60 | display: flex; 61 | align-items: center; 62 | justify-content: center; 63 | svg { 64 | opacity: 0; 65 | } 66 | } 67 | `; 68 | -------------------------------------------------------------------------------- /frontend/src/components/DeleteAccount.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import Modal from "react-modal"; 3 | import DeleteAccountModal from "@/components/modals/DeleteAccount"; 4 | import { Delete } from "react-iconly"; 5 | 6 | Modal.setAppElement("#__next"); 7 | 8 | const DeleteAccount = () => { 9 | const [deleteAccountModal, setDeleteAccountModal] = useState(false); 10 | 11 | return ( 12 | <> 13 |
14 | 15 | 19 |
20 | 25 | 26 | ); 27 | }; 28 | 29 | export default DeleteAccount; 30 | -------------------------------------------------------------------------------- /frontend/src/components/Dropdown.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useState } from "react"; 2 | import styled from "styled-components"; 3 | 4 | interface Props { 5 | children: React.ReactNode; 6 | state: boolean; 7 | target: React.ReactNode; 8 | useRef?: React.RefObject; 9 | } 10 | const Dropdown: FC = ({ children, state, target, useRef }) => { 11 | return ( 12 | 16 | ); 17 | }; 18 | 19 | export default Dropdown; 20 | 21 | const Style = styled.div<{ isActive: boolean }>` 22 | position: relative; 23 | > *:nth-child(2) { 24 | ${({ isActive }) => (isActive ? "display: block" : "display: none")}; 25 | position: absolute; 26 | top: calc(100% + 10px); 27 | right: 0; 28 | background-color: var(--background-200); 29 | border-radius: 5px; 30 | box-shadow: 0 0 10px rgba(0, 0, 0, 0.2); 31 | z-index: 10; 32 | padding: 8px; 33 | width: 230px; 34 | max-height: 80vh; 35 | overflow: auto; 36 | 37 | hr { 38 | margin: 8px 0; 39 | border: none; 40 | border-top: 1px solid var(--border); 41 | } 42 | 43 | .item { 44 | display: flex; 45 | align-items: center; 46 | justify-content: flex-start; 47 | padding: 8px; 48 | font-family: var(--font-regular); 49 | width: 100%; 50 | color: var(--text-100); 51 | gap: 10px; 52 | font-weight: 400; 53 | border-radius: 5px !important; 54 | height: auto !important; 55 | cursor: pointer; 56 | 57 | &:hover { 58 | background-color: var(--background-100); 59 | } 60 | &:focus { 61 | outline: 2px solid; 62 | outline-offset: -2px; 63 | box-shadow: none; 64 | } 65 | 66 | &.disabled { 67 | ::after { 68 | display: none; 69 | } 70 | } 71 | 72 | &.danger { 73 | :hover { 74 | background-color: var(--danger); 75 | color: var(--text-100); 76 | span { 77 | color: var(--text-100); 78 | } 79 | } 80 | } 81 | } 82 | 83 | .flex { 84 | display: flex; 85 | align-items: center; 86 | &.between { 87 | justify-content: space-between; 88 | } 89 | } 90 | } 91 | `; 92 | -------------------------------------------------------------------------------- /frontend/src/components/Friends.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import Avatar from "@/components/Avatar"; 3 | import Tippy from "@tippyjs/react"; 4 | import Link from "next/link"; 5 | import { useFriends } from "@/hooks/useFriends"; 6 | 7 | interface FriendsProp { 8 | id: string; 9 | username: string; 10 | avatar: string; 11 | } 12 | 13 | const Friends = () => { 14 | const friends = useFriends(); 15 | return ( 16 | 33 | ); 34 | }; 35 | export default Friends; 36 | 37 | const Style = styled.div` 38 | position: fixed; 39 | bottom: 0; 40 | right: 0; 41 | z-index: 9; 42 | overflow: auto; 43 | background-color: #2c254aca; 44 | border-top-left-radius: 10px; 45 | border-top-right-radius: 10px; 46 | backdrop-filter: blur(3px); 47 | padding: 14px 8px; 48 | display: flex; 49 | flex-direction: column; 50 | align-items: center; 51 | justify-content: flex-end; 52 | gap: 5px; 53 | 54 | height: 60px; 55 | transition: height 0.3s ease-in-out; 56 | 57 | :hover { 58 | height: 100%; 59 | transition: height 0.5s ease-in-out; 60 | } 61 | `; 62 | -------------------------------------------------------------------------------- /frontend/src/components/GridEffect.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useRef, useEffect } from "react"; 2 | import styled from "styled-components"; 3 | 4 | interface Props { 5 | children: React.ReactNode; 6 | } 7 | 8 | const GridEffect: FC = ({ children }) => { 9 | const cardsRef = useRef(null); 10 | 11 | useEffect(() => { 12 | const currentCardsRef = cardsRef.current; 13 | const handleMouseMove = (e: MouseEvent) => { 14 | const cards = currentCardsRef?.getElementsByClassName("card"); 15 | if (cards) { 16 | Array.from(cards).forEach((card) => { 17 | if (card instanceof HTMLElement) { 18 | const rect = card.getBoundingClientRect(), 19 | x = e.clientX - rect.left, 20 | y = e.clientY - rect.top; 21 | 22 | card.style.setProperty("--mouse-x", `${x}px`); 23 | card.style.setProperty("--mouse-y", `${y}px`); 24 | } 25 | }); 26 | } 27 | }; 28 | currentCardsRef?.addEventListener("mousemove", handleMouseMove); 29 | return () => { 30 | currentCardsRef?.removeEventListener("mousemove", handleMouseMove); 31 | }; 32 | }, []); 33 | 34 | return ; 35 | }; 36 | 37 | export default GridEffect; 38 | 39 | const Style = styled.div` 40 | display: grid; 41 | grid-template-columns: repeat(3, 1fr); 42 | gap: 10px; 43 | @media (max-width: 1270px) { 44 | grid-template-columns: repeat(2, 1fr); 45 | } 46 | @media (max-width: 768px) { 47 | grid-template-columns: repeat(1, 1fr); 48 | } 49 | 50 | :hover > .card::after { 51 | opacity: 1; 52 | } 53 | 54 | .card { 55 | background-color: rgba(255, 255, 255, 0.1); 56 | border-radius: 10px; 57 | cursor: pointer; 58 | display: flex; 59 | height: 298px; 60 | flex-direction: column; 61 | position: relative; 62 | min-width: 260px; 63 | overflow: hidden; 64 | } 65 | 66 | .card:hover::before { 67 | opacity: 1; 68 | } 69 | 70 | .card::before, 71 | .card::after { 72 | border-radius: inherit; 73 | content: ""; 74 | height: 100%; 75 | left: 0px; 76 | opacity: 0; 77 | position: absolute; 78 | top: 0px; 79 | transition: opacity 500ms; 80 | width: 100%; 81 | pointer-events: none; 82 | user-select: none; 83 | } 84 | 85 | .card::before { 86 | background: radial-gradient( 87 | 800px circle at var(--mouse-x) var(--mouse-y), 88 | rgba(255, 255, 255, 0.03), 89 | transparent 40% 90 | ); 91 | z-index: 3; 92 | } 93 | 94 | .card::after { 95 | background: radial-gradient( 96 | 600px circle at var(--mouse-x) var(--mouse-y), 97 | rgba(255, 255, 255, 0.4), 98 | transparent 40% 99 | ); 100 | z-index: 1; 101 | } 102 | 103 | .card > .card-content { 104 | background-color: var(--background-200); 105 | border-radius: inherit; 106 | display: flex; 107 | flex-direction: column; 108 | flex-grow: 1; 109 | inset: 1px; 110 | padding: 10px; 111 | position: absolute; 112 | z-index: 2; 113 | } 114 | `; 115 | -------------------------------------------------------------------------------- /frontend/src/components/GroupDefaultAvatar.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import styled from "styled-components"; 3 | import { People } from "react-iconly"; 4 | 5 | const GroupDefaultAvatar: FC<{ size: number }> = ({ size }) => { 6 | return ( 7 | 10 | ); 11 | }; 12 | export default GroupDefaultAvatar; 13 | 14 | const Style = styled.figure<{ size: number }>` 15 | width: ${(p) => p.size}px; 16 | height: ${(p) => p.size}px; 17 | display: flex; 18 | justify-content: center; 19 | align-items: center; 20 | border-radius: ${(p) => p.size}px; 21 | background-color: var(--border); 22 | `; 23 | -------------------------------------------------------------------------------- /frontend/src/components/Level.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useMemo } from "react"; 2 | import Tippy from "@tippyjs/react"; 3 | import { getLevel } from "@/src/tools"; 4 | 5 | import Level_1_lg from "@/images/levels/newbie-lg.svg"; 6 | import Level_2_lg from "@/images/levels/nadi-lg.svg"; 7 | import Level_3_lg from "@/images/levels/agent-lg.svg"; 8 | import Level_4_lg from "@/images/levels/epic-lg.svg"; 9 | import Level_5_lg from "@/images/levels/legend-lg.svg"; 10 | import Level_6_lg from "@/images/levels/3ankoob-lg.svg"; 11 | 12 | import Level_1_sm from "@/images/levels/newbie-sm.svg"; 13 | import Level_2_sm from "@/images/levels/nadi-sm.svg"; 14 | import Level_3_sm from "@/images/levels/agent-sm.svg"; 15 | import Level_4_sm from "@/images/levels/epic-sm.svg"; 16 | import Level_5_sm from "@/images/levels/legend-sm.svg"; 17 | import Level_6_sm from "@/images/levels/3ankoob-sm.svg"; 18 | 19 | import Level_1_md from "@/images/levels/newbie-md.svg"; 20 | import Level_2_md from "@/images/levels/nadi-md.svg"; 21 | import Level_3_md from "@/images/levels/agent-md.svg"; 22 | import Level_4_md from "@/images/levels/epic-md.svg"; 23 | import Level_5_md from "@/images/levels/legend-md.svg"; 24 | import Level_6_md from "@/images/levels/3ankoob-md.svg"; 25 | 26 | interface Props { 27 | score?: number; 28 | size?: "sm" | "md" | "lg"; 29 | } 30 | const Level: FC = ({ score = 0, size = "md" }) => { 31 | const grade = useMemo(() => getLevel(score), [score]); 32 | return ( 33 | 34 |
41 | {grade == "Newbie" && size === "sm" && } 42 | {grade == "Newbie" && size === "md" && } 43 | {grade == "Newbie" && size === "lg" && } 44 | {grade == "Nadi" && size === "sm" && } 45 | {grade == "Nadi" && size === "md" && } 46 | {grade == "Nadi" && size === "lg" && } 47 | {grade == "Agent" && size === "sm" && } 48 | {grade == "Agent" && size === "md" && } 49 | {grade == "Agent" && size === "lg" && } 50 | {grade == "Expic" && size === "sm" && } 51 | {grade == "Expic" && size === "md" && } 52 | {grade == "Expic" && size === "lg" && } 53 | {grade == "Legend" && size === "sm" && } 54 | {grade == "Legend" && size === "md" && } 55 | {grade == "Legend" && size === "lg" && } 56 | {grade == "3ankoob" && size === "sm" && } 57 | {grade == "3ankoob" && size === "md" && } 58 | {grade == "3ankoob" && size === "lg" && } 59 |
60 |
61 | ); 62 | }; 63 | export default Level; 64 | -------------------------------------------------------------------------------- /frontend/src/components/LiveGames/Card.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import Avatar from "@/components/Avatar"; 3 | import Map from "@/components/Map"; 4 | import Link from "next/link"; 5 | import { CardStyle } from "./styles"; 6 | 7 | export interface CardProps { 8 | id: string; 9 | map: string; 10 | players: { 11 | username: string; 12 | avatar: string; 13 | }[]; 14 | } 15 | 16 | export const Card: FC = (props) => { 17 | return ( 18 | 19 | 20 |
21 |
22 | 23 | 24 |
25 |
26 | 27 |
28 |
29 |
30 | {props.players[0].username} vs {props.players[1].username} 31 |
32 | {props.map} 33 | 34 |
35 | ); 36 | }; 37 | -------------------------------------------------------------------------------- /frontend/src/components/LiveGames/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { Game, ChevronLeft, ChevronRight } from "react-iconly"; 3 | import { Swiper, SwiperSlide } from "swiper/react"; 4 | 5 | import SwiperCore, { Navigation, A11y } from "swiper"; 6 | import { useGame } from "@/src/context/gameSocket"; 7 | import { Style } from "./styles"; 8 | import { CardProps, Card } from "./Card"; 9 | import { toast } from "react-toastify"; 10 | 11 | SwiperCore.use([Navigation, A11y]); 12 | 13 | const params = { 14 | navigation: { 15 | nextEl: ".next", 16 | prevEl: ".prev", 17 | }, 18 | }; 19 | const LiveGames = () => { 20 | const lives: CardProps[] = useGame()?.lives; 21 | 22 | return ( 23 | 65 | ); 66 | }; 67 | export default LiveGames; 68 | -------------------------------------------------------------------------------- /frontend/src/components/LiveGames/styles.ts: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const Style = styled.section` 4 | margin-top: 50px; 5 | header { 6 | display: flex; 7 | align-items: center; 8 | justify-content: space-between; 9 | margin-bottom: 30px; 10 | h1 { 11 | width: 100%; 12 | } 13 | > div { 14 | display: flex; 15 | align-items: center; 16 | gap: 1rem; 17 | :first-child { 18 | flex: 1; 19 | } 20 | :last-child { 21 | gap: 0.5rem; 22 | } 23 | } 24 | } 25 | .swiper-slide { 26 | width: 275px; 27 | transform: scale(0.8); 28 | transform-origin: right; 29 | transition: transform 0.3s ease, transform-origin 0.3s ease; 30 | 31 | &-active { 32 | transition: transform 0.5s ease, transform-origin 0.4s ease; 33 | transform: scale(1); 34 | } 35 | &-next, 36 | &-prev { 37 | transform: scale(0.9); 38 | transform-origin: center; 39 | } 40 | &-next ~ .swiper-slide { 41 | transform-origin: left; 42 | } 43 | .swiper-button-prev { 44 | position: static; 45 | } 46 | } 47 | `; 48 | 49 | export const CardStyle = styled.article` 50 | display: inline-flex; 51 | flex-direction: column; 52 | align-items: center; 53 | width: 100%; 54 | height: 100%; 55 | h5 { 56 | white-space: nowrap; 57 | margin-top: 10px; 58 | } 59 | b { 60 | color: var(--text-300); 61 | } 62 | a { 63 | width: 100%; 64 | text-align: center; 65 | color: var(--text-100); 66 | } 67 | .top { 68 | border-radius: 20px; 69 | overflow: hidden; 70 | gap: 5px; 71 | display: flex; 72 | flex-direction: column; 73 | } 74 | .avatars { 75 | display: flex; 76 | gap: 5px; 77 | } 78 | .map { 79 | height: 150px; 80 | background-color: #151515; 81 | } 82 | `; 83 | -------------------------------------------------------------------------------- /frontend/src/components/Map.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useMemo } from "react"; 2 | import styled from "styled-components"; 3 | 4 | interface Props { 5 | mapName: string; 6 | } 7 | interface StyleProps { 8 | backgroundColor: string; 9 | } 10 | 11 | const Maps: FC = ({ mapName }) => { 12 | const styling = useMemo(() => { 13 | switch (mapName) { 14 | case "map1": 15 | return { 16 | backgroundColor: "#0C5D98", 17 | }; 18 | case "map2": 19 | return { 20 | backgroundColor: "#33862C", 21 | }; 22 | case "map3": 23 | return { 24 | backgroundColor: "#5C0606", 25 | }; 26 | default: 27 | return { 28 | backgroundColor: "#151515", 29 | }; 30 | } 31 | }, [mapName]); 32 | 33 | return ( 34 | 39 | ); 40 | }; 41 | 42 | export default Maps; 43 | 44 | const Style = styled.div` 45 | background-color: ${(props) => props.backgroundColor}; 46 | position: relative; 47 | width: 100%; 48 | height: 100%; 49 | span { 50 | position: absolute; 51 | width: 4px; 52 | height: 30%; 53 | background-color: var(--text-100); 54 | top: 50%; 55 | transform: translateY(-50%); 56 | } 57 | span:nth-child(1) { 58 | left: 5px; 59 | } 60 | span:nth-child(2) { 61 | right: 5px; 62 | } 63 | span:nth-child(3) { 64 | left: 50%; 65 | transform: translate(-50%, -50%); 66 | width: 15px; 67 | height: 15px; 68 | border-radius: 15px; 69 | } 70 | `; 71 | -------------------------------------------------------------------------------- /frontend/src/components/MapData.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import { Game } from "react-iconly"; 3 | import Views from "../../public/images/icons/views.svg"; 4 | 5 | interface Props { 6 | mapName: string | string[] | undefined; 7 | views: any; 8 | frontWidth?: number; 9 | } 10 | 11 | interface StyleProps { 12 | frontWidth?: any; 13 | } 14 | 15 | const MapData: React.FC = ({ mapName, views, frontWidth }) => { 16 | return ( 17 | 27 | ); 28 | }; 29 | 30 | export default MapData; 31 | 32 | const Style = styled.section` 33 | width: ${(props) => props.frontWidth * 0.8}px; 34 | margin-top: 20px; 35 | display: flex; 36 | justify-content: space-between; 37 | div { 38 | display: flex; 39 | align-items: center; 40 | svg { 41 | margin-right: 10px; 42 | } 43 | } 44 | `; 45 | -------------------------------------------------------------------------------- /frontend/src/components/PlayCard.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import styled from "styled-components"; 3 | import { Game } from "react-iconly"; 4 | import PlayGameModal from "@/components/modals/PlayGame"; 5 | import PlayCardImg from "@/images/illustrations/desktop.svg"; 6 | import Background from "@/images/backgrounds/gradient.jpg"; 7 | 8 | const PlayCard = () => { 9 | const [playModal, setPlayModal] = useState(false); 10 | 11 | return ( 12 | 13 |
14 |
Join a game
15 | 16 |
Let the fun begin
17 | 18 |
19 | 22 | 23 |
24 | ); 25 | }; 26 | export default PlayCard; 27 | 28 | const PlayCardStyle = styled.div` 29 | margin-top: 30px; 30 | margin-bottom: 30px; 31 | > div { 32 | background: url(${Background.src}) no-repeat center; 33 | background-size: cover; 34 | display: flex; 35 | flex-direction: column; 36 | align-items: center; 37 | padding: 10px 24px 20px; 38 | margin-inline: 24px; 39 | border: 1px solid var(--border); 40 | filter: drop-shadow(3px 3px 20px rgba(34, 13, 95, 0.25)); 41 | border-radius: 14px; 42 | overflow: hidden; 43 | ::after { 44 | content: ""; 45 | position: absolute; 46 | inset: 0; 47 | width: 100%; 48 | height: 100%; 49 | background-color: rgba(0, 0, 0, 0.3); 50 | border-radius: 14px; 51 | z-index: -1; 52 | } 53 | button { 54 | width: 100%; 55 | background: none; 56 | border-color: var(--text-100); 57 | :hover { 58 | background-color: var(--text-100); 59 | color: var(--background-200); 60 | } 61 | } 62 | svg { 63 | margin-top: 15px; 64 | margin-bottom: 24px; 65 | } 66 | } 67 | h6 { 68 | margin-bottom: 14px; 69 | } 70 | .icon { 71 | display: none; 72 | } 73 | @media (max-width: 992px) { 74 | text-align: center; 75 | margin-bottom: 24px; 76 | > div { 77 | display: none; 78 | } 79 | .icon { 80 | display: inline-flex; 81 | width: 40px; 82 | height: 40px; 83 | svg { 84 | width: 20px; 85 | } 86 | } 87 | } 88 | `; 89 | -------------------------------------------------------------------------------- /frontend/src/components/Players.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import Avatar from "./Avatar"; 3 | import Diamond from "../../public/images/icons/diamond.svg"; 4 | 5 | interface Props { 6 | player1: any; 7 | player2: any; 8 | frontWidth: any; 9 | leftScore: number; 10 | rightScore: number; 11 | player1Points: number; 12 | player2Points: number; 13 | } 14 | interface StyleProps { 15 | frontWidth?: any; 16 | } 17 | 18 | interface PlayerProps { 19 | player2?: any; 20 | } 21 | 22 | const Players: React.FC = ({ 23 | player1, 24 | player2, 25 | frontWidth, 26 | leftScore, 27 | rightScore, 28 | player1Points, 29 | player2Points, 30 | }) => { 31 | return ( 32 | 67 | ); 68 | }; 69 | 70 | export default Players; 71 | 72 | const Style = styled.section` 73 | margin-top: 60px; 74 | width: ${(props) => props.frontWidth * 0.8}px; 75 | padding: 30px 0; 76 | display: flex; 77 | justify-content: space-between; 78 | align-items: center; 79 | `; 80 | 81 | const Score = styled.div` 82 | display: flex; 83 | align-items: center; 84 | justify-content: space-between; 85 | span:nth-child(2) { 86 | margin: 0 10px; 87 | opacity: 0.5; 88 | } 89 | `; 90 | 91 | const Player = styled.div` 92 | display: flex; 93 | align-items: center; 94 | flex-direction: ${(props) => (props.player2 ? "row-reverse" : "row")}; 95 | figure { 96 | margin-right: ${(props) => (props.player2 ? "0" : "10px")}; 97 | } 98 | div { 99 | margin-right: ${(props) => (props.player2 ? "10px" : "0")}; 100 | justify-content: ${(props) => (props.player2 ? "flex-end" : "flex-start")}; 101 | span { 102 | display: flex; 103 | justify-content: ${(props) => (props.player2 ? "flex-end" : "flex-start")}; 104 | &:last-child { 105 | display: flex; 106 | align-items: center; 107 | svg { 108 | margin-right: 5px; 109 | } 110 | } 111 | } 112 | } 113 | @media (width < 768px) { 114 | flex-direction: column; 115 | justify-content: center; 116 | align-items: center; 117 | figure { 118 | margin-right: 0; 119 | } 120 | div { 121 | margin-right: 0; 122 | justify-content: center; 123 | } 124 | } 125 | `; 126 | -------------------------------------------------------------------------------- /frontend/src/components/SearchInput.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import styled from "styled-components"; 3 | import { Search, CloseSquare } from "react-iconly"; 4 | 5 | interface Props { 6 | setValue: React.Dispatch>; 7 | value: string; 8 | } 9 | const SearchInput: FC = ({ setValue, value }) => { 10 | const handleClear = () => { 11 | setValue(""); 12 | }; 13 | return ( 14 | 28 | ); 29 | }; 30 | 31 | export default SearchInput; 32 | 33 | const Style = styled.div` 34 | display: flex; 35 | align-items: center; 36 | position: relative; 37 | margin-bottom: 14px; 38 | input { 39 | padding-left: 40px; 40 | width: 100%; 41 | border: none; 42 | } 43 | > svg { 44 | position: absolute; 45 | left: 10px; 46 | } 47 | button { 48 | position: absolute; 49 | right: 5px; 50 | } 51 | `; 52 | -------------------------------------------------------------------------------- /frontend/src/components/TwoFactor.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import Modal from "react-modal"; 3 | import { toast } from "react-toastify"; 4 | 5 | import TwoFactorModal from "@/components/modals/TwoFactor"; 6 | import Checkbox from "@/components/Checkbox"; 7 | 8 | Modal.setAppElement("#__next"); 9 | 10 | const TwoFactor = ({ state }: { state: boolean }) => { 11 | const [twoFactorModal, setModal] = useState(false); 12 | const [isChecked, setIsChecked] = useState(state); 13 | 14 | const handleModal = async () => { 15 | if (!isChecked) { 16 | setModal(true); 17 | return; 18 | } 19 | await fetch("/api/twoFactor/turnOff", { method: "POST" }) 20 | .then((res) => res.json()) 21 | .then((data) => { 22 | if (data.statusCode === 200) { 23 | setIsChecked(false); 24 | toast.success("Two factor authentication has been disabled"); 25 | } else toast.error(data.message); 26 | }) 27 | .catch((err) => toast.error(err.message)); 28 | }; 29 | 30 | return ( 31 | <> 32 |
33 | 34 | Two Factor Authentication 35 | 36 |

37 | you will need to scan a QR code to enable this feature 38 |
39 | once you enable this feature you will need to use a code to login 40 |

41 |
42 | 48 | 49 | ); 50 | }; 51 | 52 | export default TwoFactor; 53 | -------------------------------------------------------------------------------- /frontend/src/components/boardPlayers.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import { useAuth } from "../context/auth"; 3 | import Avatar from "./Avatar"; 4 | 5 | interface Props { 6 | idk: any; 7 | something: any; 8 | } 9 | 10 | const BoardPlayers: React.FC = ({idk, something}) => { 11 | 12 | const auth = useAuth(); 13 | const user = auth?.user; 14 | 15 | 16 | return ( 17 | 20 | ); 21 | }; 22 | 23 | export default BoardPlayers; 24 | 25 | const Style = styled.div` 26 | 27 | ` -------------------------------------------------------------------------------- /frontend/src/components/dropdowns/Notifications/Request.tsx: -------------------------------------------------------------------------------- 1 | import Avatar from "@/components/Avatar"; 2 | import moment from "moment"; 3 | import { Notification, useSocket } from "@/src/context/socket"; 4 | 5 | const Request: React.FC<{ data: Notification }> = ({ data }) => { 6 | const { id, sender, senderId, message, image, seen, createdAt } = data; 7 | const { updateNotification } = useSocket(); 8 | 9 | const handleAccept = async () => { 10 | await fetch(`${process.env.USERS}/acceptfriendrequest?id=${senderId}`, { 11 | method: "POST", 12 | credentials: "include", 13 | }).then(() => updateNotification(id)); 14 | }; 15 | 16 | const handleDecline = async () => { 17 | await fetch(`${process.env.USERS}/declineFriendRequest?id=${senderId}`, { 18 | method: "DELETE", 19 | credentials: "include", 20 | }).then(() => updateNotification(id)); 21 | }; 22 | return ( 23 | <> 24 | 25 |
26 |
27 | {message.split(" ")[0]} 28 | {message.substring(message.indexOf(" ") + 1)} 29 |
30 | {!message.includes("accepted") && !seen && ( 31 |
32 | 33 | 36 |
37 | )} 38 | {moment(createdAt).fromNow()} 39 |
40 | 41 | ); 42 | }; 43 | export default Request; 44 | -------------------------------------------------------------------------------- /frontend/src/components/dropdowns/Profile.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useState, useRef } from "react"; 2 | import Link from "next/link"; 3 | 4 | import Avatar from "@/components/Avatar"; 5 | import Dropdown from "@/components/Dropdown"; 6 | import { useOnClickOutside } from "@/hooks/useOnClickOutside"; 7 | import { Setting, Logout } from "react-iconly"; 8 | import { useAuth } from "@/src/context/auth"; 9 | 10 | const Profile: FC = () => { 11 | const divRef = useRef(null); 12 | const [profile, setProfile] = useState(false); 13 | const { user, logout } = useAuth(); 14 | 15 | useOnClickOutside(divRef, () => setProfile(false)); 16 | 17 | return ( 18 | setProfile((prev) => !prev)} 26 | > 27 | 34 | 35 | } 36 | > 37 |
38 |
    39 |
  • setProfile(false)} role="button"> 40 | 41 | 48 |
    49 |
    {user?.username}
    50 |

    {"online"}

    51 |
    52 | 53 |
  • 54 |
    55 |
  • setProfile(false)}> 56 | 57 | 58 | Settings 59 | 60 |
  • 61 |
  • setProfile(false)}> 62 | 66 |
  • 67 |
68 |
69 |
70 | ); 71 | }; 72 | export default Profile; 73 | -------------------------------------------------------------------------------- /frontend/src/components/dropdowns/UserOptions.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useState, useRef } from "react"; 2 | import { Danger } from "react-iconly"; 3 | import { toast } from "react-toastify"; 4 | 5 | import Dropdown from "@/components/Dropdown"; 6 | import { useOnClickOutside } from "@/hooks/useOnClickOutside"; 7 | import DotsIcon from "@/images/icons/dots.svg"; 8 | import BlockUserModal from "@/components/modals/BlockUser"; 9 | 10 | interface Props { 11 | id: string; 12 | isBlocked?: boolean; 13 | setIsBlocked?: React.Dispatch>; 14 | } 15 | 16 | const UserOptions: FC = ({ id, isBlocked, setIsBlocked }) => { 17 | const divRef = useRef(null); 18 | const [drop, setDrop] = useState(false); 19 | const [modalBlock, setModalBlock] = useState(false); 20 | 21 | useOnClickOutside(divRef, () => setDrop(false)); 22 | 23 | const handleUnblock = async () => { 24 | if (!setIsBlocked) return; 25 | await fetch(`${process.env.USERS}/unblockUser?id=${id}`, { 26 | method: "DELETE", 27 | credentials: "include", 28 | }).then(() => { 29 | toast.success("User unblocked"); 30 | setIsBlocked(false); 31 | setDrop(false); 32 | }); 33 | }; 34 | 35 | return ( 36 | setDrop((prev) => !prev)}> 41 | 42 | 43 | } 44 | > 45 |
    46 |
  • 47 | {isBlocked ? ( 48 | 52 | ) : ( 53 | 57 | )} 58 | 64 |
  • 65 |
66 |
67 | ); 68 | }; 69 | export default UserOptions; 70 | -------------------------------------------------------------------------------- /frontend/src/components/modals/AchievementModal.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import { Props as Interface } from "./interface"; 3 | import styled from "styled-components"; 4 | import { useSound } from "use-sound"; 5 | 6 | import Modale from "@/layout/Modal"; 7 | import { achievementsList } from "@/src/achievements"; 8 | import { CardStyle } from "@/components/Achievements"; 9 | import GridEffect from "@/components/GridEffect"; 10 | 11 | interface Props extends Interface { 12 | achievements: string[]; 13 | } 14 | 15 | const Achievement: FC = ({ isOpen, setIsOpen, contentLabel, achievements }) => { 16 | const [play] = useSound("/sounds/achievement.mp3"); 17 | 18 | if (isOpen) play(); 19 | 20 | return ( 21 | setIsOpen(false)} contentLabel={contentLabel}> 22 | 40 | 41 | ); 42 | }; 43 | export default Achievement; 44 | 45 | const Style = styled.div` 46 | > div { 47 | grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); 48 | grid-gap: 10px; 49 | } 50 | > button { 51 | margin-top: 20px; 52 | width: 100%; 53 | background-color: #faff00; 54 | color: #000; 55 | border: none; 56 | padding: 12px 10px; 57 | } 58 | `; 59 | -------------------------------------------------------------------------------- /frontend/src/components/modals/BanUser.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import Modale from "@/layout/Modal"; 3 | import styled from "styled-components"; 4 | import axios from "axios"; 5 | import { toast } from "react-toastify"; 6 | import { Props as Interface } from "./interface"; 7 | 8 | // merge 2 interfaces 9 | interface Props extends Interface { 10 | userId: string; 11 | roomId: string; 12 | setIsBanned: (value: boolean) => void; 13 | } 14 | 15 | const BanUser: FC = (props) => { 16 | const { isOpen, setIsOpen, contentLabel, userId, roomId, setIsBanned } = props; 17 | const handleBan = async () => { 18 | await axios 19 | .post(`${process.env.CHAT}/banUser`, { roomId, userId }, { withCredentials: true }) 20 | .then(() => { 21 | setIsOpen(false); 22 | setIsBanned(true); 23 | toast.info("User banned"); 24 | }) 25 | .catch((err) => toast.error(err.response.data)); 26 | }; 27 | return ( 28 | setIsOpen(false)} contentLabel={contentLabel}> 29 |

Are you sure you want to ban this user from this room?

30 | 38 |
39 | ); 40 | }; 41 | export default BanUser; 42 | 43 | const Style = styled.div` 44 | display: flex; 45 | justify-content: flex-end; 46 | margin-top: 30px; 47 | gap: 10px; 48 | `; 49 | -------------------------------------------------------------------------------- /frontend/src/components/modals/BlockUser.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import Modale from "@/layout/Modal"; 3 | import styled from "styled-components"; 4 | import { useAuth } from "@/src/context/auth"; 5 | import { useRouter } from "next/router"; 6 | import { Props as Interface } from "./interface"; 7 | 8 | interface Props extends Interface { 9 | id: string; 10 | } 11 | 12 | const BlockUser: FC = ({ isOpen, setIsOpen, contentLabel, id }) => { 13 | const { user } = useAuth(); 14 | const router = useRouter(); 15 | 16 | const handleBlock = async () => { 17 | await fetch(`${process.env.USERS}/blockUser?id=${id}`, { 18 | method: "POST", 19 | credentials: "include", 20 | }).then(() => { 21 | setIsOpen(false); 22 | router.push(`/${user?.username}/blockList`); 23 | }); 24 | }; 25 | 26 | return ( 27 | setIsOpen(false)} contentLabel={contentLabel}> 28 |

Are you sure you want to block this user?

29 | 37 |
38 | ); 39 | }; 40 | export default BlockUser; 41 | 42 | const Style = styled.div` 43 | display: flex; 44 | justify-content: flex-end; 45 | margin-top: 30px; 46 | gap: 10px; 47 | `; 48 | -------------------------------------------------------------------------------- /frontend/src/components/modals/CancelFriendReq.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import Modale from "@/layout/Modal"; 3 | import styled from "styled-components"; 4 | import { Props as Interface } from "./interface"; 5 | 6 | interface Props extends Interface { 7 | id: string; 8 | username: string; 9 | setPending: React.Dispatch>; 10 | } 11 | 12 | const CancelFriendReq: FC = (props) => { 13 | const { isOpen, setIsOpen, contentLabel, id, username, setPending } = props; 14 | 15 | const handleCancel = async () => { 16 | await fetch(`${process.env.USERS}/cancelFriendRequest?id=${id}`, { 17 | method: "DELETE", 18 | credentials: "include", 19 | }).then(() => { 20 | setIsOpen(false); 21 | setPending(false); 22 | }); 23 | }; 24 | 25 | return ( 26 | setIsOpen(false)} contentLabel={contentLabel}> 27 |

Are you sure you want to cancel your friend request to {username}?

28 | 34 |
35 | ); 36 | }; 37 | export default CancelFriendReq; 38 | 39 | const Style = styled.div` 40 | display: flex; 41 | justify-content: flex-end; 42 | margin-top: 30px; 43 | gap: 10px; 44 | `; 45 | -------------------------------------------------------------------------------- /frontend/src/components/modals/DeleteAccount.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import Modale from "@/layout/Modal"; 3 | import styled from "styled-components"; 4 | import { toast } from "react-toastify"; 5 | import { Props } from "./interface"; 6 | import { useAuth } from "@/src/context/auth"; 7 | 8 | const DeleteAccount: FC = ({ isOpen, setIsOpen, contentLabel }) => { 9 | const { logout } = useAuth(); 10 | 11 | const handleDelete = async () => { 12 | await fetch(`${process.env.USERS}/deleteUser`, { 13 | method: "DELETE", 14 | credentials: "include", 15 | }) 16 | .then((res) => res.json()) 17 | .then((data) => { 18 | setIsOpen(false); 19 | toast.success(data.message); 20 | logout(); 21 | }); 22 | }; 23 | 24 | return ( 25 | setIsOpen(false)} contentLabel={contentLabel}> 26 |

Are you sure you want to delete your account? This action cannot be undone.

27 | 35 |
36 | ); 37 | }; 38 | export default DeleteAccount; 39 | 40 | const Style = styled.div` 41 | display: flex; 42 | justify-content: flex-end; 43 | margin-top: 30px; 44 | gap: 10px; 45 | `; 46 | -------------------------------------------------------------------------------- /frontend/src/components/modals/Feedback.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import Modale from "@/layout/Modal"; 3 | import { Props } from "./interface"; 4 | import Link from "next/link"; 5 | 6 | const Feedback: FC = ({ isOpen, setIsOpen, contentLabel }) => { 7 | return ( 8 | setIsOpen(false)} contentLabel={contentLabel}> 9 |

10 | If you have any feedback, please send it to{" "} 11 | PongChamp 12 |

13 |
14 | ); 15 | }; 16 | export default Feedback; 17 | -------------------------------------------------------------------------------- /frontend/src/components/modals/InvitePlayer.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | import Avatar from "@/components/Avatar"; 4 | import Modal from "@/layout/Modal"; 5 | import { useFriends } from "@/hooks/useFriends"; 6 | import { useInvitePlayer } from "@/src/hooks/useInvitePlayer"; 7 | import { useSocket } from "@/src/context/socket"; 8 | import { Props } from "./interface"; 9 | 10 | const InvitePlayer: React.FC = ({ isOpen, setIsOpen, contentLabel }) => { 11 | const { handleInvite } = useInvitePlayer(); 12 | const friends = useFriends(); 13 | const { getStatus } = useSocket(); 14 | 15 | const handleInvitePlayer = async (username: string) => { 16 | handleInvite(username); 17 | setIsOpen(false); 18 | }; 19 | 20 | return ( 21 | setIsOpen(false)} contentLabel={contentLabel}> 22 |
    23 | {friends.filter((friend) => getStatus(friend.username) === "online").length === 0 && ( 24 |

    there is no online friends

    25 | )} 26 | {friends 27 | .filter((friend) => getStatus(friend.username) === "online") 28 | .map((friend, index) => ( 29 | 30 |
    31 | 38 |
    {friend.username}
    39 |
    40 | 41 |
    42 | ))} 43 |
44 |
45 | ); 46 | }; 47 | export default InvitePlayer; 48 | 49 | const Row = styled.li` 50 | display: flex; 51 | align-items: center; 52 | justify-content: space-between; 53 | padding: 0.5rem 1rem; 54 | border-radius: 0.5rem; 55 | :nth-child(even) { 56 | background-color: var(--background-200); 57 | } 58 | h5 { 59 | margin-left: 1rem; 60 | span { 61 | color: var(--text-300); 62 | font-family: var(--font-light); 63 | } 64 | } 65 | > div { 66 | display: flex; 67 | align-items: center; 68 | } 69 | `; 70 | -------------------------------------------------------------------------------- /frontend/src/components/modals/MuteUser.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useState } from "react"; 2 | import Modale from "@/layout/Modal"; 3 | import styled from "styled-components"; 4 | import axios from "axios"; 5 | import { toast } from "react-toastify"; 6 | import { Props as Interface } from "./interface"; 7 | 8 | interface Props extends Interface { 9 | roomId: string; 10 | userId: string; 11 | setIsMutted: React.Dispatch>; 12 | } 13 | 14 | type MuteDuration = "2m" | "1w" | "8h" | "1d"; 15 | 16 | const MuteUser: FC = (props) => { 17 | const { isOpen, setIsOpen, contentLabel, roomId, userId, setIsMutted } = props; 18 | const [duration, setDuration] = useState("2m"); 19 | 20 | const handleMute = async (e: React.FormEvent) => { 21 | e.preventDefault(); 22 | await axios 23 | .post(`${process.env.CHAT}/muteUser`, { roomId, userId, duration }, { withCredentials: true }) 24 | .then(() => { 25 | setIsOpen(false); 26 | setIsMutted(true); 27 | toast.info("User is Mutted"); 28 | }) 29 | .catch((err) => toast.error(err.response.data)); 30 | }; 31 | 32 | return ( 33 | setIsOpen(false)} contentLabel={contentLabel}> 34 | 62 | 63 | ); 64 | }; 65 | export default MuteUser; 66 | 67 | const Style = styled.div` 68 | .radio-group { 69 | display: flex; 70 | gap: 10px; 71 | margin-top: 20px; 72 | > div { 73 | display: flex; 74 | align-items: center; 75 | gap: 10px; 76 | flex: 1; 77 | } 78 | input[type="radio"] { 79 | display: none; 80 | } 81 | label { 82 | margin: 0; 83 | width: 100%; 84 | } 85 | } 86 | .actions { 87 | display: flex; 88 | justify-content: flex-end; 89 | margin-top: 30px; 90 | gap: 10px; 91 | } 92 | `; 93 | -------------------------------------------------------------------------------- /frontend/src/components/modals/PlayGame.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useEffect, useState } from "react"; 2 | import Modale from "@/layout/Modal"; 3 | import { Game } from "react-iconly"; 4 | import styled from "styled-components"; 5 | import { Swiper, SwiperSlide } from "swiper/react"; 6 | import { EffectCards } from "swiper"; 7 | import Link from "next/link"; 8 | import SwiperCore from "swiper"; 9 | import Image from "next/image"; 10 | 11 | import { Props } from "./interface"; 12 | import Map1 from "@/images/maps/defaultMap.jpeg"; 13 | import Map2 from "@/images/maps/map_2.jpg"; 14 | import Map3 from "@/images/maps/map_3.jpg"; 15 | import Map4 from "@/images/maps/map_4.png"; 16 | 17 | export const maps = ["1337", "FarAway", "Iceland", "Miramar"]; 18 | 19 | const PlayGame: FC = ({ isOpen, setIsOpen, contentLabel }) => { 20 | const [swiper, setSwiper] = useState(null); 21 | const [mapName, setMapName] = useState(maps[0]); 22 | const [mapIndex, setMapIndex] = useState(0); 23 | 24 | useEffect(() => { 25 | if (swiper) { 26 | swiper.on("slideChange", () => { 27 | setMapName(maps[swiper.activeIndex]); 28 | setMapIndex(swiper.activeIndex); 29 | }); 30 | } 31 | }, [swiper]); 32 | 33 | return ( 34 | setIsOpen(false)} contentLabel={contentLabel}> 35 | 56 | 57 | ); 58 | }; 59 | export default PlayGame; 60 | 61 | const Style = styled.div` 62 | display: flex; 63 | flex-direction: column; 64 | align-items: center; 65 | .lg { 66 | margin-top: 30px; 67 | width: 100%; 68 | } 69 | .swiper { 70 | width: 450px; 71 | height: 300px; 72 | } 73 | .btn { 74 | max-width: 300px; 75 | } 76 | h5 { 77 | text-align: center; 78 | margin-top: 20px; 79 | } 80 | .swiper-slide { 81 | display: flex; 82 | align-items: center; 83 | justify-content: center; 84 | border-radius: 18px; 85 | font-size: 22px; 86 | font-weight: bold; 87 | color: #fff; 88 | } 89 | 90 | img { 91 | object-fit: cover; 92 | } 93 | 94 | .swiper-slide:nth-child(1n) { 95 | background-color: rgb(0, 0, 0); 96 | } 97 | `; 98 | -------------------------------------------------------------------------------- /frontend/src/components/modals/TwoFactor.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useEffect, useState } from "react"; 2 | import Modale from "@/layout/Modal"; 3 | import styled from "styled-components"; 4 | import { toast } from "react-toastify"; 5 | import { Props as Interface } from "./interface"; 6 | 7 | interface Props extends Interface { 8 | setTwoFactor: React.Dispatch>; 9 | } 10 | 11 | const TwoFactor: FC = ({ isOpen, setIsOpen, contentLabel, setTwoFactor }) => { 12 | const [code, setCode] = useState(""); 13 | const [error, setError] = useState(""); 14 | 15 | useEffect(() => { 16 | setCode(""); 17 | setError(""); 18 | }, [isOpen]); 19 | 20 | const handleSubmit = async (e: React.FormEvent) => { 21 | e.preventDefault(); 22 | await fetch("/api/twoFactor/turnOn", { 23 | method: "POST", 24 | body: code, 25 | }) 26 | .then((res) => res.json()) 27 | .then((data) => { 28 | if (data.statusCode === 200) { 29 | setTwoFactor(true); 30 | setIsOpen(false); 31 | toast.success("Two factor authentication has been enabled"); 32 | setError(""); 33 | } else setError(data.message); 34 | }) 35 | .catch((err) => setError(err.message)); 36 | }; 37 | 38 | return ( 39 | setIsOpen(false)} contentLabel={contentLabel}> 40 | 62 | 63 | ); 64 | }; 65 | export default TwoFactor; 66 | 67 | const Style = styled.form` 68 | display: flex; 69 | flex-direction: column; 70 | max-width: 400px; 71 | margin-inline: auto; 72 | gap: 10px; 73 | .qr-code { 74 | width: 300px; 75 | height: 300px; 76 | margin: 0 auto 10px; 77 | background-color: var(--background-200); 78 | border-radius: 5px; 79 | } 80 | `; 81 | -------------------------------------------------------------------------------- /frontend/src/components/modals/Unfriend.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import Modale from "@/layout/Modal"; 3 | import styled from "styled-components"; 4 | import { Props as Interface } from "./interface"; 5 | 6 | interface Props extends Interface { 7 | setFriend: React.Dispatch>; 8 | id: string; 9 | username: string; 10 | } 11 | 12 | const Unfriend: FC = ({ isOpen, setIsOpen, contentLabel, id, username, setFriend }) => { 13 | const handleUnfriend = async () => { 14 | await fetch(`${process.env.USERS}/unFriend?id=${id}`, { 15 | method: "POST", 16 | credentials: "include", 17 | }).then(() => { 18 | setIsOpen(false); 19 | setFriend(false); 20 | }); 21 | }; 22 | 23 | return ( 24 | setIsOpen(false)} contentLabel={contentLabel}> 25 |

Are you sure you want to remove {username} as your friend?

26 | 32 |
33 | ); 34 | }; 35 | export default Unfriend; 36 | 37 | const Style = styled.div` 38 | display: flex; 39 | justify-content: flex-end; 40 | margin-top: 30px; 41 | gap: 10px; 42 | `; 43 | -------------------------------------------------------------------------------- /frontend/src/components/modals/interface.ts: -------------------------------------------------------------------------------- 1 | export interface Props { 2 | isOpen: boolean; 3 | setIsOpen: React.Dispatch>; 4 | contentLabel: string; 5 | } 6 | -------------------------------------------------------------------------------- /frontend/src/components/toasts/FriendRequest.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | import { useSocket } from "@/src/context/socket"; 4 | import { Notification } from "@/src/context/socket"; 5 | import Avatar from "@/components/Avatar"; 6 | import Check from "@/images/icons/check.svg"; 7 | import Close from "@/images/icons/close.svg"; 8 | 9 | const FriendRequest: React.FC = (props) => { 10 | const { id, sender, senderId, message, image, seen } = props; 11 | const { updateNotification } = useSocket(); 12 | 13 | const makeNotificationSeen = async () => { 14 | await fetch(`${process.env.USERS}/notificationsSeen?id=${id}`, { 15 | method: "POST", 16 | credentials: "include", 17 | }).then(() => { 18 | updateNotification(id); 19 | }); 20 | }; 21 | 22 | const handleAccept = async () => { 23 | await fetch(`${process.env.USERS}/acceptfriendrequest?id=${senderId}`, { 24 | method: "POST", 25 | credentials: "include", 26 | }).then(() => makeNotificationSeen()); 27 | }; 28 | 29 | const handleDecline = async () => { 30 | await fetch(`${process.env.USERS}/declineFriendRequest?id=${senderId}`, { 31 | method: "DELETE", 32 | credentials: "include", 33 | }).then(() => updateNotification(id)); 34 | }; 35 | 36 | return ( 37 | 51 | ); 52 | }; 53 | 54 | export default FriendRequest; 55 | 56 | const Style = styled.div` 57 | display: flex; 58 | align-items: center; 59 | gap: 10px; 60 | 61 | .actions { 62 | display: flex; 63 | align-items: center; 64 | gap: 5px; 65 | } 66 | `; 67 | -------------------------------------------------------------------------------- /frontend/src/components/toasts/MessageToast.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import Link from "next/link"; 3 | import { MessagesType } from "@/src/layout/messenger/interfaces"; 4 | import Avatar from "@/components/Avatar"; 5 | 6 | const MessageToast: React.FC = (props) => { 7 | const { roomId, username, avatar, isRoom, message } = props; 8 | 9 | return ( 10 | 17 | ); 18 | }; 19 | 20 | export default MessageToast; 21 | 22 | const Style = styled(Link)` 23 | display: flex; 24 | align-items: center; 25 | gap: 10px; 26 | color: var(--text-100); 27 | 28 | p { 29 | font-family: var(--font-regular); 30 | overflow: hidden; 31 | text-overflow: ellipsis; 32 | display: -webkit-box; 33 | -webkit-line-clamp: 1; // number of lines to show 34 | -webkit-box-orient: vertical; 35 | line-height: 1.5; 36 | word-break: break-all; 37 | } 38 | `; 39 | -------------------------------------------------------------------------------- /frontend/src/components/toasts/PlayRequest.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | import { useGame, PlayInvite } from "@/src/context/gameSocket"; 4 | import { Notification } from "@/src/context/socket"; 5 | import Avatar from "@/components/Avatar"; 6 | import Check from "@/images/icons/check.svg"; 7 | import Close from "@/images/icons/close.svg"; 8 | import { useAuth } from "@/src/context/auth"; 9 | import { useRouter } from "next/router"; 10 | 11 | const PlayRequest: React.FC = (props) => { 12 | const { id, username, message, avatar } = props; 13 | const socket = useGame()?.socket; 14 | // const { user } = useAuth(); 15 | const router = useRouter(); 16 | 17 | const handleAccept = async () => { 18 | socket?.emit("acceptPlayRequest", username); 19 | socket?.off("getInvitationRoomId").on("getInvitationRoomId", (room: string, roomReserved: boolean) => { 20 | if (roomReserved) router.push(`/game/${room}?requestType=invited`); 21 | }); 22 | }; 23 | 24 | const handleDecline = async () => { 25 | socket?.emit("declinePlayRequest"); 26 | }; 27 | 28 | // console.log("message", message); 29 | 30 | return ( 31 | 43 | ); 44 | }; 45 | 46 | export default PlayRequest; 47 | 48 | const Style = styled.div` 49 | display: flex; 50 | align-items: center; 51 | gap: 10px; 52 | 53 | .actions { 54 | display: flex; 55 | align-items: center; 56 | gap: 5px; 57 | } 58 | `; 59 | -------------------------------------------------------------------------------- /frontend/src/context/auth.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useState, useContext, useEffect, useCallback } from "react"; 2 | import { useRouter } from "next/router"; 3 | import axios from "axios"; 4 | 5 | interface UserType { 6 | id: string; 7 | avatar: string; 8 | losses: number; 9 | wins: number; 10 | country: string | null; 11 | email: string; 12 | fullName: string; 13 | username: string; 14 | score: number; 15 | twoFactor: boolean; 16 | createdAt: string; 17 | updatedAt: string; 18 | phoneNumber: string; 19 | } 20 | 21 | export interface authContextType { 22 | user: UserType | null; 23 | logout: () => void; 24 | setUser: React.Dispatch>; 25 | setAvatar: (avatar: string) => void; 26 | setUsername: (username: string) => void; 27 | } 28 | 29 | const AuthContext = createContext({ 30 | user: null, 31 | logout: () => {}, 32 | setUser: () => {}, 33 | setAvatar: (avatar: string) => {}, 34 | setUsername: () => {}, 35 | }); 36 | 37 | export const useAuth = () => useContext(AuthContext); 38 | 39 | export const AuthProvider = ({ children }: { children: React.ReactNode }) => { 40 | const [user, setUser] = useState(null); 41 | const router = useRouter(); 42 | 43 | const logout = useCallback(async () => { 44 | await fetch("/api/logout", { 45 | method: "DELETE", 46 | }) 47 | .then((res) => res.json()) 48 | .then((data) => { 49 | if (data?.message === "OK") { 50 | setUser(null); 51 | router.push("/login"); 52 | } 53 | }); 54 | }, [router]); 55 | 56 | const setAvatar = useCallback((avatar: string) => { 57 | setUser((prev) => { 58 | if (prev) 59 | return { 60 | ...prev, 61 | avatar, 62 | }; 63 | return null; 64 | }); 65 | }, []); 66 | 67 | const setUsername = useCallback((username: string) => { 68 | setUser((prev) => { 69 | if (prev) 70 | return { 71 | ...prev, 72 | username, 73 | }; 74 | return null; 75 | }); 76 | }, []); 77 | 78 | useEffect(() => { 79 | const getUserData = async () => { 80 | try { 81 | await axios 82 | .get(`${process.env.USERS}/me`, { 83 | withCredentials: true, 84 | }) 85 | .then(({ data }) => { 86 | if (data) setUser(data); 87 | }) 88 | .catch((err) => { 89 | if (err.response.data.statusCode === 401) logout(); 90 | }); 91 | } catch (err) { 92 | logout(); 93 | } 94 | }; 95 | getUserData(); 96 | }, [logout]); 97 | 98 | return ( 99 | 100 | {children} 101 | 102 | ); 103 | }; 104 | -------------------------------------------------------------------------------- /frontend/src/context/chat.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useState, useContext, useEffect } from "react"; 2 | import { useRouter } from "next/router"; 3 | import { io, Socket } from "socket.io-client"; 4 | import { toast } from "react-toastify"; 5 | import { useSound } from "use-sound"; 6 | 7 | import { MessagesType } from "@/src/layout/messenger/interfaces"; 8 | import MessageToast from "@/components/toasts/MessageToast"; 9 | import { useAuth } from "@/src/context/auth"; 10 | 11 | interface ChatSocket { 12 | socket: Socket | null; 13 | message: MessagesType | null; 14 | setMessage: React.Dispatch>; 15 | } 16 | 17 | const ChatContext = createContext({ 18 | socket: null, 19 | message: null, 20 | setMessage: () => {}, 21 | }); 22 | 23 | export const useChat = () => useContext(ChatContext); 24 | 25 | export const ChatSocketProvider = ({ children }: { children: React.ReactNode }) => { 26 | const [socket, setSocket] = useState(null); 27 | const [message, setMessage] = useState(null); 28 | const { pathname } = useRouter(); 29 | const { user } = useAuth(); 30 | const [play] = useSound("/sounds/message.mp3", { volume: 0.5 }); 31 | 32 | useEffect(() => { 33 | const newSocket = io(`${process.env.SERVER_URL}/chat`, { 34 | withCredentials: true, 35 | }); 36 | setSocket(newSocket); 37 | return () => { 38 | newSocket.close(); 39 | }; 40 | }, []); 41 | 42 | useEffect(() => { 43 | if (!socket) return; 44 | return () => { 45 | socket.close(); 46 | }; 47 | }, [socket]); 48 | 49 | useEffect(() => { 50 | if (!socket || !user) return; 51 | socket?.off("chatToClient").on("chatToClient", (msg: MessagesType) => { 52 | if (!msg.blockedUsers.includes(user?.username) && !msg.bannedUsers.includes(user?.username)) { 53 | setMessage(msg); 54 | if (pathname !== "/messages/[id]") { 55 | play(); 56 | toast(, { 57 | closeButton: false, 58 | className: "toast", 59 | bodyClassName: "friend-request-toast", 60 | }); 61 | } 62 | } 63 | }); 64 | 65 | return () => { 66 | socket?.off("chatToClient"); 67 | }; 68 | }, [socket, pathname, user, play]); 69 | 70 | return ( 71 | {children} 72 | ); 73 | }; 74 | -------------------------------------------------------------------------------- /frontend/src/context/gameSocket.tsx: -------------------------------------------------------------------------------- 1 | import { io, Socket } from "socket.io-client"; 2 | import { toast } from "react-toastify"; 3 | import { createContext, useState, useContext, useEffect } from "react"; 4 | import { useAuth } from "@/src/context/auth"; 5 | import InvitePlayToast from "@/components/toasts/PlayRequest"; 6 | 7 | interface GameSocket { 8 | socket: Socket | null; 9 | lives: any; 10 | joinQueue: () => void; 11 | achievements: string[]; 12 | setAchievements: React.Dispatch>; 13 | } 14 | 15 | export interface PlayInvite { 16 | id: string; 17 | username: string; 18 | message: string; 19 | avatar: string; 20 | } 21 | 22 | const GameContext = createContext({ 23 | socket: null, 24 | lives: null, 25 | joinQueue: () => {}, 26 | achievements: [], 27 | setAchievements: () => {}, 28 | }); 29 | 30 | export const useGame = () => useContext(GameContext); 31 | 32 | export const GameSocketProvider = ({ children }: { children: React.ReactNode }) => { 33 | const [socket, setSocket] = useState(null); 34 | const [lives, setLives] = useState(null); 35 | const [achievements, setAchievements] = useState([]); 36 | const { user } = useAuth(); 37 | 38 | useEffect(() => { 39 | const newSocket = io(`${process.env.SERVER_URL}/game`, { 40 | withCredentials: true, 41 | }); 42 | setSocket(newSocket); 43 | return () => { 44 | newSocket.close(); 45 | }; 46 | }, []); 47 | 48 | useEffect(() => { 49 | if (!socket) return; 50 | 51 | return () => { 52 | socket.close(); 53 | }; 54 | }, [socket]); 55 | 56 | useEffect(() => { 57 | if (!user || !socket) return; 58 | socket?.emit("getLiveGames_F", user.username); 59 | socket?.off("liveGames").on("liveGames", (rooms: any) => { 60 | setLives(rooms); 61 | }); 62 | socket?.off("invitation").on("invitation", (data: PlayInvite) => { 63 | toast(, { 64 | position: "bottom-right", 65 | closeButton: false, 66 | className: "toast", 67 | bodyClassName: "friend-request-toast", 68 | autoClose: 10000, 69 | }); 70 | }); 71 | socket?.off("gameEnds").on("gameEnds", (state) => { 72 | toast.info(`you ${state} the previous game`); 73 | }); 74 | 75 | socket?.off("achievement").on("achievement", (achievement) => { 76 | setAchievements((prev) => [...prev, achievement.toString()]); 77 | }); 78 | 79 | return () => { 80 | socket?.off("liveGames"); 81 | socket?.off("invitation"); 82 | socket?.off("gameEnds"); 83 | }; 84 | }, [user, socket]); 85 | 86 | const joinQueue = () => { 87 | if (!socket) return; 88 | socket.emit("joinRoom_F", user, "", "toPlay"); 89 | }; 90 | 91 | return ( 92 | 93 | {children} 94 | 95 | ); 96 | }; 97 | -------------------------------------------------------------------------------- /frontend/src/hooks/useBlockList.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | 3 | export const useBlockList = () => { 4 | const [blockList, setBlockList] = useState(null); 5 | 6 | const getBlockList = async () => { 7 | const res = await fetch(`${process.env.USERS}/getblockedUsers`, { 8 | method: "GET", 9 | credentials: "include", 10 | }); 11 | const data = await res.json(); 12 | if (data) setBlockList(data.map((user: any) => user.login)); 13 | }; 14 | useEffect(() => { 15 | getBlockList(); 16 | }, []); 17 | 18 | return blockList; 19 | }; 20 | -------------------------------------------------------------------------------- /frontend/src/hooks/useConversations.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useCallback } from "react"; 2 | import axios from "axios"; 3 | import { ConversationProps } from "@/layout/messenger/interfaces"; 4 | import { useChat } from "@/src/context/chat"; 5 | import { useRouter } from "next/router"; 6 | 7 | export const useConversations = () => { 8 | const [conversations, setConversations] = useState([]); 9 | const { message, setMessage } = useChat(); 10 | const { id } = useRouter()?.query; 11 | 12 | const setToRead = () => { 13 | const index = conversations.findIndex((c) => c.id === id); 14 | if (index !== -1) { 15 | const newConversations = [...conversations]; 16 | newConversations[index].unreadMessages = false; 17 | setConversations(newConversations); 18 | } 19 | }; 20 | 21 | useEffect(() => { 22 | return () => { 23 | setToRead(); 24 | }; 25 | }, [id]); 26 | 27 | const getRooms = useCallback(async () => { 28 | await axios 29 | .get(`${process.env.CHAT}/getMyRooms`, { withCredentials: true }) 30 | .then(({ data }) => { 31 | setConversations(data); 32 | }); 33 | }, []); 34 | 35 | useEffect(() => { 36 | getRooms(); 37 | }, [getRooms]); 38 | 39 | useEffect(() => { 40 | if (message) { 41 | setConversations((prev) => { 42 | const index = prev.findIndex((c) => c.id === message.roomId); 43 | if (index !== -1) { 44 | const newConversations = [...prev]; 45 | newConversations[index].lastMessage = message.message; 46 | newConversations[index].lastMessageTime = message.time; 47 | newConversations[index].unreadMessages = true; 48 | return newConversations; 49 | } 50 | return prev; 51 | }); 52 | } 53 | return () => { 54 | setMessage(null); 55 | }; 56 | }, [message, setMessage]); 57 | 58 | return conversations; 59 | }; 60 | -------------------------------------------------------------------------------- /frontend/src/hooks/useFriends.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | import axios from "axios"; 3 | 4 | interface FriendsType { 5 | id: string; 6 | username: string; 7 | avatar: string; 8 | } 9 | 10 | export const useFriends = () => { 11 | const [friends, setFriends] = useState([]); 12 | 13 | useEffect(() => { 14 | const getData = async () => { 15 | await axios 16 | .get(`${process.env.USERS}/getFriends`, { 17 | withCredentials: true, 18 | }) 19 | .then((res) => { 20 | setFriends( 21 | res.data.map((friend: any) => ({ 22 | id: friend.id, 23 | username: friend.login, 24 | avatar: friend.avatarUrl, 25 | })) 26 | ); 27 | }); 28 | }; 29 | getData(); 30 | }, []); 31 | 32 | return friends; 33 | }; 34 | -------------------------------------------------------------------------------- /frontend/src/hooks/useInvitePlayer.ts: -------------------------------------------------------------------------------- 1 | import Router from "next/router"; 2 | import { useGame } from "@/src/context/gameSocket"; 3 | import { useSocket } from "@/src/context/socket"; 4 | 5 | export const useInvitePlayer = () => { 6 | const socket = useGame()?.socket; 7 | const { getStatus } = useSocket(); 8 | 9 | const handleInvite = async (username: string) => { 10 | if (!socket || getStatus(username) !== "online") return; 11 | socket.emit("inviteToPlay", username); 12 | Router.push("/game?map=map_1&requestType=toInvite"); 13 | }; 14 | 15 | return { handleInvite }; 16 | }; 17 | -------------------------------------------------------------------------------- /frontend/src/hooks/useNotifications.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import axios from "axios"; 3 | import { useSocket } from "@/src/context/socket"; 4 | 5 | export const useNotifications = () => { 6 | const { notifications, updateNotifications, setNotifications } = useSocket(); 7 | 8 | useEffect(() => { 9 | const getData = async () => { 10 | await axios 11 | .get(`${process.env.USERS}/getNotifications`, { withCredentials: true }) 12 | .then((res) => { 13 | setNotifications(res.data); 14 | }); 15 | }; 16 | getData(); 17 | }, [setNotifications]); 18 | 19 | return { notifications, updateNotifications }; 20 | }; 21 | -------------------------------------------------------------------------------- /frontend/src/hooks/useOnClickOutside.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | 3 | export function useOnClickOutside( 4 | ref: React.RefObject, 5 | handler: (event: MouseEvent) => void 6 | ) { 7 | useEffect(() => { 8 | const listener = (event: MouseEvent) => { 9 | if (!ref.current || ref.current.contains(event.target as Node)) { 10 | return; 11 | } 12 | handler(event); 13 | }; 14 | document.addEventListener("mousedown", listener); 15 | document.addEventListener("touchstart", listener as any); 16 | return () => { 17 | document.removeEventListener("mousedown", listener); 18 | document.removeEventListener("touchstart", listener as any); 19 | }; 20 | }, [ref, handler]); 21 | } 22 | -------------------------------------------------------------------------------- /frontend/src/hooks/useProfile.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | import axios from "axios"; 3 | 4 | interface ProfileData { 5 | id: string; 6 | username: string; 7 | friends: number; 8 | avatar: string; 9 | score: number; 10 | isFriend: boolean; 11 | } 12 | 13 | export const useProfile = (username: string) => { 14 | const [data, setData] = useState(null); 15 | const [error, setError] = useState(null); 16 | const [loading, setLoading] = useState(false); 17 | 18 | useEffect(() => { 19 | const getData = async () => { 20 | try { 21 | await axios 22 | .get(`${process.env.USERS}/getUser?username=${username}`, { withCredentials: true }) 23 | .then(({ data }) => { 24 | setData({ 25 | id: data.id, 26 | username: data.login, 27 | friends: data.friends, 28 | avatar: data.avatarUrl, 29 | score: data.score, 30 | isFriend: data.isFriend, 31 | }); 32 | }); 33 | } catch (err) { 34 | setError(err); 35 | } finally { 36 | setLoading(false); 37 | } 38 | }; 39 | getData(); 40 | }, [username]); 41 | 42 | return { data, loading, error }; 43 | }; 44 | -------------------------------------------------------------------------------- /frontend/src/hooks/useRooms.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useCallback } from "react"; 2 | import axios from "axios"; 3 | import { toast } from "react-toastify"; 4 | import Router from "next/router"; 5 | 6 | export interface RoomParticipantsType { 7 | id: string; 8 | username: string; 9 | avatar: string; 10 | } 11 | export interface RoomType { 12 | id: string; 13 | name: string; 14 | state: "public" | "protected" | "private"; 15 | participants: RoomParticipantsType[]; 16 | isJoined: boolean; 17 | } 18 | 19 | export const useRooms = () => { 20 | const [rooms, setRooms] = useState([]); 21 | const [passwordError, setPasswordError] = useState(""); 22 | const [createError, setCreateError] = useState(""); 23 | 24 | const getRooms = async () => { 25 | try { 26 | const res = await fetch(`${process.env.CHAT}/getRooms`, { 27 | method: "GET", 28 | credentials: "include", 29 | }); 30 | const data = await res.json(); 31 | if (data) setRooms(data); 32 | } catch (err) {} 33 | }; 34 | 35 | useEffect(() => { 36 | getRooms(); 37 | }, []); 38 | 39 | const handleJoin = useCallback(async (e: any, id: string, password: string) => { 40 | e.preventDefault(); 41 | setPasswordError(""); 42 | 43 | await axios 44 | .post(`${process.env.CHAT}/joinRoom`, { roomId: id, password }, { withCredentials: true }) 45 | .then(({ data }) => { 46 | if (data?.statusCode) setPasswordError(data?.message); 47 | else Router.push(`/messages/${id}`); 48 | }) 49 | .catch((err) => setPasswordError(err?.response?.data?.message)); 50 | }, []); 51 | 52 | const createRoom = useCallback( 53 | async ( 54 | name: string, 55 | state: "public" | "private" | "protected", 56 | password: string | null, 57 | participants: string[] 58 | ) => { 59 | await axios 60 | .post( 61 | `${process.env.CHAT}/createRoom`, 62 | { name, state, password, participants }, 63 | { withCredentials: true } 64 | ) 65 | .then(({ data }) => { 66 | if (data?.statusCode) setCreateError(data?.message); 67 | else { 68 | setCreateError(""); 69 | toast.success("Room created successfully"); 70 | } 71 | }) 72 | .catch((err) => setCreateError(err?.response?.data?.message)); 73 | }, 74 | [] 75 | ); 76 | 77 | return { rooms, handleJoin, passwordError, createRoom, createError }; 78 | }; 79 | -------------------------------------------------------------------------------- /frontend/src/hooks/useWindowSize.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | export function useWindowSize() { 4 | // Initialize state with undefined width/height so server and client renders match 5 | // Learn more here: https://joshwcomeau.com/react/the-perils-of-rehydration/ 6 | 7 | const [windowSize, setWindowSize] = useState({ 8 | width: 0, 9 | height: 0, 10 | }); 11 | 12 | useEffect(() => { 13 | // only execute all the code below in client side 14 | if (typeof window !== "undefined") { 15 | // Handler to call on window resize 16 | const handleResize = () => { 17 | // Set window width/height to state 18 | setWindowSize({ 19 | width: window.innerWidth, 20 | height: window.innerHeight, 21 | }); 22 | }; 23 | 24 | // Add event listener 25 | window.addEventListener("resize", handleResize); 26 | 27 | // Call handler right away so state gets updated with initial window size 28 | handleResize(); 29 | 30 | // Remove event listener on cleanup 31 | return () => window.removeEventListener("resize", handleResize); 32 | } 33 | }, []); // Empty array ensures that effect is only run on mount 34 | return windowSize; 35 | } -------------------------------------------------------------------------------- /frontend/src/layout/Head.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import NextHead from "next/head"; 3 | 4 | interface HeadProps { 5 | title: string; 6 | description?: string; 7 | } 8 | 9 | const Head: FC = ({ title, description }) => { 10 | return ( 11 | <> 12 | 13 | {title || "PongChamp"} 14 | 15 | 16 | 17 | 18 | ); 19 | }; 20 | 21 | export default Head; 22 | -------------------------------------------------------------------------------- /frontend/src/layout/Modal.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import ReactModal from "react-modal"; 3 | import styled from "styled-components"; 4 | import Close from "@/images/icons/close.svg"; 5 | 6 | interface Props { 7 | isOpen: boolean; 8 | closeModal?: () => void; 9 | children?: React.ReactNode; 10 | contentLabel?: string; 11 | } 12 | 13 | const Modal: FC = ({ isOpen, closeModal, children, contentLabel }) => { 14 | return ( 15 | 24 | ); 25 | }; 26 | 27 | export default Modal; 28 | 29 | const Style = styled(ReactModal)` 30 | > h4 { 31 | margin-bottom: 30px; 32 | padding-bottom: 10px; 33 | border-bottom: 1px solid var(--border); 34 | font-weight: 500; 35 | } 36 | `; 37 | -------------------------------------------------------------------------------- /frontend/src/layout/Navbar.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import styled from "styled-components"; 3 | import Link from "next/link"; 4 | import { Message } from "react-iconly"; 5 | import { useRouter } from "next/router"; 6 | import Tippy from "@tippyjs/react"; 7 | 8 | import Logo from "@/images/brand/brand.svg"; 9 | import Diamond from "@/images/icons/diamond.svg"; 10 | import Search from "@/components/NavbarSearch"; 11 | import Level from "@/components/Level"; 12 | import { getLevel } from "@/src/tools"; 13 | import ProfileDropdown from "@/components/dropdowns/Profile"; 14 | import Notifications from "@/components/dropdowns/Notifications"; 15 | import { useAuth } from "@/src/context/auth"; 16 | 17 | const Navbar: FC = () => { 18 | const router = useRouter(); 19 | const user = useAuth()?.user; 20 | 21 | if (router.pathname.startsWith("/login") || router.pathname == "/404") return null; 22 | return ( 23 | 50 | ); 51 | }; 52 | export default Navbar; 53 | 54 | const Style = styled.nav` 55 | display: flex; 56 | justify-content: space-between; 57 | align-items: center; 58 | background-color: var(--background-200); 59 | padding: 10px 24px; 60 | z-index: 10; 61 | position: fixed; 62 | top: 0; 63 | left: 0; 64 | right: 0; 65 | 66 | .search { 67 | flex: 1; 68 | max-width: 500px; 69 | margin: 0 10px; 70 | } 71 | 72 | .logo { 73 | &:focus { 74 | box-shadow: none; 75 | } 76 | } 77 | 78 | .score { 79 | display: inline-flex; 80 | align-items: center; 81 | margin-right: 12px; 82 | padding: 4px 10px; 83 | border-radius: 10px; 84 | 85 | :hover { 86 | background-color: #342e59; 87 | } 88 | 89 | span { 90 | margin-left: 5px; 91 | font-family: var(--font-medium); 92 | } 93 | @media (max-width: 768px) { 94 | display: none; 95 | } 96 | } 97 | 98 | .right { 99 | display: flex; 100 | align-items: center; 101 | gap: 12px; 102 | } 103 | `; 104 | -------------------------------------------------------------------------------- /frontend/src/layout/messenger/ChatRoomParticipants.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useState, useEffect } from "react"; 2 | import styled from "styled-components"; 3 | 4 | import SearchBar from "@/components/SearchInput"; 5 | import Avatar from "@/components/Avatar"; 6 | import ParticipantsOptions from "@/components/dropdowns/ParticipantsOptions"; 7 | import { ParticipantType } from "@/layout/messenger/interfaces"; 8 | import { useAuth } from "@/src/context/auth"; 9 | 10 | interface Props { 11 | roomId: string; 12 | participants: ParticipantType[]; 13 | isAdmin: boolean; 14 | isOwner: boolean; 15 | } 16 | 17 | const ChatRoomParticipants: FC = ({ participants, roomId, isAdmin, isOwner }) => { 18 | const [search, setSearch] = useState(""); 19 | const myId = useAuth()?.user?.id; 20 | 21 | if (participants?.length === 0) return

No users found

; 22 | return ( 23 | 53 | ); 54 | }; 55 | export default ChatRoomParticipants; 56 | 57 | const Style = styled.div` 58 | .participants { 59 | display: flex; 60 | align-items: center; 61 | justify-content: space-between; 62 | margin-bottom: 15px; 63 | > div { 64 | display: flex; 65 | align-items: center; 66 | gap: 10px; 67 | } 68 | span { 69 | color: var(--text-200); 70 | font-size: 14px; 71 | font-weight: 300; 72 | } 73 | .icon { 74 | opacity: 0; 75 | transition: opacity 0.3s ease; 76 | &:focus { 77 | opacity: 1; 78 | transition: opacity 0.3s ease; 79 | } 80 | } 81 | &:hover { 82 | .icon { 83 | opacity: 1; 84 | transition: opacity 0.3s ease; 85 | } 86 | } 87 | } 88 | `; 89 | -------------------------------------------------------------------------------- /frontend/src/layout/messenger/Layout.tsx: -------------------------------------------------------------------------------- 1 | import { ReactElement, ReactNode } from "react"; 2 | import styled from "styled-components"; 3 | 4 | import Head from "@/layout/Head"; 5 | import Conversations from "@/layout/messenger/Conversations"; 6 | import { SocketProvider } from "@/src/context/socket"; 7 | import { ChatSocketProvider } from "@/src/context/chat"; 8 | import { AuthProvider } from "@/src/context/auth"; 9 | import { GameSocketProvider } from "@/src/context/gameSocket"; 10 | 11 | export type MessagesPageWithLayout = { 12 | getLayout: (page: ReactElement) => ReactNode; 13 | }; 14 | 15 | export const MessagesLayout = (page: ReactElement) => { 16 | return ( 17 | 18 | 19 | 20 | 21 | 22 | 28 | 29 | 30 | 31 | 32 | ); 33 | }; 34 | 35 | const Style = styled.main` 36 | background-color: var(--background-200); 37 | padding: 0 10px 10px; 38 | 39 | > div { 40 | background-color: var(--background-100); 41 | display: flex; 42 | height: 100%; 43 | border-radius: 14px; 44 | overflow: hidden; 45 | > div { 46 | flex: 1; 47 | display: flex; 48 | position: relative; 49 | } 50 | @media (max-width: 768px) { 51 | border-radius: 0; 52 | } 53 | } 54 | .conversation { 55 | border-right: 1px solid var(--border); 56 | overflow-x: hidden; 57 | overflow-y: auto; 58 | max-width: 370px; 59 | width: 33.33%; 60 | min-width: 300px; 61 | 62 | @media (max-width: 768px) { 63 | width: 60px; 64 | min-width: 30px; 65 | padding: 0; 66 | border-right: none; 67 | } 68 | } 69 | .messages { 70 | overflow-x: hidden; 71 | overflow-y: auto; 72 | flex: 66.33%; 73 | } 74 | .informations { 75 | overflow-x: hidden; 76 | overflow-y: auto; 77 | max-width: 320px; 78 | min-width: 270px; 79 | flex: 33.33%; 80 | @media (max-width: 992px) { 81 | position: absolute; 82 | background-color: var(--background-100); 83 | top: 0; 84 | right: 0; 85 | bottom: 0; 86 | width: 300px; 87 | box-shadow: -2px 0px 10px 0px rgba(0, 0, 0, 0.2); 88 | z-index: 1; 89 | } 90 | } 91 | @media (max-width: 768px) { 92 | padding: 0; 93 | } 94 | `; 95 | -------------------------------------------------------------------------------- /frontend/src/layout/messenger/interfaces.ts: -------------------------------------------------------------------------------- 1 | // conversations component props 2 | export interface ConversationProps { 3 | id: string; 4 | name: string; 5 | avatar: string; 6 | lastMessage: string; 7 | lastMessageTime: Date; 8 | unreadMessages: boolean; 9 | state: "private" | "protected" | "public" | null; 10 | } 11 | 12 | // Right side informations component of the messenger props 13 | export interface InfoProps { 14 | data: DataType; 15 | setToggle: (value: boolean) => void; 16 | toggle: boolean; 17 | roomId: string; 18 | setState: (value: "private" | "protected" | "public" | null) => void; 19 | state: "private" | "protected" | "public" | null; 20 | } 21 | 22 | // room participants component 23 | export interface ParticipantType { 24 | id: string; 25 | username: string; 26 | avatar: string; 27 | isOwner: boolean; 28 | isAdmin: boolean; 29 | isMutted: boolean; 30 | isBanned: boolean; 31 | } 32 | export interface MessagesType { 33 | id: string; 34 | roomId: string; 35 | username: string; 36 | avatar: string; 37 | message: string; 38 | time: Date; 39 | blockedUsers: string[]; 40 | bannedUsers: string[]; 41 | isRoom: boolean; 42 | } 43 | 44 | // a room or direct conversation data type 45 | export interface DataType { 46 | id: string; 47 | userId: string; 48 | isBlocked: boolean; 49 | name: string; 50 | avatar: string | null; 51 | state: "private" | "protected" | "public" | null; 52 | participants: ParticipantType[] | null; 53 | isAdmin: boolean; 54 | isOwner: boolean; 55 | messages: MessagesType[]; 56 | } 57 | -------------------------------------------------------------------------------- /frontend/src/layout/profile/Layout.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import { ReactElement, ReactNode } from "react"; 3 | import { useRouter } from "next/router"; 4 | 5 | import Head from "@/layout/Head"; 6 | import Sidebar from "@/src/layout/profile/Sidebar"; 7 | import { SocketProvider } from "@/src/context/socket"; 8 | import { ChatSocketProvider } from "@/src/context/chat"; 9 | import { AuthProvider } from "@/src/context/auth"; 10 | import { GameSocketProvider } from "@/src/context/gameSocket"; 11 | 12 | export type ProfilePageWithLayout = { 13 | getLayout: (page: ReactElement) => ReactNode; 14 | }; 15 | 16 | export const ProfileLayout = (page: ReactElement, title?: string) => { 17 | const router = useRouter(); 18 | const { profile } = router.query; 19 | 20 | return ( 21 | 22 | 23 | 24 | 25 | 26 | 30 | 31 | 32 | 33 | 34 | ); 35 | }; 36 | 37 | const Style = styled.main` 38 | display: flex; 39 | flex-direction: row; 40 | background-color: var(--background-200); 41 | `; 42 | const Content = styled.section` 43 | flex: 1; 44 | background-color: var(--background-100); 45 | border-radius: 14px; 46 | margin: 10px; 47 | margin-top: 0; 48 | margin-left: 0; 49 | padding: 30px 50px; 50 | overflow-y: auto; 51 | overflow-x: hidden; 52 | @media (max-width: 878px) { 53 | padding: 30px 20px; 54 | } 55 | h1 { 56 | display: flex; 57 | align-items: center; 58 | margin-bottom: 24px; 59 | gap: 10px; 60 | svg { 61 | width: 2rem; 62 | height: 2rem; 63 | } 64 | } 65 | `; 66 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "baseUrl": ".", 18 | "paths": { 19 | "@/src/*": ["src/*"], 20 | "@/pages/*": ["pages/*"], 21 | "@/components/*": ["src/components/*"], 22 | "@/layout/*": ["src/layout/*"], 23 | "@/hooks/*": ["src/hooks/*"], 24 | "@/images/*": ["public/images/*"], 25 | } 26 | }, 27 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "pages/game/index.tsx", "pages/game/[id].tsx", "pages/.game.tsx", "src/components/.Canvas.tsx"], 28 | "exclude": ["node_modules"] 29 | } 30 | -------------------------------------------------------------------------------- /screenshots/Create room modal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hfadyl/ft_transcendence/b6a90753015261eaeefbf6e188a224db250792d9/screenshots/Create room modal.png -------------------------------------------------------------------------------- /screenshots/Enter username.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hfadyl/ft_transcendence/b6a90753015261eaeefbf6e188a224db250792d9/screenshots/Enter username.png -------------------------------------------------------------------------------- /screenshots/Friends list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hfadyl/ft_transcendence/b6a90753015261eaeefbf6e188a224db250792d9/screenshots/Friends list.png -------------------------------------------------------------------------------- /screenshots/Game.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hfadyl/ft_transcendence/b6a90753015261eaeefbf6e188a224db250792d9/screenshots/Game.png -------------------------------------------------------------------------------- /screenshots/Home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hfadyl/ft_transcendence/b6a90753015261eaeefbf6e188a224db250792d9/screenshots/Home.png -------------------------------------------------------------------------------- /screenshots/Match history.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hfadyl/ft_transcendence/b6a90753015261eaeefbf6e188a224db250792d9/screenshots/Match history.png -------------------------------------------------------------------------------- /screenshots/Profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hfadyl/ft_transcendence/b6a90753015261eaeefbf6e188a224db250792d9/screenshots/Profile.png -------------------------------------------------------------------------------- /screenshots/Settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hfadyl/ft_transcendence/b6a90753015261eaeefbf6e188a224db250792d9/screenshots/Settings.png -------------------------------------------------------------------------------- /screenshots/chat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hfadyl/ft_transcendence/b6a90753015261eaeefbf6e188a224db250792d9/screenshots/chat.png --------------------------------------------------------------------------------