From 221100e8422d5c5c5c5b4ba5aa0778ef4903a702 Mon Sep 17 00:00:00 2001 From: maxime Date: Fri, 16 Feb 2024 19:54:07 +0100 Subject: [PATCH] configure eslint --- .eslintrc.cjs | 30 +++--- .eslintrc.js | 29 +++--- .php-cs-fixer.php | 16 --- .prettierrc | 12 +-- Documentation/Conception.md | 53 +++++----- Documentation/Description.md | 21 ++-- Documentation/README.md | 6 +- Documentation/how-to-dev.md | 56 +++++----- README.md | 2 + ci/.drone.yml | 97 +++++++----------- index.html | 20 ++-- package.json | 79 ++++++++------- src/App.tsx | 41 +++++--- src/Fetcher.ts | 25 ++--- src/api/failure.ts | 4 +- src/api/session.ts | 2 +- src/components/TitleInput.tsx | 4 +- src/components/editor/CourtBall.tsx | 2 +- src/index.css | 2 - src/main.tsx | 13 ++- src/pages/404.tsx | 17 ++-- src/pages/CreateTeamPage.tsx | 3 +- src/pages/Editor.tsx | 106 +++++++++++-------- src/pages/HomePage.tsx | 15 +-- src/pages/LoginPage.tsx | 86 +++++++++++----- src/pages/NewTacticPage.tsx | 23 +++-- src/pages/RegisterPage.tsx | 152 +++++++++++++++++++--------- src/pages/TeamPanel.tsx | 65 ++++++------ src/pages/Visualizer.tsx | 46 ++++----- src/pages/template/Header.tsx | 39 +++---- src/style/app.css | 2 +- src/style/form.css | 16 +-- src/style/template/header.css | 5 +- tsconfig.json | 43 ++++---- tsconfig.node.json | 18 ++-- vite.config.ts | 29 +++--- 36 files changed, 651 insertions(+), 528 deletions(-) delete mode 100644 .php-cs-fixer.php diff --git a/.eslintrc.cjs b/.eslintrc.cjs index d6c9537..3961d0a 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -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 }, + ], + }, } diff --git a/.eslintrc.js b/.eslintrc.js index 16f7f84..062a82e 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -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' - } - } -}; \ No newline at end of file + version: "detect", + }, + }, +} diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php deleted file mode 100644 index 77ef0e7..0000000 --- a/.php-cs-fixer.php +++ /dev/null @@ -1,16 +0,0 @@ -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); diff --git a/.prettierrc b/.prettierrc index 7db0434..2b63492 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,7 +1,7 @@ { - "bracketSameLine": true, - "trailingComma": "all", - "printWidth": 80, - "tabWidth": 4, - "semi": false -} \ No newline at end of file + "bracketSameLine": true, + "trailingComma": "all", + "printWidth": 80, + "tabWidth": 4, + "semi": false +} diff --git a/Documentation/Conception.md b/Documentation/Conception.md index 177be45..31249e0 100644 --- a/Documentation/Conception.md +++ b/Documentation/Conception.md @@ -4,25 +4,27 @@ Notre projet est divisé en plusieurs parties: -- `src/API`, qui définit les classes qui implémentent les actions de l’api -- `src/App`, qui définit les contrôleurs et les vues de l’application 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 l’application et les actions que l’ont 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 n’est pas initialisée. -- `profiles`, définit les profiles d’execution, voir `Documentation/how-to-dev.md` pour plus d’info -- `front` contient le code front-end react/typescript -- `ci` contient les scripts de déploiement et la définition du workflow d’intégration continue et de déploiement constant vers notre staging server ([maxou.dev//public/](https://maxou.dev/IQBall/master/public)). -- `public` point d’entrée, avec : - - `public/index.php` point d’entrée pour la webapp - - `public/api/index.php` point d’entrée pour l’api. +- `src/API`, qui définit les classes qui implémentent les actions de l’api +- `src/App`, qui définit les contrôleurs et les vues de l’application 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 l’application et les actions que l’ont 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 n’est pas initialisée. +- `profiles`, définit les profiles d’execution, voir `Documentation/how-to-dev.md` pour plus d’info +- `front` contient le code front-end react/typescript +- `ci` contient les scripts de déploiement et la définition du workflow d’intégration continue et de déploiement constant vers notre staging server ([maxou.dev//public/](https://maxou.dev/IQBall/master/public)). +- `public` point d’entrée, avec : + - `public/index.php` point d’entrée pour la webapp + - `public/api/index.php` point d’entrée pour l’api. ## 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 d’un ensemble de joueurs et d’adversaires sur le terrain, +Une stratégie est un arbre composé de plusieurs étapes, une étape étant constituée d’un ensemble de joueurs et d’adversaires 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 d’une étape, sont d’autres é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 l’interface soit réactive : si l’on bouge un joueur, +Pour rendre le tout agréable à utiliser, il faut que l’interface soit réactive : si l’on 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 l’effectuer en utilisant le framework React qui rend simple le développement d’interfaces dynamiques, et d’utiliser typescript parce qu’ici on code bien et qu’on impose une type safety a notre code. - diff --git a/Documentation/Description.md b/Documentation/Description.md index fee90c5..fecd0e0 100644 --- a/Documentation/Description.md +++ b/Documentation/Description.md @@ -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) diff --git a/Documentation/README.md b/Documentation/README.md index 24c5472..6cc1eb2 100644 --- a/Documentation/README.md +++ b/Documentation/README.md @@ -1,3 +1,3 @@ -* [Description.md](Description.md) -* [Conception.md](Conception.md) -* [how-to-dev.md](how-to-dev.md) \ No newline at end of file +- [Description.md](Description.md) +- [Conception.md](Conception.md) +- [how-to-dev.md](how-to-dev.md) diff --git a/Documentation/how-to-dev.md b/Documentation/how-to-dev.md index 39435e6..287a7fe 100644 --- a/Documentation/how-to-dev.md +++ b/Documentation/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 ` ``` -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 ` - + + + + + Vite + React + TS + + +
+ + diff --git a/package.json b/package.json index 9904fe4..75215b4 100644 --- a/package.json +++ b/package.json @@ -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" + } } diff --git a/src/App.tsx b/src/App.tsx index 0f8dcb4..13379fe 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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() { - } /> } /> - }> } /> } /> - } /> - } /> - } /> - } /> - } /> + } + /> + } + /> + } + /> + } + /> + } + /> } /> - @@ -46,8 +57,10 @@ export default function App() { } function AppLayout() { - return <> -
- - -} \ No newline at end of file + return ( + <> +
+ + + ) +} diff --git a/src/Fetcher.ts b/src/Fetcher.ts index 05d8d47..52a71b3 100644 --- a/src/Fetcher.ts +++ b/src/Fetcher.ts @@ -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 { - 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 { - 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 { +async function handleResponse( + session: Session, + response: Response, + redirectIfNotAuth: boolean, +): Promise { // 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 diff --git a/src/api/failure.ts b/src/api/failure.ts index 7219ecd..3ba5e88 100644 --- a/src/api/failure.ts +++ b/src/api/failure.ts @@ -1,6 +1,4 @@ - - export interface Failure { type: string messages: string[] -} \ No newline at end of file +} diff --git a/src/api/session.ts b/src/api/session.ts index fad80ad..a061228 100644 --- a/src/api/session.ts +++ b/src/api/session.ts @@ -20,4 +20,4 @@ export function saveSession(session: Session) { export function getSession(): Session { const json = localStorage.getItem(SESSION_KEY) return json ? JSON.parse(json) : {} -} \ No newline at end of file +} diff --git a/src/components/TitleInput.tsx b/src/components/TitleInput.tsx index 25f4697..90447e2 100644 --- a/src/components/TitleInput.tsx +++ b/src/components/TitleInput.tsx @@ -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() }} diff --git a/src/components/editor/CourtBall.tsx b/src/components/editor/CourtBall.tsx index b167126..e1ac542 100644 --- a/src/components/editor/CourtBall.tsx +++ b/src/components/editor/CourtBall.tsx @@ -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" diff --git a/src/index.css b/src/index.css index 7ee0cd3..d7d83a6 100644 --- a/src/index.css +++ b/src/index.css @@ -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; } - diff --git a/src/main.tsx b/src/main.tsx index 7229199..7042692 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -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( - + , ) diff --git a/src/pages/404.tsx b/src/pages/404.tsx index 558b442..33c4b58 100644 --- a/src/pages/404.tsx +++ b/src/pages/404.tsx @@ -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
-

{target.pathname} NOT FOUND !

- -
+ return ( +
+

{target.pathname} NOT FOUND !

+ +
+ ) } diff --git a/src/pages/CreateTeamPage.tsx b/src/pages/CreateTeamPage.tsx index daefb83..e7deacc 100644 --- a/src/pages/CreateTeamPage.tsx +++ b/src/pages/CreateTeamPage.tsx @@ -1,4 +1,3 @@ - export default function CreateTeamPage() { return

Create Team Page

-} \ No newline at end of file +} diff --git a/src/pages/Editor.tsx b/src/pages/Editor.tsx index afd4501..5bd05b9 100644 --- a/src/pages/Editor.tsx +++ b/src/pages/Editor.tsx @@ -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 + return ( + + ) } return } - function EditorLoadingScreen() { return
Loading Editor, please wait...
} @@ -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 ( - 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({}) @@ -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( ) return [content, setContentSynced, savingState] -} \ No newline at end of file +} diff --git a/src/pages/HomePage.tsx b/src/pages/HomePage.tsx index 6f21c90..8b94656 100644 --- a/src/pages/HomePage.tsx +++ b/src/pages/HomePage.tsx @@ -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({tactics: [], teams: []}) - + type UserDataResponse = { user?: User; tactics: Tactic[]; teams: Team[] } + const [{ tactics, teams }, setInfo] = useState({ + 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 + return ( + + ) } function Home({ diff --git a/src/pages/LoginPage.tsx b/src/pages/LoginPage.tsx index 231b75b..b12c386 100644 --- a/src/pages/LoginPage.tsx +++ b/src/pages/LoginPage.tsx @@ -6,8 +6,6 @@ import { getSession, saveSession } from "../api/session.ts" import "../style/form.css" export default function LoginApp() { - - const [errors, setErrors] = useState([]) const emailRef = useRef(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(failures).map(([type, messages]) => ({ type, messages }))) + setErrors( + Object.entries(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
-
-

Se connecter

-
+ return ( +
+
+

Se connecter

+
- {errors.map(({ type, messages }) => - messages.map(message =>

{type} : {message}

))} + {errors.map(({ type, messages }) => + messages.map((message) => ( +

+ {type} : {message} +

+ )), + )} -
-
- - + +
+ + - - + + - Vous n'avez pas de compte ? -

- -
- -
-} \ No newline at end of file + +
+ ) +} diff --git a/src/pages/NewTacticPage.tsx b/src/pages/NewTacticPage.tsx index dba5240..fa8313c 100644 --- a/src/pages/NewTacticPage.tsx +++ b/src/pages/NewTacticPage.tsx @@ -10,7 +10,6 @@ import { getSession } from "../api/session.ts" export const DEFAULT_TACTIC_NAME = "Nouvelle tactique" - export default function NewTacticPage() { return (
@@ -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({
{ - // 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])}>
diff --git a/src/pages/RegisterPage.tsx b/src/pages/RegisterPage.tsx index f3eeebb..263b680 100644 --- a/src/pages/RegisterPage.tsx +++ b/src/pages/RegisterPage.tsx @@ -7,13 +7,11 @@ import { fetchAPI, redirect } from "../Fetcher.ts" import { getSession, saveSession } from "../api/session.ts" export default function RegisterPage() { - const usernameField = useRef(null) const passwordField = useRef(null) const confirmpasswordField = useRef(null) const emailField = useRef(null) - const [errors, setErrors] = useState([]) 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(failures).map(([type, messages]) => ({ type, messages }))) + setErrors( + Object.entries(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 ( +
+
+

S'enregistrer

+
+ +
+ {errors.map(({ type, messages }) => + messages.map((message) => ( +

+ {type} : {message} +

+ )), + )} +
- return
-
-

S'enregistrer

-
- -
- {errors.map(({ type, messages }) => - messages.map(message =>

{type} : {message}

))} +
+
+ + + + + + + + + + + + + + + + Vous avez déjà un compte ? + +
+
+ +
+
- -
-
- - - - - - - - - - - - - - - - Vous avez déjà un compte ? -
-
- -
-
-
-} \ No newline at end of file + ) +} diff --git a/src/pages/TeamPanel.tsx b/src/pages/TeamPanel.tsx index e0e2baa..eb35239 100644 --- a/src/pages/TeamPanel.tsx +++ b/src/pages/TeamPanel.tsx @@ -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 + return ( + + ) } function TeamPanel({ - isCoach, - team, - currentUserId, - }: { + isCoach, + team, + currentUserId, +}: { isCoach: boolean team: Team currentUserId: number @@ -31,9 +38,9 @@ function TeamPanel({ IQBall
- + - {isCoach && } + {isCoach && }

{team.name}

- +
@@ -58,19 +65,19 @@ function TeamDisplay({team}: { team: TeamInfo }) {

Couleur secondaire

- - + +
) } -function ColorDisplay({color}: { color: string }) { - return
+function ColorDisplay({ color }: { color: string }) { + return
} -function CoachOptions({id}: { id: number }) { +function CoachOptions({ id }: { id: number }) { return (
-
{ - if (username) { - redirect("/settings") - return - } - saveSession({...session, urlTarget: location.pathname}) - redirect("/login") - }}> +
{ + if (username) { + redirect("/settings") + return + } + saveSession({ + ...session, + urlTarget: location.pathname, + }) + redirect("/login") + }}> {/* */}

{username ?? "Log In"}

diff --git a/src/style/app.css b/src/style/app.css index 09e66b9..38b4ffc 100644 --- a/src/style/app.css +++ b/src/style/app.css @@ -3,4 +3,4 @@ width: 100vw; display: flex; flex-direction: column; -} \ No newline at end of file +} diff --git a/src/style/form.css b/src/style/form.css index 3db12c0..c950f7d 100644 --- a/src/style/form.css +++ b/src/style/form.css @@ -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; -} \ No newline at end of file +} diff --git a/src/style/template/header.css b/src/style/template/header.css index 7a8d6e5..46607aa 100644 --- a/src/style/template/header.css +++ b/src/style/template/header.css @@ -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; } - diff --git a/tsconfig.json b/tsconfig.json index a7fc6fb..bb336e5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -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" }] } diff --git a/tsconfig.node.json b/tsconfig.node.json index 97ede7e..6675f46 100644 --- a/tsconfig.node.json +++ b/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"] } diff --git a/vite.config.ts b/vite.config.ts index 596d3e8..93ea92c 100644 --- a/vite.config.ts +++ b/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", + }), + ], })