configure eslint
continuous-integration/drone/push Build is passing Details

pull/107/head
maxime 1 year ago
parent a4a9b6c6e2
commit 221100e842

@ -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,21 +4,23 @@
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.
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
@ -56,6 +58,7 @@ 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 :
@ -74,6 +77,7 @@ public function doPostAction(array $form): HttpResponse {
// 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
@ -84,6 +88,7 @@ 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.
@ -92,6 +97,7 @@ et de garder à un seul endroit la responsabilitée d'écrire le contenu de la r
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.
@ -100,12 +106,12 @@ L'index définit aussi quoi faire lorsque l'application retourne une réponse. D
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
@ -120,4 +126,3 @@ il faut que les flèches qui y sont liés bougent aussi, il faut que les joueurs
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,6 +23,7 @@ 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.
@ -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!
@ -54,6 +57,7 @@ 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.
@ -61,21 +65,26 @@ We have two successors for now. ViewHttpResponse render not only a code but also
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.
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.
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,9 +70,9 @@ 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>
```
@ -86,6 +84,7 @@ This method then uses the `send_react_front`, to render the `views/SampleForm.ts
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 :
![](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).
@ -101,6 +100,7 @@ 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 :
### Development profile
```php
@ -112,9 +112,11 @@ 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 :
@ -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";

@ -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,5 +1,3 @@
export interface Failure {
type: string
messages: string[]

@ -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,

@ -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>

@ -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