diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml new file mode 100644 index 00000000..d1af4033 --- /dev/null +++ b/.github/workflows/pylint.yml @@ -0,0 +1,25 @@ +name: Pylint + +on: [push] + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.9", "3.10", "3.12"] + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + cp app/back-end/requirements.txt . + pip install -r requirements.txt + pip install pylint pylint-django + - name: Analysing the code with pylint + run: | + cp app/back-end/.pylintrc . + pylint $(git ls-files '*.py') --rcfile=.pylintrc --django-settings=ft_transcendence.settings diff --git a/Makefile b/Makefile index 6f194691..13501efc 100644 --- a/Makefile +++ b/Makefile @@ -1,43 +1,83 @@ -dc-up = docker-compose up -dc-upb = docker-compose up --build -dc-down = docker-compose down -dp = docker ps -drm = docker system prune -af -dc-downp = docker-compose down --rmi all --volumes --remove-orphans +# Makefile +# Set default shell +SHELL := /bin/bash -all: mkdir upb +# Set variables +DOCKER_COMPOSE := docker-compose +DOCKER_COMPOSE_FILE := docker-compose.yml +DOCKER_COMPOSE_FLAGS := --env-file .env +MAJOR ?= 3 +MINOR ?= 8 +PATCH ?= 0 +VERSION := $(MAJOR).$(MINOR).$(PATCH) +DATA_DIR := /Users/${USER}/Desktop/data -mkdir: - mkdir -p /Users/$(USER)/Desktop/data +# Define colors +GREEN := $(shell tput -Txterm setaf 2) +YELLOW := $(shell tput -Txterm setaf 3) +RESET := $(shell tput -Txterm sgr0) -rmdir: down - rm -rf /Users/$(USER)/Desktop/data +# Define targets +.PHONY: help build up down restart logs -up: - $(dc-up) +help: ## Show this help message + @echo "Usage: make [target]" + @echo "" + @echo "Targets:" + @egrep '^(.+)\:\ ##\ (.+)' $(MAKEFILE_LIST) | column -t -c 2 -s ':#' -upb: - $(dc-upb) +build: ## Build Docker images + @echo "$(GREEN)Building Docker images...$(RESET)" + $(DOCKER_COMPOSE) -f $(DOCKER_COMPOSE_FILE) $(DOCKER_COMPOSE_FLAGS) build --no-cache -down: - $(dc-down) +up: ## Start Docker containers + @echo "$(GREEN)Starting Docker containers...$(RESET)" + $(DOCKER_COMPOSE) -f $(DOCKER_COMPOSE_FILE) $(DOCKER_COMPOSE_FLAGS) up -d -ps: - $(dp) +down: ## Stop and remove Docker containers + @echo "$(YELLOW)Stopping and removing Docker containers...$(RESET)" + $(DOCKER_COMPOSE) -f $(DOCKER_COMPOSE_FILE) $(DOCKER_COMPOSE_FLAGS) down -prune: - $(drm) +restart: down up ## Restart Docker containers -front-end: - docker-compose up -d front-end +logs: ## View logs from Docker containers + @echo "$(GREEN)Viewing logs from Docker containers...$(RESET)" + $(DOCKER_COMPOSE) -f $(DOCKER_COMPOSE_FILE) $(DOCKER_COMPOSE_FLAGS) logs -f -back-end: - docker-compose up -d back-end +version: ## Show the current version + @echo "$(GREEN)Current version: $(VERSION)$(RESET)" -data-base: - docker-compose up -d data-base +remove-data-dir: ## Remove the data directory + @echo "$(YELLOW)Removing data directory $(DATA_DIR)...$(RESET)" + rm -rf $(DATA_DIR) -clean: - $(dc-downp) +create-data-dir: ## Create the data directory + @echo "$(GREEN)Creating data directory $(DATA_DIR)...$(RESET)" + mkdir -p $(DATA_DIR) -fclean: rmdir prune +remove-volumes: ## Remove Docker volumes + @echo "$(YELLOW)Removing Docker volumes...$(RESET)" + $(DOCKER_COMPOSE) -f $(DOCKER_COMPOSE_FILE) $(DOCKER_COMPOSE_FLAGS) down -v + +.PHONY: clean-all +clean-all: down remove-volumes remove-data-dir ## Clean up Docker containers, volumes, and data directory + @echo "$(GREEN)Clean up completed.$(RESET)" + +clean: clean-all ## Remove build artifacts and temporary files + @echo "$(YELLOW)Cleaning up build artifacts and temporary files...$(RESET)" + rm -rf ./app/front-end/node_modules + rm -rf ./app/front-end/.next + rm -rf ./app/back-end/__pycache__ + find . -type f -name '*.pyc' -delete + find . -type d -name '__pycache__' -delete + +.PHONY: update-version +update-version: ## Update the version number + @read -p "Enter new version (MAJOR.MINOR.PATCH): " NEW_VERSION; \ + MAJOR=$$(echo $$NEW_VERSION | cut -d'.' -f1); \ + MINOR=$$(echo $$NEW_VERSION | cut -d'.' -f2); \ + PATCH=$$(echo $$NEW_VERSION | cut -d'.' -f3); \ + sed -i '' "s/MAJOR ?= [0-9]*/MAJOR ?= $$MAJOR/" $(MAKEFILE_LIST); \ + sed -i '' "s/MINOR ?= [0-9]*/MINOR ?= $$MINOR/" $(MAKEFILE_LIST); \ + sed -i '' "s/PATCH ?= [0-9]*/PATCH ?= $$PATCH/" $(MAKEFILE_LIST); \ + echo "$(GREEN)Version updated to $$NEW_VERSION$(RESET)" diff --git a/app/back-end/.pylintrc b/app/back-end/.pylintrc new file mode 100644 index 00000000..5103165c --- /dev/null +++ b/app/back-end/.pylintrc @@ -0,0 +1,33 @@ +[MASTER] +load-plugins=pylint_django +django-settings-module=ft_transcendence.settings + +[MESSAGES CONTROL] +disable=missing-docstring, + import-error, + invalid-name, + too-few-public-methods, + abstract-method, + arguments-renamed, + too-many-locals, + too-many-branches, + too-many-statements, + import-outside-toplevel, + arguments-differ, + no-else-return + +[FORMAT] +max-line-length=120 + +[DESIGN] +max-parents=13 + +[TYPECHECK] +ignored-modules=django.contrib.admin + +[REPORTS] +output-format=colorized +reports=no + +[BASIC] +good-names=i,j,k,ex,pk,Run,_ \ No newline at end of file diff --git a/app/back-end/authentication/admin.py b/app/back-end/authentication/admin.py index 4cabdd4f..7683dbf4 100644 --- a/app/back-end/authentication/admin.py +++ b/app/back-end/authentication/admin.py @@ -1,9 +1,7 @@ -"""Module providing a User class.""" from django.contrib import admin from .models import User class UserAdmin(admin.ModelAdmin): - "Class for user display in admin page" list_display = ('id', 'email', 'username','first_name', 'last_name', 'is_staff', 'is_active', 'date_joined', 'is_superuser', 'last_login', 'image_url', 'cover_url', 'location') @@ -11,7 +9,6 @@ class UserAdmin(admin.ModelAdmin): 'date_joined', 'is_superuser', 'last_login', 'image_url', 'cover_url', 'location') readonly_fields = ('id', 'date_joined', 'last_login') - filter_horizontal = () list_filter = () fieldsets = () diff --git a/app/back-end/authentication/models.py b/app/back-end/authentication/models.py index a8a654b3..37c01499 100644 --- a/app/back-end/authentication/models.py +++ b/app/back-end/authentication/models.py @@ -1,10 +1,7 @@ -"""Module providing the data from the database mounted to models""" from django.db import models from django.contrib.auth.models import PermissionsMixin, AbstractBaseUser, BaseUserManager class UserManager(BaseUserManager): - """Class representing a User Manager""" - def create_user(self, email, password=None, **extra_fields): """Function create a User""" if not email: @@ -16,7 +13,6 @@ def create_user(self, email, password=None, **extra_fields): return user def create_superuser(self, email, password=None, **extra_fields): - """Function create a Superuser""" extra_fields.setdefault('is_staff', True) extra_fields.setdefault('is_superuser', True) if extra_fields.get('is_staff') is not True: @@ -24,10 +20,8 @@ def create_superuser(self, email, password=None, **extra_fields): if extra_fields.get('is_superuser') is not True: raise ValueError('Superuser must have is_superuser=True.') return self.create_user(email, password, **extra_fields) - class User(AbstractBaseUser, PermissionsMixin): - """Class representing a User""" username = models.CharField(max_length=150) email = models.EmailField(max_length=254, unique=True) password = models.CharField(max_length=128) @@ -54,6 +48,4 @@ class User(AbstractBaseUser, PermissionsMixin): objects = UserManager() USERNAME_FIELD = 'email' class Meta: - """Class to change the behavior of your model fields""" db_table = 'authentication_users' - \ No newline at end of file diff --git a/app/back-end/authentication/serializer.py b/app/back-end/authentication/serializer.py index 4827c766..56045d44 100644 --- a/app/back-end/authentication/serializer.py +++ b/app/back-end/authentication/serializer.py @@ -1,4 +1,3 @@ -"""Module providing rest serailizers""" import sys import requests import pyotp @@ -31,7 +30,8 @@ def validate(self, data): if user.is_2fa_enabled: user.two_fa_secret_key = pyotp.random_base32() user.save() - url_code = pyotp.totp.TOTP(user.two_fa_secret_key).provisioning_uri(name = user.email, issuer_name = "ft_transcendence") + url_code = pyotp.totp.TOTP(user.two_fa_secret_key).provisioning_uri( + name = user.email, issuer_name = "ft_transcendence") data['user'] = user data['url_code'] = url_code return data @@ -55,15 +55,20 @@ def validate(self, data): class SocialAuthSerializer(serializers.Serializer): def validate(self, data): + """ + Validate method for SocialAuth serializer + """ token = self.context.get('access_token') platform = self.context.get('platform') headers = {'Authorization': f'Bearer {token}'} if platform == 'github': try: - response = requests.get('https://api.github.com/user', headers=headers) + response = requests.get('https://api.github.com/user', + headers=headers, timeout=10000) response.raise_for_status() user_info = response.json() - email_response = requests.get('https://api.github.com/user/emails', headers=headers) + email_response = requests.get('https://api.github.com/user/emails', + headers=headers, timeout=10000) email_response.raise_for_status() email_info = email_response.json() email = next((email['email'] for email in email_info if email['primary']), None) @@ -89,8 +94,8 @@ def validate(self, data): raise serializers.ValidationError("Email already exists") from e elif platform == 'google': try : - response = requests.get('https://www.googleapis.com/oauth2/v1/userinfo?alt=json', headers=headers, - timeout=1000) + response = requests.get('https://www.googleapis.com/oauth2/v1/userinfo?alt=json', + headers=headers,timeout=10000) response.raise_for_status() user_info = response.json() email = user_info['email'] @@ -106,13 +111,14 @@ def validate(self, data): data['email'] = email return data except requests.exceptions.RequestException as e: - raise serializers.ValidationError("Failed to fetch user data from Google") + raise serializers.ValidationError("Failed to fetch user data from Google") except IntegrityError as e: raise serializers.ValidationError("Email already exists") from e elif platform == "42": try: print(headers, file=sys.stderr) - response = requests.get('https://api.intra.42.fr/v2/me', headers=headers, timeout=1000) + response = requests.get('https://api.intra.42.fr/v2/me', headers=headers, + timeout=1000) response.raise_for_status() user_info = response.json() email = user_info['email'] @@ -127,6 +133,8 @@ def validate(self, data): data['email'] = email return data except requests.exceptions.RequestException as e: - raise serializers.ValidationError("Failed to fetch user data from 42") + raise serializers.ValidationError("Failed to fetch user data from 42") except IntegrityError as e: raise serializers.ValidationError("Email already exists") from e + else : + return None diff --git a/app/back-end/authentication/tests.py b/app/back-end/authentication/tests.py index 01faac3c..2a93f9c4 100644 --- a/app/back-end/authentication/tests.py +++ b/app/back-end/authentication/tests.py @@ -1,12 +1,7 @@ from django.test import TestCase -from rest_framework.test import APIClient, force_authenticate +from rest_framework.test import APIClient from rest_framework import status from .models import User -import requests_mock -from django.test import TestCase, RequestFactory -from .views import GithubLogin -import jwt -from django.test import Client class SignInTest(TestCase): def setUp(self): @@ -14,23 +9,23 @@ def setUp(self): self.user = User.objects.create_user( email='zakariaemrabet48@gmail.com', password='admin') def test_login(self): - response = self.client.post('/api/sign-in', {'email': 'zakariaemrabet48@gmail.com', 'password': 'admin'}, format='json') + response = self.client.post('/api/sign-in', + {'email': 'zakariaemrabet48@gmail.com', + 'password': 'admin'}, format='json') self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertIn('tokens', response.data) - self.assertIn('access', response.data['tokens']) - self.assertIn('refresh', response.data['tokens']) + self.assertIn('access', response.data) + self.assertIn('refresh', response.data) + self.assertIn('email', response.data) + self.assertIn('is_2fa_enabled', response.data) - -class GithubLoginTest(TestCase): +class SignUpTest(TestCase): def setUp(self): - self.client = Client() - self.view = GithubLogin.as_view() + self.client = APIClient() - @requests_mock.Mocker() - def test_github_login(self, m): - m.get('https://api.github.com/user', json={'login': 'zakarm', 'id': 1}) - m.get('https://api.github.com/user/emails', json=[{'email': 'zakariaemrabet1@gmail.com', 'primary': True, 'verified': True}]) - response = self.client.post('/api/github', {'token': 'testtoken'}, format='json') - self.assertEqual(response.status_code, 200) - decoded_jwt = jwt.decode(response.data['access'], options={"verify_signature": False}) - self.assertEqual(decoded_jwt['user_id'], 1) \ No newline at end of file + def test_signup(self): + response = self.client.post('/api/sign-up', + {'email': 'zakariaemrabet48@gmail.com', + 'username': 'testuser', + 'password': 'admin'}, format='json') + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertIn('email', response.data) diff --git a/app/back-end/authentication/urls.py b/app/back-end/authentication/urls.py index 23161391..520961b6 100644 --- a/app/back-end/authentication/urls.py +++ b/app/back-end/authentication/urls.py @@ -1,13 +1,18 @@ -from django.urls import path, include, re_path -from .views import * +""" +Module providing urls utils +""" +from django.urls import path, re_path +from .views import (SignIn2Fa, SignInView, SocialAuthExchangeView, + SocialAuthRedirectView, SignUpView, SignOutView) urlpatterns = [ path("sign-up", SignUpView.as_view(), name="sign-up"), path("sign-in", SignInView.as_view(), name="sign-in"), - path("sign-out", SignUpView.as_view(), name="sign-out"), + path("sign-out", SignOutView.as_view(), name="sign-out"), - # re_path(r'^social/(?P(github|42|google))/auth$', SocialAuthView.as_view(), name='social-auth'), path('two-fa', SignIn2Fa.as_view(), name="two-fa"), - re_path(r'^social/(?P(github|42|google))/redirect$', SocialAuthRedirectView.as_view(), name='social-redirect'), - re_path(r'^social/(?P(github|42|google))/callback$', SocialAuthExchangeView.as_view(), name='social-callback'), -] \ No newline at end of file + re_path(r'^social/(?P(github|42|google))/redirect$', + SocialAuthRedirectView.as_view(), name='social-redirect'), + re_path(r'^social/(?P(github|42|google))/callback$', + SocialAuthExchangeView.as_view(), name='social-callback'), +] diff --git a/app/back-end/authentication/views.py b/app/back-end/authentication/views.py index e2f77994..1a0ae09e 100644 --- a/app/back-end/authentication/views.py +++ b/app/back-end/authentication/views.py @@ -16,10 +16,8 @@ SocialAuthSerializer) class SignUpView(APIView): - """Class for sign up""" serializer_class = UsersSignUpSerializer def post(self, request): - """Function post""" if User.objects.filter(email=request.data['email']).exists(): return Response({"error": "Email already exists"}, status=status.HTTP_409_CONFLICT) if User.objects.filter(username=request.data['username']).exists(): @@ -33,10 +31,8 @@ def post(self, request): return Response(data, status=status.HTTP_201_CREATED) class SignInView(APIView): - """Class for sign in""" serializer_class = UserSignInSerializer def post(self, request): - """Post function""" serializer = self.serializer_class(data=request.data) if serializer.is_valid(): data = serializer.validated_data @@ -54,10 +50,11 @@ def post(self, request): return Response(serializer.errors, status=status.HTTP_401_UNAUTHORIZED) class SignIn2Fa(APIView): - """Class for two fact auth""" serializer_class = User2FASerializer def post(self, request): - """Post function""" + """ + Post function + """ serializer = self.serializer_class(data = request.data) if serializer.is_valid(): user = serializer.validated_data @@ -68,9 +65,7 @@ def post(self, request): }, status = status.HTTP_200_OK) return Response(serializer.errors, status=status.HTTP_401_UNAUTHORIZED) - class SignOutView(APIView): - """Class for sign out""" def post(self, request): """Post function""" refresh_token = request.data.get('refresh') @@ -81,10 +76,8 @@ def post(self, request): return Response({'message:', 'Successfully logged out'}, status=status.HTTP_200_OK) class SocialAuthExchangeView(APIView): - """Class for exchanging the oauth code""" serializer_class = SocialAuthSerializer def get(self, request, platform): - """Get function""" code = request.GET.get('code') platform = platform.lower() if not code : @@ -118,46 +111,60 @@ def get(self, request, platform): url = "https://api.intra.42.fr/oauth/token" headers = {'Accept': 'application/json'} try: - response = requests.post(url, data=data, headers=headers) + response = requests.post(url, data=data, headers=headers, timeout=20000) response.raise_for_status() except HTTPError as e: return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST) access_token = response.json().get('access_token') - serializer = self.serializer_class(data=request.data, context={"platform": platform, "access_token": access_token}) + serializer = self.serializer_class(data=request.data, context={"platform": platform, + "access_token": access_token}) serializer.is_valid(raise_exception=True) email = serializer.validated_data.get('email') try: user = User.objects.get(email=email) - except User.DoesNotExist: + except User.objects.model.DoesNotExist: return Response({'error': 'User does not exist'}, status=status.HTTP_400_BAD_REQUEST) if user and user.is_active: refresh = RefreshToken.for_user(user) - access_token = str(refresh.access_token) + access_token = refresh.access_token response = HttpResponseRedirect(settings.FRONTEND_HOST) - response.set_cookie('access_token', access_token, httponly=True, samesite='Lax', secure=True) - response.set_cookie('refresh_token', refresh, httponly=True, samesite='Lax', secure=True) + response.set_cookie('access', access_token) + response.set_cookie('refresh', refresh) return response else: return Response({'error': 'Invalid credentials'}, status=status.HTTP_400_BAD_REQUEST) - - + class SocialAuthRedirectView(APIView): - """Class for autorize page""" def get(self, request, platform): - """Get function""" platform = platform.lower() if platform == 'github': CLIENT_ID = settings.GITHUB_CLIENT_ID REDIRECT_URI = settings.GITHUB_REDIRECT_URI - return redirect(f'https://github.com/login/oauth/authorize?client_id={CLIENT_ID}&redirect_uri={REDIRECT_URI}&scope=user:email') + url = ( + f'https://github.com/login/oauth/authorize?client_id={CLIENT_ID}' + f'&redirect_uri={REDIRECT_URI}&scope=user:email' + ) + return redirect(url) elif platform == 'google': CLIENT_ID = settings.GOOGLE_CLIENT_ID REDIRECT_URI = settings.GOOGLE_REDIRECT_URI - SCOPE = "https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email" - return redirect(f'https://accounts.google.com/o/oauth2/v2/auth?client_id={CLIENT_ID}&redirect_uri={REDIRECT_URI}&scope={urllib.parse.quote(SCOPE)}&response_type=code') + SCOPE = ( + "https://www.googleapis.com/auth/userinfo.profile" + " https://www.googleapis.com/auth/userinfo.email" + ) + url = ( + f'https://accounts.google.com/o/oauth2/v2/auth?client_id={CLIENT_ID}' + f'&redirect_uri={REDIRECT_URI}&scope={urllib.parse.quote(SCOPE)}&response_type=code' + ) + return redirect(url) elif platform == '42': CLIENT_ID = settings.FORTYTWO_CLIENT_ID REDIRECT_URI = settings.FORTYTWO_REDIRECT_URI SCOPE = "public" - return redirect(f'https://api.intra.42.fr/oauth/authorize?client_id={CLIENT_ID}&redirect_uri={REDIRECT_URI}&scope={SCOPE}&response_type=code') \ No newline at end of file + url = ( + f'https://api.intra.42.fr/oauth/authorize?client_id={CLIENT_ID}' + f'&redirect_uri={REDIRECT_URI}&scope={SCOPE}&response_type=code' + ) + return redirect(url) + return None diff --git a/app/back-end/dashboards/admin.py b/app/back-end/dashboards/admin.py index 8c38f3f3..5d28852b 100644 --- a/app/back-end/dashboards/admin.py +++ b/app/back-end/dashboards/admin.py @@ -1,3 +1,2 @@ -from django.contrib import admin - +# from django.contrib import admin # Register your models here. diff --git a/app/back-end/dashboards/models.py b/app/back-end/dashboards/models.py index 71a83623..474ee081 100644 --- a/app/back-end/dashboards/models.py +++ b/app/back-end/dashboards/models.py @@ -1,3 +1,2 @@ -from django.db import models - +# from django.db import models # Create your models here. diff --git a/app/back-end/dashboards/serializer.py b/app/back-end/dashboards/serializer.py index 3ebf4c95..b7e6ee96 100644 --- a/app/back-end/dashboards/serializer.py +++ b/app/back-end/dashboards/serializer.py @@ -1,9 +1,11 @@ -"""Module providing rest serializers""" from rest_framework import serializers from authentication.models import User from game.models import Match -from .utils import * -import sys +from .utils import (get_total_games, + get_win_games, + get_lose_games, + get_monthly_game_stats, + get_total_minutes) class MatchSerializer(serializers.ModelSerializer): class Meta: @@ -11,15 +13,12 @@ class Meta: fields = '__all__' class MainDashboardSerializer(serializers.ModelSerializer): - """Serializer class for main dashboard""" matches_as_user_one = serializers.SerializerMethodField() matches_as_user_two = serializers.SerializerMethodField() total_minutes = serializers.SerializerMethodField() - class Meta: - """Meta class for MainDashboard serializer""" model = User - fields = ('username', 'email', 'first_name', 'last_name', + fields = ('username', 'email', 'first_name', 'last_name', 'image_url', 'matches_as_user_one', 'matches_as_user_two', 'total_minutes') @@ -29,45 +28,38 @@ def get_matches_as_user_one(self, obj): return serializer.data def get_matches_as_user_two(self, obj): - matches = Match.objects.filter(user_one=obj)[:10] + matches = Match.objects.filter(user_two=obj)[:10] serializer = MatchSerializer(matches, many=True) return serializer.data - + def get_total_minutes(self, obj): return get_total_minutes(obj) - + class ProfileSerializer(serializers.ModelSerializer): - """Serializer class for Profile""" total_games = serializers.SerializerMethodField() win_games = serializers.SerializerMethodField() lose_games = serializers.SerializerMethodField() monthly_stats = serializers.SerializerMethodField() class Meta: - """Meta class for Profile serializer""" model = User fields = ('username', 'email', 'first_name', 'last_name', - 'intro', 'quote', 'rank', 'level', 'score', - 'location', 'total_games', 'win_games', + 'intro', 'quote', 'rank', 'level', 'score', 'cover_url' + 'location', 'total_games', 'win_games', 'image_url', 'lose_games', 'monthly_stats') def get_total_games(self, obj): - """Calculate the total number of games played by the user.""" return get_total_games(obj) def get_win_games(self, obj): - """Calculate the total number of games win.""" return get_win_games(obj) def get_lose_games(self, obj): - """Calculate the total number of games lose.""" return get_lose_games(obj) def get_monthly_stats(self, obj): - """Calculate the total number of games win and lose in past 6 months.""" return get_monthly_game_stats(obj) def to_representation(self, instance): - """Customize the serialized data representation.""" data = super().to_representation(instance) - return data \ No newline at end of file + return data diff --git a/app/back-end/dashboards/tests.py b/app/back-end/dashboards/tests.py index 7ce503c2..601fc861 100644 --- a/app/back-end/dashboards/tests.py +++ b/app/back-end/dashboards/tests.py @@ -1,3 +1,2 @@ -from django.test import TestCase - +# from django.test import TestCase # Create your tests here. diff --git a/app/back-end/dashboards/urls.py b/app/back-end/dashboards/urls.py index 2ba6127f..eb32ca86 100644 --- a/app/back-end/dashboards/urls.py +++ b/app/back-end/dashboards/urls.py @@ -1,7 +1,7 @@ -from django.urls import path, include, re_path -from .views import * +from django.urls import path +from .views import MainDashboardView, ProfileView urlpatterns = [ path('dashboard', MainDashboardView.as_view(), name='dashboard'), path('profile', ProfileView.as_view(), name='profile') -] \ No newline at end of file +] diff --git a/app/back-end/dashboards/utils.py b/app/back-end/dashboards/utils.py index acc043b8..4ba02076 100644 --- a/app/back-end/dashboards/utils.py +++ b/app/back-end/dashboards/utils.py @@ -1,36 +1,30 @@ -from django.db.models import F, Q from datetime import datetime, timedelta from game.models import Match -import sys +from django.db.models import F, Q def get_total_games(obj): - """ - Calculate the total number of games played by the user. - """ user_matches_as_one = Match.objects.filter(user_one=obj) user_matches_as_two = Match.objects.filter(user_two=obj) return user_matches_as_one.count() + user_matches_as_two.count() - + def get_win_games(obj): - """ - Calculate the total number of games win. - """ - win_matches_as_one = Match.objects.filter(user_one = obj).filter(score_user_one__gt = F('score_user_two')).count() - win_matches_as_two = Match.objects.filter(user_two = obj).filter(score_user_two__gt = F('score_user_one')).count() + win_matches_as_one = Match.objects.filter(user_one = + obj).filter(score_user_one__gt = + F('score_user_two')).count() + win_matches_as_two = Match.objects.filter(user_two = + obj).filter(score_user_two__gt = + F('score_user_one')).count() return win_matches_as_one + win_matches_as_two def get_lose_games(obj): - """ - Calculate the total number of games lose. - """ - lose_matches_as_one = Match.objects.filter(user_one = obj).filter(score_user_one__lt = F('score_user_two')).count() - lose_matches_as_two = Match.objects.filter(user_two = obj).filter(score_user_two__lt = F('score_user_one')).count() + lose_matches_as_one = Match.objects.filter(user_one = + obj).filter(score_user_one__lt = + F('score_user_two')).count() + lose_matches_as_two = Match.objects.filter(user_two = obj).filter(score_user_two__lt = + F('score_user_one')).count() return lose_matches_as_one + lose_matches_as_two def get_monthly_game_stats(obj): - """ - Calculate the total number of games win in past 6 months. - """ win_month = [] lose_month = [] current_year = datetime.now().year @@ -106,4 +100,4 @@ def get_total_minutes(obj): total_minutes = sum(match_durations) minutes_months.insert(0, int(total_minutes)) - return {"months": months, "minutes_months": minutes_months} \ No newline at end of file + return {"months": months, "minutes_months": minutes_months} diff --git a/app/back-end/dashboards/views.py b/app/back-end/dashboards/views.py index f11afb62..58b5ecd9 100644 --- a/app/back-end/dashboards/views.py +++ b/app/back-end/dashboards/views.py @@ -1,38 +1,24 @@ -"""Module providing rest views""" -from django.shortcuts import render from rest_framework.views import APIView from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response from rest_framework_simplejwt.authentication import JWTAuthentication from .serializer import MainDashboardSerializer, ProfileSerializer -from game.models import Match -from authentication.models import User -from rest_framework.response import Response -from django.db.models import Prefetch -import sys class MainDashboardView(APIView): - """ - This view requires JWT authentication and is only accessible to authenticated users. - """ authentication_classes = [JWTAuthentication] permission_classes = [IsAuthenticated] def get(self, request): - """ - Handle GET requests to retrieve data for the main dashboard. - """ user = request.user serializer_data = MainDashboardSerializer(instance=user) return Response(serializer_data.data) class ProfileView(APIView): - """This view requires JWT authentication and is only accessible to authenticated users.""" authentication_classes = [JWTAuthentication] permission_classes = [IsAuthenticated] def get(self, request): - """Handle GET requests to retrieve data for the profile.""" user = request.user serializer_data = ProfileSerializer(instance=user) - return Response(serializer_data.data) \ No newline at end of file + return Response(serializer_data.data) diff --git a/app/back-end/ft_transcendence/urls.py b/app/back-end/ft_transcendence/urls.py index d7edabb8..f5ab7659 100644 --- a/app/back-end/ft_transcendence/urls.py +++ b/app/back-end/ft_transcendence/urls.py @@ -16,7 +16,9 @@ """ from django.contrib import admin from django.urls import path, include -from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView, TokenVerifyView +from rest_framework_simplejwt.views import (TokenObtainPairView, + TokenRefreshView, + TokenVerifyView) urlpatterns = [ path('admin/', admin.site.urls), diff --git a/app/back-end/game/admin.py b/app/back-end/game/admin.py index 81d14fba..8b45aff9 100644 --- a/app/back-end/game/admin.py +++ b/app/back-end/game/admin.py @@ -1,11 +1,10 @@ from django.contrib import admin from .models import Match -# Register your models here. + class UserAdmin(admin.ModelAdmin): "Class for user display in admin page" list_display = ('match_id', 'user_one', 'user_two','score_user_one', 'score_user_two') search_fields = ('match_id', 'user_one', 'user_two','score_user_one', 'score_user_two') - # readonly_fields = ('id', 'date_joined', 'last_login') filter_horizontal = () list_filter = () diff --git a/app/back-end/game/asgi.py b/app/back-end/game/asgi.py index eab12922..8d5b6ba4 100644 --- a/app/back-end/game/asgi.py +++ b/app/back-end/game/asgi.py @@ -1,10 +1,10 @@ -import os +# import os from django.core.asgi import get_asgi_application from channels.routing import ProtocolTypeRouter -from game.routing import application, websocket_urlpatterns +from game.routing import websocket_urlpatterns application = ProtocolTypeRouter({ "http": get_asgi_application(), "websocket": websocket_urlpatterns, -}) \ No newline at end of file +}) diff --git a/app/back-end/game/consumers.py b/app/back-end/game/consumers.py index 72649844..e59bde03 100644 --- a/app/back-end/game/consumers.py +++ b/app/back-end/game/consumers.py @@ -1,11 +1,11 @@ +# from rest_framework_simplejwt.exceptions import InvalidToken, TokenError +# from rest_framework_simplejwt.tokens import UntypedToken +# from urllib.parse import parse_qs +import sys +import json from channels.generic.websocket import AsyncWebsocketConsumer -from rest_framework_simplejwt.tokens import UntypedToken -from rest_framework_simplejwt.exceptions import InvalidToken, TokenError -import json, sys -from urllib.parse import parse_qs class AsyncConsumer(AsyncWebsocketConsumer): - async def connect(self): # query_string = parse_qs(self.scope['query_string'].decode()) # token = query_string.get('token') @@ -17,7 +17,7 @@ async def connect(self): # except (InvalidToken, TokenError) as e: # await self.close() # else: - await self.accept() + await self.accept() async def disconnect(self, close_code): pass @@ -32,4 +32,4 @@ async def receive(self, text_data): await self.send(text_data=json.dumps({ 'message': message - })) \ No newline at end of file + })) diff --git a/app/back-end/game/models.py b/app/back-end/game/models.py index b914c4c3..940b458c 100644 --- a/app/back-end/game/models.py +++ b/app/back-end/game/models.py @@ -1,8 +1,6 @@ -"""Module providing the data from the database mounted to models""" from django.db import models class Match(models.Model): - """Class representing Matches""" match_id = models.AutoField(primary_key=True) user_one = models.ForeignKey('authentication.User', models.DO_NOTHING, db_column='user_one', related_name='match_user_one_set') @@ -15,27 +13,22 @@ class Match(models.Model): tackle_user_one = models.IntegerField() tackle_user_two = models.IntegerField() class Meta: - """Class to change the behavior of your model fields""" db_table = 'Match' class Tournaments(models.Model): - """Class representing Tournaments""" tournament_id = models.AutoField(primary_key=True) tournament_name = models.CharField(max_length=30) tournament_start = models.DateField() tournament_end = models.DateField() class Meta: - """Class to change the behavior of your model fields""" db_table = 'Tournaments' class Tournamentsmatches(models.Model): - """Class representing Tournamentsmatches""" tournament = models.OneToOneField(Tournaments, models.DO_NOTHING, primary_key=True) match = models.ForeignKey(Match, models.DO_NOTHING) tournament_round = models.CharField(max_length=30) class Meta: - """Class to change the behavior of your model fields""" db_table = 'TournamentsMatches' unique_together = (('tournament', 'match'),) diff --git a/app/back-end/game/routing.py b/app/back-end/game/routing.py index e1a6615e..8713d336 100644 --- a/app/back-end/game/routing.py +++ b/app/back-end/game/routing.py @@ -10,4 +10,4 @@ application = ProtocolTypeRouter({ "http": get_asgi_application(), "websocket": URLRouter(websocket_urlpatterns), -}) \ No newline at end of file +}) diff --git a/app/back-end/game/tests.py b/app/back-end/game/tests.py index 7ce503c2..601fc861 100644 --- a/app/back-end/game/tests.py +++ b/app/back-end/game/tests.py @@ -1,3 +1,2 @@ -from django.test import TestCase - +# from django.test import TestCase # Create your tests here. diff --git a/app/back-end/game/urls.py b/app/back-end/game/urls.py index 699b7c15..637600f5 100644 --- a/app/back-end/game/urls.py +++ b/app/back-end/game/urls.py @@ -1,3 +1 @@ -urlpatterns = [ - -] \ No newline at end of file +urlpatterns = [] diff --git a/app/back-end/game/views.py b/app/back-end/game/views.py index 91ea44a2..60f00ef0 100644 --- a/app/back-end/game/views.py +++ b/app/back-end/game/views.py @@ -1,3 +1 @@ -from django.shortcuts import render - # Create your views here. diff --git a/app/back-end/init.py b/app/back-end/init.py index e2acf600..462dc1e4 100644 --- a/app/back-end/init.py +++ b/app/back-end/init.py @@ -1,4 +1,4 @@ -#This script is not for django, it is a custom script to check if the database is available and +#This script is not for django, it is a custom script to check if the database is available and #then run the migrations and start the server [Kolchi moujtahid, m3ndnach m3a lkousala] import socket @@ -24,11 +24,11 @@ def ping_postgres(): dbname=POSTGRES_DB ) conn.close() - logging.info(f"Connection to {POSTGRES_HOST} successful") + logging.info("Connection to %s successful", POSTGRES_HOST) time.sleep(3) return True except psycopg2.OperationalError: - logging.info(f"Connection to {POSTGRES_HOST} failed") + logging.info("Connection to %s failed", POSTGRES_HOST) return False @@ -40,7 +40,7 @@ def database_connection(): with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: s.settimeout(1) s.connect((POSTGRES_HOST, int(POSTGRES_PORT))) - logging.info(f"{POSTGRES_HOST} container is available") + logging.info("%s container is available", POSTGRES_HOST) while not ping_postgres(): time.sleep(1) break @@ -52,18 +52,18 @@ def database_connection(): def migrate_and_run_server(): logging.info("Applying migrations...") make_migrations_cmd = "python manage.py makemigrations authentication" - subprocess.run(make_migrations_cmd.split()) + subprocess.run(make_migrations_cmd.split(), check=True) make_migrations_cmd = "python manage.py makemigrations game" - subprocess.run(make_migrations_cmd.split()) + subprocess.run(make_migrations_cmd.split(), check=True) make_migrations_cmd = "python manage.py makemigrations dashboards" - subprocess.run(make_migrations_cmd.split()) + subprocess.run(make_migrations_cmd.split(), check=True) migrate_cmd = "python manage.py migrate" - subprocess.run(migrate_cmd.split()) + subprocess.run(migrate_cmd.split(), check=True) time.sleep(3) create_superuser() logging.info("Starting server...") server_cmd = "python manage.py runserver 0.0.0.0:8000" - subprocess.run(server_cmd.split()) + subprocess.run(server_cmd.split(), check=True) def create_superuser(): conn = psycopg2.connect( @@ -88,7 +88,8 @@ def create_superuser(): '--noinput', '--email', email, ] - subprocess.run(create_superuser_cmd, input=f'{password}\n{password}\n', text=True, check=True) + subprocess.run(create_superuser_cmd, input=f'{password}\n{password}\n', + text=True, check=True) else: logging.info("Superuser already exists in the database") cursor.close() @@ -98,4 +99,4 @@ def create_superuser(): if __name__ == "__main__": logging.basicConfig(level=logging.INFO) database_connection() - migrate_and_run_server() \ No newline at end of file + migrate_and_run_server() diff --git a/app/back-end/models.py b/app/back-end/models.py index 0df96e87..b1704be2 100644 --- a/app/back-end/models.py +++ b/app/back-end/models.py @@ -1,6 +1,5 @@ from django.db import models - class Achievements(models.Model): achievement_id = models.AutoField(primary_key=True) achiev_name = models.CharField(max_length=45) @@ -9,79 +8,41 @@ class Meta: managed = False db_table = 'Achievements' - class Friendship(models.Model): freindship_id = models.AutoField(primary_key=True) - user_from = models.ForeignKey('Users', models.DO_NOTHING, db_column='user_from') - user_to = models.ForeignKey('Users', models.DO_NOTHING, db_column='user_to', related_name='friendship_user_to_set') + user_from = models.ForeignKey('Users', models.DO_NOTHING, + db_column='user_from') + user_to = models.ForeignKey('Users', models.DO_NOTHING, + db_column='user_to', + related_name='friendship_user_to_set') is_accepted = models.BooleanField() class Meta: managed = False db_table = 'Friendship' - -class Matches(models.Model): - match_id = models.AutoField(primary_key=True) - user_one = models.ForeignKey('Users', models.DO_NOTHING, db_column='user_one') - user_two = models.ForeignKey('Users', models.DO_NOTHING, db_column='user_two', related_name='matches_user_two_set') - score_user_one = models.IntegerField() - score_user_two = models.IntegerField() - match_start = models.DateField() - match_end = models.DateField() - tackle_user_one = models.IntegerField() - tackle_user_two = models.IntegerField() - - class Meta: - managed = False - db_table = 'Matches' - - class Messages(models.Model): - user_one = models.OneToOneField('Users', models.DO_NOTHING, db_column='user_one', primary_key=True) # The composite primary key (user_one, user_two) found, that is not supported. The first column is selected. - user_two = models.ForeignKey('Users', models.DO_NOTHING, db_column='user_two', related_name='messages_user_two_set') + user_one = models.OneToOneField('Users', models.DO_NOTHING, + db_column='user_one', primary_key=True) + user_two = models.ForeignKey('Users', models.DO_NOTHING, + db_column='user_two', + related_name='messages_user_two_set') message_content = models.CharField(max_length=512) message_date = models.DateField() message_direction = models.CharField(max_length=20) - class Meta: managed = False db_table = 'Messages' unique_together = (('user_one', 'user_two'),) - -class Tournaments(models.Model): - tournament_id = models.AutoField(primary_key=True) - tournament_name = models.CharField(max_length=30) - tournament_start = models.DateField() - tournament_end = models.DateField() - - class Meta: - managed = False - db_table = 'Tournaments' - - -class Tournamentsmatches(models.Model): - tournament = models.OneToOneField(Tournaments, models.DO_NOTHING, primary_key=True) # The composite primary key (tournament_id, match_id) found, that is not supported. The first column is selected. - match = models.ForeignKey(Matches, models.DO_NOTHING) - tournament_round = models.CharField(max_length=30) - - class Meta: - managed = False - db_table = 'TournamentsMatches' - unique_together = (('tournament', 'match'),) - - class Userachievements(models.Model): - user = models.OneToOneField('Users', models.DO_NOTHING, primary_key=True) # The composite primary key (user_id, achivement_id) found, that is not supported. The first column is selected. + user = models.OneToOneField('Users', models.DO_NOTHING, primary_key=True) achivement = models.ForeignKey(Achievements, models.DO_NOTHING) achive_date = models.DateField() - class Meta: managed = False db_table = 'UserAchievements' unique_together = (('user', 'achivement'),) - class Users(models.Model): user_id = models.AutoField(primary_key=True) first_name = models.CharField(max_length=20) @@ -98,7 +59,6 @@ class Users(models.Model): country = models.CharField(max_length=60) city = models.CharField(max_length=60) password = models.CharField(max_length=200) - class Meta: managed = False - db_table = 'Users' \ No newline at end of file + db_table = 'Users' diff --git a/app/front-end/Dockerfile b/app/front-end/Dockerfile index b3fa748d..d0618960 100644 --- a/app/front-end/Dockerfile +++ b/app/front-end/Dockerfile @@ -8,6 +8,8 @@ RUN npm install COPY . . +RUN npm run build + EXPOSE 3000 CMD ["npm", "run", "dev"] diff --git a/app/front-end/next.config.mjs b/app/front-end/next.config.mjs index 6b4990d6..afa78f83 100644 --- a/app/front-end/next.config.mjs +++ b/app/front-end/next.config.mjs @@ -2,10 +2,14 @@ const nextConfig = { // productionBrowserSourceMaps: false, // Disable source maps in development // optimizeFonts: false, // Disable font optimization - // minify: false, + minify: true, images: { domains: ['cdn.cloudflare.steamstatic.com', 'ddragon.leagueoflegends.com'], - }, + }, + eslint: + { + ignoreDuringBuilds: true, + }, }; export default nextConfig; diff --git a/app/front-end/package-lock.json b/app/front-end/package-lock.json index 29dd529c..ef49f4b1 100644 --- a/app/front-end/package-lock.json +++ b/app/front-end/package-lock.json @@ -17,6 +17,7 @@ "jquery": "^3.7.1", "js-cookie": "^3.0.5", "js-cookies": "^1.0.4", + "jwt-decode": "^4.0.0", "next": "14.1.3", "popper.js": "^1.16.1", "react": "^18.2.0", @@ -4786,6 +4787,14 @@ "node": ">=4.0" } }, + "node_modules/jwt-decode": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", + "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", + "engines": { + "node": ">=18" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", diff --git a/app/front-end/package.json b/app/front-end/package.json index 8f25b26f..c18a5c3d 100644 --- a/app/front-end/package.json +++ b/app/front-end/package.json @@ -18,6 +18,7 @@ "jquery": "^3.7.1", "js-cookie": "^3.0.5", "js-cookies": "^1.0.4", + "jwt-decode": "^4.0.0", "next": "14.1.3", "popper.js": "^1.16.1", "react": "^18.2.0", diff --git a/app/front-end/src/app/(authentication)/sign-in/page.tsx b/app/front-end/src/app/(authentication)/sign-in/page.tsx index cb00d572..404ccd38 100644 --- a/app/front-end/src/app/(authentication)/sign-in/page.tsx +++ b/app/front-end/src/app/(authentication)/sign-in/page.tsx @@ -13,6 +13,7 @@ import SocialAuth from "../../../components/socialAuth"; import TwoFa from '../../../components/twoFa'; import '../../global.css' + export default function SignInPage() { const router = useRouter(); const [twoFaData, setTwoFaData] = useState<{ value: string; email: string }>(); @@ -41,8 +42,8 @@ export default function SignInPage() { else { toast.success('Successfully signed in !'); - Cookies.set("access", access) - Cookies.set("refresh", refresh) + Cookies.set("access", access, { path: '/' }) + Cookies.set("refresh", refresh, { path: '/' }) router.push('/dashboard') } } diff --git a/app/front-end/src/app/chat/layout.tsx b/app/front-end/src/app/chat/layout.tsx index b7f35b71..80986fa9 100644 --- a/app/front-end/src/app/chat/layout.tsx +++ b/app/front-end/src/app/chat/layout.tsx @@ -1,13 +1,14 @@ import React from 'react'; import MainContainer from '../../components/mainContainer'; +import AuthChecker from "../../components/authChecker"; export default function ChatLayout({children}: {children: React.ReactNode}) { return ( - <> + {children} - + ); } \ No newline at end of file diff --git a/app/front-end/src/app/chat/page.tsx b/app/front-end/src/app/chat/page.tsx index 878a9911..2a16aed8 100644 --- a/app/front-end/src/app/chat/page.tsx +++ b/app/front-end/src/app/chat/page.tsx @@ -1,26 +1,69 @@ "use client"; - import styles from './style.module.css'; import ChatAbout from '@/components/chat_about'; +import ChatFriendsResp from '@/components/chat_friend_resp'; import ChatFriends from '@/components/chat_friends'; import ChatMessages from '@/components/chat_messages'; +import { useEffect, useState } from 'react'; +import Modal from 'react-bootstrap/Modal'; +import Offcanvas from 'react-bootstrap/Offcanvas'; export default function () { - return ( - <> -
-
- -
-
- -
-
- -
+ const [show, setShow] = useState(false); + + const [showAbout, setAbout] = useState(false); + + const handleClose = () => setAbout(false); + + const [fullscreen, setFullscreen] = useState((window.innerWidth <= 768) ? true : false); + + useEffect(() => { + const handleResize = () => { + if (window.innerWidth <= 768) + setFullscreen(true); + else + setFullscreen(false); + }; + + window.addEventListener('resize', handleResize); + + // Cleanup: remove the event listener when the component unmounts + return () => { + window.removeEventListener('resize', handleResize); + }; + }, []); // Empty dependency array ensures the effect runs only once on mount + + + return ( + <> +
+
+ { + (fullscreen) ? + () : + () + } +  
+
+ + setShow(false)} animation> + - - ); + + + + + + + + +
+
+ +
+
+ + ); } \ No newline at end of file diff --git a/app/front-end/src/app/chat/style.module.css b/app/front-end/src/app/chat/style.module.css index acf7061e..4faa7e2f 100644 --- a/app/front-end/src/app/chat/style.module.css +++ b/app/front-end/src/app/chat/style.module.css @@ -5,9 +5,25 @@ font-family: 'itim'; } +.chat_modal{ + background-color: #000000; +} + +.canvas{ + background-color: #181B20; + z-index: 10; +} + @media (max-width: 1200px) { + .about{ + display: none; + } +} + +@media (max-width: 768px) +{ .chat{ display: none; } diff --git a/app/front-end/src/app/dashboard/layout.tsx b/app/front-end/src/app/dashboard/layout.tsx index 04644630..1fdefc64 100644 --- a/app/front-end/src/app/dashboard/layout.tsx +++ b/app/front-end/src/app/dashboard/layout.tsx @@ -1,13 +1,14 @@ -'use client' +'use client'; import MainContainer from "../../components/mainContainer"; -import React, { useState } from 'react'; +import AuthChecker from "../../components/authChecker"; +import React from 'react'; export default function DashboardLayout({ children }: { children: React.ReactNode }) { return ( - <> + - {children} + {children} - + ); } \ No newline at end of file diff --git a/app/front-end/src/app/dashboard/page.tsx b/app/front-end/src/app/dashboard/page.tsx index 8000a028..7dac744b 100644 --- a/app/front-end/src/app/dashboard/page.tsx +++ b/app/front-end/src/app/dashboard/page.tsx @@ -8,21 +8,126 @@ import { FaChevronDown } from "react-icons/fa"; import GameHistoryCard from '../../components/table'; import ButtonValo from '../../components/button' import { Line } from 'react-chartjs-2'; -import Chart from 'chart.js/auto'; +import { Chart } from 'chart.js/auto'; +import Cookies from 'js-cookie'; +import { useState, useEffect } from 'react'; +import { ChartOptions, ChartData } from 'chart.js'; +import { LineController } from 'chart.js/auto'; +import { useRouter } from 'next/navigation' import { CategoryScale, LinearScale, Title, Legend, - Tooltip, - LineController, + Tooltip, PointElement, LineElement } from 'chart.js'; +interface TotalMinutes { + months: string[]; + minutes_months: number[]; +} + +interface Matches { + match_id: number; + score_user_one: number; + score_user_two: number; + match_start: string; + match_end: string; + tackle_user_one: number; + tackle_user_two: number; + user_one: number; + user_two: number; +} + +interface UserData { + username: string; + email: string; + first_name: string | null; + last_name: string | null; + matches_as_user_one: Matches[]; + matches_as_user_two: Matches[]; + total_minutes: TotalMinutes; +} + +interface GameData { + player: string; + score: number; + date: string; + result: 'WIN' | 'LOSS'; + } export default function Dashboard() { - Chart.register(CategoryScale, LinearScale, Title, Legend, Tooltip, LineController, PointElement, LineElement); + const [dashboardData, setDashboardData] = useState(null); + const router = useRouter(); + const gameData: GameData[] = []; + useEffect(() => { + const fetchData = async () => + { + const access = Cookies.get('access'); + + if (access) { + try { + const response = await fetch('http://localhost:8000/api/dashboard', { + headers: { Authorization: `Bearer ${access}` }, + }); + if (response.ok) { + const data = await response.json(); + setDashboardData(data); + } else if (response.status === 401) { + console.log('Unauthorized'); + } else { + console.error('An unexpected error happened:', response.status); + } + } catch (error) { + console.error('An unexpected error happened:', error); + } + } else { + console.log('Access token is undefined or falsy'); + } + }; + fetchData(); + }, []); + + if (dashboardData) { + dashboardData.matches_as_user_one.forEach((match) => { + gameData.push({ + player: dashboardData.username, + score: match.score_user_one, + date: match.match_start, + result: match.score_user_one > match.score_user_two ? 'WIN' : 'LOSS', + }); + }); + + dashboardData.matches_as_user_two.forEach((match) => { + gameData.push({ + player: dashboardData.username, + score: match.score_user_two, + date: match.match_start, + result: match.score_user_two > match.score_user_one ? 'WIN' : 'LOSS', + }); + }); + } + + function clickButton(){ + router.push('/game'); + } + + const chartLabels = dashboardData?.total_minutes.months || []; + const chartData = dashboardData?.total_minutes.minutes_months || []; + + Chart.register( + CategoryScale, + LinearScale, + Title, + Legend, + Tooltip, + LineController, + PointElement, + LineElement); - const options: Chart.ChartOptions = { + Chart.defaults.font.family = 'Itim'; + Chart.defaults.font.size = 14; + const options: ChartOptions<'line'> = { responsive: true, plugins: { legend: { @@ -35,99 +140,96 @@ export default function Dashboard() { }, }; - const labels: string[] = ['January', 'February', 'March', 'April', 'May', 'June', 'July']; - Chart.defaults.font.family = 'Itim'; - const data: Chart.ChartData = { + const labels: string[] = chartLabels; + const data: ChartData<'line'> = { labels, datasets: [ - { - label: 'time', - data: [10, 20, 30, 40, 30, 15, 28, 25, 40, 50], - borderColor: 'rgb(255, 99, 132, 0)', - backgroundColor: 'rgb(255, 99, 132, 0.5)', - fill: true, - font: { - family: 'itim', - size: 14, + { + label: 'time', + data: chartData, + borderColor: 'rgb(255, 99, 132, 0)', + backgroundColor: 'rgb(255, 99, 132, 0.5)', + fill: true, }, - }, ], }; return ( -
-
-
+
+ {/*
*/}
-
+
upcoming
-
-

THE ULTIMATE PING-PONG GAME

-
+ {/*
*/} + THE ULTIMATE PING-PONG GAME + {/*
*/}
-
Welcome to our online ping pong paradise! Dive into + Welcome to our online ping pong paradise! Dive into the fast-paced world of table tennis with our website, where players of all levels can connect, compete, and improve their skills. From casual matches to intense tournaments, we've got everything you need to serve up some excitement! -
-
-
- +
+
-
-
- anime charachter +
+
+
+ anime charachter +
-
-
-
-
-
-
-
-

GAME HISTORY

-
-
-

ALL

+ {/*
*/} +
+
+
+
+
+
+
+

GAME HISTORY

+
+
+

ALL

+
+
+
-
-
-
-
-
-
-

MY GAME STATS

+
+
+
+
+
+

MY GAME STATS

+
+
+

ALL

+
-
-

ALL

+
+
+ +  
-
-
-
 
- -
-
); } \ No newline at end of file diff --git a/app/front-end/src/app/dashboard/style.module.css b/app/front-end/src/app/dashboard/style.module.css index 4dd6b262..ed014d01 100644 --- a/app/front-end/src/app/dashboard/style.module.css +++ b/app/front-end/src/app/dashboard/style.module.css @@ -23,7 +23,10 @@ .card { border-radius: 15px; - height: 85%; +<<<<<<< HEAD +======= + height: 100%; +>>>>>>> 8f93d643e161cb735f2123c0b7c055a373c1dabc } .buttom_cards { @@ -46,24 +49,31 @@ .med_titles { color: #FFEBEB; - font-size: medium; - width: 100%; } .small_title { color: #FF4755; } .imageContainer { + /* height: 350px; */ + position: relative; +} + +.ping_img{ + position: absolute; + right: 0; + bottom: 1px; height: 350px; + width: 350px; } -.imageContainer > img { +/* .imageContainer > img { position: absolute; right: 0; max-width: 100%; max-height: 100%; object-fit: contain; -} +} */ .page_body { overflow: auto; @@ -113,15 +123,22 @@ cursor: pointer; height: 64px; } - .imageContainer > img { + /* .imageContainer > img { position: static; margin: 0 auto; - } + } */ .chart_grid { margin-top: 20px; } .card { - height: 100%; + /* height: 100%; */ border-radius: 15px; } } + +@media (max-width: 992px) +{ + .imageContainer{ + height: 350px; + } +} diff --git a/app/front-end/src/app/game/layout.tsx b/app/front-end/src/app/game/layout.tsx index 04644630..53cdb4ca 100644 --- a/app/front-end/src/app/game/layout.tsx +++ b/app/front-end/src/app/game/layout.tsx @@ -1,13 +1,14 @@ 'use client' import MainContainer from "../../components/mainContainer"; -import React, { useState } from 'react'; +import AuthChecker from "../../components/authChecker" +import React from 'react'; export default function DashboardLayout({ children }: { children: React.ReactNode }) { return ( - <> - - {children} - - + + + {children} + + ); } \ No newline at end of file diff --git a/app/front-end/src/app/profile/layout.tsx b/app/front-end/src/app/profile/layout.tsx index 9c83ce7c..9e8a98d9 100644 --- a/app/front-end/src/app/profile/layout.tsx +++ b/app/front-end/src/app/profile/layout.tsx @@ -1,14 +1,15 @@ import MainContainer from '../../components/mainContainer'; +import AuthChecker from "../../components/authChecker"; import React from 'react'; export default function ProfileLayout({children}: {children: React.ReactNode}) { return ( - <> + {children} - + ); } \ No newline at end of file diff --git a/app/front-end/src/app/profile/page.tsx b/app/front-end/src/app/profile/page.tsx index af85ce31..4347c67e 100644 --- a/app/front-end/src/app/profile/page.tsx +++ b/app/front-end/src/app/profile/page.tsx @@ -11,8 +11,7 @@ import { CategoryScale, LinearScale, Title, Legend, - Tooltip, - LineController, + Tooltip, PointElement, LineElement } from 'chart.js'; @@ -31,6 +30,8 @@ import { ImUsers } from "react-icons/im"; import { SiRepublicofgamers } from "react-icons/si"; import { BsFillChatLeftQuoteFill } from "react-icons/bs"; import { MdRoundaboutRight } from "react-icons/md"; +import { ChartOptions, ChartData } from 'chart.js'; +import { LineController } from 'chart.js/auto'; export default function () { @@ -62,7 +63,8 @@ export default function () Chart.register(CategoryScale, LinearScale, Title, Legend, Tooltip, LineController, PointElement, LineElement); Chart.defaults.font.family = 'Itim'; - const data: Chart.ChartData = { + Chart.defaults.font.size = 14; + const data: ChartData<'line'> = { labels: profile.summary.months, datasets: [ { @@ -70,10 +72,6 @@ export default function () data: profile.summary.win, borderColor: 'rgba(116,206,151, 0.5)', backgroundColor: 'green', - font: { - family: 'itim', - size: 14, - }, fill: false, tension: 0.1, }, @@ -82,16 +80,12 @@ export default function () data: profile.summary.loss, borderColor: 'rgba(181,55,49, 0.5)', backgroundColor: 'red', - font: { - family: 'itim', - size: 14, - }, fill: false, tension: 0.1, } ] }; - const options: Chart.ChartOptions = { + const options: ChartOptions<'line'> = { responsive: true, plugins: { legend: { diff --git a/app/front-end/src/components/Splayer.tsx b/app/front-end/src/components/Splayer.tsx index fa21154b..470715fe 100644 --- a/app/front-end/src/components/Splayer.tsx +++ b/app/front-end/src/components/Splayer.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React , { forwardRef } from 'react'; import Dropdown from 'react-bootstrap/Dropdown'; import styles from './styles/Splayer.module.css' import Image from 'next/image' @@ -8,49 +8,26 @@ interface PlayerProps { id: number; image: string; isConnected: boolean; -} - -interface CustomToggleProps { - onClick: () => void; - children: React.ReactNode; -} - -const CustomToggle = React.forwardRef( - ({ children, onClick }, ref) => ( -
- {children} -
- ) -); -CustomToggle.displayName = 'CustomToggle'; +} export default function Player({ nickname, id, image, isConnected }: PlayerProps) { return (
- - - Profile - - - opt 1 -
- opt 2 -
- opt 3 -
-
-
+
+ Profile +
+
{nickname}
diff --git a/app/front-end/src/components/authChecker.tsx b/app/front-end/src/components/authChecker.tsx new file mode 100644 index 00000000..4ac86fe7 --- /dev/null +++ b/app/front-end/src/components/authChecker.tsx @@ -0,0 +1,52 @@ +"use client" + +import React, { useEffect, useState } from 'react'; +import { useRouter } from 'next/navigation'; +import Cookies from 'js-cookie'; +import Spinner from 'react-bootstrap/Spinner' +import styles from './styles/authChecker.module.css' + + +const AuthChecker = ({ children }: { children: React.ReactNode }) => { + const router = useRouter(); + const [isAuthenticated, setIsAuthenticated] = useState(null); + + useEffect(() => { + const authentication = async () => { + const access = Cookies.get('access'); + console.log(access) + if (access) { + const response = await fetch('http://localhost:8000/api/verify', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ token: access }) + }); + if (response.ok) { + setIsAuthenticated(true); + } else { + setIsAuthenticated(false); + router.push('/sign-in'); + } + } + else + { + setIsAuthenticated(false); + router.push('/sign-in'); + } + }; + authentication(); + }, []); + + if (isAuthenticated === null) { + return ( +
+ +

LOADING ...

+
+ ); + } + + return isAuthenticated ? <>{children} : null; +}; + +export default AuthChecker; diff --git a/app/front-end/src/components/button.tsx b/app/front-end/src/components/button.tsx index 32ca1809..c646bf73 100644 --- a/app/front-end/src/components/button.tsx +++ b/app/front-end/src/components/button.tsx @@ -2,18 +2,17 @@ import styles from './styles/button.module.css' interface Props{ value? : string; + onClick: () => void; } -export default function ButtonValo({value}: Props) { +export default function ButtonValo({value, onClick}: Props) { return ( - <>
-
- ); } \ No newline at end of file diff --git a/app/front-end/src/components/chat_about.tsx b/app/front-end/src/components/chat_about.tsx index e05439b5..07aba625 100644 --- a/app/front-end/src/components/chat_about.tsx +++ b/app/front-end/src/components/chat_about.tsx @@ -1,48 +1,51 @@ - +"use client"; import Image from 'next/image'; import React from 'react'; -import StepsPrograssBar from 'react-line-progress-bar-steps'; +// import StepsPrograssBar from 'react-line-progress-bar-steps'; import { Radar } from 'react-chartjs-2'; import styles from './styles/chat_about.module.css'; import Chart from 'chart.js/auto'; - +import { ChartOptions, ChartData, RadarController } from 'chart.js'; import { IoCloseCircleSharp } from "react-icons/io5"; -export default function ChatAbout() +interface Props{ + handleClose: () => void; +} + +export default function ChatAbout({handleClose}: Props) { const data = Chart.ChartData = { labels: [ - 'Win', - 'Loss', - 'High score', - 'Time', - 'totale games' + 'Win', + 'Loss', + 'High score', + 'Time', + 'totale games' ], datasets: [{ - label: 'My First Dataset', - data: [65, 59, 90, 81, 150], - fill: true, - pointBackgroundColor: '#FFFFFF', - pointBorderColor: 'rgba(0, 255, 0)', - pointHoverBackgroundColor: '#fff', - pointHoverBorderColor: 'rgb(255, 99, 132)', - color: '#fe4755', - backgroundColor: 'rgba(255, 99, 132, 0.2)', // Example dataset background color - borderColor: '#FE4755', // Example dataset border color - tickColor: '#FFFFFF' + label: 'My First Dataset', + data: [65, 59, 90, 81, 150], + fill: true, + pointBackgroundColor: '#FFFFFF', + pointBorderColor: 'rgba(0, 255, 0)', + pointHoverBackgroundColor: '#fff', + pointHoverBorderColor: 'rgb(255, 99, 132)', + backgroundColor: 'rgba(255, 99, 132, 0.2)', // Example dataset background color + borderColor: '#FE4755', // Example dataset border color + // tickColor: '#FFFFFF' }] - }; + }; - const options: Chart.ChartOptions = { + const options: ChartOptions<'radar'> = { responsive: true, plugins: { - legend: { - position: 'top', - }, - title: { - display: true, - text: `Win/Loss for test` - } + legend: { + position: 'top', + }, + title: { + display: true, + text: `Win/Loss for test` + } }, scales: { r: { @@ -56,49 +59,49 @@ export default function ChatAbout() color: '#FE4755' }, ticks: { - color: '#FE4755', + color: '#FE4755', } } } - } + } return ( <> -
-
-
+
+
+
-
welcome
+
welcome
OTHMAN NOUAKCHI - Casablanca, Morocco - Game on! 🎮 Play hard, level up! 💪 + Casablanca, Morocco + Game on! 🎮 Play hard, level up! 💪
-
 
+
 
-
+

- High score: 1337 + High score: 1337 Rank: 90135
Matches
-
- +
+ {/* */}
Tournaments
-
- +
+ {/* */}
diff --git a/app/front-end/src/components/chat_friend_resp.tsx b/app/front-end/src/components/chat_friend_resp.tsx new file mode 100644 index 00000000..566a8be5 --- /dev/null +++ b/app/front-end/src/components/chat_friend_resp.tsx @@ -0,0 +1,79 @@ + +import styles from './styles/chat_friends.module.css'; +import Image from 'next/image'; +import { InputGroup } from 'react-bootstrap'; +import friends from '@/components/friends.json'; +import Form from 'react-bootstrap/Form'; +import User from '@/components/user'; + +import { CiSearch } from "react-icons/ci"; +import UserChatResp from './user_chat_resp'; + +interface Props{ + setAbout: React.Dispatch>; +} + +export default function ChatFriendsResp( { setAbout }: Props ) { + + const friendsData = friends.sort((usr1, usr2) => { + if (usr1.connected && !usr2.connected) { + return -1; + } + // Sort disconnected users second + if (!usr1.connected && usr2.connected) { + return 1; + } + // Sort by ID if isConnected flag is the same + return usr1.id - usr2.id; + }) + // .slice(0, 5) + .map((user, index) => + + ); + + const handleShow = () => setAbout(true); + + return ( + <> +
+
+
+ + GAME HUB + LET'S CHAT + & PLAY + + welcome +
+
+ + + + +
+
+ {friendsData} +
+
+
+
+ + + + + + + + + + + + + + + +
+
+ + ); +} \ No newline at end of file diff --git a/app/front-end/src/components/chat_friends.tsx b/app/front-end/src/components/chat_friends.tsx index c23fefdf..92fa110e 100644 --- a/app/front-end/src/components/chat_friends.tsx +++ b/app/front-end/src/components/chat_friends.tsx @@ -1,15 +1,19 @@ - +"use client"; import styles from './styles/chat_friends.module.css'; import Image from 'next/image'; import { InputGroup } from 'react-bootstrap'; -import UserChat from '@/components/user_chat'; +import UserChat from './user_chat'; import friends from '@/components/friends.json'; import Form from 'react-bootstrap/Form'; -import User from '@/components/user'; - +import User from './user'; import { CiSearch } from "react-icons/ci"; -export default function ChatFriends() { +interface Props{ + setShow: React.Dispatch>; + setAbout: React.Dispatch>; +} + +export default function ChatFriends( {setShow , setAbout}: Props ) { const friendsData = friends.sort((usr1, usr2) => { if (usr1.connected && !usr2.connected) { @@ -27,6 +31,8 @@ export default function ChatFriends() { ); + const handleShow = () => setAbout(true); + return ( <>
@@ -48,20 +54,23 @@ export default function ChatFriends() {
{friendsData}
-
+
- - - - - - - - - - - + + + + + + + + + + + + + +
diff --git a/app/front-end/src/components/chat_messages.tsx b/app/front-end/src/components/chat_messages.tsx index 53422bea..e6c7e876 100644 --- a/app/front-end/src/components/chat_messages.tsx +++ b/app/front-end/src/components/chat_messages.tsx @@ -1,4 +1,4 @@ - +"use client"; import styles from './styles/chat_messages.module.css'; import { InputGroup } from 'react-bootstrap'; import Image from 'next/image'; @@ -9,37 +9,38 @@ import { FaTableTennisPaddleBall } from 'react-icons/fa6'; import { ImUserMinus } from 'react-icons/im'; import { IoIosSend } from "react-icons/io"; -export default function ChatMessages() { - return ( - <> -
-
-
- welcome -
-
-
!Snake_007
-
Online
-
-
- - -
-
-
CHAT
-
- - - - -
+export default function ChatMessages( ) { + + return ( + <> +
+
+
+ welcome +
+
+
!Snake_007
+
Online
+
+
+ + +
+
+
CHAT
+
+ + + +
- - ); +
+ + ); } \ No newline at end of file diff --git a/app/front-end/src/components/friends.json b/app/front-end/src/components/friends.json index f5ae8379..4b142129 100644 --- a/app/front-end/src/components/friends.json +++ b/app/front-end/src/components/friends.json @@ -13,7 +13,7 @@ }, { "id": 9876, - "nickname": "ValorantPlayer2", + "nickname": "ValorantPlayer1", "image_url": "https://cdn.cloudflare.steamstatic.com/steam/apps/1086940/header.jpg?t=1639029468", "connected": true }, @@ -83,6 +83,96 @@ "image_url": "https://ddragon.leagueoflegends.com/cdn/img/champion/splash/Annie_0.jpg", "connected": true }, + { + "id": 5432, + "nickname": "ValorantPlayer8", + "image_url": "https://cdn.cloudflare.steamstatic.com/steam/apps/1086940/header.jpg?t=1639029468", + "connected": true + }, + { + "id": 5432, + "nickname": "ValorantPlayer8", + "image_url": "https://cdn.cloudflare.steamstatic.com/steam/apps/1086940/header.jpg?t=1639029468", + "connected": true + }, + { + "id": 5432, + "nickname": "ValorantPlayer8", + "image_url": "https://cdn.cloudflare.steamstatic.com/steam/apps/1086940/header.jpg?t=1639029468", + "connected": true + }, + { + "id": 5432, + "nickname": "ValorantPlayer8", + "image_url": "https://cdn.cloudflare.steamstatic.com/steam/apps/1086940/header.jpg?t=1639029468", + "connected": true + }, + { + "id": 5432, + "nickname": "ValorantPlayer8", + "image_url": "https://cdn.cloudflare.steamstatic.com/steam/apps/1086940/header.jpg?t=1639029468", + "connected": true + }, + { + "id": 5432, + "nickname": "ValorantPlayer8", + "image_url": "https://cdn.cloudflare.steamstatic.com/steam/apps/1086940/header.jpg?t=1639029468", + "connected": true + }, + { + "id": 5432, + "nickname": "ValorantPlayer8", + "image_url": "https://cdn.cloudflare.steamstatic.com/steam/apps/1086940/header.jpg?t=1639029468", + "connected": true + }, + { + "id": 5432, + "nickname": "ValorantPlayer8", + "image_url": "https://cdn.cloudflare.steamstatic.com/steam/apps/1086940/header.jpg?t=1639029468", + "connected": true + }, + { + "id": 5432, + "nickname": "ValorantPlayer8", + "image_url": "https://cdn.cloudflare.steamstatic.com/steam/apps/1086940/header.jpg?t=1639029468", + "connected": true + }, + { + "id": 5432, + "nickname": "ValorantPlayer8", + "image_url": "https://cdn.cloudflare.steamstatic.com/steam/apps/1086940/header.jpg?t=1639029468", + "connected": true + }, + { + "id": 5432, + "nickname": "ValorantPlayer8", + "image_url": "https://cdn.cloudflare.steamstatic.com/steam/apps/1086940/header.jpg?t=1639029468", + "connected": true + }, + { + "id": 5432, + "nickname": "ValorantPlayer8", + "image_url": "https://cdn.cloudflare.steamstatic.com/steam/apps/1086940/header.jpg?t=1639029468", + "connected": true + }, + { + "id": 5432, + "nickname": "ValorantPlayer8", + "image_url": "https://cdn.cloudflare.steamstatic.com/steam/apps/1086940/header.jpg?t=1639029468", + "connected": true + }, + { + "id": 5432, + "nickname": "ValorantPlayer8", + "image_url": "https://cdn.cloudflare.steamstatic.com/steam/apps/1086940/header.jpg?t=1639029468", + "connected": true + }, + { + "id": 5432, + "nickname": "ValorantPlayer8", + "image_url": "https://cdn.cloudflare.steamstatic.com/steam/apps/1086940/header.jpg?t=1639029468", + "connected": true + }, { "id": 5432, "nickname": "ValorantPlayer8", diff --git a/app/front-end/src/components/inviteFriend.tsx b/app/front-end/src/components/inviteFriend.tsx new file mode 100644 index 00000000..8918f48d --- /dev/null +++ b/app/front-end/src/components/inviteFriend.tsx @@ -0,0 +1,73 @@ +"use client"; + +import styles from './styles/inviteFriend.module.css'; +import React, { useState } from 'react'; +import { InputGroup, Modal, Form, Button } from 'react-bootstrap'; +import friends from './friends.json'; +import Splayer from "./Splayer"; + +import { TiUserAdd } from "react-icons/ti"; +import { IoIosSearch } from "react-icons/io"; +import { ImUserPlus } from "react-icons/im"; + +interface Props{ + show: boolean; + close: () => void; +} + +export default function InviteFriend( {show, close}: Props) { + + const [searchTerm, setSearchTerm] = useState(''); + const [searchedFriends, setSearchedFriends] = useState<{nickname: string, image_url: string, connected: boolean}[]>([]); + const handle_search = () => { + if (searchTerm === '') + setSearchedFriends([]); + else + { + const foundFriends = friends.filter(friend => friend.nickname.toLowerCase().startsWith(searchTerm.toLowerCase())); + setSearchedFriends(foundFriends); + } + } + + return ( + <> + + + + Add Friend + + + + {setSearchTerm(e.target.value)}} + /> + + + + + { + searchedFriends.map((user, index) => + ( +
+
+
+
+ ) + ) + } +
+ +
+
+
+ + ); +} \ No newline at end of file diff --git a/app/front-end/src/components/mainContainer.tsx b/app/front-end/src/components/mainContainer.tsx index 8fe7d45b..00f6f96f 100644 --- a/app/front-end/src/components/mainContainer.tsx +++ b/app/front-end/src/components/mainContainer.tsx @@ -8,11 +8,13 @@ import SrightBar from "./srightBar"; import Togglebar from './toggleBar'; import styles from './styles/mainContainer.module.css' import Image from 'next/image' +import InviteFriend from "./inviteFriend"; export default function MainContainer({ children }: { children: React.ReactNode }) { - const [show, setShow] = useState(false); - const [showSide, setShowSide] = useState(false); + const [show, setShow] = useState(false); + const [showSide, setShowSide] = useState(false); + const [friendModal, setFriendModal] = useState(false); const handleClose = () => setShow(false); const toggleShow = () => setShow((s) => !s); const handleToggle = () => setShowSide(false); @@ -47,12 +49,13 @@ export default function MainContainer({ children }: { children: React.ReactNode
- + setFriendModal(true)} show={show} setShow={setShow} handleClose={handleClose} toggleShow={toggleShow}/>
- + setFriendModal(true)} toggleShow={toggleShow}/>
+ setFriendModal(false)}/>
diff --git a/app/front-end/src/components/rightBar.tsx b/app/front-end/src/components/rightBar.tsx index 7a315082..2bf58ec2 100644 --- a/app/front-end/src/components/rightBar.tsx +++ b/app/front-end/src/components/rightBar.tsx @@ -7,7 +7,7 @@ import Player from "./Player"; import Notification from "./Notification"; import Offcanvas from 'react-bootstrap/Offcanvas'; import friends from './friends.json'; -import React, { forwardRef } from 'react'; +import React, { forwardRef, useState } from 'react'; import styles from './styles/rightBar.module.css'; import Image from 'next/image'; @@ -18,6 +18,7 @@ interface Props setShow: (show: boolean) => void; handleClose: () => void; toggleShow: () => void; + setfriendModal: () => void; } interface CustomToggleProps { @@ -42,9 +43,32 @@ const CustomToggle = forwardRef( CustomToggle.displayName = 'CustomToggle'; -export default function RightBar({setShow, show, handleClose, toggleShow} : Props) { +export default function RightBar({setShow, show, handleClose, toggleShow, setfriendModal} : Props) { - const friendsData = friends + // const friendsData = friends + // .sort((usr1: Friend, usr2: Friend) => { + // if (usr1.connected && !usr2.connected) { + // return -1; + // } + // if (!usr1.connected && usr2.connected) { + // return 1; + // } + // return usr1.id - usr2.id; + // }) + // .map((user: Friend, index: number) => ( + // + // )); + + const [searchTerm, setSearchTerm] = useState(''); + + const filteredFriends = friends + .filter((friend: Friend) => friend.nickname.toLowerCase().startsWith(searchTerm.toLowerCase())) .sort((usr1: Friend, usr2: Friend) => { if (usr1.connected && !usr2.connected) { return -1; @@ -63,6 +87,11 @@ export default function RightBar({setShow, show, handleClose, toggleShow} : Prop isConnected={user.connected} /> )); + + const searchOnlineFriends = (e: React.ChangeEvent) => { + // console.log(e.target.value); + setSearchTerm(e.target.value); + } return ( @@ -111,16 +140,16 @@ export default function RightBar({setShow, show, handleClose, toggleShow} : Prop
- + searchOnlineFriends(e)} style={{backgroundColor: '#2C3143', border: 0}}/>
- {friendsData} + {filteredFriends}
-
+
add friend
diff --git a/app/front-end/src/components/srightBar.tsx b/app/front-end/src/components/srightBar.tsx index e6813ab7..d4eb634e 100644 --- a/app/front-end/src/components/srightBar.tsx +++ b/app/front-end/src/components/srightBar.tsx @@ -1,3 +1,5 @@ + + import Dropdown from 'react-bootstrap/Dropdown'; import { IoIosSearch } from "react-icons/io"; import { ImUserPlus } from "react-icons/im"; @@ -8,9 +10,11 @@ import friends from './friends.json'; import React, { forwardRef } from 'react'; import Image from 'next/image' + interface Props { toggleShow: () => void; + setfriendModal: () => void; } interface CustomToggleProps { @@ -28,7 +32,8 @@ const CustomToggle = forwardRef( CustomToggle.displayName = 'CustomToggle'; -export default function SrightBar({toggleShow} : Props) { +export default function SrightBar({toggleShow, setfriendModal} : Props) { + const friendsData = friends.sort((usr1, usr2) => { if (usr1.connected && !usr2.connected) { return -1; @@ -44,6 +49,8 @@ export default function SrightBar({toggleShow} : Props) { .map((user, index) => ); + + return (
@@ -77,7 +84,7 @@ export default function SrightBar({toggleShow} : Props) { {friendsData}
-
+
diff --git a/app/front-end/src/components/styles/authChecker.module.css b/app/front-end/src/components/styles/authChecker.module.css new file mode 100644 index 00000000..72307e07 --- /dev/null +++ b/app/front-end/src/components/styles/authChecker.module.css @@ -0,0 +1,21 @@ +.spinnerContainer { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100vh; +} + +.darkBackground { + background-color: #161625; + color: #fff; +} + +.valo-font { + font-family: 'Valo', sans-serif; + font-size: 1.2rem; +} + +.loadingMessage { + margin-top: 1rem; +} diff --git a/app/front-end/src/components/styles/chat_about.module.css b/app/front-end/src/components/styles/chat_about.module.css index 5611c011..a5930919 100644 --- a/app/front-end/src/components/styles/chat_about.module.css +++ b/app/front-end/src/components/styles/chat_about.module.css @@ -5,3 +5,8 @@ border: 2px solid #27B299; } +@media (min-width: 1200px){ + .close_btn{ + display: none; + } +} diff --git a/app/front-end/src/components/styles/chat_friends.module.css b/app/front-end/src/components/styles/chat_friends.module.css index ec1f0676..281f113c 100644 --- a/app/front-end/src/components/styles/chat_friends.module.css +++ b/app/front-end/src/components/styles/chat_friends.module.css @@ -25,7 +25,6 @@ overflow: auto; padding: 15px; -ms-overflow-style: none; - /* scrollbar-width: none; */ } @@ -38,4 +37,4 @@ .chat_container::-webkit-scrollbar { display: none; -} \ No newline at end of file +} \ No newline at end of file diff --git a/app/front-end/src/components/styles/chat_messages.module.css b/app/front-end/src/components/styles/chat_messages.module.css index 4f0add18..a574920e 100644 --- a/app/front-end/src/components/styles/chat_messages.module.css +++ b/app/front-end/src/components/styles/chat_messages.module.css @@ -2,4 +2,5 @@ height: 64px; width: 64px; border-radius: 999px; + cursor: pointer; } \ No newline at end of file diff --git a/app/front-end/src/components/styles/inviteFriend.module.css b/app/front-end/src/components/styles/inviteFriend.module.css new file mode 100644 index 00000000..9ae2de3a --- /dev/null +++ b/app/front-end/src/components/styles/inviteFriend.module.css @@ -0,0 +1,25 @@ +.friend_modal{ + background-color: #2C3143; +} +.form_control{ + font-family: 'itim'; +} + +.form_control:focus{ + color:#bebebe; +} + +.form_control::placeholder{ + color: #7C7C7C; +} + +.edit_btn{ + border-radius: 25px; + background-color: #FF4755; + cursor: pointer; +} + +.edit_btn button{ + border: none; + background-color: transparent; +} \ No newline at end of file diff --git a/app/front-end/src/components/styles/srightBar.module.css b/app/front-end/src/components/styles/srightBar.module.css index cd622980..50db68ad 100644 --- a/app/front-end/src/components/styles/srightBar.module.css +++ b/app/front-end/src/components/styles/srightBar.module.css @@ -45,6 +45,33 @@ box-sizing: border-box; opacity: 1; } + + .edit_btn{ + border-radius: 25px; + background-color: #FF4755; + cursor: pointer; +} + +.edit_btn button{ + border: none; + background-color: transparent; +} + +.friend_modal{ + background-color: #2C3143; +} + +.form_control{ + font-family: 'itim'; +} + +.form_control:focus{ + color:#bebebe; +} + +.form_control::placeholder{ + color: #7C7C7C; +} @media (min-width: 1600px) { diff --git a/app/front-end/src/components/styles/user_chat.module.css b/app/front-end/src/components/styles/user_chat.module.css index 966fc3ba..b8489895 100644 --- a/app/front-end/src/components/styles/user_chat.module.css +++ b/app/front-end/src/components/styles/user_chat.module.css @@ -20,26 +20,7 @@ width: 75px; border-radius: 999px; overflow: hidden; -} - -.status{ - position: absolute; - bottom: 10px; - right: 10px; - height: 15px; - width: 15px; - background-color: rgba(34, 42, 56, 38.43); - border-radius: 999px; - display: flex; - justify-content: center; - align-items: center; -} - -.status_flag{ - height: 7.5px; - width: 7.5px; - background-color: #43C016; - border-radius: 25px; + border: 1px solid #27B299; } @media (max-width: 1600px) { @@ -51,22 +32,8 @@ @media (max-width: 1200px) { .profile_img{ - height: 35px; - width: 35px; - } - - .status{ - bottom: 28px; - right: 8px; - height: 10px; - width: 10px; - } - - .status_flag{ - height: 5px; - width: 5px; - background-color: #43C016; - border-radius: 25px; + height: 45px; + width: 45px; } } diff --git a/app/front-end/src/components/table.tsx b/app/front-end/src/components/table.tsx index 07fc7941..3540e253 100644 --- a/app/front-end/src/components/table.tsx +++ b/app/front-end/src/components/table.tsx @@ -9,67 +9,39 @@ interface GameData { date: string; result: 'WIN' | 'LOSS'; } +interface GameHistoryCardProps { + data: GameData[]; + } + +const GameHistoryCard: React.FC = ({ data }) => { -const GameHistoryCard: React.FC = () => { - - return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - + return ( +
PlayerScoreDateResult
-
- Player Avatar - Zakariae7910 -
-
13372024-01-13WIN
-
- Player Avatar - Zakariae7910 -
-
13372024-01-13WIN
-
- Player Avatar - Zakariae7910 -
-
13372024-01-13LOSE
+ + + + + + - + + + {data.map((game, index) => ( + - - - + + + - -
PlayerScoreDateResult
-
- Player Avatar - Zakariae7910 -
+
+ Player Avatar + {game.player} +
13372024-01-13LOSE{game.score}{game.date}{game.result}
- ); + ))} + + + ); }; export default GameHistoryCard; \ No newline at end of file diff --git a/app/front-end/src/components/twoFa.tsx b/app/front-end/src/components/twoFa.tsx index 35f63591..c6dcfdd0 100644 --- a/app/front-end/src/components/twoFa.tsx +++ b/app/front-end/src/components/twoFa.tsx @@ -30,10 +30,10 @@ export default function TwoFa({ value = '', email }: QrCode) { if (response.ok) { const data = await response.json(); - const { accessToken, refreshToken } = data; + const { access, refresh } = data; toast.success('Successfully signed in!'); - Cookies.set('accessToken', accessToken); - Cookies.set('refreshToken', refreshToken); + Cookies.set('access', access); + Cookies.set('refresh', refresh); router.push('/dashboard'); } else if (response.status === 401) { toast.error('Invalid otp!'); @@ -70,7 +70,7 @@ export default function TwoFa({ value = '', email }: QrCode) {
- +

diff --git a/app/front-end/src/components/user_chat.tsx b/app/front-end/src/components/user_chat.tsx index 1ee4485e..97405569 100644 --- a/app/front-end/src/components/user_chat.tsx +++ b/app/front-end/src/components/user_chat.tsx @@ -3,20 +3,24 @@ import React from 'react'; import styles from './styles/user_chat.module.css'; import Image from 'next/image'; -export default function UserChat () +interface Props{ + setShow: React.Dispatch>; + handleShow: () => void; +} + +export default function UserChat ( {setShow , handleShow}: Props ) { return ( <>

- profile_image -
-
+
+ profile_image
-
+
setShow(true)}> !Snake_007 - Hey, do you wanna play, i dear you to win. + Hey, do you wanna play, i dare you to win.
now diff --git a/app/front-end/src/components/user_chat_resp.tsx b/app/front-end/src/components/user_chat_resp.tsx new file mode 100644 index 00000000..b5f765ca --- /dev/null +++ b/app/front-end/src/components/user_chat_resp.tsx @@ -0,0 +1,30 @@ + +import React from 'react'; +import styles from './styles/user_chat.module.css'; +import Image from 'next/image'; + +interface Props{ + handleShow: () => void; +} + +export default function UserChatResp ({handleShow}: Props) +{ + return ( + <> +
+
+
+ profile_image +
+
+
+ !Appolo_007 + Hey, do you wanna play, i dare you to win. +
+
+ now +
+
+ + ); +} \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 1676f6b5..5712b193 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,7 +7,7 @@ services: build: context: ./app/back-end dockerfile: Dockerfile - restart: always + restart: on-failure depends_on: - data-base ports: @@ -26,7 +26,7 @@ services: build: context: ./app/front-end dockerfile: Dockerfile - restart: always + restart: on-failure depends_on: - back-end ports: @@ -47,7 +47,7 @@ services: build: context: ./app/data-base dockerfile: Dockerfile - restart: always + restart: on-failure ports: - "5432:5432" env_file: diff --git a/services/elk/.env b/services/elk/.env new file mode 100644 index 00000000..5b279789 --- /dev/null +++ b/services/elk/.env @@ -0,0 +1,3 @@ +ELASTIC_PASSWORD=a123456789 +KIBANA_PASSWORD=a123456789 +MEM_LIMIT=1073741824 diff --git a/services/elk/Makefile b/services/elk/Makefile new file mode 100644 index 00000000..3f523ac2 --- /dev/null +++ b/services/elk/Makefile @@ -0,0 +1,43 @@ +PROJECT_NAME = elk +DOCKER_COMPOSE_FILE = docker-compose.yml + +COMPOSE_UP = docker-compose -p $(PROJECT_NAME) -f $(DOCKER_COMPOSE_FILE) up +COMPOSE_DOWN = docker-compose -p $(PROJECT_NAME) -f $(DOCKER_COMPOSE_FILE) down +COMPOSE_BUILD = docker-compose -p $(PROJECT_NAME) -f $(DOCKER_COMPOSE_FILE) build +COMPOSE_PULL = docker-compose -p $(PROJECT_NAME) -f $(DOCKER_COMPOSE_FILE) pull +COMPOSE_LOGS = docker-compose -p $(PROJECT_NAME) -f $(DOCKER_COMPOSE_FILE) logs -f +COMPOSE_PS = docker-compose -p $(PROJECT_NAME) -f $(DOCKER_COMPOSE_FILE) ps + +all: build up + +build: + @echo "Building containers..." + @$(COMPOSE_BUILD) + +up: + @echo "Starting containers..." + @$(COMPOSE_UP) -d + +down: + @echo "Stopping containers..." + @$(COMPOSE_DOWN) + +pull: + @echo "Pulling latest images..." + @$(COMPOSE_PULL) + +clean: down + @echo "Removing containers..." + @$(COMPOSE_DOWN) --rmi all -v + +logs: + @echo "Following logs..." + @$(COMPOSE_LOGS) + +ps: + @echo "Container status..." + @$(COMPOSE_PS) + +re: clean build up + +.PHONY: all build up down pull clean logs ps rebuild diff --git a/services/elk/docker-compose.yml b/services/elk/docker-compose.yml new file mode 100644 index 00000000..4b5098b6 --- /dev/null +++ b/services/elk/docker-compose.yml @@ -0,0 +1,122 @@ +version: "3.8" + +services: + setup: + image: docker.elastic.co/elasticsearch/elasticsearch:8.12.2 + container_name: setup + volumes: + - certs:/usr/share/elasticsearch/config/certs + - ./tools/setup_es_certs.sh:/usr/share/elasticsearch/setup_es_certs.sh + user: "0" + command: > + /bin/bash /usr/share/elasticsearch/setup_es_certs.sh + environment: + ELASTIC_PASSWORD: ${ELASTIC_PASSWORD} + KIBANA_PASSWORD: ${KIBANA_PASSWORD} + healthcheck: + test: ["CMD-SHELL", "[ -f config/certs/elasticsearch/elasticsearch.crt ]"] + interval: 1s + timeout: 5s + retries: 120 + + elasticsearch: + image: docker.elastic.co/elasticsearch/elasticsearch:8.12.2 + container_name: elasticsearch + volumes: + - ./elasticsearch/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml + - certs:/usr/share/elasticsearch/config/certs + - elasticsearchdata:/usr/share/elasticsearch/data + ports: + - "9200:9200" + environment: + ELASTIC_PASSWORD: ${ELASTIC_PASSWORD} + mem_limit: ${MEM_LIMIT} + ulimits: + memlock: { soft: -1, hard: -1 } + healthcheck: + test: + [ + "CMD-SHELL", + "curl -s --cacert config/certs/ca/ca.crt https://localhost:9200 | grep -q 'missing authentication credentials'", + ] + interval: 10s + timeout: 10s + retries: 120 + depends_on: + setup: + condition: service_healthy + + kibana: + image: docker.elastic.co/kibana/kibana:8.12.2 + container_name: kibana + volumes: + - certs:/usr/share/kibana/config/certs + - kibanadata:/usr/share/kibana/data + - ./kibana/config/kibana.yml:/usr/share/kibana/config/kibana.yml + ports: + - "5601:5601" + environment: + KIBANA_PASSWORD: ${KIBANA_PASSWORD} + mem_limit: ${MEM_LIMIT} + healthcheck: + test: + [ + "CMD-SHELL", + "curl -s --cacert config/certs/ca/ca.crt --insecure https://kibana:5601/api/status | grep -q available", + ] + interval: 10s + timeout: 10s + retries: 120 + depends_on: + elasticsearch: + condition: service_healthy + + logstash: + image: docker.elastic.co/logstash/logstash:8.12.2 + container_name: logstash + user: root + volumes: + - ./logstash/config/logstash.yml:/usr/share/logstash/config/logstash.yml + - ./logstash/pipeline:/usr/share/logstash/pipeline + - certs:/usr/share/logstash/certs + ports: + - "5044:5044" + environment: + ELASTIC_PASSWORD: ${ELASTIC_PASSWORD} + depends_on: + elasticsearch: + condition: service_healthy + kibana: + condition: service_healthy + + filebeat: + image: docker.elastic.co/beats/filebeat:8.12.2 + container_name: filebeat + user: root + volumes: + - /var/lib/docker/containers:/var/lib/docker/containers:ro + - /var/run/docker.sock:/var/run/docker.sock:ro + - ./filebeat/filebeat.yml:/usr/share/filebeat/filebeat.yml:ro + - certs:/usr/share/filebeat/certs + depends_on: + - logstash + + nodejs_app: + container_name: nodejs_app + build: + context: ./node + dockerfile: Dockerfile + ports: + - "8080:8080" + +volumes: + certs: + name: certs + elasticsearchdata: + name: elasticsearchdata + kibanadata: + name: kibanadata + +networks: + default: + name: elk diff --git a/services/elk/Dockerfile b/services/elk/elasticsearch/config/elasticsearch.yml similarity index 100% rename from services/elk/Dockerfile rename to services/elk/elasticsearch/config/elasticsearch.yml diff --git a/services/elk/filebeat/filebeat.yml b/services/elk/filebeat/filebeat.yml new file mode 100644 index 00000000..e69de29b diff --git a/services/elk/kibana/config/kibana.yml b/services/elk/kibana/config/kibana.yml new file mode 100644 index 00000000..e69de29b diff --git a/services/elk/logstash/config/logstash.yml b/services/elk/logstash/config/logstash.yml new file mode 100644 index 00000000..e69de29b diff --git a/services/elk/logstash/pipeline/logstash.conf b/services/elk/logstash/pipeline/logstash.conf new file mode 100644 index 00000000..e69de29b diff --git a/services/elk/tools/setup_es_certs.sh b/services/elk/tools/setup_es_certs.sh new file mode 100644 index 00000000..e69de29b