Push project
This commit is contained in:
commit
ea9f2f4e49
3
.env
Normal file
3
.env
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
POSTGRES_DB=django
|
||||||
|
POSTGRES_USER=django
|
||||||
|
POSTGRES_PASSWORD=apagnan
|
47
README.md
Normal file
47
README.md
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
# FT_TRANSCENDENCE
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
This project is part of the 42 School Common Core curriculum. Its purpose is to learn how websites function by creating one to play the Pong game.
|
||||||
|
|
||||||
|
## Skills Acquired
|
||||||
|
- Django
|
||||||
|
- JavaScript
|
||||||
|
- Rest API
|
||||||
|
- Python
|
||||||
|
- HTML
|
||||||
|
- CSS
|
||||||
|
|
||||||
|
## 
|
||||||
|
|
||||||
|
## Modules
|
||||||
|
|
||||||
|
| Module | Nb point | state|
|
||||||
|
| ------ | -------- | ---- |
|
||||||
|
| Multiplayer | 2 | 🏁 |
|
||||||
|
| Remote | 2 | 🏁 |
|
||||||
|
| Bootstrap | 1 | 🏁 |
|
||||||
|
| Django | 2 | 🏁 |
|
||||||
|
| Bdd | 1 | 🏁 |
|
||||||
|
| Accounts | 2 | 🏁 |
|
||||||
|
| WebGL | 2 | 🏁 |
|
||||||
|
| Other game | 2 | 🏁 |
|
||||||
|
| Chat | 2 | 🏁 |
|
||||||
|
| Translation | 1 | 🏁 |
|
||||||
|
| Other browser | 1 | 🏁 |
|
||||||
|
| Smartphone support | 1 | 🏁 |
|
||||||
|
| --- | --- | ---
|
||||||
|
| Total | 19 | 🏁 |
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Manual
|
||||||
|
- Clone the project:
|
||||||
|
``` bash
|
||||||
|
git clone https://git.chauvet.pro/michel/ft_transcendence
|
||||||
|
cd ft_transcendence
|
||||||
|
```
|
||||||
|
- Start docker containers.
|
||||||
|
``` bash
|
||||||
|
docker-compose up --build
|
||||||
|
```
|
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 BigIntegerField, 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=1024)
|
||||||
|
time = BigIntegerField(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.filter(asker_id=asker_id, asked_id=asked_id).first() 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()),
|
||||||
|
]
|
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.'})
|
11
django/chat/urls.py
Normal file
11
django/chat/urls.py
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
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"),
|
||||||
|
path("ask/<int:pk>", ask.AskView.as_view(), name="chats_ask_get"),
|
||||||
|
]
|
98
django/chat/views/ask.py
Normal file
98
django/chat/views/ask.py
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
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 games.models import GameModel
|
||||||
|
|
||||||
|
from notice.consumers import notice_manager
|
||||||
|
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
|
||||||
|
|
||||||
|
class AskView(APIView):
|
||||||
|
|
||||||
|
permission_classes = (permissions.IsAuthenticated,)
|
||||||
|
authentication_classes = (SessionAuthentication,)
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
data: dict = request.data
|
||||||
|
|
||||||
|
asker_id = request.user.pk
|
||||||
|
asked_id = data.get("asked")
|
||||||
|
|
||||||
|
if (asked_id is None):
|
||||||
|
return Response(status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
if AskModel().is_asked(asker_id, asked_id):
|
||||||
|
return Response(status=status.HTTP_208_ALREADY_REPORTED)
|
||||||
|
|
||||||
|
asked = User.objects.get(pk=asked_id)
|
||||||
|
|
||||||
|
notice_manager.ask_game(asked, request.user.username)
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
asker_id = data.get("asker")
|
||||||
|
asked_id = request.user.pk
|
||||||
|
|
||||||
|
if (asker_id is None):
|
||||||
|
return Response(status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
if not AskModel().is_asked(asker_id, asked_id):
|
||||||
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
# Don't need more verification, just above is enough
|
||||||
|
asker = User.objects.get(pk=asker_id)
|
||||||
|
|
||||||
|
notice_manager.refuse_game(asker, request.user.username)
|
||||||
|
|
||||||
|
AskModel.objects.get(asker_id=asker_id, asked_id=asked_id).delete()
|
||||||
|
|
||||||
|
return Response(status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
def get(self, request, pk=None):
|
||||||
|
data: dict = request.data
|
||||||
|
|
||||||
|
asker_id = request.user.pk
|
||||||
|
asked_id = pk
|
||||||
|
|
||||||
|
if (asked_id is None):
|
||||||
|
return Response(status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
if not AskModel().is_asked(asked_id, asker_id):
|
||||||
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
return Response(status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
class AskAcceptView(APIView):
|
||||||
|
|
||||||
|
permission_classes = (permissions.IsAuthenticated,)
|
||||||
|
authentication_classes = (SessionAuthentication,)
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
|
||||||
|
data: dict = request.data
|
||||||
|
|
||||||
|
asker_id = data.get("asker")
|
||||||
|
asked_id = request.user.pk
|
||||||
|
|
||||||
|
if (asker_id is None):
|
||||||
|
return Response(status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
if not AskModel().is_asked(asker_id, asked_id):
|
||||||
|
return Response(status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
asker = User.objects.get(pk=asker_id)
|
||||||
|
asked = request.user
|
||||||
|
|
||||||
|
id_game = GameModel(game_type="pong").create([asker, asked]).pk
|
||||||
|
|
||||||
|
notice_manager.accept_game(asker, request.user.username, id_game)
|
||||||
|
|
||||||
|
AskModel.objects.get(asker_id=asker_id, asked_id=asked_id).delete()
|
||||||
|
return Response({"id_game":id_game}, 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%;
|
||||||
|
}
|
27
django/frontend/static/css/gameOffline.css
Normal file
27
django/frontend/static/css/gameOffline.css
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
body {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#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;
|
||||||
|
}
|
14
django/frontend/static/css/pongOnline.css
Normal file
14
django/frontend/static/css/pongOnline.css
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
#up:active, #down:active {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
#up, #down {
|
||||||
|
min-height: 60px;
|
||||||
|
min-width: 60px;
|
||||||
|
|
||||||
|
font-size: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#up, #down {
|
||||||
|
position: relative;
|
||||||
|
}
|
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 #username-input{
|
||||||
|
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 };
|
34
django/frontend/static/js/3D/cube.js
Normal file
34
django/frontend/static/js/3D/cube.js
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
function renderCube(ctx, shader_infos, 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]
|
||||||
|
);
|
||||||
|
|
||||||
|
const normalMatrix = mat4.create();
|
||||||
|
mat4.invert(normalMatrix, modelMatrix);
|
||||||
|
mat4.transpose(normalMatrix, normalMatrix);
|
||||||
|
|
||||||
|
ctx.uniformMatrix4fv(shader_infos.uniformLocations.modelMatrix, false, modelMatrix);
|
||||||
|
ctx.uniformMatrix4fv(shader_infos.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
85
django/frontend/static/js/3D/shaders.js
Normal file
85
django/frontend/static/js/3D/shaders.js
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
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);
|
||||||
|
|
||||||
|
const 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 shaderInfos;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
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;
|
||||||
|
await 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};
|
129
django/frontend/static/js/api/Notice.js
Normal file
129
django/frontend/static/js/api/Notice.js
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
import {Client} from './Client.js';
|
||||||
|
import {createNotification} from '../utils/noticeUtils.js'
|
||||||
|
import { lastView, navigateTo } from '../index.js';
|
||||||
|
import ProfilePageView from '../views/ProfilePageView.js';
|
||||||
|
import Search from '../views/Search.js';
|
||||||
|
import { sleep } from '../utils/sleep.js';
|
||||||
|
|
||||||
|
export default class Notice {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Client} client
|
||||||
|
*/
|
||||||
|
constructor(client) {
|
||||||
|
/**
|
||||||
|
* @type {Client}
|
||||||
|
*/
|
||||||
|
this.client = client;
|
||||||
|
this.url = location.origin.replace('http', 'ws') + '/ws/notice';
|
||||||
|
}
|
||||||
|
|
||||||
|
async start() {
|
||||||
|
this._socket = new WebSocket(this.url);
|
||||||
|
|
||||||
|
this._socket.onclose = _ => this._socket = undefined;
|
||||||
|
this._socket.onmessage = async message => {
|
||||||
|
const data = JSON.parse(message.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)
|
||||||
|
} else if (data.type == 'game_asked') {
|
||||||
|
this.game_asked(data.asker);
|
||||||
|
} else if (data.type == 'game_accepted') {
|
||||||
|
this.game_accepted(data.asked, data.id_game);
|
||||||
|
} else if (data.type == 'game_refused') {
|
||||||
|
this.game_refused(data.asked);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async stop() {
|
||||||
|
if (this._socket) {
|
||||||
|
while (!this._socket.readyState === 1)
|
||||||
|
await sleep(100);
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
game_asked(asker) {
|
||||||
|
createNotification('Game Invite', `<b>${asker}</b> ask to play a 1vs1 pong`);
|
||||||
|
if (lastView instanceof Search)
|
||||||
|
lastView.display_invite();
|
||||||
|
}
|
||||||
|
|
||||||
|
game_refused(asked) {
|
||||||
|
createNotification('Game Refused', `<b>${asked}</b> refuse your proposition to play`);
|
||||||
|
if (lastView instanceof Search)
|
||||||
|
lastView.display_invite();
|
||||||
|
}
|
||||||
|
|
||||||
|
async game_accepted(asked, id_game) {
|
||||||
|
createNotification('Game Accepted', `<b>${asked}</b> accept your proposition to play`);
|
||||||
|
if (lastView instanceof Search)
|
||||||
|
lastView.display_invite();
|
||||||
|
|
||||||
|
await navigateTo(`/games/pong/${id_game}`);
|
||||||
|
}
|
||||||
|
}
|
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};
|
50
django/frontend/static/js/api/chat/Ask.js
Normal file
50
django/frontend/static/js/api/chat/Ask.js
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
import { lastView, navigateTo } from '../../index.js';
|
||||||
|
import Search from '../../views/Search.js';
|
||||||
|
|
||||||
|
export default class Ask {
|
||||||
|
constructor(client) {
|
||||||
|
this.client = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
async ask_game(asked) {
|
||||||
|
let response = await this.client._post(`/api/chat/ask/`, {
|
||||||
|
asked:asked,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async ask_game_accepted(asker) {
|
||||||
|
let response = await this.client._post(`/api/chat/ask/accept/`, {
|
||||||
|
asker:asker,
|
||||||
|
});
|
||||||
|
|
||||||
|
const statu = response.status;
|
||||||
|
if (statu == 404 || statu == 204)
|
||||||
|
return
|
||||||
|
if (lastView instanceof Search)
|
||||||
|
lastView.display_invite();
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
await navigateTo(`/games/pong/${data.id_game}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async ask_game_refused(asker) {
|
||||||
|
let response = await this.client._delete(`/api/chat/ask/`, {
|
||||||
|
asker:asker,
|
||||||
|
});
|
||||||
|
|
||||||
|
const statu = response.status;
|
||||||
|
if (statu == 404 || statu == 204)
|
||||||
|
return
|
||||||
|
if (lastView instanceof Search)
|
||||||
|
lastView.display_invite();
|
||||||
|
}
|
||||||
|
|
||||||
|
async is_asked(asked) {
|
||||||
|
let response = await this.client._get(`/api/chat/ask/${asked}`);
|
||||||
|
|
||||||
|
const statu = response.status;
|
||||||
|
if (statu == 404 || statu == 204)
|
||||||
|
return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
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_id,
|
||||||
|
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};
|
22
django/frontend/static/js/api/chat/Channels.js
Normal file
22
django/frontend/static/js/api/chat/Channels.js
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
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();
|
||||||
|
|
||||||
|
this.channel = new Channel(this.client, data.id, members_id, data.messages, reload);
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue