dockered
This commit is contained in:
7
django/.gitignore
vendored
Normal file
7
django/.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
.env
|
||||
*.pyc
|
||||
db.sqlite3
|
||||
**/migrations/**
|
||||
/profiles/static/avatars/*
|
||||
!/profiles/static/avatars/default.avif
|
||||
*.mo
|
3
django/.jshintrc
Normal file
3
django/.jshintrc
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"esversion": 11
|
||||
}
|
13
django/Dockerfile
Normal file
13
django/Dockerfile
Normal file
@ -0,0 +1,13 @@
|
||||
FROM python:slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN apt-get update && apt-get -y install gettext
|
||||
|
||||
COPY requirements.txt .
|
||||
RUN pip install -r requirements.txt
|
||||
|
||||
COPY . .
|
||||
|
||||
ENTRYPOINT ["sh", "docker-entrypoint.sh"]
|
||||
CMD ["0.0.0.0:8000"]
|
0
django/accounts/__init__.py
Normal file
0
django/accounts/__init__.py
Normal file
3
django/accounts/admin.py
Normal file
3
django/accounts/admin.py
Normal file
@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
6
django/accounts/apps.py
Normal file
6
django/accounts/apps.py
Normal file
@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AccountsConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'accounts'
|
39
django/accounts/locale/fr/LC_MESSAGES/django.po
Normal file
39
django/accounts/locale/fr/LC_MESSAGES/django.po
Normal file
@ -0,0 +1,39 @@
|
||||
# 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 <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2024-03-20 10:25+0100\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\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"
|
||||
|
||||
#: serializers/update_password.py:19
|
||||
msgid "Current password is incorrect."
|
||||
msgstr "Mot de passe actuel incorrect."
|
||||
|
||||
#: serializers/update_password.py:24
|
||||
msgid "The password does not match."
|
||||
msgstr "Le mot de passe ne correspond pas."
|
||||
|
||||
#: serializers/update_password.py:31 serializers/update_user.py:15
|
||||
msgid "You dont have permission for this user."
|
||||
msgstr "Vous n'avez pas de permissions pour cet utilisateur."
|
||||
|
||||
#: views/delete.py:19
|
||||
msgid "Password incorrect."
|
||||
msgstr "Mot de passe incorrect."
|
||||
|
||||
#: views/login.py:23
|
||||
msgid "Invalid username or password."
|
||||
msgstr "Nom d'utilisateur ou mot de passe incorect."
|
12
django/accounts/serializers/login.py
Normal file
12
django/accounts/serializers/login.py
Normal file
@ -0,0 +1,12 @@
|
||||
from rest_framework.serializers import Serializer, CharField
|
||||
from django.contrib.auth import authenticate
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
class LoginSerializer(Serializer):
|
||||
|
||||
username = CharField()
|
||||
password = CharField()
|
||||
|
||||
def get_user(self, data):
|
||||
user = authenticate(username=data['username'], password=data['password'])
|
||||
return user
|
12
django/accounts/serializers/register.py
Normal file
12
django/accounts/serializers/register.py
Normal file
@ -0,0 +1,12 @@
|
||||
from rest_framework.serializers import ModelSerializer
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
class RegisterSerialiser(ModelSerializer):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ['username', 'password']
|
||||
|
||||
def create(self, data):
|
||||
user_obj = User.objects.create_user(username=data['username'], password=data['password'])
|
||||
user_obj.save()
|
||||
return user_obj
|
37
django/accounts/serializers/update_password.py
Normal file
37
django/accounts/serializers/update_password.py
Normal file
@ -0,0 +1,37 @@
|
||||
from rest_framework.serializers import ModelSerializer, ValidationError
|
||||
from rest_framework.fields import CharField
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.auth import login
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
|
||||
class UpdatePasswordSerializer(ModelSerializer):
|
||||
current_password = CharField(write_only=True, required=True)
|
||||
new_password = CharField(write_only=True, required=True)
|
||||
new_password2 = CharField(write_only=True, required=True)
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ['current_password', 'new_password', 'new_password2']
|
||||
|
||||
def validate_current_password(self, value):
|
||||
if not self.instance.check_password(value):
|
||||
raise ValidationError(_('Current password is incorrect.'))
|
||||
return value
|
||||
|
||||
def validate(self, data):
|
||||
if data['new_password'] != data['new_password2']:
|
||||
raise ValidationError({'new_password2': _('The password does not match.')})
|
||||
return data
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
user = self.context['request'].user
|
||||
|
||||
if user.pk != instance.pk:
|
||||
raise ValidationError({'authorize': _('You dont have permission for this user.')})
|
||||
|
||||
instance.set_password(validated_data['new_password'])
|
||||
|
||||
instance.save()
|
||||
login(self.context['request'], instance)
|
||||
return instance
|
20
django/accounts/serializers/update_user.py
Normal file
20
django/accounts/serializers/update_user.py
Normal file
@ -0,0 +1,20 @@
|
||||
from rest_framework.serializers import ModelSerializer, ValidationError
|
||||
from django.contrib.auth.models import User
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
|
||||
class UpdateUserSerializer(ModelSerializer):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ['username']
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
user = self.context['request'].user
|
||||
|
||||
if user.pk != instance.pk:
|
||||
raise ValidationError({'authorize': _('You dont have permission for this user.')})
|
||||
|
||||
instance.username = validated_data.get('username', instance.username)
|
||||
|
||||
instance.save()
|
||||
return instance
|
5
django/accounts/tests/__init__.py
Normal file
5
django/accounts/tests/__init__.py
Normal file
@ -0,0 +1,5 @@
|
||||
from .register import *
|
||||
from .login import *
|
||||
from .logout import *
|
||||
from .edit import *
|
||||
from .delete import *
|
37
django/accounts/tests/delete.py
Normal file
37
django/accounts/tests/delete.py
Normal file
@ -0,0 +1,37 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
from django.test.client import Client
|
||||
from django.http import HttpResponse
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
import uuid
|
||||
|
||||
class DeleteTest(TestCase):
|
||||
def setUp(self):
|
||||
self.client = Client()
|
||||
|
||||
self.url = "/api/accounts/delete"
|
||||
|
||||
self.username: str = str(uuid.uuid4())
|
||||
self.password: str = str(uuid.uuid4())
|
||||
|
||||
user: User = User.objects.create_user(username=self.username, password=self.password)
|
||||
self.client.login(username=self.username, password=self.password)
|
||||
|
||||
|
||||
def test_normal_delete(self):
|
||||
response: HttpResponse = self.client.delete(self.url, {"password": self.password}, content_type='application/json')
|
||||
response_text: str = response.content.decode("utf-8")
|
||||
self.assertEqual(response_text, '"user deleted"')
|
||||
|
||||
def test_wrong_pass(self):
|
||||
response: HttpResponse = self.client.delete(self.url, {"password": "cacaman a frapper"}, content_type='application/json')
|
||||
errors: dict = eval(response.content)
|
||||
self.assertDictEqual(errors, {"password": ["Password wrong."]})
|
||||
|
||||
def test_no_logged(self):
|
||||
self.client.logout()
|
||||
response: HttpResponse = self.client.delete(self.url, {"password": self.password}, content_type='application/json')
|
||||
errors: dict = eval(response.content)
|
||||
self.assertDictEqual(errors, {"detail":"Authentication credentials were not provided."})
|
49
django/accounts/tests/edit.py
Normal file
49
django/accounts/tests/edit.py
Normal file
@ -0,0 +1,49 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
from django.test.client import Client
|
||||
from django.http import HttpResponse
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
import uuid
|
||||
|
||||
class EditTest(TestCase):
|
||||
def setUp(self):
|
||||
self.client = Client()
|
||||
|
||||
self.url = "/api/accounts/edit"
|
||||
|
||||
self.username: str = str(uuid.uuid4())
|
||||
self.password: str = str(uuid.uuid4())
|
||||
self.new_password: str = str(uuid.uuid4())
|
||||
|
||||
User.objects.create_user(username = self.username, password = self.password)
|
||||
|
||||
def test_normal(self):
|
||||
self.client.login(username = self.username, password = self.password)
|
||||
response: HttpResponse = self.client.patch(self.url, {"current_password": self.password, "new_password": self.new_password, "username": "bozo"}, content_type='application/json')
|
||||
response_text: str = response.content.decode('utf-8')
|
||||
self.assertEqual(response_text, '"data has been alterate"')
|
||||
|
||||
def test_invalid_current_password(self):
|
||||
self.client.login(username = self.username, password = self.password)
|
||||
response: HttpResponse = self.client.patch(self.url, {"current_password": "bozo", "new_password": self.new_password, "username": "bozo"}, content_type='application/json')
|
||||
errors: dict = eval(response.content)
|
||||
self.assertDictEqual(errors, {"current_password":["Password is wrong."]})
|
||||
|
||||
def test_invalid_new_username_blank(self):
|
||||
self.client.login(username = self.username, password = self.password)
|
||||
response: HttpResponse = self.client.patch(self.url, {"current_password": self.password, "username": " "}, content_type='application/json')
|
||||
errors: dict = eval(response.content)
|
||||
self.assertDictEqual(errors, {'username': ['This field may not be blank.']})
|
||||
|
||||
def test_invalid_new_username_char(self):
|
||||
self.client.login(username = self.username, password = self.password)
|
||||
response: HttpResponse = self.client.patch(self.url, {"current_password": self.password, "username": "*&"}, content_type='application/json')
|
||||
errors: dict = eval(response.content)
|
||||
self.assertDictEqual(errors, {'username': ['Enter a valid username. This value may contain only letters, numbers, and @/./+/-/_ characters.']})
|
||||
|
||||
def test_nologged(self):
|
||||
response: HttpResponse = self.client.patch(self.url, {"current_password": self.password, "new_password": self.new_password}, content_type='application/json')
|
||||
errors: dict = eval(response.content)
|
||||
self.assertDictEqual(errors, {'detail': 'Authentication credentials were not provided.'})
|
53
django/accounts/tests/login.py
Normal file
53
django/accounts/tests/login.py
Normal file
@ -0,0 +1,53 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
from django.test.client import Client
|
||||
from django.contrib.auth.models import User
|
||||
from django.http import HttpResponse
|
||||
import uuid
|
||||
|
||||
class LoginTest(TestCase):
|
||||
def setUp(self):
|
||||
self.client = Client()
|
||||
|
||||
self.url = "/api/accounts/login"
|
||||
|
||||
self.username: str = str(uuid.uuid4())
|
||||
self.password: str = str(uuid.uuid4())
|
||||
|
||||
User.objects.create_user(username=self.username, password=self.password)
|
||||
|
||||
def test_normal_login(self):
|
||||
response: HttpResponse = self.client.post(self.url, {'username': self.username, 'password': self.password})
|
||||
response_text = response.content.decode('utf-8')
|
||||
#self.assertEqual(response_text, 'user connected')
|
||||
|
||||
def test_invalid_username(self):
|
||||
response: HttpResponse = self.client.post(self.url, {"username": self.password, "password": self.password})
|
||||
errors: dict = eval(response.content)
|
||||
errors_expected: dict = {'user': ['Username or password wrong.']}
|
||||
self.assertEqual(errors, errors_expected)
|
||||
|
||||
def test_invalid_password(self):
|
||||
response: HttpResponse = self.client.post(self.url, {"username": self.username, "password": self.username})
|
||||
errors: dict = eval(response.content)
|
||||
errors_expected: dict = {'user': ['Username or password wrong.']}
|
||||
self.assertEqual(errors, errors_expected)
|
||||
|
||||
def test_invalid_no_username(self):
|
||||
response: HttpResponse = self.client.post(self.url, {"password": self.password})
|
||||
errors: dict = eval(response.content)
|
||||
errors_expected: dict = {'username': ['This field is required.']}
|
||||
self.assertEqual(errors, errors_expected)
|
||||
|
||||
def test_invalid_no_password(self):
|
||||
response: HttpResponse = self.client.post(self.url, {"username": self.username})
|
||||
errors: dict = eval(response.content)
|
||||
errors_expected: dict = {'password': ['This field is required.']}
|
||||
self.assertEqual(errors, errors_expected)
|
||||
|
||||
def test_invalid_no_password_no_username(self):
|
||||
response: HttpResponse = self.client.post(self.url, {})
|
||||
errors: dict = eval(response.content)
|
||||
errors_expected: dict = {'username': ['This field is required.'], 'password': ['This field is required.']}
|
||||
self.assertEqual(errors, errors_expected)
|
17
django/accounts/tests/logout.py
Normal file
17
django/accounts/tests/logout.py
Normal file
@ -0,0 +1,17 @@
|
||||
from django.test import TestCase
|
||||
|
||||
from django.test.client import Client
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.auth import login
|
||||
|
||||
class LoginTest(TestCase):
|
||||
def setUp(self):
|
||||
self.client = Client()
|
||||
|
||||
self.url = "/api/accounts/logout"
|
||||
|
||||
self.client.login()
|
||||
|
||||
def test_normal_logout(self):
|
||||
self.client.post(self.url)
|
||||
self.assertNotIn('_auth_user_id', self.client.session)
|
52
django/accounts/tests/register.py
Normal file
52
django/accounts/tests/register.py
Normal file
@ -0,0 +1,52 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
from rest_framework import status
|
||||
from django.test.client import Client
|
||||
from django.contrib.auth.models import User
|
||||
from django.http import HttpResponse
|
||||
import uuid
|
||||
|
||||
class RegisterTest(TestCase):
|
||||
def setUp(self):
|
||||
self.client = Client()
|
||||
|
||||
self.url: str = "/api/accounts/register"
|
||||
|
||||
self.username: str = str(uuid.uuid4())
|
||||
self.password: str = str(uuid.uuid4())
|
||||
|
||||
def test_normal_register(self):
|
||||
response: HttpResponse = self.client.post(self.url, {'username': self.username, 'password': self.password})
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
|
||||
def test_incomplet_form_no_username_no_password(self):
|
||||
response: HttpResponse = self.client.post(self.url)
|
||||
errors: dict = eval(response.content)
|
||||
errors_expected: dict = {'username': ['This field is required.'], 'password': ['This field is required.']}
|
||||
self.assertEqual(errors, errors_expected)
|
||||
|
||||
def test_incomplet_form_no_password(self):
|
||||
response: HttpResponse = self.client.post(self.url, {"username": self.username})
|
||||
errors: dict = eval(response.content)
|
||||
errors_expected: dict = {'password': ['This field is required.']}
|
||||
self.assertEqual(errors, errors_expected)
|
||||
|
||||
def test_incomplet_form_no_username(self):
|
||||
response: HttpResponse = self.client.post(self.url, {"password": self.password})
|
||||
errors: dict = eval(response.content)
|
||||
errors_expected: dict = {}
|
||||
self.assertEqual(errors, errors_expected)
|
||||
|
||||
def test_incomplet_form_no_username(self):
|
||||
response: HttpResponse = self.client.post(self.url, {"password": self.password})
|
||||
errors: dict = eval(response.content)
|
||||
errors_expected: dict = {'username': ['This field is required.']}
|
||||
self.assertEqual(errors, errors_expected)
|
||||
|
||||
def test_already_registered(self):
|
||||
User(username=self.username, password=self.password).save()
|
||||
response: HttpResponse = self.client.post(self.url, {'username': self.username, 'password': self.password})
|
||||
errors: dict = eval(response.content)
|
||||
errors_expected: dict = {'username': ['A user with that username already exists.']}
|
||||
self.assertEqual(errors, errors_expected)
|
13
django/accounts/urls.py
Normal file
13
django/accounts/urls.py
Normal file
@ -0,0 +1,13 @@
|
||||
from django.urls import path
|
||||
|
||||
from .views import register, login, logout, delete, logged, update_profile, update_password
|
||||
|
||||
urlpatterns = [
|
||||
path("register", register.RegisterView.as_view(), name="register"),
|
||||
path("login", login.LoginView.as_view(), name="login"),
|
||||
path("logout", logout.LogoutView.as_view(), name="logout"),
|
||||
path("logged", logged.LoggedView.as_view(), name="logged"),
|
||||
path("delete", delete.DeleteView.as_view(), name="delete"),
|
||||
path('update_profile', update_profile.UpdateProfileView.as_view(), name='update_profile'),
|
||||
path('update_password', update_password.UpdatePasswordView.as_view(), name='update_password')
|
||||
]
|
23
django/accounts/views/delete.py
Normal file
23
django/accounts/views/delete.py
Normal file
@ -0,0 +1,23 @@
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework import permissions, status
|
||||
from rest_framework.response import Response
|
||||
from django.contrib.auth import logout
|
||||
from django.http import HttpRequest
|
||||
from rest_framework.authentication import SessionAuthentication
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
|
||||
class DeleteView(APIView):
|
||||
permission_classes = (permissions.IsAuthenticated,)
|
||||
authentication_classes = (SessionAuthentication,)
|
||||
|
||||
def delete(self, request: HttpRequest):
|
||||
data: dict = request.data
|
||||
|
||||
password: str = data["password"]
|
||||
if (request.user.check_password(password) is False):
|
||||
return Response({"password": _("Password incorrect.")},
|
||||
status.HTTP_401_UNAUTHORIZED)
|
||||
request.user.delete()
|
||||
logout(request)
|
||||
return Response(status=status.HTTP_200_OK)
|
14
django/accounts/views/logged.py
Normal file
14
django/accounts/views/logged.py
Normal file
@ -0,0 +1,14 @@
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import permissions, status
|
||||
from django.http import HttpRequest
|
||||
from rest_framework.authentication import SessionAuthentication
|
||||
|
||||
|
||||
class LoggedView(APIView):
|
||||
|
||||
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)
|
25
django/accounts/views/login.py
Normal file
25
django/accounts/views/login.py
Normal file
@ -0,0 +1,25 @@
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
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,)
|
||||
|
||||
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)
|
15
django/accounts/views/logout.py
Normal file
15
django/accounts/views/logout.py
Normal file
@ -0,0 +1,15 @@
|
||||
from rest_framework.views import APIView
|
||||
from django.contrib.auth import logout
|
||||
from rest_framework import permissions, status
|
||||
from rest_framework.response import Response
|
||||
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 logged out", status.HTTP_200_OK)
|
20
django/accounts/views/register.py
Normal file
20
django/accounts/views/register.py
Normal file
@ -0,0 +1,20 @@
|
||||
from rest_framework import permissions, status
|
||||
from ..serializers.register import RegisterSerialiser
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
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)
|
13
django/accounts/views/update_password.py
Normal file
13
django/accounts/views/update_password.py
Normal file
@ -0,0 +1,13 @@
|
||||
from ..serializers.update_password import UpdatePasswordSerializer
|
||||
from rest_framework.generics import UpdateAPIView
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
|
||||
class UpdatePasswordView(UpdateAPIView):
|
||||
queryset = User.objects.all()
|
||||
permission_classes = (IsAuthenticated,)
|
||||
serializer_class = UpdatePasswordSerializer
|
||||
|
||||
def get_object(self):
|
||||
return self.queryset.get(pk=self.request.user.pk)
|
14
django/accounts/views/update_profile.py
Normal file
14
django/accounts/views/update_profile.py
Normal file
@ -0,0 +1,14 @@
|
||||
from ..serializers.update_user import UpdateUserSerializer
|
||||
from rest_framework.generics import UpdateAPIView
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
|
||||
class UpdateProfileView(UpdateAPIView):
|
||||
|
||||
queryset = User.objects.all()
|
||||
permission_classes = (IsAuthenticated,)
|
||||
serializer_class = UpdateUserSerializer
|
||||
|
||||
def get_object(self):
|
||||
return self.queryset.get(pk=self.request.user.pk)
|
0
django/chat/__init__.py
Normal file
0
django/chat/__init__.py
Normal file
6
django/chat/admin.py
Normal file
6
django/chat/admin.py
Normal file
@ -0,0 +1,6 @@
|
||||
from django.contrib import admin
|
||||
from .models import ChatChannelModel, ChatMemberModel, ChatMessageModel
|
||||
|
||||
admin.site.register(ChatChannelModel)
|
||||
admin.site.register(ChatMemberModel)
|
||||
admin.site.register(ChatMessageModel)
|
6
django/chat/apps.py
Normal file
6
django/chat/apps.py
Normal file
@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ChatConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'chat'
|
72
django/chat/consumersChat.py
Normal file
72
django/chat/consumersChat.py
Normal file
@ -0,0 +1,72 @@
|
||||
from channels.generic.websocket import WebsocketConsumer
|
||||
from asgiref.sync import async_to_sync
|
||||
from .models import ChatMemberModel, ChatMessageModel
|
||||
|
||||
import time
|
||||
import json
|
||||
|
||||
|
||||
class ChatConsumer(WebsocketConsumer):
|
||||
|
||||
def connect(self):
|
||||
|
||||
self.user = self.scope["user"]
|
||||
if not self.user.is_authenticated:
|
||||
return
|
||||
|
||||
self.channel_id: int = int(self.scope['url_route']['kwargs']['chat_id'])
|
||||
|
||||
if not ChatMemberModel.objects.filter(member_id=self.user.pk, channel_id=self.channel_id).exists():
|
||||
return
|
||||
|
||||
if self.channel_layer is None:
|
||||
return
|
||||
|
||||
self.room_group_name = f'chat{self.channel_id}'
|
||||
|
||||
async_to_sync(self.channel_layer.group_add)(
|
||||
self.room_group_name,
|
||||
self.channel_name
|
||||
)
|
||||
self.accept()
|
||||
|
||||
def receive(self, text_data=None, bytes_data=None):
|
||||
if text_data is None:
|
||||
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
|
||||
|
||||
message_time: int = int(time.time() * 1000)
|
||||
|
||||
async_to_sync(self.channel_layer.group_send)(
|
||||
self.room_group_name,
|
||||
{
|
||||
'type': 'chat_message',
|
||||
'author_id': user.pk,
|
||||
'content': message,
|
||||
'time': message_time,
|
||||
}
|
||||
)
|
||||
|
||||
ChatMessageModel(
|
||||
channel_id=self.channel_id,
|
||||
author_id=user.pk,
|
||||
content=message,
|
||||
time=message_time
|
||||
).save()
|
||||
|
||||
def chat_message(self, event):
|
||||
self.send(text_data=json.dumps({
|
||||
'type': 'chat',
|
||||
'author_id': event['author_id'],
|
||||
'content': event['content'],
|
||||
'time': event['time'],
|
||||
}))
|
41
django/chat/models.py
Normal file
41
django/chat/models.py
Normal file
@ -0,0 +1,41 @@
|
||||
from django.db.models import Model, IntegerField, ForeignKey, CharField, CASCADE
|
||||
from django.db.models.signals import post_delete
|
||||
from django.dispatch import receiver
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
|
||||
class ChatChannelModel(Model):
|
||||
def create(self, members: [User]):
|
||||
self.save()
|
||||
for member in members:
|
||||
ChatMemberModel(channel=self, member=member).save()
|
||||
return self
|
||||
|
||||
def get_members(self):
|
||||
return [member_channel.member for member_channel in ChatMemberModel.objects.filter(channel=self)]
|
||||
|
||||
|
||||
class ChatMemberModel(Model):
|
||||
member = ForeignKey(User, on_delete=CASCADE)
|
||||
channel = ForeignKey(ChatChannelModel, on_delete=CASCADE)
|
||||
|
||||
|
||||
@receiver(post_delete, sender=ChatMemberModel)
|
||||
def delete_channel_when_member_deleted(sender, instance, **kwargs):
|
||||
print(sender, instance)
|
||||
|
||||
|
||||
class ChatMessageModel(Model):
|
||||
channel = ForeignKey(ChatChannelModel, on_delete=CASCADE)
|
||||
author = ForeignKey(User, on_delete=CASCADE)
|
||||
content = CharField(max_length=255)
|
||||
time = IntegerField(primary_key=False)
|
||||
|
||||
|
||||
class AskModel(Model):
|
||||
asker_id = IntegerField(primary_key=False)
|
||||
asked_id = IntegerField(primary_key=False)
|
||||
|
||||
# return if the asker ask the asked to play a game
|
||||
def is_asked(self, asker_id, asked_id):
|
||||
return AskModel.objects.get(asker_id=asker_id, asked_id=asked_id) is not None
|
7
django/chat/routing.py
Normal file
7
django/chat/routing.py
Normal file
@ -0,0 +1,7 @@
|
||||
from django.urls import re_path
|
||||
|
||||
from . import consumersChat
|
||||
|
||||
websocket_urlpatterns = [
|
||||
re_path(r'ws/chat/(?P<chat_id>\d+)$', consumersChat.ChatConsumer.as_asgi()),
|
||||
]
|
10
django/chat/serializers/ask.py
Normal file
10
django/chat/serializers/ask.py
Normal file
@ -0,0 +1,10 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from profiles.models import ProfileModel
|
||||
from ..models import ChatChannelModel, ChatMessageModel
|
||||
|
||||
class AskSerializer(serializers.ModelSerializer):
|
||||
|
||||
members_id = serializers.ListField(child=serializers.IntegerField())
|
40
django/chat/serializers/chat.py
Normal file
40
django/chat/serializers/chat.py
Normal file
@ -0,0 +1,40 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from django.utils.translation import gettext as _
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
from ..models import ChatChannelModel, ChatMessageModel
|
||||
|
||||
|
||||
class ChatChannelSerializer(serializers.ModelSerializer):
|
||||
|
||||
members_id = serializers.ListField(child=serializers.IntegerField(), required=True, write_only=True)
|
||||
messages = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = ChatChannelModel
|
||||
fields = ["members_id", "id", 'messages']
|
||||
|
||||
def validate_members_id(self, value):
|
||||
members_id: [int] = value
|
||||
if len(members_id) < 2:
|
||||
raise serializers.ValidationError(_('There is not enough members to create the channel.'))
|
||||
if len(set(members_id)) != len(members_id):
|
||||
raise serializers.ValidationError(_('Duplicate in members list.'))
|
||||
if self.context.get('user').pk not in members_id:
|
||||
raise serializers.ValidationError(_('You are trying to create a group chat without you.'))
|
||||
for member_id in members_id:
|
||||
if not User.objects.filter(pk=member_id).exists():
|
||||
raise serializers.ValidationError(_(f"The profile {member_id} doesn't exist."))
|
||||
return members_id
|
||||
|
||||
def get_messages(self, obj: ChatChannelModel):
|
||||
messages = ChatMessageModel.objects.filter(channel=obj).order_by('time')
|
||||
return ChatMessageSerializer(messages, many=True).data
|
||||
|
||||
|
||||
class ChatMessageSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = ChatMessageModel
|
||||
fields = ["channel", "author", "content", "time"]
|
30
django/chat/tests.py
Normal file
30
django/chat/tests.py
Normal file
@ -0,0 +1,30 @@
|
||||
from django.test import TestCase
|
||||
|
||||
from django.test.client import Client
|
||||
from django.http import HttpResponse, HttpRequest
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
# Create your tests here.
|
||||
class ChatTest(TestCase):
|
||||
def setUp(self):
|
||||
self.client = Client()
|
||||
|
||||
self.username='bozo1'
|
||||
self.password='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.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_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.'})
|
10
django/chat/urls.py
Normal file
10
django/chat/urls.py
Normal file
@ -0,0 +1,10 @@
|
||||
from django.urls import path
|
||||
|
||||
from .views import chat
|
||||
from .views import ask
|
||||
|
||||
urlpatterns = [
|
||||
path("", chat.ChannelView.as_view(), name="chats_page"),
|
||||
path("ask/", ask.AskView.as_view(), name="chats_ask"),
|
||||
path("ask/accept", ask.AskAcceptView.as_view(), name="chats_ask_accept"),
|
||||
]
|
74
django/chat/views/ask.py
Normal file
74
django/chat/views/ask.py
Normal file
@ -0,0 +1,74 @@
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import permissions, status
|
||||
from rest_framework.authentication import SessionAuthentication
|
||||
|
||||
from chat.models import AskModel
|
||||
|
||||
from ..serializers.ask import AskSerializer
|
||||
|
||||
from notice.consumers import notice_manager
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
|
||||
class AskView(APIView):
|
||||
|
||||
serializer_class = AskSerializer
|
||||
permission_classes = (permissions.IsAuthenticated,)
|
||||
authentication_classes = (SessionAuthentication,)
|
||||
|
||||
def post(self, request):
|
||||
data: dict = request.data
|
||||
if (data["asked"] is None):
|
||||
return
|
||||
|
||||
asker_id = request.user.pk
|
||||
asked_id = data["asked"]
|
||||
|
||||
if AskModel().is_asked(asker_id, asked_id):
|
||||
return Response(status=status.HTTP_208_ALREADY_REPORTED)
|
||||
|
||||
AskModel(asker_id=asker_id, asked_id=asked_id).save()
|
||||
return Response(status=status.HTTP_201_CREATED)
|
||||
|
||||
def delete(self, request):
|
||||
data: dict = request.data
|
||||
if (data["asker"] is None):
|
||||
return
|
||||
|
||||
asker_id = data["asker"]
|
||||
asked_id = request.user.pk
|
||||
|
||||
if not AskModel().is_asked(asker_id, asked_id):
|
||||
return Response(status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
asker = User.objects.filter(pk=asked_id)[0]
|
||||
notice_manager.refuse_game(request.user, asker)
|
||||
|
||||
AskModel(asker_id=asker_id, asked_id=asked_id).delete()
|
||||
|
||||
return Response(status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class AskAcceptView(APIView):
|
||||
|
||||
serializer_class = AskSerializer
|
||||
permission_classes = (permissions.IsAuthenticated,)
|
||||
authentication_classes = (SessionAuthentication,)
|
||||
|
||||
def post(self, request):
|
||||
data: dict = request.data
|
||||
if (data["asker"] is None):
|
||||
return
|
||||
|
||||
asker_id = data["asker"]
|
||||
asked_id = request.user.pk
|
||||
|
||||
if not AskModel().is_asked(asker_id, asked_id):
|
||||
return Response(status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
notice_manager.accept_game(asker=User.objects.filter(pk=asker_id)[0], asked=User.objects.filter(pk=asked_id)[0])
|
||||
|
||||
AskModel(asker_id=asker_id, asked_id=asked_id).delete()
|
||||
return Response(status=status.HTTP_200_OK)
|
31
django/chat/views/chat.py
Normal file
31
django/chat/views/chat.py
Normal file
@ -0,0 +1,31 @@
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import permissions
|
||||
from rest_framework.authentication import SessionAuthentication
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
from ..models import ChatChannelModel, ChatMemberModel
|
||||
from ..serializers.chat import ChatChannelSerializer
|
||||
|
||||
|
||||
class ChannelView(APIView):
|
||||
|
||||
serializer_class = ChatChannelSerializer
|
||||
permission_classes = (permissions.IsAuthenticated,)
|
||||
authentication_classes = (SessionAuthentication,)
|
||||
|
||||
def post(self, request):
|
||||
serializer = self.serializer_class(data=request.data, context={'user': request.user})
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
members_id = serializer.validated_data.get('members_id')
|
||||
member_list = [User.objects.get(pk=member_id) for member_id in members_id]
|
||||
|
||||
for member_channel in ChatMemberModel.objects.filter(member=member_list[0]):
|
||||
channel: ChatChannelModel = member_channel.channel
|
||||
if set(channel.get_members()) == set(member_list):
|
||||
break
|
||||
else:
|
||||
channel = ChatChannelModel().create(member_list)
|
||||
return Response(self.serializer_class(channel).data)
|
7
django/docker-entrypoint.sh
Normal file
7
django/docker-entrypoint.sh
Normal file
@ -0,0 +1,7 @@
|
||||
#!/bin/env sh
|
||||
|
||||
python manage.py makemigrations chat games profiles notice
|
||||
python manage.py migrate
|
||||
python manage.py compilemessages
|
||||
|
||||
exec python manage.py runserver "$@"
|
0
django/frontend/__init__.py
Normal file
0
django/frontend/__init__.py
Normal file
3
django/frontend/admin.py
Normal file
3
django/frontend/admin.py
Normal file
@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
6
django/frontend/apps.py
Normal file
6
django/frontend/apps.py
Normal file
@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class FrontendConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'frontend'
|
3
django/frontend/models.py
Normal file
3
django/frontend/models.py
Normal file
@ -0,0 +1,3 @@
|
||||
from django.db import models
|
||||
|
||||
# Create your models here.
|
51
django/frontend/static/css/TournamentPage.css
Normal file
51
django/frontend/static/css/TournamentPage.css
Normal file
@ -0,0 +1,51 @@
|
||||
#tournament-tree {
|
||||
display:flex;
|
||||
flex-direction:row;
|
||||
}
|
||||
|
||||
.round {
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
justify-content:center;
|
||||
width:200px;
|
||||
list-style:none;
|
||||
padding:0;
|
||||
}
|
||||
|
||||
.round .spacer{ flex-grow:1; }
|
||||
.round .spacer:first-child,
|
||||
.round .spacer:last-child{ flex-grow:.5; }
|
||||
|
||||
.round .game-spacer{
|
||||
flex-grow:1;
|
||||
}
|
||||
|
||||
body{
|
||||
font-family:sans-serif;
|
||||
font-size:small;
|
||||
padding:10px;
|
||||
line-height:1.4em;
|
||||
}
|
||||
|
||||
li.game{
|
||||
padding-left:20px;
|
||||
}
|
||||
|
||||
li.game.winner{
|
||||
font-weight:bold;
|
||||
}
|
||||
li.game span{
|
||||
float:right;
|
||||
margin-right:5px;
|
||||
}
|
||||
|
||||
li.game-top{ border-bottom:1px solid #aaa; }
|
||||
|
||||
li.game-spacer{
|
||||
border-right:1px solid #aaa;
|
||||
min-height:40px;
|
||||
}
|
||||
|
||||
li.game-bottom{
|
||||
border-top:1px solid #aaa;
|
||||
}
|
4085
django/frontend/static/css/bootstrap/bootstrap-grid.css
vendored
Normal file
4085
django/frontend/static/css/bootstrap/bootstrap-grid.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
6
django/frontend/static/css/bootstrap/bootstrap-grid.min.css
vendored
Normal file
6
django/frontend/static/css/bootstrap/bootstrap-grid.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
4084
django/frontend/static/css/bootstrap/bootstrap-grid.rtl.css
vendored
Normal file
4084
django/frontend/static/css/bootstrap/bootstrap-grid.rtl.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
6
django/frontend/static/css/bootstrap/bootstrap-grid.rtl.min.css
vendored
Normal file
6
django/frontend/static/css/bootstrap/bootstrap-grid.rtl.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
597
django/frontend/static/css/bootstrap/bootstrap-reboot.css
vendored
Normal file
597
django/frontend/static/css/bootstrap/bootstrap-reboot.css
vendored
Normal file
@ -0,0 +1,597 @@
|
||||
/*!
|
||||
* Bootstrap Reboot v5.3.2 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2023 The Bootstrap Authors
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||
*/
|
||||
:root,
|
||||
[data-bs-theme=light] {
|
||||
--bs-blue: #0d6efd;
|
||||
--bs-indigo: #6610f2;
|
||||
--bs-purple: #6f42c1;
|
||||
--bs-pink: #d63384;
|
||||
--bs-red: #dc3545;
|
||||
--bs-orange: #fd7e14;
|
||||
--bs-yellow: #ffc107;
|
||||
--bs-green: #198754;
|
||||
--bs-teal: #20c997;
|
||||
--bs-cyan: #0dcaf0;
|
||||
--bs-black: #000;
|
||||
--bs-white: #fff;
|
||||
--bs-gray: #6c757d;
|
||||
--bs-gray-dark: #343a40;
|
||||
--bs-gray-100: #f8f9fa;
|
||||
--bs-gray-200: #e9ecef;
|
||||
--bs-gray-300: #dee2e6;
|
||||
--bs-gray-400: #ced4da;
|
||||
--bs-gray-500: #adb5bd;
|
||||
--bs-gray-600: #6c757d;
|
||||
--bs-gray-700: #495057;
|
||||
--bs-gray-800: #343a40;
|
||||
--bs-gray-900: #212529;
|
||||
--bs-primary: #0d6efd;
|
||||
--bs-secondary: #6c757d;
|
||||
--bs-success: #198754;
|
||||
--bs-info: #0dcaf0;
|
||||
--bs-warning: #ffc107;
|
||||
--bs-danger: #dc3545;
|
||||
--bs-light: #f8f9fa;
|
||||
--bs-dark: #212529;
|
||||
--bs-primary-rgb: 13, 110, 253;
|
||||
--bs-secondary-rgb: 108, 117, 125;
|
||||
--bs-success-rgb: 25, 135, 84;
|
||||
--bs-info-rgb: 13, 202, 240;
|
||||
--bs-warning-rgb: 255, 193, 7;
|
||||
--bs-danger-rgb: 220, 53, 69;
|
||||
--bs-light-rgb: 248, 249, 250;
|
||||
--bs-dark-rgb: 33, 37, 41;
|
||||
--bs-primary-text-emphasis: #052c65;
|
||||
--bs-secondary-text-emphasis: #2b2f32;
|
||||
--bs-success-text-emphasis: #0a3622;
|
||||
--bs-info-text-emphasis: #055160;
|
||||
--bs-warning-text-emphasis: #664d03;
|
||||
--bs-danger-text-emphasis: #58151c;
|
||||
--bs-light-text-emphasis: #495057;
|
||||
--bs-dark-text-emphasis: #495057;
|
||||
--bs-primary-bg-subtle: #cfe2ff;
|
||||
--bs-secondary-bg-subtle: #e2e3e5;
|
||||
--bs-success-bg-subtle: #d1e7dd;
|
||||
--bs-info-bg-subtle: #cff4fc;
|
||||
--bs-warning-bg-subtle: #fff3cd;
|
||||
--bs-danger-bg-subtle: #f8d7da;
|
||||
--bs-light-bg-subtle: #fcfcfd;
|
||||
--bs-dark-bg-subtle: #ced4da;
|
||||
--bs-primary-border-subtle: #9ec5fe;
|
||||
--bs-secondary-border-subtle: #c4c8cb;
|
||||
--bs-success-border-subtle: #a3cfbb;
|
||||
--bs-info-border-subtle: #9eeaf9;
|
||||
--bs-warning-border-subtle: #ffe69c;
|
||||
--bs-danger-border-subtle: #f1aeb5;
|
||||
--bs-light-border-subtle: #e9ecef;
|
||||
--bs-dark-border-subtle: #adb5bd;
|
||||
--bs-white-rgb: 255, 255, 255;
|
||||
--bs-black-rgb: 0, 0, 0;
|
||||
--bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
|
||||
--bs-body-font-family: var(--bs-font-sans-serif);
|
||||
--bs-body-font-size: 1rem;
|
||||
--bs-body-font-weight: 400;
|
||||
--bs-body-line-height: 1.5;
|
||||
--bs-body-color: #212529;
|
||||
--bs-body-color-rgb: 33, 37, 41;
|
||||
--bs-body-bg: #fff;
|
||||
--bs-body-bg-rgb: 255, 255, 255;
|
||||
--bs-emphasis-color: #000;
|
||||
--bs-emphasis-color-rgb: 0, 0, 0;
|
||||
--bs-secondary-color: rgba(33, 37, 41, 0.75);
|
||||
--bs-secondary-color-rgb: 33, 37, 41;
|
||||
--bs-secondary-bg: #e9ecef;
|
||||
--bs-secondary-bg-rgb: 233, 236, 239;
|
||||
--bs-tertiary-color: rgba(33, 37, 41, 0.5);
|
||||
--bs-tertiary-color-rgb: 33, 37, 41;
|
||||
--bs-tertiary-bg: #f8f9fa;
|
||||
--bs-tertiary-bg-rgb: 248, 249, 250;
|
||||
--bs-heading-color: inherit;
|
||||
--bs-link-color: #0d6efd;
|
||||
--bs-link-color-rgb: 13, 110, 253;
|
||||
--bs-link-decoration: underline;
|
||||
--bs-link-hover-color: #0a58ca;
|
||||
--bs-link-hover-color-rgb: 10, 88, 202;
|
||||
--bs-code-color: #d63384;
|
||||
--bs-highlight-color: #212529;
|
||||
--bs-highlight-bg: #fff3cd;
|
||||
--bs-border-width: 1px;
|
||||
--bs-border-style: solid;
|
||||
--bs-border-color: #dee2e6;
|
||||
--bs-border-color-translucent: rgba(0, 0, 0, 0.175);
|
||||
--bs-border-radius: 0.375rem;
|
||||
--bs-border-radius-sm: 0.25rem;
|
||||
--bs-border-radius-lg: 0.5rem;
|
||||
--bs-border-radius-xl: 1rem;
|
||||
--bs-border-radius-xxl: 2rem;
|
||||
--bs-border-radius-2xl: var(--bs-border-radius-xxl);
|
||||
--bs-border-radius-pill: 50rem;
|
||||
--bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
||||
--bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
||||
--bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175);
|
||||
--bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075);
|
||||
--bs-focus-ring-width: 0.25rem;
|
||||
--bs-focus-ring-opacity: 0.25;
|
||||
--bs-focus-ring-color: rgba(13, 110, 253, 0.25);
|
||||
--bs-form-valid-color: #198754;
|
||||
--bs-form-valid-border-color: #198754;
|
||||
--bs-form-invalid-color: #dc3545;
|
||||
--bs-form-invalid-border-color: #dc3545;
|
||||
}
|
||||
|
||||
[data-bs-theme=dark] {
|
||||
color-scheme: dark;
|
||||
--bs-body-color: #dee2e6;
|
||||
--bs-body-color-rgb: 222, 226, 230;
|
||||
--bs-body-bg: #212529;
|
||||
--bs-body-bg-rgb: 33, 37, 41;
|
||||
--bs-emphasis-color: #fff;
|
||||
--bs-emphasis-color-rgb: 255, 255, 255;
|
||||
--bs-secondary-color: rgba(222, 226, 230, 0.75);
|
||||
--bs-secondary-color-rgb: 222, 226, 230;
|
||||
--bs-secondary-bg: #343a40;
|
||||
--bs-secondary-bg-rgb: 52, 58, 64;
|
||||
--bs-tertiary-color: rgba(222, 226, 230, 0.5);
|
||||
--bs-tertiary-color-rgb: 222, 226, 230;
|
||||
--bs-tertiary-bg: #2b3035;
|
||||
--bs-tertiary-bg-rgb: 43, 48, 53;
|
||||
--bs-primary-text-emphasis: #6ea8fe;
|
||||
--bs-secondary-text-emphasis: #a7acb1;
|
||||
--bs-success-text-emphasis: #75b798;
|
||||
--bs-info-text-emphasis: #6edff6;
|
||||
--bs-warning-text-emphasis: #ffda6a;
|
||||
--bs-danger-text-emphasis: #ea868f;
|
||||
--bs-light-text-emphasis: #f8f9fa;
|
||||
--bs-dark-text-emphasis: #dee2e6;
|
||||
--bs-primary-bg-subtle: #031633;
|
||||
--bs-secondary-bg-subtle: #161719;
|
||||
--bs-success-bg-subtle: #051b11;
|
||||
--bs-info-bg-subtle: #032830;
|
||||
--bs-warning-bg-subtle: #332701;
|
||||
--bs-danger-bg-subtle: #2c0b0e;
|
||||
--bs-light-bg-subtle: #343a40;
|
||||
--bs-dark-bg-subtle: #1a1d20;
|
||||
--bs-primary-border-subtle: #084298;
|
||||
--bs-secondary-border-subtle: #41464b;
|
||||
--bs-success-border-subtle: #0f5132;
|
||||
--bs-info-border-subtle: #087990;
|
||||
--bs-warning-border-subtle: #997404;
|
||||
--bs-danger-border-subtle: #842029;
|
||||
--bs-light-border-subtle: #495057;
|
||||
--bs-dark-border-subtle: #343a40;
|
||||
--bs-heading-color: inherit;
|
||||
--bs-link-color: #6ea8fe;
|
||||
--bs-link-hover-color: #8bb9fe;
|
||||
--bs-link-color-rgb: 110, 168, 254;
|
||||
--bs-link-hover-color-rgb: 139, 185, 254;
|
||||
--bs-code-color: #e685b5;
|
||||
--bs-highlight-color: #dee2e6;
|
||||
--bs-highlight-bg: #664d03;
|
||||
--bs-border-color: #495057;
|
||||
--bs-border-color-translucent: rgba(255, 255, 255, 0.15);
|
||||
--bs-form-valid-color: #75b798;
|
||||
--bs-form-valid-border-color: #75b798;
|
||||
--bs-form-invalid-color: #ea868f;
|
||||
--bs-form-invalid-border-color: #ea868f;
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
:root {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: var(--bs-body-font-family);
|
||||
font-size: var(--bs-body-font-size);
|
||||
font-weight: var(--bs-body-font-weight);
|
||||
line-height: var(--bs-body-line-height);
|
||||
color: var(--bs-body-color);
|
||||
text-align: var(--bs-body-text-align);
|
||||
background-color: var(--bs-body-bg);
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: 1rem 0;
|
||||
color: inherit;
|
||||
border: 0;
|
||||
border-top: var(--bs-border-width) solid;
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
||||
h6, h5, h4, h3, h2, h1 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
color: var(--bs-heading-color);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: calc(1.375rem + 1.5vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h1 {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: calc(1.325rem + 0.9vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h2 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: calc(1.3rem + 0.6vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h3 {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: calc(1.275rem + 0.3vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h4 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
abbr[title] {
|
||||
-webkit-text-decoration: underline dotted;
|
||||
text-decoration: underline dotted;
|
||||
cursor: help;
|
||||
-webkit-text-decoration-skip-ink: none;
|
||||
text-decoration-skip-ink: none;
|
||||
}
|
||||
|
||||
address {
|
||||
margin-bottom: 1rem;
|
||||
font-style: normal;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul {
|
||||
padding-left: 2rem;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul,
|
||||
dl {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
ol ol,
|
||||
ul ul,
|
||||
ol ul,
|
||||
ul ol {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
dt {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin-bottom: 0.5rem;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
small {
|
||||
font-size: 0.875em;
|
||||
}
|
||||
|
||||
mark {
|
||||
padding: 0.1875em;
|
||||
color: var(--bs-highlight-color);
|
||||
background-color: var(--bs-highlight-bg);
|
||||
}
|
||||
|
||||
sub,
|
||||
sup {
|
||||
position: relative;
|
||||
font-size: 0.75em;
|
||||
line-height: 0;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
a {
|
||||
color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));
|
||||
text-decoration: underline;
|
||||
}
|
||||
a:hover {
|
||||
--bs-link-color-rgb: var(--bs-link-hover-color-rgb);
|
||||
}
|
||||
|
||||
a:not([href]):not([class]), a:not([href]):not([class]):hover {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
pre,
|
||||
code,
|
||||
kbd,
|
||||
samp {
|
||||
font-family: var(--bs-font-monospace);
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
pre {
|
||||
display: block;
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
overflow: auto;
|
||||
font-size: 0.875em;
|
||||
}
|
||||
pre code {
|
||||
font-size: inherit;
|
||||
color: inherit;
|
||||
word-break: normal;
|
||||
}
|
||||
|
||||
code {
|
||||
font-size: 0.875em;
|
||||
color: var(--bs-code-color);
|
||||
word-wrap: break-word;
|
||||
}
|
||||
a > code {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
kbd {
|
||||
padding: 0.1875rem 0.375rem;
|
||||
font-size: 0.875em;
|
||||
color: var(--bs-body-bg);
|
||||
background-color: var(--bs-body-color);
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
kbd kbd {
|
||||
padding: 0;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
figure {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
img,
|
||||
svg {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
table {
|
||||
caption-side: bottom;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
caption {
|
||||
padding-top: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
color: var(--bs-secondary-color);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: inherit;
|
||||
text-align: -webkit-match-parent;
|
||||
}
|
||||
|
||||
thead,
|
||||
tbody,
|
||||
tfoot,
|
||||
tr,
|
||||
td,
|
||||
th {
|
||||
border-color: inherit;
|
||||
border-style: solid;
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
button:focus:not(:focus-visible) {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
input,
|
||||
button,
|
||||
select,
|
||||
optgroup,
|
||||
textarea {
|
||||
margin: 0;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
button,
|
||||
select {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
[role=button] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
select {
|
||||
word-wrap: normal;
|
||||
}
|
||||
select:disabled {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
button,
|
||||
[type=button],
|
||||
[type=reset],
|
||||
[type=submit] {
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
button:not(:disabled),
|
||||
[type=button]:not(:disabled),
|
||||
[type=reset]:not(:disabled),
|
||||
[type=submit]:not(:disabled) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
::-moz-focus-inner {
|
||||
padding: 0;
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
legend {
|
||||
float: left;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: calc(1.275rem + 0.3vw);
|
||||
line-height: inherit;
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
legend {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
legend + * {
|
||||
clear: left;
|
||||
}
|
||||
|
||||
::-webkit-datetime-edit-fields-wrapper,
|
||||
::-webkit-datetime-edit-text,
|
||||
::-webkit-datetime-edit-minute,
|
||||
::-webkit-datetime-edit-hour-field,
|
||||
::-webkit-datetime-edit-day-field,
|
||||
::-webkit-datetime-edit-month-field,
|
||||
::-webkit-datetime-edit-year-field {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
::-webkit-inner-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
[type=search] {
|
||||
-webkit-appearance: textfield;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
/* rtl:raw:
|
||||
[type="tel"],
|
||||
[type="url"],
|
||||
[type="email"],
|
||||
[type="number"] {
|
||||
direction: ltr;
|
||||
}
|
||||
*/
|
||||
::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
::-webkit-color-swatch-wrapper {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
::-webkit-file-upload-button {
|
||||
font: inherit;
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
::file-selector-button {
|
||||
font: inherit;
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
output {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
iframe {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/*# sourceMappingURL=bootstrap-reboot.css.map */
|
File diff suppressed because one or more lines are too long
6
django/frontend/static/css/bootstrap/bootstrap-reboot.min.css
vendored
Normal file
6
django/frontend/static/css/bootstrap/bootstrap-reboot.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
594
django/frontend/static/css/bootstrap/bootstrap-reboot.rtl.css
vendored
Normal file
594
django/frontend/static/css/bootstrap/bootstrap-reboot.rtl.css
vendored
Normal file
@ -0,0 +1,594 @@
|
||||
/*!
|
||||
* Bootstrap Reboot v5.3.2 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2023 The Bootstrap Authors
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||
*/
|
||||
:root,
|
||||
[data-bs-theme=light] {
|
||||
--bs-blue: #0d6efd;
|
||||
--bs-indigo: #6610f2;
|
||||
--bs-purple: #6f42c1;
|
||||
--bs-pink: #d63384;
|
||||
--bs-red: #dc3545;
|
||||
--bs-orange: #fd7e14;
|
||||
--bs-yellow: #ffc107;
|
||||
--bs-green: #198754;
|
||||
--bs-teal: #20c997;
|
||||
--bs-cyan: #0dcaf0;
|
||||
--bs-black: #000;
|
||||
--bs-white: #fff;
|
||||
--bs-gray: #6c757d;
|
||||
--bs-gray-dark: #343a40;
|
||||
--bs-gray-100: #f8f9fa;
|
||||
--bs-gray-200: #e9ecef;
|
||||
--bs-gray-300: #dee2e6;
|
||||
--bs-gray-400: #ced4da;
|
||||
--bs-gray-500: #adb5bd;
|
||||
--bs-gray-600: #6c757d;
|
||||
--bs-gray-700: #495057;
|
||||
--bs-gray-800: #343a40;
|
||||
--bs-gray-900: #212529;
|
||||
--bs-primary: #0d6efd;
|
||||
--bs-secondary: #6c757d;
|
||||
--bs-success: #198754;
|
||||
--bs-info: #0dcaf0;
|
||||
--bs-warning: #ffc107;
|
||||
--bs-danger: #dc3545;
|
||||
--bs-light: #f8f9fa;
|
||||
--bs-dark: #212529;
|
||||
--bs-primary-rgb: 13, 110, 253;
|
||||
--bs-secondary-rgb: 108, 117, 125;
|
||||
--bs-success-rgb: 25, 135, 84;
|
||||
--bs-info-rgb: 13, 202, 240;
|
||||
--bs-warning-rgb: 255, 193, 7;
|
||||
--bs-danger-rgb: 220, 53, 69;
|
||||
--bs-light-rgb: 248, 249, 250;
|
||||
--bs-dark-rgb: 33, 37, 41;
|
||||
--bs-primary-text-emphasis: #052c65;
|
||||
--bs-secondary-text-emphasis: #2b2f32;
|
||||
--bs-success-text-emphasis: #0a3622;
|
||||
--bs-info-text-emphasis: #055160;
|
||||
--bs-warning-text-emphasis: #664d03;
|
||||
--bs-danger-text-emphasis: #58151c;
|
||||
--bs-light-text-emphasis: #495057;
|
||||
--bs-dark-text-emphasis: #495057;
|
||||
--bs-primary-bg-subtle: #cfe2ff;
|
||||
--bs-secondary-bg-subtle: #e2e3e5;
|
||||
--bs-success-bg-subtle: #d1e7dd;
|
||||
--bs-info-bg-subtle: #cff4fc;
|
||||
--bs-warning-bg-subtle: #fff3cd;
|
||||
--bs-danger-bg-subtle: #f8d7da;
|
||||
--bs-light-bg-subtle: #fcfcfd;
|
||||
--bs-dark-bg-subtle: #ced4da;
|
||||
--bs-primary-border-subtle: #9ec5fe;
|
||||
--bs-secondary-border-subtle: #c4c8cb;
|
||||
--bs-success-border-subtle: #a3cfbb;
|
||||
--bs-info-border-subtle: #9eeaf9;
|
||||
--bs-warning-border-subtle: #ffe69c;
|
||||
--bs-danger-border-subtle: #f1aeb5;
|
||||
--bs-light-border-subtle: #e9ecef;
|
||||
--bs-dark-border-subtle: #adb5bd;
|
||||
--bs-white-rgb: 255, 255, 255;
|
||||
--bs-black-rgb: 0, 0, 0;
|
||||
--bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
|
||||
--bs-body-font-family: var(--bs-font-sans-serif);
|
||||
--bs-body-font-size: 1rem;
|
||||
--bs-body-font-weight: 400;
|
||||
--bs-body-line-height: 1.5;
|
||||
--bs-body-color: #212529;
|
||||
--bs-body-color-rgb: 33, 37, 41;
|
||||
--bs-body-bg: #fff;
|
||||
--bs-body-bg-rgb: 255, 255, 255;
|
||||
--bs-emphasis-color: #000;
|
||||
--bs-emphasis-color-rgb: 0, 0, 0;
|
||||
--bs-secondary-color: rgba(33, 37, 41, 0.75);
|
||||
--bs-secondary-color-rgb: 33, 37, 41;
|
||||
--bs-secondary-bg: #e9ecef;
|
||||
--bs-secondary-bg-rgb: 233, 236, 239;
|
||||
--bs-tertiary-color: rgba(33, 37, 41, 0.5);
|
||||
--bs-tertiary-color-rgb: 33, 37, 41;
|
||||
--bs-tertiary-bg: #f8f9fa;
|
||||
--bs-tertiary-bg-rgb: 248, 249, 250;
|
||||
--bs-heading-color: inherit;
|
||||
--bs-link-color: #0d6efd;
|
||||
--bs-link-color-rgb: 13, 110, 253;
|
||||
--bs-link-decoration: underline;
|
||||
--bs-link-hover-color: #0a58ca;
|
||||
--bs-link-hover-color-rgb: 10, 88, 202;
|
||||
--bs-code-color: #d63384;
|
||||
--bs-highlight-color: #212529;
|
||||
--bs-highlight-bg: #fff3cd;
|
||||
--bs-border-width: 1px;
|
||||
--bs-border-style: solid;
|
||||
--bs-border-color: #dee2e6;
|
||||
--bs-border-color-translucent: rgba(0, 0, 0, 0.175);
|
||||
--bs-border-radius: 0.375rem;
|
||||
--bs-border-radius-sm: 0.25rem;
|
||||
--bs-border-radius-lg: 0.5rem;
|
||||
--bs-border-radius-xl: 1rem;
|
||||
--bs-border-radius-xxl: 2rem;
|
||||
--bs-border-radius-2xl: var(--bs-border-radius-xxl);
|
||||
--bs-border-radius-pill: 50rem;
|
||||
--bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
||||
--bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
||||
--bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175);
|
||||
--bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075);
|
||||
--bs-focus-ring-width: 0.25rem;
|
||||
--bs-focus-ring-opacity: 0.25;
|
||||
--bs-focus-ring-color: rgba(13, 110, 253, 0.25);
|
||||
--bs-form-valid-color: #198754;
|
||||
--bs-form-valid-border-color: #198754;
|
||||
--bs-form-invalid-color: #dc3545;
|
||||
--bs-form-invalid-border-color: #dc3545;
|
||||
}
|
||||
|
||||
[data-bs-theme=dark] {
|
||||
color-scheme: dark;
|
||||
--bs-body-color: #dee2e6;
|
||||
--bs-body-color-rgb: 222, 226, 230;
|
||||
--bs-body-bg: #212529;
|
||||
--bs-body-bg-rgb: 33, 37, 41;
|
||||
--bs-emphasis-color: #fff;
|
||||
--bs-emphasis-color-rgb: 255, 255, 255;
|
||||
--bs-secondary-color: rgba(222, 226, 230, 0.75);
|
||||
--bs-secondary-color-rgb: 222, 226, 230;
|
||||
--bs-secondary-bg: #343a40;
|
||||
--bs-secondary-bg-rgb: 52, 58, 64;
|
||||
--bs-tertiary-color: rgba(222, 226, 230, 0.5);
|
||||
--bs-tertiary-color-rgb: 222, 226, 230;
|
||||
--bs-tertiary-bg: #2b3035;
|
||||
--bs-tertiary-bg-rgb: 43, 48, 53;
|
||||
--bs-primary-text-emphasis: #6ea8fe;
|
||||
--bs-secondary-text-emphasis: #a7acb1;
|
||||
--bs-success-text-emphasis: #75b798;
|
||||
--bs-info-text-emphasis: #6edff6;
|
||||
--bs-warning-text-emphasis: #ffda6a;
|
||||
--bs-danger-text-emphasis: #ea868f;
|
||||
--bs-light-text-emphasis: #f8f9fa;
|
||||
--bs-dark-text-emphasis: #dee2e6;
|
||||
--bs-primary-bg-subtle: #031633;
|
||||
--bs-secondary-bg-subtle: #161719;
|
||||
--bs-success-bg-subtle: #051b11;
|
||||
--bs-info-bg-subtle: #032830;
|
||||
--bs-warning-bg-subtle: #332701;
|
||||
--bs-danger-bg-subtle: #2c0b0e;
|
||||
--bs-light-bg-subtle: #343a40;
|
||||
--bs-dark-bg-subtle: #1a1d20;
|
||||
--bs-primary-border-subtle: #084298;
|
||||
--bs-secondary-border-subtle: #41464b;
|
||||
--bs-success-border-subtle: #0f5132;
|
||||
--bs-info-border-subtle: #087990;
|
||||
--bs-warning-border-subtle: #997404;
|
||||
--bs-danger-border-subtle: #842029;
|
||||
--bs-light-border-subtle: #495057;
|
||||
--bs-dark-border-subtle: #343a40;
|
||||
--bs-heading-color: inherit;
|
||||
--bs-link-color: #6ea8fe;
|
||||
--bs-link-hover-color: #8bb9fe;
|
||||
--bs-link-color-rgb: 110, 168, 254;
|
||||
--bs-link-hover-color-rgb: 139, 185, 254;
|
||||
--bs-code-color: #e685b5;
|
||||
--bs-highlight-color: #dee2e6;
|
||||
--bs-highlight-bg: #664d03;
|
||||
--bs-border-color: #495057;
|
||||
--bs-border-color-translucent: rgba(255, 255, 255, 0.15);
|
||||
--bs-form-valid-color: #75b798;
|
||||
--bs-form-valid-border-color: #75b798;
|
||||
--bs-form-invalid-color: #ea868f;
|
||||
--bs-form-invalid-border-color: #ea868f;
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
:root {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: var(--bs-body-font-family);
|
||||
font-size: var(--bs-body-font-size);
|
||||
font-weight: var(--bs-body-font-weight);
|
||||
line-height: var(--bs-body-line-height);
|
||||
color: var(--bs-body-color);
|
||||
text-align: var(--bs-body-text-align);
|
||||
background-color: var(--bs-body-bg);
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: 1rem 0;
|
||||
color: inherit;
|
||||
border: 0;
|
||||
border-top: var(--bs-border-width) solid;
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
||||
h6, h5, h4, h3, h2, h1 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
color: var(--bs-heading-color);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: calc(1.375rem + 1.5vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h1 {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: calc(1.325rem + 0.9vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h2 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: calc(1.3rem + 0.6vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h3 {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: calc(1.275rem + 0.3vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h4 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
abbr[title] {
|
||||
-webkit-text-decoration: underline dotted;
|
||||
text-decoration: underline dotted;
|
||||
cursor: help;
|
||||
-webkit-text-decoration-skip-ink: none;
|
||||
text-decoration-skip-ink: none;
|
||||
}
|
||||
|
||||
address {
|
||||
margin-bottom: 1rem;
|
||||
font-style: normal;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul {
|
||||
padding-right: 2rem;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul,
|
||||
dl {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
ol ol,
|
||||
ul ul,
|
||||
ol ul,
|
||||
ul ol {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
dt {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin-bottom: 0.5rem;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
small {
|
||||
font-size: 0.875em;
|
||||
}
|
||||
|
||||
mark {
|
||||
padding: 0.1875em;
|
||||
color: var(--bs-highlight-color);
|
||||
background-color: var(--bs-highlight-bg);
|
||||
}
|
||||
|
||||
sub,
|
||||
sup {
|
||||
position: relative;
|
||||
font-size: 0.75em;
|
||||
line-height: 0;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
a {
|
||||
color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));
|
||||
text-decoration: underline;
|
||||
}
|
||||
a:hover {
|
||||
--bs-link-color-rgb: var(--bs-link-hover-color-rgb);
|
||||
}
|
||||
|
||||
a:not([href]):not([class]), a:not([href]):not([class]):hover {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
pre,
|
||||
code,
|
||||
kbd,
|
||||
samp {
|
||||
font-family: var(--bs-font-monospace);
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
pre {
|
||||
display: block;
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
overflow: auto;
|
||||
font-size: 0.875em;
|
||||
}
|
||||
pre code {
|
||||
font-size: inherit;
|
||||
color: inherit;
|
||||
word-break: normal;
|
||||
}
|
||||
|
||||
code {
|
||||
font-size: 0.875em;
|
||||
color: var(--bs-code-color);
|
||||
word-wrap: break-word;
|
||||
}
|
||||
a > code {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
kbd {
|
||||
padding: 0.1875rem 0.375rem;
|
||||
font-size: 0.875em;
|
||||
color: var(--bs-body-bg);
|
||||
background-color: var(--bs-body-color);
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
kbd kbd {
|
||||
padding: 0;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
figure {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
img,
|
||||
svg {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
table {
|
||||
caption-side: bottom;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
caption {
|
||||
padding-top: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
color: var(--bs-secondary-color);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: inherit;
|
||||
text-align: -webkit-match-parent;
|
||||
}
|
||||
|
||||
thead,
|
||||
tbody,
|
||||
tfoot,
|
||||
tr,
|
||||
td,
|
||||
th {
|
||||
border-color: inherit;
|
||||
border-style: solid;
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
button:focus:not(:focus-visible) {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
input,
|
||||
button,
|
||||
select,
|
||||
optgroup,
|
||||
textarea {
|
||||
margin: 0;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
button,
|
||||
select {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
[role=button] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
select {
|
||||
word-wrap: normal;
|
||||
}
|
||||
select:disabled {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
button,
|
||||
[type=button],
|
||||
[type=reset],
|
||||
[type=submit] {
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
button:not(:disabled),
|
||||
[type=button]:not(:disabled),
|
||||
[type=reset]:not(:disabled),
|
||||
[type=submit]:not(:disabled) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
::-moz-focus-inner {
|
||||
padding: 0;
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
legend {
|
||||
float: right;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: calc(1.275rem + 0.3vw);
|
||||
line-height: inherit;
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
legend {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
legend + * {
|
||||
clear: right;
|
||||
}
|
||||
|
||||
::-webkit-datetime-edit-fields-wrapper,
|
||||
::-webkit-datetime-edit-text,
|
||||
::-webkit-datetime-edit-minute,
|
||||
::-webkit-datetime-edit-hour-field,
|
||||
::-webkit-datetime-edit-day-field,
|
||||
::-webkit-datetime-edit-month-field,
|
||||
::-webkit-datetime-edit-year-field {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
::-webkit-inner-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
[type=search] {
|
||||
-webkit-appearance: textfield;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
[type="tel"],
|
||||
[type="url"],
|
||||
[type="email"],
|
||||
[type="number"] {
|
||||
direction: ltr;
|
||||
}
|
||||
::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
::-webkit-color-swatch-wrapper {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
::-webkit-file-upload-button {
|
||||
font: inherit;
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
::file-selector-button {
|
||||
font: inherit;
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
output {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
iframe {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
/*# sourceMappingURL=bootstrap-reboot.rtl.css.map */
|
File diff suppressed because one or more lines are too long
6
django/frontend/static/css/bootstrap/bootstrap-reboot.rtl.min.css
vendored
Normal file
6
django/frontend/static/css/bootstrap/bootstrap-reboot.rtl.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
5402
django/frontend/static/css/bootstrap/bootstrap-utilities.css
vendored
Normal file
5402
django/frontend/static/css/bootstrap/bootstrap-utilities.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
6
django/frontend/static/css/bootstrap/bootstrap-utilities.min.css
vendored
Normal file
6
django/frontend/static/css/bootstrap/bootstrap-utilities.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
5393
django/frontend/static/css/bootstrap/bootstrap-utilities.rtl.css
vendored
Normal file
5393
django/frontend/static/css/bootstrap/bootstrap-utilities.rtl.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
6
django/frontend/static/css/bootstrap/bootstrap-utilities.rtl.min.css
vendored
Normal file
6
django/frontend/static/css/bootstrap/bootstrap-utilities.rtl.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
12068
django/frontend/static/css/bootstrap/bootstrap.css
vendored
Normal file
12068
django/frontend/static/css/bootstrap/bootstrap.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
django/frontend/static/css/bootstrap/bootstrap.css.map
Normal file
1
django/frontend/static/css/bootstrap/bootstrap.css.map
Normal file
File diff suppressed because one or more lines are too long
6
django/frontend/static/css/bootstrap/bootstrap.min.css
vendored
Normal file
6
django/frontend/static/css/bootstrap/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
12032
django/frontend/static/css/bootstrap/bootstrap.rtl.css
vendored
Normal file
12032
django/frontend/static/css/bootstrap/bootstrap.rtl.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
6
django/frontend/static/css/bootstrap/bootstrap.rtl.min.css
vendored
Normal file
6
django/frontend/static/css/bootstrap/bootstrap.rtl.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
8
django/frontend/static/css/game.css
Normal file
8
django/frontend/static/css/game.css
Normal file
@ -0,0 +1,8 @@
|
||||
#canva {
|
||||
background-color: white;
|
||||
border: 1px;
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
15
django/frontend/static/css/gameHistory.css
Normal file
15
django/frontend/static/css/gameHistory.css
Normal file
@ -0,0 +1,15 @@
|
||||
|
||||
#game-list {
|
||||
justify-content: flex-start;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
#game-list .game-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 160px;
|
||||
width: 160px;
|
||||
margin: 10px;
|
||||
border-radius: 5%;
|
||||
}
|
23
django/frontend/static/css/gameOffline.css
Normal file
23
django/frontend/static/css/gameOffline.css
Normal file
@ -0,0 +1,23 @@
|
||||
#gameCanvas {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#up1:active, #down1:active, #up2:active, #down2:active {
|
||||
color: red;
|
||||
}
|
||||
|
||||
#up1, #down1, #up2, #down2 {
|
||||
min-height: 60px;
|
||||
min-width: 60px;
|
||||
|
||||
font-size: 40px;
|
||||
}
|
||||
|
||||
#up1, #down1 {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#up2, #down2 {
|
||||
position: relative;
|
||||
left: calc(420px - (60px * 3));
|
||||
}
|
23
django/frontend/static/css/index.css
Normal file
23
django/frontend/static/css/index.css
Normal file
@ -0,0 +1,23 @@
|
||||
#popup {
|
||||
position: fixed;
|
||||
font-size: 1.2em;
|
||||
z-index: 1; /* foreground */
|
||||
|
||||
top:calc(1% + 0.1em);
|
||||
left:50%;
|
||||
transform: translate(-50%, 50%);
|
||||
|
||||
border: 1em solid #1a1a1a;
|
||||
color: #1a1a1a;
|
||||
background-color: #cccccc;
|
||||
|
||||
padding: 5px;
|
||||
border-width: 0.1em;
|
||||
|
||||
opacity: 0;
|
||||
transition: opacity 0.25s;
|
||||
}
|
||||
|
||||
#languageSelector > .dropdown-item.active {
|
||||
background-color: transparent;
|
||||
}
|
25
django/frontend/static/css/profile.css
Normal file
25
django/frontend/static/css/profile.css
Normal file
@ -0,0 +1,25 @@
|
||||
#app * {
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
#app #username
|
||||
{
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
#app #block, #app #friend {
|
||||
cursor: pointer;
|
||||
font-size: 0.7em;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
#app {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
#app #yes, #app #no {
|
||||
display:inline;
|
||||
cursor: pointer;
|
||||
font-size: 0.7em;
|
||||
text-decoration: underline;
|
||||
}
|
134
django/frontend/static/css/search.css
Normal file
134
django/frontend/static/css/search.css
Normal file
@ -0,0 +1,134 @@
|
||||
#app * {
|
||||
font-size: 40px;
|
||||
}
|
||||
|
||||
#app img
|
||||
|
||||
{
|
||||
max-height: 4em;
|
||||
max-width: 4em;
|
||||
min-height: 2em;
|
||||
min-width: 2em;
|
||||
}
|
||||
|
||||
#app ul
|
||||
{
|
||||
font-size: 0.75em;
|
||||
margin: 0.25em 0 0 0;
|
||||
padding: 0 0 0 0;
|
||||
list-style-type: none;
|
||||
max-height: 80vh;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
#app li
|
||||
{
|
||||
margin: 0.25em 0.25em 0 0;
|
||||
}
|
||||
|
||||
#app #chats {
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
text-align: left;
|
||||
}
|
||||
#app #users {
|
||||
margin: 0em 1.0em 0em 0.05em;
|
||||
}
|
||||
|
||||
#app #chat {
|
||||
position: relative;
|
||||
max-height: 100vh;
|
||||
width: 100vh;
|
||||
/*border: 2px solid green;*/
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#app #members {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
#app #add_chat_off {
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#app #add_chat_on {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#app #messages {
|
||||
max-height: 60vh;
|
||||
overflow: scroll;
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
font-size: 0.75em;
|
||||
}
|
||||
|
||||
#app #input_user{
|
||||
color: green;
|
||||
width: 8.5em;
|
||||
height: 1.1em;
|
||||
font-size: 0.65em;
|
||||
border: none;
|
||||
outline: none;
|
||||
border-bottom: 0.15em solid green;
|
||||
}
|
||||
|
||||
#app #input_chat{
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
/*width: calc(100% - 8px);*/
|
||||
width: 100%;
|
||||
border: none;
|
||||
outline: none;
|
||||
border-bottom: 0.15em solid green;
|
||||
color: green;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
#app #you {
|
||||
text-align: left;
|
||||
position: relative;
|
||||
max-width: 48%;
|
||||
left: 0.5em;
|
||||
margin: 0.5em 0 0 0;
|
||||
color: green;
|
||||
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
#app #other {
|
||||
text-align: right;
|
||||
position: relative;
|
||||
max-width: 48%;
|
||||
margin: 0.5em 0 0 auto;
|
||||
right: 0.5em;
|
||||
color: red;
|
||||
|
||||
/* permet le retour à la ligne à la place de dépasser*/
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
#app #invite, #app #yes, #app #no {
|
||||
position: relative;
|
||||
border: none;
|
||||
color: white;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
font-size: 0.8em;
|
||||
height: 2em;
|
||||
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;
|
||||
}
|
14
django/frontend/static/css/tictactoe.css
Normal file
14
django/frontend/static/css/tictactoe.css
Normal file
@ -0,0 +1,14 @@
|
||||
#canva {
|
||||
width: 510px;
|
||||
height:510px;
|
||||
margin: 0px auto;
|
||||
}
|
||||
|
||||
#Morpion {
|
||||
margin: 0px auto;
|
||||
}
|
||||
|
||||
#rule {
|
||||
text-align: center;
|
||||
margin: 0px auto;
|
||||
}
|
69
django/frontend/static/js/3D/buffers.js
Normal file
69
django/frontend/static/js/3D/buffers.js
Normal file
@ -0,0 +1,69 @@
|
||||
function initBuffers(gl)
|
||||
{
|
||||
const vertexBuffer = initVertexBuffer(gl);
|
||||
const indexBuffer = initIndexBuffer(gl);
|
||||
const normalBuffer = initNormalBuffer(gl);
|
||||
return { vertex: vertexBuffer, index : indexBuffer, normal: normalBuffer };
|
||||
}
|
||||
|
||||
function initVertexBuffer(gl)
|
||||
{
|
||||
const positionBuffer = gl.createBuffer();
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
|
||||
const positions = [
|
||||
// Front face
|
||||
-1.0, -1.0, 1.0, 1.0, -1.0, 1.0, 1.0, 1.0, 1.0, -1.0, 1.0, 1.0,
|
||||
// Back face
|
||||
-1.0, -1.0, -1.0, -1.0, 1.0, -1.0, 1.0, 1.0, -1.0, 1.0, -1.0, -1.0,
|
||||
// Top face
|
||||
-1.0, 1.0, -1.0, -1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, -1.0,
|
||||
// Bottom face
|
||||
-1.0, -1.0, -1.0, 1.0, -1.0, -1.0, 1.0, -1.0, 1.0, -1.0, -1.0, 1.0,
|
||||
// Right face
|
||||
1.0, -1.0, -1.0, 1.0, 1.0, -1.0, 1.0, 1.0, 1.0, 1.0, -1.0, 1.0,
|
||||
// Left face
|
||||
-1.0, -1.0, -1.0, -1.0, -1.0, 1.0, -1.0, 1.0, 1.0, -1.0, 1.0, -1.0
|
||||
];
|
||||
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
|
||||
return positionBuffer;
|
||||
}
|
||||
|
||||
function initNormalBuffer(gl)
|
||||
{
|
||||
const normalBuffer = gl.createBuffer();
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, normalBuffer);
|
||||
const vertexNormals = [
|
||||
// Front
|
||||
0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0,
|
||||
// Back
|
||||
0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0,
|
||||
// Top
|
||||
0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0,
|
||||
// Bottom
|
||||
0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0,
|
||||
// Right
|
||||
1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0,
|
||||
// Left
|
||||
-1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0,
|
||||
];
|
||||
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertexNormals), gl.STATIC_DRAW);
|
||||
return normalBuffer;
|
||||
}
|
||||
|
||||
function initIndexBuffer(gl)
|
||||
{
|
||||
const indexBuffer = gl.createBuffer();
|
||||
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
|
||||
const indices = [
|
||||
0, 1, 2, 0, 2, 3, // front
|
||||
4, 5, 6, 4, 6, 7, // back
|
||||
8, 9, 10, 8, 10, 11, // top
|
||||
12, 13, 14, 12, 14, 15, // bottom
|
||||
16, 17, 18, 16, 18, 19, // right
|
||||
20, 21, 22, 20, 22, 23, // left
|
||||
];
|
||||
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW);
|
||||
return indexBuffer;
|
||||
}
|
||||
|
||||
export { initBuffers };
|
42
django/frontend/static/js/3D/cube.js
Normal file
42
django/frontend/static/js/3D/cube.js
Normal file
@ -0,0 +1,42 @@
|
||||
import { shaderInfos } from "../3D/shaders.js"
|
||||
|
||||
function renderCube(ctx, x, y, z, angle = 0, sx = 1, sy = 1, sz = 1)
|
||||
{
|
||||
const modelMatrix = mat4.create();
|
||||
|
||||
mat4.translate(
|
||||
modelMatrix,
|
||||
modelMatrix,
|
||||
[x, y, z]
|
||||
);
|
||||
|
||||
mat4.rotate(
|
||||
modelMatrix,
|
||||
modelMatrix,
|
||||
angle,
|
||||
[0, 1, 0],
|
||||
);
|
||||
|
||||
mat4.scale(
|
||||
modelMatrix,
|
||||
modelMatrix,
|
||||
[sx, sy, sz]
|
||||
);
|
||||
|
||||
mat4.translate(
|
||||
modelMatrix,
|
||||
modelMatrix,
|
||||
[-1, 0, 0] // wtf, this works ?
|
||||
);
|
||||
|
||||
const normalMatrix = mat4.create();
|
||||
mat4.invert(normalMatrix, modelMatrix);
|
||||
mat4.transpose(normalMatrix, normalMatrix);
|
||||
|
||||
ctx.uniformMatrix4fv(shaderInfos.uniformLocations.modelMatrix, false, modelMatrix);
|
||||
ctx.uniformMatrix4fv(shaderInfos.uniformLocations.normalMatrix, false, normalMatrix);
|
||||
|
||||
ctx.drawElements(ctx.TRIANGLES, 36, ctx.UNSIGNED_SHORT, 0);
|
||||
}
|
||||
|
||||
export { renderCube };
|
28
django/frontend/static/js/3D/maths/gl-matrix-min.js
vendored
Normal file
28
django/frontend/static/js/3D/maths/gl-matrix-min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
87
django/frontend/static/js/3D/shaders.js
Normal file
87
django/frontend/static/js/3D/shaders.js
Normal file
@ -0,0 +1,87 @@
|
||||
const vertex_shader_source = `
|
||||
attribute vec4 aPos;
|
||||
attribute vec3 aNormal;
|
||||
|
||||
uniform mat4 uMod;
|
||||
uniform mat4 uView;
|
||||
uniform mat4 uProj;
|
||||
uniform mat4 uNormalMat;
|
||||
|
||||
varying highp vec3 vLighting;
|
||||
|
||||
void main()
|
||||
{
|
||||
gl_Position = uProj * uView * uMod * aPos;
|
||||
|
||||
highp vec3 ambientLight = vec3(0.3, 0.3, 0.3);
|
||||
highp vec3 directionalLightColor = vec3(1, 1, 1);
|
||||
highp vec3 directionalVector = vec3(-10, 2, -10);
|
||||
|
||||
highp vec4 transformedNormal = uNormalMat * vec4(aNormal, 1.0);
|
||||
|
||||
highp float directional = max(dot(transformedNormal.xyz, directionalVector), 0.0);
|
||||
vLighting = ambientLight + (directionalLightColor * directional);
|
||||
}
|
||||
`;
|
||||
|
||||
const fragment_shader_source = `
|
||||
varying highp vec3 vLighting;
|
||||
|
||||
void main()
|
||||
{
|
||||
highp vec3 color = vec3(1.0, 1.0, 1.0);
|
||||
gl_FragColor = vec4(color * vLighting, 1.0);
|
||||
}
|
||||
`;
|
||||
|
||||
export function initShaderProgram(gl)
|
||||
{
|
||||
const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vertex_shader_source);
|
||||
const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fragment_shader_source);
|
||||
|
||||
const prog = gl.createProgram();
|
||||
gl.attachShader(prog, vertexShader);
|
||||
gl.attachShader(prog, fragmentShader);
|
||||
gl.linkProgram(prog);
|
||||
|
||||
shaderInfos = {
|
||||
program: prog,
|
||||
attribLocations: {
|
||||
vertexPosition: gl.getAttribLocation(prog, "aPos"),
|
||||
vertexNormal: gl.getAttribLocation(prog, "aNormal"),
|
||||
},
|
||||
uniformLocations: {
|
||||
projectionMatrix: gl.getUniformLocation(prog, "uProj"),
|
||||
modelMatrix: gl.getUniformLocation(prog, "uMod"),
|
||||
viewMatrix: gl.getUniformLocation(prog, "uView"),
|
||||
normalMatrix: gl.getUniformLocation(prog, "uNormalMat"),
|
||||
},
|
||||
};
|
||||
|
||||
if(!gl.getProgramParameter(prog, gl.LINK_STATUS))
|
||||
{
|
||||
alert(`Unable to initialize the shader program: ${gl.getProgramInfoLog(prog)}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
return prog;
|
||||
}
|
||||
|
||||
function loadShader(gl, type, source)
|
||||
{
|
||||
const shader = gl.createShader(type);
|
||||
|
||||
gl.shaderSource(shader, source);
|
||||
gl.compileShader(shader);
|
||||
|
||||
if(!gl.getShaderParameter(shader, gl.COMPILE_STATUS))
|
||||
{
|
||||
alert(`An error occurred while compiling the shaders: ${gl.getShaderInfoLog(shader)}`);
|
||||
gl.deleteShader(shader);
|
||||
return null;
|
||||
}
|
||||
|
||||
return shader;
|
||||
}
|
||||
|
||||
export let shaderInfos;
|
51
django/frontend/static/js/api/AExchangable.js
Normal file
51
django/frontend/static/js/api/AExchangable.js
Normal file
@ -0,0 +1,51 @@
|
||||
|
||||
|
||||
export class AExchangeable
|
||||
{
|
||||
/**
|
||||
* This abstract class implement import and export method useful to export/import data to/from the server
|
||||
* @param {[String]} fieldNameList
|
||||
*/
|
||||
export(fieldNameList = [])
|
||||
{
|
||||
let valueList = [];
|
||||
|
||||
fieldNameList.forEach(fieldName => {
|
||||
let value;
|
||||
|
||||
if (this[fieldName] instanceof AExchangeable)
|
||||
value = this[fieldName].export();
|
||||
else
|
||||
value = this[fieldName];
|
||||
});
|
||||
|
||||
return valueList;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} data
|
||||
*/
|
||||
import(data)
|
||||
{
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
|
||||
if (Array.isArray(value))
|
||||
{
|
||||
for (let i = 0; i < value.length; i++)
|
||||
{
|
||||
if (this[key][i] instanceof AExchangeable)
|
||||
this[key][i].import(value[i]);
|
||||
else
|
||||
this[key][i] = value[i];
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (this[key] instanceof AExchangeable)
|
||||
this[key].import(value);
|
||||
else
|
||||
this[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
93
django/frontend/static/js/api/Account.js
Normal file
93
django/frontend/static/js/api/Account.js
Normal file
@ -0,0 +1,93 @@
|
||||
import { Client } from "./Client.js";
|
||||
|
||||
class Account
|
||||
{
|
||||
/**
|
||||
* @param {Client} client
|
||||
*/
|
||||
constructor (client)
|
||||
{
|
||||
/**
|
||||
* @type {Client} client
|
||||
*/
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {String} username
|
||||
* @param {String} password
|
||||
* @returns {Response}
|
||||
*/
|
||||
async create(username, password)
|
||||
{
|
||||
let response = await this.client._post("/api/accounts/register", {username: username, password: password});
|
||||
|
||||
if (response.status === 201)
|
||||
await this.client._update_logged(true);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {String} password
|
||||
* @returns {?Promise<Object>}
|
||||
*/
|
||||
async delete(password)
|
||||
{
|
||||
const response = await this.client._delete("/api/accounts/delete", {password: password});
|
||||
|
||||
if (response.ok) {
|
||||
this.client._update_logged(false);
|
||||
return null;
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {String} newUsername
|
||||
* @returns {?Promise<Object>}
|
||||
*/
|
||||
async updateUsername(newUsername)
|
||||
{
|
||||
const data = {
|
||||
username: newUsername
|
||||
};
|
||||
const response = await this.client._patch_json(`/api/accounts/update_profile`, data);
|
||||
const respondeData = await response.json();
|
||||
|
||||
if (response.status === 200) {
|
||||
this.client.me.username = respondeData.username;
|
||||
document.getElementById('navbarDropdownButton').innerHTML = respondeData.username;
|
||||
document.getElementById('myProfileLink').href = '/profiles/' + respondeData.username;
|
||||
return null;
|
||||
}
|
||||
return respondeData['authorize'] || respondeData['detail'] || respondeData['username']?.join(' ') || 'Error.';
|
||||
}
|
||||
|
||||
async updatePassword(currentPassword, newPassword, newPassword2)
|
||||
{
|
||||
const data = {
|
||||
current_password: currentPassword,
|
||||
new_password: newPassword,
|
||||
new_password2: newPassword2
|
||||
};
|
||||
const response = await this.client._put('/api/accounts/update_password', data);
|
||||
if (response.ok)
|
||||
return null;
|
||||
|
||||
const responseData = await response.json();
|
||||
const formatedData = {};
|
||||
if (responseData['current_password'])
|
||||
formatedData['currentPasswordDetail'] = responseData['current_password'];
|
||||
if (responseData['new_password'])
|
||||
formatedData['newPasswordDetail'] = responseData['new_password'];
|
||||
if (responseData['new_password2'])
|
||||
formatedData['newPassword2Detail'] = responseData['new_password2'];
|
||||
if (formatedData == {})
|
||||
formatedData['passwordDetail'] = 'Error';
|
||||
return formatedData;
|
||||
}
|
||||
}
|
||||
|
||||
export { Account };
|
258
django/frontend/static/js/api/Client.js
Normal file
258
django/frontend/static/js/api/Client.js
Normal file
@ -0,0 +1,258 @@
|
||||
import { Account } from "./Account.js";
|
||||
import { MatchMaking } from "./Matchmaking.js";
|
||||
import { Profiles } from "./Profiles.js";
|
||||
import { MyProfile } from "./MyProfile.js";
|
||||
import Notice from "./Notice.js";
|
||||
import LanguageManager from './LanguageManager.js';
|
||||
|
||||
function getCookie(name)
|
||||
{
|
||||
let cookie = {};
|
||||
document.cookie.split(';').forEach(function(el) {
|
||||
let split = el.split('=');
|
||||
cookie[split[0].trim()] = split.slice(1).join("=");
|
||||
});
|
||||
return cookie[name];
|
||||
}
|
||||
|
||||
class Client
|
||||
{
|
||||
/**
|
||||
*
|
||||
* @param {String} url
|
||||
*/
|
||||
constructor(url)
|
||||
{
|
||||
/**
|
||||
* @type {String}
|
||||
*/
|
||||
this._url = url;
|
||||
|
||||
/**
|
||||
* @type {Account}
|
||||
*/
|
||||
this.account = new Account(this);
|
||||
|
||||
/**
|
||||
* @type {Profiles}
|
||||
*/
|
||||
this.profiles = new Profiles(this);
|
||||
|
||||
/**
|
||||
* @type {MatchMaking}
|
||||
*/
|
||||
this.matchmaking = new MatchMaking(this);
|
||||
|
||||
/**
|
||||
* @type {Boolean} A private var represent if the is is log NEVER USE IT use await isAuthenticated()
|
||||
*/
|
||||
this._logged = undefined;
|
||||
|
||||
/**
|
||||
* @type {Notice}
|
||||
*/
|
||||
this.notice = new Notice(this);
|
||||
|
||||
this.lang = new LanguageManager();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* The only right way to determine is the user is logged
|
||||
* @returns {Promise<Boolean>}
|
||||
*/
|
||||
async isAuthenticated()
|
||||
{
|
||||
if (this._logged == undefined)
|
||||
this._logged = await this._test_logged();
|
||||
return this._logged;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a GET request to %uri%
|
||||
* @param {String} uri
|
||||
* @param {*} data
|
||||
* @returns {Promise<Response>}
|
||||
*/
|
||||
async _get(uri, data)
|
||||
{
|
||||
let response = await fetch(this._url + uri, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
'Accept-Language': this.lang.currentLang
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a POST request
|
||||
* @param {String} uri
|
||||
* @param {*} data
|
||||
* @returns {Promise<Response>}
|
||||
*/
|
||||
async _post(uri, data)
|
||||
{
|
||||
let response = await fetch(this._url + uri, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRFToken": getCookie("csrftoken"),
|
||||
'Accept-Language': this.lang.currentLang,
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a DELETE request
|
||||
* @param {String} uri
|
||||
* @param {String} data
|
||||
* @returns {Promise<Response>}
|
||||
*/
|
||||
async _delete(uri, data)
|
||||
{
|
||||
let response = await fetch(this._url + uri, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRFToken": getCookie("csrftoken"),
|
||||
'Accept-Language': this.lang.currentLang,
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a PUT request with json
|
||||
* @param {String} uri
|
||||
* @param {*} data
|
||||
* @returns {Promise<Response>}
|
||||
*/
|
||||
async _put(uri, data)
|
||||
{
|
||||
let response = await fetch(this._url + uri, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"X-CSRFToken": getCookie("csrftoken"),
|
||||
"Content-Type": "application/json",
|
||||
'Accept-Language': this.lang.currentLang,
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a PATCH request with json
|
||||
* @param {String} uri
|
||||
* @param {*} data
|
||||
* @returns {Promise<Response>}
|
||||
*/
|
||||
async _patch_json(uri, data)
|
||||
{
|
||||
let response = await fetch(this._url + uri, {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
"X-CSRFToken": getCookie("csrftoken"),
|
||||
"Content-Type": "application/json",
|
||||
'Accept-Language': this.lang.currentLang,
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a PATCH request with file
|
||||
* @param {String} uri
|
||||
* @param {*} file
|
||||
* @returns {Promise<Response>}
|
||||
*/
|
||||
async _patch_file(uri, file)
|
||||
{
|
||||
let response = await fetch(this._url + uri, {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
"X-CSRFToken": getCookie("csrftoken"),
|
||||
'Accept-Language': this.lang.currentLang,
|
||||
},
|
||||
body: file,
|
||||
});
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change logged state. Use It if you recv an 403 error
|
||||
* @param {Promise<?>} state
|
||||
* @returns
|
||||
*/
|
||||
async _update_logged(state)
|
||||
{
|
||||
if (this._logged == state)
|
||||
return;
|
||||
|
||||
if (state)
|
||||
{
|
||||
this.me = new MyProfile(this);
|
||||
await this.me.init();
|
||||
this.notice.start();
|
||||
document.getElementById('navbarLoggedOut').classList.add('d-none');
|
||||
document.getElementById('navbarLoggedIn').classList.remove('d-none');
|
||||
document.getElementById('navbarDropdownButton').innerHTML = this.me.username;
|
||||
document.getElementById('myProfileLink').href = '/profiles/' + this.me.username;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.me = undefined;
|
||||
this.notice.stop();
|
||||
document.getElementById('navbarLoggedOut').classList.remove('d-none');
|
||||
document.getElementById('navbarLoggedIn').classList.add('d-none');
|
||||
document.getElementById('navbarDropdownButton').innerHTML = 'Me';
|
||||
document.getElementById('myProfileLink').href = '';
|
||||
}
|
||||
this._logged = state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loggin the user
|
||||
* @param {String} username
|
||||
* @param {String} password
|
||||
* @returns {Promise<Response>}
|
||||
*/
|
||||
async login(username, password)
|
||||
{
|
||||
let response = await this._post("/api/accounts/login", {username: username, password: password});
|
||||
if (response.status == 200)
|
||||
await this._update_logged(true);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Logout the user
|
||||
* @returns {Promise<?>}
|
||||
*/
|
||||
async logout()
|
||||
{
|
||||
await this._get("/api/accounts/logout");
|
||||
await this._update_logged(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the user is logged. NEVER USE IT, USE isAuthenticated()
|
||||
* @returns {Promise<Boolean>}
|
||||
*/
|
||||
async _test_logged()
|
||||
{
|
||||
let response = await this._get("/api/accounts/logged");
|
||||
|
||||
await this._update_logged(response.status === 200);
|
||||
return response.status === 200;
|
||||
}
|
||||
}
|
||||
|
||||
export {Client};
|
74
django/frontend/static/js/api/LanguageManager.js
Normal file
74
django/frontend/static/js/api/LanguageManager.js
Normal file
@ -0,0 +1,74 @@
|
||||
import { reloadView } from '../index.js';
|
||||
|
||||
export default class LanguageManager {
|
||||
constructor() {
|
||||
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.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;
|
||||
|
||||
await this.loadDict(this.chosenLang);
|
||||
if (!this.dict)
|
||||
return 1;
|
||||
|
||||
document.querySelectorAll('[data-i18n]').forEach(el => {
|
||||
let key = el.getAttribute('data-i18n');
|
||||
el.innerHTML = this.dict[key];
|
||||
});
|
||||
await reloadView();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
async changeLanguage(lang) {
|
||||
if (lang === this.currentLang || !this.availableLanguages.includes(lang))
|
||||
return 1;
|
||||
|
||||
this.chosenLang = lang;
|
||||
if (await this.translatePage() !== 0)
|
||||
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;
|
||||
}
|
||||
}
|
61
django/frontend/static/js/api/Matchmaking.js
Normal file
61
django/frontend/static/js/api/Matchmaking.js
Normal file
@ -0,0 +1,61 @@
|
||||
import { Client } from "./Client.js";
|
||||
|
||||
class MatchMaking
|
||||
{
|
||||
/**
|
||||
* @param {Client} client
|
||||
*/
|
||||
constructor(client)
|
||||
{
|
||||
/**
|
||||
* @type {Client}
|
||||
*/
|
||||
this.client = client;
|
||||
this.searching = false;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {CallableFunction} receive_func
|
||||
* @param {CallableFunction} disconnect_func
|
||||
* @param {Number} mode The number of players in a game
|
||||
* @returns {Promise<?>}
|
||||
*/
|
||||
async start(receive_func, disconnect_func, game_type, mode)
|
||||
{
|
||||
if (!await this.client.isAuthenticated())
|
||||
return null;
|
||||
|
||||
let url = `${window.location.protocol[4] === 's' ? 'wss' : 'ws'}://${window.location.host}/ws/matchmaking/${game_type}/${mode}`;
|
||||
|
||||
this._socket = new WebSocket(url);
|
||||
|
||||
this.searching = true;
|
||||
|
||||
this.receive_func = receive_func;
|
||||
this.disconnect_func = disconnect_func;
|
||||
|
||||
this._socket.onmessage = function (event) {
|
||||
const data = JSON.parse(event.data);
|
||||
receive_func(data);
|
||||
};
|
||||
|
||||
this._socket.onclose = this.onclose.bind(this);
|
||||
}
|
||||
|
||||
onclose(event)
|
||||
{
|
||||
this.stop();
|
||||
this.disconnect_func(event);
|
||||
}
|
||||
|
||||
stop()
|
||||
{
|
||||
if (this._socket)
|
||||
this._socket.close();
|
||||
this._socket = undefined;
|
||||
this.searching = false;
|
||||
}
|
||||
}
|
||||
|
||||
export {MatchMaking};
|
144
django/frontend/static/js/api/MyProfile.js
Normal file
144
django/frontend/static/js/api/MyProfile.js
Normal file
@ -0,0 +1,144 @@
|
||||
import { Client } from "./Client.js";
|
||||
import { Profile } from "./Profile.js";
|
||||
|
||||
class MyProfile extends Profile
|
||||
{
|
||||
|
||||
/**
|
||||
* @param {Client} client
|
||||
*/
|
||||
constructor (client)
|
||||
{
|
||||
super(client, "../me");
|
||||
|
||||
/**
|
||||
* @type {[Profile]}
|
||||
*/
|
||||
this.blockedUsers = [];
|
||||
// /**
|
||||
// * @type {[Profile]}
|
||||
// */
|
||||
// this.friendList = [];
|
||||
// /**
|
||||
// * @type {[Profile]}
|
||||
// */
|
||||
// this.incomingFriendRequests = [];
|
||||
// /**
|
||||
// * @type {[Profile]}
|
||||
// */
|
||||
// this.outgoingFriendRequests = [];
|
||||
}
|
||||
|
||||
async init() {
|
||||
await super.init();
|
||||
await this.getBlockedUsers();
|
||||
// await this.getFriends();
|
||||
// await this.getIncomingFriendRequests()
|
||||
// await this.getOutgoingFriendRequests()
|
||||
}
|
||||
|
||||
async getBlockedUsers() {
|
||||
const response = await this.client._get('/api/profiles/block');
|
||||
const data = await response.json();
|
||||
data.forEach(profileData => this.blockedUsers.push(new Profile(this.client, profileData.username, profileData.id, profileData.avatar)));
|
||||
}
|
||||
|
||||
async getFriends() {
|
||||
const response = await this.client._get('/api/profiles/friends');
|
||||
const data = await response.json();
|
||||
data.forEach(profileData => this.friendList.push(new Profile(this.client, profileData.username, profileData.id, profileData.avatar)));
|
||||
}
|
||||
async getIncomingFriendRequests() {
|
||||
const response = await this.client._get('/api/profiles/incoming_friend_requests');
|
||||
const data = await response.json();
|
||||
data.forEach(profileData => this.incomingFriendRequests.push(
|
||||
new Profile(this.client, profileData.username, profileData.id, profileData.avatar)
|
||||
));
|
||||
}
|
||||
async getOutgoingFriendRequests() {
|
||||
const response = await this.client._get('/api/profiles/outgoing_friend_requests');
|
||||
const data = await response.json();
|
||||
data.forEach(profileData => this.outgoingFriendRequests.push(
|
||||
new Profile(this.client, profileData.username, profileData.id, profileData.avatar)
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Profile} profile
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
_isFriend(profile) {
|
||||
for (const user of this.friendList) {
|
||||
if (user.id === profile.id)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* @param {Profile} profile
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
_isBlocked(profile) {
|
||||
for (const user of this.blockedUsers) {
|
||||
if (user.id === profile.id)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* @param {Profile} profile
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
_hasIncomingRequestFrom(profile) {
|
||||
for (const user of this.incomingFriendRequests) {
|
||||
if (user.id === profile.id)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* @param {Profile} profile
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
_hasOutgoingRequestTo(profile) {
|
||||
for (const user of this.outgoingFriendRequests) {
|
||||
if (user.id === profile.id)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {File} selectedFile
|
||||
* @returns {Promise<Response>}
|
||||
*/
|
||||
async changeAvatar(selectedFile)
|
||||
{
|
||||
const formData = new FormData();
|
||||
formData.append('avatar', selectedFile);
|
||||
|
||||
const response = await this.client._patch_file(`/api/profiles/settings`, formData);
|
||||
const responseData = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
this.avatar = responseData.avatar;
|
||||
return null;
|
||||
}
|
||||
return responseData;
|
||||
}
|
||||
|
||||
async deleteAvatar() {
|
||||
const response = await this.client._delete('/api/profiles/settings');
|
||||
const responseData = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
this.avatar = responseData.avatar;
|
||||
return null;
|
||||
}
|
||||
return responseData;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export {MyProfile};
|
101
django/frontend/static/js/api/Notice.js
Normal file
101
django/frontend/static/js/api/Notice.js
Normal file
@ -0,0 +1,101 @@
|
||||
import {Client} from './Client.js';
|
||||
import {createNotification} from '../utils/noticeUtils.js'
|
||||
import { lastView } from '../index.js';
|
||||
import ProfilePageView from '../views/ProfilePageView.js';
|
||||
import Search from '../views/Search.js';
|
||||
|
||||
export default class Notice {
|
||||
|
||||
/**
|
||||
* @param {Client} client
|
||||
*/
|
||||
constructor(client) {
|
||||
/**
|
||||
* @type {Client}
|
||||
*/
|
||||
this.client = client;
|
||||
this.url = location.origin.replace('http', 'ws') + '/ws/notice';
|
||||
}
|
||||
|
||||
start() {
|
||||
this._socket = new WebSocket(this.url);
|
||||
|
||||
this._socket.onclose = _ => this._socket = undefined;
|
||||
this._socket.onmessage = message => {
|
||||
const data = JSON.parse(message.data);
|
||||
//console.log(data)
|
||||
|
||||
if (data.type === 'friend_request') {
|
||||
this.friend_request(data.author);
|
||||
} else if (data.type === 'new_friend') {
|
||||
this.new_friend(data.friend);
|
||||
} else if (data.type === 'friend_removed') {
|
||||
this.friend_removed(data.friend);
|
||||
} else if (data.type === 'friend_request_canceled') {
|
||||
this.friend_request_canceled(data.author);
|
||||
} else if (data.type === 'online') {
|
||||
this.online(data.user)
|
||||
} else if (data.type === 'offline') {
|
||||
this.offline(data.user)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
stop() {
|
||||
if (this._socket) {
|
||||
this._socket.close();
|
||||
this._socket = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
_setOnlineStatus(user, status) {
|
||||
if (lastView instanceof ProfilePageView && lastView.profile.id === user.id) {
|
||||
lastView.profile.online = status;
|
||||
lastView.loadFriendshipStatus();
|
||||
}
|
||||
else if (lastView instanceof Search) {
|
||||
lastView.display_specific_user(user.id);
|
||||
}
|
||||
}
|
||||
|
||||
online(user) {
|
||||
this._setOnlineStatus(user, true)
|
||||
}
|
||||
|
||||
offline(user) {
|
||||
this._setOnlineStatus(user, false)
|
||||
}
|
||||
|
||||
friend_request(author) {
|
||||
createNotification('Friend Request', `<strong>${author.username}</strong> sent you a friend request.`);
|
||||
if (lastView instanceof ProfilePageView && lastView.profile.id === author.id) {
|
||||
lastView.profile.hasIncomingRequest = true;
|
||||
lastView.loadFriendshipStatus();
|
||||
}
|
||||
}
|
||||
|
||||
new_friend(friend) {
|
||||
createNotification('New Friend', `<strong>${friend.username}</strong> accepted your friend request.`);
|
||||
if (lastView instanceof ProfilePageView && lastView.profile.id === friend.id) {
|
||||
lastView.profile.isFriend = true;
|
||||
lastView.profile.hasIncomingRequest = false;
|
||||
lastView.profile.hasOutgoingRequest = false;
|
||||
lastView.loadFriendshipStatus();
|
||||
}
|
||||
}
|
||||
|
||||
friend_removed(exFriend) {
|
||||
if (lastView instanceof ProfilePageView && lastView.profile.id === exFriend.id) {
|
||||
lastView.profile.isFriend = false;
|
||||
lastView.profile.online = null;
|
||||
lastView.loadFriendshipStatus();
|
||||
}
|
||||
}
|
||||
|
||||
friend_request_canceled(author) {
|
||||
if (lastView instanceof ProfilePageView && lastView.profile.id === author.id) {
|
||||
lastView.profile.hasIncomingRequest = false;
|
||||
lastView.loadFriendshipStatus();
|
||||
}
|
||||
}
|
||||
}
|
101
django/frontend/static/js/api/Profile.js
Normal file
101
django/frontend/static/js/api/Profile.js
Normal file
@ -0,0 +1,101 @@
|
||||
import { AExchangeable } from "./AExchangable.js";
|
||||
import { Client } from "./Client.js";
|
||||
|
||||
export class Profile extends AExchangeable
|
||||
{
|
||||
/**
|
||||
* @param {Client} client
|
||||
*/
|
||||
constructor (client, username, id, avatar)
|
||||
{
|
||||
super();
|
||||
|
||||
/**
|
||||
* @type {Client} client
|
||||
*/
|
||||
this.client = client;
|
||||
|
||||
/**
|
||||
* @type {String}
|
||||
*/
|
||||
this.username = username;
|
||||
|
||||
/**
|
||||
* @type {Number}
|
||||
*/
|
||||
this.id = id;
|
||||
|
||||
/**
|
||||
* @type {String}
|
||||
*/
|
||||
this.avatar = avatar;
|
||||
|
||||
/**
|
||||
* @type {Boolean}
|
||||
**/
|
||||
this.online = null;
|
||||
|
||||
/**
|
||||
* @type {Boolean}
|
||||
*/
|
||||
this.isFriend;
|
||||
this.isBlocked;
|
||||
this.hasIncomingRequest;
|
||||
this.hasOutgoingRequest;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
async init()
|
||||
{
|
||||
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;
|
||||
|
||||
const responseData = await response.json();
|
||||
this.id = responseData.id;
|
||||
this.username = responseData.username;
|
||||
this.avatar = responseData.avatar;
|
||||
this.online = responseData.online
|
||||
|
||||
if (!this.client.me || this.client.me.id === this.id)
|
||||
return;
|
||||
|
||||
this.hasIncomingRequest = responseData.has_incoming_request;
|
||||
this.hasOutgoingRequest = responseData.has_outgoing_request;
|
||||
this.isFriend = responseData.is_friend;
|
||||
this.isBlocked = this.client.me._isBlocked(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<[Object]>}
|
||||
*/
|
||||
async getGameHistory()
|
||||
{
|
||||
const response = await this.client._get(`/api/games/history/${this.id}`);
|
||||
const response_data = await response.json();
|
||||
|
||||
const games = [];
|
||||
|
||||
response_data.forEach(game_data => {
|
||||
games.push(game_data);
|
||||
});
|
||||
|
||||
return games;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {[String]} additionalFieldList
|
||||
*/
|
||||
export(additionalFieldList = [])
|
||||
{
|
||||
super.export([...["username", "avatar", "id"], ...additionalFieldList])
|
||||
}
|
||||
}
|
59
django/frontend/static/js/api/Profiles.js
Normal file
59
django/frontend/static/js/api/Profiles.js
Normal file
@ -0,0 +1,59 @@
|
||||
import { Profile } from "./Profile.js";
|
||||
|
||||
class Profiles
|
||||
{
|
||||
/**
|
||||
* @param {Client} client
|
||||
*/
|
||||
constructor (client)
|
||||
{
|
||||
/**
|
||||
* @type {Client} client
|
||||
*/
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns {Promise<[Profile]>}
|
||||
*/
|
||||
async all()
|
||||
{
|
||||
let response = await this.client._get("/api/profiles/");
|
||||
let response_data = await response.json();
|
||||
|
||||
let profiles = [];
|
||||
response_data.forEach((profile) => {
|
||||
profiles.push(new Profile(this.client, profile.username, profile.id, profile.avatar));
|
||||
});
|
||||
return profiles;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {String} username
|
||||
* @returns {?Promise<Profile>}
|
||||
*/
|
||||
async getProfile(username)
|
||||
{
|
||||
let profile = new Profile(this.client, username);
|
||||
if (await profile.init())
|
||||
return null;
|
||||
return profile;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Number} id
|
||||
* @returns {Profile}
|
||||
*/
|
||||
async getProfileId(id)
|
||||
{
|
||||
let profile = new Profile(this.client, undefined, id);
|
||||
if (await profile.init())
|
||||
return null;
|
||||
return profile;
|
||||
}
|
||||
}
|
||||
|
||||
export {Profiles};
|
22
django/frontend/static/js/api/chat/Ask.js
Normal file
22
django/frontend/static/js/api/chat/Ask.js
Normal file
@ -0,0 +1,22 @@
|
||||
|
||||
class Ask {
|
||||
constructor(client) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
async ask_game(asked) {
|
||||
response = await this.client._post(`/api/chat/ask/`);
|
||||
}
|
||||
|
||||
async ask_game_canceled() {
|
||||
|
||||
}
|
||||
|
||||
async ask_game_accepted() {
|
||||
|
||||
}
|
||||
|
||||
async ask_game_refused() {
|
||||
|
||||
}
|
||||
}
|
66
django/frontend/static/js/api/chat/Channel.js
Normal file
66
django/frontend/static/js/api/chat/Channel.js
Normal file
@ -0,0 +1,66 @@
|
||||
import {Message} from "./Message.js";
|
||||
|
||||
class Channel {
|
||||
constructor(client, channel, members, messages, reload) {
|
||||
this.client = client;
|
||||
this.channel = channel;
|
||||
this.members = members;
|
||||
this.messages = [];
|
||||
if (messages != undefined)
|
||||
this.updateMessages(messages);
|
||||
|
||||
this.connect(reload);
|
||||
}
|
||||
|
||||
// reload = function to use when we receive a message
|
||||
connect(reload) {
|
||||
const url = location.origin.replace('http', 'ws') +
|
||||
'/ws/chat/' +
|
||||
this.channel;
|
||||
|
||||
this.chatSocket = new WebSocket(url);
|
||||
this.chatSocket.onmessage = (event) =>{
|
||||
let data = JSON.parse(event.data);
|
||||
|
||||
this.messages.push(new Message(
|
||||
this.channel,
|
||||
data.author,
|
||||
data.content,
|
||||
data.time,
|
||||
));
|
||||
|
||||
reload();
|
||||
};
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
this.chatSocket.close();
|
||||
}
|
||||
|
||||
updateMessages(messages)
|
||||
{
|
||||
this.messages = [];
|
||||
|
||||
messages.forEach((message) => {
|
||||
this.messages.push(new Message(
|
||||
message.channel,
|
||||
message.author,
|
||||
message.content,
|
||||
message.time,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
async sendMessageChannel(message, receivers_id) {
|
||||
|
||||
if (this.chatSocket == undefined)
|
||||
return;
|
||||
|
||||
this.chatSocket.send(JSON.stringify({
|
||||
'message':message,
|
||||
'receivers_id':receivers_id,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
export {Channel};
|
23
django/frontend/static/js/api/chat/Channels.js
Normal file
23
django/frontend/static/js/api/chat/Channels.js
Normal file
@ -0,0 +1,23 @@
|
||||
import {Channel} from "./Channel.js";
|
||||
|
||||
export default class Channels {
|
||||
constructor(client) {
|
||||
this.client = client;
|
||||
this.channel = undefined;
|
||||
}
|
||||
|
||||
async createChannel(members_id, reload) {
|
||||
|
||||
const response = await this.client._post("/api/chat/", {
|
||||
members_id:members_id
|
||||
});
|
||||
|
||||
if (response.status >= 300)
|
||||
return undefined;
|
||||
|
||||
const data = await response.json();
|
||||
console.log(data)
|
||||
|
||||
this.channel = new Channel(this.client, data.id, members_id, data.messages, reload);
|
||||
}
|
||||
}
|
10
django/frontend/static/js/api/chat/Message.js
Normal file
10
django/frontend/static/js/api/chat/Message.js
Normal file
@ -0,0 +1,10 @@
|
||||
class Message {
|
||||
constructor(channel, author, content, time) {
|
||||
this.channel = channel;
|
||||
this.author = author;
|
||||
this.content = content;
|
||||
this.time = time;
|
||||
}
|
||||
}
|
||||
|
||||
export {Message};
|
159
django/frontend/static/js/api/game/AGame.js
Normal file
159
django/frontend/static/js/api/game/AGame.js
Normal file
@ -0,0 +1,159 @@
|
||||
import { AExchangeable } from "../AExchangable.js";
|
||||
import { APlayer } from "./APlayer.js";
|
||||
import { Client } from "../Client.js"
|
||||
import { sleep } from "../../utils/sleep.js";
|
||||
import { Profile } from "../Profile.js";
|
||||
|
||||
export class AGame extends AExchangeable
|
||||
{
|
||||
/**
|
||||
* Abstract class to create commununication between client and server
|
||||
* @param {Client} client
|
||||
* @param {Number} id
|
||||
* @param {CallableFunction} receiveHandler
|
||||
* @param {CallableFunction} disconntectHandler
|
||||
* @param {"tictactoe" | "pong"} gameType
|
||||
*/
|
||||
constructor(client, id, receiveHandler, disconntectHandler, gameType)
|
||||
{
|
||||
super();
|
||||
|
||||
/**
|
||||
* @type {Client}
|
||||
*/
|
||||
this.client = client;
|
||||
|
||||
/**
|
||||
* @type {Number}
|
||||
*/
|
||||
this.id = id;
|
||||
|
||||
/**
|
||||
* ex: Tictactoe, Pong
|
||||
* @type {String}
|
||||
*/
|
||||
this.gameType = gameType;
|
||||
|
||||
/**
|
||||
* @type {CallableFunction}
|
||||
*/
|
||||
this._receiveHandler = receiveHandler;
|
||||
|
||||
/**
|
||||
* @type {CallableFunction}
|
||||
*/
|
||||
this._disconntectHandler = disconntectHandler;
|
||||
|
||||
/**
|
||||
* @type {Profile}
|
||||
*/
|
||||
this.winner;
|
||||
|
||||
/**
|
||||
* @type {Number}
|
||||
*/
|
||||
this.startTimestamp;
|
||||
|
||||
/**
|
||||
* @type {Number}
|
||||
*/
|
||||
this.stopTimestamp;
|
||||
|
||||
/**
|
||||
* @type {Boolean}
|
||||
*/
|
||||
this.started;
|
||||
|
||||
/**
|
||||
* @type {Boolean}
|
||||
*/
|
||||
this.finished;
|
||||
|
||||
/**
|
||||
* @type {[APlayer]}
|
||||
*/
|
||||
this.players = [];
|
||||
}
|
||||
|
||||
async init()
|
||||
{
|
||||
let response = await this.client._get(`/api/games/${this.id}`);
|
||||
|
||||
if (response.status !== 200)
|
||||
return response.status;
|
||||
|
||||
let response_data = await response.json();
|
||||
|
||||
this.import(response_data);
|
||||
}
|
||||
|
||||
getState()
|
||||
{
|
||||
return ["waiting", "started", "finished"][this.started + this.finished];
|
||||
}
|
||||
|
||||
/**
|
||||
* Send string to the server, must be excuted after .join()
|
||||
* @param {String} data
|
||||
*/
|
||||
send(data)
|
||||
{
|
||||
if (this._socket === undefined || this._socket.readyState !== WebSocket.OPEN)
|
||||
return;
|
||||
this._socket.send(data);
|
||||
}
|
||||
|
||||
async join()
|
||||
{
|
||||
if (this.finished === true)
|
||||
{
|
||||
console.error("The Game is not currently ongoing.");
|
||||
return;
|
||||
}
|
||||
|
||||
const url = `${window.location.protocol[4] === 's' ? 'wss' : 'ws'}://${window.location.host}/ws/games/${this.gameType}/${this.id}`;
|
||||
|
||||
this._socket = new WebSocket(url);
|
||||
|
||||
this._socket.onmessage = async (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
await this._receiveHandler(data);
|
||||
};
|
||||
|
||||
this._socket.onclose = async () => {
|
||||
this._socket = undefined;
|
||||
await this._disconntectHandler();
|
||||
};
|
||||
}
|
||||
|
||||
leave()
|
||||
{
|
||||
if (this._socket)
|
||||
{
|
||||
this._socket.close();
|
||||
this._socket = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Should be redefine using your own APlayer inherited
|
||||
* @param {Object} data
|
||||
|
||||
import(data)
|
||||
{
|
||||
super.import(data);
|
||||
|
||||
// just an example code
|
||||
|
||||
/*
|
||||
this.players.length = 0;
|
||||
|
||||
data.players.forEach(player_data => {
|
||||
let player = new APlayer(this.client, this);
|
||||
player.import(player_data);
|
||||
this.players.push(player);
|
||||
});
|
||||
|
||||
}
|
||||
*/
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user