dockered
This commit is contained in:
0
django/frontend/__init__.py
Normal file
0
django/frontend/__init__.py
Normal file
3
django/frontend/admin.py
Normal file
3
django/frontend/admin.py
Normal file
@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
6
django/frontend/apps.py
Normal file
6
django/frontend/apps.py
Normal file
@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class FrontendConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'frontend'
|
3
django/frontend/models.py
Normal file
3
django/frontend/models.py
Normal file
@ -0,0 +1,3 @@
|
||||
from django.db import models
|
||||
|
||||
# Create your models here.
|
51
django/frontend/static/css/TournamentPage.css
Normal file
51
django/frontend/static/css/TournamentPage.css
Normal file
@ -0,0 +1,51 @@
|
||||
#tournament-tree {
|
||||
display:flex;
|
||||
flex-direction:row;
|
||||
}
|
||||
|
||||
.round {
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
justify-content:center;
|
||||
width:200px;
|
||||
list-style:none;
|
||||
padding:0;
|
||||
}
|
||||
|
||||
.round .spacer{ flex-grow:1; }
|
||||
.round .spacer:first-child,
|
||||
.round .spacer:last-child{ flex-grow:.5; }
|
||||
|
||||
.round .game-spacer{
|
||||
flex-grow:1;
|
||||
}
|
||||
|
||||
body{
|
||||
font-family:sans-serif;
|
||||
font-size:small;
|
||||
padding:10px;
|
||||
line-height:1.4em;
|
||||
}
|
||||
|
||||
li.game{
|
||||
padding-left:20px;
|
||||
}
|
||||
|
||||
li.game.winner{
|
||||
font-weight:bold;
|
||||
}
|
||||
li.game span{
|
||||
float:right;
|
||||
margin-right:5px;
|
||||
}
|
||||
|
||||
li.game-top{ border-bottom:1px solid #aaa; }
|
||||
|
||||
li.game-spacer{
|
||||
border-right:1px solid #aaa;
|
||||
min-height:40px;
|
||||
}
|
||||
|
||||
li.game-bottom{
|
||||
border-top:1px solid #aaa;
|
||||
}
|
4085
django/frontend/static/css/bootstrap/bootstrap-grid.css
vendored
Normal file
4085
django/frontend/static/css/bootstrap/bootstrap-grid.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
6
django/frontend/static/css/bootstrap/bootstrap-grid.min.css
vendored
Normal file
6
django/frontend/static/css/bootstrap/bootstrap-grid.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
4084
django/frontend/static/css/bootstrap/bootstrap-grid.rtl.css
vendored
Normal file
4084
django/frontend/static/css/bootstrap/bootstrap-grid.rtl.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
6
django/frontend/static/css/bootstrap/bootstrap-grid.rtl.min.css
vendored
Normal file
6
django/frontend/static/css/bootstrap/bootstrap-grid.rtl.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
597
django/frontend/static/css/bootstrap/bootstrap-reboot.css
vendored
Normal file
597
django/frontend/static/css/bootstrap/bootstrap-reboot.css
vendored
Normal file
@ -0,0 +1,597 @@
|
||||
/*!
|
||||
* Bootstrap Reboot v5.3.2 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2023 The Bootstrap Authors
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||
*/
|
||||
:root,
|
||||
[data-bs-theme=light] {
|
||||
--bs-blue: #0d6efd;
|
||||
--bs-indigo: #6610f2;
|
||||
--bs-purple: #6f42c1;
|
||||
--bs-pink: #d63384;
|
||||
--bs-red: #dc3545;
|
||||
--bs-orange: #fd7e14;
|
||||
--bs-yellow: #ffc107;
|
||||
--bs-green: #198754;
|
||||
--bs-teal: #20c997;
|
||||
--bs-cyan: #0dcaf0;
|
||||
--bs-black: #000;
|
||||
--bs-white: #fff;
|
||||
--bs-gray: #6c757d;
|
||||
--bs-gray-dark: #343a40;
|
||||
--bs-gray-100: #f8f9fa;
|
||||
--bs-gray-200: #e9ecef;
|
||||
--bs-gray-300: #dee2e6;
|
||||
--bs-gray-400: #ced4da;
|
||||
--bs-gray-500: #adb5bd;
|
||||
--bs-gray-600: #6c757d;
|
||||
--bs-gray-700: #495057;
|
||||
--bs-gray-800: #343a40;
|
||||
--bs-gray-900: #212529;
|
||||
--bs-primary: #0d6efd;
|
||||
--bs-secondary: #6c757d;
|
||||
--bs-success: #198754;
|
||||
--bs-info: #0dcaf0;
|
||||
--bs-warning: #ffc107;
|
||||
--bs-danger: #dc3545;
|
||||
--bs-light: #f8f9fa;
|
||||
--bs-dark: #212529;
|
||||
--bs-primary-rgb: 13, 110, 253;
|
||||
--bs-secondary-rgb: 108, 117, 125;
|
||||
--bs-success-rgb: 25, 135, 84;
|
||||
--bs-info-rgb: 13, 202, 240;
|
||||
--bs-warning-rgb: 255, 193, 7;
|
||||
--bs-danger-rgb: 220, 53, 69;
|
||||
--bs-light-rgb: 248, 249, 250;
|
||||
--bs-dark-rgb: 33, 37, 41;
|
||||
--bs-primary-text-emphasis: #052c65;
|
||||
--bs-secondary-text-emphasis: #2b2f32;
|
||||
--bs-success-text-emphasis: #0a3622;
|
||||
--bs-info-text-emphasis: #055160;
|
||||
--bs-warning-text-emphasis: #664d03;
|
||||
--bs-danger-text-emphasis: #58151c;
|
||||
--bs-light-text-emphasis: #495057;
|
||||
--bs-dark-text-emphasis: #495057;
|
||||
--bs-primary-bg-subtle: #cfe2ff;
|
||||
--bs-secondary-bg-subtle: #e2e3e5;
|
||||
--bs-success-bg-subtle: #d1e7dd;
|
||||
--bs-info-bg-subtle: #cff4fc;
|
||||
--bs-warning-bg-subtle: #fff3cd;
|
||||
--bs-danger-bg-subtle: #f8d7da;
|
||||
--bs-light-bg-subtle: #fcfcfd;
|
||||
--bs-dark-bg-subtle: #ced4da;
|
||||
--bs-primary-border-subtle: #9ec5fe;
|
||||
--bs-secondary-border-subtle: #c4c8cb;
|
||||
--bs-success-border-subtle: #a3cfbb;
|
||||
--bs-info-border-subtle: #9eeaf9;
|
||||
--bs-warning-border-subtle: #ffe69c;
|
||||
--bs-danger-border-subtle: #f1aeb5;
|
||||
--bs-light-border-subtle: #e9ecef;
|
||||
--bs-dark-border-subtle: #adb5bd;
|
||||
--bs-white-rgb: 255, 255, 255;
|
||||
--bs-black-rgb: 0, 0, 0;
|
||||
--bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
|
||||
--bs-body-font-family: var(--bs-font-sans-serif);
|
||||
--bs-body-font-size: 1rem;
|
||||
--bs-body-font-weight: 400;
|
||||
--bs-body-line-height: 1.5;
|
||||
--bs-body-color: #212529;
|
||||
--bs-body-color-rgb: 33, 37, 41;
|
||||
--bs-body-bg: #fff;
|
||||
--bs-body-bg-rgb: 255, 255, 255;
|
||||
--bs-emphasis-color: #000;
|
||||
--bs-emphasis-color-rgb: 0, 0, 0;
|
||||
--bs-secondary-color: rgba(33, 37, 41, 0.75);
|
||||
--bs-secondary-color-rgb: 33, 37, 41;
|
||||
--bs-secondary-bg: #e9ecef;
|
||||
--bs-secondary-bg-rgb: 233, 236, 239;
|
||||
--bs-tertiary-color: rgba(33, 37, 41, 0.5);
|
||||
--bs-tertiary-color-rgb: 33, 37, 41;
|
||||
--bs-tertiary-bg: #f8f9fa;
|
||||
--bs-tertiary-bg-rgb: 248, 249, 250;
|
||||
--bs-heading-color: inherit;
|
||||
--bs-link-color: #0d6efd;
|
||||
--bs-link-color-rgb: 13, 110, 253;
|
||||
--bs-link-decoration: underline;
|
||||
--bs-link-hover-color: #0a58ca;
|
||||
--bs-link-hover-color-rgb: 10, 88, 202;
|
||||
--bs-code-color: #d63384;
|
||||
--bs-highlight-color: #212529;
|
||||
--bs-highlight-bg: #fff3cd;
|
||||
--bs-border-width: 1px;
|
||||
--bs-border-style: solid;
|
||||
--bs-border-color: #dee2e6;
|
||||
--bs-border-color-translucent: rgba(0, 0, 0, 0.175);
|
||||
--bs-border-radius: 0.375rem;
|
||||
--bs-border-radius-sm: 0.25rem;
|
||||
--bs-border-radius-lg: 0.5rem;
|
||||
--bs-border-radius-xl: 1rem;
|
||||
--bs-border-radius-xxl: 2rem;
|
||||
--bs-border-radius-2xl: var(--bs-border-radius-xxl);
|
||||
--bs-border-radius-pill: 50rem;
|
||||
--bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
||||
--bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
||||
--bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175);
|
||||
--bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075);
|
||||
--bs-focus-ring-width: 0.25rem;
|
||||
--bs-focus-ring-opacity: 0.25;
|
||||
--bs-focus-ring-color: rgba(13, 110, 253, 0.25);
|
||||
--bs-form-valid-color: #198754;
|
||||
--bs-form-valid-border-color: #198754;
|
||||
--bs-form-invalid-color: #dc3545;
|
||||
--bs-form-invalid-border-color: #dc3545;
|
||||
}
|
||||
|
||||
[data-bs-theme=dark] {
|
||||
color-scheme: dark;
|
||||
--bs-body-color: #dee2e6;
|
||||
--bs-body-color-rgb: 222, 226, 230;
|
||||
--bs-body-bg: #212529;
|
||||
--bs-body-bg-rgb: 33, 37, 41;
|
||||
--bs-emphasis-color: #fff;
|
||||
--bs-emphasis-color-rgb: 255, 255, 255;
|
||||
--bs-secondary-color: rgba(222, 226, 230, 0.75);
|
||||
--bs-secondary-color-rgb: 222, 226, 230;
|
||||
--bs-secondary-bg: #343a40;
|
||||
--bs-secondary-bg-rgb: 52, 58, 64;
|
||||
--bs-tertiary-color: rgba(222, 226, 230, 0.5);
|
||||
--bs-tertiary-color-rgb: 222, 226, 230;
|
||||
--bs-tertiary-bg: #2b3035;
|
||||
--bs-tertiary-bg-rgb: 43, 48, 53;
|
||||
--bs-primary-text-emphasis: #6ea8fe;
|
||||
--bs-secondary-text-emphasis: #a7acb1;
|
||||
--bs-success-text-emphasis: #75b798;
|
||||
--bs-info-text-emphasis: #6edff6;
|
||||
--bs-warning-text-emphasis: #ffda6a;
|
||||
--bs-danger-text-emphasis: #ea868f;
|
||||
--bs-light-text-emphasis: #f8f9fa;
|
||||
--bs-dark-text-emphasis: #dee2e6;
|
||||
--bs-primary-bg-subtle: #031633;
|
||||
--bs-secondary-bg-subtle: #161719;
|
||||
--bs-success-bg-subtle: #051b11;
|
||||
--bs-info-bg-subtle: #032830;
|
||||
--bs-warning-bg-subtle: #332701;
|
||||
--bs-danger-bg-subtle: #2c0b0e;
|
||||
--bs-light-bg-subtle: #343a40;
|
||||
--bs-dark-bg-subtle: #1a1d20;
|
||||
--bs-primary-border-subtle: #084298;
|
||||
--bs-secondary-border-subtle: #41464b;
|
||||
--bs-success-border-subtle: #0f5132;
|
||||
--bs-info-border-subtle: #087990;
|
||||
--bs-warning-border-subtle: #997404;
|
||||
--bs-danger-border-subtle: #842029;
|
||||
--bs-light-border-subtle: #495057;
|
||||
--bs-dark-border-subtle: #343a40;
|
||||
--bs-heading-color: inherit;
|
||||
--bs-link-color: #6ea8fe;
|
||||
--bs-link-hover-color: #8bb9fe;
|
||||
--bs-link-color-rgb: 110, 168, 254;
|
||||
--bs-link-hover-color-rgb: 139, 185, 254;
|
||||
--bs-code-color: #e685b5;
|
||||
--bs-highlight-color: #dee2e6;
|
||||
--bs-highlight-bg: #664d03;
|
||||
--bs-border-color: #495057;
|
||||
--bs-border-color-translucent: rgba(255, 255, 255, 0.15);
|
||||
--bs-form-valid-color: #75b798;
|
||||
--bs-form-valid-border-color: #75b798;
|
||||
--bs-form-invalid-color: #ea868f;
|
||||
--bs-form-invalid-border-color: #ea868f;
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
:root {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: var(--bs-body-font-family);
|
||||
font-size: var(--bs-body-font-size);
|
||||
font-weight: var(--bs-body-font-weight);
|
||||
line-height: var(--bs-body-line-height);
|
||||
color: var(--bs-body-color);
|
||||
text-align: var(--bs-body-text-align);
|
||||
background-color: var(--bs-body-bg);
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: 1rem 0;
|
||||
color: inherit;
|
||||
border: 0;
|
||||
border-top: var(--bs-border-width) solid;
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
||||
h6, h5, h4, h3, h2, h1 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
color: var(--bs-heading-color);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: calc(1.375rem + 1.5vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h1 {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: calc(1.325rem + 0.9vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h2 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: calc(1.3rem + 0.6vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h3 {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: calc(1.275rem + 0.3vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h4 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
abbr[title] {
|
||||
-webkit-text-decoration: underline dotted;
|
||||
text-decoration: underline dotted;
|
||||
cursor: help;
|
||||
-webkit-text-decoration-skip-ink: none;
|
||||
text-decoration-skip-ink: none;
|
||||
}
|
||||
|
||||
address {
|
||||
margin-bottom: 1rem;
|
||||
font-style: normal;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul {
|
||||
padding-left: 2rem;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul,
|
||||
dl {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
ol ol,
|
||||
ul ul,
|
||||
ol ul,
|
||||
ul ol {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
dt {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin-bottom: 0.5rem;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
small {
|
||||
font-size: 0.875em;
|
||||
}
|
||||
|
||||
mark {
|
||||
padding: 0.1875em;
|
||||
color: var(--bs-highlight-color);
|
||||
background-color: var(--bs-highlight-bg);
|
||||
}
|
||||
|
||||
sub,
|
||||
sup {
|
||||
position: relative;
|
||||
font-size: 0.75em;
|
||||
line-height: 0;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
a {
|
||||
color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));
|
||||
text-decoration: underline;
|
||||
}
|
||||
a:hover {
|
||||
--bs-link-color-rgb: var(--bs-link-hover-color-rgb);
|
||||
}
|
||||
|
||||
a:not([href]):not([class]), a:not([href]):not([class]):hover {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
pre,
|
||||
code,
|
||||
kbd,
|
||||
samp {
|
||||
font-family: var(--bs-font-monospace);
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
pre {
|
||||
display: block;
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
overflow: auto;
|
||||
font-size: 0.875em;
|
||||
}
|
||||
pre code {
|
||||
font-size: inherit;
|
||||
color: inherit;
|
||||
word-break: normal;
|
||||
}
|
||||
|
||||
code {
|
||||
font-size: 0.875em;
|
||||
color: var(--bs-code-color);
|
||||
word-wrap: break-word;
|
||||
}
|
||||
a > code {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
kbd {
|
||||
padding: 0.1875rem 0.375rem;
|
||||
font-size: 0.875em;
|
||||
color: var(--bs-body-bg);
|
||||
background-color: var(--bs-body-color);
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
kbd kbd {
|
||||
padding: 0;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
figure {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
img,
|
||||
svg {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
table {
|
||||
caption-side: bottom;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
caption {
|
||||
padding-top: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
color: var(--bs-secondary-color);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: inherit;
|
||||
text-align: -webkit-match-parent;
|
||||
}
|
||||
|
||||
thead,
|
||||
tbody,
|
||||
tfoot,
|
||||
tr,
|
||||
td,
|
||||
th {
|
||||
border-color: inherit;
|
||||
border-style: solid;
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
button:focus:not(:focus-visible) {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
input,
|
||||
button,
|
||||
select,
|
||||
optgroup,
|
||||
textarea {
|
||||
margin: 0;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
button,
|
||||
select {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
[role=button] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
select {
|
||||
word-wrap: normal;
|
||||
}
|
||||
select:disabled {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
button,
|
||||
[type=button],
|
||||
[type=reset],
|
||||
[type=submit] {
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
button:not(:disabled),
|
||||
[type=button]:not(:disabled),
|
||||
[type=reset]:not(:disabled),
|
||||
[type=submit]:not(:disabled) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
::-moz-focus-inner {
|
||||
padding: 0;
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
legend {
|
||||
float: left;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: calc(1.275rem + 0.3vw);
|
||||
line-height: inherit;
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
legend {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
legend + * {
|
||||
clear: left;
|
||||
}
|
||||
|
||||
::-webkit-datetime-edit-fields-wrapper,
|
||||
::-webkit-datetime-edit-text,
|
||||
::-webkit-datetime-edit-minute,
|
||||
::-webkit-datetime-edit-hour-field,
|
||||
::-webkit-datetime-edit-day-field,
|
||||
::-webkit-datetime-edit-month-field,
|
||||
::-webkit-datetime-edit-year-field {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
::-webkit-inner-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
[type=search] {
|
||||
-webkit-appearance: textfield;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
/* rtl:raw:
|
||||
[type="tel"],
|
||||
[type="url"],
|
||||
[type="email"],
|
||||
[type="number"] {
|
||||
direction: ltr;
|
||||
}
|
||||
*/
|
||||
::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
::-webkit-color-swatch-wrapper {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
::-webkit-file-upload-button {
|
||||
font: inherit;
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
::file-selector-button {
|
||||
font: inherit;
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
output {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
iframe {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/*# sourceMappingURL=bootstrap-reboot.css.map */
|
File diff suppressed because one or more lines are too long
6
django/frontend/static/css/bootstrap/bootstrap-reboot.min.css
vendored
Normal file
6
django/frontend/static/css/bootstrap/bootstrap-reboot.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
594
django/frontend/static/css/bootstrap/bootstrap-reboot.rtl.css
vendored
Normal file
594
django/frontend/static/css/bootstrap/bootstrap-reboot.rtl.css
vendored
Normal file
@ -0,0 +1,594 @@
|
||||
/*!
|
||||
* Bootstrap Reboot v5.3.2 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2023 The Bootstrap Authors
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||
*/
|
||||
:root,
|
||||
[data-bs-theme=light] {
|
||||
--bs-blue: #0d6efd;
|
||||
--bs-indigo: #6610f2;
|
||||
--bs-purple: #6f42c1;
|
||||
--bs-pink: #d63384;
|
||||
--bs-red: #dc3545;
|
||||
--bs-orange: #fd7e14;
|
||||
--bs-yellow: #ffc107;
|
||||
--bs-green: #198754;
|
||||
--bs-teal: #20c997;
|
||||
--bs-cyan: #0dcaf0;
|
||||
--bs-black: #000;
|
||||
--bs-white: #fff;
|
||||
--bs-gray: #6c757d;
|
||||
--bs-gray-dark: #343a40;
|
||||
--bs-gray-100: #f8f9fa;
|
||||
--bs-gray-200: #e9ecef;
|
||||
--bs-gray-300: #dee2e6;
|
||||
--bs-gray-400: #ced4da;
|
||||
--bs-gray-500: #adb5bd;
|
||||
--bs-gray-600: #6c757d;
|
||||
--bs-gray-700: #495057;
|
||||
--bs-gray-800: #343a40;
|
||||
--bs-gray-900: #212529;
|
||||
--bs-primary: #0d6efd;
|
||||
--bs-secondary: #6c757d;
|
||||
--bs-success: #198754;
|
||||
--bs-info: #0dcaf0;
|
||||
--bs-warning: #ffc107;
|
||||
--bs-danger: #dc3545;
|
||||
--bs-light: #f8f9fa;
|
||||
--bs-dark: #212529;
|
||||
--bs-primary-rgb: 13, 110, 253;
|
||||
--bs-secondary-rgb: 108, 117, 125;
|
||||
--bs-success-rgb: 25, 135, 84;
|
||||
--bs-info-rgb: 13, 202, 240;
|
||||
--bs-warning-rgb: 255, 193, 7;
|
||||
--bs-danger-rgb: 220, 53, 69;
|
||||
--bs-light-rgb: 248, 249, 250;
|
||||
--bs-dark-rgb: 33, 37, 41;
|
||||
--bs-primary-text-emphasis: #052c65;
|
||||
--bs-secondary-text-emphasis: #2b2f32;
|
||||
--bs-success-text-emphasis: #0a3622;
|
||||
--bs-info-text-emphasis: #055160;
|
||||
--bs-warning-text-emphasis: #664d03;
|
||||
--bs-danger-text-emphasis: #58151c;
|
||||
--bs-light-text-emphasis: #495057;
|
||||
--bs-dark-text-emphasis: #495057;
|
||||
--bs-primary-bg-subtle: #cfe2ff;
|
||||
--bs-secondary-bg-subtle: #e2e3e5;
|
||||
--bs-success-bg-subtle: #d1e7dd;
|
||||
--bs-info-bg-subtle: #cff4fc;
|
||||
--bs-warning-bg-subtle: #fff3cd;
|
||||
--bs-danger-bg-subtle: #f8d7da;
|
||||
--bs-light-bg-subtle: #fcfcfd;
|
||||
--bs-dark-bg-subtle: #ced4da;
|
||||
--bs-primary-border-subtle: #9ec5fe;
|
||||
--bs-secondary-border-subtle: #c4c8cb;
|
||||
--bs-success-border-subtle: #a3cfbb;
|
||||
--bs-info-border-subtle: #9eeaf9;
|
||||
--bs-warning-border-subtle: #ffe69c;
|
||||
--bs-danger-border-subtle: #f1aeb5;
|
||||
--bs-light-border-subtle: #e9ecef;
|
||||
--bs-dark-border-subtle: #adb5bd;
|
||||
--bs-white-rgb: 255, 255, 255;
|
||||
--bs-black-rgb: 0, 0, 0;
|
||||
--bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
|
||||
--bs-body-font-family: var(--bs-font-sans-serif);
|
||||
--bs-body-font-size: 1rem;
|
||||
--bs-body-font-weight: 400;
|
||||
--bs-body-line-height: 1.5;
|
||||
--bs-body-color: #212529;
|
||||
--bs-body-color-rgb: 33, 37, 41;
|
||||
--bs-body-bg: #fff;
|
||||
--bs-body-bg-rgb: 255, 255, 255;
|
||||
--bs-emphasis-color: #000;
|
||||
--bs-emphasis-color-rgb: 0, 0, 0;
|
||||
--bs-secondary-color: rgba(33, 37, 41, 0.75);
|
||||
--bs-secondary-color-rgb: 33, 37, 41;
|
||||
--bs-secondary-bg: #e9ecef;
|
||||
--bs-secondary-bg-rgb: 233, 236, 239;
|
||||
--bs-tertiary-color: rgba(33, 37, 41, 0.5);
|
||||
--bs-tertiary-color-rgb: 33, 37, 41;
|
||||
--bs-tertiary-bg: #f8f9fa;
|
||||
--bs-tertiary-bg-rgb: 248, 249, 250;
|
||||
--bs-heading-color: inherit;
|
||||
--bs-link-color: #0d6efd;
|
||||
--bs-link-color-rgb: 13, 110, 253;
|
||||
--bs-link-decoration: underline;
|
||||
--bs-link-hover-color: #0a58ca;
|
||||
--bs-link-hover-color-rgb: 10, 88, 202;
|
||||
--bs-code-color: #d63384;
|
||||
--bs-highlight-color: #212529;
|
||||
--bs-highlight-bg: #fff3cd;
|
||||
--bs-border-width: 1px;
|
||||
--bs-border-style: solid;
|
||||
--bs-border-color: #dee2e6;
|
||||
--bs-border-color-translucent: rgba(0, 0, 0, 0.175);
|
||||
--bs-border-radius: 0.375rem;
|
||||
--bs-border-radius-sm: 0.25rem;
|
||||
--bs-border-radius-lg: 0.5rem;
|
||||
--bs-border-radius-xl: 1rem;
|
||||
--bs-border-radius-xxl: 2rem;
|
||||
--bs-border-radius-2xl: var(--bs-border-radius-xxl);
|
||||
--bs-border-radius-pill: 50rem;
|
||||
--bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
||||
--bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
||||
--bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175);
|
||||
--bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075);
|
||||
--bs-focus-ring-width: 0.25rem;
|
||||
--bs-focus-ring-opacity: 0.25;
|
||||
--bs-focus-ring-color: rgba(13, 110, 253, 0.25);
|
||||
--bs-form-valid-color: #198754;
|
||||
--bs-form-valid-border-color: #198754;
|
||||
--bs-form-invalid-color: #dc3545;
|
||||
--bs-form-invalid-border-color: #dc3545;
|
||||
}
|
||||
|
||||
[data-bs-theme=dark] {
|
||||
color-scheme: dark;
|
||||
--bs-body-color: #dee2e6;
|
||||
--bs-body-color-rgb: 222, 226, 230;
|
||||
--bs-body-bg: #212529;
|
||||
--bs-body-bg-rgb: 33, 37, 41;
|
||||
--bs-emphasis-color: #fff;
|
||||
--bs-emphasis-color-rgb: 255, 255, 255;
|
||||
--bs-secondary-color: rgba(222, 226, 230, 0.75);
|
||||
--bs-secondary-color-rgb: 222, 226, 230;
|
||||
--bs-secondary-bg: #343a40;
|
||||
--bs-secondary-bg-rgb: 52, 58, 64;
|
||||
--bs-tertiary-color: rgba(222, 226, 230, 0.5);
|
||||
--bs-tertiary-color-rgb: 222, 226, 230;
|
||||
--bs-tertiary-bg: #2b3035;
|
||||
--bs-tertiary-bg-rgb: 43, 48, 53;
|
||||
--bs-primary-text-emphasis: #6ea8fe;
|
||||
--bs-secondary-text-emphasis: #a7acb1;
|
||||
--bs-success-text-emphasis: #75b798;
|
||||
--bs-info-text-emphasis: #6edff6;
|
||||
--bs-warning-text-emphasis: #ffda6a;
|
||||
--bs-danger-text-emphasis: #ea868f;
|
||||
--bs-light-text-emphasis: #f8f9fa;
|
||||
--bs-dark-text-emphasis: #dee2e6;
|
||||
--bs-primary-bg-subtle: #031633;
|
||||
--bs-secondary-bg-subtle: #161719;
|
||||
--bs-success-bg-subtle: #051b11;
|
||||
--bs-info-bg-subtle: #032830;
|
||||
--bs-warning-bg-subtle: #332701;
|
||||
--bs-danger-bg-subtle: #2c0b0e;
|
||||
--bs-light-bg-subtle: #343a40;
|
||||
--bs-dark-bg-subtle: #1a1d20;
|
||||
--bs-primary-border-subtle: #084298;
|
||||
--bs-secondary-border-subtle: #41464b;
|
||||
--bs-success-border-subtle: #0f5132;
|
||||
--bs-info-border-subtle: #087990;
|
||||
--bs-warning-border-subtle: #997404;
|
||||
--bs-danger-border-subtle: #842029;
|
||||
--bs-light-border-subtle: #495057;
|
||||
--bs-dark-border-subtle: #343a40;
|
||||
--bs-heading-color: inherit;
|
||||
--bs-link-color: #6ea8fe;
|
||||
--bs-link-hover-color: #8bb9fe;
|
||||
--bs-link-color-rgb: 110, 168, 254;
|
||||
--bs-link-hover-color-rgb: 139, 185, 254;
|
||||
--bs-code-color: #e685b5;
|
||||
--bs-highlight-color: #dee2e6;
|
||||
--bs-highlight-bg: #664d03;
|
||||
--bs-border-color: #495057;
|
||||
--bs-border-color-translucent: rgba(255, 255, 255, 0.15);
|
||||
--bs-form-valid-color: #75b798;
|
||||
--bs-form-valid-border-color: #75b798;
|
||||
--bs-form-invalid-color: #ea868f;
|
||||
--bs-form-invalid-border-color: #ea868f;
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
:root {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: var(--bs-body-font-family);
|
||||
font-size: var(--bs-body-font-size);
|
||||
font-weight: var(--bs-body-font-weight);
|
||||
line-height: var(--bs-body-line-height);
|
||||
color: var(--bs-body-color);
|
||||
text-align: var(--bs-body-text-align);
|
||||
background-color: var(--bs-body-bg);
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: 1rem 0;
|
||||
color: inherit;
|
||||
border: 0;
|
||||
border-top: var(--bs-border-width) solid;
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
||||
h6, h5, h4, h3, h2, h1 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
color: var(--bs-heading-color);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: calc(1.375rem + 1.5vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h1 {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: calc(1.325rem + 0.9vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h2 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: calc(1.3rem + 0.6vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h3 {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: calc(1.275rem + 0.3vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h4 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
abbr[title] {
|
||||
-webkit-text-decoration: underline dotted;
|
||||
text-decoration: underline dotted;
|
||||
cursor: help;
|
||||
-webkit-text-decoration-skip-ink: none;
|
||||
text-decoration-skip-ink: none;
|
||||
}
|
||||
|
||||
address {
|
||||
margin-bottom: 1rem;
|
||||
font-style: normal;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul {
|
||||
padding-right: 2rem;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul,
|
||||
dl {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
ol ol,
|
||||
ul ul,
|
||||
ol ul,
|
||||
ul ol {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
dt {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin-bottom: 0.5rem;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
small {
|
||||
font-size: 0.875em;
|
||||
}
|
||||
|
||||
mark {
|
||||
padding: 0.1875em;
|
||||
color: var(--bs-highlight-color);
|
||||
background-color: var(--bs-highlight-bg);
|
||||
}
|
||||
|
||||
sub,
|
||||
sup {
|
||||
position: relative;
|
||||
font-size: 0.75em;
|
||||
line-height: 0;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
a {
|
||||
color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));
|
||||
text-decoration: underline;
|
||||
}
|
||||
a:hover {
|
||||
--bs-link-color-rgb: var(--bs-link-hover-color-rgb);
|
||||
}
|
||||
|
||||
a:not([href]):not([class]), a:not([href]):not([class]):hover {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
pre,
|
||||
code,
|
||||
kbd,
|
||||
samp {
|
||||
font-family: var(--bs-font-monospace);
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
pre {
|
||||
display: block;
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
overflow: auto;
|
||||
font-size: 0.875em;
|
||||
}
|
||||
pre code {
|
||||
font-size: inherit;
|
||||
color: inherit;
|
||||
word-break: normal;
|
||||
}
|
||||
|
||||
code {
|
||||
font-size: 0.875em;
|
||||
color: var(--bs-code-color);
|
||||
word-wrap: break-word;
|
||||
}
|
||||
a > code {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
kbd {
|
||||
padding: 0.1875rem 0.375rem;
|
||||
font-size: 0.875em;
|
||||
color: var(--bs-body-bg);
|
||||
background-color: var(--bs-body-color);
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
kbd kbd {
|
||||
padding: 0;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
figure {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
img,
|
||||
svg {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
table {
|
||||
caption-side: bottom;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
caption {
|
||||
padding-top: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
color: var(--bs-secondary-color);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: inherit;
|
||||
text-align: -webkit-match-parent;
|
||||
}
|
||||
|
||||
thead,
|
||||
tbody,
|
||||
tfoot,
|
||||
tr,
|
||||
td,
|
||||
th {
|
||||
border-color: inherit;
|
||||
border-style: solid;
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
button:focus:not(:focus-visible) {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
input,
|
||||
button,
|
||||
select,
|
||||
optgroup,
|
||||
textarea {
|
||||
margin: 0;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
button,
|
||||
select {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
[role=button] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
select {
|
||||
word-wrap: normal;
|
||||
}
|
||||
select:disabled {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
button,
|
||||
[type=button],
|
||||
[type=reset],
|
||||
[type=submit] {
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
button:not(:disabled),
|
||||
[type=button]:not(:disabled),
|
||||
[type=reset]:not(:disabled),
|
||||
[type=submit]:not(:disabled) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
::-moz-focus-inner {
|
||||
padding: 0;
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
legend {
|
||||
float: right;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: calc(1.275rem + 0.3vw);
|
||||
line-height: inherit;
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
legend {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
legend + * {
|
||||
clear: right;
|
||||
}
|
||||
|
||||
::-webkit-datetime-edit-fields-wrapper,
|
||||
::-webkit-datetime-edit-text,
|
||||
::-webkit-datetime-edit-minute,
|
||||
::-webkit-datetime-edit-hour-field,
|
||||
::-webkit-datetime-edit-day-field,
|
||||
::-webkit-datetime-edit-month-field,
|
||||
::-webkit-datetime-edit-year-field {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
::-webkit-inner-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
[type=search] {
|
||||
-webkit-appearance: textfield;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
[type="tel"],
|
||||
[type="url"],
|
||||
[type="email"],
|
||||
[type="number"] {
|
||||
direction: ltr;
|
||||
}
|
||||
::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
::-webkit-color-swatch-wrapper {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
::-webkit-file-upload-button {
|
||||
font: inherit;
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
::file-selector-button {
|
||||
font: inherit;
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
output {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
iframe {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
/*# sourceMappingURL=bootstrap-reboot.rtl.css.map */
|
File diff suppressed because one or more lines are too long
6
django/frontend/static/css/bootstrap/bootstrap-reboot.rtl.min.css
vendored
Normal file
6
django/frontend/static/css/bootstrap/bootstrap-reboot.rtl.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
5402
django/frontend/static/css/bootstrap/bootstrap-utilities.css
vendored
Normal file
5402
django/frontend/static/css/bootstrap/bootstrap-utilities.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
6
django/frontend/static/css/bootstrap/bootstrap-utilities.min.css
vendored
Normal file
6
django/frontend/static/css/bootstrap/bootstrap-utilities.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
5393
django/frontend/static/css/bootstrap/bootstrap-utilities.rtl.css
vendored
Normal file
5393
django/frontend/static/css/bootstrap/bootstrap-utilities.rtl.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
6
django/frontend/static/css/bootstrap/bootstrap-utilities.rtl.min.css
vendored
Normal file
6
django/frontend/static/css/bootstrap/bootstrap-utilities.rtl.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
12068
django/frontend/static/css/bootstrap/bootstrap.css
vendored
Normal file
12068
django/frontend/static/css/bootstrap/bootstrap.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
django/frontend/static/css/bootstrap/bootstrap.css.map
Normal file
1
django/frontend/static/css/bootstrap/bootstrap.css.map
Normal file
File diff suppressed because one or more lines are too long
6
django/frontend/static/css/bootstrap/bootstrap.min.css
vendored
Normal file
6
django/frontend/static/css/bootstrap/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
12032
django/frontend/static/css/bootstrap/bootstrap.rtl.css
vendored
Normal file
12032
django/frontend/static/css/bootstrap/bootstrap.rtl.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
6
django/frontend/static/css/bootstrap/bootstrap.rtl.min.css
vendored
Normal file
6
django/frontend/static/css/bootstrap/bootstrap.rtl.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
8
django/frontend/static/css/game.css
Normal file
8
django/frontend/static/css/game.css
Normal file
@ -0,0 +1,8 @@
|
||||
#canva {
|
||||
background-color: white;
|
||||
border: 1px;
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
15
django/frontend/static/css/gameHistory.css
Normal file
15
django/frontend/static/css/gameHistory.css
Normal file
@ -0,0 +1,15 @@
|
||||
|
||||
#game-list {
|
||||
justify-content: flex-start;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
#game-list .game-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 160px;
|
||||
width: 160px;
|
||||
margin: 10px;
|
||||
border-radius: 5%;
|
||||
}
|
23
django/frontend/static/css/gameOffline.css
Normal file
23
django/frontend/static/css/gameOffline.css
Normal file
@ -0,0 +1,23 @@
|
||||
#gameCanvas {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#up1:active, #down1:active, #up2:active, #down2:active {
|
||||
color: red;
|
||||
}
|
||||
|
||||
#up1, #down1, #up2, #down2 {
|
||||
min-height: 60px;
|
||||
min-width: 60px;
|
||||
|
||||
font-size: 40px;
|
||||
}
|
||||
|
||||
#up1, #down1 {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#up2, #down2 {
|
||||
position: relative;
|
||||
left: calc(420px - (60px * 3));
|
||||
}
|
23
django/frontend/static/css/index.css
Normal file
23
django/frontend/static/css/index.css
Normal file
@ -0,0 +1,23 @@
|
||||
#popup {
|
||||
position: fixed;
|
||||
font-size: 1.2em;
|
||||
z-index: 1; /* foreground */
|
||||
|
||||
top:calc(1% + 0.1em);
|
||||
left:50%;
|
||||
transform: translate(-50%, 50%);
|
||||
|
||||
border: 1em solid #1a1a1a;
|
||||
color: #1a1a1a;
|
||||
background-color: #cccccc;
|
||||
|
||||
padding: 5px;
|
||||
border-width: 0.1em;
|
||||
|
||||
opacity: 0;
|
||||
transition: opacity 0.25s;
|
||||
}
|
||||
|
||||
#languageSelector > .dropdown-item.active {
|
||||
background-color: transparent;
|
||||
}
|
25
django/frontend/static/css/profile.css
Normal file
25
django/frontend/static/css/profile.css
Normal file
@ -0,0 +1,25 @@
|
||||
#app * {
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
#app #username
|
||||
{
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
#app #block, #app #friend {
|
||||
cursor: pointer;
|
||||
font-size: 0.7em;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
#app {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
#app #yes, #app #no {
|
||||
display:inline;
|
||||
cursor: pointer;
|
||||
font-size: 0.7em;
|
||||
text-decoration: underline;
|
||||
}
|
134
django/frontend/static/css/search.css
Normal file
134
django/frontend/static/css/search.css
Normal file
@ -0,0 +1,134 @@
|
||||
#app * {
|
||||
font-size: 40px;
|
||||
}
|
||||
|
||||
#app img
|
||||
|
||||
{
|
||||
max-height: 4em;
|
||||
max-width: 4em;
|
||||
min-height: 2em;
|
||||
min-width: 2em;
|
||||
}
|
||||
|
||||
#app ul
|
||||
{
|
||||
font-size: 0.75em;
|
||||
margin: 0.25em 0 0 0;
|
||||
padding: 0 0 0 0;
|
||||
list-style-type: none;
|
||||
max-height: 80vh;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
#app li
|
||||
{
|
||||
margin: 0.25em 0.25em 0 0;
|
||||
}
|
||||
|
||||
#app #chats {
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
text-align: left;
|
||||
}
|
||||
#app #users {
|
||||
margin: 0em 1.0em 0em 0.05em;
|
||||
}
|
||||
|
||||
#app #chat {
|
||||
position: relative;
|
||||
max-height: 100vh;
|
||||
width: 100vh;
|
||||
/*border: 2px solid green;*/
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#app #members {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
#app #add_chat_off {
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#app #add_chat_on {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#app #messages {
|
||||
max-height: 60vh;
|
||||
overflow: scroll;
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
font-size: 0.75em;
|
||||
}
|
||||
|
||||
#app #input_user{
|
||||
color: green;
|
||||
width: 8.5em;
|
||||
height: 1.1em;
|
||||
font-size: 0.65em;
|
||||
border: none;
|
||||
outline: none;
|
||||
border-bottom: 0.15em solid green;
|
||||
}
|
||||
|
||||
#app #input_chat{
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
/*width: calc(100% - 8px);*/
|
||||
width: 100%;
|
||||
border: none;
|
||||
outline: none;
|
||||
border-bottom: 0.15em solid green;
|
||||
color: green;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
#app #you {
|
||||
text-align: left;
|
||||
position: relative;
|
||||
max-width: 48%;
|
||||
left: 0.5em;
|
||||
margin: 0.5em 0 0 0;
|
||||
color: green;
|
||||
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
#app #other {
|
||||
text-align: right;
|
||||
position: relative;
|
||||
max-width: 48%;
|
||||
margin: 0.5em 0 0 auto;
|
||||
right: 0.5em;
|
||||
color: red;
|
||||
|
||||
/* permet le retour à la ligne à la place de dépasser*/
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
#app #invite, #app #yes, #app #no {
|
||||
position: relative;
|
||||
border: none;
|
||||
color: white;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
font-size: 0.8em;
|
||||
height: 2em;
|
||||
width: 4em;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#app #yes, #app #no {
|
||||
position: relative;
|
||||
border: none;
|
||||
color: white;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
font-size: 0.8em;
|
||||
height: 2em;
|
||||
width: 2em;
|
||||
cursor: pointer;
|
||||
}
|
14
django/frontend/static/css/tictactoe.css
Normal file
14
django/frontend/static/css/tictactoe.css
Normal file
@ -0,0 +1,14 @@
|
||||
#canva {
|
||||
width: 510px;
|
||||
height:510px;
|
||||
margin: 0px auto;
|
||||
}
|
||||
|
||||
#Morpion {
|
||||
margin: 0px auto;
|
||||
}
|
||||
|
||||
#rule {
|
||||
text-align: center;
|
||||
margin: 0px auto;
|
||||
}
|
69
django/frontend/static/js/3D/buffers.js
Normal file
69
django/frontend/static/js/3D/buffers.js
Normal file
@ -0,0 +1,69 @@
|
||||
function initBuffers(gl)
|
||||
{
|
||||
const vertexBuffer = initVertexBuffer(gl);
|
||||
const indexBuffer = initIndexBuffer(gl);
|
||||
const normalBuffer = initNormalBuffer(gl);
|
||||
return { vertex: vertexBuffer, index : indexBuffer, normal: normalBuffer };
|
||||
}
|
||||
|
||||
function initVertexBuffer(gl)
|
||||
{
|
||||
const positionBuffer = gl.createBuffer();
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
|
||||
const positions = [
|
||||
// Front face
|
||||
-1.0, -1.0, 1.0, 1.0, -1.0, 1.0, 1.0, 1.0, 1.0, -1.0, 1.0, 1.0,
|
||||
// Back face
|
||||
-1.0, -1.0, -1.0, -1.0, 1.0, -1.0, 1.0, 1.0, -1.0, 1.0, -1.0, -1.0,
|
||||
// Top face
|
||||
-1.0, 1.0, -1.0, -1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, -1.0,
|
||||
// Bottom face
|
||||
-1.0, -1.0, -1.0, 1.0, -1.0, -1.0, 1.0, -1.0, 1.0, -1.0, -1.0, 1.0,
|
||||
// Right face
|
||||
1.0, -1.0, -1.0, 1.0, 1.0, -1.0, 1.0, 1.0, 1.0, 1.0, -1.0, 1.0,
|
||||
// Left face
|
||||
-1.0, -1.0, -1.0, -1.0, -1.0, 1.0, -1.0, 1.0, 1.0, -1.0, 1.0, -1.0
|
||||
];
|
||||
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
|
||||
return positionBuffer;
|
||||
}
|
||||
|
||||
function initNormalBuffer(gl)
|
||||
{
|
||||
const normalBuffer = gl.createBuffer();
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, normalBuffer);
|
||||
const vertexNormals = [
|
||||
// Front
|
||||
0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0,
|
||||
// Back
|
||||
0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0,
|
||||
// Top
|
||||
0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0,
|
||||
// Bottom
|
||||
0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0,
|
||||
// Right
|
||||
1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0,
|
||||
// Left
|
||||
-1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0,
|
||||
];
|
||||
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertexNormals), gl.STATIC_DRAW);
|
||||
return normalBuffer;
|
||||
}
|
||||
|
||||
function initIndexBuffer(gl)
|
||||
{
|
||||
const indexBuffer = gl.createBuffer();
|
||||
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
|
||||
const indices = [
|
||||
0, 1, 2, 0, 2, 3, // front
|
||||
4, 5, 6, 4, 6, 7, // back
|
||||
8, 9, 10, 8, 10, 11, // top
|
||||
12, 13, 14, 12, 14, 15, // bottom
|
||||
16, 17, 18, 16, 18, 19, // right
|
||||
20, 21, 22, 20, 22, 23, // left
|
||||
];
|
||||
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW);
|
||||
return indexBuffer;
|
||||
}
|
||||
|
||||
export { initBuffers };
|
42
django/frontend/static/js/3D/cube.js
Normal file
42
django/frontend/static/js/3D/cube.js
Normal file
@ -0,0 +1,42 @@
|
||||
import { shaderInfos } from "../3D/shaders.js"
|
||||
|
||||
function renderCube(ctx, x, y, z, angle = 0, sx = 1, sy = 1, sz = 1)
|
||||
{
|
||||
const modelMatrix = mat4.create();
|
||||
|
||||
mat4.translate(
|
||||
modelMatrix,
|
||||
modelMatrix,
|
||||
[x, y, z]
|
||||
);
|
||||
|
||||
mat4.rotate(
|
||||
modelMatrix,
|
||||
modelMatrix,
|
||||
angle,
|
||||
[0, 1, 0],
|
||||
);
|
||||
|
||||
mat4.scale(
|
||||
modelMatrix,
|
||||
modelMatrix,
|
||||
[sx, sy, sz]
|
||||
);
|
||||
|
||||
mat4.translate(
|
||||
modelMatrix,
|
||||
modelMatrix,
|
||||
[-1, 0, 0] // wtf, this works ?
|
||||
);
|
||||
|
||||
const normalMatrix = mat4.create();
|
||||
mat4.invert(normalMatrix, modelMatrix);
|
||||
mat4.transpose(normalMatrix, normalMatrix);
|
||||
|
||||
ctx.uniformMatrix4fv(shaderInfos.uniformLocations.modelMatrix, false, modelMatrix);
|
||||
ctx.uniformMatrix4fv(shaderInfos.uniformLocations.normalMatrix, false, normalMatrix);
|
||||
|
||||
ctx.drawElements(ctx.TRIANGLES, 36, ctx.UNSIGNED_SHORT, 0);
|
||||
}
|
||||
|
||||
export { renderCube };
|
28
django/frontend/static/js/3D/maths/gl-matrix-min.js
vendored
Normal file
28
django/frontend/static/js/3D/maths/gl-matrix-min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
87
django/frontend/static/js/3D/shaders.js
Normal file
87
django/frontend/static/js/3D/shaders.js
Normal file
@ -0,0 +1,87 @@
|
||||
const vertex_shader_source = `
|
||||
attribute vec4 aPos;
|
||||
attribute vec3 aNormal;
|
||||
|
||||
uniform mat4 uMod;
|
||||
uniform mat4 uView;
|
||||
uniform mat4 uProj;
|
||||
uniform mat4 uNormalMat;
|
||||
|
||||
varying highp vec3 vLighting;
|
||||
|
||||
void main()
|
||||
{
|
||||
gl_Position = uProj * uView * uMod * aPos;
|
||||
|
||||
highp vec3 ambientLight = vec3(0.3, 0.3, 0.3);
|
||||
highp vec3 directionalLightColor = vec3(1, 1, 1);
|
||||
highp vec3 directionalVector = vec3(-10, 2, -10);
|
||||
|
||||
highp vec4 transformedNormal = uNormalMat * vec4(aNormal, 1.0);
|
||||
|
||||
highp float directional = max(dot(transformedNormal.xyz, directionalVector), 0.0);
|
||||
vLighting = ambientLight + (directionalLightColor * directional);
|
||||
}
|
||||
`;
|
||||
|
||||
const fragment_shader_source = `
|
||||
varying highp vec3 vLighting;
|
||||
|
||||
void main()
|
||||
{
|
||||
highp vec3 color = vec3(1.0, 1.0, 1.0);
|
||||
gl_FragColor = vec4(color * vLighting, 1.0);
|
||||
}
|
||||
`;
|
||||
|
||||
export function initShaderProgram(gl)
|
||||
{
|
||||
const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vertex_shader_source);
|
||||
const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fragment_shader_source);
|
||||
|
||||
const prog = gl.createProgram();
|
||||
gl.attachShader(prog, vertexShader);
|
||||
gl.attachShader(prog, fragmentShader);
|
||||
gl.linkProgram(prog);
|
||||
|
||||
shaderInfos = {
|
||||
program: prog,
|
||||
attribLocations: {
|
||||
vertexPosition: gl.getAttribLocation(prog, "aPos"),
|
||||
vertexNormal: gl.getAttribLocation(prog, "aNormal"),
|
||||
},
|
||||
uniformLocations: {
|
||||
projectionMatrix: gl.getUniformLocation(prog, "uProj"),
|
||||
modelMatrix: gl.getUniformLocation(prog, "uMod"),
|
||||
viewMatrix: gl.getUniformLocation(prog, "uView"),
|
||||
normalMatrix: gl.getUniformLocation(prog, "uNormalMat"),
|
||||
},
|
||||
};
|
||||
|
||||
if(!gl.getProgramParameter(prog, gl.LINK_STATUS))
|
||||
{
|
||||
alert(`Unable to initialize the shader program: ${gl.getProgramInfoLog(prog)}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
return prog;
|
||||
}
|
||||
|
||||
function loadShader(gl, type, source)
|
||||
{
|
||||
const shader = gl.createShader(type);
|
||||
|
||||
gl.shaderSource(shader, source);
|
||||
gl.compileShader(shader);
|
||||
|
||||
if(!gl.getShaderParameter(shader, gl.COMPILE_STATUS))
|
||||
{
|
||||
alert(`An error occurred while compiling the shaders: ${gl.getShaderInfoLog(shader)}`);
|
||||
gl.deleteShader(shader);
|
||||
return null;
|
||||
}
|
||||
|
||||
return shader;
|
||||
}
|
||||
|
||||
export let shaderInfos;
|
51
django/frontend/static/js/api/AExchangable.js
Normal file
51
django/frontend/static/js/api/AExchangable.js
Normal file
@ -0,0 +1,51 @@
|
||||
|
||||
|
||||
export class AExchangeable
|
||||
{
|
||||
/**
|
||||
* This abstract class implement import and export method useful to export/import data to/from the server
|
||||
* @param {[String]} fieldNameList
|
||||
*/
|
||||
export(fieldNameList = [])
|
||||
{
|
||||
let valueList = [];
|
||||
|
||||
fieldNameList.forEach(fieldName => {
|
||||
let value;
|
||||
|
||||
if (this[fieldName] instanceof AExchangeable)
|
||||
value = this[fieldName].export();
|
||||
else
|
||||
value = this[fieldName];
|
||||
});
|
||||
|
||||
return valueList;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} data
|
||||
*/
|
||||
import(data)
|
||||
{
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
|
||||
if (Array.isArray(value))
|
||||
{
|
||||
for (let i = 0; i < value.length; i++)
|
||||
{
|
||||
if (this[key][i] instanceof AExchangeable)
|
||||
this[key][i].import(value[i]);
|
||||
else
|
||||
this[key][i] = value[i];
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (this[key] instanceof AExchangeable)
|
||||
this[key].import(value);
|
||||
else
|
||||
this[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
93
django/frontend/static/js/api/Account.js
Normal file
93
django/frontend/static/js/api/Account.js
Normal file
@ -0,0 +1,93 @@
|
||||
import { Client } from "./Client.js";
|
||||
|
||||
class Account
|
||||
{
|
||||
/**
|
||||
* @param {Client} client
|
||||
*/
|
||||
constructor (client)
|
||||
{
|
||||
/**
|
||||
* @type {Client} client
|
||||
*/
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {String} username
|
||||
* @param {String} password
|
||||
* @returns {Response}
|
||||
*/
|
||||
async create(username, password)
|
||||
{
|
||||
let response = await this.client._post("/api/accounts/register", {username: username, password: password});
|
||||
|
||||
if (response.status === 201)
|
||||
await this.client._update_logged(true);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {String} password
|
||||
* @returns {?Promise<Object>}
|
||||
*/
|
||||
async delete(password)
|
||||
{
|
||||
const response = await this.client._delete("/api/accounts/delete", {password: password});
|
||||
|
||||
if (response.ok) {
|
||||
this.client._update_logged(false);
|
||||
return null;
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {String} newUsername
|
||||
* @returns {?Promise<Object>}
|
||||
*/
|
||||
async updateUsername(newUsername)
|
||||
{
|
||||
const data = {
|
||||
username: newUsername
|
||||
};
|
||||
const response = await this.client._patch_json(`/api/accounts/update_profile`, data);
|
||||
const respondeData = await response.json();
|
||||
|
||||
if (response.status === 200) {
|
||||
this.client.me.username = respondeData.username;
|
||||
document.getElementById('navbarDropdownButton').innerHTML = respondeData.username;
|
||||
document.getElementById('myProfileLink').href = '/profiles/' + respondeData.username;
|
||||
return null;
|
||||
}
|
||||
return respondeData['authorize'] || respondeData['detail'] || respondeData['username']?.join(' ') || 'Error.';
|
||||
}
|
||||
|
||||
async updatePassword(currentPassword, newPassword, newPassword2)
|
||||
{
|
||||
const data = {
|
||||
current_password: currentPassword,
|
||||
new_password: newPassword,
|
||||
new_password2: newPassword2
|
||||
};
|
||||
const response = await this.client._put('/api/accounts/update_password', data);
|
||||
if (response.ok)
|
||||
return null;
|
||||
|
||||
const responseData = await response.json();
|
||||
const formatedData = {};
|
||||
if (responseData['current_password'])
|
||||
formatedData['currentPasswordDetail'] = responseData['current_password'];
|
||||
if (responseData['new_password'])
|
||||
formatedData['newPasswordDetail'] = responseData['new_password'];
|
||||
if (responseData['new_password2'])
|
||||
formatedData['newPassword2Detail'] = responseData['new_password2'];
|
||||
if (formatedData == {})
|
||||
formatedData['passwordDetail'] = 'Error';
|
||||
return formatedData;
|
||||
}
|
||||
}
|
||||
|
||||
export { Account };
|
258
django/frontend/static/js/api/Client.js
Normal file
258
django/frontend/static/js/api/Client.js
Normal file
@ -0,0 +1,258 @@
|
||||
import { Account } from "./Account.js";
|
||||
import { MatchMaking } from "./Matchmaking.js";
|
||||
import { Profiles } from "./Profiles.js";
|
||||
import { MyProfile } from "./MyProfile.js";
|
||||
import Notice from "./Notice.js";
|
||||
import LanguageManager from './LanguageManager.js';
|
||||
|
||||
function getCookie(name)
|
||||
{
|
||||
let cookie = {};
|
||||
document.cookie.split(';').forEach(function(el) {
|
||||
let split = el.split('=');
|
||||
cookie[split[0].trim()] = split.slice(1).join("=");
|
||||
});
|
||||
return cookie[name];
|
||||
}
|
||||
|
||||
class Client
|
||||
{
|
||||
/**
|
||||
*
|
||||
* @param {String} url
|
||||
*/
|
||||
constructor(url)
|
||||
{
|
||||
/**
|
||||
* @type {String}
|
||||
*/
|
||||
this._url = url;
|
||||
|
||||
/**
|
||||
* @type {Account}
|
||||
*/
|
||||
this.account = new Account(this);
|
||||
|
||||
/**
|
||||
* @type {Profiles}
|
||||
*/
|
||||
this.profiles = new Profiles(this);
|
||||
|
||||
/**
|
||||
* @type {MatchMaking}
|
||||
*/
|
||||
this.matchmaking = new MatchMaking(this);
|
||||
|
||||
/**
|
||||
* @type {Boolean} A private var represent if the is is log NEVER USE IT use await isAuthenticated()
|
||||
*/
|
||||
this._logged = undefined;
|
||||
|
||||
/**
|
||||
* @type {Notice}
|
||||
*/
|
||||
this.notice = new Notice(this);
|
||||
|
||||
this.lang = new LanguageManager();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* The only right way to determine is the user is logged
|
||||
* @returns {Promise<Boolean>}
|
||||
*/
|
||||
async isAuthenticated()
|
||||
{
|
||||
if (this._logged == undefined)
|
||||
this._logged = await this._test_logged();
|
||||
return this._logged;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a GET request to %uri%
|
||||
* @param {String} uri
|
||||
* @param {*} data
|
||||
* @returns {Promise<Response>}
|
||||
*/
|
||||
async _get(uri, data)
|
||||
{
|
||||
let response = await fetch(this._url + uri, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
'Accept-Language': this.lang.currentLang
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a POST request
|
||||
* @param {String} uri
|
||||
* @param {*} data
|
||||
* @returns {Promise<Response>}
|
||||
*/
|
||||
async _post(uri, data)
|
||||
{
|
||||
let response = await fetch(this._url + uri, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRFToken": getCookie("csrftoken"),
|
||||
'Accept-Language': this.lang.currentLang,
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a DELETE request
|
||||
* @param {String} uri
|
||||
* @param {String} data
|
||||
* @returns {Promise<Response>}
|
||||
*/
|
||||
async _delete(uri, data)
|
||||
{
|
||||
let response = await fetch(this._url + uri, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRFToken": getCookie("csrftoken"),
|
||||
'Accept-Language': this.lang.currentLang,
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a PUT request with json
|
||||
* @param {String} uri
|
||||
* @param {*} data
|
||||
* @returns {Promise<Response>}
|
||||
*/
|
||||
async _put(uri, data)
|
||||
{
|
||||
let response = await fetch(this._url + uri, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"X-CSRFToken": getCookie("csrftoken"),
|
||||
"Content-Type": "application/json",
|
||||
'Accept-Language': this.lang.currentLang,
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a PATCH request with json
|
||||
* @param {String} uri
|
||||
* @param {*} data
|
||||
* @returns {Promise<Response>}
|
||||
*/
|
||||
async _patch_json(uri, data)
|
||||
{
|
||||
let response = await fetch(this._url + uri, {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
"X-CSRFToken": getCookie("csrftoken"),
|
||||
"Content-Type": "application/json",
|
||||
'Accept-Language': this.lang.currentLang,
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a PATCH request with file
|
||||
* @param {String} uri
|
||||
* @param {*} file
|
||||
* @returns {Promise<Response>}
|
||||
*/
|
||||
async _patch_file(uri, file)
|
||||
{
|
||||
let response = await fetch(this._url + uri, {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
"X-CSRFToken": getCookie("csrftoken"),
|
||||
'Accept-Language': this.lang.currentLang,
|
||||
},
|
||||
body: file,
|
||||
});
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change logged state. Use It if you recv an 403 error
|
||||
* @param {Promise<?>} state
|
||||
* @returns
|
||||
*/
|
||||
async _update_logged(state)
|
||||
{
|
||||
if (this._logged == state)
|
||||
return;
|
||||
|
||||
if (state)
|
||||
{
|
||||
this.me = new MyProfile(this);
|
||||
await this.me.init();
|
||||
this.notice.start();
|
||||
document.getElementById('navbarLoggedOut').classList.add('d-none');
|
||||
document.getElementById('navbarLoggedIn').classList.remove('d-none');
|
||||
document.getElementById('navbarDropdownButton').innerHTML = this.me.username;
|
||||
document.getElementById('myProfileLink').href = '/profiles/' + this.me.username;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.me = undefined;
|
||||
this.notice.stop();
|
||||
document.getElementById('navbarLoggedOut').classList.remove('d-none');
|
||||
document.getElementById('navbarLoggedIn').classList.add('d-none');
|
||||
document.getElementById('navbarDropdownButton').innerHTML = 'Me';
|
||||
document.getElementById('myProfileLink').href = '';
|
||||
}
|
||||
this._logged = state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loggin the user
|
||||
* @param {String} username
|
||||
* @param {String} password
|
||||
* @returns {Promise<Response>}
|
||||
*/
|
||||
async login(username, password)
|
||||
{
|
||||
let response = await this._post("/api/accounts/login", {username: username, password: password});
|
||||
if (response.status == 200)
|
||||
await this._update_logged(true);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Logout the user
|
||||
* @returns {Promise<?>}
|
||||
*/
|
||||
async logout()
|
||||
{
|
||||
await this._get("/api/accounts/logout");
|
||||
await this._update_logged(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the user is logged. NEVER USE IT, USE isAuthenticated()
|
||||
* @returns {Promise<Boolean>}
|
||||
*/
|
||||
async _test_logged()
|
||||
{
|
||||
let response = await this._get("/api/accounts/logged");
|
||||
|
||||
await this._update_logged(response.status === 200);
|
||||
return response.status === 200;
|
||||
}
|
||||
}
|
||||
|
||||
export {Client};
|
74
django/frontend/static/js/api/LanguageManager.js
Normal file
74
django/frontend/static/js/api/LanguageManager.js
Normal file
@ -0,0 +1,74 @@
|
||||
import { reloadView } from '../index.js';
|
||||
|
||||
export default class LanguageManager {
|
||||
constructor() {
|
||||
this.availableLanguages = ['en', 'fr', 'tp', 'cr'];
|
||||
|
||||
this.dict = null;
|
||||
this.currentLang = 'en';
|
||||
this.chosenLang = localStorage.getItem('preferedLanguage') || this.currentLang;
|
||||
if (this.chosenLang !== this.currentLang && this.availableLanguages.includes(this.chosenLang)) {
|
||||
this.loading = this.translatePage();
|
||||
this.currentLang = this.chosenLang;
|
||||
} else {
|
||||
this.loading = this.loadDict(this.chosenLang);
|
||||
}
|
||||
document.getElementById('languageDisplay').innerHTML =
|
||||
document.querySelector(`#languageSelector > [value=${this.currentLang}]`)?.innerHTML;
|
||||
}
|
||||
|
||||
async translatePage() {
|
||||
if (this.currentLang === this.chosenLang)
|
||||
return;
|
||||
|
||||
await this.loadDict(this.chosenLang);
|
||||
if (!this.dict)
|
||||
return 1;
|
||||
|
||||
document.querySelectorAll('[data-i18n]').forEach(el => {
|
||||
let key = el.getAttribute('data-i18n');
|
||||
el.innerHTML = this.dict[key];
|
||||
});
|
||||
await reloadView();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
async changeLanguage(lang) {
|
||||
if (lang === this.currentLang || !this.availableLanguages.includes(lang))
|
||||
return 1;
|
||||
|
||||
this.chosenLang = lang;
|
||||
if (await this.translatePage() !== 0)
|
||||
return 1;
|
||||
|
||||
this.currentLang = this.chosenLang;
|
||||
localStorage.setItem('preferedLanguage', lang);
|
||||
document.getElementById('languageDisplay').innerHTML =
|
||||
document.querySelector(`#languageSelector > [value=${this.currentLang}]`)?.innerHTML;
|
||||
return 0;
|
||||
}
|
||||
|
||||
async loadDict(lang) {
|
||||
let dictUrl = `${location.origin}/static/js/lang/${lang}.json`;
|
||||
let response = await fetch(dictUrl);
|
||||
|
||||
if (response.status !== 200) {
|
||||
console.log(`No translation found for language ${lang}`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.dict = await response.json();
|
||||
}
|
||||
|
||||
async waitLoading() {
|
||||
await this.loading;
|
||||
}
|
||||
|
||||
get(key, defaultTxt) {
|
||||
if (!this.dict)
|
||||
return defaultTxt;
|
||||
|
||||
return this.dict[key] || defaultTxt;
|
||||
}
|
||||
}
|
61
django/frontend/static/js/api/Matchmaking.js
Normal file
61
django/frontend/static/js/api/Matchmaking.js
Normal file
@ -0,0 +1,61 @@
|
||||
import { Client } from "./Client.js";
|
||||
|
||||
class MatchMaking
|
||||
{
|
||||
/**
|
||||
* @param {Client} client
|
||||
*/
|
||||
constructor(client)
|
||||
{
|
||||
/**
|
||||
* @type {Client}
|
||||
*/
|
||||
this.client = client;
|
||||
this.searching = false;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {CallableFunction} receive_func
|
||||
* @param {CallableFunction} disconnect_func
|
||||
* @param {Number} mode The number of players in a game
|
||||
* @returns {Promise<?>}
|
||||
*/
|
||||
async start(receive_func, disconnect_func, game_type, mode)
|
||||
{
|
||||
if (!await this.client.isAuthenticated())
|
||||
return null;
|
||||
|
||||
let url = `${window.location.protocol[4] === 's' ? 'wss' : 'ws'}://${window.location.host}/ws/matchmaking/${game_type}/${mode}`;
|
||||
|
||||
this._socket = new WebSocket(url);
|
||||
|
||||
this.searching = true;
|
||||
|
||||
this.receive_func = receive_func;
|
||||
this.disconnect_func = disconnect_func;
|
||||
|
||||
this._socket.onmessage = function (event) {
|
||||
const data = JSON.parse(event.data);
|
||||
receive_func(data);
|
||||
};
|
||||
|
||||
this._socket.onclose = this.onclose.bind(this);
|
||||
}
|
||||
|
||||
onclose(event)
|
||||
{
|
||||
this.stop();
|
||||
this.disconnect_func(event);
|
||||
}
|
||||
|
||||
stop()
|
||||
{
|
||||
if (this._socket)
|
||||
this._socket.close();
|
||||
this._socket = undefined;
|
||||
this.searching = false;
|
||||
}
|
||||
}
|
||||
|
||||
export {MatchMaking};
|
144
django/frontend/static/js/api/MyProfile.js
Normal file
144
django/frontend/static/js/api/MyProfile.js
Normal file
@ -0,0 +1,144 @@
|
||||
import { Client } from "./Client.js";
|
||||
import { Profile } from "./Profile.js";
|
||||
|
||||
class MyProfile extends Profile
|
||||
{
|
||||
|
||||
/**
|
||||
* @param {Client} client
|
||||
*/
|
||||
constructor (client)
|
||||
{
|
||||
super(client, "../me");
|
||||
|
||||
/**
|
||||
* @type {[Profile]}
|
||||
*/
|
||||
this.blockedUsers = [];
|
||||
// /**
|
||||
// * @type {[Profile]}
|
||||
// */
|
||||
// this.friendList = [];
|
||||
// /**
|
||||
// * @type {[Profile]}
|
||||
// */
|
||||
// this.incomingFriendRequests = [];
|
||||
// /**
|
||||
// * @type {[Profile]}
|
||||
// */
|
||||
// this.outgoingFriendRequests = [];
|
||||
}
|
||||
|
||||
async init() {
|
||||
await super.init();
|
||||
await this.getBlockedUsers();
|
||||
// await this.getFriends();
|
||||
// await this.getIncomingFriendRequests()
|
||||
// await this.getOutgoingFriendRequests()
|
||||
}
|
||||
|
||||
async getBlockedUsers() {
|
||||
const response = await this.client._get('/api/profiles/block');
|
||||
const data = await response.json();
|
||||
data.forEach(profileData => this.blockedUsers.push(new Profile(this.client, profileData.username, profileData.id, profileData.avatar)));
|
||||
}
|
||||
|
||||
async getFriends() {
|
||||
const response = await this.client._get('/api/profiles/friends');
|
||||
const data = await response.json();
|
||||
data.forEach(profileData => this.friendList.push(new Profile(this.client, profileData.username, profileData.id, profileData.avatar)));
|
||||
}
|
||||
async getIncomingFriendRequests() {
|
||||
const response = await this.client._get('/api/profiles/incoming_friend_requests');
|
||||
const data = await response.json();
|
||||
data.forEach(profileData => this.incomingFriendRequests.push(
|
||||
new Profile(this.client, profileData.username, profileData.id, profileData.avatar)
|
||||
));
|
||||
}
|
||||
async getOutgoingFriendRequests() {
|
||||
const response = await this.client._get('/api/profiles/outgoing_friend_requests');
|
||||
const data = await response.json();
|
||||
data.forEach(profileData => this.outgoingFriendRequests.push(
|
||||
new Profile(this.client, profileData.username, profileData.id, profileData.avatar)
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Profile} profile
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
_isFriend(profile) {
|
||||
for (const user of this.friendList) {
|
||||
if (user.id === profile.id)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* @param {Profile} profile
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
_isBlocked(profile) {
|
||||
for (const user of this.blockedUsers) {
|
||||
if (user.id === profile.id)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* @param {Profile} profile
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
_hasIncomingRequestFrom(profile) {
|
||||
for (const user of this.incomingFriendRequests) {
|
||||
if (user.id === profile.id)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* @param {Profile} profile
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
_hasOutgoingRequestTo(profile) {
|
||||
for (const user of this.outgoingFriendRequests) {
|
||||
if (user.id === profile.id)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {File} selectedFile
|
||||
* @returns {Promise<Response>}
|
||||
*/
|
||||
async changeAvatar(selectedFile)
|
||||
{
|
||||
const formData = new FormData();
|
||||
formData.append('avatar', selectedFile);
|
||||
|
||||
const response = await this.client._patch_file(`/api/profiles/settings`, formData);
|
||||
const responseData = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
this.avatar = responseData.avatar;
|
||||
return null;
|
||||
}
|
||||
return responseData;
|
||||
}
|
||||
|
||||
async deleteAvatar() {
|
||||
const response = await this.client._delete('/api/profiles/settings');
|
||||
const responseData = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
this.avatar = responseData.avatar;
|
||||
return null;
|
||||
}
|
||||
return responseData;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export {MyProfile};
|
101
django/frontend/static/js/api/Notice.js
Normal file
101
django/frontend/static/js/api/Notice.js
Normal file
@ -0,0 +1,101 @@
|
||||
import {Client} from './Client.js';
|
||||
import {createNotification} from '../utils/noticeUtils.js'
|
||||
import { lastView } from '../index.js';
|
||||
import ProfilePageView from '../views/ProfilePageView.js';
|
||||
import Search from '../views/Search.js';
|
||||
|
||||
export default class Notice {
|
||||
|
||||
/**
|
||||
* @param {Client} client
|
||||
*/
|
||||
constructor(client) {
|
||||
/**
|
||||
* @type {Client}
|
||||
*/
|
||||
this.client = client;
|
||||
this.url = location.origin.replace('http', 'ws') + '/ws/notice';
|
||||
}
|
||||
|
||||
start() {
|
||||
this._socket = new WebSocket(this.url);
|
||||
|
||||
this._socket.onclose = _ => this._socket = undefined;
|
||||
this._socket.onmessage = message => {
|
||||
const data = JSON.parse(message.data);
|
||||
//console.log(data)
|
||||
|
||||
if (data.type === 'friend_request') {
|
||||
this.friend_request(data.author);
|
||||
} else if (data.type === 'new_friend') {
|
||||
this.new_friend(data.friend);
|
||||
} else if (data.type === 'friend_removed') {
|
||||
this.friend_removed(data.friend);
|
||||
} else if (data.type === 'friend_request_canceled') {
|
||||
this.friend_request_canceled(data.author);
|
||||
} else if (data.type === 'online') {
|
||||
this.online(data.user)
|
||||
} else if (data.type === 'offline') {
|
||||
this.offline(data.user)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
stop() {
|
||||
if (this._socket) {
|
||||
this._socket.close();
|
||||
this._socket = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
_setOnlineStatus(user, status) {
|
||||
if (lastView instanceof ProfilePageView && lastView.profile.id === user.id) {
|
||||
lastView.profile.online = status;
|
||||
lastView.loadFriendshipStatus();
|
||||
}
|
||||
else if (lastView instanceof Search) {
|
||||
lastView.display_specific_user(user.id);
|
||||
}
|
||||
}
|
||||
|
||||
online(user) {
|
||||
this._setOnlineStatus(user, true)
|
||||
}
|
||||
|
||||
offline(user) {
|
||||
this._setOnlineStatus(user, false)
|
||||
}
|
||||
|
||||
friend_request(author) {
|
||||
createNotification('Friend Request', `<strong>${author.username}</strong> sent you a friend request.`);
|
||||
if (lastView instanceof ProfilePageView && lastView.profile.id === author.id) {
|
||||
lastView.profile.hasIncomingRequest = true;
|
||||
lastView.loadFriendshipStatus();
|
||||
}
|
||||
}
|
||||
|
||||
new_friend(friend) {
|
||||
createNotification('New Friend', `<strong>${friend.username}</strong> accepted your friend request.`);
|
||||
if (lastView instanceof ProfilePageView && lastView.profile.id === friend.id) {
|
||||
lastView.profile.isFriend = true;
|
||||
lastView.profile.hasIncomingRequest = false;
|
||||
lastView.profile.hasOutgoingRequest = false;
|
||||
lastView.loadFriendshipStatus();
|
||||
}
|
||||
}
|
||||
|
||||
friend_removed(exFriend) {
|
||||
if (lastView instanceof ProfilePageView && lastView.profile.id === exFriend.id) {
|
||||
lastView.profile.isFriend = false;
|
||||
lastView.profile.online = null;
|
||||
lastView.loadFriendshipStatus();
|
||||
}
|
||||
}
|
||||
|
||||
friend_request_canceled(author) {
|
||||
if (lastView instanceof ProfilePageView && lastView.profile.id === author.id) {
|
||||
lastView.profile.hasIncomingRequest = false;
|
||||
lastView.loadFriendshipStatus();
|
||||
}
|
||||
}
|
||||
}
|
101
django/frontend/static/js/api/Profile.js
Normal file
101
django/frontend/static/js/api/Profile.js
Normal file
@ -0,0 +1,101 @@
|
||||
import { AExchangeable } from "./AExchangable.js";
|
||||
import { Client } from "./Client.js";
|
||||
|
||||
export class Profile extends AExchangeable
|
||||
{
|
||||
/**
|
||||
* @param {Client} client
|
||||
*/
|
||||
constructor (client, username, id, avatar)
|
||||
{
|
||||
super();
|
||||
|
||||
/**
|
||||
* @type {Client} client
|
||||
*/
|
||||
this.client = client;
|
||||
|
||||
/**
|
||||
* @type {String}
|
||||
*/
|
||||
this.username = username;
|
||||
|
||||
/**
|
||||
* @type {Number}
|
||||
*/
|
||||
this.id = id;
|
||||
|
||||
/**
|
||||
* @type {String}
|
||||
*/
|
||||
this.avatar = avatar;
|
||||
|
||||
/**
|
||||
* @type {Boolean}
|
||||
**/
|
||||
this.online = null;
|
||||
|
||||
/**
|
||||
* @type {Boolean}
|
||||
*/
|
||||
this.isFriend;
|
||||
this.isBlocked;
|
||||
this.hasIncomingRequest;
|
||||
this.hasOutgoingRequest;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
async init()
|
||||
{
|
||||
let response;
|
||||
if (this.username !== undefined)
|
||||
response = await this.client._get(`/api/profiles/user/${this.username}`);
|
||||
else
|
||||
response = await this.client._get(`/api/profiles/id/${this.id}`);
|
||||
|
||||
if (response.status !== 200)
|
||||
return response.status;
|
||||
|
||||
const responseData = await response.json();
|
||||
this.id = responseData.id;
|
||||
this.username = responseData.username;
|
||||
this.avatar = responseData.avatar;
|
||||
this.online = responseData.online
|
||||
|
||||
if (!this.client.me || this.client.me.id === this.id)
|
||||
return;
|
||||
|
||||
this.hasIncomingRequest = responseData.has_incoming_request;
|
||||
this.hasOutgoingRequest = responseData.has_outgoing_request;
|
||||
this.isFriend = responseData.is_friend;
|
||||
this.isBlocked = this.client.me._isBlocked(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<[Object]>}
|
||||
*/
|
||||
async getGameHistory()
|
||||
{
|
||||
const response = await this.client._get(`/api/games/history/${this.id}`);
|
||||
const response_data = await response.json();
|
||||
|
||||
const games = [];
|
||||
|
||||
response_data.forEach(game_data => {
|
||||
games.push(game_data);
|
||||
});
|
||||
|
||||
return games;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {[String]} additionalFieldList
|
||||
*/
|
||||
export(additionalFieldList = [])
|
||||
{
|
||||
super.export([...["username", "avatar", "id"], ...additionalFieldList])
|
||||
}
|
||||
}
|
59
django/frontend/static/js/api/Profiles.js
Normal file
59
django/frontend/static/js/api/Profiles.js
Normal file
@ -0,0 +1,59 @@
|
||||
import { Profile } from "./Profile.js";
|
||||
|
||||
class Profiles
|
||||
{
|
||||
/**
|
||||
* @param {Client} client
|
||||
*/
|
||||
constructor (client)
|
||||
{
|
||||
/**
|
||||
* @type {Client} client
|
||||
*/
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns {Promise<[Profile]>}
|
||||
*/
|
||||
async all()
|
||||
{
|
||||
let response = await this.client._get("/api/profiles/");
|
||||
let response_data = await response.json();
|
||||
|
||||
let profiles = [];
|
||||
response_data.forEach((profile) => {
|
||||
profiles.push(new Profile(this.client, profile.username, profile.id, profile.avatar));
|
||||
});
|
||||
return profiles;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {String} username
|
||||
* @returns {?Promise<Profile>}
|
||||
*/
|
||||
async getProfile(username)
|
||||
{
|
||||
let profile = new Profile(this.client, username);
|
||||
if (await profile.init())
|
||||
return null;
|
||||
return profile;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Number} id
|
||||
* @returns {Profile}
|
||||
*/
|
||||
async getProfileId(id)
|
||||
{
|
||||
let profile = new Profile(this.client, undefined, id);
|
||||
if (await profile.init())
|
||||
return null;
|
||||
return profile;
|
||||
}
|
||||
}
|
||||
|
||||
export {Profiles};
|
22
django/frontend/static/js/api/chat/Ask.js
Normal file
22
django/frontend/static/js/api/chat/Ask.js
Normal file
@ -0,0 +1,22 @@
|
||||
|
||||
class Ask {
|
||||
constructor(client) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
async ask_game(asked) {
|
||||
response = await this.client._post(`/api/chat/ask/`);
|
||||
}
|
||||
|
||||
async ask_game_canceled() {
|
||||
|
||||
}
|
||||
|
||||
async ask_game_accepted() {
|
||||
|
||||
}
|
||||
|
||||
async ask_game_refused() {
|
||||
|
||||
}
|
||||
}
|
66
django/frontend/static/js/api/chat/Channel.js
Normal file
66
django/frontend/static/js/api/chat/Channel.js
Normal file
@ -0,0 +1,66 @@
|
||||
import {Message} from "./Message.js";
|
||||
|
||||
class Channel {
|
||||
constructor(client, channel, members, messages, reload) {
|
||||
this.client = client;
|
||||
this.channel = channel;
|
||||
this.members = members;
|
||||
this.messages = [];
|
||||
if (messages != undefined)
|
||||
this.updateMessages(messages);
|
||||
|
||||
this.connect(reload);
|
||||
}
|
||||
|
||||
// reload = function to use when we receive a message
|
||||
connect(reload) {
|
||||
const url = location.origin.replace('http', 'ws') +
|
||||
'/ws/chat/' +
|
||||
this.channel;
|
||||
|
||||
this.chatSocket = new WebSocket(url);
|
||||
this.chatSocket.onmessage = (event) =>{
|
||||
let data = JSON.parse(event.data);
|
||||
|
||||
this.messages.push(new Message(
|
||||
this.channel,
|
||||
data.author,
|
||||
data.content,
|
||||
data.time,
|
||||
));
|
||||
|
||||
reload();
|
||||
};
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
this.chatSocket.close();
|
||||
}
|
||||
|
||||
updateMessages(messages)
|
||||
{
|
||||
this.messages = [];
|
||||
|
||||
messages.forEach((message) => {
|
||||
this.messages.push(new Message(
|
||||
message.channel,
|
||||
message.author,
|
||||
message.content,
|
||||
message.time,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
async sendMessageChannel(message, receivers_id) {
|
||||
|
||||
if (this.chatSocket == undefined)
|
||||
return;
|
||||
|
||||
this.chatSocket.send(JSON.stringify({
|
||||
'message':message,
|
||||
'receivers_id':receivers_id,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
export {Channel};
|
23
django/frontend/static/js/api/chat/Channels.js
Normal file
23
django/frontend/static/js/api/chat/Channels.js
Normal file
@ -0,0 +1,23 @@
|
||||
import {Channel} from "./Channel.js";
|
||||
|
||||
export default class Channels {
|
||||
constructor(client) {
|
||||
this.client = client;
|
||||
this.channel = undefined;
|
||||
}
|
||||
|
||||
async createChannel(members_id, reload) {
|
||||
|
||||
const response = await this.client._post("/api/chat/", {
|
||||
members_id:members_id
|
||||
});
|
||||
|
||||
if (response.status >= 300)
|
||||
return undefined;
|
||||
|
||||
const data = await response.json();
|
||||
console.log(data)
|
||||
|
||||
this.channel = new Channel(this.client, data.id, members_id, data.messages, reload);
|
||||
}
|
||||
}
|
10
django/frontend/static/js/api/chat/Message.js
Normal file
10
django/frontend/static/js/api/chat/Message.js
Normal file
@ -0,0 +1,10 @@
|
||||
class Message {
|
||||
constructor(channel, author, content, time) {
|
||||
this.channel = channel;
|
||||
this.author = author;
|
||||
this.content = content;
|
||||
this.time = time;
|
||||
}
|
||||
}
|
||||
|
||||
export {Message};
|
159
django/frontend/static/js/api/game/AGame.js
Normal file
159
django/frontend/static/js/api/game/AGame.js
Normal file
@ -0,0 +1,159 @@
|
||||
import { AExchangeable } from "../AExchangable.js";
|
||||
import { APlayer } from "./APlayer.js";
|
||||
import { Client } from "../Client.js"
|
||||
import { sleep } from "../../utils/sleep.js";
|
||||
import { Profile } from "../Profile.js";
|
||||
|
||||
export class AGame extends AExchangeable
|
||||
{
|
||||
/**
|
||||
* Abstract class to create commununication between client and server
|
||||
* @param {Client} client
|
||||
* @param {Number} id
|
||||
* @param {CallableFunction} receiveHandler
|
||||
* @param {CallableFunction} disconntectHandler
|
||||
* @param {"tictactoe" | "pong"} gameType
|
||||
*/
|
||||
constructor(client, id, receiveHandler, disconntectHandler, gameType)
|
||||
{
|
||||
super();
|
||||
|
||||
/**
|
||||
* @type {Client}
|
||||
*/
|
||||
this.client = client;
|
||||
|
||||
/**
|
||||
* @type {Number}
|
||||
*/
|
||||
this.id = id;
|
||||
|
||||
/**
|
||||
* ex: Tictactoe, Pong
|
||||
* @type {String}
|
||||
*/
|
||||
this.gameType = gameType;
|
||||
|
||||
/**
|
||||
* @type {CallableFunction}
|
||||
*/
|
||||
this._receiveHandler = receiveHandler;
|
||||
|
||||
/**
|
||||
* @type {CallableFunction}
|
||||
*/
|
||||
this._disconntectHandler = disconntectHandler;
|
||||
|
||||
/**
|
||||
* @type {Profile}
|
||||
*/
|
||||
this.winner;
|
||||
|
||||
/**
|
||||
* @type {Number}
|
||||
*/
|
||||
this.startTimestamp;
|
||||
|
||||
/**
|
||||
* @type {Number}
|
||||
*/
|
||||
this.stopTimestamp;
|
||||
|
||||
/**
|
||||
* @type {Boolean}
|
||||
*/
|
||||
this.started;
|
||||
|
||||
/**
|
||||
* @type {Boolean}
|
||||
*/
|
||||
this.finished;
|
||||
|
||||
/**
|
||||
* @type {[APlayer]}
|
||||
*/
|
||||
this.players = [];
|
||||
}
|
||||
|
||||
async init()
|
||||
{
|
||||
let response = await this.client._get(`/api/games/${this.id}`);
|
||||
|
||||
if (response.status !== 200)
|
||||
return response.status;
|
||||
|
||||
let response_data = await response.json();
|
||||
|
||||
this.import(response_data);
|
||||
}
|
||||
|
||||
getState()
|
||||
{
|
||||
return ["waiting", "started", "finished"][this.started + this.finished];
|
||||
}
|
||||
|
||||
/**
|
||||
* Send string to the server, must be excuted after .join()
|
||||
* @param {String} data
|
||||
*/
|
||||
send(data)
|
||||
{
|
||||
if (this._socket === undefined || this._socket.readyState !== WebSocket.OPEN)
|
||||
return;
|
||||
this._socket.send(data);
|
||||
}
|
||||
|
||||
async join()
|
||||
{
|
||||
if (this.finished === true)
|
||||
{
|
||||
console.error("The Game is not currently ongoing.");
|
||||
return;
|
||||
}
|
||||
|
||||
const url = `${window.location.protocol[4] === 's' ? 'wss' : 'ws'}://${window.location.host}/ws/games/${this.gameType}/${this.id}`;
|
||||
|
||||
this._socket = new WebSocket(url);
|
||||
|
||||
this._socket.onmessage = async (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
await this._receiveHandler(data);
|
||||
};
|
||||
|
||||
this._socket.onclose = async () => {
|
||||
this._socket = undefined;
|
||||
await this._disconntectHandler();
|
||||
};
|
||||
}
|
||||
|
||||
leave()
|
||||
{
|
||||
if (this._socket)
|
||||
{
|
||||
this._socket.close();
|
||||
this._socket = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Should be redefine using your own APlayer inherited
|
||||
* @param {Object} data
|
||||
|
||||
import(data)
|
||||
{
|
||||
super.import(data);
|
||||
|
||||
// just an example code
|
||||
|
||||
/*
|
||||
this.players.length = 0;
|
||||
|
||||
data.players.forEach(player_data => {
|
||||
let player = new APlayer(this.client, this);
|
||||
player.import(player_data);
|
||||
this.players.push(player);
|
||||
});
|
||||
|
||||
}
|
||||
*/
|
||||
}
|
39
django/frontend/static/js/api/game/APlayer.js
Normal file
39
django/frontend/static/js/api/game/APlayer.js
Normal file
@ -0,0 +1,39 @@
|
||||
import { Client } from "../Client.js";
|
||||
import { Profile } from "../Profile.js";
|
||||
import { AGame } from "./AGame.js";
|
||||
|
||||
export class APlayer extends Profile
|
||||
{
|
||||
/**
|
||||
*
|
||||
* @param {Client} client
|
||||
* @param {AGame} game
|
||||
* @param {Number} id
|
||||
* @param {String} username
|
||||
* @param {String} avatar
|
||||
* @param {Boolean} isConnected
|
||||
*/
|
||||
constructor (client, game, id, username, avatar, isConnected)
|
||||
{
|
||||
super(client, username, id, avatar);
|
||||
|
||||
/**
|
||||
* @type {AGame}
|
||||
*/
|
||||
this.game = game
|
||||
|
||||
/**
|
||||
* @type {Boolean}
|
||||
*/
|
||||
this.isConnected = isConnected;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {[String]} additionalFieldList
|
||||
*/
|
||||
export(additionalFieldList = [])
|
||||
{
|
||||
super.export([...additionalFieldList, ...["isConnected"]])
|
||||
}
|
||||
}
|
32
django/frontend/static/js/api/game/pong/Point.js
Normal file
32
django/frontend/static/js/api/game/pong/Point.js
Normal file
@ -0,0 +1,32 @@
|
||||
import { AExchangeable } from "../../AExchangable.js";
|
||||
|
||||
class Point extends AExchangeable
|
||||
{
|
||||
/**
|
||||
* @param {Number} x
|
||||
* @param {Number} y
|
||||
*/
|
||||
constructor(x, y)
|
||||
{
|
||||
super();
|
||||
|
||||
/**
|
||||
* @type {Number}
|
||||
*/
|
||||
this.x = x;
|
||||
/**
|
||||
* @type {Number}
|
||||
*/
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {[String]}
|
||||
*/
|
||||
export(additionalFieldList)
|
||||
{
|
||||
super.export([...additionalFieldList, "x", "y"])
|
||||
}
|
||||
}
|
||||
|
||||
export { Point };
|
83
django/frontend/static/js/api/game/pong/PongBall.js
Normal file
83
django/frontend/static/js/api/game/pong/PongBall.js
Normal file
@ -0,0 +1,83 @@
|
||||
import { AExchangeable } from "../../AExchangable.js";
|
||||
import { PongGame } from "./PongGame.js";
|
||||
import { renderCube} from "../../../3D/cube.js"
|
||||
import { Position } from "./Position.js";
|
||||
import { Point } from "./Point.js";
|
||||
|
||||
export class PongBall extends AExchangeable
|
||||
{
|
||||
/**
|
||||
*
|
||||
* @param {PongGame} game
|
||||
* @param {Position} position
|
||||
* @param {Number} angle
|
||||
* @param {Number} speed
|
||||
* @param {Number} size
|
||||
*/
|
||||
constructor(game, size, position = new Position(new Point(game.config.CENTER_X, game.config.CENTER_Y), 0), angle, speed)
|
||||
{
|
||||
super();
|
||||
|
||||
/**
|
||||
* @type {PongGame}
|
||||
*/
|
||||
this.game = game;
|
||||
|
||||
/**
|
||||
* @type {Position}
|
||||
*/
|
||||
this.position = position;
|
||||
|
||||
/**
|
||||
* @type {Number}
|
||||
*/
|
||||
this.size = size;
|
||||
|
||||
/**
|
||||
* @type {Number}
|
||||
*/
|
||||
this.angle = angle;
|
||||
|
||||
/**
|
||||
* @type {Number}
|
||||
*/
|
||||
this.speed = speed;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {CanvasRenderingContext2D} ctx
|
||||
*/
|
||||
draw(ctx)
|
||||
{
|
||||
if(ctx instanceof CanvasRenderingContext2D)
|
||||
{
|
||||
ctx.rect(this.position.location.x - this.size / 2, this.position.location.y - this.size / 2, this.size, this.size);
|
||||
}
|
||||
else if(ctx instanceof WebGLRenderingContext)
|
||||
{
|
||||
const size = this.size * 3;
|
||||
const posx = (this.position.location.x - this.size / 2) - this.game.config.MAP_SIZE_X / 2;
|
||||
const posy = (this.position.location.y - this.size / 2) - this.game.config.MAP_SIZE_Y / 2;
|
||||
renderCube(ctx, posx, 0, posy, 0, size, size, size);
|
||||
}
|
||||
else
|
||||
{
|
||||
alert('Unknown rendering context type');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {CanvasRenderingContext2D} ctx
|
||||
*/
|
||||
render(ctx)
|
||||
{
|
||||
let distance = this.speed * (this.game.time.deltaTime() / 1000);
|
||||
|
||||
this.position.location.x = this.position.location.x + distance * Math.cos(this.angle);
|
||||
this.position.location.y = this.position.location.y - distance * Math.sin(this.angle);
|
||||
|
||||
this.draw(ctx);
|
||||
}
|
||||
}
|
94
django/frontend/static/js/api/game/pong/PongConfig.js
Normal file
94
django/frontend/static/js/api/game/pong/PongConfig.js
Normal file
@ -0,0 +1,94 @@
|
||||
import { AExchangeable } from "../../AExchangable.js";
|
||||
|
||||
export class PongConfig extends AExchangeable
|
||||
{
|
||||
/**
|
||||
* @param {Client} client
|
||||
*/
|
||||
constructor(client)
|
||||
{
|
||||
super();
|
||||
|
||||
/**
|
||||
* @type {Client}
|
||||
*/
|
||||
this.client = client;
|
||||
|
||||
/**
|
||||
* @type {Number}
|
||||
*/
|
||||
this.MAP_SIZE_X;
|
||||
|
||||
/**
|
||||
* @type {Number}
|
||||
*/
|
||||
this.MAP_SIZE_Y;
|
||||
|
||||
/**
|
||||
* @type {Number}
|
||||
*/
|
||||
this.WALL_RATIO;
|
||||
|
||||
/**
|
||||
* @type {Number}
|
||||
*/
|
||||
this.PADDLE_SPEED_PER_SECOND_MAX;
|
||||
|
||||
/**
|
||||
* @type {Number}
|
||||
*/
|
||||
this.PADDLE_RATIO;
|
||||
|
||||
/**
|
||||
* @type {Number}
|
||||
*/
|
||||
this.BALL_SIZE;
|
||||
|
||||
/**
|
||||
* @type {Number}
|
||||
*/
|
||||
this.BALL_SPEED_INC;
|
||||
|
||||
/**
|
||||
* @type {Number}
|
||||
*/
|
||||
this.BALL_SPEED_START;
|
||||
|
||||
/**
|
||||
* @type {Number}
|
||||
*/
|
||||
this.STROKE_THICKNESS;
|
||||
|
||||
/**
|
||||
* @type {Number}
|
||||
*/
|
||||
this.GAME_MAX_SCORE;
|
||||
|
||||
/**
|
||||
* @type {Number}
|
||||
*/
|
||||
this.MAP_CENTER_X;
|
||||
|
||||
/**
|
||||
* @type {Number}
|
||||
*/
|
||||
this.MAP_CENTER_Y;
|
||||
}
|
||||
|
||||
async init()
|
||||
{
|
||||
let response = await this.client._get("/api/games/");
|
||||
|
||||
if (response.status !== 200)
|
||||
return response.status;
|
||||
|
||||
let response_data = await response.json();
|
||||
|
||||
this.import(response_data);
|
||||
|
||||
this.MAP_CENTER_X = this.MAP_SIZE_X / 2;
|
||||
this.MAP_CENTER_Y = this.MAP_SIZE_Y / 2;
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
230
django/frontend/static/js/api/game/pong/PongGame.js
Normal file
230
django/frontend/static/js/api/game/pong/PongGame.js
Normal file
@ -0,0 +1,230 @@
|
||||
import { Time } from "./Time.js";
|
||||
import { AGame } from "../AGame.js";
|
||||
import { PongConfig } from "./PongConfig.js";
|
||||
import { PongPlayer } from "./PongPlayer.js";
|
||||
import { Client } from "../../Client.js";
|
||||
import { PongBall } from "./PongBall.js";
|
||||
import { sleep } from "../../../utils/sleep.js";
|
||||
import { Wall } from "./Wall.js"
|
||||
import { Position } from "./Position.js";
|
||||
|
||||
export class PongGame extends AGame
|
||||
{
|
||||
/**
|
||||
* @param {Client} client
|
||||
* @param {CallableFunction} goal_handler
|
||||
* @param {CallableFunction} finish_handler
|
||||
* @param {CallableFunction} disconnect_handler
|
||||
* @param {CallableFunction} startHandler
|
||||
* @param {Number} id
|
||||
*/
|
||||
constructor(client, id, disconnectHandler, goalHandler, startHandler, finishHandler)
|
||||
{
|
||||
super(client, id, undefined, disconnectHandler, "pong");
|
||||
|
||||
this._receiveHandler = this._receive;
|
||||
|
||||
/**
|
||||
* @type {CallableFunction}
|
||||
*/
|
||||
this._goalHandler = goalHandler;
|
||||
|
||||
/**
|
||||
* @type {CallableFunction}
|
||||
*/
|
||||
this._startHandler = startHandler;
|
||||
|
||||
/**
|
||||
* @type {CallableFunction}
|
||||
*/
|
||||
this._finishHandler = finishHandler
|
||||
|
||||
/**
|
||||
* @type {Time}
|
||||
*/
|
||||
this.time;
|
||||
|
||||
/**
|
||||
* @type {Boolean}
|
||||
*/
|
||||
this._inited = false;
|
||||
|
||||
/**
|
||||
* @type {PongConfig}
|
||||
*/
|
||||
this.config;
|
||||
|
||||
/**
|
||||
* @type {Ball}
|
||||
*/
|
||||
this.ball = new PongBall(this, undefined, new Position(), 0, 0);
|
||||
|
||||
/**
|
||||
* @type {[Wall]}
|
||||
*/
|
||||
this.walls = [];
|
||||
|
||||
/**
|
||||
* @type {[PongPlayer]}
|
||||
*/
|
||||
this.players = [];
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns {Promise<Number>}
|
||||
*/
|
||||
async init()
|
||||
{
|
||||
let response = await this.client._get(`/api/games/${this.id}`);
|
||||
|
||||
if (response.status !== 200)
|
||||
return response.status;
|
||||
|
||||
let response_data = await response.json();
|
||||
|
||||
response_data.players.forEach((player_data) => {
|
||||
let player = new PongPlayer(this.client, this)
|
||||
this.players.push(player);
|
||||
});
|
||||
|
||||
this.import(response_data);
|
||||
|
||||
if (this.finished === true)
|
||||
return 0;
|
||||
|
||||
this.config = new PongConfig(this.client);
|
||||
|
||||
let ret = await this.config.init();
|
||||
|
||||
if (ret !== 0)
|
||||
return ret;
|
||||
|
||||
this.time = new Time();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {CanvasRenderingContext2D} ctx
|
||||
*/
|
||||
drawSides(ctx)
|
||||
{
|
||||
this.walls.forEach(wall => {
|
||||
wall.draw(ctx);
|
||||
});
|
||||
|
||||
this.players.forEach(player => {
|
||||
player.draw(ctx);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {CanvasRenderingContext2D} ctx
|
||||
*/
|
||||
render(ctx)
|
||||
{
|
||||
ctx.clearRect(0, 0, this.config.MAP_SIZE_Y, this.config.MAP_SIZE_Y);
|
||||
|
||||
this.drawSides(ctx);
|
||||
this.ball.render(ctx);
|
||||
|
||||
ctx.strokeStyle = "#000000";
|
||||
ctx.lineWidth = this.config.STROKE_THICKNESS;
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} data
|
||||
*/
|
||||
send(data)
|
||||
{
|
||||
super.send(JSON.stringify(data));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} data
|
||||
*/
|
||||
async _receive(data)
|
||||
{
|
||||
console.log(data)
|
||||
|
||||
if (this._inited === false && data.detail === "init_game")
|
||||
{
|
||||
this._initGame(data);
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.detail === "update_player")
|
||||
this._updatePlayer(data);
|
||||
else if (data.detail === "update_ball")
|
||||
this._updateBall(data);
|
||||
else if (data.detail === "goal")
|
||||
await this._receiveGoal(data);
|
||||
else if (data.detail === "finish")
|
||||
await this._receiveFinish(data);
|
||||
else if (data.detail === "start")
|
||||
await this._receiveStart();
|
||||
}
|
||||
|
||||
async _receiveFinish(data)
|
||||
{
|
||||
const winner = this.players.find(player => player.id === data.winner_id)
|
||||
this.finished = true;
|
||||
await this._finishHandler(winner);
|
||||
}
|
||||
|
||||
|
||||
async _receiveStart()
|
||||
{
|
||||
this.started = true;
|
||||
await this._startHandler();
|
||||
}
|
||||
|
||||
async _receiveGoal(data)
|
||||
{
|
||||
const player = this.players.find((player) => player.id === data.player_id);
|
||||
|
||||
if (player === undefined)
|
||||
{
|
||||
console.error("error: unknown player.")
|
||||
return
|
||||
}
|
||||
|
||||
player.score.push(data.timestamp)
|
||||
console.log(player)
|
||||
|
||||
await this._goalHandler(player);
|
||||
}
|
||||
|
||||
_updatePlayer(data)
|
||||
{
|
||||
let player = this.players.find((player) => player.id === data.id);
|
||||
|
||||
player.import(data);
|
||||
}
|
||||
|
||||
_updateBall(data)
|
||||
{
|
||||
this.ball.import(data);
|
||||
}
|
||||
|
||||
_initGame(data)
|
||||
{
|
||||
data.walls.forEach((wall_data) => {
|
||||
this.walls.push(new Wall(this));
|
||||
});
|
||||
|
||||
this.import(data);
|
||||
|
||||
this._inited = true;
|
||||
}
|
||||
|
||||
async waitInit()
|
||||
{
|
||||
while (this._inited !== true)
|
||||
await sleep(100);
|
||||
}
|
||||
}
|
110
django/frontend/static/js/api/game/pong/PongMyPlayer.js
Normal file
110
django/frontend/static/js/api/game/pong/PongMyPlayer.js
Normal file
@ -0,0 +1,110 @@
|
||||
import { PongPlayer } from "./PongPlayer.js";
|
||||
import { Client } from "../../Client.js";
|
||||
import { Segment } from "./Segment.js";
|
||||
import { PongGame } from "./PongGame.js";
|
||||
import { Position } from "./Position.js";
|
||||
|
||||
export class PongMyPlayer extends PongPlayer
|
||||
{
|
||||
/**
|
||||
* @param {Client} client
|
||||
* @param {PongGame} game
|
||||
* @param {Segment} rail
|
||||
* @param {[Number]} score
|
||||
* @param {Position} position
|
||||
*/
|
||||
constructor(client, game, score, rail, position = new Position(0.5))
|
||||
{
|
||||
super(client, game, client.me.id, client.me.username, client.me.avatar, score, rail, position, true);
|
||||
/**
|
||||
* @type {Client}
|
||||
*/
|
||||
this.client = client;
|
||||
|
||||
/**
|
||||
* @type {PongGame}
|
||||
*/
|
||||
this.game;
|
||||
|
||||
this.upKeys = [];
|
||||
this.downKeys = [];
|
||||
|
||||
if (rail.start.x != rail.stop.x)
|
||||
{
|
||||
if (rail.start.x < rail.stop.x)
|
||||
{
|
||||
this.upKeys.push("a");
|
||||
this.downKeys.push("d");
|
||||
}
|
||||
else
|
||||
{
|
||||
this.upKeys.push("d");
|
||||
this.downKeys.push("a");
|
||||
}
|
||||
}
|
||||
if (rail.start.y != rail.stop.y)
|
||||
{
|
||||
if (rail.start.y < rail.stop.y)
|
||||
{
|
||||
this.upKeys.push("w");
|
||||
this.downKeys.push("s");
|
||||
}
|
||||
else
|
||||
{
|
||||
this.upKeys.push("s");
|
||||
this.downKeys.push("w");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {[string]} keys_pressed
|
||||
*/
|
||||
updatePaddle(keys_pressed)
|
||||
{
|
||||
let new_location = this.position.location;
|
||||
|
||||
keys_pressed.forEach(key => {
|
||||
if (this.downKeys.includes(key))
|
||||
new_location += this.game.config.PADDLE_SPEED_PER_SECOND_MAX * this.game.time.deltaTimeSecond();
|
||||
if (this.upKeys.includes(key))
|
||||
new_location -= this.game.config.PADDLE_SPEED_PER_SECOND_MAX * this.game.time.deltaTimeSecond();
|
||||
});
|
||||
|
||||
new_location = Math.max(0 + this.game.config.PADDLE_RATIO / 2, new_location);
|
||||
new_location = Math.min(1 - this.game.config.PADDLE_RATIO / 2, new_location);
|
||||
|
||||
if (this.position.location === new_location)
|
||||
return;
|
||||
|
||||
this.position.location = new_location;
|
||||
|
||||
this._sendPaddlePosition();
|
||||
}
|
||||
|
||||
_sendPaddlePosition()
|
||||
{
|
||||
this.game.send({"detail": "update_my_paddle_pos", ...{"time": this.game.time._currentFrame, "position": this.position}});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Position} newPosition
|
||||
*/
|
||||
updatePos(newPosition)
|
||||
{
|
||||
let position_verified = newPosition;
|
||||
|
||||
let time_diff = (this.time._current_frame - newPosition.time) / 1000;
|
||||
|
||||
let sign = this.position.location - newPosition.location >= 0 ? 1 : -1;
|
||||
|
||||
let distance = Math.abs(this.position.location - newPosition.location);
|
||||
|
||||
let distance_max = time_diff * this.game.config.paddle_speed_per_second_max;
|
||||
|
||||
if (distance > distance_max)
|
||||
position_verified.location = distance_max * sign;
|
||||
|
||||
this.position = position_verified;
|
||||
}
|
||||
}
|
102
django/frontend/static/js/api/game/pong/PongPlayer.js
Normal file
102
django/frontend/static/js/api/game/pong/PongPlayer.js
Normal file
@ -0,0 +1,102 @@
|
||||
import { APlayer } from "../APlayer.js";
|
||||
import { Point } from "./Point.js";
|
||||
import { Segment } from "./Segment.js";
|
||||
import { Client } from "../../Client.js";
|
||||
import { PongGame } from "./PongGame.js";
|
||||
import { Position } from "./Position.js";
|
||||
|
||||
export class PongPlayer extends APlayer
|
||||
{
|
||||
/**
|
||||
* @param {Number} id
|
||||
* @param {PongGame} game
|
||||
* @param {Segment} rail
|
||||
* @param {[Number]} score
|
||||
* @param {Position} position
|
||||
* @param {Boolean} isConnected
|
||||
* @param {String} username
|
||||
* @param {String} avatar
|
||||
* @param {Client} client
|
||||
* @param {Boolean} isEliminated
|
||||
*/
|
||||
constructor(client, game, id, username, avatar, score = [], rail = new Segment(game), position = new Position(0.5), isConnected, isEliminated)
|
||||
{
|
||||
super(client, game, id, username, avatar, isConnected)
|
||||
|
||||
/**
|
||||
* @type {Position}
|
||||
*/
|
||||
this.position = position;
|
||||
|
||||
/**
|
||||
* @type {[Number]}
|
||||
*/
|
||||
this.score = score;
|
||||
|
||||
/**
|
||||
* @type {Segment}
|
||||
*/
|
||||
this.rail = rail;
|
||||
|
||||
/**
|
||||
* @type {PongPlayer}
|
||||
*/
|
||||
this.game = game;
|
||||
|
||||
/**
|
||||
* @type {Boolean}
|
||||
*/
|
||||
this.isEliminated = isEliminated;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Number} new_position
|
||||
*/
|
||||
updatePos(new_position)
|
||||
{
|
||||
this.position = new_position;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {CanvasRenderingContext2D} ctx
|
||||
*/
|
||||
draw(ctx)
|
||||
{
|
||||
if (this.isConnected === false || this.isEliminated === true)
|
||||
{
|
||||
ctx.moveTo(this.rail.start.x, this.rail.start.y);
|
||||
ctx.lineTo(this.rail.stop.x, this.rail.stop.y);
|
||||
return;
|
||||
}
|
||||
|
||||
const diffX = this.rail.stop.x - this.rail.start.x,
|
||||
diffY = this.rail.stop.y - this.rail.start.y;
|
||||
|
||||
const railLength = this.rail.len(),
|
||||
paddleLength = railLength * this.game.config.PADDLE_RATIO;
|
||||
|
||||
const paddleCenter = new Point(this.rail.start.x + diffX * this.position.location,
|
||||
this.rail.start.y + diffY * this.position.location);
|
||||
|
||||
const paddleStartX = paddleCenter.x - (diffX * (paddleLength / 2 / railLength)),
|
||||
paddleStartY = paddleCenter.y - (diffY * (paddleLength / 2 / railLength)),
|
||||
paddleStopX = paddleCenter.x + (diffX * (paddleLength / 2 / railLength)),
|
||||
paddleStopY = paddleCenter.y + (diffY * (paddleLength / 2 / railLength));
|
||||
|
||||
let paddleStart = new Point(paddleStartX, paddleStartY),
|
||||
paddleStop = new Point (paddleStopX, paddleStopY);
|
||||
|
||||
let paddle = new Segment(this.game, paddleStart, paddleStop);
|
||||
|
||||
paddle.draw(ctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {[String]} additionalFieldList
|
||||
*/
|
||||
export(additionalFieldList = [])
|
||||
{
|
||||
super.export([...additionalFieldList, "position", "rail", "score"])
|
||||
}
|
||||
}
|
31
django/frontend/static/js/api/game/pong/Position.js
Normal file
31
django/frontend/static/js/api/game/pong/Position.js
Normal file
@ -0,0 +1,31 @@
|
||||
import { AExchangeable } from "../../AExchangable.js";
|
||||
import { Point } from "./Point.js";
|
||||
|
||||
export class Position extends AExchangeable
|
||||
{
|
||||
/**
|
||||
* @param {Point | Number} location
|
||||
* @param {Number} time
|
||||
*/
|
||||
constructor(location = new Point(), time)
|
||||
{
|
||||
super();
|
||||
/**
|
||||
* @type {Point | Number}
|
||||
*/
|
||||
this.location = location;
|
||||
|
||||
/**
|
||||
* @type {Number}
|
||||
*/
|
||||
this.time = time;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {[]} additionalFieldList
|
||||
*/
|
||||
export(additionalFieldList)
|
||||
{
|
||||
super.export([...additionalFieldList + "location", "time"]);
|
||||
}
|
||||
}
|
80
django/frontend/static/js/api/game/pong/Segment.js
Normal file
80
django/frontend/static/js/api/game/pong/Segment.js
Normal file
@ -0,0 +1,80 @@
|
||||
import { Point } from "./Point.js";
|
||||
import { AExchangeable } from "../../AExchangable.js";
|
||||
import { PongGame } from "./PongGame.js";
|
||||
import { renderCube } from "../../../3D/cube.js";
|
||||
|
||||
class Segment extends AExchangeable
|
||||
{
|
||||
/**
|
||||
* @param {Point} start
|
||||
* @param {Point} stop
|
||||
* @param {PongGame} game
|
||||
*/
|
||||
constructor(game, start = new Point(), stop = new Point())
|
||||
{
|
||||
super();
|
||||
|
||||
/**
|
||||
* @type {Point}
|
||||
*/
|
||||
this.start = start;
|
||||
|
||||
/**
|
||||
* @type {Point}
|
||||
*/
|
||||
this.stop = stop;
|
||||
|
||||
/**
|
||||
* @type {PongGame}
|
||||
*/
|
||||
this.game = game
|
||||
|
||||
}
|
||||
|
||||
angle()
|
||||
{
|
||||
let x = this.start.x - this.stop.x,
|
||||
y = this.start.y - this.stop.y;
|
||||
|
||||
return Math.atan2(y, x);
|
||||
}
|
||||
|
||||
len()
|
||||
{
|
||||
let x = this.start.x - this.stop.x,
|
||||
y = this.start.y - this.stop.y;
|
||||
|
||||
return (x ** 2 + y ** 2) ** (1 / 2);
|
||||
}
|
||||
|
||||
draw(ctx)
|
||||
{
|
||||
if(ctx instanceof CanvasRenderingContext2D)
|
||||
{
|
||||
ctx.moveTo(this.start.x, this.start.y);
|
||||
ctx.lineTo(this.stop.x, this.stop.y);
|
||||
}
|
||||
else if(ctx instanceof WebGLRenderingContext)
|
||||
{
|
||||
const size = this.game.config.BALL_SIZE * 2;
|
||||
const sizex = this.len() / 2;
|
||||
const posx = (this.start.x - this.game.config.MAP_CENTER_X);
|
||||
const posy = (this.start.y - this.game.config.MAP_CENTER_Y);
|
||||
renderCube(ctx, posx, 0, posy, -this.angle(), sizex, size, size);
|
||||
}
|
||||
else
|
||||
{
|
||||
alert('Unknown rendering context type');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {[String]} additionalFieldList
|
||||
*/
|
||||
export(additionalFieldList)
|
||||
{
|
||||
super.export([...additionalFieldList, "start", "stop"]);
|
||||
}
|
||||
}
|
||||
|
||||
export { Segment }
|
42
django/frontend/static/js/api/game/pong/Time.js
Normal file
42
django/frontend/static/js/api/game/pong/Time.js
Normal file
@ -0,0 +1,42 @@
|
||||
|
||||
|
||||
class Time
|
||||
{
|
||||
constructor()
|
||||
{
|
||||
/**
|
||||
* @type {Number}
|
||||
*/
|
||||
this._lastFrame = undefined;
|
||||
|
||||
/**
|
||||
* @type {Number}
|
||||
*/
|
||||
this._currentFrame = undefined;
|
||||
}
|
||||
|
||||
deltaTime()
|
||||
{
|
||||
if (this._lastFrame === undefined)
|
||||
return 0;
|
||||
return (this._currentFrame - this._lastFrame);
|
||||
}
|
||||
|
||||
deltaTimeSecond()
|
||||
{
|
||||
return this.deltaTime() / 1000;
|
||||
}
|
||||
|
||||
get_fps()
|
||||
{
|
||||
return 1 / this.deltaTimeSecond();
|
||||
}
|
||||
|
||||
new_frame()
|
||||
{
|
||||
this._lastFrame = this._currentFrame;
|
||||
this._currentFrame = Date.now();
|
||||
}
|
||||
}
|
||||
|
||||
export { Time };
|
44
django/frontend/static/js/api/game/pong/Wall.js
Normal file
44
django/frontend/static/js/api/game/pong/Wall.js
Normal file
@ -0,0 +1,44 @@
|
||||
import { Point } from "./Point.js";
|
||||
import { PongGame } from "./PongGame.js";
|
||||
import { Segment } from "./Segment.js";
|
||||
import { renderCube} from "../../../3D/cube.js"
|
||||
|
||||
export class Wall extends Segment
|
||||
{
|
||||
|
||||
/**
|
||||
* @param {PongGame} game
|
||||
* @param {Point} start
|
||||
* @param {Point} stop
|
||||
*/
|
||||
constructor(game, start, stop)
|
||||
{
|
||||
super(game, start, stop)
|
||||
|
||||
/**
|
||||
* @type {PongGame}
|
||||
*/
|
||||
this.game = game
|
||||
}
|
||||
|
||||
draw(ctx)
|
||||
{
|
||||
if(ctx instanceof CanvasRenderingContext2D)
|
||||
{
|
||||
ctx.moveTo(this.start.x, this.start.y);
|
||||
ctx.lineTo(this.stop.x, this.stop.y);
|
||||
}
|
||||
else if(ctx instanceof WebGLRenderingContext)
|
||||
{
|
||||
const size = this.game.config.BALL_SIZE * 2;
|
||||
const sizeX = this.len() / 2;
|
||||
const posX = (this.start.x - this.game.config.MAP_CENTER_X);
|
||||
const posY = (this.start.y - this.game.config.MAP_CENTER_Y);
|
||||
renderCube(ctx, posX, 0, posY, -this.angle(), sizeX, size, size);
|
||||
}
|
||||
else
|
||||
{
|
||||
alert('Unknown rendering context type');
|
||||
}
|
||||
}
|
||||
}
|
334
django/frontend/static/js/api/game/tictactoe/TicTacToeGame.js
Normal file
334
django/frontend/static/js/api/game/tictactoe/TicTacToeGame.js
Normal file
@ -0,0 +1,334 @@
|
||||
import { client, lang } from "../../../index.js";
|
||||
|
||||
import { AGame } from "../AGame.js";
|
||||
|
||||
class TicTacToe
|
||||
{
|
||||
constructor(height, width, gap, rectsize, canvas, game_id)
|
||||
{
|
||||
this.height = height;
|
||||
this.width = width;
|
||||
this.gap = gap;
|
||||
this.rectsize = rectsize;
|
||||
this.map = [[],[],[],[],[],[],[],[],[]];
|
||||
for (let i = 0; i < 9; i++)
|
||||
for (let j = 0; j < 9; j++)
|
||||
this.map[i].push(-1);
|
||||
this.game_id = game_id;
|
||||
this.game = new AGame(client, game_id, this.onReceive.bind(this), this.uninit.bind(this), "tictactoe")
|
||||
this.canvas = canvas
|
||||
this.context = this.canvas.getContext("2d");
|
||||
this.sign;
|
||||
this.currentMorpion = 4;
|
||||
this.turn;
|
||||
}
|
||||
|
||||
async init()
|
||||
{
|
||||
await this.game.join();
|
||||
this.canvas.addEventListener("mousedown", (event, morpion = this) => this.onClick(event, morpion));
|
||||
}
|
||||
|
||||
async uninit()
|
||||
{
|
||||
this.canvas.removeEventListener("mousedown", (event, morpion = this) => this.onClick(event, morpion));
|
||||
this.game.leave()
|
||||
}
|
||||
|
||||
async onReceive(messageData)
|
||||
{
|
||||
switch (messageData.detail)
|
||||
{
|
||||
case 'x':
|
||||
case 'o':
|
||||
this.sign = messageData.detail;
|
||||
this.turn = messageData.detail == "x";
|
||||
break;
|
||||
|
||||
case 'game_start':
|
||||
this.game.started = true;
|
||||
this.game.finished = false;
|
||||
if (this.turn)
|
||||
this.setOutline(4, false);
|
||||
this.printTimer();
|
||||
break;
|
||||
|
||||
case 'game_move':
|
||||
if (messageData.targetMorpion === undefined || messageData.targetCase === undefined)
|
||||
return ;
|
||||
this.map[messageData.targetMorpion][messageData.targetCase] = (this.sign == "x") ? 1 : 0;
|
||||
this.printSign(messageData.targetMorpion, messageData.targetCase, (this.sign == "x") ? "o" : "x");
|
||||
this.setOutline(this.currentMorpion, false);
|
||||
this.printTimer();
|
||||
break;
|
||||
|
||||
case 'game_end':
|
||||
this.game.finished = true;
|
||||
this.canvas.removeEventListener("mousedown", (event, morpion = this) => this.onClick(event, morpion));
|
||||
this.printWin(messageData.winning_sign);
|
||||
break;
|
||||
case 'catchup':
|
||||
this.map = messageData.morpion;
|
||||
for (let i = 0; i < 9; i++)
|
||||
{
|
||||
for (let j = 0; j < 9; j++)
|
||||
{
|
||||
if (this.map[i][j] != -1)
|
||||
this.printSign(i, j, this.map[i][j])
|
||||
}
|
||||
}
|
||||
this.turn = (messageData.turn == this.sign);
|
||||
this.currentMorpion = messageData.currentMorpion;
|
||||
if (this.turn)
|
||||
this.setOutline(this.currentMorpion, false);
|
||||
}
|
||||
}
|
||||
|
||||
printWin(winning_sign)
|
||||
{
|
||||
this.context.beginPath();
|
||||
this.context.fillStyle = "white";
|
||||
this.context.fillRect(this.width / 2 - 200, this.height - this.gap + 10, 400, 80);
|
||||
this.context.closePath();
|
||||
this.context.beginPath();
|
||||
this.context.font = `20px sans-serif`;
|
||||
this.context.fillStyle = (winning_sign == "o") ? "red" : "green";
|
||||
this.context.fillText((winning_sign == "o") ? lang.get("morpionWin") + "O" : lang.get("morpionWin") + "X", this.width / 2 - 85, this.height - this.gap / 2 + 10, 180);
|
||||
}
|
||||
|
||||
printTimer()
|
||||
{
|
||||
let sec = 20;
|
||||
let turn = this.turn
|
||||
if (this.turn)
|
||||
{
|
||||
this.context.beginPath();
|
||||
this.context.fillStyle = "white";
|
||||
this.context.font = `20px Verdana`;
|
||||
this.context.fillText(sec, this.width / 2, this.gap / 2);
|
||||
this.context.closePath();
|
||||
sec--;
|
||||
}
|
||||
let id = setInterval(() =>
|
||||
{
|
||||
this.context.beginPath();
|
||||
this.context.fillStyle = "#1a1a1d";
|
||||
this.context.fillRect(this.width / 2 - 40, 0, this.width / 2 + 40, this.gap - 10)
|
||||
this.context.closePath();
|
||||
if (sec == 0 || turn != this.turn || this.game.finished)
|
||||
{
|
||||
clearInterval(id);
|
||||
if (sec == 0 && !this.turn && this.game.finished == false)
|
||||
this.game.send(JSON.stringify({"timerIsDue" : this.sign}))
|
||||
return;
|
||||
}
|
||||
if (this.turn)
|
||||
{
|
||||
this.context.beginPath();
|
||||
this.context.fillStyle = "white";
|
||||
this.context.font = `20px Verdana`;
|
||||
this.context.fillText(sec, this.width / 2, this.gap / 2);
|
||||
this.context.closePath();
|
||||
}
|
||||
sec--;
|
||||
}, 1000
|
||||
)
|
||||
}
|
||||
checkWin()
|
||||
{
|
||||
for (let i = 0; i < 9; i++)
|
||||
{
|
||||
for (let j = 0; j < 3; j++)
|
||||
{
|
||||
if (this.map[i][j] == this.map[i][j + 3] && this.map[i][j + 3] == this.map[i][j + 6])
|
||||
return (this.map[i][j])
|
||||
}
|
||||
for (let j = 0; i < 9; i += 3)
|
||||
{
|
||||
if (this.map[i][j] == this.map[i][j + 1] && this.map[i][j + 1] == this.map[i][j + 2])
|
||||
return (this.map[i][j])
|
||||
}
|
||||
if (this.map[i][0] == this.map[i][4] && this.map[i][4] == this.map[i][8])
|
||||
return (this.map[i][0]);
|
||||
if (this.map[i][6] == this.map[i][4] && this.map[i][4] == this.map[i][2])
|
||||
return (this.map[i][6]);
|
||||
return -1
|
||||
}
|
||||
}
|
||||
|
||||
onClick(event, morpion)
|
||||
{
|
||||
let x = event.offsetX;
|
||||
let y = event.offsetY;
|
||||
let targetMorpion, targetCase;
|
||||
|
||||
if (this.game.finished)
|
||||
{
|
||||
return;
|
||||
}
|
||||
targetMorpion = morpion.findPlace(x, this) + morpion.findPlace(y, this) * 3;
|
||||
if (morpion.findPlace(x, this) < 0 || morpion.findPlace(y, this) < 0)
|
||||
return -1;
|
||||
targetCase = morpion.findSquare(x, this.rectsize * 3 * morpion.findPlace(x, this) + this.gap, this) + morpion.findSquare(y, this.rectsize * 3 * morpion. findPlace(y, this) + this.gap, this) * 3;
|
||||
|
||||
if (morpion.checkCase(targetMorpion, targetCase))
|
||||
{
|
||||
morpion.setOutline(this.currentMorpion, true);
|
||||
morpion.sendCase(targetMorpion, targetCase);
|
||||
morpion.printTimer()
|
||||
}
|
||||
else
|
||||
morpion.incorrectCase();
|
||||
}
|
||||
|
||||
checkCase(targetMorpion, targetCase)
|
||||
{
|
||||
return (this.map[targetMorpion][targetCase] == -1 && this.turn == true && targetMorpion == this.currentMorpion);
|
||||
}
|
||||
|
||||
incorrectCase()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
sendCase(targetMorpion, targetCase)
|
||||
{
|
||||
this.map[targetMorpion][targetCase] = (this.sign == "x") ? 0 : 1;
|
||||
this.currentMorpion = targetCase;
|
||||
this.printSign(targetMorpion, targetCase, this.sign);
|
||||
this.game.send(JSON.stringify({"targetMorpion" : targetMorpion, "targetCase" : targetCase, "sign" : this.sign}));
|
||||
this.turn = !this.turn;
|
||||
}
|
||||
|
||||
printSign(targetMorpion, targetCase, sign)
|
||||
{
|
||||
let targetX = (this.gap + targetMorpion % 3 * this.rectsize * 3) + (targetCase % 3 * this.rectsize);
|
||||
let targetY = (this.gap + Math.floor(targetMorpion / 3) * this.rectsize * 3) + (Math.floor(targetCase / 3) * this.rectsize);
|
||||
if (sign == "x")
|
||||
{
|
||||
this.context.beginPath();
|
||||
this.context.strokeStyle = "green";
|
||||
this.context.moveTo(targetX + 10, targetY + 10);
|
||||
this.context.lineTo(targetX + 40, targetY + 40);
|
||||
this.context.moveTo(targetX + 40, targetY + 10);
|
||||
this.context.lineTo(targetX + 10, targetY + 40);
|
||||
this.context.stroke();
|
||||
this.context.closePath();
|
||||
}
|
||||
else
|
||||
{
|
||||
this.context.beginPath();
|
||||
this.context.strokeStyle = "red";
|
||||
targetX += this.rectsize / 2;
|
||||
targetY += this.rectsize / 2;
|
||||
this.context.arc(targetX, targetY, 15, 0, 2 * Math.PI);
|
||||
this.context.stroke();
|
||||
this.context.closePath();
|
||||
}
|
||||
if (sign != this.sign)
|
||||
this.turn = true;
|
||||
}
|
||||
|
||||
findPlace(x, morpion)
|
||||
{
|
||||
if (x <= this.gap || x >= this.gap + this.rectsize * 9)
|
||||
return -1;
|
||||
if (x <= this.gap + this.rectsize * 3)
|
||||
return 0;
|
||||
if (x >= this.gap + this.rectsize * 3 && x <= this.gap + this.rectsize * 6)
|
||||
return 1;
|
||||
if (x >= this.gap + this.rectsize * 6)
|
||||
return 2;
|
||||
return -1;
|
||||
}
|
||||
|
||||
findSquare(x, gap, morpion)
|
||||
{
|
||||
if (x <= gap + this.rectsize)
|
||||
return 0;
|
||||
if (x >= gap + this.rectsize && x <= gap + this.rectsize * 2)
|
||||
return 1;
|
||||
if (x >= gap + this.rectsize * 2)
|
||||
return 2;
|
||||
return -1;
|
||||
}
|
||||
|
||||
setOutline(targetMorpion, clear)
|
||||
{
|
||||
let targetX = (this.gap + targetMorpion % 3 * this.rectsize * 3);
|
||||
let targetY = (this.gap + Math.floor(targetMorpion / 3) * this.rectsize * 3);
|
||||
if (this.game.finished)
|
||||
return;
|
||||
if (!clear)
|
||||
{
|
||||
this.context.beginPath();
|
||||
this.context.strokeStyle = (this.sign == "x") ? "green" : "red";
|
||||
this.context.rect(targetX, targetY, this.rectsize * 3, this.rectsize * 3)
|
||||
this.context.stroke();
|
||||
this.context.closePath();
|
||||
}
|
||||
else
|
||||
{
|
||||
this.context.beginPath();
|
||||
this.context.strokeStyle = `rgb(230 230 230)`;
|
||||
this.context.rect(targetX, targetY, this.rectsize * 3, this.rectsize * 3)
|
||||
this.context.stroke();
|
||||
this.context.closePath();
|
||||
}
|
||||
}
|
||||
|
||||
DrawSuperMorpion()
|
||||
{
|
||||
this.context.fillStyle = "#1a1a1d";
|
||||
this.context.roundRect(0, 0, this.canvas.width, this.canvas.height, 20);
|
||||
this.context.fill();
|
||||
for (let i = 1, x = this.gap, y = this.gap; i <= 9; i++)
|
||||
{
|
||||
this.DrawMorpion(x, y);
|
||||
x += this.rectsize * 3;
|
||||
if (i % 3 == 0)
|
||||
{
|
||||
y += this.rectsize * 3;
|
||||
x = this.gap;
|
||||
}
|
||||
}
|
||||
this.context.lineWidth = 6;
|
||||
for (let i = 0; i < 4; i++)
|
||||
{
|
||||
this.context.beginPath();
|
||||
this.context.strokeStyle = `rgb(230 230 230)`;
|
||||
this.context.moveTo(this.gap + i * this.rectsize * 3, this.gap - 3);
|
||||
this.context.lineTo(this.gap + i * this.rectsize * 3, this.canvas.height - this.gap + 3);
|
||||
this.context.stroke();
|
||||
this.context.closePath();
|
||||
};
|
||||
for (let i = 0; i < 4; i++)
|
||||
{
|
||||
this.context.beginPath();
|
||||
this.context.strokeStyle = `rgb(230 230 230)`;
|
||||
this.context.moveTo(this.gap, this.gap + i * this.rectsize * 3);
|
||||
this.context.lineTo(this.canvas.height - this.gap, this.gap + i * this.rectsize * 3);
|
||||
this.context.stroke();
|
||||
this.context.closePath();
|
||||
}
|
||||
}
|
||||
|
||||
DrawMorpion(start_x, start_y)
|
||||
{
|
||||
this.context.beginPath();
|
||||
this.context.strokeStyle = `rgb(200 200 200)`;
|
||||
for (let i = 1, x = 0, y = 0; i <= 9; i++)
|
||||
{
|
||||
this.context.strokeRect(start_x + x, start_y + y, this.rectsize, this.rectsize);
|
||||
x += this.rectsize;
|
||||
if (i % 3 == 0)
|
||||
{
|
||||
y += this.rectsize;
|
||||
x = 0;
|
||||
}
|
||||
}
|
||||
this.context.closePath();
|
||||
}
|
||||
}
|
||||
|
||||
export { TicTacToe };
|
6314
django/frontend/static/js/bootstrap/bootstrap.bundle.js
vendored
Normal file
6314
django/frontend/static/js/bootstrap/bootstrap.bundle.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
7
django/frontend/static/js/bootstrap/bootstrap.bundle.min.js
vendored
Normal file
7
django/frontend/static/js/bootstrap/bootstrap.bundle.min.js
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
4447
django/frontend/static/js/bootstrap/bootstrap.esm.js
vendored
Normal file
4447
django/frontend/static/js/bootstrap/bootstrap.esm.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
django/frontend/static/js/bootstrap/bootstrap.esm.js.map
Normal file
1
django/frontend/static/js/bootstrap/bootstrap.esm.js.map
Normal file
File diff suppressed because one or more lines are too long
7
django/frontend/static/js/bootstrap/bootstrap.esm.min.js
vendored
Normal file
7
django/frontend/static/js/bootstrap/bootstrap.esm.min.js
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
4494
django/frontend/static/js/bootstrap/bootstrap.js
vendored
Normal file
4494
django/frontend/static/js/bootstrap/bootstrap.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
django/frontend/static/js/bootstrap/bootstrap.js.map
Normal file
1
django/frontend/static/js/bootstrap/bootstrap.js.map
Normal file
File diff suppressed because one or more lines are too long
7
django/frontend/static/js/bootstrap/bootstrap.min.js
vendored
Normal file
7
django/frontend/static/js/bootstrap/bootstrap.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
django/frontend/static/js/bootstrap/bootstrap.min.js.map
Normal file
1
django/frontend/static/js/bootstrap/bootstrap.min.js.map
Normal file
File diff suppressed because one or more lines are too long
1
django/frontend/static/js/chartjs/chart.umd.min.js
vendored
Normal file
1
django/frontend/static/js/chartjs/chart.umd.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
162
django/frontend/static/js/index.js
Normal file
162
django/frontend/static/js/index.js
Normal file
@ -0,0 +1,162 @@
|
||||
import { Client } from "./api/Client.js";
|
||||
|
||||
import Search from "./views/Search.js";
|
||||
import HomeView from "./views/HomeView.js";
|
||||
import LogoutView from "./views/accounts/LogoutView.js";
|
||||
|
||||
import PongOnlineView from "./views/PongOnlineView.js"
|
||||
import { PongOfflineView } from "./views/PongOfflineView.js"
|
||||
|
||||
import { TicTacToeOnlineView } from "./views/TicTacToeOnlineView.js"
|
||||
import { TicTacToeOfflineView } from "./views/TicTacToeOfflineView.js"
|
||||
|
||||
import PageNotFoundView from './views/PageNotFoundView.js' ;
|
||||
|
||||
import AbstractRedirectView from "./views/abstracts/AbstractRedirectView.js";
|
||||
import SettingsView from "./views/SettingsView.js";
|
||||
import ProfilePageView from "./views/ProfilePageView.js";
|
||||
import MatchMakingView from "./views/MatchMakingView.js";
|
||||
import AuthenticationView from "./views/accounts/AuthenticationView.js";
|
||||
|
||||
let client = new Client(location.origin);
|
||||
let lang = client.lang;
|
||||
|
||||
let lastView;
|
||||
let lastPageUrlBeforeLogin;
|
||||
|
||||
const pathToRegex = path => new RegExp("^" + path.replace(/\//g, "\\/").replace(/:\w+/g, "(.+)") + "$");
|
||||
|
||||
const getParams = match => {
|
||||
|
||||
const values = match.result.slice(1);
|
||||
const keys = Array.from(match.route.path.matchAll(/:(\w+)/g)).map(result => result[1]);
|
||||
return Object.fromEntries(keys.map((key, i) => {
|
||||
return [key, values[i]];
|
||||
}));
|
||||
};
|
||||
|
||||
const navigateTo = async (uri) => {
|
||||
|
||||
history.pushState(null, null, uri);
|
||||
|
||||
if (await router(uri) !== 0)
|
||||
return;
|
||||
|
||||
let link = document.querySelector('a[href=\'' + location.pathname + '\']');
|
||||
if (link) {
|
||||
document.querySelector('[data-link].active')?.classList.remove('active');
|
||||
link.classList.add('active');
|
||||
}
|
||||
};
|
||||
|
||||
const reloadView = async _ => {
|
||||
await lastView?.leavePage();
|
||||
await renderView(lastView);
|
||||
};
|
||||
|
||||
async function renderView(view)
|
||||
{
|
||||
let content = await view?.getHtml();
|
||||
if (content == null)
|
||||
return 1;
|
||||
|
||||
view.setTitle();
|
||||
document.querySelector("#app").innerHTML = content;
|
||||
|
||||
let error_code = await view.postInit();
|
||||
|
||||
if (error_code === 404)
|
||||
renderView(new PageNotFoundView());
|
||||
else if (error_code === 403)
|
||||
this._client._update_logged(false);
|
||||
}
|
||||
|
||||
const router = async(uri) => {
|
||||
|
||||
const routes = [
|
||||
{ path: "/", view: HomeView},
|
||||
{ path: "/profiles/:username", view: ProfilePageView },
|
||||
{ path: "/login", view: AuthenticationView },
|
||||
{ path: "/register", view: AuthenticationView },
|
||||
{ path: "/logout", view: LogoutView },
|
||||
{ path: "/search", view: Search },
|
||||
{ path: "/home", view: HomeView },
|
||||
{ path: "/settings", view: SettingsView },
|
||||
{ path: "/matchmaking", view: MatchMakingView },
|
||||
{ path: "/games/pong/offline", view: PongOfflineView },
|
||||
{ path: "/games/pong/:id", view: PongOnlineView },
|
||||
{ path: "/games/tictactoe/offline", view: TicTacToeOfflineView },
|
||||
{ path: "/games/tictactoe/:id", view: TicTacToeOnlineView },
|
||||
];
|
||||
|
||||
// Test each route for potential match
|
||||
const potentialMatches = routes.map(route => {
|
||||
return {
|
||||
route: route,
|
||||
result: uri.match(pathToRegex(route.path))
|
||||
};
|
||||
});
|
||||
|
||||
let match = potentialMatches.find(potentialMatch => potentialMatch.result !== null);
|
||||
|
||||
if (!match) {
|
||||
match = {
|
||||
route: {
|
||||
path: uri,
|
||||
view: PageNotFoundView
|
||||
},
|
||||
result: [uri]
|
||||
};
|
||||
}
|
||||
|
||||
if (lastView !== undefined)
|
||||
await lastView.leavePage();
|
||||
|
||||
const view = new match.route.view(getParams(match), lastPageUrlBeforeLogin);
|
||||
|
||||
if (!(view instanceof AuthenticationView) && ! (view instanceof LogoutView))
|
||||
lastPageUrlBeforeLogin = uri;
|
||||
|
||||
if (view instanceof AbstractRedirectView && await view.redirect())
|
||||
return 1;
|
||||
|
||||
lastView = view;
|
||||
|
||||
if (await renderView(view))
|
||||
return 1;
|
||||
|
||||
return 0;
|
||||
};
|
||||
|
||||
window.addEventListener("popstate", function() {router(location.pathname);});
|
||||
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
document.body.addEventListener("click", e => {
|
||||
if (e.target.matches("[data-link]")) {
|
||||
e.preventDefault();
|
||||
document.querySelector('[data-link].nav-link.active')?.classList.remove('active');
|
||||
if (e.target.classList.contains('nav-link'))
|
||||
e.target.classList.add('active');
|
||||
navigateTo(e.target.href.slice(location.origin.length));
|
||||
}
|
||||
});
|
||||
|
||||
//Languages
|
||||
await lang.waitLoading();
|
||||
Array.from(document.getElementById('languageSelector').children).forEach(el => {
|
||||
el.onclick = async _ => {
|
||||
if (await lang.changeLanguage(el.value))
|
||||
return;
|
||||
document.querySelector('#languageSelector > .active')?.classList.remove('active');
|
||||
el.classList.add('active');
|
||||
};
|
||||
});
|
||||
document.querySelector(`#languageSelector > [value=${lang.chosenLang}]`)
|
||||
?.classList.add('active');
|
||||
|
||||
await client.isAuthenticated();
|
||||
router(location.pathname);
|
||||
document.querySelector('a[href=\'' + location.pathname + '\']')?.classList.add('active');
|
||||
});
|
||||
|
||||
export { client, lang, lastView, navigateTo, reloadView };
|
85
django/frontend/static/js/lang/cr.json
Normal file
85
django/frontend/static/js/lang/cr.json
Normal file
@ -0,0 +1,85 @@
|
||||
{
|
||||
"navbarSearch": "cherchbeh",
|
||||
"navbarHome": "Quoicoubouse",
|
||||
"navbarLogin": "Quoicouconnec",
|
||||
"navbarRegister": "Quoicougistré",
|
||||
"navbarProfile": "Mon crampté Profile",
|
||||
"navbarSettings": "Roue Crampté",
|
||||
"navbarLogout": "Déconnexion crampté",
|
||||
"homeWindowTitle": "Quoicoubouse",
|
||||
"homeTitle": "Quoicoubouse",
|
||||
"homeOnline": "Jouer en crampté",
|
||||
"homeOffline": "Jouer hors crampté",
|
||||
"homeSettings": "Roue Crampté",
|
||||
"homeLogout": "Déconnexion crampté",
|
||||
"loginWindowTitle": "Quoicouconnec",
|
||||
"loginFormTitle": "Quoicouconnec",
|
||||
"loginFormUsername": "Nom d'crampté",
|
||||
"loginFormPassword": "Mot de crampté",
|
||||
"loginFormButton": "Quoicouconnec",
|
||||
"loginNoAccount": "Pas de compte encore crampté?",
|
||||
"loginRegister": "Quoicougistré",
|
||||
"errorEmptyField": "Ce champ ne peut pas être vide crampté.",
|
||||
"logoutWindowTitle": "Déconnexion crampté",
|
||||
"registerWindowTitle": "Quoicougistré",
|
||||
"registerFormTitle": "Quoicougistré",
|
||||
"registerFormUsername": "Nom d'crampté",
|
||||
"registerFormPassword": "Mot de crampté",
|
||||
"registerFormButton": "Quoicougistré",
|
||||
"registerAlreadyAccount": "Déjà un compte crampté?",
|
||||
"registerLogin": "Quoicouconnec",
|
||||
"404WindowTitle": "Pas crampté",
|
||||
"SearchWindowTitle": "cherchbeh",
|
||||
"profileAddFriend": "Demander le cramptéman",
|
||||
"profileRemoveFriend": "Supprimer le cramptéman",
|
||||
"profileDenyRequest": "Refuser le cramptéman",
|
||||
"profileAcceptRequest": "Accepter le cramptéman",
|
||||
"profileUnblock": "Quoicoudebloquer",
|
||||
"profileBlock": "Quoicoubloquer",
|
||||
"gameGoalTaken": "Tu es quoicoucringe",
|
||||
"gamePlayersListName": "Crampteurs",
|
||||
"ticTacToe": "Quoicoumorpion hors crampté",
|
||||
"gamemodeChoice" : "Mode de crampté",
|
||||
"ticTacToeOnline" : "Quoicoumorpion crampté",
|
||||
"ticTacToeTitle": "Cramption",
|
||||
"ruleTitle" : "Règles cramptés",
|
||||
"ruleBase" : "cramptun. Vous devez quouicougagner sur une des 9 quoicougrilles pour gagner la croustipartie",
|
||||
"ruleMovement" : "quoicoudeux. Vous quoicommencez sur le morpion quoicoucentral, et jouez sur le quoicoumorpion correspondant a votre croustichoix a votre prochain cramptour",
|
||||
"ruleDraw" : "cramptrois. Si votre quoicouchoix rempli entièrement un quoicoumorpion et provoque une cramptégalité, vous perdez",
|
||||
"morpionWin" : "Le quoicougagnant est : ",
|
||||
"matchmakingTitle": "Matchmaking crampté",
|
||||
"matchmakingStartSearch": "Cramptrouver une partie",
|
||||
"matchmakingStopSearch": "Crampter le matchmaking",
|
||||
"matchmakingNbPlayers": "Nombre de crampteurs",
|
||||
"TournamentCreateTitle": "Créer un cramptournoi",
|
||||
"TournamentCreateButton": "Créer le cramptournoi",
|
||||
"TournamentCreateTournamentName": "Nom du cramptournoi",
|
||||
"TournamentCreateNbPlayerByGame": "Nombre de crampteurs en crampté",
|
||||
"TournamentCreateNbPlayer": "Nombre de crampteurs dans le cramptournoi",
|
||||
|
||||
"settingsWindowTitle": "Roue crampté",
|
||||
"settingsAvatarTitle": "Avatar",
|
||||
"settingsAvatarSave": "Cramptregistrer",
|
||||
"settingsAvatarDelete": "Décrampté",
|
||||
"settingsAvatarSaved": "Avatar crampté.",
|
||||
"settingsAvatarDeleted": "Avatar décrampté.",
|
||||
"settingsAvatarDeleteError": "Une cramptérreur s'est produite.",
|
||||
"settingsAvatarTooLarge": "L'image est trop crampté",
|
||||
"settingsTitle": "Paramètres de crampté",
|
||||
"settingsUsername": "Nom d'crampté",
|
||||
"settingsUsernameSave": "Cramptregistrer",
|
||||
"settingsUsernameSaved": "Nom d'crampté cramptregistré.",
|
||||
"settingsPasswordTitle": "Modifier le crampté de passe",
|
||||
"settingsCurrentPassword": "Mot de crampté actuel",
|
||||
"settingsNewPassword": "Nouveau crampté de passe",
|
||||
"settingsRepeatNewPassword": "Répéter nouveau crampte de passe",
|
||||
"settingsPasswordSave": "Cramptregistrer",
|
||||
"settingsPasswordSaved": "Nouveau crampté de passe enregistré.",
|
||||
"settingsDeleteButton": "Supprimer le crousticompte",
|
||||
"settingsDeleteTitle": "Êtes vous cramptsûr ? Cette action est permanente !!",
|
||||
"settingsDeleteConfirm": "Crampté de passe nécessaire pour confirmer la suppression:",
|
||||
"settingsDeleteInput": "Cranpté de passe",
|
||||
"settingsDeleteCancel": "Annuler",
|
||||
"settingsDeleteSuccess": "Crousticompte décrampté avec succès."
|
||||
}
|
||||
|
83
django/frontend/static/js/lang/en.json
Normal file
83
django/frontend/static/js/lang/en.json
Normal file
@ -0,0 +1,83 @@
|
||||
{
|
||||
"navbarSearch": "Search",
|
||||
"navbarHome": "Home",
|
||||
"navbarLogin": "Login",
|
||||
"navbarRegister": "Register",
|
||||
"navbarProfile": "My Profile",
|
||||
"navbarSettings": "Settings",
|
||||
"navbarLogout": "Logout",
|
||||
"homeWindowTitle": "Home",
|
||||
"homeTitle": "Home",
|
||||
"homeOnline": "Play online",
|
||||
"homeOffline": "Play offline",
|
||||
"homeSettings": "Settings",
|
||||
"homeLogout": "Logout",
|
||||
"loginWindowTitle": "Login",
|
||||
"loginFormTitle": "Login",
|
||||
"loginFormUsername": "Username",
|
||||
"loginFormPassword": "Password",
|
||||
"loginFormButton": "Login",
|
||||
"loginNoAccount": "No account yet?",
|
||||
"loginRegister": "Register",
|
||||
"errorEmptyField": "This field may not be blank.",
|
||||
"logoutWindowTitle": "Logout",
|
||||
"registerWindowTitle": "Register",
|
||||
"registerFormTitle": "Register",
|
||||
"registerFormUsername": "Username",
|
||||
"registerFormPassword": "Password",
|
||||
"registerFormButton": "Register",
|
||||
"registerAlreadyAccount": "Already have an account?",
|
||||
"registerLogin": "Login",
|
||||
"404WindowTitle": "Not Found",
|
||||
"SearchWindowTitle": "Search",
|
||||
"profileAddFriend": "Ask Friend",
|
||||
"profileRemoveFriend": "Remove Friend",
|
||||
"profileDenyRequest": "Decline Friend",
|
||||
"profileAcceptRequest": "Accept Friend",
|
||||
"profileUnblock": "Unblock",
|
||||
"profileBlock": "Block",
|
||||
"gameGoalTaken": "Goal Taken",
|
||||
"gamePlayersListName": "Players",
|
||||
"ticTacToe": "TicTacToe offline",
|
||||
"ticTacToeTitle": "TicTacToe",
|
||||
"gamemodeChoice" : "Gamemode",
|
||||
"ruleTitle" : "Rules",
|
||||
"ruleBase" : "1. Win on one of the 9 tictactoe to win the game",
|
||||
"ruleMovement" : "2. You start on the central tictactoe, and play on the one corresponding to your choice on the next turn",
|
||||
"ruleDraw" : "3. If your play cause a tictactoe to be full and a draw, you lose the game",
|
||||
"morpionWin" : "The winner is : ",
|
||||
"matchmakingTitle": "Matchmaking",
|
||||
"matchmakingStartSearch": "Find a game",
|
||||
"matchmakingStopSearch": "Stop matchmaking",
|
||||
"matchmakingNbPlayers": "Number of players",
|
||||
"TournamentCreateTitle": "Create tournament",
|
||||
"TournamentCreateButton": "Create tournament",
|
||||
"TournamentCreateTournamentName": "Tournament Name",
|
||||
"TournamentCreateNbPlayerByGame": "Number of player in a game",
|
||||
"TournamentCreateNbPlayer": "Number of players in the tournament",
|
||||
|
||||
"settingsWindowTitle": "Settings",
|
||||
"settingsAvatarTitle": "Avatar",
|
||||
"settingsAvatarSave": "Save",
|
||||
"settingsAvatarDelete": "Delete",
|
||||
"settingsAvatarSaved": "Avatar saved.",
|
||||
"settingsAvatarDeleted": "Avatar deleted.",
|
||||
"settingsAvatarDeleteError": "Something went wrong.",
|
||||
"settingsAvatarTooLarge": "Image is too large.",
|
||||
"settingsTitle": "Account settings",
|
||||
"settingsUsername": "Username",
|
||||
"settingsUsernameSave": "Save",
|
||||
"settingsUsernameSaved": "Username saved.",
|
||||
"settingsPasswordTitle": "Change password",
|
||||
"settingsCurrentPassword": "Current password",
|
||||
"settingsNewPassword": "New password",
|
||||
"settingsRepeatNewPassword": "Repeat new password",
|
||||
"settingsPasswordSave": "Save",
|
||||
"settingsPasswordSaved": "New password has been saved.",
|
||||
"settingsDeleteButton": "Delete account",
|
||||
"settingsDeleteTitle": "Are you sure ? This action is permanent !!",
|
||||
"settingsDeleteConfirm": "Please enter your password to confirm deletion:",
|
||||
"settingsDeleteInput": "Password",
|
||||
"settingsDeleteCancel": "Cancel",
|
||||
"settingsDeleteSuccess": "Account successfully deleted."
|
||||
}
|
83
django/frontend/static/js/lang/fr.json
Normal file
83
django/frontend/static/js/lang/fr.json
Normal file
@ -0,0 +1,83 @@
|
||||
{
|
||||
"navbarSearch": "Recherche",
|
||||
"navbarHome": "Maison",
|
||||
"navbarLogin": "Connexion",
|
||||
"navbarRegister": "S'inscrire",
|
||||
"navbarProfile": "Mon Profil",
|
||||
"navbarSettings": "Paramètres",
|
||||
"navbarLogout": "Déconnexion",
|
||||
"homeWindowTitle": "Maison",
|
||||
"homeTitle": "Maison",
|
||||
"homeOnline": "Jouer en ligne",
|
||||
"homeOffline": "Jouer hors ligne",
|
||||
"homeSettings": "Paramètres",
|
||||
"homeLogout": "Déconnexion",
|
||||
"loginWindowTitle": "Connexion",
|
||||
"loginFormTitle": "Connexion",
|
||||
"loginFormUsername": "Nom d'utilisateur",
|
||||
"loginFormPassword": "Mot de passe",
|
||||
"loginFormButton": "Connexion",
|
||||
"loginNoAccount": "Pas de compte?",
|
||||
"loginRegister": "S'inscrire",
|
||||
"errorEmptyField": "Ce champ ne peut être vide.",
|
||||
"logoutWindowTitle": "Déconnexion",
|
||||
"registerWindowTitle": "S'inscrire",
|
||||
"registerFormTitle": "S'inscrire",
|
||||
"registerFormUsername": "Nom d'utilisateur",
|
||||
"registerFormPassword": "Mot de passe",
|
||||
"registerFormButton": "S'inscrire",
|
||||
"registerAlreadyAccount": "Déjà un compte?",
|
||||
"registerLogin": "Connexion",
|
||||
"404WindowTitle": "Pas trouvé",
|
||||
"SearchWindowTitle": "Recherche",
|
||||
"profileAddFriend": "Demander en ami",
|
||||
"profileRemoveFriend": "Retirer l'ami",
|
||||
"profileDenyRequest": "Refuser l'ami",
|
||||
"profileAcceptRequest": "Accepter l'ami",
|
||||
"profileUnblock": "Débloquer",
|
||||
"profileBlock": "Bloquer",
|
||||
"gameGoalTaken": "But pris",
|
||||
"gamePlayersListName": "Joueurs",
|
||||
"ticTacToe" : "Morpion hors ligne",
|
||||
"ticTacToeTitle": "Morpion",
|
||||
"gamemodeChoice" : "Mode de jeu",
|
||||
"ruleTitle" : "Règles",
|
||||
"ruleBase" : "1. Vous devez gagner sur une des 9 grilles pour gagner la partie",
|
||||
"ruleMovement" : "2. Vous commencez sur le morpion central, et jouez sur le morpion correspondant a votre choix a votre prochain tour",
|
||||
"ruleDraw" : "3. Si votre choix rempli entièrement un morpion et provoque une égalité, vous perdez",
|
||||
"morpionWin" : "Le gagnant est : ",
|
||||
"matchmakingTitle": "Matchmaking",
|
||||
"matchmakingStartSearch": "Trouver une partie",
|
||||
"matchmakingStopSearch": "Arrêter le matchmaking",
|
||||
"matchmakingNbPlayers": "Nombre de joueurs",
|
||||
"TournamentCreateTitle": "Créer un tournoi",
|
||||
"TournamentCreateButton": "Créer le tournoi",
|
||||
"TournamentCreateTournamentName": "Nom du tournoi",
|
||||
"TournamentCreateNbPlayerByGame": "Nombre de joueurs en jeu",
|
||||
"TournamentCreateNbPlayer": "Nombre de joueurs dans le tournoi",
|
||||
|
||||
"settingsWindowTitle": "Paramètres",
|
||||
"settingsAvatarTitle": "Avatar",
|
||||
"settingsAvatarSave": "Enregister",
|
||||
"settingsAvatarDelete": "Supprimer",
|
||||
"settingsAvatarSaved": "Avatar enregistré.",
|
||||
"settingsAvatarDeleted": "Avatar supprimé.",
|
||||
"settingsAvatarDeleteError": "Une erreur s'est produite.",
|
||||
"settingsAvatarTooLarge": "L'image est trop lourde",
|
||||
"settingsTitle": "Paramètres de compte",
|
||||
"settingsUsername": "Nom d'utilisateur",
|
||||
"settingsUsernameSave": "Enregister",
|
||||
"settingsUsernameSaved": "Now d'utilisateur enregistré.",
|
||||
"settingsPasswordTitle": "Modifier le mot de passe",
|
||||
"settingsCurrentPassword": "Mot de passe actuel",
|
||||
"settingsNewPassword": "Nouveau mot de passe",
|
||||
"settingsRepeatNewPassword": "Répéter nouveau mot de passe",
|
||||
"settingsPasswordSave": "Enregister",
|
||||
"settingsPasswordSaved": "Nouveau mot de passe enregistré.",
|
||||
"settingsDeleteButton": "Supprimer le compte",
|
||||
"settingsDeleteTitle": "Êtes vous sûr ? Cette action est permanente !!",
|
||||
"settingsDeleteConfirm": "Mot de passe nécessaire pour confirmer la suppression:",
|
||||
"settingsDeleteInput": "Mot de passe",
|
||||
"settingsDeleteCancel": "Annuler",
|
||||
"settingsDeleteSuccess": "Compte supprimé avec succès."
|
||||
}
|
83
django/frontend/static/js/lang/tp.json
Normal file
83
django/frontend/static/js/lang/tp.json
Normal file
@ -0,0 +1,83 @@
|
||||
{
|
||||
"navbarSearch": "Lukin",
|
||||
"navbarHome": "Tomo",
|
||||
"navbarLogin": "Open",
|
||||
"navbarRegister": "Sitelen",
|
||||
"navbarProfile": "Sitelen mi",
|
||||
"navbarSettings": "Nasin",
|
||||
"navbarLogout": "Tawa ala",
|
||||
"homeWindowTitle": "Tomo",
|
||||
"homeTitle": "Tomo",
|
||||
"homeOnline": "Mute tawa",
|
||||
"homeOffline": "Mute lon",
|
||||
"homeSettings": "Nasin",
|
||||
"homeLogout": "Tawa ala",
|
||||
"loginWindowTitle": "Open",
|
||||
"loginFormTitle": "Open",
|
||||
"loginFormUsername": "nimi pi jan Open",
|
||||
"loginFormPassword": "nimi nasa",
|
||||
"loginFormButton": "Open",
|
||||
"loginNoAccount": "sina wile ala wile jo e nimi pi jan Open?",
|
||||
"loginRegister": "Sitelen",
|
||||
"errorEmptyField": "nimi ni li wile sitelen.",
|
||||
"logoutWindowTitle": "Tawa ala",
|
||||
"registerWindowTitle": "Sitelen",
|
||||
"registerFormTitle": "Sitelen",
|
||||
"registerFormUsername": "nimi pi jan sin",
|
||||
"registerFormPassword": "nimi nasa",
|
||||
"registerFormButton": "Sitelen",
|
||||
"registerAlreadyAccount": "sina jo ala jo e nimi pi jan sin?",
|
||||
"registerLogin": "Open",
|
||||
"404WindowTitle": "Ala o lukin e ni",
|
||||
"SearchWindowTitle": "Lukin",
|
||||
"profileAddFriend": "kama jo e jan",
|
||||
"profileRemoveFriend": "tawa ala e jan",
|
||||
"profileDenyRequest": "ante e ijo ni",
|
||||
"profileAcceptRequest": "kama jo e ijo ni",
|
||||
"profileUnblock": "Tawa ala e nimi pi jan ni",
|
||||
"profileBlock": "Tawa e nimi pi jan ni",
|
||||
"gameGoalTaken": "Wile pali",
|
||||
"gamePlayersListName": "Musi",
|
||||
"ticTacToe": "TicTacToe offline",
|
||||
"ticTacToeTitle": "TicTacToe",
|
||||
"gamemodeChoice" : "sitelen sitelen",
|
||||
"ruleTitle" : "Rules",
|
||||
"ruleBase" : "1. Win on wan pi the 9 tictactoe tawa win the game",
|
||||
"ruleMovement" : "2. Sina open on the central tictactoe, en play on the wan corresponding tawa your choice on the next turn",
|
||||
"ruleDraw" : "3. If your play cause a tictactoe tawa be full en a draw, sina lose the game",
|
||||
"morpionWin" : "jan li jo e poka sina : ",
|
||||
"matchmakingTitle": "Matchmaking",
|
||||
"matchmakingStartSearch": "lukin e ilo musi",
|
||||
"matchmakingStopSearch": "o pini e pana sona e jan pi pana sona e jan ante.",
|
||||
"matchmakingNbPlayers": "nanpa pi jan ante",
|
||||
"TournamentCreateTitle": "o pana e musi ante e musi",
|
||||
"TournamentCreateButton": "jo ala pona li jo e ijo li pali e ijo li pana e ijo li toki e ijo li kama jo e ijo li kama pali e ijo li kama pana e ijo li kama toki e ijo li kama jo e ijo li kama pali e ijo li kama pana e ijo",
|
||||
"TournamentCreateTournamentName": "ilo kipisi",
|
||||
"TournamentCreateNbPlayerByGame": "ilo jan lon poki pi lon anpa en sike pimeja li kama.",
|
||||
"TournamentCreateNbPlayer": "nanpa pi jan pona lon soweli musi",
|
||||
|
||||
"settingsWindowTitle": "Ilo open",
|
||||
"settingsAvatarTitle": "Jan ante",
|
||||
"settingsAvatarSave": "O lukin",
|
||||
"settingsAvatarDelete": "O pana e tawa ala",
|
||||
"settingsAvatarSaved": "Jan ante li kama.",
|
||||
"settingsAvatarDeleted": "Jan ante li pakala e ijo.",
|
||||
"settingsAvatarDeleteError": "Ijo li pakala.",
|
||||
"settingsAvatarTooLarge": "Ijo li suli mute.",
|
||||
"settingsTitle": "Ilo pi ijo sina",
|
||||
"settingsUsername": "Nimi jan",
|
||||
"settingsUsernameSave": "O lukin",
|
||||
"settingsUsernameSaved": "Nimi jan li kama.",
|
||||
"settingsPasswordTitle": "O ante e nimi pi toki anu nimi",
|
||||
"settingsCurrentPassword": "Nimi pi tenpo ni",
|
||||
"settingsNewPassword": "Nimi sin",
|
||||
"settingsRepeatNewPassword": "Nimi sin",
|
||||
"settingsPasswordSave": "O lukin",
|
||||
"settingsPasswordSaved": "Nimi sin li kama.",
|
||||
"settingsDeleteButton": "O pakala e ilo mi",
|
||||
"settingsDeleteTitle": "Sina wile ala wile ? Ni li ike mute !!",
|
||||
"settingsDeleteConfirm": "O toki e nimi sina lon insa.",
|
||||
"settingsDeleteInput": "Nimi",
|
||||
"settingsDeleteCancel": "O pana tawa ala",
|
||||
"settingsDeleteSuccess": "Ijo sina li pakala mute."
|
||||
}
|
BIN
django/frontend/static/js/sound/tictactoe/incorrectbuzzer.mp3
Normal file
BIN
django/frontend/static/js/sound/tictactoe/incorrectbuzzer.mp3
Normal file
Binary file not shown.
BIN
django/frontend/static/js/sound/tictactoe/play-move.mp3
Normal file
BIN
django/frontend/static/js/sound/tictactoe/play-move.mp3
Normal file
Binary file not shown.
20
django/frontend/static/js/utils/formUtils.js
Normal file
20
django/frontend/static/js/utils/formUtils.js
Normal file
@ -0,0 +1,20 @@
|
||||
export function clearIds(property_name, elements_id)
|
||||
{
|
||||
elements_id.forEach(element_id => {
|
||||
let element = document.getElementById(element_id);
|
||||
element[property_name] = "";
|
||||
});
|
||||
}
|
||||
|
||||
export function clearElements(prop, elements) {
|
||||
elements.forEach(element => element[prop] = '');
|
||||
}
|
||||
|
||||
export function fill_errors(errors, property_name)
|
||||
{
|
||||
Object.keys(errors).forEach(error_field =>
|
||||
{
|
||||
let element = document.getElementById(error_field);
|
||||
element[property_name] = errors[error_field];
|
||||
});
|
||||
}
|
61
django/frontend/static/js/utils/graph.js
Normal file
61
django/frontend/static/js/utils/graph.js
Normal file
@ -0,0 +1,61 @@
|
||||
|
||||
export function generateRandomColor()
|
||||
{
|
||||
return `#${Math.floor(Math.random()*16777215).toString(16)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {[Number]} data
|
||||
*/
|
||||
export function transformData(data)
|
||||
{
|
||||
let newData = [];
|
||||
|
||||
for (let index = 0; index < data.length; index++) {
|
||||
newData.push({x: Math.round(data[index] / 1000),
|
||||
y: index + 1});
|
||||
}
|
||||
|
||||
return newData;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export function range(start, stop, step = 1)
|
||||
{
|
||||
if (stop === undefined)
|
||||
{
|
||||
stop = start;
|
||||
start = 0;
|
||||
}
|
||||
let newArr = [];
|
||||
for (let i = start; i <= stop; i += step)
|
||||
newArr.push(i);
|
||||
|
||||
return newArr;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {[Object]} dataset
|
||||
*/
|
||||
export function get_labels(dataset)
|
||||
{
|
||||
let labelsSet = new Set();
|
||||
|
||||
dataset.forEach(player_data => {
|
||||
player_data.data.forEach(data => {
|
||||
labelsSet.add(data.x);
|
||||
});
|
||||
});
|
||||
|
||||
let labels = Array.from(labelsSet);
|
||||
|
||||
labels.sort(function(a, b){return b - a;});
|
||||
|
||||
labels.reverse();
|
||||
|
||||
return labels;
|
||||
}
|
18
django/frontend/static/js/utils/noticeUtils.js
Normal file
18
django/frontend/static/js/utils/noticeUtils.js
Normal file
@ -0,0 +1,18 @@
|
||||
export function createNotification(title = 'New notification', content, delay = 3000) {
|
||||
|
||||
const toastElement = document.createElement('div');
|
||||
toastElement.classList.add('toast');
|
||||
toastElement.role = 'alert';
|
||||
toastElement.setAttribute('data-bs-delay', delay);
|
||||
toastElement.innerHTML =
|
||||
`<div class='toast-header'>
|
||||
<strong class='me-auto'>${title}</strong>
|
||||
<button type='button' class='btn-close' data-bs-dismiss='toast'></button>
|
||||
</div>
|
||||
<div class='toast-body'>${content}</div>`
|
||||
toastElement.addEventListener('hidden.bs.toast', e => e.target.remove());
|
||||
new bootstrap.Toast(toastElement).show();
|
||||
|
||||
const toastContainer = document.getElementById('toastContainer');
|
||||
toastContainer.insertBefore(toastElement, toastContainer.firstChild);
|
||||
}
|
6
django/frontend/static/js/utils/sleep.js
Normal file
6
django/frontend/static/js/utils/sleep.js
Normal file
@ -0,0 +1,6 @@
|
||||
function sleep(ms)
|
||||
{
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
export {sleep}
|
20
django/frontend/static/js/views/HomeView.js
Normal file
20
django/frontend/static/js/views/HomeView.js
Normal file
@ -0,0 +1,20 @@
|
||||
import { lang } from "../index.js";
|
||||
import AbstractAuthenticatedView from "./abstracts/AbstractAuthenticatedView.js";
|
||||
|
||||
export default class extends AbstractAuthenticatedView {
|
||||
constructor(params) {
|
||||
super(params, 'homeWindowTitle');
|
||||
this.redirect_url = "/login";
|
||||
}
|
||||
|
||||
async getHtml() {
|
||||
return /* HTML */ `
|
||||
<h1>${lang.get('homeTitle', 'Home')}</h1>
|
||||
<a href="/matchmaking" data-link>${lang.get('homeOnline', 'Play online')}</a>
|
||||
<a href="/games/pong/offline" data-link>${lang.get('homeOffline', 'Play offline')}</a>
|
||||
<a href="/games/tictactoe/offline" data-link>${lang.get('ticTacToe')}</a>
|
||||
<a href="/settings" data-link>${lang.get('homeSettings', 'Settings')}</a>
|
||||
<a href="/logout" data-link>${lang.get('homeLogout', 'Logout')}</a>
|
||||
`;
|
||||
}
|
||||
}
|
143
django/frontend/static/js/views/MatchMakingView.js
Normal file
143
django/frontend/static/js/views/MatchMakingView.js
Normal file
@ -0,0 +1,143 @@
|
||||
import { client, lang, navigateTo } from "../index.js";
|
||||
import { clearIds, fill_errors } from "../utils/formUtils.js";
|
||||
import AbstractAuthenticatedView from "./abstracts/AbstractAuthenticatedView.js";
|
||||
|
||||
export default class extends AbstractAuthenticatedView {
|
||||
|
||||
constructor(params)
|
||||
{
|
||||
super(params, "Matchmaking");
|
||||
}
|
||||
|
||||
async toggle_search()
|
||||
{
|
||||
clearIds("innerText", ["detail"]);
|
||||
if (client.matchmaking.searching)
|
||||
{
|
||||
client.matchmaking.stop();
|
||||
this.button.innerHTML = lang.get("matchmakingStartSearch");
|
||||
}
|
||||
else
|
||||
{
|
||||
await client.matchmaking.start(this.onreceive.bind(this), this.ondisconnect.bind(this), this.game_type_input.value, this.nb_players_input.value);
|
||||
|
||||
this.button.innerHTML = lang.get("matchmakingStopSearch");
|
||||
}
|
||||
}
|
||||
|
||||
ondisconnect(event)
|
||||
{
|
||||
this.button.innerHTML = lang.get("matchmakingStartSearch");
|
||||
}
|
||||
|
||||
onreceive(data)
|
||||
{
|
||||
if (data.detail === "game_found")
|
||||
{
|
||||
navigateTo(`/games/${data.game_type}/${data.game_id}`);
|
||||
return;
|
||||
}
|
||||
this.display_data(data);
|
||||
}
|
||||
|
||||
display_data(data)
|
||||
{
|
||||
clearIds("innerText", ["detail"]);
|
||||
fill_errors(data, "innerText");
|
||||
}
|
||||
|
||||
addEnterEvent()
|
||||
{
|
||||
[this.nb_players_input, this.game_type_input].forEach((input) => {
|
||||
|
||||
input.addEventListener('keydown', async ev => {
|
||||
|
||||
if (ev.key !== 'Enter')
|
||||
return;
|
||||
|
||||
await this.toggle_search.bind(this);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
addChangeNbPlayersEvent()
|
||||
{
|
||||
let update = () => {
|
||||
this.button.disabled = (this.nb_players_input.value < 2 || this.nb_players_input.value > 4);
|
||||
};
|
||||
|
||||
["change", "oninput"].forEach((event_name) => {
|
||||
this.nb_players_input.addEventListener(event_name, update);
|
||||
});
|
||||
}
|
||||
|
||||
addChangegame_typeEvent()
|
||||
{
|
||||
let nb_players_div = document.getElementById("nb-players-div");
|
||||
|
||||
this.game_type_input.addEventListener("change", () => {
|
||||
|
||||
if (this.game_type_input.value === "tictactoe")
|
||||
{
|
||||
nb_players_div.style.display = 'none';
|
||||
this.nb_players_input.value = 2;
|
||||
}
|
||||
else
|
||||
nb_players_div.style.display = 'block';
|
||||
|
||||
client.matchmaking.stop();
|
||||
clearIds("innerText", ["detail"]);
|
||||
});
|
||||
}
|
||||
|
||||
addEvents()
|
||||
{
|
||||
this.addEnterEvent();
|
||||
this.addChangegame_typeEvent();
|
||||
this.addChangeNbPlayersEvent();
|
||||
}
|
||||
|
||||
async postInit()
|
||||
{
|
||||
this.button = document.getElementById("toggle-search");
|
||||
this.nb_players_input = document.getElementById("nb-players-input");
|
||||
this.game_type_input = document.getElementById("game-type-input");
|
||||
|
||||
this.button.onclick = this.toggle_search.bind(this);
|
||||
|
||||
this.addEvents()
|
||||
}
|
||||
|
||||
async getHtml() {
|
||||
return /* HTML */ `
|
||||
<div class='container-fluid'>
|
||||
<div class='border border-2 rounded bg-light-subtle mx-auto p-2 col-md-7 col-lg-4'>
|
||||
<h4 class='text-center fw-semibold mb-4' id="title">${lang.get("matchmakingTitle")}</h4>
|
||||
<div>
|
||||
<div class='form-floating mb-2' id='game_type-div'>
|
||||
<select class='form-control' id='game-type-input'>
|
||||
<option value='pong'>Pong</option>
|
||||
<option value='tictactoe'>ticTacToe</option>
|
||||
</select>
|
||||
<label for='game-type-input'>${lang.get("gamemodeChoice")}</label>
|
||||
</div>
|
||||
<div class='form-floating mb-2' id='nb-players-div'>
|
||||
<input type='number' min='2' value='2' max='4' class='form-control' id='nb-players-input' placeholder='${lang.get("matchmakingNbPlayers")}'>
|
||||
<label for='nb-players-input' id='username-label'>${lang.get("matchmakingNbPlayers")}</label>
|
||||
<span class='text-danger' id='username'></span>
|
||||
</div>
|
||||
<div class='d-flex'>
|
||||
<button type='button' class='btn btn-primary mt-3 mb-2 mx-2' id='toggle-search'>${lang.get("matchmakingStartSearch")}</button>
|
||||
<span class='text-danger my-auto mx-2' id='detail'></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
async leavePage()
|
||||
{
|
||||
await client.matchmaking.stop();
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user