configure eslint

pull/116/head
maxime 1 year ago committed by maxime.batista
parent 7a614325f0
commit 48558e66de

@ -1,18 +1,18 @@
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parser: '@typescript-eslint/parser',
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
root: true,
env: { browser: true, es2023: true },
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react-hooks/recommended",
],
},
ignorePatterns: ["dist", ".eslintrc.cjs"],
parser: "@typescript-eslint/parser",
plugins: ["react-refresh"],
rules: {
"react-refresh/only-export-components": [
"warn",
{ allowConstantExport: true },
],
},
}

@ -1,23 +1,22 @@
module.exports = {
root: true,
parser: '@typescript-eslint/parser',
plugins: [
'@typescript-eslint',
'react',
'react-hooks'
],
parser: "@typescript-eslint/parser",
plugins: ["@typescript-eslint", "react", "react-hooks"],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react/recommended',
'plugin:react/jsx-runtime',
'plugin:react-hooks/recommended'
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react/recommended",
"plugin:react/jsx-runtime",
"plugin:react-hooks/recommended",
],
rules: {
"prefer-const": ["error", {
"destructuring": "all"
}]
},
settings: {
react: {
version: 'detect'
}
}
};
version: "detect",
},
},
}

@ -1,16 +0,0 @@
<?php
$finder = (new PhpCsFixer\Finder())->in(__DIR__);
return (new PhpCsFixer\Config())
->setRules([
'@PER-CS' => true,
'@PHP74Migration' => true,
'array_syntax' => ['syntax' => 'short'],
'braces_position' => [
'classes_opening_brace' => 'same_line',
'functions_opening_brace' => 'same_line'
]
])
->setIndent(" ")
->setFinder($finder);

@ -1,7 +1,7 @@
{
"bracketSameLine": true,
"trailingComma": "all",
"printWidth": 80,
"tabWidth": 4,
"semi": false
}
"bracketSameLine": true,
"trailingComma": "all",
"printWidth": 80,
"tabWidth": 4,
"semi": false
}

