This commit is contained in:
AdrienLSH
2024-05-14 08:50:37 +02:00
parent 95f0097ce5
commit e308e8f012
231 changed files with 70 additions and 22 deletions

View File

3
django/frontend/admin.py Normal file
View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

6
django/frontend/apps.py Normal file
View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class FrontendConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'frontend'

View File

@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

View 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;
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View 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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View 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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,8 @@
#canva {
background-color: white;
border: 1px;
display: block;
margin-left: auto;
margin-right: auto;
}

View 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%;
}

View 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));
}

View 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;
}

View 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;
}

View 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;
}

View File

@ -0,0 +1,14 @@
#canva {
width: 510px;
height:510px;
margin: 0px auto;
}
#Morpion {
margin: 0px auto;
}
#rule {
text-align: center;
margin: 0px auto;
}

View 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 };

View 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 };

File diff suppressed because one or more lines are too long

View 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;

View 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;
}
}
}
}

View 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 };

View 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};

View 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;
}
}

View 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};

View 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};

View 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();
}
}
}

View 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])
}
}

View 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};

View 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() {
}
}

View 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};

View 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);
}
}

View 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};

View 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);
});
}
*/
}

View 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"]])
}
}

View 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 };

View 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);
}
}

View 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;
}
}

View 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);
}
}

View 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;
}
}

View 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"])
}
}

View 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"]);
}
}

View 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 }

View 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 };

View 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');
}
}
}

View 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 };

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View 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 };

View 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."
}

View 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."
}

View 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."
}

View 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."
}

View 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];
});
}

View 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;
}

View 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);
}

View File

@ -0,0 +1,6 @@
function sleep(ms)
{
return new Promise(resolve => setTimeout(resolve, ms));
}
export {sleep}

View 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>
`;
}
}

View 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