diff --git a/.gitignore b/.gitignore index 17ded36..aee563f 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ db.sqlite3 **/migrations/** /profiles/static/avatars/* -!/profiles/static/avatars/default.env +!/profiles/static/avatars/default.avif +*.mo diff --git a/README.md b/README.md index ae970a3..89b0430 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,24 @@ -# BACKEND +# FT_TRANSCENDENCE + +# Modules + +| Module | Nb point | state| +| ------ | -------- | ---- | +| Multiplayer | 2 | 🚧 +| Remote | 2 | 🚧 +| Bootstrap | 1 | 🏁 | +| Django | 2 | 🏁 | +| Bdd | 1 | 🚧 | +| Accounts | 2 | 🏁 | +| WebGL | 2 | 🚧 | +| Other game | 2 | 🚧 | +| Chat | 2 | 🏁 | +| Translation | 1 | 🚧 | +| Other browser | 1 | 🏁 | +| Smartphone support | 1 | 🚧 | +| --- | --- | --- +| Ready | 8 | | +| Total | 19 | | ## Installation @@ -37,6 +57,10 @@ python manage.py makemigrations chat python manage.py makemigrations tournament python manage.py migrate ``` +- Compile translations +``` +python manage.py compilemessages +``` - Start the developpement server ``` python manage.py runserver 0.0.0.0:8000 diff --git a/accounts/locale/fr/LC_MESSAGES/django.po b/accounts/locale/fr/LC_MESSAGES/django.po new file mode 100644 index 0000000..eed9cf2 --- /dev/null +++ b/accounts/locale/fr/LC_MESSAGES/django.po @@ -0,0 +1,22 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2024-02-01 13:59+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" +#: views/login.py:22 +msgid "Invalid username or password." +msgstr "Nom d'utilisateur ou mot de passe incorect." diff --git a/accounts/serializers/login.py b/accounts/serializers/login.py index 9fcbaff..b38fead 100644 --- a/accounts/serializers/login.py +++ b/accounts/serializers/login.py @@ -4,9 +4,9 @@ from django.core.exceptions import ValidationError class LoginSerializer(Serializer): - username = CharField() - password = CharField() + username = CharField() + password = CharField() - def get_user(self, data): - user = authenticate(username=data['username'], password=data['password']) - return user \ No newline at end of file + def get_user(self, data): + user = authenticate(username=data['username'], password=data['password']) + return user \ No newline at end of file diff --git a/accounts/views/delete.py b/accounts/views/delete.py index 9a83800..1333f7e 100644 --- a/accounts/views/delete.py +++ b/accounts/views/delete.py @@ -6,16 +6,16 @@ from django.http import HttpRequest from rest_framework.authentication import SessionAuthentication class DeleteView(APIView): - permission_classes = (permissions.IsAuthenticated,) - authentication_classes = (SessionAuthentication,) - def delete(self, request: HttpRequest): - data: dict = request.data + permission_classes = (permissions.IsAuthenticated,) + authentication_classes = (SessionAuthentication,) + def delete(self, request: HttpRequest): + data: dict = request.data - password: str = data["password"] - if (password is None): - return Response({"password": ["This field may not be blank."]}) - if (request.user.check_password(password) == False): - return Response({"password": ["Password wrong."]}) - request.user.delete() - logout(request) - return Response("user deleted", status=status.HTTP_200_OK) \ No newline at end of file + password: str = data["password"] + if (password is None): + return Response({"password": ["This field may not be blank."]}) + if (request.user.check_password(password) == False): + return Response({"password": ["Password wrong."]}) + request.user.delete() + logout(request) + return Response("user deleted", status=status.HTTP_200_OK) \ No newline at end of file diff --git a/accounts/views/logged.py b/accounts/views/logged.py index 1cc9cce..b67abe4 100644 --- a/accounts/views/logged.py +++ b/accounts/views/logged.py @@ -9,8 +9,8 @@ from ..serializers.login import LoginSerializer class LoggedView(APIView): - permission_classes = (permissions.AllowAny,) - authentication_classes = (SessionAuthentication,) + permission_classes = (permissions.AllowAny,) + authentication_classes = (SessionAuthentication,) - def get(self, request: HttpRequest): - return Response(status = (status.HTTP_200_OK if request.user.is_authenticated else status.HTTP_400_BAD_REQUEST)) + def get(self, request: HttpRequest): + return Response(status = (status.HTTP_200_OK if request.user.is_authenticated else status.HTTP_400_BAD_REQUEST)) diff --git a/accounts/views/login.py b/accounts/views/login.py index fc02e4a..d981f41 100644 --- a/accounts/views/login.py +++ b/accounts/views/login.py @@ -4,20 +4,21 @@ from rest_framework import permissions, status from django.http import HttpRequest from django.contrib.auth import login from rest_framework.authentication import SessionAuthentication +from django.utils.translation import gettext as _ from ..serializers.login import LoginSerializer class LoginView(APIView): - permission_classes = (permissions.AllowAny,) - authentication_classes = (SessionAuthentication,) + permission_classes = (permissions.AllowAny,) + authentication_classes = (SessionAuthentication,) - def post(self, request: HttpRequest): - data = request.data - serializer = LoginSerializer(data=data) - serializer.is_valid(raise_exception=True) - user = serializer.get_user(data) - if user is None: - return Response({'login': ['Invalid username or password.']}, status.HTTP_400_BAD_REQUEST) - login(request, user) - return Response({'id': user.pk}, status=status.HTTP_200_OK) + def post(self, request: HttpRequest): + data = request.data + serializer = LoginSerializer(data=data) + serializer.is_valid(raise_exception=True) + user = serializer.get_user(data) + if user is None: + return Response({'login': [_('Invalid username or password.')]}, status.HTTP_401_UNAUTHORIZED) + login(request, user) + return Response({'id': user.pk}, status=status.HTTP_200_OK) diff --git a/accounts/views/logout.py b/accounts/views/logout.py index f64e5b1..cee8868 100644 --- a/accounts/views/logout.py +++ b/accounts/views/logout.py @@ -6,8 +6,8 @@ from django.http import HttpRequest from rest_framework.authentication import SessionAuthentication class LogoutView(APIView): - permission_classes = (permissions.IsAuthenticated,) - authentication_classes = (SessionAuthentication,) - def get(self, request: HttpRequest): - logout(request) - return Response("user unlogged", status=status.HTTP_200_OK) \ No newline at end of file + permission_classes = (permissions.IsAuthenticated,) + authentication_classes = (SessionAuthentication,) + def get(self, request: HttpRequest): + logout(request) + return Response("user unlogged", status=status.HTTP_200_OK) \ No newline at end of file diff --git a/accounts/views/register.py b/accounts/views/register.py index 667ef2a..903609a 100644 --- a/accounts/views/register.py +++ b/accounts/views/register.py @@ -6,13 +6,13 @@ from django.http import HttpRequest from django.contrib.auth import login class RegisterView(APIView): - permission_classes = (permissions.AllowAny,) - def post(self, request: HttpRequest): - data = request.data - serializer = RegisterSerialiser(data=data) - if serializer.is_valid(raise_exception=True): - user = serializer.create(data) - if user: - login(request, user) - return Response("user created", status=status.HTTP_201_CREATED) - return Response(status=status.HTTP_400_BAD_REQUEST) + permission_classes = (permissions.AllowAny,) + def post(self, request: HttpRequest): + data = request.data + serializer = RegisterSerialiser(data=data) + if serializer.is_valid(raise_exception=True): + user = serializer.create(data) + if user: + login(request, user) + return Response("user created", status=status.HTTP_201_CREATED) + return Response(status=status.HTTP_400_BAD_REQUEST) diff --git a/chat/consumersChat.py b/chat/consumersChat.py index 2de3c68..289fe7c 100644 --- a/chat/consumersChat.py +++ b/chat/consumersChat.py @@ -8,244 +8,244 @@ import json class ChatConsumer(WebsocketConsumer): - def connect(self): + def connect(self): - user = self.scope["user"] - if (user.is_anonymous or not user.is_authenticated): - return + user = self.scope["user"] + if (user.is_anonymous or not user.is_authenticated): + return - channel_id : int = int(self.scope['url_route']['kwargs']['chat_id']) + channel_id : int = int(self.scope['url_route']['kwargs']['chat_id']) - if ChatMemberModel.objects.filter(member_id=user.pk, channel_id=int(channel_id)).count() != 1: - return + if ChatMemberModel.objects.filter(member_id=user.pk, channel_id=int(channel_id)).count() != 1: + return - if (self.channel_layer == None): - return + if (self.channel_layer == None): + return - self.room_group_name = f'chat{channel_id}' + self.room_group_name = f'chat{channel_id}' - async_to_sync(self.channel_layer.group_add)( - self.room_group_name, - self.channel_name - ) + async_to_sync(self.channel_layer.group_add)( + self.room_group_name, + self.channel_name + ) - self.accept() + self.accept() - def receive(self, text_data=None, bytes_data=None): + def receive(self, text_data=None, bytes_data=None): - if text_data == None: - return + if text_data == None: + return - user = self.scope["user"] - if (user.is_anonymous or not user.is_authenticated): - return + user = self.scope["user"] + if (user.is_anonymous or not user.is_authenticated): + return - text_data_json: dict = json.loads(text_data) - - message = text_data_json.get('message') - if (message is None): - return - - receivers_id = text_data_json.get('receivers_id') - if (receivers_id is None): - return + text_data_json: dict = json.loads(text_data) + + message = text_data_json.get('message') + if (message is None): + return + + receivers_id = text_data_json.get('receivers_id') + if (receivers_id is None): + return - channel_id : int = int(self.scope['url_route']['kwargs']['chat_id']) + channel_id : int = int(self.scope['url_route']['kwargs']['chat_id']) - if ChatMemberModel.objects.filter(member_id = user.pk, channel_id = channel_id).count() != 1: - return + if ChatMemberModel.objects.filter(member_id = user.pk, channel_id = channel_id).count() != 1: + return - if (self.channel_layer == None): - return + if (self.channel_layer == None): + return - message_time: int = int(time.time() * 1000) - - if (len(receivers_id) == 1 and - BlockModel.objects.filter(blocker=user.pk, blocked=receivers_id[0]) or - BlockModel.objects.filter(blocker=receivers_id[0], blocked=user.pk) - ): - return + message_time: int = int(time.time() * 1000) + + if (len(receivers_id) == 1 and + BlockModel.objects.filter(blocker=user.pk, blocked=receivers_id[0]) or + BlockModel.objects.filter(blocker=receivers_id[0], blocked=user.pk) + ): + return - async_to_sync(self.channel_layer.group_send)( - self.room_group_name, - { - 'type':'chat_message', - 'author_id':user.pk, - 'content':message, - 'time':message_time, - } - ) - - new_message = ChatMessageModel( - channel_id = channel_id, - author_id = user.pk, - content = message, - time = message_time - ).save() + async_to_sync(self.channel_layer.group_send)( + self.room_group_name, + { + 'type':'chat_message', + 'author_id':user.pk, + 'content':message, + 'time':message_time, + } + ) + + new_message = ChatMessageModel( + channel_id = channel_id, + author_id = user.pk, + content = message, + time = message_time + ).save() - def chat_message(self, event): + def chat_message(self, event): - user = self.scope["user"] - if (user.is_anonymous or not user.is_authenticated): - return + user = self.scope["user"] + if (user.is_anonymous or not user.is_authenticated): + return - channel_id: int = int(self.scope['url_route']['kwargs']['chat_id']) + channel_id: int = int(self.scope['url_route']['kwargs']['chat_id']) - if ChatMemberModel.objects.filter(member_id=user.pk, channel_id=channel_id).count() != 1: - return + if ChatMemberModel.objects.filter(member_id=user.pk, channel_id=channel_id).count() != 1: + return - self.send(text_data=json.dumps({ - 'type':'chat', - 'author_id':event['author_id'], - 'content':event['content'], - 'time': event['time'], - })) + self.send(text_data=json.dumps({ + 'type':'chat', + 'author_id':event['author_id'], + 'content':event['content'], + 'time': event['time'], + })) class ChatNoticeConsumer(WebsocketConsumer): - def connect(self): + def connect(self): - user = self.scope["user"] - #if (user.is_anonymous or not user.is_authenticated): - #return + user = self.scope["user"] + #if (user.is_anonymous or not user.is_authenticated): + #return - if (self.channel_layer == None): - return + if (self.channel_layer == None): + return - self.room_group_name = f'chatNotice{user.pk}' - if (not hasattr(self.channel_layer, "users_channels")): - self.channel_layer.users_channels = {} - self.channel_layer.users_channels[user.pk] = self.channel_name + self.room_group_name = f'chatNotice{user.pk}' + if (not hasattr(self.channel_layer, "users_channels")): + self.channel_layer.users_channels = {} + self.channel_layer.users_channels[user.pk] = self.channel_name - if (not hasattr(self.channel_layer, "invite")): - self.channel_layer.invite = {} - self.channel_layer.invite[user.pk] = []; + if (not hasattr(self.channel_layer, "invite")): + self.channel_layer.invite = {} + self.channel_layer.invite[user.pk] = []; - async_to_sync(self.channel_layer.group_add)( - self.room_group_name, - self.channel_name - ) + async_to_sync(self.channel_layer.group_add)( + self.room_group_name, + self.channel_name + ) - self.accept() + self.accept() - message_time: int = int(time.time() * 1000) - targets = list(self.channel_layer.users_channels.keys()) - for target in targets: - channel = self.channel_layer.users_channels.get(target) - if (channel == None or target == user.pk): - continue - async_to_sync(self.channel_layer.send)(channel, { - 'type':"online_users", - 'author_id':user.pk, - 'time':message_time, - }) + message_time: int = int(time.time() * 1000) + targets = list(self.channel_layer.users_channels.keys()) + for target in targets: + channel = self.channel_layer.users_channels.get(target) + if (channel == None or target == user.pk): + continue + async_to_sync(self.channel_layer.send)(channel, { + 'type':"online_users", + 'author_id':user.pk, + 'time':message_time, + }) - def disconnect(self, code): + def disconnect(self, code): - user = self.scope["user"] - if (user.is_anonymous or not user.is_authenticated): - return + user = self.scope["user"] + if (user.is_anonymous or not user.is_authenticated): + return - self.channel_layer.users_channels.pop(user.pk) + self.channel_layer.users_channels.pop(user.pk) - message_time: int = int(time.time() * 1000) + message_time: int = int(time.time() * 1000) - targets = list(self.channel_layer.users_channels.keys()) - for target in targets: - channel = self.channel_layer.users_channels.get(target) - if (channel == None or target == user.pk): - continue - async_to_sync(self.channel_layer.send)(channel, { - 'type':"online_users", - 'author_id':user.pk, - 'time':message_time, - }) + targets = list(self.channel_layer.users_channels.keys()) + for target in targets: + channel = self.channel_layer.users_channels.get(target) + if (channel == None or target == user.pk): + continue + async_to_sync(self.channel_layer.send)(channel, { + 'type':"online_users", + 'author_id':user.pk, + 'time':message_time, + }) - def receive(self, text_data=None, bytes_data=None): + def receive(self, text_data=None, bytes_data=None): - if text_data == None: - return + if text_data == None: + return - user = self.scope["user"] - #if (user.is_anonymous or not user.is_authenticated): - #return + user = self.scope["user"] + #if (user.is_anonymous or not user.is_authenticated): + #return - text_data_json = json.loads(text_data) - - type_notice = text_data_json.get('type') - targets : list = text_data_json.get('targets') - content : dict = text_data_json.get('content') + text_data_json = json.loads(text_data) + + type_notice = text_data_json.get('type') + targets : list = text_data_json.get('targets') + content : dict = text_data_json.get('content') - if (type_notice == None or targets == None): - return + if (type_notice == None or targets == None): + return - if (self.channel_layer == None): - return + if (self.channel_layer == None): + return - message_time: int = int(time.time() * 1000) - status = 200; + message_time: int = int(time.time() * 1000) + status = 200; - #print("receive" + str(user.pk)) + #print("receive" + str(user.pk)) - if targets == "all": - targets = list(self.channel_layer.users_channels.keys()) + if targets == "all": + targets = list(self.channel_layer.users_channels.keys()) - for target in targets: - channel = self.channel_layer.users_channels.get(target) - if (channel == None or target == user.pk): - if (channel == None): - status = 404 - continue - async_to_sync(self.channel_layer.send)(channel, { - 'type':type_notice, - 'author_id':user.pk, - 'content':content, - 'time':message_time, - 'status': 200, - }) - async_to_sync(self.channel_layer.send)(self.channel_layer.users_channels.get(user.pk), { - 'type':type_notice, - 'author_id':user.pk, - 'content':"notice return", - 'time':message_time, - 'status':status, - }) + for target in targets: + channel = self.channel_layer.users_channels.get(target) + if (channel == None or target == user.pk): + if (channel == None): + status = 404 + continue + async_to_sync(self.channel_layer.send)(channel, { + 'type':type_notice, + 'author_id':user.pk, + 'content':content, + 'time':message_time, + 'status': 200, + }) + async_to_sync(self.channel_layer.send)(self.channel_layer.users_channels.get(user.pk), { + 'type':type_notice, + 'author_id':user.pk, + 'content':"notice return", + 'time':message_time, + 'status':status, + }) - def invite(self, event): + def invite(self, event): - user = self.scope["user"] - if (user.is_anonymous or not user.is_authenticated): - return + user = self.scope["user"] + if (user.is_anonymous or not user.is_authenticated): + return - if (self.channel_layer.invite[event["author_id"]].get(user.pk)): - return - self.channel_layer.invite[event["author_id"]].append(user.pl) + if (self.channel_layer.invite[event["author_id"]].get(user.pk)): + return + self.channel_layer.invite[event["author_id"]].append(user.pl) - self.send(text_data=json.dumps({ - 'type':event['type'], - 'author_id':event['author_id'], - 'content':event['content'], - 'time': event['time'], - 'status':event['status'], - })) + self.send(text_data=json.dumps({ + 'type':event['type'], + 'author_id':event['author_id'], + 'content':event['content'], + 'time': event['time'], + 'status':event['status'], + })) - def online_users(self, event): + def online_users(self, event): - user = self.scope["user"] - #if (user.is_anonymous or not user.is_authenticated): - #return + user = self.scope["user"] + #if (user.is_anonymous or not user.is_authenticated): + #return - #print("online_users" + str(user.pk)) + #print("online_users" + str(user.pk)) - event['content'] = self.channel_layer.users_channels + event['content'] = self.channel_layer.users_channels - self.send(text_data=json.dumps({ - 'type':event['type'], - 'author_id':event['author_id'], - 'content':event['content'], - 'time': event['time'], - 'status':event['status'], - })) + self.send(text_data=json.dumps({ + 'type':event['type'], + 'author_id':event['author_id'], + 'content':event['content'], + 'time': event['time'], + 'status':event['status'], + })) diff --git a/chat/consumersNotice.py b/chat/consumersNotice.py index 37267a2..1070c8e 100644 --- a/chat/consumersNotice.py +++ b/chat/consumersNotice.py @@ -1,177 +1,379 @@ from channels.generic.websocket import WebsocketConsumer from asgiref.sync import async_to_sync +from games.models import GameModel +from profiles.models import FriendModel, AskFriendModel + import time import json class ChatNoticeConsumer(WebsocketConsumer): - def connect(self): + def connect(self): - user = self.scope["user"] - #if (user.is_anonymous or not user.is_authenticated): - #return + user = self.scope["user"] + #if (user.is_anonymous or not user.is_authenticated): + #return - if (self.channel_layer == None): - return + if (self.channel_layer == None): + return - self.room_group_name = f'chatNotice{user.pk}' - if (not hasattr(self.channel_layer, "users_channels")): - self.channel_layer.users_channels = {} - self.channel_layer.users_channels[user.pk] = self.channel_name + self.room_group_name = f'chatNotice{user.pk}' + if (not hasattr(self.channel_layer, "users_channels")): + self.channel_layer.users_channels = {} + self.channel_layer.users_channels[user.pk] = self.channel_name - if (not hasattr(self.channel_layer, "invite")): - self.channel_layer.invite = {} - self.channel_layer.invite[user.pk] = []; + if (not hasattr(self.channel_layer, "invite")): + self.channel_layer.invite = {} + self.channel_layer.invite[user.pk] = []; - async_to_sync(self.channel_layer.group_add)( - self.room_group_name, - self.channel_name - ) + async_to_sync(self.channel_layer.group_add)( + self.room_group_name, + self.channel_name + ) - self.accept() + self.accept() - message_time: int = int(time.time() * 1000) - targets = list(self.channel_layer.users_channels.keys()) - for target in targets: - channel = self.channel_layer.users_channels.get(target) - if (channel == None or target == user.pk): - continue - async_to_sync(self.channel_layer.send)(channel, { - 'type':"online_users", - 'author_id':user.pk, - 'targets': targets, - 'time':message_time, - 'status': 200, - }) + self.sync() - def disconnect(self, code): + def disconnect(self, code): - user = self.scope["user"] - if (user.is_anonymous or not user.is_authenticated): - return + user = self.scope["user"] + if (user.is_anonymous or not user.is_authenticated): + return - self.channel_layer.users_channels.pop(user.pk) + del self.channel_layer.users_channels[user.pk] + del self.channel_layer.invite[user.pk] - message_time: int = int(time.time() * 1000) + for inviter_id, inviteds_id in self.channel_layer.invite.items(): + if (user.pk in inviteds_id): + self.channel_layer.invite[inviter_id].remove(user.pk) - targets = list(self.channel_layer.users_channels.keys()) - for target in targets: - channel = self.channel_layer.users_channels.get(target) - if (channel == None or target == user.pk): - continue - async_to_sync(self.channel_layer.send)(channel, { - 'type':"online_users", - 'author_id':user.pk, - 'targets': targets, - 'time':message_time, - 'status': 200, - }) - - def receive(self, text_data=None, bytes_data=None): - - if text_data == None: - return - - user = self.scope["user"] - #if (user.is_anonymous or not user.is_authenticated): - #return - - text_data_json = json.loads(text_data) - - type_notice = text_data_json.get('type') - targets : list = text_data_json.get('targets') - content : dict = text_data_json.get('content') - - if (type_notice == None or targets == None): - return - - if (self.channel_layer == None): - return - - message_time: int = int(time.time() * 1000) - status = 200; - - #print("receive" + str(user.pk)) - - try: - status = getattr(self, "pre_" + type_notice)(user, targets) - except AttributeError: - print(f"La fonction pre_{type_notice} n'existe pas.") - - if targets == "all": - targets = list(self.channel_layer.users_channels.keys()) - - for target in targets: - channel = self.channel_layer.users_channels.get(target) - if (channel == None or target == user.pk): - if (channel == None): - status = 404 - continue - async_to_sync(self.channel_layer.send)(channel, { - 'type':type_notice, - 'author_id':user.pk, - 'content':content, - 'targets': targets, - 'time':message_time, - 'status': 200, - }) - async_to_sync(self.channel_layer.send)(self.channel_layer.users_channels.get(user.pk), { - 'type':type_notice, - 'author_id':user.pk, - 'content':"notice return", - 'targets': targets, - 'time':message_time, - 'status':status, - }) + self.sync() - def pre_invite(self, user, targets): - status = 200 + def receive(self, text_data=None, bytes_data=None): - for target in targets: - if (target in self.channel_layer.invite[user.pk]): - status = 409 - return status + if text_data == None: + return - def invite(self, event): + user = self.scope["user"] + #if (user.is_anonymous or not user.is_authenticated): + #return - user = self.scope["user"] - if (user.is_anonymous or not user.is_authenticated): - return + text_data_json = json.loads(text_data) + + type_notice = text_data_json.get('type') + targets : list = text_data_json.get('targets') + content : dict = text_data_json.get('content') - if (user.pk in self.channel_layer.invite[event["author_id"]]): - return + if (type_notice == None or targets == None): + return - if (user.pk != event["author_id"]): - self.channel_layer.invite[event["author_id"]].append(user.pk) + if (self.channel_layer == None): + return - self.send(text_data=json.dumps({ - 'type':event['type'], - 'author_id':event['author_id'], - 'content':event['content'], - 'targets': event['targets'], - 'time': event['time'], - 'status':event['status'], - })) + message_time: int = text_data_json.get('time') - def pre_online_users(self, user, targets): - pass + if (message_time == None): + message_time: int = int(time.time() * 1000) - def online_users(self, event): + result = None + try: + status, result = getattr(self, "pre_" + type_notice)(user, targets) + except AttributeError as error: + status = 200 - user = self.scope["user"] - #if (user.is_anonymous or not user.is_authenticated): - #return + if (status < 300): + if targets == "all": + targets = list(self.channel_layer.users_channels.keys()) - #print("online_users" + str(user.pk)) + for target in targets: + channel = self.channel_layer.users_channels.get(target) + if (channel == None or target == user.pk): + if (channel == None): + status = 444 # target not connected + continue + async_to_sync(self.channel_layer.send)(channel, { + 'type':type_notice, + 'author_id':user.pk, + 'content':content, + 'result':result, + 'targets': targets, + 'time':message_time, + 'status': 200, + }) - event['content'] = self.channel_layer.users_channels + async_to_sync(self.channel_layer.send)(self.channel_layer.users_channels.get(user.pk), { + 'type':type_notice, + 'author_id':user.pk, + 'result':result, + 'targets': targets, + 'time':message_time, + 'status':status, + }) - self.send(text_data=json.dumps({ - 'type':event['type'], - 'author_id':event['author_id'], - 'content':event['content'], - 'time': event['time'], - 'status':event['status'], - })) + def sync(self, user = None, level = None): + + sendToUser = True + if (user == None): + user = self.scope["user"] + sendToUser = False + + + if (level == None): + level = 0 + + message_time: int = int(time.time() * 1000) + if (sendToUser): + targets = [user.pk] + else: + targets = list(self.channel_layer.users_channels.keys()) + + for target in targets: + channel = self.channel_layer.users_channels.get(target) + if (channel == None or (not sendToUser and target == user.pk)): + continue + async_to_sync(self.channel_layer.send)(channel, { + 'type':"online_users", + 'author_id':user.pk, + 'targets': targets, + 'time':message_time, + 'status': 200, + }) + if (level >= 1): + async_to_sync(self.channel_layer.send)(channel, { + 'type':"invite", + 'author_id':user.pk, + 'targets': targets, + 'time':message_time, + 'status': 200, + }) + + + def pre_invite(self, user, targets): + + if (user.is_anonymous or not user.is_authenticated): + return 400, None + + status = 200 + for target in targets: + if (target in self.channel_layer.invite[user.pk]): + status = 409 + continue + + channel = self.channel_layer.users_channels.get(target) + if (channel == None): + status = 404 + continue + + # Add the invited in "self.channel_layer.invite" + if (user.pk != target): + self.channel_layer.invite[user.pk].append(target) + + return status, None + + def get_invites(self, user): + invites = [] + for inviter_id, inviteds_id in self.channel_layer.invite.items(): + if (user.pk in inviteds_id and user.pk != inviter_id): + invites.append(inviter_id) + return invites; + + def invite(self, event): + + user = self.scope["user"] + if (user.is_anonymous or not user.is_authenticated): + return + + invites = self.get_invites(user) + + self.send(text_data=json.dumps({ + 'type':event['type'], + 'author_id':event['author_id'], + 'invites': invites, + 'targets': event['targets'], + 'time': event['time'], + 'status':event['status'], + })) + + def pre_accept_invite(self, user, targets): + + if (user.is_anonymous or not user.is_authenticated): + return 400, None + + if (user.pk not in self.channel_layer.invite[targets[0]]): + return 400, None + + self.channel_layer.invite[targets[0]].remove(user.pk) + + if (targets[0] in self.channel_layer.invite[user.pk]): + self.channel_layer.invite[user.pk].remove(targets[0]) + + id_game = GameModel().create([user.pk, targets[0]]); + + return 200, id_game + + def accept_invite(self, event): + + user = self.scope["user"] + if (user.is_anonymous or not user.is_authenticated): + return + + invites = self.get_invites(user) + + self.send(text_data=json.dumps({ + 'type':event['type'], + 'author_id':event['author_id'], + 'id_game': event['result'], + 'invites': invites, + 'time': event['time'], + 'status':event['status'], + })) + + def pre_refuse_invite(self, user, targets): + + if (user.is_anonymous or not user.is_authenticated): + return 400, None + + if (user.pk not in self.channel_layer.invite[targets[0]]): + return 400, None + + self.channel_layer.invite[targets[0]].remove(user.pk) + + if (targets[0] in self.channel_layer.invite[user.pk]): + self.channel_layer.invite[user.pk].remove(targets[0]) + + return 200, None + + def refuse_invite(self, event): + + user = self.scope["user"] + if (user.is_anonymous or not user.is_authenticated): + return + + invites = self.get_invites(user) + + self.send(text_data=json.dumps({ + 'type':event['type'], + 'author_id':event['author_id'], + 'invites': invites, + 'time': event['time'], + 'status':event['status'], + })) + + def pre_ask_friend(self, user, targets): + + if (user.is_anonymous or not user.is_authenticated): + return 400, None + + if (AskFriendModel.objects.filter(asker=user.pk, asked=targets[0])): + return 409, None + + if (FriendModel().isFriend(user.pk, targets[0])): + return 409, None + + if (targets[0] != None): + AskFriendModel(asker=user.pk, asked=targets[0]).save() + + return 200, None + + def send_ask_friend(self, event): + + user = self.scope["user"] + if (user.is_anonymous or not user.is_authenticated): + return + + asked = AskFriendModel().getAsked(user.pk) + asker = AskFriendModel().getAsker(user.pk) + + online_friends = self.get_online_friend(user) + + self.send(text_data=json.dumps({ + 'type':event['type'], + 'author_id':event['author_id'], + 'targets':event['targets'], + 'asker': asker, + 'asked': asked, + 'online': online_friends, + 'time': event['time'], + 'status':event['status'], + })) + + def ask_friend(self, event): + self.send_ask_friend(event) + + def delete_ask(self, asker, user_asked): + if (user_asked.is_anonymous or not user_asked.is_authenticated): + return 400, None + + asked = user_asked.pk + + if (not AskFriendModel.objects.filter(asker=asker, asked=asked)): + return 404, None + + if (FriendModel().isFriend(asker, asked)): + return 409, None + + if (not AskFriendModel().deleteAsk(asker, asked)): + return 400, None + + return 200, None + + def pre_accept_friend(self, user, targets): + status, result = self.delete_ask(targets[0], user) + + if (status == 200): + FriendModel(user_id1=user.pk, user_id2=targets[0]).save() + return status, result + + def accept_friend(self, event): + self.send_ask_friend(event) + + def pre_refuse_friend(self, user, targets): + return self.delete_ask(targets[0], user) + + def refuse_friend(self, event): + self.send_ask_friend(event) + + def pre_remove_friend(self, user, targets): + + if (user.is_anonymous or not user.is_authenticated): + return 400, None + + if (not FriendModel().isFriend(user.pk, targets[0])): + return 409, None + + if (not FriendModel().deleteFriend(user.pk, targets[0])): + return 400, None + + return 200, None + + def remove_friend(self, event): + self.send_ask_friend(event) + + + def get_online_friend(self, user): + online_friends = {} + for friend in FriendModel().getFriends(user.pk): + if (friend in self.channel_layer.users_channels): + online_friends[friend] = "green" + else: + online_friends[friend] = "red" + return online_friends + + def online_users(self, event): + + user = self.scope["user"] + if (user.is_anonymous or not user.is_authenticated): + return + + online_friends = self.get_online_friend(user) + + self.send(text_data=json.dumps({ + 'type':event['type'], + 'author_id':event['author_id'], + 'online':online_friends, + 'time': event['time'], + 'status':event['status'], + })) diff --git a/chat/models.py b/chat/models.py index d8eb491..a53d15c 100644 --- a/chat/models.py +++ b/chat/models.py @@ -5,28 +5,28 @@ from django.contrib import admin # Create your models here. class ChatChannelModel(models.Model): - - def create(self, users_id: [int]): - self.save() - for user_id in users_id: - ChatMemberModel(channel_id = self.pk, member_id = user_id).save() - return self.pk + + def create(self, users_id: [int]): + self.save() + for user_id in users_id: + ChatMemberModel(channel_id = self.pk, member_id = user_id).save() + return self.pk - def get_members_id(self): - return [member_channel.member_id for member_channel in ChatMemberModel.objects.filter(channel_id = self.pk)] + def get_members_id(self): + return [member_channel.member_id for member_channel in ChatMemberModel.objects.filter(channel_id = self.pk)] class ChatMemberModel(models.Model): - member_id = IntegerField(primary_key=False) - channel_id = IntegerField(primary_key=False) + member_id = IntegerField(primary_key=False) + channel_id = IntegerField(primary_key=False) - def __str__(self): - return "member_id: " + str(self.member_id) + ", channel_id: " + str(self.channel_id) + def __str__(self): + return "member_id: " + str(self.member_id) + ", channel_id: " + str(self.channel_id) class ChatMessageModel(models.Model): - channel_id = IntegerField(primary_key=False) - author_id = IntegerField(primary_key=False) - content = models.CharField(max_length=255) - time = IntegerField(primary_key=False) + channel_id = IntegerField(primary_key=False) + author_id = IntegerField(primary_key=False) + content = models.CharField(max_length=255) + time = IntegerField(primary_key=False) - def __str__(self): - return "author_id: " + str(self.author_id) + ", channel_id: " + str(self.channel_id) + ", content: " + self.content + def __str__(self): + return "author_id: " + str(self.author_id) + ", channel_id: " + str(self.channel_id) + ", content: " + self.content diff --git a/chat/routing.py b/chat/routing.py index 6b8fc6f..642f6c0 100644 --- a/chat/routing.py +++ b/chat/routing.py @@ -4,6 +4,6 @@ from . import consumersChat from . import consumersNotice websocket_urlpatterns = [ - re_path(r'ws/chat/(?P\d+)$', consumersChat.ChatConsumer.as_asgi()), - re_path(r'ws/chat/notice$', consumersNotice.ChatNoticeConsumer.as_asgi()), + re_path(r'ws/chat/(?P\d+)$', consumersChat.ChatConsumer.as_asgi()), + re_path(r'ws/chat/notice$', consumersNotice.ChatNoticeConsumer.as_asgi()), ] diff --git a/chat/tests.py b/chat/tests.py index cf0ee9f..6be487e 100644 --- a/chat/tests.py +++ b/chat/tests.py @@ -6,25 +6,25 @@ from django.contrib.auth.models import User # Create your tests here. class ChatTest(TestCase): - def setUp(self): - self.client = Client() + def setUp(self): + self.client = Client() - self.username='bozo1' - self.password='password' + self.username='bozo1' + self.password='password' - self.user: User = User.objects.create_user(username=self.username, password=self.password) + self.user: User = User.objects.create_user(username=self.username, password=self.password) - self.dest: User = User.objects.create_user(username="bozo2", password=self.password) + self.dest: User = User.objects.create_user(username="bozo2", password=self.password) - self.url = "/api/chat/" + self.url = "/api/chat/" - def test_create_chat(self): - self.client.login(username=self.username, password=self.password) - response: HttpResponse = self.client.post(self.url + str(self.user.pk), {"members_id": [self.user.pk, self.dest.pk]}) - response_dict: dict = eval(response.content) - self.assertDictEqual(response_dict, {}) + def test_create_chat(self): + self.client.login(username=self.username, password=self.password) + response: HttpResponse = self.client.post(self.url + str(self.user.pk), {"members_id": [self.user.pk, self.dest.pk]}) + response_dict: dict = eval(response.content) + self.assertDictEqual(response_dict, {}) - def test_create_chat_unlogged(self): - response: HttpResponse = self.client.post(self.url + str(self.user.pk), {"members_id": [self.user.pk, self.dest.pk]}) - response_dict: dict = eval(response.content) - self.assertDictEqual(response_dict, {'detail': 'Authentication credentials were not provided.'}) + def test_create_chat_unlogged(self): + response: HttpResponse = self.client.post(self.url + str(self.user.pk), {"members_id": [self.user.pk, self.dest.pk]}) + response_dict: dict = eval(response.content) + self.assertDictEqual(response_dict, {'detail': 'Authentication credentials were not provided.'}) diff --git a/chat/views.py b/chat/views.py index 125d728..4b73cbf 100644 --- a/chat/views.py +++ b/chat/views.py @@ -14,35 +14,35 @@ from .serializers import ChatChannelSerializer, ChatMessageSerializer class ChannelView(APIView): - queryset = ChatChannelModel.objects - serializer_class = ChatChannelSerializer - permission_classes = (permissions.IsAuthenticated,) - authentication_classes = (SessionAuthentication,) + queryset = ChatChannelModel.objects + serializer_class = ChatChannelSerializer + permission_classes = (permissions.IsAuthenticated,) + authentication_classes = (SessionAuthentication,) - def post(self, request): + def post(self, request): - serializer = self.serializer_class(data = request.data) - - serializer.is_valid(raise_exception=True) + serializer = self.serializer_class(data = request.data) + + serializer.is_valid(raise_exception=True) - data: dict = serializer.validated_data + data: dict = serializer.validated_data - members_id = data.get("members_id") - if members_id == None: - return Response({"detail": "members_id is None."}, status = status.HTTP_400_BAD_REQUEST) + members_id = data.get("members_id") + if members_id == None: + return Response({"detail": "members_id is None."}, status = status.HTTP_400_BAD_REQUEST) - if self.request.user.pk not in members_id: - return Response({"detail": "You are trying to create a chat group without you."}, status = status.HTTP_400_BAD_REQUEST) + if self.request.user.pk not in members_id: + return Response({"detail": "You are trying to create a chat group without you."}, status = status.HTTP_400_BAD_REQUEST) - for member_channel in ChatMemberModel.objects.filter(member_id = members_id[0]): - channel_id: int = member_channel.channel_id - if not ChatChannelModel.objects.filter(pk = channel_id).exists(): - continue - channel: ChatChannelModel = ChatChannelModel.objects.get(pk = channel_id) - if set(channel.get_members_id()) == set(members_id): - messages = ChatMessageModel.objects.filter(channel_id = channel_id).order_by("time") - messages = serializers.serialize("json", messages) - return Response({'channel_id': channel_id, 'messages': messages}, status=status.HTTP_200_OK) + for member_channel in ChatMemberModel.objects.filter(member_id = members_id[0]): + channel_id: int = member_channel.channel_id + if not ChatChannelModel.objects.filter(pk = channel_id).exists(): + continue + channel: ChatChannelModel = ChatChannelModel.objects.get(pk = channel_id) + if set(channel.get_members_id()) == set(members_id): + messages = ChatMessageModel.objects.filter(channel_id = channel_id).order_by("time") + messages = serializers.serialize("json", messages) + return Response({'channel_id': channel_id, 'messages': messages}, status=status.HTTP_200_OK) - new_channel_id = ChatChannelModel().create(members_id) - return Response({'channel_id': new_channel_id}, status=status.HTTP_201_CREATED) + new_channel_id = ChatChannelModel().create(members_id) + return Response({'channel_id': new_channel_id}, status=status.HTTP_201_CREATED) diff --git a/frontend/static/css/index.css b/frontend/static/css/index.css index 7e35fb0..6c04bb5 100644 --- a/frontend/static/css/index.css +++ b/frontend/static/css/index.css @@ -1,12 +1,3 @@ -*{ - color: #cccccc; - font-size: 35px; - background-color: #1a1a1a; -} - -body { -} - #app #avatar { max-height: 10em; max-width: 10em; @@ -34,3 +25,7 @@ body { opacity: 0; transition: opacity 0.25s; } + +#languageSelector > .dropdown-item.active { + background-color: transparent; +} diff --git a/frontend/static/css/me.css b/frontend/static/css/me.css deleted file mode 100644 index 1d731ee..0000000 --- a/frontend/static/css/me.css +++ /dev/null @@ -1,17 +0,0 @@ -#app #main .account -{ - color: #1a1a1a; -} - -#app #main -{ - width: 60%; - display: flex; - flex-direction: column; - color: #1a1a1a; -} - -#app #main .profile -{ - color: #1a1a1a; -} diff --git a/frontend/static/css/profile.css b/frontend/static/css/profile.css index b86bfb0..a1ead7d 100644 --- a/frontend/static/css/profile.css +++ b/frontend/static/css/profile.css @@ -1,14 +1,25 @@ +#app * { + font-size: 30px; +} + #app #username { font-size: 0.8em; } -#app #block { +#app #block, #app #friend { cursor: pointer; font-size: 0.7em; text-decoration: underline; } #app { - margin-top: 20px; + margin-top: 1em; +} + +#app #yes, #app #no { + display:inline; + cursor: pointer; + font-size: 0.7em; + text-decoration: underline; } diff --git a/frontend/static/css/search.css b/frontend/static/css/search.css index 1b70a0a..27b9734 100644 --- a/frontend/static/css/search.css +++ b/frontend/static/css/search.css @@ -1,3 +1,7 @@ +#app * { + font-size: 40px; +} + #app img { @@ -78,7 +82,6 @@ border: none; outline: none; border-bottom: 0.15em solid green; - caret-color: green; color: green; font-size: 0.8em; } @@ -106,9 +109,8 @@ word-wrap: break-word; } -#app #invite { +#app #invite, #app #yes, #app #no { position: relative; - background-color: green; border: none; color: white; text-align: center; @@ -118,3 +120,15 @@ width: 4em; cursor: pointer; } + +#app #yes, #app #no { + position: relative; + border: none; + color: white; + text-align: center; + text-decoration: none; + font-size: 0.8em; + height: 2em; + width: 2em; + cursor: pointer; +} diff --git a/frontend/static/css/settings.css b/frontend/static/css/settings.css new file mode 100644 index 0000000..6879d41 --- /dev/null +++ b/frontend/static/css/settings.css @@ -0,0 +1,11 @@ +#app * { + font-size: 30px; +} + + +#app #main +{ + width: 60%; + display: flex; + flex-direction: column; +} diff --git a/frontend/static/js/api/LanguageManager.js b/frontend/static/js/api/LanguageManager.js index 3e1c9b3..e74724b 100644 --- a/frontend/static/js/api/LanguageManager.js +++ b/frontend/static/js/api/LanguageManager.js @@ -1,45 +1,74 @@ +import { reloadView } from '../index.js' + export default class LanguageManager { constructor() { - this.availableLanguages = ['en', 'fr']; + this.availableLanguages = ['en', 'fr', 'tp', 'cr']; + this.dict = null; this.currentLang = 'en' this.chosenLang = localStorage.getItem('preferedLanguage') || this.currentLang; if (this.chosenLang !== this.currentLang && this.availableLanguages.includes(this.chosenLang)) { - this.translatePage(); + this.loading = this.translatePage(); + this.currentLang = this.chosenLang; + } else { + this.loading = this.loadDict(this.chosenLang); } + document.getElementById('languageDisplay').innerHTML = + document.querySelector(`#languageSelector > [value=${this.currentLang}]`)?.innerHTML; } async translatePage() { if (this.currentLang === this.chosenLang) return; - let dictUrl = `${location.origin}/static/js/lang/${this.chosenLang}.json`; - let translation = await fetch(dictUrl).then(response => { - if (response.status !== 200) - return null; - return response.json(); - }); - if (!translation) { - console.log(`No translation found for language ${this.chosenLang}`); + await this.loadDict(this.chosenLang); + if (!this.dict) return 1; - } + document.querySelectorAll('[data-i18n]').forEach(el => { let key = el.getAttribute('data-i18n'); - el.innerHTML = translation[key]; + el.innerHTML = this.dict[key]; }) + await reloadView(); - this.currentLang = this.chosenLang; return 0; } async changeLanguage(lang) { if (lang === this.currentLang || !this.availableLanguages.includes(lang)) - return; + return 1; this.chosenLang = lang; if (await this.translatePage() !== 0) - return; + return 1; + this.currentLang = this.chosenLang; localStorage.setItem('preferedLanguage', lang); + document.getElementById('languageDisplay').innerHTML = + document.querySelector(`#languageSelector > [value=${this.currentLang}]`)?.innerHTML; + return 0; + } + + async loadDict(lang) { + let dictUrl = `${location.origin}/static/js/lang/${lang}.json`; + let response = await fetch(dictUrl); + + if (response.status !== 200) { + console.log(`No translation found for language ${lang}`); + return; + } + + this.dict = await response.json(); + } + + async waitLoading() { + await this.loading; + } + + get(key, defaultTxt) { + if (!this.dict) + return defaultTxt; + + return this.dict[key] || defaultTxt; } } diff --git a/frontend/static/js/api/MyProfile.js b/frontend/static/js/api/MyProfile.js index de3f46e..fb4a9fd 100644 --- a/frontend/static/js/api/MyProfile.js +++ b/frontend/static/js/api/MyProfile.js @@ -9,7 +9,7 @@ class MyProfile extends Profile */ constructor (client) { - super(client, "me") + super(client, "../me") } /** @@ -19,7 +19,7 @@ class MyProfile extends Profile */ async change_avatar(form_data) { - let response = await this.client._patch_file(`/api/profiles/me`, form_data); + let response = await this.client._patch_file(`/api/profiles/settings`, form_data); let response_data = await response.json() return response_data; @@ -27,4 +27,4 @@ class MyProfile extends Profile } -export {MyProfile} \ No newline at end of file +export {MyProfile} diff --git a/frontend/static/js/api/account.js b/frontend/static/js/api/account.js index b8b7e83..61f96bd 100644 --- a/frontend/static/js/api/account.js +++ b/frontend/static/js/api/account.js @@ -16,19 +16,16 @@ class Account /** * @param {String} username * @param {String} password - * @returns {?Promise} + * @returns {Response} */ async create(username, password) { let response = await this.client._post("/api/accounts/register", {username: username, password: password}); - let response_data = await response.json() - if (response_data == "user created") - { + if (response.status === 201) await this.client._update_logged(true); - return null; - } - return response_data + + return response; } /** diff --git a/frontend/static/js/api/chat/notice.js b/frontend/static/js/api/chat/notice.js index 1ae4d23..f512d91 100644 --- a/frontend/static/js/api/chat/notice.js +++ b/frontend/static/js/api/chat/notice.js @@ -1,3 +1,4 @@ +import { navigateTo } from "../../index.js"; import {create_popup} from "../../utils/noticeUtils.js"; class Notice { @@ -5,11 +6,10 @@ class Notice { this.client = client; this.data = {}; - // users online, invited by, asked friend by, - let data_variable = ["online", "invited", "asked"]; - for (let i in data_variable) { + // users online, invited by ..., asked by ..., asker to ... + let data_variable = ["online", "invited", "asked", "asker"]; + for (let i in data_variable) this.data[data_variable[i]] = []; - } this.connect(); @@ -20,15 +20,20 @@ class Notice { this.chatSocket = new WebSocket(url); this.chatSocket.onmessage = (event) =>{ - let data = JSON.parse(event.data); - //console.log("notice: ", data); - if (data.type == "invite") - this.receiveInvite(data); - else if (data.type == "online_users" || data.type == "disconnect") - this.receiveOnlineUser(data); + let send = JSON.parse(event.data); + //console.log("notice: ", send); + + try { + this["receive_" + send.type](send); + } + catch (error) { + console.log("receive_" + send.type + ": Function not found"); + } + } this.chatSocket.onopen = (event) => { this.getOnlineUser(); + this.ask_friend(); } } @@ -37,64 +42,126 @@ class Notice { return ; this.chatSocket.close(); + } + + async reconnect() { + this.disconnect(); + this.connect(); + } + + async accept_invite(invitedBy) { + + this.sendRequest({ + type: "accept_invite", + targets: [invitedBy], + }); } - async sendInvite(id_inviter, id_inviteds) { - - if (this.chatSocket == undefined) - return; + async receive_accept_invite(send) { - this.chatSocket.send(JSON.stringify({ + this.data["invited"] = send.invites; + let id_game = send["id_game"]; + navigateTo("/game/" + id_game); + + } + + async refuse_invite(invitedBy) { + + this.sendRequest({ + type: "refuse_invite", + targets: [invitedBy], + }); + + + } + async receive_refuse_invite(send) { + + this.data["invited"] = send.invites; + + if (send.author_id == this.client.me.id) { + if (this.rewrite_invite !== undefined) + this.rewrite_invite(); + } + else { + let sender = await this.client.profiles.getProfileId(send.author_id); + create_popup(sender.username + " refuse your invitation"); + } + + } + + + async send_invite(id_inviteds) { + + this.sendRequest({ type: "invite", targets: id_inviteds, - })); + time: new Date().getTime(), + }); } - async receiveInvite(data) { + async receive_invite(send) { - if (data.content === "notice return") { - if (data.status == 200) { - for (let target in data.targets) - this.data["invited"].push(target); + if (this.client.me == undefined) + return ; + + let content = send.invites; + + if (send.author_id == this.client.me.id) { + if (send.status == 200) { + for (let target in send.targets) return create_popup("Invitation send"); } - else if (data.status == 404) + else if (send.status == 444) return create_popup("User not connected"); - else if (data.status == 409) + else if (send.status == 409) return create_popup("Already invited"); } else { - let sender = await this.client.profiles.getProfile(data.author_id); - this.inviter.push(data.author_id); + // Regarder qu'il est bien invité par l'auteur + // Et qu'il n'est pas déjà invité + if (!content.includes(send.author_id) || + this.data["invited"].includes(send.author_id)) + return; + + this.data["invited"] = content; + let sender = await this.client.profiles.getProfileId(send.author_id); + create_popup("Invitation received by " + sender.username); - // Géré la reception de l'invitation + if (this.rewrite_invite !== undefined) + this.rewrite_invite(); } } async getOnlineUser() { - if (this.chatSocket == undefined) - return; - this.online_users = {}; - this.chatSocket.send(JSON.stringify({ + this.sendRequest({ type: "online_users", - targets: "all", - })); + targets: [], + time: new Date().getTime(), + }); } - async receiveOnlineUser(data) { - if (data.content !== undefined) { + async receive_online_users(send) { + let content = send.online; + if (content !== undefined) { - if (this.online_users.length > 0) { + if (this.data["online"].length > 0) { // get all disconnect user - let disconnects = this.online_users.filter(id => !Object.keys(data.content).includes(id)); + //let disconnects = this.data["online"].filter(id => !Object.keys(content).includes(id)); + + let disconnects = []; + + for (const [key, value] of Object.entries(this.data["online"])) { + if (content[key] == "red" && value == "green") + disconnects.push(key); + } // delete invite this.data["invited"] = this.data["invited"].filter(id => !disconnects.includes(id)); @@ -102,12 +169,139 @@ class Notice { //console.log(this.data["invited"]); } - this.data["online"] = Object.keys(data.content); + this.data["online"] = content; if (this.rewrite_usernames !== undefined) this.rewrite_usernames(); } } + + async ask_friend(user_id=undefined) { + this.sendRequest({ + type: "ask_friend", + targets: [user_id], + time: new Date().getTime(), + }); + } + + async receive_ask_friend(send) { + + let my_id = (this.client.me && this.client.me.id) || send.author_id; + if (send.author_id == my_id) { + if (send.status == 400) + create_popup("Friend ask error"); + else if (send.status == 409) + create_popup("Already asked friend"); + } + + //if (!send.asked.includes(send.author_id) || + //this.data["asked"].includes(send.author_id)) + //return; + + //if (!send.asker.includes(send.author_id) || + //this.data["asker"].includes(send.author_id)) + //return; + + this.data["asked"] = send.asked; + this.data["asker"] = send.asker; + + if (send.author_id != my_id) { + let sender = await this.client.profiles.getProfileId(send.author_id); + if (this.data["asker"].includes(send.author_id)) + create_popup(sender.username + " ask you as friend"); + if (this.rewrite_profile !== undefined) + await this.rewrite_profile(); + } + + } + + async remove_friend(user_id) { + this.sendRequest({ + type: "remove_friend", + targets: [user_id], + time: new Date().getTime(), + }); + } + + async receive_remove_friend(send) { + + if (send.author_id == this.client.me.id) { + if (send.status == 400) + create_popup("Error remove Friend"); + else if (send.status == 409) + create_popup("Not friend, wtf"); + + } + + if (this.rewrite_profile !== undefined) + await this.rewrite_profile(); + + this.receive_online_users(send); + } + + async accept_friend(user_id) { + this.sendRequest({ + type: "accept_friend", + targets: [user_id], + time: new Date().getTime(), + }); + } + + async receive_accept_friend(send) { + this.data["asked"] = send.asked; + this.data["asker"] = send.asker; + let sender = await this.client.profiles.getProfileId(send.author_id); + + if (send.author_id == this.client.me.id) { + if (send.status == 400) + create_popup("Error accept Friend"); + else if (send.status == 404) + create_popup("Not found request Friend"); + else if (send.status == 409) + create_popup("Already Friend, wtf"); + } + else { + create_popup(sender.username + " accept your friend request"); + } + + if (this.rewrite_profile !== undefined) + await this.rewrite_profile(); + + this.receive_online_users(send); + } + + async refuse_friend(user_id) { + this.sendRequest({ + type: "refuse_friend", + targets: [user_id], + time: new Date().getTime(), + }); + } + + async receive_refuse_friend(send) { + this.data["asked"] = send.asked; + this.data["asker"] = send.asker; + let sender = await this.client.profiles.getProfileId(send.author_id); + + if (send.author_id == this.client.me.id) { + if (send.status == 400) + create_popup("Error refuse Friend"); + else if (send.status == 404) + create_popup("Not found request Friend"); + else if (send.status == 409) + create_popup("Already Friend, WTF"); + + } + if (this.rewrite_profile !== undefined) + await this.rewrite_profile(); + } + + async sendRequest(content) { + if (this.chatSocket == undefined) + return; + + this.chatSocket.send(JSON.stringify(content)); + } } export {Notice} diff --git a/frontend/static/js/api/client.js b/frontend/static/js/api/client.js index 3d0a900..df0a7b1 100644 --- a/frontend/static/js/api/client.js +++ b/frontend/static/js/api/client.js @@ -53,7 +53,7 @@ class Client this.tournaments = new Tourmanents(this); /** - * @type {Boolean} A private var represent if the is is log NEVER USE IT use await isAuthentificated() + * @type {Boolean} A private var represent if the is is log NEVER USE IT use await isAuthenticated() */ this._logged = undefined; @@ -73,13 +73,14 @@ class Client this.notice = new Notice(this); this.lang = new LanguageManager; + } /** * The only right way to determine is the user is logged * @returns {Promise} */ - async isAuthentificate() + async isAuthenticated() { if (this._logged == undefined) this._logged = await this._test_logged(); @@ -114,7 +115,8 @@ class Client headers: { "Content-Type": "application/json", "X-CSRFToken": getCookie("csrftoken"), - }, + 'Accept-Language': this.lang.currentLang, + }, body: JSON.stringify(data), }); return response; @@ -190,6 +192,7 @@ class Client { this.me = new MyProfile(this); await this.me.init(); + this.notice.reconnect(); document.getElementById('navbarLoggedOut').classList.add('d-none'); document.getElementById('navbarLoggedIn').classList.remove('d-none'); document.getElementById('navbarDropdownButton').innerHTML = this.me.username; @@ -198,6 +201,7 @@ class Client else { this.me = undefined; + this.notice.reconnect(); document.getElementById('navbarLoggedOut').classList.remove('d-none'); document.getElementById('navbarLoggedIn').classList.add('d-none'); document.getElementById('navbarDropdownButton').innerHTML = 'Me'; @@ -232,7 +236,7 @@ class Client } /** - * Determine if the user is logged. NEVER USE IT, USE isAuthentificated() + * Determine if the user is logged. NEVER USE IT, USE isAuthenticated() * @returns {Promise} */ async _test_logged() diff --git a/frontend/static/js/api/game/Game.js b/frontend/static/js/api/game/Game.js index e2417a7..97d74a0 100644 --- a/frontend/static/js/api/game/Game.js +++ b/frontend/static/js/api/game/Game.js @@ -4,6 +4,7 @@ import { GameConfig } from "./GameConfig.js" import { Player } from "./Player.js"; import { Time } from "./Time.js"; import { Wall } from "./Wall.js"; +import { Client } from "../client.js"; class Game { @@ -21,7 +22,7 @@ class Game /** * - * @returns {Number} + * @returns {Promise} */ async init() { @@ -32,23 +33,52 @@ class Game let response_data = await response.json(); + /** + * @type {[Number]} + */ this.players_id = response_data.players_id; + + /** + * @type {String} + */ this.state = response_data.state; + + /** + * @type {Boolean} + */ this.started = response_data.started; + + /** + * @type {Boolean} + */ this.finished = response_data.finished; + + /** + * @type {Number} + */ this.winner_id = this.finished ? response_data.winner_id : undefined; if (this.finished === true) return 0; + /** + * @type {GameConfig} + */ this.config = new GameConfig(this.client); + let ret = await this.config.init(); if (ret !== 0) return ret; + /** + * @type {Time} + */ this.time = new Time(); - this.last_pos = null + + /** + * @type {Boolean} + */ this._inited = false; return 0; @@ -72,14 +102,14 @@ class Game * * @param {CanvasRenderingContext2D} ctx */ - draw(ctx) + render(ctx) { if(ctx instanceof CanvasRenderingContext2D) { ctx.clearRect(0, 0, this.config.size_x, this.config.size_y); } this.draw_sides(ctx); - this.ball.draw(ctx); + this.ball.render(ctx); } _send(data) @@ -93,50 +123,25 @@ class Game } } + /** + * @param {Number} position + * @param {Number} time + */ _send_paddle_position(position, time) { - if (this.last_pos !== null && this.last_pos.time >= time) - return; - - this.last_pos = {"time": time, "position": position}; - - this._send({"detail": "update_my_paddle_pos", ...this.last_pos}); - } - - _receive_player_join(player_data) - { - console.log(player_data) - let index = this.players.indexOf((player) => player.id === player_data.user_id); - - this.players[index].is_connected = true; - } - - _receive_player_leave(player_data) - { - let index = this.players.indexOf((player) => player.id === player_data.user_id); - - this.players[index].is_connected = false; - } - - _receive_update_ball(data) - { - this.ball.position_x = data.position_x - this.ball.position_y = data.position_y - this.ball.position_x = data.position_x - this.ball.position_x = data.position_x + this._send({"detail": "update_my_paddle_pos", ...{"time": time, "position": position}}); } _receive_update_paddle(data) { let player = this.players.find((player) => player.id === data.user_id); - if (player === null) - { - this._receive_player_join(data); - return; - } - player.is_connected = data.is_connected; - player.update_pos(data.position.position, data.position.time); + player.from_json(data); + } + + _receive_ball(data) + { + this.ball.from_json(data); } _receive(data) @@ -144,26 +149,30 @@ class Game if (data.detail === "update_paddle") this._receive_update_paddle(data); else if (data.detail === "update_ball") - this._receive_update_ball(data); + this._receive_ball(data) else if (data.detail === "init_game") - this._init_game(data) - else if (data.detail === "player_join") - this._receive_player_join(data) - else if (data.detail === "player_leave") - this._receive_player_leave(data) + this._init_game(data); } _init_game(data) { - const ball_data = data.ball; - this.ball = new Ball(this, ball_data.position_x, ball_data.position_y, ball_data.velocity_x, ball_data.velocity_y); + /** + * @type {Ball} + */ + this.ball = (new Ball(this)).from_json(data.ball) + /** + * @type {[Wall]} + */ this.walls = []; const walls_data = data.walls; walls_data.forEach((wall_data) => { this.walls.push(new Wall().from_json(wall_data)); }); + /** + * @type {[Player]} + */ this.players = [] const players_data = data.players; players_data.forEach((player_data) => { diff --git a/frontend/static/js/api/game/GameConfig.js b/frontend/static/js/api/game/GameConfig.js index dab00ad..4b2ae61 100644 --- a/frontend/static/js/api/game/GameConfig.js +++ b/frontend/static/js/api/game/GameConfig.js @@ -24,14 +24,17 @@ class GameConfig * @type {Number} */ this.size_x = response_data.MAP_SIZE_X; + /** * @type {Number} */ this.size_y = response_data.MAP_SIZE_Y; + /** * @type {Number} */ this.center_x = this.size_x / 2; + /** * @type {Number} */ @@ -63,10 +66,12 @@ class GameConfig * @type {Number} */ this.ball_size = response_data.BALL_SIZE; + /** * @type {Number} */ this.ball_spawn_x = this.center_x; + /** * @type {Number} */ diff --git a/frontend/static/js/api/game/Segment.js b/frontend/static/js/api/game/Segment.js index 23bf3b6..4f05482 100644 --- a/frontend/static/js/api/game/Segment.js +++ b/frontend/static/js/api/game/Segment.js @@ -9,8 +9,32 @@ class Segment */ constructor(start, stop) { + /** + * @type {Point} + */ this.start = start === undefined ? new Point() : start; + + /** + * @type {Point} + */ this.stop = stop === undefined ? new Point() : stop; + + } + + angle() + { + let x = this.start.x - this.stop.x, + y = this.start.y - this.stop.y; + + return Math.atan2(y, x); + } + + len() + { + let x = this.start.x - this.stop.x, + y = this.start.y - this.stop.y; + + return (x ** 2 + y ** 2) ** (1 / 2); } /** diff --git a/frontend/static/js/api/game/Time.js b/frontend/static/js/api/game/Time.js index e8885b9..7edb1d7 100644 --- a/frontend/static/js/api/game/Time.js +++ b/frontend/static/js/api/game/Time.js @@ -17,7 +17,9 @@ class Time deltaTime() { - return (this._current_frame - this._last_frame) !== NaN ? this._current_frame - this._last_frame : 0; + if (this._last_frame === undefined) + return 0; + return (this._current_frame - this._last_frame); } deltaTimeSecond() diff --git a/frontend/static/js/api/matchmaking.js b/frontend/static/js/api/matchmaking.js index c63dd4d..03063ca 100644 --- a/frontend/static/js/api/matchmaking.js +++ b/frontend/static/js/api/matchmaking.js @@ -23,7 +23,7 @@ class MatchMaking */ async start(receive_func, disconnect_func, mode) { - if (!await this.client.isAuthentificate()) + if (!await this.client.isAuthenticated()) return null; let url = `${window.location.protocol[4] === 's' ? 'wss' : 'ws'}://${window.location.host}/ws/matchmaking/${mode}`; diff --git a/frontend/static/js/api/profile.js b/frontend/static/js/api/profile.js index 918761d..65390e0 100644 --- a/frontend/static/js/api/profile.js +++ b/frontend/static/js/api/profile.js @@ -5,7 +5,7 @@ class Profile /** * @param {Client} client */ - constructor (client, username, id = undefined, avatar_url = undefined) + constructor (client, username=undefined, id=undefined, avatar_url=undefined) { /** * @type {Client} client @@ -31,6 +31,7 @@ class Profile * @type {Boolean} */ this.isBlocked = false; + this.isFriend = false; } /** @@ -39,7 +40,11 @@ class Profile */ async init() { - let response = await this.client._get(`/api/profiles/${this.username}`); + let response; + if (this.username !== undefined) + response = await this.client._get(`/api/profiles/user/${this.username}`); + else + response = await this.client._get(`/api/profiles/id/${this.id}`); if (response.status !== 200) return response.status; @@ -47,25 +52,49 @@ class Profile let response_data = await response.json(); this.id = response_data.user_id; this.username = response_data.username; - this.avatar_url = response_data.avatar_url; + this.avatar_url = response_data.avatar; - if (this.client.me == undefined) - return; + await this.getBlock(); + await this.getFriend(); + } + + async getBlock() { let block_response = await this.client._get("/api/profiles/block"); - + if (block_response.status != 200) return let block_data = await block_response.json(); - let block_list = JSON.parse(block_data); + let block_list = JSON.parse(block_data["blockeds"]); + let client_id = block_data["user_id"]; block_list.forEach(block => { let blocker = block.fields.blocker; let blocked = block.fields.blocked; - if (blocker == this.client.me.user_id && blocked == user_id) + if (blocker == client_id && blocked == this.id) return this.isBlocked = true; }); } + + async getFriend() { + let friend_response = await this.client._get("/api/profiles/friend"); + + this.isFriend = false; + if (friend_response.status != 200) + return this.isFriend; + + let friend_data = await friend_response.json(); + let friends_list = friend_data["friends"]; + let client_id = friend_data["user_id"]; + friends_list.forEach(friend => { + if (friend == this.id) { + this.isFriend = true; + return this.isFriend; + } + }); + return this.isFriend; + } + } export {Profile} diff --git a/frontend/static/js/api/profiles.js b/frontend/static/js/api/profiles.js index 3d46efe..8a00d8a 100644 --- a/frontend/static/js/api/profiles.js +++ b/frontend/static/js/api/profiles.js @@ -24,7 +24,7 @@ class Profiles let profiles = [] response_data.forEach((profile) => { - profiles.push(new Profile(this.client, profile.username, profile.user_id, profile.avatar_url)) + profiles.push(new Profile(this.client, profile.username, profile.user_id, profile.avatar)) }); return profiles; } @@ -32,7 +32,7 @@ class Profiles /** * * @param {String} username - * @returns {?Profile} + * @returns {?Promise} */ async getProfile(username) { @@ -42,6 +42,14 @@ class Profiles return profile; } + async getProfileId(id) + { + let profile = new Profile(this.client, undefined, id); + if (await profile.init()) + return null; + return profile; + } + /** * Block a user * @param {Number} user_id @@ -51,7 +59,7 @@ class Profiles // blocker & blocked let response = await this.client._post("/api/profiles/block", { - users_id:[this.client.me.user_id, user_id], + users_id:[this.client.me.id, user_id], }); let data = await response.json(); @@ -68,7 +76,7 @@ class Profiles // blocker & blocked let response = await this.client._delete("/api/profiles/block", { - users_id:[this.client.me.user_id, user_id], + users_id:[this.client.me.id, user_id], }); let data = await response.json(); diff --git a/frontend/static/js/api/tournament/tournament.js b/frontend/static/js/api/tournament/tournament.js index 46a9502..bbba2d8 100644 --- a/frontend/static/js/api/tournament/tournament.js +++ b/frontend/static/js/api/tournament/tournament.js @@ -120,7 +120,7 @@ class Tourmanent */ async join(receive_func, disconnect_func) { - if (!await this.client.isAuthentificate()) + if (!await this.client.isAuthenticated()) return null; let url = `${window.location.protocol[4] === 's' ? 'wss' : 'ws'}://${window.location.host}/ws/tournaments/${this.id}`; diff --git a/frontend/static/js/index.js b/frontend/static/js/index.js index 8b9de7c..58cbaa8 100644 --- a/frontend/static/js/index.js +++ b/frontend/static/js/index.js @@ -1,10 +1,8 @@ import { Client } from "./api/client.js"; -import LoginView from "./views/accounts/LoginView.js"; import Dashboard from "./views/Dashboard.js"; import Search from "./views/Search.js"; import HomeView from "./views/HomeView.js"; -import RegisterView from "./views/accounts/RegisterView.js"; import LogoutView from "./views/accounts/LogoutView.js"; import GameOfflineView from "./views/GameOfflineView.js"; //import GameView from "./views/GameView.js"; @@ -13,17 +11,19 @@ import GameView from "./views/GameView3D.js"; import PageNotFoundView from './views/PageNotFoundView.js' import AbstractRedirectView from "./views/abstracts/AbstractRedirectView.js"; -import MeView from "./views/MeView.js"; +import SettingsView from "./views/SettingsView.js"; import ProfilePageView from "./views/ProfilePageView.js"; import MatchMakingView from "./views/MatchMakingView.js"; import TournamentPageView from "./views/tournament/TournamentPageView.js"; import TournamentsView from "./views/tournament/TournamentsListView.js"; import TournamentCreateView from "./views/tournament/TournamentCreateView.js"; +import AuthenticationView from "./views/accounts/AuthenticationView.js"; -let client = new Client(location.protocol + "//" + location.host) +let client = new Client(location.origin); +let lang = client.lang; -let lastView = undefined -let lastPageUrlBeforeLogin = undefined +let lastView = undefined; +let lastPageUrlBeforeLogin = undefined; const pathToRegex = path => new RegExp("^" + path.replace(/\//g, "\\/").replace(/:\w+/g, "(.+)") + "$"); @@ -37,11 +37,12 @@ const getParams = match => { }; const navigateTo = async (uri) => { - if (await router(uri) !== 0) - return; history.pushState(null, null, uri); + if (await router(uri) !== 0) + return; + let link = document.querySelector('a[href=\'' + location.pathname + '\']'); if (link) { document.querySelector('[data-link].active')?.classList.remove('active'); @@ -49,9 +50,14 @@ const navigateTo = async (uri) => { } }; +const reloadView = async _ => { + await lastView?.leavePage(); + await renderView(lastView); +} + async function renderView(view) { - let content = await view.getHtml(); + let content = await view?.getHtml(); if (content == null) return 1; @@ -74,12 +80,12 @@ const router = async(uri) => { { path: "/tournaments/create", view: TournamentCreateView }, { path: "/tournaments/:id", view: TournamentPageView }, { path: "/tournaments/", view: TournamentsView }, - { path: "/login", view: LoginView }, + { path: "/login", view: AuthenticationView }, + { path: "/register", view: AuthenticationView }, { path: "/logout", view: LogoutView }, - { path: "/register", view: RegisterView }, { path: "/search", view: Search }, { path: "/home", view: HomeView }, - { path: "/me", view: MeView }, + { path: "/settings", view: SettingsView }, { path: "/matchmaking", view: MatchMakingView }, { path: "/games/offline", view: GameOfflineView }, { path: "/games/:id", view: GameView }, @@ -110,12 +116,13 @@ const router = async(uri) => { const view = new match.route.view(getParams(match), lastPageUrlBeforeLogin); + if (!(view instanceof AuthenticationView) && ! (view instanceof LogoutView)) + lastPageUrlBeforeLogin = uri; + if (view instanceof AbstractRedirectView && await view.redirect()) return 1; lastView = view; - if (uri !== '/login' && uri !== '/register' && uri !== '/logout') - lastPageUrlBeforeLogin = uri; if (await renderView(view)) return 1; @@ -138,13 +145,22 @@ document.addEventListener("DOMContentLoaded", async () => { }); //Languages + await lang.waitLoading(); Array.from(document.getElementById('languageSelector').children).forEach(el => { - el.onclick = _ => client.lang.changeLanguage(el.value); + el.onclick = async _ => { + if (await lang.changeLanguage(el.value)) + return; + console.log(lang); + document.querySelector('#languageSelector > .active')?.classList.remove('active'); + el.classList.add('active'); + }; }); + document.querySelector(`#languageSelector > [value=${lang.chosenLang}]`) + ?.classList.add('active'); - await client.isAuthentificate(); + await client.isAuthenticated(); router(location.pathname); document.querySelector('a[href=\'' + location.pathname + '\']')?.classList.add('active'); }); -export { client, navigateTo } +export { client, lang, navigateTo, reloadView } diff --git a/frontend/static/js/lang/cr.json b/frontend/static/js/lang/cr.json new file mode 100644 index 0000000..82bf7c1 --- /dev/null +++ b/frontend/static/js/lang/cr.json @@ -0,0 +1,39 @@ +{ + "navbarSearch": "cherchbeh", + "navbarHome": "Quoicoubouse", + "navbarLogin": "Quoicouconnec", + "navbarRegister": "Quoicougistré", + "navbarProfile": "Mon crampté Profile", + "navbarSettings": "Roue Crampté", + "navbarLogout": "Déconnexion crampté", + "homeWindowTitle": "Quoicoubouse", + "homeTitle": "Quoicoubouse", + "homeOnline": "Jouer en crampté", + "homeOffline": "Jouer hors crampté", + "homeSettings": "Roue Crampté", + "homeLogout": "Déconnexion crampté", + "loginWindowTitle": "Quoicouconnec", + "loginFormTitle": "Quoicouconnec", + "loginFormUsername": "Nom d'crampté", + "loginFormPassword": "Mot de crampté", + "loginFormButton": "Quoicouconnec", + "loginNoAccount": "Pas de compte encore crampté?", + "loginRegister": "Quoicougistré", + "errorEmptyField": "Ce champ ne peut pas être vide crampté.", + "logoutWindowTitle": "Déconnexion crampté", + "registerWindowTitle": "Quoicougistré", + "registerFormTitle": "Quoicougistré", + "registerFormUsername": "Nom d'crampté", + "registerFormPassword": "Mot de crampté", + "registerFormButton": "Quoicougistré", + "registerAlreadyAccount": "Déjà un compte crampté?", + "registerLogin": "Quoicouconnec", + "404WindowTitle": "Pas crampté", + "SearchWindowTitle": "cherchbeh", + "profileAddFriend": "Demander le cramptéman", + "profileRemoveFriend": "Supprimer le cramptéman", + "profileDenyRequest": "Refuser le cramptéman", + "profileAcceptRequest": "Accepter le cramptéman", + "profileUnblock": "Quoicoudebloquer", + "profileBlock": "Quoicoubloquer" +} diff --git a/frontend/static/js/lang/en.json b/frontend/static/js/lang/en.json index 3d9b14e..8bd8b4b 100644 --- a/frontend/static/js/lang/en.json +++ b/frontend/static/js/lang/en.json @@ -5,5 +5,35 @@ "navbarRegister": "Register", "navbarProfile": "My Profile", "navbarSettings": "Settings", - "navbarLogout": "Logout" + "navbarLogout": "Logout", + "homeWindowTitle": "Home", + "homeTitle": "Home", + "homeOnline": "Play online", + "homeOffline": "Play offline", + "homeSettings": "Settings", + "homeLogout": "Logout", + "loginWindowTitle": "Login", + "loginFormTitle": "Login", + "loginFormUsername": "Username", + "loginFormPassword": "Password", + "loginFormButton": "Login", + "loginNoAccount": "No account yet?", + "loginRegister": "Register", + "errorEmptyField": "This field may not be blank.", + "logoutWindowTitle": "Logout", + "registerWindowTitle": "Register", + "registerFormTitle": "Register", + "registerFormUsername": "Username", + "registerFormPassword": "Password", + "registerFormButton": "Register", + "registerAlreadyAccount": "Already have an account?", + "registerLogin": "Login", + "404WindowTitle": "Not Found", + "SearchWindowTitle": "Search", + "profileAddFriend": "Ask Friend", + "profileRemoveFriend": "Remove Friend", + "profileDenyRequest": "Decline Friend", + "profileAcceptRequest": "Accept Friend", + "profileUnblock": "Unblock", + "profileBlock": "Block" } diff --git a/frontend/static/js/lang/fr.json b/frontend/static/js/lang/fr.json index 3307204..06d2434 100644 --- a/frontend/static/js/lang/fr.json +++ b/frontend/static/js/lang/fr.json @@ -1,9 +1,39 @@ { "navbarSearch": "Recherche", "navbarHome": "Maison", - "navbarLogin": "Se connecter", + "navbarLogin": "Connexion", "navbarRegister": "S'inscrire", "navbarProfile": "Mon Profil", "navbarSettings": "Paramètres", - "navbarLogout": "Se déconnecter" + "navbarLogout": "Déconnexion", + "homeWindowTitle": "Maison", + "homeTitle": "Maison", + "homeOnline": "Jouer en ligne", + "homeOffline": "Jouer hors ligne", + "homeSettings": "Paramètres", + "homeLogout": "Déconnexion", + "loginWindowTitle": "Connexion", + "loginFormTitle": "Connexion", + "loginFormUsername": "Nom d'utilisateur", + "loginFormPassword": "Mot de passe", + "loginFormButton": "Connexion", + "loginNoAccount": "Pas de compte?", + "loginRegister": "S'inscrire", + "errorEmptyField": "Ce champ ne peut être vide.", + "logoutWindowTitle": "Déconnexion", + "registerWindowTitle": "S'inscrire", + "registerFormTitle": "S'inscrire", + "registerFormUsername": "Nom d'utilisateur", + "registerFormPassword": "Mot de passe", + "registerFormButton": "S'inscrire", + "registerAlreadyAccount": "Déjà un compte?", + "registerLogin": "Connexion", + "404WindowTitle": "Pas trouvé", + "SearchWindowTitle": "Recherche", + "profileAddFriend": "Demander en ami", + "profileRemoveFriend": "Retirer l'ami", + "profileDenyRequest": "Refuser l'ami", + "profileAcceptRequest": "Accepter l'ami", + "profileUnblock": "Débloquer", + "profileBlock": "Bloquer" } diff --git a/frontend/static/js/lang/tp.json b/frontend/static/js/lang/tp.json new file mode 100644 index 0000000..7874462 --- /dev/null +++ b/frontend/static/js/lang/tp.json @@ -0,0 +1,40 @@ +{ + "navbarSearch": "Lukin", + "navbarHome": "Tomo", + "navbarLogin": "Open", + "navbarRegister": "Sitelen", + "navbarProfile": "Sitelen mi", + "navbarSettings": "Nasin", + "navbarLogout": "Tawa ala", + "homeWindowTitle": "Tomo", + "homeTitle": "Tomo", + "homeOnline": "Mute tawa", + "homeOffline": "Mute lon", + "homeSettings": "Nasin", + "homeLogout": "Tawa ala", + "loginWindowTitle": "Open", + "loginFormTitle": "Open", + "loginFormUsername": "nimi pi jan Open", + "loginFormPassword": "nimi nasa", + "loginFormButton": "Open", + "loginNoAccount": "sina wile ala wile jo e nimi pi jan Open?", + "loginRegister": "Sitelen", + "errorEmptyField": "nimi ni li wile sitelen.", + "logoutWindowTitle": "Tawa ala", + "registerWindowTitle": "Sitelen", + "registerFormTitle": "Sitelen", + "registerFormUsername": "nimi pi jan sin", + "registerFormPassword": "nimi nasa", + "registerFormButton": "Sitelen", + "registerAlreadyAccount": "sina jo ala jo e nimi pi jan sin?", + "registerLogin": "Open", + "404WindowTitle": "Ala o lukin e ni", + "SearchWindowTitle": "Lukin", + "profileAddFriend": "kama jo e jan", + "profileRemoveFriend": "tawa ala e jan", + "profileDenyRequest": "ante e ijo ni", + "profileAcceptRequest": "kama jo e ijo ni", + "profileUnblock": "Tawa ala e nimi pi jan ni", + "profileBlock": "Tawa e nimi pi jan ni" +} + diff --git a/frontend/static/js/views/GameOfflineView.js b/frontend/static/js/views/GameOfflineView.js index 62c3066..a7dc96e 100644 --- a/frontend/static/js/views/GameOfflineView.js +++ b/frontend/static/js/views/GameOfflineView.js @@ -19,6 +19,10 @@ export default class extends AbstractView { document.getElementById('stopGameButton').onclick = this.stopGame.bind(this); } + async leavePage() { + this.game?.cleanup(); + } + startGame() { if (this.game == null) { document.getElementById('startGameButton').innerHTML = 'Reset Game'; @@ -26,6 +30,7 @@ export default class extends AbstractView { } else { document.getElementById('app').removeChild(this.game.canvas); + document.getElementById('app').removeChild(this.game.scoresDisplay); this.game.cleanup(); this.game = new Game; } diff --git a/frontend/static/js/views/GameView.js b/frontend/static/js/views/GameView.js index 2b8e3c7..11f5580 100644 --- a/frontend/static/js/views/GameView.js +++ b/frontend/static/js/views/GameView.js @@ -13,7 +13,7 @@ export default class extends AbstractView this.my_player = undefined; } - keyStretchHandler(event) + keyReleaseHandler(event) { const idx = this.keys_pressed.indexOf(event.key); if (idx != -1) @@ -26,7 +26,7 @@ export default class extends AbstractView this.keys_pressed.push(event.key); } - draw() + render_game() { const canva = document.getElementById('canva'); @@ -40,36 +40,39 @@ export default class extends AbstractView ctx.beginPath(); - this.game.draw(ctx); + this.game.render(ctx); ctx.strokeStyle = "#000000"; - ctx.lineWidth = 10; + ctx.lineWidth = 1; ctx.stroke(); } - render_game() + render() { let loop_id = setInterval(() => { if (this.game === undefined) clearInterval(loop_id); if (this.my_player) this.my_player.update_paddle(this.keys_pressed); - this.draw(); + this.render_game(); this.game?.time.new_frame(); + //clearInterval(loop_id); // 1 sec fps }, 1000 / 60); } register_key() { - document.addEventListener('keydown', this.keyPressHandler.bind(this)); - document.addEventListener('keyup', this.keyStretchHandler.bind(this)); + this.keyPressHandler = this.keyPressHandler.bind(this); + this.keyReleaseHandler = this.keyReleaseHandler.bind(this); + document.addEventListener('keydown', this.keyPressHandler); + document.addEventListener('keyup', this.keyReleaseHandler); } unregister_key() { document.removeEventListener('keydown', this.keyPressHandler); - document.removeEventListener('keyup', this.keyStretchHandler); + document.removeEventListener('keyup', this.keyReleaseHandler); } async join_game() @@ -99,7 +102,7 @@ export default class extends AbstractView this.register_key() - this.render_game(); + this.render(); } async update_game_state() diff --git a/frontend/static/js/views/HomeView.js b/frontend/static/js/views/HomeView.js index fb13511..6ff6b0e 100644 --- a/frontend/static/js/views/HomeView.js +++ b/frontend/static/js/views/HomeView.js @@ -1,18 +1,19 @@ -import AbstractAuthentificateView from "./abstracts/AbstractAuthentifiedView.js"; +import { lang } from "../index.js"; +import AbstractAuthenticatedView from "./abstracts/AbstractAuthenticatedView.js"; -export default class extends AbstractAuthentificateView { +export default class extends AbstractAuthenticatedView { constructor(params) { - super(params, "Home"); + super(params, 'homeWindowTitle'); this.redirect_url = "/login" } async getHtml() { return /* HTML */ ` -

HOME

- Play a game - Play offline - Me - Logout +

${lang.get('homeTitle', 'Home')}

+ ${lang.get('homeOnline', 'Play online')} + ${lang.get('homeOffline', 'Play offline')} + ${lang.get('homeSettings', 'Settings')} + ${lang.get('homeLogout', 'Logout')} `; } } diff --git a/frontend/static/js/views/MatchMakingView.js b/frontend/static/js/views/MatchMakingView.js index eb084d3..0414fa2 100644 --- a/frontend/static/js/views/MatchMakingView.js +++ b/frontend/static/js/views/MatchMakingView.js @@ -1,8 +1,8 @@ import { client, navigateTo } from "../index.js"; import { clear, fill_errors } from "../utils/formUtils.js"; -import AbstractAuthentifiedView from "./abstracts/AbstractAuthentifiedView.js"; +import AbstractAuthenticatedView from "./abstracts/AbstractAuthenticatedView.js"; -export default class extends AbstractAuthentifiedView { +export default class extends AbstractAuthenticatedView { constructor(params) { super(params, "Matchmaking"); diff --git a/frontend/static/js/views/PageNotFoundView.js b/frontend/static/js/views/PageNotFoundView.js index 5d875c3..01a8ef3 100644 --- a/frontend/static/js/views/PageNotFoundView.js +++ b/frontend/static/js/views/PageNotFoundView.js @@ -1,13 +1,15 @@ import AbstractView from "./abstracts/AbstractView.js"; +import { lang } from '../index.js' export default class extends AbstractView { constructor(params) { - super(params, "Dashboard"); + super(params, '404WindowTitle'); } async getHtml() { return `

404 Bozo

+

Git gud

`; } diff --git a/frontend/static/js/views/ProfilePageView.js b/frontend/static/js/views/ProfilePageView.js index 58aa891..7f69a64 100644 --- a/frontend/static/js/views/ProfilePageView.js +++ b/frontend/static/js/views/ProfilePageView.js @@ -1,25 +1,26 @@ import AbstractView from "./abstracts/AbstractView.js"; -import { client } from "../index.js" +import { client, lang } from "../index.js" export default class extends AbstractView { constructor(params) { - super(params, params.username); - this.username = params.username; + super(params, decodeURI(params.username)); + this.username = decodeURI(params.username); } async postInit() { this.profile = await client.profiles.getProfile(this.username); + if (this.profile === null) return 404; - this.userId = this.profile.id; + this.user_id = this.profile.id; this.info = document.getElementById("info"); // Username let username = document.createElement("a"); username.id = "username"; - username.appendChild(document.createTextNode(this.profile.username)); + username.appendChild(document.createTextNode(this.username)); this.info.appendChild(username); this.info.appendChild(document.createElement("br")); @@ -31,30 +32,97 @@ export default class extends AbstractView { this.info.appendChild(avatar); await this.blockButton(); + + await this.friendButton(); + + client.notice.rewrite_profile = async () => { + let result = await this.profile.getFriend(); + await this.profile.getBlock() + await this.friendButton(); + } } async blockButton() { // Block option - if (await client.isAuthentificate() === false) + if (await client.isAuthenticated() === false) return; - if (client.me.id != this.userId) { - let block = document.getElementById("block") || document.createElement("a"); + if (client.me.id != this.user_id) { + let block = document.getElementById("block"); + if (block == undefined) { + block = document.createElement("p"); + this.info.appendChild(block); + } + block.id = "block"; - block.innerText = ""; block.onclick = async () => { if (!this.profile.isBlocked) - await client.profiles.block(this.userId); + await client.profiles.block(this.user_id); else - await client.profiles.deblock(this.userId); + await client.profiles.deblock(this.user_id); this.profile = await client.profiles.getProfile(this.username); + this.blockButton(); }; if (this.profile.isBlocked) - block.appendChild(document.createTextNode("Deblock")); + block.textContent = lang.get('profileUnblock', 'Unblock'); else - block.appendChild(document.createTextNode("Block")); - this.info.appendChild(block); + block.textContent = lang.get('profileBlock', 'Block'); + } + } + + async friendButton() { + if (await client.isAuthenticated() === false) + return; + + if (client.me.id != this.user_id) { + let yes = document.getElementById("yes") || document.createElement("p"); + let no = document.getElementById("no") || document.createElement("p"); + let friend = document.getElementById("friend") || document.createElement("p"); + + if (client.notice.data["asker"].includes(this.user_id)) { + + if (friend) + friend.remove(); + + yes.id = "yes"; + yes.textContent = lang.get('profileAcceptRequest', 'Accept Friend'); + yes.onclick = async () => { + client.notice.accept_friend(this.user_id); + } + + no.id = "no"; + no.textContent = lang.get('profileDenyRequest', 'Decline Friend'); + no.onclick = async () => { + client.notice.refuse_friend(this.user_id); + } + + this.info.appendChild(yes); + this.info.appendChild(document.createTextNode(" ")); + this.info.appendChild(no); + + } + else { + + if (yes && no) + yes.remove(); no.remove(); + + friend.id = "friend" + friend.onclick = async () => { + if (this.profile.isFriend) + await client.notice.remove_friend(this.user_id); + else + await client.notice.ask_friend(this.user_id); + await client.profiles.getProfile(this.username); + this.friendButton(); + }; + if (this.profile.isFriend) + friend.textContent = lang.get('profileRemoveFriend', 'Remove Friend'); + else { + friend.textContent = lang.get('profileAddFriend', 'Ask Friend'); + } + this.info.appendChild(friend); + } } } diff --git a/frontend/static/js/views/Search.js b/frontend/static/js/views/Search.js index 4313530..9911f1a 100644 --- a/frontend/static/js/views/Search.js +++ b/frontend/static/js/views/Search.js @@ -1,27 +1,27 @@ import AbstractView from "./abstracts/AbstractView.js"; -import {client} from "../index.js"; +import { client, lang } from "../index.js"; import {Message} from "../api/chat/message.js" export default class extends AbstractView { constructor(params) { - super(params, "Search"); + super(params, 'SearchWindowTitle'); } async wait_get_online_users() { return new Promise((resolve) => { const checkInterval = setInterval(() => { - //console.log(client.notice.data["online"].length); - if (client.notice.data["online"].length > 0) { + console.log(client.notice.data["online"]); + if (Object.keys(client.notice.data["online"]).length > 0) { clearInterval(checkInterval); resolve(); } - }, 100); + }, 1); }); } async postInit() { - let logged = await client.isAuthentificate(); + let logged = await client.isAuthenticated(); let profiles = await client.profiles.all(); //console.log(client.notice.data); @@ -29,11 +29,13 @@ export default class extends AbstractView { return console.log("Error"); //await client.notice.getOnlineUser(); - await this.wait_get_online_users(); + //await this.wait_get_online_users(); client.notice.rewrite_usernames = this.rewrite_usernames; + client.notice.rewrite_invite = this.display_invite; let search = document.getElementById("input_user"); - search.oninput = () => this.display_users(logged, profiles); + if (search != undefined) + search.oninput = () => this.display_users(logged, profiles); let chat_input = document.getElementById("input_chat"); //chat_input.addEventListener("keydown", this.display_chat_manager) @@ -66,7 +68,12 @@ export default class extends AbstractView { username.setAttribute('data-link', ''); username.id = `username${user.id}` username.href = `/profiles/${user.username}`; - username.style.color = client.notice.data["online"].includes(user.id.toString()) ? "green" : "red"; + if (logged && user.id == client.me.id) + username.style.color = "green"; + else { + let online = client.notice.data["online"][user.id]; + username.style.color = online !== undefined ? online : "gray"; + } username.appendChild(document.createTextNode(user.username)); new_user.appendChild(username); @@ -135,8 +142,14 @@ export default class extends AbstractView { profiles.filter(user => user.username.toLowerCase().startsWith(search) == true).forEach((user) => { let username = document.getElementById(`username${user.id}`); - if (username !== null) - username.style.color = client.notice.data["online"].includes(user.id.toString()) ? "green" : "red"; + if (username !== null) { + if (user.id == client.me.id) + username.style.color = "green"; + else { + let online = client.notice.data["online"][user.id]; + username.style.color = online !== undefined ? online : "gray"; + } + } }); } @@ -175,13 +188,15 @@ export default class extends AbstractView { chat_input.maxLength=255; chat.appendChild(chat_input); + let members_id = client.channels.channel.members_id; + chat_input.onkeydown = async () => { if (event.keyCode == 13 && client.channels.channel != undefined) { //let chat_input = document.getElementById("input_chat"); let chat_text = chat_input.value; let receivers_id = []; - client.channels.channel.members_id.forEach((member_id) => { + members_id.forEach((member_id) => { if (member_id != client.me.id) receivers_id.push(profiles.filter(user => user.id == member_id)[0].id); }); @@ -196,7 +211,7 @@ export default class extends AbstractView { // Scroll to the bottom of messages messages.scrollTop = messages.scrollHeight; - this.display_invite(chat); + this.display_invite(); } @@ -242,33 +257,86 @@ export default class extends AbstractView { } async display_members(chat, profiles) { + + let members_id = client.channels.channel.members_id; + let members = document.createElement("h2"); members.id = "members"; let usernames = ""; - client.channels.channel.members_id.forEach((member_id) => { + members_id.forEach((member_id) => { if (member_id != client.me.id) { if (usernames.length > 0) usernames += ", "; usernames += (profiles.filter(user => user.id == member_id)[0].username); } }); - members.appendChild(document.createTextNode(usernames)); + members.textContent = usernames; chat.appendChild(members); + return members } - async display_invite(chat, profiles) { + async display_invite() { + + let chat = document.getElementById("chat"); + + if (chat == undefined) + return ; + + let members_id = client.channels.channel.members_id; + let others = members_id.filter(id => id !== client.me.id); - // Button to send invite to play let invite = document.getElementById("invite") || document.createElement("button"); - invite.id = "invite"; - invite.innerText = "invite"; - invite.onclick = async () => { - await client.notice.sendInvite(client.me.id, - client.channels.channel.members_id.filter(id => id !== client.me.id)); - }; - chat.appendChild(invite); + let yes = document.getElementById("yes") || document.createElement("button"); + let no = document.getElementById("no") || document.createElement("button"); + + let invitedBy = undefined; + for (let x in others) { + if (client.notice.data["invited"].includes(others[x])) { + invitedBy = others[x]; + } + } + + if (invitedBy == undefined) { + + if (yes && no) { + yes.remove(); + no.remove(); + } + + // Button to send invite to play + invite.id = "invite"; + invite.style.background = "orange"; + invite.innerText = "invite"; + invite.title = "Invite to play a game" + invite.onclick = async () => { + await client.notice.send_invite(others); + }; + chat.appendChild(invite); + } + else { + + if (invite) + invite.remove() + + yes.id = "yes"; + yes.style.background = "green"; + yes.title = "Accept to play a game" + yes.onclick = async () => { + await client.notice.accept_invite(invitedBy); + }; + + no.id = "no"; + no.style.background = "red"; + no.title = "Refuse to play a game" + no.onclick = async () => { + await client.notice.refuse_invite(invitedBy); + }; + + chat.appendChild(yes); + chat.appendChild(no); + } } diff --git a/frontend/static/js/views/MeView.js b/frontend/static/js/views/SettingsView.js similarity index 83% rename from frontend/static/js/views/MeView.js rename to frontend/static/js/views/SettingsView.js index 357c072..5f2ceff 100644 --- a/frontend/static/js/views/MeView.js +++ b/frontend/static/js/views/SettingsView.js @@ -1,12 +1,13 @@ import { client, navigateTo } from "../index.js"; import { clear, fill_errors } from "../utils/formUtils.js"; -import AbstractAuthentificateView from "./abstracts/AbstractAuthentifiedView.js"; +import AbstractAuthenticatedView from "./abstracts/AbstractAuthenticatedView.js"; -export default class extends AbstractAuthentificateView +export default class extends AbstractAuthenticatedView { constructor(params) { - super(params, "Me"); + super(params, "Settings"); + this.PROFILE_PICTURE_MAX_SIZE = 2 * 1024 * 1024; // 2MB } async postInit() @@ -24,7 +25,7 @@ export default class extends AbstractAuthentificateView document.getElementById("avatar").remove(); let avatar = document.createElement("img"); avatar.id = "avatar"; - avatar.src = profile.avatar_url; + avatar.src = profile.avatar_url + '?t=' +new Date().getTime(); document.getElementsByClassName("avatar")[0].appendChild(avatar); } } @@ -35,7 +36,7 @@ export default class extends AbstractAuthentificateView let response_data = await client.account.delete(current_password); - console.log(await client.isAuthentificate()) + console.log(await client.isAuthenticated()) if (response_data === null || response_data === "user deleted") { navigateTo("/login"); @@ -78,18 +79,24 @@ export default class extends AbstractAuthentificateView if (avatar.files[0] !== undefined) { + if (avatar.files[0].size > this.PROFILE_PICTURE_MAX_SIZE) { + document.getElementById("save-profile").classList.add('text-danger'); + document.getElementById("save-profile").innerHTML = "Image too large :/"; + return; + } let form_data = new FormData(); - form_data.append("file", avatar.files[0]); + form_data.append("avatar", avatar.files[0]); await client.me.change_avatar(form_data); this.display_avatar(); } + document.getElementById("save-profile").classList.remove('text-danger'); document.getElementById("save-profile").innerHTML = "Saved"; } async getHtml() { return /* HTML */ ` - +

ME

diff --git a/frontend/static/js/views/abstracts/AbstractAuthentifiedView.js b/frontend/static/js/views/abstracts/AbstractAuthenticatedView.js similarity index 67% rename from frontend/static/js/views/abstracts/AbstractAuthentifiedView.js rename to frontend/static/js/views/abstracts/AbstractAuthenticatedView.js index eff0e36..d5b52df 100644 --- a/frontend/static/js/views/abstracts/AbstractAuthentifiedView.js +++ b/frontend/static/js/views/abstracts/AbstractAuthenticatedView.js @@ -2,13 +2,13 @@ import { client, navigateTo } from "../../index.js"; import AbstractRedirectView from "./AbstractRedirectView.js"; export default class extends AbstractRedirectView{ - constructor(params, title) { - super(params, title, "/login"); + constructor(params, titleKey, uri = "/login") { + super(params, titleKey, uri); } async redirect() { - if (await client.isAuthentificate() === false) + if (await client.isAuthenticated() === false) { navigateTo(this.redirect_url); return 1; diff --git a/frontend/static/js/views/abstracts/AbstractNonAuthentified.js b/frontend/static/js/views/abstracts/AbstractNonAuthenticatedView.js similarity index 67% rename from frontend/static/js/views/abstracts/AbstractNonAuthentified.js rename to frontend/static/js/views/abstracts/AbstractNonAuthenticatedView.js index cf28f45..07185ae 100644 --- a/frontend/static/js/views/abstracts/AbstractNonAuthentified.js +++ b/frontend/static/js/views/abstracts/AbstractNonAuthenticatedView.js @@ -2,13 +2,13 @@ import { client, navigateTo } from "../../index.js"; import AbstractRedirectView from "./AbstractRedirectView.js"; export default class extends AbstractRedirectView{ - constructor(params, title, url) { - super(params, title, url); + constructor(params, titleKey, uri = "/home") { + super(params, titleKey, uri); } async redirect() { - if (await client.isAuthentificate() === false) + if (await client.isAuthenticated() === false) return 0; navigateTo(this.redirect_url); return 1; diff --git a/frontend/static/js/views/abstracts/AbstractRedirectView.js b/frontend/static/js/views/abstracts/AbstractRedirectView.js index 81f1936..4d84134 100644 --- a/frontend/static/js/views/abstracts/AbstractRedirectView.js +++ b/frontend/static/js/views/abstracts/AbstractRedirectView.js @@ -2,14 +2,14 @@ import { navigateTo } from "../../index.js"; import AbstractView from "./AbstractView.js"; export default class extends AbstractView{ - constructor(params, title, url) + constructor(params, titleKey, uri) { - super(params, title); - this.redirect_url = url; + super(params, titleKey); + this.redirect_url = uri; } async redirect() { - navigateTo(url); + navigateTo(this.redirect_url); } } diff --git a/frontend/static/js/views/abstracts/AbstractView.js b/frontend/static/js/views/abstracts/AbstractView.js index a488d80..4868d62 100644 --- a/frontend/static/js/views/abstracts/AbstractView.js +++ b/frontend/static/js/views/abstracts/AbstractView.js @@ -1,7 +1,9 @@ +import {lang} from '../../index.js' + export default class { - constructor(params, title) { + constructor(params, titleKey) { this.params = params; - this.title = title; + this.titleKey = titleKey; } async postInit() { @@ -11,7 +13,7 @@ export default class { } setTitle() { - document.title = this.title; + document.title = lang.get(this.titleKey, 'Bozo Pong'); } async getHtml() { diff --git a/frontend/static/js/views/accounts/AuthenticationView.js b/frontend/static/js/views/accounts/AuthenticationView.js new file mode 100644 index 0000000..5e457b4 --- /dev/null +++ b/frontend/static/js/views/accounts/AuthenticationView.js @@ -0,0 +1,177 @@ +import { client, lang, navigateTo } from "../../index.js"; +import { clear, fill_errors } from "../../utils/formUtils.js"; +import AbstractNonAuthenticatedView from "../abstracts/AbstractNonAuthenticatedView.js"; + +export default class extends AbstractNonAuthenticatedView +{ + constructor(params, lastUrlBeforeLogin = '/home') + { + super(params, 'loginWindowTitle', lastUrlBeforeLogin); + this.redirect_url = lastUrlBeforeLogin; + this.current_mode = undefined + } + + async leavePage() + { + this.current_mode = undefined; + } + + /** + * @returns {Promise} + */ + async postInit() + { + let element = document.getElementById("toggle-register-login"); + + element.onclick = this.toggle_register_login.bind(this); + + let new_mode = location.pathname.slice(1); + this.update_mode(new_mode); + + document.getElementById("button").onclick = this.authentication.bind(this); + + let username_input = document.getElementById('username-input'), + password_input = document.getElementById('password-input'); + + [username_input, password_input].forEach(input => { + input.addEventListener('keydown', async ev => { + if (ev.key === 'Enter') + await this.authentication.bind(this)() + }); + }); + username_input.focus(); + } + + /** + * Check if field is normal + * @param username {String} + * @param password {String} + * @returns {Boolean} + */ + basic_verif(username, password) + { + if (username === '') + document.getElementById('username').innerHTML = lang.get('errorEmptyField', 'This field may not be blank.'); + + if (password === '') + document.getElementById('password').innerHTML = lang.get('errorEmptyField', 'This field may not be blank.'); + + if (username === '' || password === '') + return false; + + return true + } + + /** + * @returns { undefined } + */ + toggle_register_login(event) + { + event.preventDefault(); + + let new_mode = this.current_mode === "register" ? "login" : "register"; + + this.update_mode(new_mode); + } + + /** + * @param {String} new_mode + */ + update_mode(new_mode) + { + if (new_mode === this.current_mode) + return; + + this.current_mode = new_mode; + + let title = document.getElementById("title"), + username_label = document.getElementById("username-label"), + password_label = document.getElementById("password-label"), + toggle_register_login = document.getElementById("toggle-register-login"), + toggle_register_login_label = document.getElementById("toggle-register-login-label"), + button = document.getElementById("button") + ; + + let title_text = this.current_mode === "register" ? "registerFormTitle" : "loginFormTitle"; + title.innerText = lang.get(title_text, "ERROR LANG"); + + let username_label_text = this.current_mode === "register" ? "registerFormUsername" : "loginFormUsername"; + username_label.innerText = lang.get(username_label_text, "ERROR LANG"); + + let password_label_text = this.current_mode === "register" ? "registerFormPassword" : "loginFormPassword"; + password_label.innerText = lang.get(password_label_text, "ERROR LANG"); + + let toggle_register_login_label_text = this.current_mode === "register" ? "registerAlreadyAccount" : "loginNoAccount"; + toggle_register_login_label.innerText = lang.get(toggle_register_login_label_text, "ERROR LANG");; + + let toggle_register_login_text = this.current_mode === "register" ? "registerLogin" : "loginRegister"; + toggle_register_login.innerText = lang.get(toggle_register_login_text, "ERROR LANG"); + + let button_text = this.current_mode === "register" ? "registerFormButton" : "loginFormButton"; + button.innerText = lang.get(button_text, "ERROR LANG"); + + this.titleKey = this.current_mode === 'register' ? 'registerWindowTitle' : 'loginWindowTitle'; + this.setTitle(); + } + + /** + * @returns {Promise} + */ + async authentication() + { + let username = document.getElementById("username-input").value, + password = document.getElementById("password-input").value; + + if (!this.basic_verif()) + return; + + let response; + + if (this.current_mode === "register") + response = await client.account.create(username, password) + else + response = await client.login(username, password); + + if (response.status === 200 || response.status === 201) + { + navigateTo(this.redirect_url); + return; + } + + let response_data = await response.json() + + console.log(response_data); + + clear("innerHTML", ["username", "password", 'login']); + fill_errors(response_data, "innerHTML"); + } + + async getHtml() + { + return /* HTML */ ` +
+
+

Loading...

+
+ + + +
+
+ + + +
+
+ + +
+

Loading...

+ Loading... +
+
+
+
+ `; + } +} diff --git a/frontend/static/js/views/accounts/LoginView.js b/frontend/static/js/views/accounts/LoginView.js deleted file mode 100644 index 6ecc3e1..0000000 --- a/frontend/static/js/views/accounts/LoginView.js +++ /dev/null @@ -1,79 +0,0 @@ -import { client, navigateTo } from "../../index.js"; -import { clear, fill_errors } from "../../utils/formUtils.js"; -import AbstractNonAuthentifiedView from "../abstracts/AbstractNonAuthentified.js"; - -async function login(redirectTo = '/home') -{ - clear('innerHTML', ['username', 'password', 'login']); - - let username = document.getElementById('usernameInput').value; - let password = document.getElementById('passwordInput').value; - - if (username === '') { - document.getElementById('username').innerHTML = 'This field may not be blank.'; - } - if (password === '') { - document.getElementById('password').innerHTML = 'This field may not be blank.'; - } - if (username === '' || password === '') - return; - - let response = await client.login(username, password); - - if (response.status == 200) { - await client.notice.disconnect(); - await client.notice.connect(); - navigateTo(redirectTo); - } else { - let error = await response.json(); - fill_errors(error, "innerHTML"); - } -} - -export default class extends AbstractNonAuthentifiedView { - constructor(params, lastUrlBeforeLogin = '/home') { - super(params, "Login", lastUrlBeforeLogin); - this.redirectTo = lastUrlBeforeLogin; - } - - async postInit() - { - let usernameField = document.getElementById('usernameInput'); - usernameField.addEventListener('keydown', ev => { - if (ev.key === 'Enter') - login(this.redirectTo); - }); - usernameField.focus(); - let passwordField = document.getElementById('passwordInput'); - passwordField.addEventListener('keydown', ev => { - if (ev.key === 'Enter') - login(this.redirectTo); - }); - document.getElementById('loginButton').onclick = _ => login(this.redirectTo); - } - - async getHtml() { - return ` -
-
-

Login

-
- - - -
-
- - - -
-
- - -

No account yet? Register

-
-
-
- `; - } -} diff --git a/frontend/static/js/views/accounts/LogoutView.js b/frontend/static/js/views/accounts/LogoutView.js index e60ffce..18360a3 100644 --- a/frontend/static/js/views/accounts/LogoutView.js +++ b/frontend/static/js/views/accounts/LogoutView.js @@ -1,10 +1,10 @@ import { client, navigateTo } from "../../index.js"; -import AbstractAuthentifiedView from "../abstracts/AbstractAuthentifiedView.js"; +import AbstractAuthenticatedView from "../abstracts/AbstractAuthenticatedView.js"; -export default class extends AbstractAuthentifiedView +export default class extends AbstractAuthenticatedView { constructor(params, lastPageUrl = '/login') { - super(params, "Logout"); + super(params, 'logoutWindowTitle', lastPageUrl); this.lastPageUrl = lastPageUrl; } diff --git a/frontend/static/js/views/accounts/RegisterView.js b/frontend/static/js/views/accounts/RegisterView.js deleted file mode 100644 index 9696feb..0000000 --- a/frontend/static/js/views/accounts/RegisterView.js +++ /dev/null @@ -1,77 +0,0 @@ -import { client, navigateTo } from "../../index.js"; -import { clear, fill_errors } from "../../utils/formUtils.js"; -import AbstractNonAuthentifiedView from "../abstracts/AbstractNonAuthentified.js"; - -async function register(redirectTo = '/home') -{ - let username = document.getElementById("usernameInput").value; - let password = document.getElementById("passwordInput").value; - - if (username === '' || password === '') { - clear("innerHTML", ["username", "password"]); - if (username === '') - document.getElementById('username').innerHTML = 'This field may not be blank.'; - if (password === '') - document.getElementById('password').innerHTML = 'This field may not be blank.'; - return; - } - - let response_data = await client.account.create(username, password); - - if (response_data == null) - { - navigateTo(redirectTo); - return; - } - - clear("innerHTML", ["username", "password", 'register']); - fill_errors(response_data, "innerHTML"); -} - -export default class extends AbstractNonAuthentifiedView { - constructor(params, lastUrlBeforeLogin = '/home') { - super(params, "Register", lastUrlBeforeLogin); - this.redirectTo = lastUrlBeforeLogin; - } - - async postInit() - { - let usernameField = document.getElementById('usernameInput'); - usernameField.addEventListener('keydown', ev => { - if (ev.key === 'Enter') - register(this.redirectTo); - }); - usernameField.focus(); - let passwordField = document.getElementById('passwordInput'); - passwordField.addEventListener('keydown', ev => { - if (ev.key === 'Enter') - register(this.redirectTo); - }); - document.getElementById("registerButton").onclick = _ => register(this.redirectTo); - } - - async getHtml() { - return ` -
-
-

Register

-
- - - -
-
- - - -
-
- - -

Already have an account? Login

-
-
-
- `; - } -} diff --git a/frontend/static/js/views/tournament/TournamentCreateView.js b/frontend/static/js/views/tournament/TournamentCreateView.js index 9cfc1a4..8983071 100644 --- a/frontend/static/js/views/tournament/TournamentCreateView.js +++ b/frontend/static/js/views/tournament/TournamentCreateView.js @@ -1,8 +1,8 @@ import {client, navigateTo} from "../../index.js"; import { clear, fill_errors } from "../../utils/formUtils.js"; -import AbstractAuthentifiedView from "../abstracts/AbstractAuthentifiedView.js"; +import AbstractAuthenticatedView from "../abstracts/AbstractAuthenticatedView.js"; -export default class extends AbstractAuthentifiedView +export default class extends AbstractAuthenticatedView { constructor(params) { diff --git a/frontend/static/js/views/tournament/TournamentPageView.js b/frontend/static/js/views/tournament/TournamentPageView.js index 034d27b..2d66bfa 100644 --- a/frontend/static/js/views/tournament/TournamentPageView.js +++ b/frontend/static/js/views/tournament/TournamentPageView.js @@ -1,7 +1,7 @@ import {client, navigateTo} from "../../index.js"; -import AbstractAuthentifiedView from "../abstracts/AbstractAuthentifiedView.js"; +import AbstractAuthenticatedView from "../abstracts/AbstractAuthenticatedView.js"; -export default class extends AbstractAuthentifiedView +export default class extends AbstractAuthenticatedView { constructor(params) { diff --git a/frontend/static/js/views/tournament/TournamentsListView.js b/frontend/static/js/views/tournament/TournamentsListView.js index d232698..eae0451 100644 --- a/frontend/static/js/views/tournament/TournamentsListView.js +++ b/frontend/static/js/views/tournament/TournamentsListView.js @@ -1,7 +1,7 @@ import {client} from "../../index.js"; -import AbstractAuthentifiedView from "../abstracts/AbstractAuthentifiedView.js"; +import AbstractAuthenticatedView from "../abstracts/AbstractAuthenticatedView.js"; -export default class extends AbstractAuthentifiedView +export default class extends AbstractAuthenticatedView { constructor(params) { diff --git a/frontend/templates/index.html b/frontend/templates/index.html index 499c006..3d27444 100644 --- a/frontend/templates/index.html +++ b/frontend/templates/index.html @@ -6,6 +6,7 @@ Bozo Pong +