@ -4,25 +4,27 @@
Notre projet est divisé en plusieurs parties:
- `src/API`, qui définit les classes qui implémentent les actions de lapi
- `src/App`, qui définit les contrôleurs et les vues de lapplication web
- `src/Core`, définit les modèles, les classes métiers, les librairies internes (validation, http), les gateways, en somme, les élements logiques de lapplication et les actions que lont peut faire avec.
- `sql`, définit la base de donnée utilisée, et éxécute les fichiers sql lorsque la base de donnée nest pas initialisée.
- `profiles`, définit les profiles dexecution, voir `Documentation/how-to-dev.md` pour plus dinfo
- `front` contient le code front-end react/typescript
- `ci` contient les scripts de déploiement et la définition du workflow dintégration continue et de déploiement constant vers notre staging server ([maxou.dev/<branch>/public/](https://maxou.dev/IQBall/master/public)).
- `public` point dentrée, avec :
- `public/index.php` point dentrée pour la webapp
- `public/api/index.php` point dentrée pour lapi.
- `src/API`, qui définit les classes qui implémentent les actions de lapi
- `src/App`, qui définit les contrôleurs et les vues de lapplication web
- `src/Core`, définit les modèles, les classes métiers, les librairies internes (validation, http), les gateways, en somme, les élements logiques de lapplication et les actions que lont peut faire avec.
- `sql`, définit la base de donnée utilisée, et éxécute les fichiers sql lorsque la base de donnée nest pas initialisée.
- `profiles`, définit les profiles dexecution, voir `Documentation/how-to-dev.md` pour plus dinfo
- `front` contient le code front-end react/typescript
- `ci` contient les scripts de déploiement et la définition du workflow dintégration continue et de déploiement constant vers notre staging server ([maxou.dev/<branch>/public/](https://maxou.dev/IQBall/master/public)).
- `public` point dentrée, avec :
- `public/index.php` point dentrée pour la webapp
- `public/api/index.php` point dentrée pour lapi.
## Backend
### Validation et résilience des erreurs
#### Motivation
Un controlleur a pour but de valider les données d'une requête avant de les manipuler.
Un controlleur a pour but de valider les données d'une requête avant de les manipuler.
Nous nous sommes rendu compte que la vérification des données d'une requête était redondante, même en factorisant les différents
types de validation, nous devions quand même explicitement vérifier la présence des champs utilisés dans la requête.
types de validation, nous devions quand même explicitement vérifier la présence des champs utilisés dans la requête.
```php
public function doPostAction(array $form) {
@ -39,11 +41,11 @@ public function doPostAction(array $form) {
if (Validation::isLenBetween($email, 6, 64))) {
$failures[] = "L'adresse email doit être d'une longueur comprise entre 6 et 64 charactères.";
}
if (!empty($failures)) {
return ViewHttpResponse::twig('error.html.twig', ['failures' => $failures]);
}
// traitement ...
}
```
@ -56,9 +58,10 @@ Bien souvent, lorsque le prédicat échoue, un message est ajouté à la liste d
de la redondance sur les messages d'erreurs choisis lorsqu'une validation échoue.
#### Schéma
Toutes ces lignes de code pour définir que notre requête doit contenir un champ nommé `email`, dont la valeur est une adresse mail, d'une longueur comprise entre 6 et 64.
Nous avons donc trouvé l'idée de définir un système nous permettant de déclarer le schéma d'une requête,
et de reporter le plus d'erreurs possibles lorsqu'une requête ne valide pas le schéma :
et de reporter le plus d'erreurs possibles lorsqu'une requête ne valide pas le schéma :
```php
public function doPostAction(array $form): HttpResponse {
@ -66,17 +69,18 @@ public function doPostAction(array $form): HttpResponse {
$req = HttpRequest::from($form, $failures, [
'email' => [DefaultValidators::email(), DefaultValidators::isLenBetween(6, 64)]
]);
if (!empty($failures)) { //ou $req == null
return ViewHttpResponse::twig('error.html.twig', ['failures' => $failures])
}
// traitement ...
}
```
Ce système nous permet de faire de la programmation _déclarative_, nous disons à _quoi_ ressemble la forme de la requête que l'on souhaite,
plustot que de définir _comment_ réagir face à notre requête.
Ce système permet d'être beaucoup plus clair sur la forme attendue d'une requête, et de garder une lisibilité satisfaisante peu importe le nombre de
Ce système permet d'être beaucoup plus clair sur la forme attendue d'une requête, et de garder une lisibilité satisfaisante peu importe le nombre de
champs que celle-ci contient.
Nous pouvons ensuite emballer les erreurs de validation dans des `ValidationFail` et `FieldValidationFail`, ce qui permet ensuite d'obtenir
@ -84,40 +88,41 @@ plus de précision sur une erreur, comme le nom du champ qui est invalidé, et q
les erreurs et facilement entourer les champs invalides en rouge, ainsi que d'afficher toutes les erreurs que l'utilisateur a fait, d'un coup.
### HttpRequest, HttpResponse
Nous avons choisi d'encapsuler les requêtes et les réponses afin de faciliter leur manipulation.
Cela permet de mieux repérer les endroits du code qui manipulent une requête d'un simple tableau,
et de garder à un seul endroit la responsabilitée d'écrire le contenu de la requête vers client.
et de garder à un seul endroit la responsabilitée d'écrire le contenu de la requête vers client.
`src/App` définit une `ViewHttpResponse`, qui permet aux controlleurs de retourner la vue qu'ils ont choisit.
C'est ensuite à la classe `src/App/App` d'afficher la réponse.
### index.php
Il y a deux points d'entrés, celui de la WebApp (`public/index.php`), et celui de l'API (`public/api/index.php`).
Ces fichiers ont pour but de définir ce qui va être utilisé dans l'application, comme les routes, la base de donnée, les classes métiers utilisés,
comment gérer l'authentification dans le cadre de l'API, et une session dans le cadre de la web app.
comment gérer l'authentification dans le cadre de l'API, et une session dans le cadre de la web app.
L'index définit aussi quoi faire lorsque l'application retourne une réponse. Dans les implémentations actuelles, elle délègue simplement
l'affichage dans une classe (`IQBall\App\App` et `IQBall\API\API`).
### API
Nous avons définit une petite API (`src/Api`) pour nous permettre de faire des actions en arrière plan depuis le front end.
Par exemple, l'API permet de changer le nom d'une tactique, ou de sauvegarder son contenu.
C'est plus pratique de faire des requêtes en arrière-plan plustot que faire une requête directement à la partie WebApp, ce qui
aurait eu pour conséquences de recharger la page
## Frontend
### Utilisation de React
Notre application est une application de création et de visualisation de stratégies pour des match de basket.
Léditeur devra comporter un terrain sur lequel il est possible de placer et bouger des pions, représentant les joueurs.
Une stratégie est un arbre composé de plusieurs étapes, une étape étant constituée dun ensemble de joueurs et dadversaires sur le terrain,
Une stratégie est un arbre composé de plusieurs étapes, une étape étant constituée dun ensemble de joueurs et dadversaires sur le terrain,
aillant leur position de départ, et leur position cible, avec des flèches représentant le type de mouvement (dribble, écran, etc) effectué.
les enfants dune étape, sont dautres étapes en fonction des cas de figures (si tel joueur fait tel mouvement, ou si tel joueur fait telle passe etc).
Pour rendre le tout agréable à utiliser, il faut que linterface soit réactive : si lon bouge un joueur,
Pour rendre le tout agréable à utiliser, il faut que linterface soit réactive : si lon bouge un joueur,
il faut que les flèches qui y sont liés bougent aussi, il faut que les joueurs puissent bouger le long des flèches en mode visualiseur etc…
Le front-end de léditeur et du visualiseur étant assez ambitieux, et occupant une place importante du projet, nous avons décidés de leffectuer en utilisant
le framework React qui rend simple le développement dinterfaces dynamiques, et dutiliser typescript parce quici on code bien et quon impose une type safety a notre code.

@ -1,6 +1,7 @@
# Welcome on the documentation's description
## Let's get started with the architecture diagram.
![architecture diagram](./assets/architecture.svg)
As you can see our entire application is build around three main package.
@ -13,7 +14,7 @@ Allowing to operate on it.
The App now is more about the web application itself.
Having all the controllers of the MVC architecture the use the model, the validation system and the http system in the core.
It also calls the twig's views inside of App. Finally, it uses the package Session. This one replace the $_SESSION we all know in PHP.
It also calls the twig's views inside of App. Finally, it uses the package Session. This one replace the $\_SESSION we all know in PHP.
Thanks to this we have a way cleaner use of all session's data.
Nevertheless, all the controllers call not only twig views but also react ones.
Those are present in the package "front", dispatched in several other packages.
@ -22,10 +23,11 @@ Such as assets having all the image and stuff, model containing all the data's s
Finally, we have the package "Api" that allows to share code and bind all the different third-hand application such as the web admin one.
## Main data class diagram.
![Class diagram](./assets/models.svg)
You can see how our data is structured contained in the package "data" as explained right above.
There is two clear part.
There is two clear part.
First of all, the Tactic one.
We got a nice class named TacticInfo representing as it says the information about a tactic, nothing to discuss more about.
It associates an attribute of type "CourtType". This last is just an "evoluated" type of enum with some more features.
@ -42,6 +44,7 @@ The last class we have is the Account. It could directly be incorporated in User
Then, Account only has a user and a token which is an identifier.
## Validation's class diagram
![validation's class diagram](./assets/validation.svg)
We implemented our own validation system, here it is!
@ -50,32 +53,38 @@ In general, we use the implementation "SimpleFunctionValidator".
We reconize the strategy pattern. Indeed, we need a family of algorithms because we have many classes that only differ by the way they validate.
Futhermore, you may have notices the ComposedValidator that allows to chain several Validator.
We can see that this system uses the composite pattern
The other part of the diagram is about the failure a specific field's validation.
The other part of the diagram is about the failure a specific field's validation.
We have a concrete class to return a something more general. All the successors are just more precise about the failure.
## Http's class diagram
![Http's class diagram](./assets/http.svg)
It were we centralize what the app can render, and what the api can receive.
Then, we got the "basic" response (HttpResponse) that just render a HttpCodes.
Then, we got the "basic" response (HttpResponse) that just render a HttpCodes.
We have two successors for now. ViewHttpResponse render not only a code but also a view, either react or twig ones.
Finally, we have the JsonHttpResponse that renders, as it's name says, some Json.
## Session's class diagram
![Session's class diagram](./assets/session.svg)
It encapsulates the PHP's array "$_SESSION". With two interfaces that dictate how a session should be handled, and same for a mutable one.
It encapsulates the PHP's array "$\_SESSION". With two interfaces that dictate how a session should be handled, and same for a mutable one.
## Model View Controller
All class diagram, separated by their range of action, of the imposed MVC architecture.
All of them have a controller that validates entries with the validation system and check the permission the user has,and whether or not actually do the action.
These controllers are composed by a Model that handle the pure data and is the point of contact between these and the gateways.
Speaking of which, Gateways are composing Models. They use the connection class to access the database and send their query.
### Team
![team's mvc](./assets/team.svg)
### Editor
![editor's mvc](./assets/editor.svg)
### Authentification
![auth's mvc](./assets/auth.svg)
![auth's mvc](./assets/auth.svg)

@ -1,3 +1,3 @@
* [Description.md](Description.md)
* [Conception.md](Conception.md)
* [how-to-dev.md](how-to-dev.md)
- [Description.md](Description.md)
- [Conception.md](Conception.md)
- [how-to-dev.md](how-to-dev.md)

@ -3,17 +3,13 @@ machine, and how it works under the hood.
# How to run the project on my local computer
1. Use phpstorm to run a local php server:
1) Use phpstorm to run a local php server:
* Go to configuration > add new configuration
* Select "PHP Built-in Web Server", then enter options as follow:
![](assets/php-server-config.png)
- port 8080
- name the configuration "RunServer" to be more explicit
- place the "Document Root" in `/public`
- host is localhost
* Click apply, OK
* Now run it.
- Go to configuration > add new configuration
- Select "PHP Built-in Web Server", then enter options as follow:
![](assets/php-server-config.png) - port 8080 - name the configuration "RunServer" to be more explicit - place the "Document Root" in `/public` - host is localhost
- Click apply, OK
- Now run it.
If you go to `http://localhost:8080` you'll see a blank page.
This is expected ! On your browser, open inspection tab (ctrl+shift+i) go to network and refresh the page.
@ -39,12 +35,14 @@ Now refresh your page, you should now see all request being fulfilled and a form
Caution: **NEVER** directly connect on the `localhost:5173` node development server, always pass through the php (`localhost:8080`) server.
# How it works
I'm glad you are interested in how that stuff works, it's a bit tricky, lets go.
I'm glad you are interested in how that stuff works, it's a bit tricky, lets go.
If you look at our `index.php` (located in `/public` folder), you'll see that it define our routes, it uses an `AltoRouter` then delegates the request's action processing to a controller.
We can see that there are two registered routes, the `GET:/` (that then calls `SampleFormController#displayForm()`) and `POST:/result` (that calls `SampleFormController#displayResults()`).
Implementation of those two methods are very simple: there is no verification to make nor model to control, thus they directly sends the view back to the client.
Implementation of those two methods are very simple: there is no verification to make nor model to control, thus they directly sends the view back to the client.
here's the implementation of the `SampleFormController`
```php
require_once "react-display.php";
class SampleFormController {
@ -63,7 +61,7 @@ As our views are now done using react (and defined under the `front/views` folde
If you look at the `send_react_front($viewURI, $viewArguments)` function, you'll see that is simply loads the file `src/react-display-file.php` with given arguments.
The file is a simple html5 template with a `<script>` block in the `<body>` section.
The script block imports the requested view and will render it.
The view entry is a function, named in PascalCase, which __must__ be be exported by default (`export default function MyReactView(args: {..})`).
The view entry is a function, named in PascalCase, which **must** be be exported by default (`export default function MyReactView(args: {..})`).
```html
<!--
@ -72,35 +70,37 @@ imports the given view URL, and assume that the view exports a function named `C
see ViewRenderer.tsx::renderView for more info
-->
<script type="module">
import {renderView} from "<?= asset("ViewRenderer.tsx") ?>"
import Component from "<?= asset($url) ?>"
renderView(Component, <?= json_encode($arguments) ?>)
import {renderView} from "<?= asset("ViewRenderer.tsx") ?>"
import Component from "<?= asset($url) ?>"
renderView(Component, <?= json_encode($arguments) ?>)
</script>
```
here's how it renders if you do a request to `http://localhost:8080/`.
here's how it renders if you do a request to `http://localhost:8080/`.
![](assets/render-react-php-file-processed.png)
The index.php's router says that for a `GET` on the `/` url, we call the `SampleFormController#displayForm` method.
This method then uses the `send_react_front`, to render the `views/SampleForm.tsx` react element, with no arguments (an empty array).
The view file **must export by default its react function component**.
The view file **must export by default its react function component**.
## Server Profiles
If you go on the staging server, you'll see that, for the exact same request equivalent, the generated `src/render-display-file` file changes :
If you go on the staging server, you'll see that, for the exact same request equivalent, the generated `src/render-display-file` file changes :
![](assets/staging-server-render-react-php-file-processed.png)
(we can also see that much less files are downloaded than with our localhost aka development server).
Remember that react components and typescript files needs to be transpiled to javascript before being executable by a browser.
The generated file no longer requests the view to a `localhost:5173` or a `maxou.dev:5173` server,
now our react components are directly served by the same server, as they have been pre-compiled by our CI (see `/ci/.drone.yml` and `/ci/build_react.msh`) into valid js files that can directly be send to the browser.
now our react components are directly served by the same server, as they have been pre-compiled by our CI (see `/ci/.drone.yml` and `/ci/build_react.msh`) into valid js files that can directly be send to the browser.
If you go back to our `index.php` file, you'll see that it requires a `../config.php` file, if you open it,
you'll see that it defines the `asset(string $uri)` function that is used by the `src/react-display-file.php`,
in the `<script>` block we talked earlier.
you'll see that it defines the `asset(string $uri)` function that is used by the `src/react-display-file.php`,
in the `<script>` block we talked earlier.
By default, the `/config.php` file uses the `dev-config-profile.php` profile,
the file is replaced with `prod-config-file.php` by the CI when deploying to the staging server (see the pipeline "prepare php" step in `/ci/.drone.yml`)
The two profiles declares an `_asset(string $uri)` function, used by the `/config.php::asset` method, but with different implementations :
The two profiles declares an `_asset(string $uri)` function, used by the `/config.php::asset` method, but with different implementations :
### Development profile
```php
@ -112,11 +112,13 @@ function _asset(string $assetURI): string {
return $front_url . "/" . $assetURI;
}
```
The simplest profile, simply redirect all assets to the development server
### Production profile
Before the CD workflow step deploys the generated files to the server,
it generates a `/views-mappings.php` file that will map the react views file names to their compiled javascript files :
Before the CD workflow step deploys the generated files to the server,
it generates a `/views-mappings.php` file that will map the react views file names to their compiled javascript files :
```php
const ASSETS = [
@ -126,7 +128,9 @@ const ASSETS = [
... // other files that does not have to be directly used by the `send_react_front()` function
];
```
The `_asset` function will then get the right javascript for the given typescript file.
```php
require "../views-mappings.php";
@ -137,4 +141,4 @@ function _asset(string $assetURI): string {
// fallback to the uri itself.
return $basePath . "/" . (ASSETS[$assetURI] ?? $assetURI);
}
```
```

@ -1,6 +1,8 @@
# IQBall - Web Application
This repository hosts the IQBall application for web
## Read the docs !
You can find some additional documentation in the [Documentation](Documentation) folder,
and in the [wiki](https://codefirst.iut.uca.fr/git/IQBall/Application-Web/wiki).

@ -3,66 +3,45 @@ type: docker
name: "CI and Deploy on maxou.dev"
volumes:
- name: server
temp: {}
- name: server
temp: {}
trigger:
event:
- push
event:
- push
steps:
- image: node:latest
name: "front CI"
commands:
- npm install
- npm run tsc
- image: composer:latest
name: "php CI"
commands:
- composer install
- vendor/bin/phpstan analyze
- image: node:latest
name: "build node"
volumes: &outputs
- name: server
path: /outputs
depends_on:
- "front CI"
commands:
- curl -L moshell.dev/setup.sh > /tmp/moshell_setup.sh
- chmod +x /tmp/moshell_setup.sh
- echo n | /tmp/moshell_setup.sh
- echo "VITE_API_ENDPOINT=/IQBall/$DRONE_BRANCH/public/api" >> .env.PROD
- echo "VITE_BASE=/IQBall/$DRONE_BRANCH/public" >> .env.PROD
- apt update && apt install jq -y
-
- BASE="/IQBall/$DRONE_BRANCH/public" OUTPUT=/outputs /root/.local/bin/moshell ci/build_react.msh
- image: ubuntu:latest
name: "prepare php"
volumes: *outputs
depends_on:
- "php CI"
commands:
- mkdir -p /outputs/public
# this sed command will replace the included `profile/dev-config-profile.php` to `profile/prod-config-file.php` in the config.php file.
- sed -E -i 's/\\/\\*PROFILE_FILE\\*\\/\\s*".*"/"profiles\\/prod-config-profile.php"/' config.php
- sed -E -i "s/const BASE_PATH = .*;/const BASE_PATH = \"\\/IQBall\\/$DRONE_BRANCH\\/public\";/" profiles/prod-config-profile.php
- rm profiles/dev-config-profile.php
- mv src config.php sql profiles vendor /outputs/
- image: eeacms/rsync:latest
name: Deliver on staging server branch
depends_on:
- "prepare php"
- "build node"
volumes: *outputs
environment:
SERVER_PRIVATE_KEY:
from_secret: SERVER_PRIVATE_KEY
commands:
- chmod +x ci/deploy.sh
- ci/deploy.sh
- image: node:latest
name: "front CI"
commands:
- npm install
- npm run tsc
# - image: node:latest
# name: "build node"
# volumes: &outputs
# - name: server
# path: /outputs
# depends_on:
# - "front CI"
# commands:
# - curl -L moshell.dev/setup.sh > /tmp/moshell_setup.sh
# - chmod +x /tmp/moshell_setup.sh
# - echo n | /tmp/moshell_setup.sh
# - echo "VITE_API_ENDPOINT=/IQBall/$DRONE_BRANCH/public/api" >> .env.PROD
# - echo "VITE_BASE=/IQBall/$DRONE_BRANCH/public" >> .env.PROD
# - apt update && apt install jq -y
# -
# - BASE="/IQBall/$DRONE_BRANCH/public" OUTPUT=/outputs /root/.local/bin/moshell ci/build_react.msh
##
# - image: eeacms/rsync:latest
# name: Deliver on staging server branch
# depends_on:
# - "prepare php"
# - "build node"
# volumes: *outputs
# environment:
# SERVER_PRIVATE_KEY:
# from_secret: SERVER_PRIVATE_KEY
# commands:
# - chmod +x ci/deploy.sh
# - ci/deploy.sh

@ -1,13 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/src/assets/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/src/assets/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

@ -1,41 +1,42 @@
{
"name": "iqball_web",
"version": "0.1.0",
"private": true,
"dependencies": {
"@loadable/component": "^5.16.3",
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"@types/jest": "^27.5.2",
"@types/node": "^16.18.59",
"@types/react": "^18.2.31",
"@types/react-dom": "^18.2.14",
"eslint-plugin-react-refresh": "^0.4.5",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-draggable": "^4.4.6",
"react-router-dom": "^6.22.0",
"typescript": "^5.2.2",
"vite": "^4.5.0",
"vite-plugin-css-injected-by-js": "^3.3.0"
},
"scripts": {
"start": "vite --host",
"build": "vite build",
"test": "vite test",
"format": "prettier --config .prettierrc 'front' --write",
"tsc": "tsc"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^6.11.0",
"@typescript-eslint/parser": "^6.11.0",
"@vitejs/plugin-react": "^4.1.0",
"eslint": "^8.53.0",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0",
"prettier": "^3.1.0",
"typescript": "^5.2.2",
"vite-plugin-svgr": "^4.1.0"
}
"name": "iqball_web",
"version": "0.1.0",
"private": true,
"dependencies": {
"@loadable/component": "^5.16.3",
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"@types/jest": "^27.5.2",
"@types/node": "^16.18.59",
"@types/react": "^18.2.31",
"@types/react-dom": "^18.2.14",
"eslint-plugin-react-refresh": "^0.4.5",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-draggable": "^4.4.6",
"react-router-dom": "^6.22.0",
"typescript": "^5.2.2",
"vite": "^4.5.0",
"vite-plugin-css-injected-by-js": "^3.3.0"
},
"scripts": {
"start": "vite --host",
"build": "vite build",
"test": "vite test",
"format": "prettier --config .prettierrc '.' --write",
"tsc": "tsc"
},
"devDependencies": {
"@types/loadable__component": "^5.13.8",
"@typescript-eslint/eslint-plugin": "^6.11.0",
"@typescript-eslint/parser": "^6.11.0",
"@vitejs/plugin-react": "^4.1.0",
"eslint": "^8.53.0",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0",
"prettier": "^3.1.0",
"typescript": "^5.2.2",
"vite-plugin-svgr": "^4.1.0"
}
}

@ -4,7 +4,6 @@ import loadable from "@loadable/component"
import { Header } from "./pages/template/Header.tsx"
import "./style/app.css"
const HomePage = loadable(() => import("./pages/HomePage.tsx"))
const LoginPage = loadable(() => import("./pages/LoginPage.tsx"))
const RegisterPage = loadable(() => import("./pages/RegisterPage.tsx"))
@ -21,24 +20,36 @@ export default function App() {
<Outlet />
<Routes>
<Route path={"/login"} element={<LoginPage />} />
<Route path={"/register"} element={<RegisterPage />} />
<Route path={"/"} element={<AppLayout />}>
<Route path={"/"} element={<HomePage />} />
<Route path={"/home"} element={<HomePage />} />
<Route path={"/team/new"} element={<CreateTeamPage />} />
<Route path={"/team/:teamId"} element={<TeamPanelPage />} />
<Route path={"/tactic/new"} element={<NewTacticPage />} />
<Route path={"/tactic/:tacticId/edit"} element={<Editor guestMode={false} />} />
<Route path={"/tactic/edit-guest"} element={<Editor guestMode={true} />} />
<Route
path={"/team/new"}
element={<CreateTeamPage />}
/>
<Route
path={"/team/:teamId"}
element={<TeamPanelPage />}
/>
<Route
path={"/tactic/new"}
element={<NewTacticPage />}
/>
<Route
path={"/tactic/:tacticId/edit"}
element={<Editor guestMode={false} />}
/>
<Route
path={"/tactic/edit-guest"}
element={<Editor guestMode={true} />}
/>
<Route path={"*"} element={<NotFoundPage />} />
</Route>
</Routes>
</BrowserRouter>
</div>
@ -46,8 +57,10 @@ export default function App() {
}
function AppLayout() {
return <>
<Header />
<Outlet />
</>
}
return (
<>
<Header />
<Outlet />
</>
)
}

@ -1,7 +1,6 @@
import { API, BASE } from "./Constants"
import { getSession, saveSession, Session } from "./api/session.ts"
export function redirect(url: string) {
location.pathname = BASE + url
}
@ -12,12 +11,11 @@ export async function fetchAPI(
method = "POST",
redirectIfNotAuth: boolean = true,
): Promise<Response> {
const session = getSession()
const token = session?.auth?.token
const headers = {
"Accept": "application/json",
const headers: HeadersInit = {
Accept: "application/json",
"Content-Type": "application/json",
}
@ -34,17 +32,15 @@ export async function fetchAPI(
return await handleResponse(session, response, redirectIfNotAuth)
}
export async function fetchAPIGet(
url: string,
redirectIfNotAuth: boolean = true,
): Promise<Response> {
const session = getSession()
const token = session?.auth?.token
const headers = {
"Accept": "application/json",
const headers: HeadersInit = {
Accept: "application/json",
"Content-Type": "application/json",
}
@ -60,18 +56,23 @@ export async function fetchAPIGet(
return await handleResponse(session, response, redirectIfNotAuth)
}
async function handleResponse(session: Session, response: Response, redirectIfNotAuth: boolean): Promise<Response> {
async function handleResponse(
session: Session,
response: Response,
redirectIfNotAuth: boolean,
): Promise<Response> {
// if we provided a token but still unauthorized, the token has expired
if (response.status == 401) {
if (!redirectIfNotAuth)
return response
if (!redirectIfNotAuth) return response
redirect("/login")
saveSession({ ...session, urlTarget: location.pathname })
return response
}
const nextToken = response.headers.get("Next-Authorization")!
const expirationDate = Date.parse(response.headers.get("Next-Authorization-Expiration-Date")!)
const expirationDate = Date.parse(
response.headers.get("Next-Authorization-Expiration-Date")!,
)
saveSession({ ...session, auth: { token: nextToken, expirationDate } })
return response

@ -1,6 +1,4 @@
export interface Failure {
type: string
messages: string[]
}
}

@ -20,4 +20,4 @@ export function saveSession(session: Session) {
export function getSession(): Session {
const json = localStorage.getItem(SESSION_KEY)
return json ? JSON.parse(json) : {}
}
}

@ -1,4 +1,4 @@
import React, { CSSProperties, useRef, useState } from "react"
import { CSSProperties, useRef, useState } from "react"
import "../style/title_input.css"
export interface TitleInputOptions {
@ -23,7 +23,7 @@ export default function TitleInput({
type="text"
value={value}
onChange={(event) => setValue(event.target.value)}
onBlur={(_) => onValidated(value)}
onBlur={() => onValidated(value)}
onKeyUp={(event) => {
if (event.key == "Enter") ref.current?.blur()
}}

@ -1,4 +1,4 @@
import React, { useRef } from "react"
import { useRef } from "react"
import Draggable from "react-draggable"
import { BallPiece } from "./BallPiece"
import { NULL_POS } from "../../geo/Pos"

@ -9,7 +9,6 @@
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
@ -23,4 +22,3 @@ h1 {
font-size: 3.2em;
line-height: 1.1;
}

@ -1,11 +1,10 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import './index.css'
import App from "./App.tsx";
import React from "react"
import ReactDOM from "react-dom/client"
import "./index.css"
import App from "./App.tsx"
ReactDOM.createRoot(document.getElementById('root')!).render(
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<App/>
<App />
</React.StrictMode>,
)

@ -1,11 +1,14 @@
import {useLocation} from "react-router-dom";
import {BASE} from "../Constants.ts";
import { useLocation } from "react-router-dom"
import { BASE } from "../Constants.ts"
export default function NotFoundPage() {
const target = useLocation()
return <div>
<h1>{target.pathname} NOT FOUND !</h1>
<button onClick={() => location.pathname = BASE + "/"}>go back to home</button>
</div>
return (
<div>
<h1>{target.pathname} NOT FOUND !</h1>
<button onClick={() => (location.pathname = BASE + "/")}>
go back to home
</button>
</div>
)
}

@ -1,4 +1,3 @@
export default function CreateTeamPage() {
return <h1>Create Team Page</h1>
}
}

@ -19,10 +19,18 @@ import { BallPiece } from "../components/editor/BallPiece"
import { Rack } from "../components/Rack"
import { PlayerPiece } from "../components/editor/PlayerPiece"
import { CourtType, Tactic, TacticComponent, TacticContent } from "../model/tactic/Tactic"
import {
CourtType,
Tactic,
TacticComponent,
TacticContent,
} from "../model/tactic/Tactic"
import { fetchAPI, fetchAPIGet } from "../Fetcher"
import SavingState, { SaveState, SaveStates } from "../components/editor/SavingState"
import SavingState, {
SaveState,
SaveStates,
} from "../components/editor/SavingState"
import { BALL_TYPE } from "../model/tactic/CourtObjects"
import { CourtAction } from "../components/editor/CourtAction"
@ -39,15 +47,30 @@ import {
removeBall,
updateComponent,
} from "../editor/TacticContentDomains"
import { BallState, Player, PlayerInfo, PlayerLike, PlayerTeam } from "../model/tactic/Player"
import {
BallState,
Player,
PlayerInfo,
PlayerLike,
PlayerTeam,
} from "../model/tactic/Player"
import { RackedCourtObject, RackedPlayer } from "../editor/RackedItems"
import CourtPlayer from "../components/editor/CourtPlayer"
import { createAction, getActionKind, isActionValid, removeAction } from "../editor/ActionsDomains"
import {
createAction,
getActionKind,
isActionValid,
removeAction,
} from "../editor/ActionsDomains"
import ArrowAction from "../components/actions/ArrowAction"
import { middlePos, Pos, ratioWithinBase } from "../geo/Pos"
import { Action, ActionKind } from "../model/tactic/Action"
import BallAction from "../components/actions/BallAction"
import { changePlayerBallState, getOrigin, removePlayer } from "../editor/PlayerDomains"
import {
changePlayerBallState,
getOrigin,
removePlayer,
} from "../editor/PlayerDomains"
import { CourtBall } from "../components/editor/CourtBall"
import { useParams } from "react-router-dom"
import { DEFAULT_TACTIC_NAME } from "./NewTacticPage.tsx"
@ -81,9 +104,13 @@ export default function EditorPage({ guestMode }: EditorPageProps) {
const id = guestMode ? -1 : parseInt(idStr!)
useEffect(() => {
if (guestMode) {
setTactic({id: -1, courtType: "PLAIN", content: "{\"components\": []}", name: DEFAULT_TACTIC_NAME})
setTactic({
id: -1,
courtType: "PLAIN",
content: '{"components": []}',
name: DEFAULT_TACTIC_NAME,
})
return
}
@ -94,7 +121,6 @@ export default function EditorPage({ guestMode }: EditorPageProps) {
const { name, courtType } = await (await infoResponse).json()
const { content } = await (await contentResponse).json()
setTactic({ id, name, courtType, content })
}
@ -102,18 +128,19 @@ export default function EditorPage({ guestMode }: EditorPageProps) {
}, [guestMode, id, idStr])
if (tactic) {
return <Editor
id={id}
courtType={tactic.courtType}
content={tactic.content}
name={tactic.name}
/>
return (
<Editor
id={id}
courtType={tactic.courtType}
content={tactic.content}
name={tactic.name}
/>
)
}
return <EditorLoadingScreen />
}
function EditorLoadingScreen() {
return <div>Loading Editor, please wait...</div>
}
@ -133,8 +160,7 @@ function Editor({ id, name, courtType, content }: EditorProps) {
isInGuestMode && storageContent != null ? storageContent : content
const storageName = localStorage.getItem(GUEST_MODE_TITLE_STORAGE_KEY)
const editorName =
isInGuestMode && storageName != null ? storageName : name
const editorName = isInGuestMode && storageName != null ? storageName : name
return (
<EditorView
@ -152,8 +178,8 @@ function Editor({ id, name, courtType, content }: EditorProps) {
)
return SaveStates.Guest
}
return fetchAPI(`tactics/${id}/1`, { content }, "PUT").then((r) =>
r.ok ? SaveStates.Ok : SaveStates.Err,
return fetchAPI(`tactics/${id}/1`, { content }, "PUT").then(
(r) => (r.ok ? SaveStates.Ok : SaveStates.Err),
)
}}
onNameChange={async (name: string) => {
@ -170,10 +196,10 @@ function Editor({ id, name, courtType, content }: EditorProps) {
}
function EditorView({
tactic: { id, name, content: initialContent, courtType },
onContentChange,
onNameChange,
}: EditorViewProps) {
tactic: { id, name, content: initialContent, courtType },
onContentChange,
onNameChange,
}: EditorViewProps) {
const isInGuestMode = id == -1
const [titleStyle, setTitleStyle] = useState<CSSProperties>({})
@ -551,12 +577,12 @@ interface PlayerRackProps {
}
function PlayerRack({
id,
objects,
setObjects,
courtRef,
setComponents,
}: PlayerRackProps) {
id,
objects,
setObjects,
courtRef,
setComponents,
}: PlayerRackProps) {
const courtBounds = useCallback(
() => courtRef.current!.getBoundingClientRect(),
[courtRef],
@ -610,15 +636,15 @@ interface CourtPlayerArrowActionProps {
}
function CourtPlayerArrowAction({
playerInfo,
player,
isInvalid,
content,
setContent,
setPreviewAction,
courtRef,
}: CourtPlayerArrowActionProps) {
playerInfo,
player,
isInvalid,
content,
setContent,
setPreviewAction,
courtRef,
}: CourtPlayerArrowActionProps) {
const courtBounds = useCallback(
() => courtRef.current!.getBoundingClientRect(),
[courtRef],
@ -649,7 +675,7 @@ function CourtPlayerArrowAction({
}))
}}
onHeadPicked={(headPos) => {
;(document.activeElement as HTMLElement).blur()
(document.activeElement as HTMLElement).blur()
setPreviewAction({
origin: playerInfo.id,
@ -785,4 +811,4 @@ function useContentState<S>(
)
return [content, setContentSynced, savingState]
}
}

@ -21,12 +21,12 @@ interface Team {
second_color: string
}
export default function HomePage() {
type UserDataResponse = {user?: User, tactics: Tactic[], teams: Team[]}
const [{tactics, teams }, setInfo] = useState<UserDataResponse>({tactics: [], teams: []})
type UserDataResponse = { user?: User; tactics: Tactic[]; teams: Team[] }
const [{ tactics, teams }, setInfo] = useState<UserDataResponse>({
tactics: [],
teams: [],
})
useLayoutEffect(() => {
const session = getSession()
@ -44,11 +44,12 @@ export default function HomePage() {
getUser()
}, [])
tactics!.sort((a, b) => b.creationDate - a.creationDate)
const lastTactics = tactics.slice(0, 5)
return <Home teams={teams!} allTactics={tactics!} lastTactics={lastTactics}/>
return (
<Home teams={teams!} allTactics={tactics!} lastTactics={lastTactics} />
)
}
function Home({

@ -6,8 +6,6 @@ import { getSession, saveSession } from "../api/session.ts"
import "../style/form.css"
export default function LoginApp() {
const [errors, setErrors] = useState<Failure[]>([])
const emailRef = useRef<HTMLInputElement>(null)
@ -19,46 +17,82 @@ export default function LoginApp() {
const email = emailRef.current!.value
const password = passwordRef.current!.value
const response = await fetchAPI("auth/token", {email, password})
const response = await fetchAPI("auth/token", { email, password })
if (response.ok) {
const session = getSession()
const { token, expirationDate } = await response.json()
saveSession({...session, auth: { token, expirationDate } })
saveSession({ ...session, auth: { token, expirationDate } })
redirect(session.urlTarget ?? "/")
return
}
try {
const failures = await response.json()
setErrors(Object.entries<string[]>(failures).map(([type, messages]) => ({ type, messages })))
setErrors(
Object.entries<string[]>(failures).map(([type, messages]) => ({
type,
messages,
})),
)
} catch (e) {
setErrors([{ type: "internal error", messages: ["an internal error occurred."] }])
setErrors([
{
type: "internal error",
messages: ["an internal error occurred."],
},
])
}
}
return <div className="container">
<center>
<h2>Se connecter</h2>
</center>
return (
<div className="container">
<center>
<h2>Se connecter</h2>
</center>
{errors.map(({ type, messages }) =>
messages.map(message => <p key={type} style={{ color: "red" }}>{type} : {message}</p>))}
{errors.map(({ type, messages }) =>
messages.map((message) => (
<p key={type} style={{ color: "red" }}>
{type} : {message}
</p>
)),
)}
<form onSubmit={handleSubmit}>
<div className="form-group">
<label htmlFor="email">Email :</label>
<input ref={emailRef} type="text" id="email" name="email" required />
<form onSubmit={handleSubmit}>
<div className="form-group">
<label htmlFor="email">Email :</label>
<input
ref={emailRef}
type="text"
id="email"
name="email"
required
/>
<label htmlFor="password">Mot de passe :</label>
<input ref={passwordRef} type="password" id="password" name="password" required />
<label htmlFor="password">Mot de passe :</label>
<input
ref={passwordRef}
type="password"
id="password"
name="password"
required
/>
<a href={BASE + "/register"} className="inscr">Vous n'avez pas de compte ?</a>
<br /><br />
<div id="buttons">
<input className="button" type="submit" value="Se connecter" />
<a href={BASE + "/register"} className="inscr">
Vous n'avez pas de compte ?
</a>
<br />
<br />
<div id="buttons">
<input
className="button"
type="submit"
value="Se connecter"
/>
</div>
</div>
</div>
</form>
</div>
}
</form>
</div>
)
}

@ -10,7 +10,6 @@ import { getSession } from "../api/session.ts"
export const DEFAULT_TACTIC_NAME = "Nouvelle tactique"
export default function NewTacticPage() {
return (
<div id={"panel-root"}>
@ -36,10 +35,10 @@ export default function NewTacticPage() {
}
function CourtKindButton({
name,
image,
courtType,
}: {
name,
image,
courtType,
}: {
name: string
image: string
courtType: CourtType
@ -48,20 +47,22 @@ function CourtKindButton({
<div
className="court-kind-button"
onClick={useCallback(async () => {
// if user is not authenticated
if (!getSession().auth) {
redirect(`/tactic/edit-guest`)
}
const response = await fetchAPI("tactics", {
name: DEFAULT_TACTIC_NAME,
courtType,
}, "POST")
const response = await fetchAPI(
"tactics",
{
name: DEFAULT_TACTIC_NAME,
courtType,
},
"POST",
)
const { id } = await response.json()
redirect(`/tactic/${id}/edit`)
}, [courtType])}>
<div className="court-kind-button-top">
<div className="court-kind-button-image-div">

@ -7,13 +7,11 @@ import { fetchAPI, redirect } from "../Fetcher.ts"
import { getSession, saveSession } from "../api/session.ts"
export default function RegisterPage() {
const usernameField = useRef<HTMLInputElement>(null)
const passwordField = useRef<HTMLInputElement>(null)
const confirmpasswordField = useRef<HTMLInputElement>(null)
const emailField = useRef<HTMLInputElement>(null)
const [errors, setErrors] = useState<Failure[]>([])
async function handleSubmit(e: FormEvent) {
@ -25,69 +23,125 @@ export default function RegisterPage() {
const email = emailField.current!.value
if (confirmpassword !== password) {
setErrors([{
type: "password",
messages: ["le mot de passe et la confirmation du mot de passe ne sont pas equivalent."],
}])
setErrors([
{
type: "password",
messages: [
"le mot de passe et la confirmation du mot de passe ne sont pas equivalent.",
],
},
])
return
}
const response = await fetchAPI("auth/register", { username, password, email })
const response = await fetchAPI("auth/register", {
username,
password,
email,
})
if (response.ok) {
const { token, expirationDate } = await response.json()
const session = getSession()
saveSession({...session, auth: { token, expirationDate } })
saveSession({ ...session, auth: { token, expirationDate } })
redirect(session.urlTarget ?? "/")
return
}
try {
const failures = await response.json()
setErrors(Object.entries<string[]>(failures).map(([type, messages]) => ({ type, messages })))
setErrors(
Object.entries<string[]>(failures).map(([type, messages]) => ({
type,
messages,
})),
)
} catch (e) {
setErrors([{ type: "internal error", messages: ["an internal error occurred."] }])
setErrors([
{
type: "internal error",
messages: ["an internal error occurred."],
},
])
}
}
return (
<div className="container">
<center>
<h2>S'enregistrer</h2>
</center>
<div>
{errors.map(({ type, messages }) =>
messages.map((message) => (
<p key={type} style={{ color: "red" }}>
{type} : {message}
</p>
)),
)}
</div>
return <div className="container">
<center>
<h2>S'enregistrer</h2>
</center>
<div>
{errors.map(({ type, messages }) =>
messages.map(message => <p key={type} style={{ color: "red" }}>{type} : {message}</p>))}
<form onSubmit={handleSubmit}>
<div className="form-group">
<label htmlFor="username">Nom d'utilisateur :</label>
<input
ref={usernameField}
type="text"
id="username"
name="username"
required
/>
<label htmlFor="password">Mot de passe :</label>
<input
ref={passwordField}
type="password"
id="password"
name="password"
required
/>
<label htmlFor="confirmpassword">
Confirmer le mot de passe :
</label>
<input
ref={confirmpasswordField}
type="password"
id="confirmpassword"
name="confirmpassword"
required
/>
<label htmlFor="email">Email :</label>
<input
ref={emailField}
type="text"
id="email"
name="email"
required
/>
<label className="consentement">
<input type="checkbox" name="consentement" required />
En cochant cette case, j'accepte que mes données
personnelles, tel que mon adresse e-mail, soient
collectées et traitées conformément à la politique de
confidentialité de Sportify.
</label>
<a href={BASE + "/login"} className="inscr">
Vous avez déjà un compte ?
</a>
</div>
<div id="buttons">
<input
className="button"
type="submit"
value="Créer votre compte"
/>
</div>
</form>
</div>
<form onSubmit={handleSubmit}>
<div className="form-group">
<label htmlFor="username">Nom d'utilisateur :</label>
<input ref={usernameField} type="text" id="username" name="username" required />
<label htmlFor="password">Mot de passe :</label>
<input ref={passwordField} type="password" id="password" name="password" required />
<label htmlFor="confirmpassword">Confirmer le mot de passe :</label>
<input ref={confirmpasswordField} type="password" id="confirmpassword" name="confirmpassword"
required />
<label htmlFor="email">Email :</label>
<input ref={emailField} type="text" id="email" name="email" required />
<label className="consentement">
<input type="checkbox" name="consentement" required />
En cochant cette case, j'accepte que mes données personnelles, tel que mon adresse e-mail, soient
collectées et traitées conformément à la politique de confidentialité de Sportify.
</label>
<a href={BASE + "/login"} className="inscr">Vous avez déjà un compte ?</a>
</div>
<div id="buttons">
<input className="button" type="submit" value="Créer votre compte" />
</div>
</form>
</div>
}
)
}

@ -1,25 +1,32 @@
import "../style/team_panel.css"
import {BASE} from "../Constants"
import {Member, Team, TeamInfo} from "../model/Team"
import {useParams} from "react-router-dom";
import { BASE } from "../Constants"
import { Member, Team, TeamInfo } from "../model/Team"
import { useParams } from "react-router-dom"
export default function TeamPanelPage() {
const {teamId} = useParams()
const { teamId } = useParams()
const teamInfo = {
id: parseInt(teamId!),
name: teamId!,
mainColor: "#FFFFFF",
secondColor: "#000000",
picture: "https://a.espncdn.com/combiner/i?img=/i/teamlogos/nba/500/lal.png"
picture:
"https://a.espncdn.com/combiner/i?img=/i/teamlogos/nba/500/lal.png",
}
return <TeamPanel team={{info: teamInfo, members: []}} currentUserId={0} isCoach={false}/>
return (
<TeamPanel
team={{ info: teamInfo, members: [] }}
currentUserId={0}
isCoach={false}
/>
)
}
function TeamPanel({
isCoach,
team,
currentUserId,
}: {
isCoach,
team,
currentUserId,
}: {
isCoach: boolean
team: Team
currentUserId: number
@ -31,9 +38,9 @@ function TeamPanel({
<a href={BASE + "/"}>IQBall</a>
</h1>
</header>
<TeamDisplay team={team.info}/>
<TeamDisplay team={team.info} />
{isCoach && <CoachOptions id={team.info.id}/>}
{isCoach && <CoachOptions id={team.info.id} />}
<MembersDisplay
members={team.members}
@ -45,12 +52,12 @@ function TeamPanel({
)
}
function TeamDisplay({team}: { team: TeamInfo }) {
function TeamDisplay({ team }: { team: TeamInfo }) {
return (
<div id="team-info">
<div id="first-part">
<h1 id="team-name">{team.name}</h1>
<img id="logo" src={team.picture} alt="Logo d'équipe"/>
<img id="logo" src={team.picture} alt="Logo d'équipe" />
</div>
<div id="colors">
<div id="colorsTitle">
@ -58,19 +65,19 @@ function TeamDisplay({team}: { team: TeamInfo }) {
<p>Couleur secondaire</p>
</div>
<div id="actual-colors">
<ColorDisplay color={team.mainColor}/>
<ColorDisplay color={team.secondColor}/>
<ColorDisplay color={team.mainColor} />
<ColorDisplay color={team.secondColor} />
</div>
</div>
</div>
)
}
function ColorDisplay({color}: { color: string }) {
return <div className="square" style={{backgroundColor: color}}/>
function ColorDisplay({ color }: { color: string }) {
return <div className="square" style={{ backgroundColor: color }} />
}
function CoachOptions({id}: { id: number }) {
function CoachOptions({ id }: { id: number }) {
return (
<div>
<button
@ -94,11 +101,11 @@ function CoachOptions({id}: { id: number }) {
}
function MembersDisplay({
members,
isCoach,
idTeam,
currentUserId,
}: {
members,
isCoach,
idTeam,
currentUserId,
}: {
members: Member[]
isCoach: boolean
idTeam: number
@ -132,11 +139,11 @@ function MembersDisplay({
}
function MemberDisplay({
member,
isCoach,
idTeam,
currentUserId,
}: {
member,
isCoach,
idTeam,
currentUserId,
}: {
member: Member
isCoach: boolean
idTeam: number

@ -1,23 +1,23 @@
import React, { CSSProperties, useState } from "react"
import "../style/visualizer.css"
import Court from "../assets/court/full_court.svg"
export default function Visualizer({ id, name }: { id: number; name: string }) {
const [style, setStyle] = useState<CSSProperties>({})
return (
<div id="main">
<div id="topbar">
<h1>{name}</h1>
</div>
<div id="court-container">
<img
id="court"
src={Court}
style={style}
alt="Basketball Court"
/>
</div>
</div>
)
}
// import React, { CSSProperties, useState } from "react"
// import "../style/visualizer.css"
// import Court from "../assets/court/full_court.svg"
//
// // export default function Visualizer({ id, name }: { id: number; name: string }) {
// // const [style, setStyle] = useState<CSSProperties>({})
// //
// // return (
// // <div id="main">
// // <div id="topbar">
// // <h1>{name}</h1>
// // </div>
// // <div id="court-container">
// // <img
// // id="court"
// // src={Court}
// // style={style}
// // alt="Basketball Court"
// // />
// // </div>
// // </div>
// // )
// // }

@ -4,17 +4,18 @@ import { useEffect, useState } from "react"
import { fetchAPIGet, redirect } from "../../Fetcher.ts"
import { getSession, saveSession } from "../../api/session.ts"
export function Header() {
const session = getSession()
const [username, setUsername] = useState<string | null>(session.username ?? null)
const [username, setUsername] = useState<string | null>(
session.username ?? null,
)
useEffect(() => {
async function loadUsername() {
const response = await fetchAPIGet("user", false)
if (response.status == 401) { //if unauthorized
if (response.status == 401) {
//if unauthorized
return
}
@ -26,12 +27,8 @@ export function Header() {
setUsername(username)
}
// if the user is authenticated and the username is not already present in the session,
if (session.auth && !session.username)
loadUsername()
if (session.auth && !session.username) loadUsername()
}, [session])
return (
@ -46,16 +43,20 @@ export function Header() {
</p>
</div>
<div id="header-right">
<div className="clickable"
id="clickable-header-right"
onClick={() => {
if (username) {
redirect("/settings")
return
}
saveSession({...session, urlTarget: location.pathname})
redirect("/login")
}}>
<div
className="clickable"
id="clickable-header-right"
onClick={() => {
if (username) {
redirect("/settings")
return
}
saveSession({
...session,
urlTarget: location.pathname,
})
redirect("/login")
}}>
{/* <AccountSvg id="img-account" /> */}
<AccountSvg id="img-account" />
<p id="username">{username ?? "Log In"}</p>

@ -3,4 +3,4 @@
width: 100vw;
display: flex;
flex-direction: column;
}
}

@ -25,7 +25,8 @@ label {
margin-bottom: 5px;
}
input[type="text"], input[type="password"] {
input[type="text"],
input[type="password"] {
width: 95%;
padding: 10px;
border: 1px solid #ccc;
@ -37,23 +38,22 @@ input[type="text"], input[type="password"] {
font-style: italic;
}
.inscr{
.inscr {
font-size: small;
text-align: right;
}
.consentement{
.consentement {
font-size: small;
}
#buttons{
#buttons {
display: flex;
justify-content: center;
padding: 10px 20px;
}
.button{
.button {
background-color: #007bff;
color: #fff;
padding: 10px 20px;
@ -62,6 +62,6 @@ input[type="text"], input[type="password"] {
cursor: pointer;
}
.button:hover{
.button:hover {
background-color: #0056b3;
}
}

@ -17,7 +17,9 @@
cursor: pointer;
}
#header-left, #header-right, #header-center {
#header-left,
#header-right,
#header-center {
width: 100%;
}
@ -57,4 +59,3 @@
font-weight: bold;
font-size: 35px;
}

@ -1,25 +1,26 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2023", "DOM", "DOM.Iterable"],
"types": ["vite/client", "vite-plugin-svgr/client"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}

@ -1,11 +1,11 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"strict": true
},
"include": ["vite.config.ts"]
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"strict": true
},
"include": ["vite.config.ts"]
}

@ -1,17 +1,20 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import cssInjectedByJsPlugin from "vite-plugin-css-injected-by-js";
import svgr from "vite-plugin-svgr";
import { defineConfig } from "vite"
import react from "@vitejs/plugin-react"
import cssInjectedByJsPlugin from "vite-plugin-css-injected-by-js"
import svgr from "vite-plugin-svgr"
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
react(),
cssInjectedByJsPlugin({
relativeCSSInjection: true,
}),
svgr({
include: "**/*.svg?react"
})
]
build: {
target: 'es2023',
},
plugins: [
react(),
cssInjectedByJsPlugin({
relativeCSSInjection: true,
}),
svgr({
include: "**/*.svg?react",
}),
],
})

Loading…
Cancel
Save