From 2ce537b2b4bfab803b80e2d64db660d8040261a1 Mon Sep 17 00:00:00 2001 From: "maxime.batista" Date: Wed, 15 Nov 2023 14:26:26 +0100 Subject: [PATCH 01/18] add phpstan and fix some errors --- composer.json | 3 ++- src/Controller/Api/APITacticController.php | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index c78fb15..e5b80e0 100644 --- a/composer.json +++ b/composer.json @@ -9,6 +9,7 @@ "ext-json": "*", "ext-pdo": "*", "ext-pdo_sqlite": "*", - "twig/twig":"^2.0" + "twig/twig":"^2.0", + "phpstan/phpstan": "*" } } \ No newline at end of file diff --git a/src/Controller/Api/APITacticController.php b/src/Controller/Api/APITacticController.php index f775ecf..a39b2ce 100644 --- a/src/Controller/Api/APITacticController.php +++ b/src/Controller/Api/APITacticController.php @@ -29,7 +29,7 @@ class APITacticController { ], function (HttpRequest $request) use ($tactic_id) { $this->model->updateName($tactic_id, $request["name"]); return HttpResponse::fromCode(HttpCodes::OK); - }); + }, true); } public function newTactic(): HttpResponse { @@ -39,7 +39,7 @@ class APITacticController { $tactic = $this->model->makeNew($request["name"]); $id = $tactic->getId(); return new JsonHttpResponse(["id" => $id]); - }); + }, true); } public function getTacticInfo(int $id): HttpResponse { From 37cb541df9e51a84d9a4928c99d3c9c6d119ca9a Mon Sep 17 00:00:00 2001 From: Override-6 Date: Tue, 7 Nov 2023 01:06:10 +0100 Subject: [PATCH 02/18] place 5 players on the basketball court --- front/ViewRenderer.tsx | 2 -- front/assets/basketball_court.svg | 39 +++++++++++++++++++++++++ front/components/editor/BasketCourt.tsx | 35 ++++++++++++++++++++++ front/components/editor/Player.tsx | 34 +++++++++++++++++++++ front/style/basket_court.css | 8 +++++ front/style/colors.css | 5 ++++ front/style/editor.css | 19 ++++++++++-- front/style/player.css | 24 +++++++++++++++ front/views/Editor.tsx | 9 ++++-- package.json | 8 +++-- public/front | 1 + vite.config.ts | 4 +++ 12 files changed, 177 insertions(+), 11 deletions(-) create mode 100644 front/assets/basketball_court.svg create mode 100644 front/components/editor/BasketCourt.tsx create mode 100644 front/components/editor/Player.tsx create mode 100644 front/style/basket_court.css create mode 100644 front/style/player.css create mode 120000 public/front diff --git a/front/ViewRenderer.tsx b/front/ViewRenderer.tsx index 17148dc..ffaf886 100644 --- a/front/ViewRenderer.tsx +++ b/front/ViewRenderer.tsx @@ -11,8 +11,6 @@ export function renderView(Component: FunctionComponent, args: {}) { document.getElementById('root') as HTMLElement ); - console.log(args) - root.render( diff --git a/front/assets/basketball_court.svg b/front/assets/basketball_court.svg new file mode 100644 index 0000000..c5378b9 --- /dev/null +++ b/front/assets/basketball_court.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/front/components/editor/BasketCourt.tsx b/front/components/editor/BasketCourt.tsx new file mode 100644 index 0000000..bc19f9e --- /dev/null +++ b/front/components/editor/BasketCourt.tsx @@ -0,0 +1,35 @@ +import courtSvg from '../../assets/basketball_court.svg'; +import '../../style/basket_court.css'; +import React, {MouseEvent, ReactElement, useRef, useState} from "react"; +import Player from "./Player"; + + +export function BasketCourt() { + const [players, setPlayers] = useState([]) + const divRef = useRef(null); + + return ( +
{ + let bounds = divRef.current.getBoundingClientRect(); + + const player = ( + + ); + setPlayers([...players, player]) + }}> + {players} +
+ ) +} + + diff --git a/front/components/editor/Player.tsx b/front/components/editor/Player.tsx new file mode 100644 index 0000000..62f858a --- /dev/null +++ b/front/components/editor/Player.tsx @@ -0,0 +1,34 @@ +import React, {useEffect, useRef, useState} from "react"; +import "../../style/player.css"; +import Draggable, {ControlPosition, DraggableBounds} from "react-draggable"; + +export default function Player({id, x, y, bounds}: { + id: number, + x: number, + y: number, + bounds: DraggableBounds +}) { + + const ref = useRef(); + useEffect(() => { + const playerRect = ref.current?.getBoundingClientRect(); + bounds.bottom -= playerRect.height / 2; + bounds.right -= playerRect.width / 2; + }, [ref]) + + return ( + +
+

{id}

+
+
+ + ) +} \ No newline at end of file diff --git a/front/style/basket_court.css b/front/style/basket_court.css new file mode 100644 index 0000000..b1a3e10 --- /dev/null +++ b/front/style/basket_court.css @@ -0,0 +1,8 @@ + + +#court-container { + background-color: rebeccapurple; + display: flex; + width: 1000px; + height: 500px; +} \ No newline at end of file diff --git a/front/style/colors.css b/front/style/colors.css index 34bdbb5..68ccdaa 100644 --- a/front/style/colors.css +++ b/front/style/colors.css @@ -5,4 +5,9 @@ --second-color: #ccde54; --background-color: #d2cdd3; + + + + --selected-team-primarycolor: #ffffff; + --selected-team-secondarycolor: #000000; } \ No newline at end of file diff --git a/front/style/editor.css b/front/style/editor.css index 2ed88d6..62c4e9d 100644 --- a/front/style/editor.css +++ b/front/style/editor.css @@ -1,13 +1,16 @@ @import "colors.css"; -#main { +#main-div { + display: flex; height: 100%; width: 100%; background-color: var(--background-color); + + flex-direction: column; } -#topbar { +#topbar-div { display: flex; background-color: var(--main-color); @@ -17,4 +20,14 @@ .title_input { width: 25ch; -} \ No newline at end of file +} + +#court-div { + background-color: var(--background-color); + height: 100%; + + display: flex; + align-items: center; + justify-content: center; +} + diff --git a/front/style/player.css b/front/style/player.css new file mode 100644 index 0000000..4450dec --- /dev/null +++ b/front/style/player.css @@ -0,0 +1,24 @@ +.player { + font-family: monospace; + + background-color: var(--selected-team-primarycolor); + color: var(--selected-team-secondarycolor); + + + border-width: 2px; + border-radius: 100px; + border-style: solid; + + width: 20px; + height: 20px; + + display: flex; + + align-items: center; + justify-content: center; + + /*apply a translation to */ + transform: translate(-50%, -50%); + + user-select: none; +} \ No newline at end of file diff --git a/front/views/Editor.tsx b/front/views/Editor.tsx index 84d24e6..471a94d 100644 --- a/front/views/Editor.tsx +++ b/front/views/Editor.tsx @@ -2,6 +2,7 @@ import React, {CSSProperties, useState} from "react"; import "../style/editor.css"; import TitleInput from "../components/TitleInput"; import {API} from "../Constants"; +import {BasketCourt} from "../components/editor/BasketCourt"; const ERROR_STYLE: CSSProperties = { borderColor: "red" @@ -12,8 +13,8 @@ export default function Editor({id, name}: { id: number, name: string }) { const [style, setStyle] = useState({}); return ( -
-
+
+
LEFT
{ fetch(`${API}/tactic/${id}/edit/name`, { @@ -35,7 +36,9 @@ export default function Editor({id, name}: { id: number, name: string }) { }}/>
RIGHT
+
+ +
) } - diff --git a/package.json b/package.json index 0eb1e79..90209d0 100644 --- a/package.json +++ b/package.json @@ -12,10 +12,10 @@ "@types/react-dom": "^18.2.14", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-draggable": "^4.4.6", "typescript": "^5.2.2", "vite": "^4.5.0", - "vite-plugin-css-injected-by-js": "^3.3.0", - "web-vitals": "^2.1.4" + "vite-plugin-css-injected-by-js": "^3.3.0" }, "scripts": { "start": "vite --host", @@ -29,6 +29,8 @@ ] }, "devDependencies": { - "@vitejs/plugin-react": "^4.1.0" + "@svgr/webpack": "^8.1.0", + "@vitejs/plugin-react": "^4.1.0", + "vite-plugin-svgr": "^4.1.0" } } diff --git a/public/front b/public/front new file mode 120000 index 0000000..c1394c9 --- /dev/null +++ b/public/front @@ -0,0 +1 @@ +../front \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts index 03ab8f4..b81e587 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -2,6 +2,7 @@ import {defineConfig} from "vite"; import react from '@vitejs/plugin-react'; import fs from "fs"; import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'; +import svgr from 'vite-plugin-svgr'; function resolve_entries(dirname: string): [string, string][] { @@ -38,6 +39,9 @@ export default defineConfig({ react(), cssInjectedByJsPlugin({ relativeCSSInjection: true, + }), + svgr({ + include: "**/*.svg", }) ] }) From 57db0e094a6817db1adf5f72495e6d1103315238 Mon Sep 17 00:00:00 2001 From: Override-6 Date: Wed, 8 Nov 2023 19:48:18 +0100 Subject: [PATCH 03/18] WIP --- front/components/editor/BasketCourt.tsx | 14 +++++++++++--- front/components/editor/Player.tsx | 12 ++++++------ package.json | 1 - vite.config.ts | 4 ---- 4 files changed, 17 insertions(+), 14 deletions(-) diff --git a/front/components/editor/BasketCourt.tsx b/front/components/editor/BasketCourt.tsx index bc19f9e..768d4a5 100644 --- a/front/components/editor/BasketCourt.tsx +++ b/front/components/editor/BasketCourt.tsx @@ -3,6 +3,7 @@ import '../../style/basket_court.css'; import React, {MouseEvent, ReactElement, useRef, useState} from "react"; import Player from "./Player"; +const TEAM_MAX_PLAYER = 5; export function BasketCourt() { const [players, setPlayers] = useState([]) @@ -15,11 +16,18 @@ export function BasketCourt() { backgroundImage: `url(${courtSvg})` }} onClick={(e: MouseEvent) => { - let bounds = divRef.current.getBoundingClientRect(); + if (e.target != divRef.current) + return + let bounds = divRef.current!.getBoundingClientRect(); + let playerCount = players.length; + + if (playerCount >= TEAM_MAX_PLAYER) { + return; + } const player = ( - (); + const ref = useRef(null); useEffect(() => { - const playerRect = ref.current?.getBoundingClientRect(); - bounds.bottom -= playerRect.height / 2; - bounds.right -= playerRect.width / 2; + const playerRect = ref.current!.getBoundingClientRect(); + bounds.bottom! -= playerRect.height / 2; + bounds.right! -= playerRect.width / 2; }, [ref]) return ( diff --git a/package.json b/package.json index 90209d0..97f0039 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,6 @@ ] }, "devDependencies": { - "@svgr/webpack": "^8.1.0", "@vitejs/plugin-react": "^4.1.0", "vite-plugin-svgr": "^4.1.0" } diff --git a/vite.config.ts b/vite.config.ts index b81e587..34cb651 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -2,7 +2,6 @@ import {defineConfig} from "vite"; import react from '@vitejs/plugin-react'; import fs from "fs"; import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'; -import svgr from 'vite-plugin-svgr'; function resolve_entries(dirname: string): [string, string][] { @@ -40,8 +39,5 @@ export default defineConfig({ cssInjectedByJsPlugin({ relativeCSSInjection: true, }), - svgr({ - include: "**/*.svg", - }) ] }) From 712f27dac9d390f9729a8d153ee9e8e7699c5da1 Mon Sep 17 00:00:00 2001 From: Override-6 Date: Wed, 8 Nov 2023 21:41:05 +0100 Subject: [PATCH 04/18] fix production base path --- ci/build_react.msh | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ci/build_react.msh b/ci/build_react.msh index 203afa0..c893498 100755 --- a/ci/build_react.msh +++ b/ci/build_react.msh @@ -4,7 +4,11 @@ mkdir -p /outputs/public apt update && apt install jq -y npm install -npm run build -- --base=/IQBall/public --mode PROD + +val drone_branch = std::env("DRONE_BRANCH").unwrap() + +val base = "/IQBall/$drone_branch/public" +npm run build -- --base=$base --mode PROD // Read generated mappings from build val result = $(jq -r 'to_entries|map(.key + " " +.value.file)|.[]' dist/manifest.json) From a7ddad4c67093ae03a1c94d5fd157d41fce70a7b Mon Sep 17 00:00:00 2001 From: Override-6 Date: Wed, 8 Nov 2023 22:45:43 +0100 Subject: [PATCH 05/18] fix basket court --- front/assets/basketball_court.svg | 81 +++++++++++++------------ front/components/editor/BasketCourt.tsx | 46 +++++++------- front/style/basket_court.css | 12 +++- vite.config.ts | 4 ++ 4 files changed, 78 insertions(+), 65 deletions(-) diff --git a/front/assets/basketball_court.svg b/front/assets/basketball_court.svg index c5378b9..25b6f6c 100644 --- a/front/assets/basketball_court.svg +++ b/front/assets/basketball_court.svg @@ -1,39 +1,44 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/front/components/editor/BasketCourt.tsx b/front/components/editor/BasketCourt.tsx index 768d4a5..394f403 100644 --- a/front/components/editor/BasketCourt.tsx +++ b/front/components/editor/BasketCourt.tsx @@ -1,7 +1,8 @@ -import courtSvg from '../../assets/basketball_court.svg'; +import CourtSvg from '../../assets/basketball_court.svg'; import '../../style/basket_court.css'; import React, {MouseEvent, ReactElement, useRef, useState} from "react"; import Player from "./Player"; +import Draggable from "react-draggable"; const TEAM_MAX_PLAYER = 5; @@ -10,31 +11,28 @@ export function BasketCourt() { const divRef = useRef(null); return ( -
{ - if (e.target != divRef.current) - return - let bounds = divRef.current!.getBoundingClientRect(); - let playerCount = players.length; +
+ { + console.log(e.target) + let bounds = divRef.current!.getBoundingClientRect(); + let playerCount = players.length; - if (playerCount >= TEAM_MAX_PLAYER) { - return; - } + if (playerCount >= TEAM_MAX_PLAYER) { + return; + } - const player = ( - - ); - setPlayers([...players, player]) - }}> + const player = ( + + ); + setPlayers([...players, player]) + }}/> {players}
) diff --git a/front/style/basket_court.css b/front/style/basket_court.css index b1a3e10..2e60cfd 100644 --- a/front/style/basket_court.css +++ b/front/style/basket_court.css @@ -1,8 +1,14 @@ #court-container { - background-color: rebeccapurple; display: flex; - width: 1000px; - height: 500px; +} + +#court-svg { + user-select: none; + -webkit-user-drag: none; +} + +#court-svg * { + stroke: var(--selected-team-secondarycolor); } \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts index 34cb651..bb04351 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -2,6 +2,7 @@ import {defineConfig} from "vite"; import react from '@vitejs/plugin-react'; import fs from "fs"; import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'; +import svgr from "vite-plugin-svgr"; function resolve_entries(dirname: string): [string, string][] { @@ -39,5 +40,8 @@ export default defineConfig({ cssInjectedByJsPlugin({ relativeCSSInjection: true, }), + svgr({ + include: "**/*.svg" + }) ] }) From 2a5ece19a92dcdc173d2491e1841271f5d7df67e Mon Sep 17 00:00:00 2001 From: Override-6 Date: Thu, 9 Nov 2023 00:53:39 +0100 Subject: [PATCH 06/18] add a way to remove players from the court --- front/assets/icon/remove.svg | 5 ++ front/components/editor/BasketCourt.tsx | 31 ++++++++---- front/components/editor/Player.tsx | 43 ++++++++++++----- front/style/colors.css | 4 +- front/style/player.css | 63 +++++++++++++++++++++++-- 5 files changed, 119 insertions(+), 27 deletions(-) create mode 100644 front/assets/icon/remove.svg diff --git a/front/assets/icon/remove.svg b/front/assets/icon/remove.svg new file mode 100644 index 0000000..6886097 --- /dev/null +++ b/front/assets/icon/remove.svg @@ -0,0 +1,5 @@ + + + + diff --git a/front/components/editor/BasketCourt.tsx b/front/components/editor/BasketCourt.tsx index 394f403..c54224c 100644 --- a/front/components/editor/BasketCourt.tsx +++ b/front/components/editor/BasketCourt.tsx @@ -1,6 +1,6 @@ import CourtSvg from '../../assets/basketball_court.svg'; import '../../style/basket_court.css'; -import React, {MouseEvent, ReactElement, useRef, useState} from "react"; +import React, {MouseEvent, ReactElement, useEffect, useRef, useState} from "react"; import Player from "./Player"; import Draggable from "react-draggable"; @@ -15,23 +15,38 @@ export function BasketCourt() { { - console.log(e.target) - let bounds = divRef.current!.getBoundingClientRect(); - let playerCount = players.length; + const bounds = divRef.current!.getBoundingClientRect(); - if (playerCount >= TEAM_MAX_PLAYER) { + if (players.length >= TEAM_MAX_PLAYER) { return; } + // find a valid number for the player to place. + let playerIndex = players.findIndex((v, i) => + v.key !== i.toString() + ); + + if (playerIndex == -1) { + playerIndex = players.length; + } + const player = ( - { + setPlayers(players => { + // recompute the player's index as it may have been moved if + // previous players were removed and added. + const playerCurrentIndex = players.findIndex(p => p.key === playerIndex.toString()) + return players.toSpliced(playerCurrentIndex, 1) + }) + }} /> ); - setPlayers([...players, player]) + setPlayers(players => players.toSpliced(playerIndex, 0, player)) }}/> {players}
diff --git a/front/components/editor/Player.tsx b/front/components/editor/Player.tsx index c77f0b4..8d3f1ff 100644 --- a/front/components/editor/Player.tsx +++ b/front/components/editor/Player.tsx @@ -1,33 +1,50 @@ -import React, {useEffect, useRef} from "react"; +import React, {useRef} from "react"; import "../../style/player.css"; +import RemoveIcon from "../../assets/icon/remove.svg"; import Draggable, {DraggableBounds} from "react-draggable"; -export default function Player({id, x, y, bounds}: { +export interface PlayerOptions { id: number, x: number, y: number, - bounds: DraggableBounds -}) { + bounds: DraggableBounds, + onRemove: () => void +} - const ref = useRef(null); - useEffect(() => { - const playerRect = ref.current!.getBoundingClientRect(); - bounds.bottom! -= playerRect.height / 2; - bounds.right! -= playerRect.width / 2; - }, [ref]) +export default function Player({id, x, y, bounds, onRemove}: PlayerOptions) { + const ref = useRef(null); return ( + defaultPosition={{x: x, y: y}} + >
-

{id}

+ +
{ + if (e.key == "Delete") + onRemove() + }}> +
+ onRemove()}/> +
+
+

{id}

+
+
+
) diff --git a/front/style/colors.css b/front/style/colors.css index 68ccdaa..f3287cb 100644 --- a/front/style/colors.css +++ b/front/style/colors.css @@ -6,8 +6,8 @@ --background-color: #d2cdd3; - - --selected-team-primarycolor: #ffffff; --selected-team-secondarycolor: #000000; + + --selection-color: #3f7fc4 } \ No newline at end of file diff --git a/front/style/player.css b/front/style/player.css index 4450dec..8fbf487 100644 --- a/front/style/player.css +++ b/front/style/player.css @@ -1,10 +1,32 @@ +/** +as the .player div content is translated, +the real .player div position is not were the user can expect. +Disable pointer events to this div as it may overlap on other components +on the court. +*/ .player { + pointer-events: none; +} + +.player-content { + /*apply a translation to center the player piece when placed*/ + transform: translate(-50%, -75%); + + display: flex; + flex-direction: column; + align-content: center; + align-items: center; + outline: none; + +} + +.player-piece { font-family: monospace; + pointer-events: all; background-color: var(--selected-team-primarycolor); color: var(--selected-team-secondarycolor); - border-width: 2px; border-radius: 100px; border-style: solid; @@ -17,8 +39,41 @@ align-items: center; justify-content: center; - /*apply a translation to */ - transform: translate(-50%, -50%); - user-select: none; +} + +.player-selection-tab { + display: flex; + margin-bottom: 10%; + justify-content: center; + visibility: hidden; +} + +.player-selection-tab-remove { + pointer-events: all; + width: 25%; + height: 25%; +} + +.player-selection-tab-remove * { + stroke: red; + fill: white; +} + +.player-selection-tab-remove:hover * { + fill: #f1dbdb; + stroke: #ff331a; + cursor: pointer; +} + +.player:focus-within .player-selection-tab { + visibility: visible; +} + +.player:focus-within .player-piece { + color: var(--selection-color); +} + +.player:focus-within { + z-index: 1000; } \ No newline at end of file From 5439fc6c47e49d1c432c69010a1a7fddf88203c7 Mon Sep 17 00:00:00 2001 From: Override-6 Date: Fri, 10 Nov 2023 00:14:39 +0100 Subject: [PATCH 07/18] drag and drop players and opponents from racks --- front/components/Rack.tsx | 59 +++++++++++++++ front/components/editor/BasketCourt.tsx | 73 ++++++++----------- .../editor/{Player.tsx => CourtPlayer.tsx} | 13 ++-- front/components/editor/PlayerPiece.tsx | 11 +++ front/data/Player.ts | 22 ++++++ front/style/editor.css | 13 ++++ front/style/player.css | 2 +- front/views/Editor.tsx | 69 +++++++++++++++++- 8 files changed, 207 insertions(+), 55 deletions(-) create mode 100644 front/components/Rack.tsx rename front/components/editor/{Player.tsx => CourtPlayer.tsx} (80%) create mode 100644 front/components/editor/PlayerPiece.tsx create mode 100644 front/data/Player.ts diff --git a/front/components/Rack.tsx b/front/components/Rack.tsx new file mode 100644 index 0000000..ad8f354 --- /dev/null +++ b/front/components/Rack.tsx @@ -0,0 +1,59 @@ +import {Dispatch, ReactElement, RefObject, SetStateAction, useRef} from "react"; +import Draggable from "react-draggable"; + +export interface RackInput { + id: string, + objects: [ReactElement[], Dispatch>], + canDetach: (ref: RefObject) => boolean, + onElementDetached: (ref: RefObject, el: ReactElement) => void, +} + +interface RackItemInput { + item: ReactElement, + onTryDetach: (ref: RefObject, el: ReactElement) => void +} + +/** + * A container of draggable objects + * */ +export function Rack({id, objects, canDetach, onElementDetached}: RackInput) { + + const [rackObjects, setRackObjects] = objects + + return ( +
+ {rackObjects.map(element => ( + { + if (!canDetach(ref)) + return + + setRackObjects(objects => { + const index = objects.findIndex(o => o.key === element.key) + return objects.toSpliced(index, 1); + }) + + onElementDetached(ref, element) + }}/> + ))} +
+ ) +} + +function RackItem({item, onTryDetach}: RackItemInput) { + const divRef = useRef(null); + + return ( + onTryDetach(divRef, item)}> +
+ {item} +
+
+ ) +} \ No newline at end of file diff --git a/front/components/editor/BasketCourt.tsx b/front/components/editor/BasketCourt.tsx index c54224c..feb4637 100644 --- a/front/components/editor/BasketCourt.tsx +++ b/front/components/editor/BasketCourt.tsx @@ -1,54 +1,39 @@ import CourtSvg from '../../assets/basketball_court.svg'; import '../../style/basket_court.css'; -import React, {MouseEvent, ReactElement, useEffect, useRef, useState} from "react"; -import Player from "./Player"; -import Draggable from "react-draggable"; +import {MouseEvent, ReactElement, useEffect, useRef, useState} from "react"; +import CourtPlayer from "./CourtPlayer"; +import {Player} from "../../data/Player"; -const TEAM_MAX_PLAYER = 5; - -export function BasketCourt() { - const [players, setPlayers] = useState([]) +export function BasketCourt({players}: { players: Player[] }) { + const [courtPlayers, setCourtPlayers] = useState([]) const divRef = useRef(null); + useEffect(() => { + const bounds = divRef.current!.getBoundingClientRect(); + setCourtPlayers(players.map(player => { + return ( + { + // setCourtPlayers(players => { + // // recompute the player's index as it may have been moved if + // // previous players were removed and added. + // const playerCurrentIndex = players.findIndex(p => p.key === playerIndex.toString()) + // return players.toSpliced(playerCurrentIndex, 1) + // }) + }} + /> + ) + })) + }, [players, divRef]); + return (
- { - const bounds = divRef.current!.getBoundingClientRect(); - - if (players.length >= TEAM_MAX_PLAYER) { - return; - } - - // find a valid number for the player to place. - let playerIndex = players.findIndex((v, i) => - v.key !== i.toString() - ); - - if (playerIndex == -1) { - playerIndex = players.length; - } - - const player = ( - { - setPlayers(players => { - // recompute the player's index as it may have been moved if - // previous players were removed and added. - const playerCurrentIndex = players.findIndex(p => p.key === playerIndex.toString()) - return players.toSpliced(playerCurrentIndex, 1) - }) - }} - /> - ); - setPlayers(players => players.toSpliced(playerIndex, 0, player)) - }}/> - {players} + + {courtPlayers}
) } diff --git a/front/components/editor/Player.tsx b/front/components/editor/CourtPlayer.tsx similarity index 80% rename from front/components/editor/Player.tsx rename to front/components/editor/CourtPlayer.tsx index 8d3f1ff..701be4f 100644 --- a/front/components/editor/Player.tsx +++ b/front/components/editor/CourtPlayer.tsx @@ -2,16 +2,20 @@ import React, {useRef} from "react"; import "../../style/player.css"; import RemoveIcon from "../../assets/icon/remove.svg"; import Draggable, {DraggableBounds} from "react-draggable"; +import {PlayerPiece} from "./PlayerPiece"; export interface PlayerOptions { - id: number, + pos: string, x: number, y: number, bounds: DraggableBounds, onRemove: () => void } -export default function Player({id, x, y, bounds, onRemove}: PlayerOptions) { +/** + * A player that is placed on the court, which can be selected, and moved in the associated bounds + * */ +export default function CourtPlayer({pos, x, y, bounds, onRemove}: PlayerOptions) { const ref = useRef(null); return ( @@ -38,10 +42,7 @@ export default function Player({id, x, y, bounds, onRemove}: PlayerOptions) { className="player-selection-tab-remove" onClick={() => onRemove()}/>
-
-

{id}

-
+
diff --git a/front/components/editor/PlayerPiece.tsx b/front/components/editor/PlayerPiece.tsx new file mode 100644 index 0000000..17accac --- /dev/null +++ b/front/components/editor/PlayerPiece.tsx @@ -0,0 +1,11 @@ +import React from "react"; +import '../../style/player.css' + + +export function PlayerPiece({text}: { text: string }) { + return ( +
+

{text}

+
+ ) +} \ No newline at end of file diff --git a/front/data/Player.ts b/front/data/Player.ts new file mode 100644 index 0000000..da66ae3 --- /dev/null +++ b/front/data/Player.ts @@ -0,0 +1,22 @@ +export interface Player { + /** + * unique identifier of the player. + * This identifier must be unique to the associated court. + */ + id: number, + + /** + * player's position + * */ + position: string, + + /** + * Percentage of the player's position to the bottom (0 means top, 1 means bottom, 0.5 means middle) + */ + bottom_percentage: number + + /** + * Percentage of the player's position to the right (0 means left, 1 means right, 0.5 means middle) + */ + right_percentage: number, +} \ No newline at end of file diff --git a/front/style/editor.css b/front/style/editor.css index 62c4e9d..15bf69f 100644 --- a/front/style/editor.css +++ b/front/style/editor.css @@ -18,10 +18,23 @@ align-items: stretch; } +#racks { + display: flex; + justify-content: space-between; +} + .title_input { width: 25ch; } +#edit-div { + height: 100%; +} + +#team-rack .player-piece , #opponent-rack .player-piece { + margin-left: 5px; +} + #court-div { background-color: var(--background-color); height: 100%; diff --git a/front/style/player.css b/front/style/player.css index 8fbf487..ebd0462 100644 --- a/front/style/player.css +++ b/front/style/player.css @@ -10,7 +10,7 @@ on the court. .player-content { /*apply a translation to center the player piece when placed*/ - transform: translate(-50%, -75%); + transform: translate(-29%, -46%); display: flex; flex-direction: column; diff --git a/front/views/Editor.tsx b/front/views/Editor.tsx index 471a94d..89858f0 100644 --- a/front/views/Editor.tsx +++ b/front/views/Editor.tsx @@ -1,17 +1,64 @@ -import React, {CSSProperties, useState} from "react"; +import {CSSProperties, ReactElement, RefObject, useRef, useState} from "react"; import "../style/editor.css"; import TitleInput from "../components/TitleInput"; import {API} from "../Constants"; import {BasketCourt} from "../components/editor/BasketCourt"; +import {Rack} from "../components/Rack"; +import {PlayerPiece} from "../components/editor/PlayerPiece"; +import {Player} from "../data/Player"; + const ERROR_STYLE: CSSProperties = { borderColor: "red" } export default function Editor({id, name}: { id: number, name: string }) { - const [style, setStyle] = useState({}); + const positions = ["PG", "SG", "SF", "PF", "C"] + const [team, setTeams] = useState( + positions.map(pos => ) + ) + const [opponents, setOpponents] = useState( + positions.map(pos => ) + ) + + const [players, setPlayers] = useState([]); + const courtDivContentRef = useRef(null); + + const canDetach = (ref: RefObject) => { + const refBounds = ref.current!.getBoundingClientRect(); + const courtBounds = courtDivContentRef.current!.getBoundingClientRect(); + + // check if refBounds overlaps courtBounds + return !( + refBounds.top > courtBounds.bottom || + refBounds.right < courtBounds.left || + refBounds.bottom < courtBounds.top || + refBounds.left > courtBounds.right + ); + } + + const onElementDetach = (ref: RefObject, element: ReactElement) => { + const refBounds = ref.current!.getBoundingClientRect(); + const courtBounds = courtDivContentRef.current!.getBoundingClientRect(); + + const relativeXPixels = refBounds.x - courtBounds.x; + const relativeYPixels = refBounds.y - courtBounds.y; + + const xPercent = relativeXPixels / courtBounds.width; + const yPercent = relativeYPixels / courtBounds.height; + + setPlayers(players => { + return [...players, { + id: players.length, + position: element.props.text, + right_percentage: xPercent, + bottom_percentage: yPercent + }] + }) + } + return (
@@ -36,8 +83,22 @@ export default function Editor({id, name}: { id: number, name: string }) { }}/>
RIGHT
-
- +
+
+ + +
+
+
+ +
+
) From 7cf719e7cbba46cdc821beb50f14d91408da6f70 Mon Sep 17 00:00:00 2001 From: "maxime.batista" Date: Fri, 10 Nov 2023 08:58:08 +0100 Subject: [PATCH 08/18] add possibility to place back players to their racks --- front/components/editor/BasketCourt.tsx | 9 ++----- front/data/Player.ts | 5 ++++ front/style/editor.css | 2 +- front/views/Editor.tsx | 32 ++++++++++++++++++++----- 4 files changed, 34 insertions(+), 14 deletions(-) diff --git a/front/components/editor/BasketCourt.tsx b/front/components/editor/BasketCourt.tsx index feb4637..f76032b 100644 --- a/front/components/editor/BasketCourt.tsx +++ b/front/components/editor/BasketCourt.tsx @@ -4,7 +4,7 @@ import {MouseEvent, ReactElement, useEffect, useRef, useState} from "react"; import CourtPlayer from "./CourtPlayer"; import {Player} from "../../data/Player"; -export function BasketCourt({players}: { players: Player[] }) { +export function BasketCourt({players, onPlayerRemove}: { players: Player[], onPlayerRemove: (Player) => void }) { const [courtPlayers, setCourtPlayers] = useState([]) const divRef = useRef(null); @@ -18,12 +18,7 @@ export function BasketCourt({players}: { players: Player[] }) { y={(bounds.height * player.bottom_percentage)} bounds={{bottom: bounds.height, top: 0, left: 0, right: bounds.width}} onRemove={() => { - // setCourtPlayers(players => { - // // recompute the player's index as it may have been moved if - // // previous players were removed and added. - // const playerCurrentIndex = players.findIndex(p => p.key === playerIndex.toString()) - // return players.toSpliced(playerCurrentIndex, 1) - // }) + onPlayerRemove(player) }} /> ) diff --git a/front/data/Player.ts b/front/data/Player.ts index da66ae3..a27e643 100644 --- a/front/data/Player.ts +++ b/front/data/Player.ts @@ -5,6 +5,11 @@ export interface Player { */ id: number, + /** + * the player's team + * */ + team: "allies" | "opponents", + /** * player's position * */ diff --git a/front/style/editor.css b/front/style/editor.css index 15bf69f..c13ac99 100644 --- a/front/style/editor.css +++ b/front/style/editor.css @@ -31,7 +31,7 @@ height: 100%; } -#team-rack .player-piece , #opponent-rack .player-piece { +#allies-rack .player-piece , #opponent-rack .player-piece { margin-left: 5px; } diff --git a/front/views/Editor.tsx b/front/views/Editor.tsx index 89858f0..60cc8fd 100644 --- a/front/views/Editor.tsx +++ b/front/views/Editor.tsx @@ -16,11 +16,11 @@ export default function Editor({id, name}: { id: number, name: string }) { const [style, setStyle] = useState({}); const positions = ["PG", "SG", "SF", "PF", "C"] - const [team, setTeams] = useState( - positions.map(pos => ) + const [allies, setAllies] = useState( + positions.map(pos => ) ) const [opponents, setOpponents] = useState( - positions.map(pos => ) + positions.map(pos => ) ) const [players, setPlayers] = useState([]); @@ -52,6 +52,7 @@ export default function Editor({id, name}: { id: number, name: string }) { setPlayers(players => { return [...players, { id: players.length, + team: element.props.team, position: element.props.text, right_percentage: xPercent, bottom_percentage: yPercent @@ -85,8 +86,8 @@ export default function Editor({id, name}: { id: number, name: string }) {
-
- + { + setPlayers(players => { + const idx = players.indexOf(player) + return players.toSpliced(idx, 1) + }) + const piece = + switch (player.team) { + case "opponents": + setOpponents(opponents => ( + [...opponents, piece] + )) + break + case "allies": + setAllies(allies => ( + [...allies, piece] + )) + } + }}/>
From 0430c76c51e2ca4918c6655bd7c81b3202c0e870 Mon Sep 17 00:00:00 2001 From: "maxime.batista" Date: Fri, 10 Nov 2023 09:38:59 +0100 Subject: [PATCH 09/18] set opponents colors to orange --- front/components/editor/BasketCourt.tsx | 1 + front/components/editor/CourtPlayer.tsx | 5 +++-- front/components/editor/PlayerPiece.tsx | 4 ++-- front/style/editor.css | 4 ++++ front/views/Editor.tsx | 1 + 5 files changed, 11 insertions(+), 4 deletions(-) diff --git a/front/components/editor/BasketCourt.tsx b/front/components/editor/BasketCourt.tsx index f76032b..d4b83d7 100644 --- a/front/components/editor/BasketCourt.tsx +++ b/front/components/editor/BasketCourt.tsx @@ -14,6 +14,7 @@ export function BasketCourt({players, onPlayerRemove}: { players: Player[], onPl return ( (null); return ( @@ -42,7 +43,7 @@ export default function CourtPlayer({pos, x, y, bounds, onRemove}: PlayerOptions className="player-selection-tab-remove" onClick={() => onRemove()}/>
- + diff --git a/front/components/editor/PlayerPiece.tsx b/front/components/editor/PlayerPiece.tsx index 17accac..b5cc41f 100644 --- a/front/components/editor/PlayerPiece.tsx +++ b/front/components/editor/PlayerPiece.tsx @@ -2,9 +2,9 @@ import React from "react"; import '../../style/player.css' -export function PlayerPiece({text}: { text: string }) { +export function PlayerPiece({team, text}: { team: string, text: string }) { return ( -
+

{text}

) diff --git a/front/style/editor.css b/front/style/editor.css index c13ac99..4791022 100644 --- a/front/style/editor.css +++ b/front/style/editor.css @@ -35,6 +35,10 @@ margin-left: 5px; } +.player-piece.opponents { + background-color: #f59264; +} + #court-div { background-color: var(--background-color); height: 100%; diff --git a/front/views/Editor.tsx b/front/views/Editor.tsx index 60cc8fd..e42e063 100644 --- a/front/views/Editor.tsx +++ b/front/views/Editor.tsx @@ -123,3 +123,4 @@ export default function Editor({id, name}: { id: number, name: string }) {
) } + From 29356f8554fc7ace388375de3db8eef05f8b0fcc Mon Sep 17 00:00:00 2001 From: "maxime.batista" Date: Wed, 15 Nov 2023 14:28:30 +0100 Subject: [PATCH 10/18] change player rolenames to player nums --- front/views/Editor.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/front/views/Editor.tsx b/front/views/Editor.tsx index e42e063..d7dccaa 100644 --- a/front/views/Editor.tsx +++ b/front/views/Editor.tsx @@ -15,7 +15,7 @@ const ERROR_STYLE: CSSProperties = { export default function Editor({id, name}: { id: number, name: string }) { const [style, setStyle] = useState({}); - const positions = ["PG", "SG", "SF", "PF", "C"] + const positions = ["1", "2", "3", "4", "5"] const [allies, setAllies] = useState( positions.map(pos => ) ) From ef80aa3192fb6083e3c2c7899459b29e013caa04 Mon Sep 17 00:00:00 2001 From: Override-6 Date: Thu, 16 Nov 2023 21:18:20 +0100 Subject: [PATCH 11/18] update court svg --- front/assets/basketball_court.svg | 102 ++++++++++++++---------- front/components/editor/BasketCourt.tsx | 4 +- front/style/basket_court.css | 5 ++ front/style/editor.css | 4 + front/views/Editor.tsx | 2 +- 5 files changed, 71 insertions(+), 46 deletions(-) diff --git a/front/assets/basketball_court.svg b/front/assets/basketball_court.svg index 25b6f6c..e0df003 100644 --- a/front/assets/basketball_court.svg +++ b/front/assets/basketball_court.svg @@ -1,44 +1,62 @@ - - - - - - - - + + + + + + + + + + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/front/components/editor/BasketCourt.tsx b/front/components/editor/BasketCourt.tsx index d4b83d7..0819633 100644 --- a/front/components/editor/BasketCourt.tsx +++ b/front/components/editor/BasketCourt.tsx @@ -18,9 +18,7 @@ export function BasketCourt({players, onPlayerRemove}: { players: Player[], onPl x={(bounds.width * player.right_percentage)} y={(bounds.height * player.bottom_percentage)} bounds={{bottom: bounds.height, top: 0, left: 0, right: bounds.width}} - onRemove={() => { - onPlayerRemove(player) - }} + onRemove={() => onPlayerRemove(player)} /> ) })) diff --git a/front/style/basket_court.css b/front/style/basket_court.css index 2e60cfd..920512b 100644 --- a/front/style/basket_court.css +++ b/front/style/basket_court.css @@ -2,13 +2,18 @@ #court-container { display: flex; + + background-color: var(--main-color); } #court-svg { + margin: 5%; user-select: none; -webkit-user-drag: none; } + + #court-svg * { stroke: var(--selected-team-secondarycolor); } \ No newline at end of file diff --git a/front/style/editor.css b/front/style/editor.css index 4791022..e2d38c9 100644 --- a/front/style/editor.css +++ b/front/style/editor.css @@ -46,5 +46,9 @@ display: flex; align-items: center; justify-content: center; + align-content: center; } +#court-div-bounds { + width: 60%; +} diff --git a/front/views/Editor.tsx b/front/views/Editor.tsx index d7dccaa..8636cad 100644 --- a/front/views/Editor.tsx +++ b/front/views/Editor.tsx @@ -96,7 +96,7 @@ export default function Editor({id, name}: { id: number, name: string }) { onElementDetached={onElementDetach}/>
-
+
{ From c53a1b024c6b026e2aec5c1ccfe3e6aa66ad477f Mon Sep 17 00:00:00 2001 From: "maxime.batista" Date: Fri, 17 Nov 2023 14:34:38 +0100 Subject: [PATCH 12/18] apply suggestions --- front/components/Rack.tsx | 39 ++++++++-------- front/components/editor/BasketCourt.tsx | 38 ++++++--------- front/components/editor/CourtPlayer.tsx | 30 ++++++------ front/components/editor/PlayerPiece.tsx | 3 +- front/data/Player.ts | 10 ++-- front/data/Team.tsx | 4 ++ front/style/basket_court.css | 1 + front/style/editor.css | 5 ++ front/style/player.css | 10 ++-- front/views/Editor.tsx | 62 ++++++++++++++++--------- tsconfig.json | 2 +- vite.config.ts | 2 +- 12 files changed, 116 insertions(+), 90 deletions(-) create mode 100644 front/data/Team.tsx diff --git a/front/components/Rack.tsx b/front/components/Rack.tsx index ad8f354..09681e7 100644 --- a/front/components/Rack.tsx +++ b/front/components/Rack.tsx @@ -1,40 +1,39 @@ -import {Dispatch, ReactElement, RefObject, SetStateAction, useRef} from "react"; +import {ReactElement, useRef} from "react"; import Draggable from "react-draggable"; -export interface RackInput { +export interface RackProps { id: string, - objects: [ReactElement[], Dispatch>], - canDetach: (ref: RefObject) => boolean, - onElementDetached: (ref: RefObject, el: ReactElement) => void, + objects: E[], + onChange: (objects: E[]) => void, + canDetach: (ref: HTMLDivElement) => boolean, + onElementDetached: (ref: HTMLDivElement, el: E) => void, + render: (e: E) => ReactElement, } -interface RackItemInput { - item: ReactElement, - onTryDetach: (ref: RefObject, el: ReactElement) => void +interface RackItemProps { + item: E, + onTryDetach: (ref: HTMLDivElement, el: E) => void, + render: (e: E) => ReactElement, } /** * A container of draggable objects * */ -export function Rack({id, objects, canDetach, onElementDetached}: RackInput) { - - const [rackObjects, setRackObjects] = objects - +export function Rack({id, objects, onChange, canDetach, onElementDetached, render}: RackProps) { return (
- {rackObjects.map(element => ( + {objects.map(element => ( { if (!canDetach(ref)) return - setRackObjects(objects => { - const index = objects.findIndex(o => o.key === element.key) - return objects.toSpliced(index, 1); - }) + const index = objects.findIndex(o => o.key === element.key) + onChange(objects.toSpliced(index, 1)) onElementDetached(ref, element) }}/> @@ -43,16 +42,16 @@ export function Rack({id, objects, canDetach, onElementDetached}: RackInput) { ) } -function RackItem({item, onTryDetach}: RackItemInput) { +function RackItem({item, onTryDetach, render}: RackItemProps) { const divRef = useRef(null); return ( onTryDetach(divRef, item)}> + onStop={() => onTryDetach(divRef.current!, item)}>
- {item} + {render(item)}
) diff --git a/front/components/editor/BasketCourt.tsx b/front/components/editor/BasketCourt.tsx index 0819633..b0b0eda 100644 --- a/front/components/editor/BasketCourt.tsx +++ b/front/components/editor/BasketCourt.tsx @@ -1,35 +1,27 @@ -import CourtSvg from '../../assets/basketball_court.svg'; +import CourtSvg from '../../assets/basketball_court.svg?react'; import '../../style/basket_court.css'; -import {MouseEvent, ReactElement, useEffect, useRef, useState} from "react"; +import {useRef} from "react"; import CourtPlayer from "./CourtPlayer"; import {Player} from "../../data/Player"; -export function BasketCourt({players, onPlayerRemove}: { players: Player[], onPlayerRemove: (Player) => void }) { - const [courtPlayers, setCourtPlayers] = useState([]) - const divRef = useRef(null); +export interface BasketCourtProps { + players: Player[], + onPlayerRemove: (p: Player) => void, +} - useEffect(() => { - const bounds = divRef.current!.getBoundingClientRect(); - setCourtPlayers(players.map(player => { - return ( - onPlayerRemove(player)} - /> - ) - })) - }, [players, divRef]); +export function BasketCourt({players, onPlayerRemove}: BasketCourtProps) { + const divRef = useRef(null); return ( -
+
- {courtPlayers} + {players.map(player => { + return onPlayerRemove(player)} + /> + })}
) } - diff --git a/front/components/editor/CourtPlayer.tsx b/front/components/editor/CourtPlayer.tsx index 08323f4..b6b64de 100644 --- a/front/components/editor/CourtPlayer.tsx +++ b/front/components/editor/CourtPlayer.tsx @@ -1,35 +1,37 @@ -import React, {useRef} from "react"; +import {useRef} from "react"; import "../../style/player.css"; -import RemoveIcon from "../../assets/icon/remove.svg"; -import Draggable, {DraggableBounds} from "react-draggable"; +import RemoveIcon from "../../assets/icon/remove.svg?react"; +import Draggable from "react-draggable"; import {PlayerPiece} from "./PlayerPiece"; +import {Player} from "../../data/Player"; -export interface PlayerOptions { - pos: string, - team: string, - x: number, - y: number, - bounds: DraggableBounds, +export interface PlayerProps { + player: Player, onRemove: () => void } /** * A player that is placed on the court, which can be selected, and moved in the associated bounds * */ -export default function CourtPlayer({pos, team, x, y, bounds, onRemove}: PlayerOptions) { +export default function CourtPlayer({player, onRemove}: PlayerProps) { const ref = useRef(null); + + const x = player.rightRatio; + const y = player.bottomRatio; + return (
onRemove()}/> + onClick={onRemove}/>
- +
diff --git a/front/components/editor/PlayerPiece.tsx b/front/components/editor/PlayerPiece.tsx index b5cc41f..83e7dfc 100644 --- a/front/components/editor/PlayerPiece.tsx +++ b/front/components/editor/PlayerPiece.tsx @@ -1,8 +1,9 @@ import React from "react"; import '../../style/player.css' +import {Team} from "../../data/Team"; -export function PlayerPiece({team, text}: { team: string, text: string }) { +export function PlayerPiece({team, text}: { team: Team, text: string }) { return (

{text}

diff --git a/front/data/Player.ts b/front/data/Player.ts index a27e643..af88c1c 100644 --- a/front/data/Player.ts +++ b/front/data/Player.ts @@ -1,3 +1,5 @@ +import {Team} from "./Team"; + export interface Player { /** * unique identifier of the player. @@ -8,20 +10,20 @@ export interface Player { /** * the player's team * */ - team: "allies" | "opponents", + team: Team, /** * player's position * */ - position: string, + role: string, /** * Percentage of the player's position to the bottom (0 means top, 1 means bottom, 0.5 means middle) */ - bottom_percentage: number + bottomRatio: number /** * Percentage of the player's position to the right (0 means left, 1 means right, 0.5 means middle) */ - right_percentage: number, + rightRatio: number, } \ No newline at end of file diff --git a/front/data/Team.tsx b/front/data/Team.tsx new file mode 100644 index 0000000..ea4c384 --- /dev/null +++ b/front/data/Team.tsx @@ -0,0 +1,4 @@ +export enum Team { + Allies = "allies", + Opponents = "opponents" +} \ No newline at end of file diff --git a/front/style/basket_court.css b/front/style/basket_court.css index 920512b..a5bc688 100644 --- a/front/style/basket_court.css +++ b/front/style/basket_court.css @@ -3,6 +3,7 @@ #court-container { display: flex; + background-color: var(--main-color); } diff --git a/front/style/editor.css b/front/style/editor.css index e2d38c9..3aad26c 100644 --- a/front/style/editor.css +++ b/front/style/editor.css @@ -52,3 +52,8 @@ #court-div-bounds { width: 60%; } + + +.react-draggable { + z-index: 2; +} \ No newline at end of file diff --git a/front/style/player.css b/front/style/player.css index ebd0462..264b479 100644 --- a/front/style/player.css +++ b/front/style/player.css @@ -9,15 +9,11 @@ on the court. } .player-content { - /*apply a translation to center the player piece when placed*/ - transform: translate(-29%, -46%); - display: flex; flex-direction: column; align-content: center; align-items: center; outline: none; - } .player-piece { @@ -44,14 +40,18 @@ on the court. .player-selection-tab { display: flex; + + position: absolute; margin-bottom: 10%; justify-content: center; visibility: hidden; + + width: 100%; + transform: translateY(-20px); } .player-selection-tab-remove { pointer-events: all; - width: 25%; height: 25%; } diff --git a/front/views/Editor.tsx b/front/views/Editor.tsx index 8636cad..dba6dc2 100644 --- a/front/views/Editor.tsx +++ b/front/views/Editor.tsx @@ -7,27 +7,36 @@ import {BasketCourt} from "../components/editor/BasketCourt"; import {Rack} from "../components/Rack"; import {PlayerPiece} from "../components/editor/PlayerPiece"; import {Player} from "../data/Player"; +import {Team} from "../data/Team"; const ERROR_STYLE: CSSProperties = { borderColor: "red" } +/** + * information about a player that is into a rack + */ +interface RackedPlayer { + team: Team, + key: string, +} + export default function Editor({id, name}: { id: number, name: string }) { const [style, setStyle] = useState({}); const positions = ["1", "2", "3", "4", "5"] const [allies, setAllies] = useState( - positions.map(pos => ) + positions.map(key => ({team: Team.Allies, key})) ) const [opponents, setOpponents] = useState( - positions.map(pos => ) + positions.map(key => ({team: Team.Opponents, key})) ) const [players, setPlayers] = useState([]); const courtDivContentRef = useRef(null); - const canDetach = (ref: RefObject) => { - const refBounds = ref.current!.getBoundingClientRect(); + const canDetach = (ref: HTMLDivElement) => { + const refBounds = ref.getBoundingClientRect(); const courtBounds = courtDivContentRef.current!.getBoundingClientRect(); // check if refBounds overlaps courtBounds @@ -39,23 +48,23 @@ export default function Editor({id, name}: { id: number, name: string }) { ); } - const onElementDetach = (ref: RefObject, element: ReactElement) => { - const refBounds = ref.current!.getBoundingClientRect(); + const onPieceDetach = (ref: HTMLDivElement, element: RackedPlayer) => { + const refBounds = ref.getBoundingClientRect(); const courtBounds = courtDivContentRef.current!.getBoundingClientRect(); const relativeXPixels = refBounds.x - courtBounds.x; const relativeYPixels = refBounds.y - courtBounds.y; - const xPercent = relativeXPixels / courtBounds.width; - const yPercent = relativeYPixels / courtBounds.height; + const xRatio = relativeXPixels / courtBounds.width; + const yRatio = relativeYPixels / courtBounds.height; setPlayers(players => { return [...players, { id: players.length, - team: element.props.team, - position: element.props.text, - right_percentage: xPercent, - bottom_percentage: yPercent + team: element.team, + role: element.key, + rightRatio: xRatio, + bottomRatio: yRatio }] }) } @@ -87,13 +96,17 @@ export default function Editor({id, name}: { id: number, name: string }) {
+ onElementDetached={onPieceDetach} + render={({team, key}) => }/> + onElementDetached={onPieceDetach} + render={({team, key}) => }/>
@@ -104,16 +117,23 @@ export default function Editor({id, name}: { id: number, name: string }) { const idx = players.indexOf(player) return players.toSpliced(idx, 1) }) - const piece = switch (player.team) { - case "opponents": + case Team.Opponents: setOpponents(opponents => ( - [...opponents, piece] + [...opponents, { + team: player.team, + pos: player.role, + key: player.role + }] )) break - case "allies": + case Team.Allies: setAllies(allies => ( - [...allies, piece] + [...allies, { + team: player.team, + pos: player.role, + key: player.role + }] )) } }}/> diff --git a/tsconfig.json b/tsconfig.json index 9da1fb5..d01f3cc 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,7 +6,7 @@ "dom.iterable", "esnext" ], - "types": ["vite/client"], + "types": ["vite/client", "vite-plugin-svgr/client"], "allowJs": true, "skipLibCheck": true, "esModuleInterop": true, diff --git a/vite.config.ts b/vite.config.ts index bb04351..4ff1dc5 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -41,7 +41,7 @@ export default defineConfig({ relativeCSSInjection: true, }), svgr({ - include: "**/*.svg" + include: "**/*.svg?react" }) ] }) From a18014d4c37affbe38d844a8cc63e897ce63da0a Mon Sep 17 00:00:00 2001 From: "maxime.batista" Date: Mon, 20 Nov 2023 08:25:40 +0100 Subject: [PATCH 13/18] apply suggestions --- front/components/editor/BasketCourt.tsx | 4 +--- front/components/editor/CourtPlayer.tsx | 6 +----- front/views/Editor.tsx | 2 +- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/front/components/editor/BasketCourt.tsx b/front/components/editor/BasketCourt.tsx index b0b0eda..7e839a8 100644 --- a/front/components/editor/BasketCourt.tsx +++ b/front/components/editor/BasketCourt.tsx @@ -10,10 +10,8 @@ export interface BasketCourtProps { } export function BasketCourt({players, onPlayerRemove}: BasketCourtProps) { - const divRef = useRef(null); - return ( -
+
{players.map(player => { return (null); - const x = player.rightRatio; const y = player.bottomRatio; return ( -
Date: Sun, 19 Nov 2023 22:18:24 +0100 Subject: [PATCH 14/18] add and configure tsc, phpstan, prettier and php-cs-fixer, format and fix reported code errors --- .gitignore | 2 + .php-cs-fixer.php | 16 ++ .prettierrc | 7 + Documentation/conception.puml | 12 -- Documentation/models.puml | 2 +- composer.json | 5 +- config.php | 3 +- format.sh | 9 ++ front/Constants.ts | 2 +- front/ViewRenderer.tsx | 16 +- front/components/Rack.tsx | 84 +++++----- front/components/TitleInput.tsx | 42 ++--- front/components/editor/BasketCourt.tsx | 34 ++-- front/components/editor/CourtPlayer.tsx | 66 ++++---- front/components/editor/PlayerPiece.tsx | 11 +- front/data/Player.ts | 12 +- front/data/Team.tsx | 4 +- front/style/basket_court.css | 7 +- front/style/colors.css | 6 +- front/style/editor.css | 7 +- front/style/player.css | 2 +- front/style/title_input.css | 1 - front/views/DisplayResults.tsx | 16 +- front/views/Editor.tsx | 172 +++++++++++---------- front/views/SampleForm.tsx | 11 +- package.json | 8 +- phpstan.neon | 15 ++ profiles/dev-config-profile.php | 6 +- profiles/prod-config-profile.php | 2 +- public/api/index.php | 4 +- public/index.php | 9 +- public/utils.php | 9 +- sql/database.php | 3 - src/Connexion.php | 22 +-- src/Controller/Api/APITacticController.php | 6 +- src/Controller/Control.php | 21 +-- src/Controller/EditorController.php | 3 +- src/Controller/ErrorController.php | 16 +- src/Controller/SampleFormController.php | 18 ++- src/Data/Account.php | 14 +- src/Data/AccountUser.php | 8 +- src/Data/Color.php | 4 +- src/Data/Member.php | 2 +- src/Data/MemberRole.php | 3 +- src/Data/TacticInfo.php | 7 +- src/Data/Team.php | 11 +- src/Data/User.php | 3 +- src/Gateway/FormResultGateway.php | 12 +- src/Gateway/TacticInfoGateway.php | 8 +- src/Http/HttpCodes.php | 2 +- src/Http/HttpRequest.php | 33 +++- src/Http/HttpResponse.php | 3 +- src/Http/JsonHttpResponse.php | 3 +- src/Http/ViewHttpResponse.php | 14 +- src/Model/TacticModel.php | 5 +- src/Validation/ComposedValidator.php | 3 +- src/Validation/FieldValidationFail.php | 8 +- src/Validation/FunctionValidator.php | 6 +- src/Validation/SimpleFunctionValidator.php | 9 +- src/Validation/Validation.php | 5 +- src/Validation/ValidationFail.php | 11 +- src/Validation/Validator.php | 7 +- src/Validation/Validators.php | 5 +- src/react-display.php | 6 +- verify.sh | 9 ++ 65 files changed, 507 insertions(+), 385 deletions(-) create mode 100644 .php-cs-fixer.php create mode 100644 .prettierrc delete mode 100644 Documentation/conception.puml create mode 100755 format.sh create mode 100644 phpstan.neon create mode 100755 verify.sh diff --git a/.gitignore b/.gitignore index 9124809..1c4404c 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,5 @@ package-lock.json npm-debug.log* yarn-debug.log* yarn-error.log* + +.php-cs-fixer.cache \ No newline at end of file diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php new file mode 100644 index 0000000..265de93 --- /dev/null +++ b/.php-cs-fixer.php @@ -0,0 +1,16 @@ +in(__DIR__); + +return (new PhpCsFixer\Config()) + ->setRules([ + '@PER-CS' => true, + '@PHP82Migration' => 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 new file mode 100644 index 0000000..7db0434 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,7 @@ +{ + "bracketSameLine": true, + "trailingComma": "all", + "printWidth": 80, + "tabWidth": 4, + "semi": false +} \ No newline at end of file diff --git a/Documentation/conception.puml b/Documentation/conception.puml deleted file mode 100644 index 06ae256..0000000 --- a/Documentation/conception.puml +++ /dev/null @@ -1,12 +0,0 @@ -@startuml - -class Connexion - -class Modele - -class Account - -class AccountGateway - - -@enduml diff --git a/Documentation/models.puml b/Documentation/models.puml index 0ad5135..1f4877a 100755 --- a/Documentation/models.puml +++ b/Documentation/models.puml @@ -50,7 +50,6 @@ enum MemberRole { class Team { - name: String - picture: Url - - members: array + getName(): String + getPicture(): Url @@ -61,6 +60,7 @@ class Team { Team --> "- mainColor" Color Team --> "- secondaryColor" Color +Team --> "- members *" Member class Color { - value: int diff --git a/composer.json b/composer.json index e5b80e0..a3c0e4b 100644 --- a/composer.json +++ b/composer.json @@ -11,5 +11,8 @@ "ext-pdo_sqlite": "*", "twig/twig":"^2.0", "phpstan/phpstan": "*" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.38" } -} \ No newline at end of file +} diff --git a/config.php b/config.php index 592ee38..6e510c8 100644 --- a/config.php +++ b/config.php @@ -5,7 +5,7 @@ // Please do not touch. require /*PROFILE_FILE*/ "profiles/dev-config-profile.php"; -CONST SUPPORTS_FAST_REFRESH = _SUPPORTS_FAST_REFRESH; +const SUPPORTS_FAST_REFRESH = _SUPPORTS_FAST_REFRESH; /** * Maps the given relative source uri (relative to the `/front` folder) to its actual location depending on imported profile. @@ -20,4 +20,3 @@ global $_data_source_name; $data_source_name = $_data_source_name; const DATABASE_USER = _DATABASE_USER; const DATABASE_PASSWORD = _DATABASE_PASSWORD; - diff --git a/format.sh b/format.sh new file mode 100755 index 0000000..a9ff1c2 --- /dev/null +++ b/format.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +## verify php and typescript types + +echo "formatting php typechecking" +vendor/bin/php-cs-fixer fix + +echo "formatting typescript typechecking" +npm run format diff --git a/front/Constants.ts b/front/Constants.ts index aaaaa43..76b37c2 100644 --- a/front/Constants.ts +++ b/front/Constants.ts @@ -1,4 +1,4 @@ /** * This constant defines the API endpoint. */ -export const API = import.meta.env.VITE_API_ENDPOINT; \ No newline at end of file +export const API = import.meta.env.VITE_API_ENDPOINT diff --git a/front/ViewRenderer.tsx b/front/ViewRenderer.tsx index ffaf886..57f2e34 100644 --- a/front/ViewRenderer.tsx +++ b/front/ViewRenderer.tsx @@ -1,5 +1,5 @@ -import ReactDOM from "react-dom/client"; -import React, {FunctionComponent} from "react"; +import ReactDOM from "react-dom/client" +import React, { FunctionComponent } from "react" /** * Dynamically renders a React component, with given arguments @@ -8,12 +8,12 @@ import React, {FunctionComponent} from "react"; */ export function renderView(Component: FunctionComponent, args: {}) { const root = ReactDOM.createRoot( - document.getElementById('root') as HTMLElement - ); + document.getElementById("root") as HTMLElement, + ) root.render( - - - ); -} \ No newline at end of file + + , + ) +} diff --git a/front/components/Rack.tsx b/front/components/Rack.tsx index 09681e7..2a7511f 100644 --- a/front/components/Rack.tsx +++ b/front/components/Rack.tsx @@ -1,58 +1,72 @@ -import {ReactElement, useRef} from "react"; -import Draggable from "react-draggable"; +import { ReactElement, useRef } from "react" +import Draggable from "react-draggable" -export interface RackProps { - id: string, - objects: E[], - onChange: (objects: E[]) => void, - canDetach: (ref: HTMLDivElement) => boolean, - onElementDetached: (ref: HTMLDivElement, el: E) => void, - render: (e: E) => ReactElement, +export interface RackProps { + id: string + objects: E[] + onChange: (objects: E[]) => void + canDetach: (ref: HTMLDivElement) => boolean + onElementDetached: (ref: HTMLDivElement, el: E) => void + render: (e: E) => ReactElement } -interface RackItemProps { - item: E, - onTryDetach: (ref: HTMLDivElement, el: E) => void, - render: (e: E) => ReactElement, +interface RackItemProps { + item: E + onTryDetach: (ref: HTMLDivElement, el: E) => void + render: (e: E) => ReactElement } /** * A container of draggable objects * */ -export function Rack({id, objects, onChange, canDetach, onElementDetached, render}: RackProps) { +export function Rack({ + id, + objects, + onChange, + canDetach, + onElementDetached, + render, +}: RackProps) { return ( -
- {objects.map(element => ( - { - if (!canDetach(ref)) - return +
+ {objects.map((element) => ( + { + if (!canDetach(ref)) return - const index = objects.findIndex(o => o.key === element.key) - onChange(objects.toSpliced(index, 1)) + const index = objects.findIndex( + (o) => o.key === element.key, + ) + onChange(objects.toSpliced(index, 1)) - onElementDetached(ref, element) - }}/> + onElementDetached(ref, element) + }} + /> ))}
) } -function RackItem({item, onTryDetach, render}: RackItemProps) { - const divRef = useRef(null); +function RackItem({ + item, + onTryDetach, + render, +}: RackItemProps) { + const divRef = useRef(null) return ( onTryDetach(divRef.current!, item)}> -
- {render(item)} -
+
{render(item)}
) -} \ No newline at end of file +} diff --git a/front/components/TitleInput.tsx b/front/components/TitleInput.tsx index eb162d1..6e4acb0 100644 --- a/front/components/TitleInput.tsx +++ b/front/components/TitleInput.tsx @@ -1,28 +1,32 @@ -import React, {CSSProperties, useRef, useState} from "react"; -import "../style/title_input.css"; +import React, { CSSProperties, useRef, useState } from "react" +import "../style/title_input.css" export interface TitleInputOptions { - style: CSSProperties, - default_value: string, + style: CSSProperties + default_value: string on_validated: (a: string) => void } -export default function TitleInput({style, default_value, on_validated}: TitleInputOptions) { - const [value, setValue] = useState(default_value); - const ref = useRef(null); +export default function TitleInput({ + style, + default_value, + on_validated, +}: TitleInputOptions) { + const [value, setValue] = useState(default_value) + const ref = useRef(null) return ( - setValue(event.target.value)} - onBlur={_ => on_validated(value)} - onKeyDown={event => { - if (event.key == 'Enter') - ref.current?.blur(); - }} + setValue(event.target.value)} + onBlur={(_) => on_validated(value)} + onKeyDown={(event) => { + if (event.key == "Enter") ref.current?.blur() + }} /> ) -} \ No newline at end of file +} diff --git a/front/components/editor/BasketCourt.tsx b/front/components/editor/BasketCourt.tsx index 7e839a8..9f4cb5d 100644 --- a/front/components/editor/BasketCourt.tsx +++ b/front/components/editor/BasketCourt.tsx @@ -1,25 +1,27 @@ -import CourtSvg from '../../assets/basketball_court.svg?react'; -import '../../style/basket_court.css'; -import {useRef} from "react"; -import CourtPlayer from "./CourtPlayer"; -import {Player} from "../../data/Player"; +import CourtSvg from "../../assets/basketball_court.svg?react" +import "../../style/basket_court.css" +import { useRef } from "react" +import CourtPlayer from "./CourtPlayer" +import { Player } from "../../data/Player" export interface BasketCourtProps { - players: Player[], - onPlayerRemove: (p: Player) => void, + players: Player[] + onPlayerRemove: (p: Player) => void } -export function BasketCourt({players, onPlayerRemove}: BasketCourtProps) { +export function BasketCourt({ players, onPlayerRemove }: BasketCourtProps) { return ( -
- - {players.map(player => { - return onPlayerRemove(player)} - /> +
+ + {players.map((player) => { + return ( + onPlayerRemove(player)} + /> + ) })}
) } - diff --git a/front/components/editor/CourtPlayer.tsx b/front/components/editor/CourtPlayer.tsx index b6b64de..9b08e7b 100644 --- a/front/components/editor/CourtPlayer.tsx +++ b/front/components/editor/CourtPlayer.tsx @@ -1,55 +1,49 @@ -import {useRef} from "react"; -import "../../style/player.css"; -import RemoveIcon from "../../assets/icon/remove.svg?react"; -import Draggable from "react-draggable"; -import {PlayerPiece} from "./PlayerPiece"; -import {Player} from "../../data/Player"; +import { useRef } from "react" +import "../../style/player.css" +import RemoveIcon from "../../assets/icon/remove.svg?react" +import Draggable from "react-draggable" +import { PlayerPiece } from "./PlayerPiece" +import { Player } from "../../data/Player" export interface PlayerProps { - player: Player, + player: Player onRemove: () => void } /** * A player that is placed on the court, which can be selected, and moved in the associated bounds * */ -export default function CourtPlayer({player, onRemove}: PlayerProps) { +export default function CourtPlayer({ player, onRemove }: PlayerProps) { + const ref = useRef(null) - const ref = useRef(null); - - const x = player.rightRatio; - const y = player.bottomRatio; + const x = player.rightRatio + const y = player.bottomRatio return ( - -
- -
{ - if (e.key == "Delete") - onRemove() - }}> + +
+
{ + if (e.key == "Delete") onRemove() + }}>
+ onClick={onRemove} + />
- +
-
- ) -} \ No newline at end of file +} diff --git a/front/components/editor/PlayerPiece.tsx b/front/components/editor/PlayerPiece.tsx index 83e7dfc..08bf36d 100644 --- a/front/components/editor/PlayerPiece.tsx +++ b/front/components/editor/PlayerPiece.tsx @@ -1,12 +1,11 @@ -import React from "react"; -import '../../style/player.css' -import {Team} from "../../data/Team"; +import React from "react" +import "../../style/player.css" +import { Team } from "../../data/Team" - -export function PlayerPiece({team, text}: { team: Team, text: string }) { +export function PlayerPiece({ team, text }: { team: Team; text: string }) { return (

{text}

) -} \ No newline at end of file +} diff --git a/front/data/Player.ts b/front/data/Player.ts index af88c1c..f2667b9 100644 --- a/front/data/Player.ts +++ b/front/data/Player.ts @@ -1,21 +1,21 @@ -import {Team} from "./Team"; +import { Team } from "./Team" export interface Player { /** * unique identifier of the player. * This identifier must be unique to the associated court. */ - id: number, + id: number /** * the player's team * */ - team: Team, + team: Team /** * player's position * */ - role: string, + role: string /** * Percentage of the player's position to the bottom (0 means top, 1 means bottom, 0.5 means middle) @@ -25,5 +25,5 @@ export interface Player { /** * Percentage of the player's position to the right (0 means left, 1 means right, 0.5 means middle) */ - rightRatio: number, -} \ No newline at end of file + rightRatio: number +} diff --git a/front/data/Team.tsx b/front/data/Team.tsx index ea4c384..5b35943 100644 --- a/front/data/Team.tsx +++ b/front/data/Team.tsx @@ -1,4 +1,4 @@ export enum Team { Allies = "allies", - Opponents = "opponents" -} \ No newline at end of file + Opponents = "opponents", +} diff --git a/front/style/basket_court.css b/front/style/basket_court.css index a5bc688..c001cc0 100644 --- a/front/style/basket_court.css +++ b/front/style/basket_court.css @@ -1,9 +1,6 @@ - - #court-container { display: flex; - background-color: var(--main-color); } @@ -13,8 +10,6 @@ -webkit-user-drag: none; } - - #court-svg * { stroke: var(--selected-team-secondarycolor); -} \ No newline at end of file +} diff --git a/front/style/colors.css b/front/style/colors.css index f3287cb..3c17a25 100644 --- a/front/style/colors.css +++ b/front/style/colors.css @@ -1,5 +1,3 @@ - - :root { --main-color: #ffffff; --second-color: #ccde54; @@ -9,5 +7,5 @@ --selected-team-primarycolor: #ffffff; --selected-team-secondarycolor: #000000; - --selection-color: #3f7fc4 -} \ No newline at end of file + --selection-color: #3f7fc4; +} diff --git a/front/style/editor.css b/front/style/editor.css index 3aad26c..b586a36 100644 --- a/front/style/editor.css +++ b/front/style/editor.css @@ -1,6 +1,5 @@ @import "colors.css"; - #main-div { display: flex; height: 100%; @@ -31,7 +30,8 @@ height: 100%; } -#allies-rack .player-piece , #opponent-rack .player-piece { +#allies-rack .player-piece, +#opponent-rack .player-piece { margin-left: 5px; } @@ -53,7 +53,6 @@ width: 60%; } - .react-draggable { z-index: 2; -} \ No newline at end of file +} diff --git a/front/style/player.css b/front/style/player.css index 264b479..7bea36e 100644 --- a/front/style/player.css +++ b/front/style/player.css @@ -76,4 +76,4 @@ on the court. .player:focus-within { z-index: 1000; -} \ No newline at end of file +} diff --git a/front/style/title_input.css b/front/style/title_input.css index 57af59b..1b6be10 100644 --- a/front/style/title_input.css +++ b/front/style/title_input.css @@ -14,4 +14,3 @@ border-bottom-color: blueviolet; } - diff --git a/front/views/DisplayResults.tsx b/front/views/DisplayResults.tsx index c4bbd1b..7e22df3 100644 --- a/front/views/DisplayResults.tsx +++ b/front/views/DisplayResults.tsx @@ -1,19 +1,13 @@ - interface DisplayResultsProps { - results: readonly { name: string, description: string}[] + results: readonly { name: string; description: string }[] } -export default function DisplayResults({results}: DisplayResultsProps) { - const list = results - .map(({name, description}) => +export default function DisplayResults({ results }: DisplayResultsProps) { + const list = results.map(({ name, description }) => (

username: {name}

description: {description}

- ) - return ( -
- {list} -
- ) + )) + return
{list}
} diff --git a/front/views/Editor.tsx b/front/views/Editor.tsx index 423385f..d98062d 100644 --- a/front/views/Editor.tsx +++ b/front/views/Editor.tsx @@ -1,43 +1,42 @@ -import {CSSProperties, useRef, useState} from "react"; -import "../style/editor.css"; -import TitleInput from "../components/TitleInput"; -import {API} from "../Constants"; -import {BasketCourt} from "../components/editor/BasketCourt"; +import { CSSProperties, useRef, useState } from "react" +import "../style/editor.css" +import TitleInput from "../components/TitleInput" +import { API } from "../Constants" +import { BasketCourt } from "../components/editor/BasketCourt" -import {Rack} from "../components/Rack"; -import {PlayerPiece} from "../components/editor/PlayerPiece"; -import {Player} from "../data/Player"; -import {Team} from "../data/Team"; +import { Rack } from "../components/Rack" +import { PlayerPiece } from "../components/editor/PlayerPiece" +import { Player } from "../data/Player" +import { Team } from "../data/Team" const ERROR_STYLE: CSSProperties = { - borderColor: "red" + borderColor: "red", } /** * information about a player that is into a rack */ interface RackedPlayer { - team: Team, - key: string, + team: Team + key: string } -export default function Editor({id, name}: { id: number, name: string }) { - const [style, setStyle] = useState({}); - +export default function Editor({ id, name }: { id: number; name: string }) { + const [style, setStyle] = useState({}) const positions = ["1", "2", "3", "4", "5"] const [allies, setAllies] = useState( - positions.map(key => ({team: Team.Allies, key})) + positions.map((key) => ({ team: Team.Allies, key })), ) const [opponents, setOpponents] = useState( - positions.map(key => ({team: Team.Opponents, key})) + positions.map((key) => ({ team: Team.Opponents, key })), ) - const [players, setPlayers] = useState([]); - const courtDivContentRef = useRef(null); + const [players, setPlayers] = useState([]) + const courtDivContentRef = useRef(null) const canDetach = (ref: HTMLDivElement) => { - const refBounds = ref.getBoundingClientRect(); - const courtBounds = courtDivContentRef.current!.getBoundingClientRect(); + const refBounds = ref.getBoundingClientRect() + const courtBounds = courtDivContentRef.current!.getBoundingClientRect() // check if refBounds overlaps courtBounds return !( @@ -45,27 +44,30 @@ export default function Editor({id, name}: { id: number, name: string }) { refBounds.right < courtBounds.left || refBounds.bottom < courtBounds.top || refBounds.left > courtBounds.right - ); + ) } const onPieceDetach = (ref: HTMLDivElement, element: RackedPlayer) => { - const refBounds = ref.getBoundingClientRect(); - const courtBounds = courtDivContentRef.current!.getBoundingClientRect(); + const refBounds = ref.getBoundingClientRect() + const courtBounds = courtDivContentRef.current!.getBoundingClientRect() - const relativeXPixels = refBounds.x - courtBounds.x; - const relativeYPixels = refBounds.y - courtBounds.y; + const relativeXPixels = refBounds.x - courtBounds.x + const relativeYPixels = refBounds.y - courtBounds.y - const xRatio = relativeXPixels / courtBounds.width; - const yRatio = relativeYPixels / courtBounds.height; + const xRatio = relativeXPixels / courtBounds.width + const yRatio = relativeYPixels / courtBounds.height - setPlayers(players => { - return [...players, { - id: players.length, - team: element.team, - role: element.key, - rightRatio: xRatio, - bottomRatio: yRatio - }] + setPlayers((players) => { + return [ + ...players, + { + id: players.length, + team: element.team, + role: element.key, + rightRatio: xRatio, + bottomRatio: yRatio, + }, + ] }) } @@ -73,74 +75,88 @@ export default function Editor({id, name}: { id: number, name: string }) {
LEFT
- { - fetch(`${API}/tactic/${id}/edit/name`, { - method: "POST", - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - name: new_name, + { + fetch(`${API}/tactic/${id}/edit/name`, { + method: "POST", + headers: { + Accept: "application/json", + "Content-Type": "application/json", + }, + body: JSON.stringify({ + name: new_name, + }), + }).then((response) => { + if (response.ok) { + setStyle({}) + } else { + setStyle(ERROR_STYLE) + } }) - }).then(response => { - if (response.ok) { - setStyle({}) - } else { - setStyle(ERROR_STYLE) - } - }) - }}/> + }} + />
RIGHT
- }/> - }/> + ( + + )} + /> + ( + + )} + />
{ - setPlayers(players => { + setPlayers((players) => { const idx = players.indexOf(player) return players.toSpliced(idx, 1) }) switch (player.team) { case Team.Opponents: - setOpponents(opponents => ( - [...opponents, { + setOpponents((opponents) => [ + ...opponents, + { team: player.team, pos: player.role, - key: player.role - }] - )) + key: player.role, + }, + ]) break case Team.Allies: - setAllies(allies => ( - [...allies, { + setAllies((allies) => [ + ...allies, + { team: player.team, pos: player.role, - key: player.role - }] - )) + key: player.role, + }, + ]) } - }}/> + }} + />
) } - diff --git a/front/views/SampleForm.tsx b/front/views/SampleForm.tsx index 604e362..00309e4 100644 --- a/front/views/SampleForm.tsx +++ b/front/views/SampleForm.tsx @@ -1,19 +1,14 @@ - - export default function SampleForm() { return (

Hello, this is a sample form made in react !

- + - - + +
) } - - - diff --git a/package.json b/package.json index 97f0039..a3ba0ad 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,9 @@ "scripts": { "start": "vite --host", "build": "vite build", - "test": "vite test" + "test": "vite test", + "format": "prettier --config .prettierrc 'front' --write", + "tsc": "node_modules/.bin/tsc" }, "eslintConfig": { "extends": [ @@ -30,6 +32,8 @@ }, "devDependencies": { "@vitejs/plugin-react": "^4.1.0", - "vite-plugin-svgr": "^4.1.0" + "vite-plugin-svgr": "^4.1.0", + "prettier": "^3.1.0", + "typescript": "^5.2.2" } } diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..7801d9b --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,15 @@ +parameters: + phpVersion: 70400 + level: 6 + paths: + - src + - public + - sql + ignoreErrors: + - + message: '#.*#' + path: sql/database.php + - + message: '#.*#' + path: src/react-display-file.php + diff --git a/profiles/dev-config-profile.php b/profiles/dev-config-profile.php index 316ff44..bd87f1d 100644 --- a/profiles/dev-config-profile.php +++ b/profiles/dev-config-profile.php @@ -10,11 +10,7 @@ $_data_source_name = "sqlite:${_SERVER['DOCUMENT_ROOT']}/../dev-database.sqlite" const _DATABASE_USER = null; const _DATABASE_PASSWORD = null; -function _asset(string $assetURI): string -{ +function _asset(string $assetURI): string { global $front_url; return $front_url . "/" . $assetURI; } - - - diff --git a/profiles/prod-config-profile.php b/profiles/prod-config-profile.php index e185dfc..e9bb12c 100644 --- a/profiles/prod-config-profile.php +++ b/profiles/prod-config-profile.php @@ -19,4 +19,4 @@ function _asset(string $assetURI): string { // If the asset uri does not figure in the available assets array, // fallback to the uri itself. return $basePath . "/" . (ASSETS[$assetURI] ?? $assetURI); -} \ No newline at end of file +} diff --git a/public/api/index.php b/public/api/index.php index b6327e1..3ed5caa 100644 --- a/public/api/index.php +++ b/public/api/index.php @@ -37,6 +37,6 @@ http_response_code($response->getCode()); if ($response instanceof JsonHttpResponse) { header('Content-type: application/json'); echo $response->getJson(); -} else if ($response instanceof ViewHttpResponse) { +} elseif ($response instanceof ViewHttpResponse) { throw new Exception("API returned a view http response."); -} \ No newline at end of file +} diff --git a/public/index.php b/public/index.php index ba9d7c0..e81d098 100644 --- a/public/index.php +++ b/public/index.php @@ -17,7 +17,6 @@ use Twig\Loader\FilesystemLoader; use App\Validation\ValidationFail; use App\Controller\ErrorController; - $loader = new FilesystemLoader('../src/Views/'); $twig = new \Twig\Environment($loader); @@ -28,7 +27,7 @@ $con = new Connexion(get_database()); $router = new AltoRouter(); $router->setBasePath($basePath); -$sampleFormController = new SampleFormController(new FormResultGateway($con), $twig); +$sampleFormController = new SampleFormController(new FormResultGateway($con)); $editorController = new EditorController(new TacticModel(new TacticInfoGateway($con))); @@ -65,12 +64,12 @@ if ($response instanceof ViewHttpResponse) { } catch (\Twig\Error\RuntimeError|\Twig\Error\SyntaxError $e) { http_response_code(500); echo "There was an error rendering your view, please refer to an administrator.\nlogs date: " . date("YYYD, d M Y H:i:s"); - throw e; + throw $e; } break; } -} else if ($response instanceof JsonHttpResponse) { +} elseif ($response instanceof JsonHttpResponse) { header('Content-type: application/json'); echo $response->getJson(); -} \ No newline at end of file +} diff --git a/public/utils.php b/public/utils.php index a3566fe..7151386 100644 --- a/public/utils.php +++ b/public/utils.php @@ -3,18 +3,19 @@ /** * relative path of the public directory from the server's document root. */ -function get_public_path() { +function get_public_path(): string { // find the server path of the index.php file $basePath = substr(__DIR__, strlen($_SERVER['DOCUMENT_ROOT'])); $basePathLen = strlen($basePath); - if ($basePathLen == 0) + if ($basePathLen == 0) { return ""; - + } + $c = $basePath[$basePathLen - 1]; if ($c == "/" || $c == "\\") { $basePath = substr($basePath, 0, $basePathLen - 1); } return $basePath; -} \ No newline at end of file +} diff --git a/sql/database.php b/sql/database.php index d49ddfd..8f5aa9d 100644 --- a/sql/database.php +++ b/sql/database.php @@ -25,6 +25,3 @@ function get_database(): PDO { return $pdo; } - - - diff --git a/src/Connexion.php b/src/Connexion.php index 788c0fb..987d35b 100644 --- a/src/Connexion.php +++ b/src/Connexion.php @@ -1,28 +1,27 @@ pdo = $pdo; } - public function lastInsertId() { + public function lastInsertId(): string { return $this->pdo->lastInsertId(); } /** * execute a request * @param string $query - * @param array $args + * @param array> $args * @return void */ public function exec(string $query, array $args) { @@ -33,8 +32,8 @@ class Connexion { /** * Execute a request, and return the returned rows * @param string $query the SQL request - * @param array $args an array containing the arguments label, value and type: ex: `[":label" => [$value, PDO::PARAM_TYPE]` - * @return array the returned rows of the request + * @param array> $args an array containing the arguments label, value and type: ex: `[":label" => [$value, PDO::PARAM_TYPE]` + * @return array[] the returned rows of the request */ public function fetch(string $query, array $args): array { $stmnt = $this->prepare($query, $args); @@ -42,6 +41,11 @@ class Connexion { return $stmnt->fetchAll(PDO::FETCH_ASSOC); } + /** + * @param string $query + * @param array> $args + * @return \PDOStatement + */ private function prepare(string $query, array $args): \PDOStatement { $stmnt = $this->pdo->prepare($query); foreach ($args as $name => $value) { @@ -50,4 +54,4 @@ class Connexion { return $stmnt; } -} \ No newline at end of file +} diff --git a/src/Controller/Api/APITacticController.php b/src/Controller/Api/APITacticController.php index a39b2ce..ec0edc8 100644 --- a/src/Controller/Api/APITacticController.php +++ b/src/Controller/Api/APITacticController.php @@ -25,7 +25,7 @@ class APITacticController { public function updateName(int $tactic_id): HttpResponse { return Control::runChecked([ - "name" => [Validators::lenBetween(1, 50), Validators::nameWithSpaces()] + "name" => [Validators::lenBetween(1, 50), Validators::nameWithSpaces()], ], function (HttpRequest $request) use ($tactic_id) { $this->model->updateName($tactic_id, $request["name"]); return HttpResponse::fromCode(HttpCodes::OK); @@ -34,7 +34,7 @@ class APITacticController { public function newTactic(): HttpResponse { return Control::runChecked([ - "name" => [Validators::lenBetween(1, 50), Validators::nameWithSpaces()] + "name" => [Validators::lenBetween(1, 50), Validators::nameWithSpaces()], ], function (HttpRequest $request) { $tactic = $this->model->makeNew($request["name"]); $id = $tactic->getId(); @@ -52,4 +52,4 @@ class APITacticController { return new JsonHttpResponse($tactic_info); } -} \ No newline at end of file +} diff --git a/src/Controller/Control.php b/src/Controller/Control.php index 8c00d2e..e50eed0 100644 --- a/src/Controller/Control.php +++ b/src/Controller/Control.php @@ -8,14 +8,16 @@ use App\Http\HttpResponse; use App\Http\JsonHttpResponse; use App\Http\ViewHttpResponse; use App\Validation\ValidationFail; +use App\Validation\Validator; class Control { - /** * Runs given callback, if the request's json validates the given schema. - * @param array $schema an array of `fieldName => Validators` which represents the request object schema + * @param array $schema an array of `fieldName => Validators` which represents the request object schema * @param callable $run the callback to run if the request is valid according to the given schema. * THe callback must accept an HttpRequest, and return an HttpResponse object. + * @param bool $errorInJson if set to true, the returned response, in case of errors, will be a JsonHttpResponse, instead + * of the ViewHttpResponse for an error view. * @return HttpResponse */ public static function runChecked(array $schema, callable $run, bool $errorInJson): HttpResponse { @@ -23,7 +25,7 @@ class Control { $payload_obj = json_decode($request_body); if (!$payload_obj instanceof \stdClass) { $fail = new ValidationFail("bad-payload", "request body is not a valid json object"); - if($errorInJson) { + if ($errorInJson) { return new JsonHttpResponse([$fail, HttpCodes::BAD_REQUEST]); } return ViewHttpResponse::twig("error.html.twig", ["failures" => [$fail]], HttpCodes::BAD_REQUEST); @@ -34,10 +36,12 @@ class Control { /** * Runs given callback, if the given request data array validates the given schema. - * @param array $data the request's data array. - * @param array $schema an array of `fieldName => Validators` which represents the request object schema + * @param array $data the request's data array. + * @param array $schema an array of `fieldName => Validators` which represents the request object schema * @param callable $run the callback to run if the request is valid according to the given schema. * THe callback must accept an HttpRequest, and return an HttpResponse object. + * @param bool $errorInJson if set to true, the returned response, in case of errors, will be a JsonHttpResponse, instead + * of the ViewHttpResponse for an error view. * @return HttpResponse */ public static function runCheckedFrom(array $data, array $schema, callable $run, bool $errorInJson): HttpResponse { @@ -45,7 +49,7 @@ class Control { $request = HttpRequest::from($data, $fails, $schema); if (!empty($fails)) { - if($errorInJson) { + if ($errorInJson) { return new JsonHttpResponse($fails, HttpCodes::BAD_REQUEST); } return ViewHttpResponse::twig("error.html.twig", ['failures' => $fails], HttpCodes::BAD_REQUEST); @@ -53,9 +57,6 @@ class Control { return call_user_func_array($run, [$request]); } - - - -} \ No newline at end of file +} diff --git a/src/Controller/EditorController.php b/src/Controller/EditorController.php index bf5dccc..ed270d1 100644 --- a/src/Controller/EditorController.php +++ b/src/Controller/EditorController.php @@ -11,7 +11,6 @@ use App\Http\ViewHttpResponse; use App\Model\TacticModel; class EditorController { - private TacticModel $model; /** @@ -45,4 +44,4 @@ class EditorController { return $this->openEditor($tactic); } -} \ No newline at end of file +} diff --git a/src/Controller/ErrorController.php b/src/Controller/ErrorController.php index e91d05f..7fc5239 100644 --- a/src/Controller/ErrorController.php +++ b/src/Controller/ErrorController.php @@ -3,17 +3,23 @@ namespace App\Controller; require_once __DIR__ . "/../react-display.php"; -use \Twig\Environment; + +use App\Validation\ValidationFail; +use Twig\Environment; use Twig\Error\LoaderError; use Twig\Error\RuntimeError; use Twig\Error\SyntaxError; -class ErrorController -{ - public static function displayFailures(array $failures, Environment $twig) { +class ErrorController { + /** + * @param ValidationFail[] $failures + * @param Environment $twig + * @return void + */ + public static function displayFailures(array $failures, Environment $twig): void { try { $twig->display("error.html.twig", ['failures' => $failures]); - } catch (LoaderError | RuntimeError | SyntaxError $e) { + } catch (LoaderError|RuntimeError|SyntaxError $e) { echo "Twig error: $e"; } } diff --git a/src/Controller/SampleFormController.php b/src/Controller/SampleFormController.php index 4241ad4..41b5f96 100644 --- a/src/Controller/SampleFormController.php +++ b/src/Controller/SampleFormController.php @@ -11,7 +11,6 @@ use App\Http\ViewHttpResponse; use App\Validation\Validators; class SampleFormController { - private FormResultGateway $gateway; /** @@ -30,10 +29,15 @@ class SampleFormController { return ViewHttpResponse::twig('sample_form.html.twig', []); } + /** + * @param array $form + * @param callable $response + * @return HttpResponse + */ private function submitForm(array $form, callable $response): HttpResponse { return Control::runCheckedFrom($form, [ "name" => [Validators::lenBetween(0, 32), Validators::name("Le nom ne peut contenir que des lettres, des chiffres et des accents")], - "description" => [Validators::lenBetween(0, 512)] + "description" => [Validators::lenBetween(0, 512)], ], function (HttpRequest $req) use ($response) { $description = htmlspecialchars($req["description"]); $this->gateway->insert($req["name"], $description); @@ -42,11 +46,19 @@ class SampleFormController { }, false); } + /** + * @param array $form + * @return HttpResponse + */ public function submitFormTwig(array $form): HttpResponse { return $this->submitForm($form, fn(array $results) => ViewHttpResponse::twig('display_results.html.twig', $results)); } + /** + * @param array $form + * @return HttpResponse + */ public function submitFormReact(array $form): HttpResponse { return $this->submitForm($form, fn(array $results) => ViewHttpResponse::react('views/DisplayResults.tsx', $results)); } -} \ No newline at end of file +} diff --git a/src/Data/Account.php b/src/Data/Account.php index 155f2ae..51d7aad 100755 --- a/src/Data/Account.php +++ b/src/Data/Account.php @@ -4,7 +4,7 @@ namespace App\Data; use http\Exception\InvalidArgumentException; -const PHONE_NUMBER_REGEXP = "\\+[0-9]+"; +const PHONE_NUMBER_REGEXP = "/^\\+[0-9]+$/"; /** * Base class of a user account. @@ -29,7 +29,7 @@ class Account { private AccountUser $user; /** - * @var array account's teams + * @var array account's teams */ private array $teams; @@ -38,10 +38,13 @@ class Account { */ private int $id; + /** * @param string $email * @param string $phoneNumber * @param AccountUser $user + * @param array $teams + * @param int $id */ public function __construct(string $email, string $phoneNumber, AccountUser $user, array $teams, int $id) { $this->email = $email; @@ -79,7 +82,7 @@ class Account { * @param string $phoneNumber */ public function setPhoneNumber(string $phoneNumber): void { - if (!filter_var($phoneNumber, FILTER_VALIDATE_REGEXP, PHONE_NUMBER_REGEXP)) { + if (!preg_match(PHONE_NUMBER_REGEXP, $phoneNumber)) { throw new InvalidArgumentException("Invalid phone number"); } $this->phoneNumber = $phoneNumber; @@ -89,6 +92,9 @@ class Account { return $this->id; } + /** + * @return Team[] + */ public function getTeams(): array { return $this->teams; } @@ -96,4 +102,4 @@ class Account { public function getUser(): AccountUser { return $this->user; } -} \ No newline at end of file +} diff --git a/src/Data/AccountUser.php b/src/Data/AccountUser.php index 7808062..5d3b497 100755 --- a/src/Data/AccountUser.php +++ b/src/Data/AccountUser.php @@ -36,17 +36,17 @@ class AccountUser implements User { return $this->age; } - public function setName(string $name) { + public function setName(string $name): void { $this->name = $name; } - public function setProfilePicture(Url $profilePicture) { + public function setProfilePicture(Url $profilePicture): void { $this->profilePicture = $profilePicture; } - public function setAge(int $age) { + public function setAge(int $age): void { $this->age = $age; } -} \ No newline at end of file +} diff --git a/src/Data/Color.php b/src/Data/Color.php index f841731..0b1fbb3 100755 --- a/src/Data/Color.php +++ b/src/Data/Color.php @@ -14,7 +14,7 @@ class Color { * @param int $value 6 bytes unsigned int that represents an RGB color * @throws \InvalidArgumentException if the value is negative or greater than 0xFFFFFF */ - public function __constructor(int $value) { + public function __construct(int $value) { if ($value < 0 || $value > 0xFFFFFF) { throw new InvalidArgumentException("int color value is invalid, must be positive and lower than 0xFFFFFF"); } @@ -27,4 +27,4 @@ class Color { public function getValue(): int { return $this->value; } -} \ No newline at end of file +} diff --git a/src/Data/Member.php b/src/Data/Member.php index 91b09c4..6500733 100755 --- a/src/Data/Member.php +++ b/src/Data/Member.php @@ -39,4 +39,4 @@ class Member { return $this->role; } -} \ No newline at end of file +} diff --git a/src/Data/MemberRole.php b/src/Data/MemberRole.php index 05d746d..a1d4d31 100755 --- a/src/Data/MemberRole.php +++ b/src/Data/MemberRole.php @@ -2,7 +2,6 @@ namespace App\Data; - use http\Exception\InvalidArgumentException; /** @@ -37,4 +36,4 @@ final class MemberRole { return ($this->value == self::ROLE_COACH); } -} \ No newline at end of file +} diff --git a/src/Data/TacticInfo.php b/src/Data/TacticInfo.php index 901280d..eef7bb3 100644 --- a/src/Data/TacticInfo.php +++ b/src/Data/TacticInfo.php @@ -30,7 +30,10 @@ class TacticInfo implements \JsonSerializable { return $this->creation_date; } - public function jsonSerialize() { + /** + * @return array + */ + public function jsonSerialize(): array { return get_object_vars($this); } -} \ No newline at end of file +} diff --git a/src/Data/Team.php b/src/Data/Team.php index 48643d9..a224bd4 100755 --- a/src/Data/Team.php +++ b/src/Data/Team.php @@ -11,7 +11,7 @@ class Team { private Color $secondColor; /** - * @var array maps users with their role + * @var array maps users with their role */ private array $members; @@ -20,7 +20,7 @@ class Team { * @param Url $picture * @param Color $mainColor * @param Color $secondColor - * @param array $members + * @param array $members */ public function __construct(string $name, Url $picture, Color $mainColor, Color $secondColor, array $members) { $this->name = $name; @@ -58,8 +58,11 @@ class Team { return $this->secondColor; } + /** + * @return array + */ public function listMembers(): array { - return array_map(fn ($id, $role) => new Member($id, $role), $this->members); + return $this->members; } -} \ No newline at end of file +} diff --git a/src/Data/User.php b/src/Data/User.php index 15c9995..6cb55c2 100755 --- a/src/Data/User.php +++ b/src/Data/User.php @@ -4,7 +4,6 @@ namespace App\Data; use http\Url; - /** * Public information about a user */ @@ -24,4 +23,4 @@ interface User { * @return int The user's age */ public function getAge(): int; -} \ No newline at end of file +} diff --git a/src/Gateway/FormResultGateway.php b/src/Gateway/FormResultGateway.php index fe0c601..36178ad 100644 --- a/src/Gateway/FormResultGateway.php +++ b/src/Gateway/FormResultGateway.php @@ -9,7 +9,6 @@ use App\Connexion; * A sample gateway, that stores the sample form's result. */ class FormResultGateway { - private Connexion $con; public function __construct(Connexion $con) { @@ -17,17 +16,20 @@ class FormResultGateway { } - function insert(string $username, string $description) { + public function insert(string $username, string $description): void { $this->con->exec( "INSERT INTO FormEntries VALUES (:name, :description)", [ ":name" => [$username, PDO::PARAM_STR], - "description" => [$description, PDO::PARAM_STR] + "description" => [$description, PDO::PARAM_STR], ] ); } - function listResults(): array { + /** + * @return array + */ + public function listResults(): array { return $this->con->fetch("SELECT * FROM FormEntries", []); } -} \ No newline at end of file +} diff --git a/src/Gateway/TacticInfoGateway.php b/src/Gateway/TacticInfoGateway.php index 20d2957..3441c9a 100644 --- a/src/Gateway/TacticInfoGateway.php +++ b/src/Gateway/TacticInfoGateway.php @@ -4,7 +4,7 @@ namespace App\Gateway; use App\Connexion; use App\Data\TacticInfo; -use \PDO; +use PDO; class TacticInfoGateway { private Connexion $con; @@ -43,14 +43,14 @@ class TacticInfoGateway { return new TacticInfo(intval($row["id"]), $name, strtotime($row["creation_date"])); } - public function updateName(int $id, string $name) { + public function updateName(int $id, string $name): void { $this->con->exec( "UPDATE TacticInfo SET name = :name WHERE id = :id", [ ":name" => [$name, PDO::PARAM_STR], - ":id" => [$id, PDO::PARAM_INT] + ":id" => [$id, PDO::PARAM_INT], ] ); } -} \ No newline at end of file +} diff --git a/src/Http/HttpCodes.php b/src/Http/HttpCodes.php index b41af8a..f9d550c 100644 --- a/src/Http/HttpCodes.php +++ b/src/Http/HttpCodes.php @@ -10,4 +10,4 @@ class HttpCodes { public const BAD_REQUEST = 400; public const NOT_FOUND = 404; -} \ No newline at end of file +} diff --git a/src/Http/HttpRequest.php b/src/Http/HttpRequest.php index f841752..6414d75 100644 --- a/src/Http/HttpRequest.php +++ b/src/Http/HttpRequest.php @@ -4,12 +4,23 @@ namespace App\Http; use App\Validation\FieldValidationFail; use App\Validation\Validation; +use App\Validation\ValidationFail; +use App\Validation\Validator; use ArrayAccess; use Exception; +/** + * @implements ArrayAccess + * */ class HttpRequest implements ArrayAccess { + /** + * @var array + */ private array $data; + /** + * @param array $data + */ private function __construct(array $data) { $this->data = $data; } @@ -17,9 +28,9 @@ class HttpRequest implements ArrayAccess { /** * Creates a new HttpRequest instance, and ensures that the given request data validates the given schema. * This is a simple function that only supports flat schemas (non-composed, the data must only be a k/v array pair.) - * @param array $request the request's data - * @param array $fails a reference to a failure array, that will contain the reported validation failures. - * @param array $schema the schema to satisfy. a schema is a simple array with a string key (which is the top-level field name), and a set of validators + * @param array $request the request's data + * @param array $fails a reference to a failure array, that will contain the reported validation failures. + * @param array> $schema the schema to satisfy. a schema is a simple array with a string key (which is the top-level field name), and a set of validators * @return HttpRequest|null the built HttpRequest instance, or null if a field is missing, or if any of the schema validator failed */ public static function from(array $request, array &$fails, array $schema): ?HttpRequest { @@ -43,15 +54,29 @@ class HttpRequest implements ArrayAccess { return isset($this->data[$offset]); } + /** + * @param $offset + * @return mixed + */ public function offsetGet($offset) { return $this->data[$offset]; } + /** + * @param $offset + * @param $value + * @return mixed + * @throws Exception + */ public function offsetSet($offset, $value) { throw new Exception("requests are immutable objects."); } + /** + * @param $offset + * @throws Exception + */ public function offsetUnset($offset) { throw new Exception("requests are immutable objects."); } -} \ No newline at end of file +} diff --git a/src/Http/HttpResponse.php b/src/Http/HttpResponse.php index 9f081a5..5d8c3bf 100644 --- a/src/Http/HttpResponse.php +++ b/src/Http/HttpResponse.php @@ -3,7 +3,6 @@ namespace App\Http; class HttpResponse { - private int $code; /** @@ -21,4 +20,4 @@ class HttpResponse { return new HttpResponse($code); } -} \ No newline at end of file +} diff --git a/src/Http/JsonHttpResponse.php b/src/Http/JsonHttpResponse.php index 9d7423f..bbd3d80 100644 --- a/src/Http/JsonHttpResponse.php +++ b/src/Http/JsonHttpResponse.php @@ -3,7 +3,6 @@ namespace App\Http; class JsonHttpResponse extends HttpResponse { - /** * @var mixed Any JSON serializable value */ @@ -26,4 +25,4 @@ class JsonHttpResponse extends HttpResponse { return $result; } -} \ No newline at end of file +} diff --git a/src/Http/ViewHttpResponse.php b/src/Http/ViewHttpResponse.php index 0e92054..2e517d7 100644 --- a/src/Http/ViewHttpResponse.php +++ b/src/Http/ViewHttpResponse.php @@ -3,7 +3,6 @@ namespace App\Http; class ViewHttpResponse extends HttpResponse { - public const TWIG_VIEW = 0; public const REACT_VIEW = 1; @@ -12,7 +11,7 @@ class ViewHttpResponse extends HttpResponse { */ private string $file; /** - * @var array View arguments + * @var array View arguments */ private array $arguments; /** @@ -24,7 +23,7 @@ class ViewHttpResponse extends HttpResponse { * @param int $code * @param int $kind * @param string $file - * @param array $arguments + * @param array $arguments */ private function __construct(int $kind, string $file, array $arguments, int $code = HttpCodes::OK) { parent::__construct($code); @@ -41,6 +40,9 @@ class ViewHttpResponse extends HttpResponse { return $this->file; } + /** + * @return array + */ public function getArguments(): array { return $this->arguments; } @@ -48,7 +50,7 @@ class ViewHttpResponse extends HttpResponse { /** * Create a twig view response * @param string $file - * @param array $arguments + * @param array $arguments * @param int $code * @return ViewHttpResponse */ @@ -59,7 +61,7 @@ class ViewHttpResponse extends HttpResponse { /** * Create a react view response * @param string $file - * @param array $arguments + * @param array $arguments * @param int $code * @return ViewHttpResponse */ @@ -67,4 +69,4 @@ class ViewHttpResponse extends HttpResponse { return new ViewHttpResponse(self::REACT_VIEW, $file, $arguments, $code); } -} \ No newline at end of file +} diff --git a/src/Model/TacticModel.php b/src/Model/TacticModel.php index c0b1ffe..cabcdec 100644 --- a/src/Model/TacticModel.php +++ b/src/Model/TacticModel.php @@ -6,7 +6,6 @@ use App\Data\TacticInfo; use App\Gateway\TacticInfoGateway; class TacticModel { - public const TACTIC_DEFAULT_NAME = "Nouvelle tactique"; @@ -40,7 +39,7 @@ class TacticModel { * Update the name of a tactic * @param int $id the tactic identifier * @param string $name the new name to set - * @return true if the update was done successfully + * @return bool true if the update was done successfully */ public function updateName(int $id, string $name): bool { if ($this->tactics->get($id) == null) { @@ -51,4 +50,4 @@ class TacticModel { return true; } -} \ No newline at end of file +} diff --git a/src/Validation/ComposedValidator.php b/src/Validation/ComposedValidator.php index 418b1ed..cc6e9e5 100644 --- a/src/Validation/ComposedValidator.php +++ b/src/Validation/ComposedValidator.php @@ -3,7 +3,6 @@ namespace App\Validation; class ComposedValidator extends Validator { - private Validator $first; private Validator $then; @@ -21,4 +20,4 @@ class ComposedValidator extends Validator { $thenFailures = $this->then->validate($name, $val); return array_merge($firstFailures, $thenFailures); } -} \ No newline at end of file +} diff --git a/src/Validation/FieldValidationFail.php b/src/Validation/FieldValidationFail.php index 5b535f7..af2ce3a 100644 --- a/src/Validation/FieldValidationFail.php +++ b/src/Validation/FieldValidationFail.php @@ -2,7 +2,6 @@ namespace App\Validation; - /** * An error that concerns a field, with a bound message name */ @@ -34,7 +33,10 @@ class FieldValidationFail extends ValidationFail { return new FieldValidationFail($fieldName, "field is missing"); } - public function jsonSerialize() { + /** + * @return array + */ + public function jsonSerialize(): array { return ["field" => $this->fieldName, "message" => $this->getMessage()]; } -} \ No newline at end of file +} diff --git a/src/Validation/FunctionValidator.php b/src/Validation/FunctionValidator.php index 6874d63..f052ea6 100644 --- a/src/Validation/FunctionValidator.php +++ b/src/Validation/FunctionValidator.php @@ -3,7 +3,9 @@ namespace App\Validation; class FunctionValidator extends Validator { - + /** + * @var callable + */ private $validate_fn; /** @@ -16,4 +18,4 @@ class FunctionValidator extends Validator { public function validate(string $name, $val): array { return call_user_func_array($this->validate_fn, [$name, $val]); } -} \ No newline at end of file +} diff --git a/src/Validation/SimpleFunctionValidator.php b/src/Validation/SimpleFunctionValidator.php index 079452d..7514bae 100644 --- a/src/Validation/SimpleFunctionValidator.php +++ b/src/Validation/SimpleFunctionValidator.php @@ -6,8 +6,13 @@ namespace App\Validation; * A simple validator that takes a predicate and an error factory */ class SimpleFunctionValidator extends Validator { - + /** + * @var callable + */ private $predicate; + /** + * @var callable + */ private $errorFactory; /** @@ -25,4 +30,4 @@ class SimpleFunctionValidator extends Validator { } return []; } -} \ No newline at end of file +} diff --git a/src/Validation/Validation.php b/src/Validation/Validation.php index 4372380..19144b7 100644 --- a/src/Validation/Validation.php +++ b/src/Validation/Validation.php @@ -6,12 +6,11 @@ namespace App\Validation; * Utility class for validation */ class Validation { - /** * Validate a value from validators, appending failures in the given errors array. * @param mixed $val the value to validate * @param string $valName the name of the value - * @param array $failures array to push when a validator fails + * @param array $failures array to push when a validator fails * @param Validator ...$validators given validators * @return bool true if any of the given validators did fail */ @@ -27,4 +26,4 @@ class Validation { return $had_errors; } -} \ No newline at end of file +} diff --git a/src/Validation/ValidationFail.php b/src/Validation/ValidationFail.php index fa5139c..4f1ec22 100644 --- a/src/Validation/ValidationFail.php +++ b/src/Validation/ValidationFail.php @@ -2,7 +2,9 @@ namespace App\Validation; -class ValidationFail implements \JsonSerializable { +use JsonSerializable; + +class ValidationFail implements JsonSerializable { private string $kind; private string $message; @@ -24,7 +26,10 @@ class ValidationFail implements \JsonSerializable { return $this->kind; } - public function jsonSerialize() { + /** + * @return array + */ + public function jsonSerialize(): array { return ["error" => $this->kind, "message" => $this->message]; } @@ -32,4 +37,4 @@ class ValidationFail implements \JsonSerializable { return new ValidationFail("not found", $message); } -} \ No newline at end of file +} diff --git a/src/Validation/Validator.php b/src/Validation/Validator.php index 6cdafb9..4ad6131 100644 --- a/src/Validation/Validator.php +++ b/src/Validation/Validator.php @@ -3,14 +3,13 @@ namespace App\Validation; abstract class Validator { - /** * validates a variable string * @param string $name the name of the tested value * @param mixed $val the value to validate - * @return array the errors the validator has reported + * @return array the errors the validator has reported */ - public abstract function validate(string $name, $val): array; + abstract public function validate(string $name, $val): array; /** * Creates a validator composed of this validator, and given validator @@ -21,4 +20,4 @@ abstract class Validator { return new ComposedValidator($this, $other); } -} \ No newline at end of file +} diff --git a/src/Validation/Validators.php b/src/Validation/Validators.php index ea9da46..6f72cf2 100644 --- a/src/Validation/Validators.php +++ b/src/Validation/Validators.php @@ -6,7 +6,6 @@ namespace App\Validation; * A collection of standard validators */ class Validators { - /** * @return Validator a validator that validates a given regex */ @@ -20,7 +19,7 @@ class Validators { /** * @return Validator a validator that validates strings that only contains numbers, letters, accents letters, `-` and `_`. */ - public static function name($msg = null): Validator { + public static function name(string $msg = null): Validator { return self::regex("/^[0-9a-zA-Zà-üÀ-Ü_-]*$/", $msg); } @@ -51,4 +50,4 @@ class Validators { } ); } -} \ No newline at end of file +} diff --git a/src/react-display.php b/src/react-display.php index b965a3a..5baf41b 100644 --- a/src/react-display.php +++ b/src/react-display.php @@ -3,11 +3,11 @@ /** * sends a react view to the user client. * @param string $url url of the react file to render - * @param array $arguments arguments to pass to the rendered react component - * The arguments must be a json-encodable key/value dictionary. + * @param array $arguments arguments to pass to the rendered react component + * The arguments must be a json-encodable key/value dictionary. * @return void */ function send_react_front(string $url, array $arguments) { // the $url and $argument values are used into the included file require_once "react-display-file.php"; -} \ No newline at end of file +} diff --git a/verify.sh b/verify.sh new file mode 100755 index 0000000..314b8bc --- /dev/null +++ b/verify.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +## verify php and typescript types + +echo "running php typechecking" +vendor/bin/phpstan analyze && echo "php types are respected" + +echo "running typescript typechecking" +npm run tsc && echo "typescript types are respected" \ No newline at end of file From 185a4d19bbf233fc70dd5ee7ee99bf48d7a353df Mon Sep 17 00:00:00 2001 From: Override-6 Date: Sun, 19 Nov 2023 22:20:22 +0100 Subject: [PATCH 15/18] add typecheckers in ci --- ci/.drone.yml | 22 +++++++++++++++++++--- ci/build_react.msh | 3 ++- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/ci/.drone.yml b/ci/.drone.yml index d6c474c..2431032 100644 --- a/ci/.drone.yml +++ b/ci/.drone.yml @@ -1,6 +1,6 @@ kind: pipeline type: docker -name: "Deploy on maxou.dev" +name: "CI and Deploy on maxou.dev" volumes: - name: server @@ -11,11 +11,26 @@ trigger: - push steps: + + - image: node:latest + name: "front CI" + commands: + - npm install + - npm run tsc + + - image: composer:latest + name: "php CI" + commands: + - composer install && composer update + - 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 @@ -24,14 +39,15 @@ steps: - - /root/.local/bin/moshell ci/build_react.msh - - image: composer:latest + - 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 -iE 's/\\/\\*PROFILE_FILE\\*\\/\\s*".*"/"profiles\\/prod-config-profile.php"/' config.php - - composer install && composer update - rm profiles/dev-config-profile.php - mv src config.php sql profiles vendor /outputs/ diff --git a/ci/build_react.msh b/ci/build_react.msh index c893498..64a0cb6 100755 --- a/ci/build_react.msh +++ b/ci/build_react.msh @@ -3,13 +3,14 @@ mkdir -p /outputs/public apt update && apt install jq -y -npm install val drone_branch = std::env("DRONE_BRANCH").unwrap() val base = "/IQBall/$drone_branch/public" npm run build -- --base=$base --mode PROD +npm run build -- --base=/IQBall/public --mode PROD + // Read generated mappings from build val result = $(jq -r 'to_entries|map(.key + " " +.value.file)|.[]' dist/manifest.json) val mappings = $result.split('\n') From bb53114d78de19b11dcb0645bf11afeaecfb7dac Mon Sep 17 00:00:00 2001 From: "maxime.batista" Date: Tue, 21 Nov 2023 10:23:01 +0100 Subject: [PATCH 16/18] apply suggestions --- .php-cs-fixer.php | 2 +- ci/.drone.yml | 2 +- package.json | 2 +- phpstan.neon | 16 +++++++--------- src/Controller/Control.php | 4 ++-- src/Controller/SampleFormController.php | 2 +- src/Data/Account.php | 4 ++-- src/Data/Team.php | 6 +++--- src/Http/HttpRequest.php | 3 +-- src/Validation/FunctionValidator.php | 4 ++-- src/Validation/SimpleFunctionValidator.php | 8 ++++---- src/Validation/Validation.php | 2 +- src/Validation/Validator.php | 2 +- src/Validation/Validators.php | 4 ++-- 14 files changed, 29 insertions(+), 32 deletions(-) diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php index 265de93..77ef0e7 100644 --- a/.php-cs-fixer.php +++ b/.php-cs-fixer.php @@ -5,7 +5,7 @@ $finder = (new PhpCsFixer\Finder())->in(__DIR__); return (new PhpCsFixer\Config()) ->setRules([ '@PER-CS' => true, - '@PHP82Migration' => true, + '@PHP74Migration' => true, 'array_syntax' => ['syntax' => 'short'], 'braces_position' => [ 'classes_opening_brace' => 'same_line', diff --git a/ci/.drone.yml b/ci/.drone.yml index 2431032..8b7058d 100644 --- a/ci/.drone.yml +++ b/ci/.drone.yml @@ -21,7 +21,7 @@ steps: - image: composer:latest name: "php CI" commands: - - composer install && composer update + - composer install - vendor/bin/phpstan analyze - image: node:latest diff --git a/package.json b/package.json index a3ba0ad..79c9d46 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "build": "vite build", "test": "vite test", "format": "prettier --config .prettierrc 'front' --write", - "tsc": "node_modules/.bin/tsc" + "tsc": "tsc" }, "eslintConfig": { "extends": [ diff --git a/phpstan.neon b/phpstan.neon index 7801d9b..bc6c041 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -4,12 +4,10 @@ parameters: paths: - src - public - - sql - ignoreErrors: - - - message: '#.*#' - path: sql/database.php - - - message: '#.*#' - path: src/react-display-file.php - + scanFiles: + - config.php + - sql/database.php + - profiles/dev-config-profile.php + - profiles/prod-config-profile.php + excludePaths: + - src/react-display-file.php diff --git a/src/Controller/Control.php b/src/Controller/Control.php index e50eed0..2b428d9 100644 --- a/src/Controller/Control.php +++ b/src/Controller/Control.php @@ -14,7 +14,7 @@ class Control { /** * Runs given callback, if the request's json validates the given schema. * @param array $schema an array of `fieldName => Validators` which represents the request object schema - * @param callable $run the callback to run if the request is valid according to the given schema. + * @param callable(HttpRequest): HttpResponse $run the callback to run if the request is valid according to the given schema. * THe callback must accept an HttpRequest, and return an HttpResponse object. * @param bool $errorInJson if set to true, the returned response, in case of errors, will be a JsonHttpResponse, instead * of the ViewHttpResponse for an error view. @@ -38,7 +38,7 @@ class Control { * Runs given callback, if the given request data array validates the given schema. * @param array $data the request's data array. * @param array $schema an array of `fieldName => Validators` which represents the request object schema - * @param callable $run the callback to run if the request is valid according to the given schema. + * @param callable(HttpRequest): HttpResponse $run the callback to run if the request is valid according to the given schema. * THe callback must accept an HttpRequest, and return an HttpResponse object. * @param bool $errorInJson if set to true, the returned response, in case of errors, will be a JsonHttpResponse, instead * of the ViewHttpResponse for an error view. diff --git a/src/Controller/SampleFormController.php b/src/Controller/SampleFormController.php index 41b5f96..773a4db 100644 --- a/src/Controller/SampleFormController.php +++ b/src/Controller/SampleFormController.php @@ -31,7 +31,7 @@ class SampleFormController { /** * @param array $form - * @param callable $response + * @param callable(array>): ViewHttpResponse $response * @return HttpResponse */ private function submitForm(array $form, callable $response): HttpResponse { diff --git a/src/Data/Account.php b/src/Data/Account.php index 51d7aad..2a21bf1 100755 --- a/src/Data/Account.php +++ b/src/Data/Account.php @@ -29,7 +29,7 @@ class Account { private AccountUser $user; /** - * @var array account's teams + * @var Team[] account's teams */ private array $teams; @@ -43,7 +43,7 @@ class Account { * @param string $email * @param string $phoneNumber * @param AccountUser $user - * @param array $teams + * @param Team[] $teams * @param int $id */ public function __construct(string $email, string $phoneNumber, AccountUser $user, array $teams, int $id) { diff --git a/src/Data/Team.php b/src/Data/Team.php index a224bd4..aca4e0d 100755 --- a/src/Data/Team.php +++ b/src/Data/Team.php @@ -11,7 +11,7 @@ class Team { private Color $secondColor; /** - * @var array maps users with their role + * @var Member[] maps users with their role */ private array $members; @@ -20,7 +20,7 @@ class Team { * @param Url $picture * @param Color $mainColor * @param Color $secondColor - * @param array $members + * @param Member[] $members */ public function __construct(string $name, Url $picture, Color $mainColor, Color $secondColor, array $members) { $this->name = $name; @@ -59,7 +59,7 @@ class Team { } /** - * @return array + * @return Member[] */ public function listMembers(): array { return $this->members; diff --git a/src/Http/HttpRequest.php b/src/Http/HttpRequest.php index 6414d75..d199227 100644 --- a/src/Http/HttpRequest.php +++ b/src/Http/HttpRequest.php @@ -30,7 +30,7 @@ class HttpRequest implements ArrayAccess { * This is a simple function that only supports flat schemas (non-composed, the data must only be a k/v array pair.) * @param array $request the request's data * @param array $fails a reference to a failure array, that will contain the reported validation failures. - * @param array> $schema the schema to satisfy. a schema is a simple array with a string key (which is the top-level field name), and a set of validators + * @param array $schema the schema to satisfy. a schema is a simple array with a string key (which is the top-level field name), and a set of validators * @return HttpRequest|null the built HttpRequest instance, or null if a field is missing, or if any of the schema validator failed */ public static function from(array $request, array &$fails, array $schema): ?HttpRequest { @@ -65,7 +65,6 @@ class HttpRequest implements ArrayAccess { /** * @param $offset * @param $value - * @return mixed * @throws Exception */ public function offsetSet($offset, $value) { diff --git a/src/Validation/FunctionValidator.php b/src/Validation/FunctionValidator.php index f052ea6..4949d8b 100644 --- a/src/Validation/FunctionValidator.php +++ b/src/Validation/FunctionValidator.php @@ -4,12 +4,12 @@ namespace App\Validation; class FunctionValidator extends Validator { /** - * @var callable + * @var callable(string, mixed): ValidationFail[] */ private $validate_fn; /** - * @param callable $validate_fn the validate function. Must have the same signature as the {@link Validator::validate()} method. + * @param callable(string, mixed): ValidationFail[] $validate_fn the validate function. Must have the same signature as the {@link Validator::validate()} method. */ public function __construct(callable $validate_fn) { $this->validate_fn = $validate_fn; diff --git a/src/Validation/SimpleFunctionValidator.php b/src/Validation/SimpleFunctionValidator.php index 7514bae..cec52c0 100644 --- a/src/Validation/SimpleFunctionValidator.php +++ b/src/Validation/SimpleFunctionValidator.php @@ -7,17 +7,17 @@ namespace App\Validation; */ class SimpleFunctionValidator extends Validator { /** - * @var callable + * @var callable(mixed): bool */ private $predicate; /** - * @var callable + * @var callable(string): ValidationFail[] */ private $errorFactory; /** - * @param callable $predicate a function predicate with signature: `(string) => bool`, to validate the given string - * @param callable $errorsFactory a factory function with signature `(string) => array` to emit failures when the predicate fails + * @param callable(mixed): bool $predicate a function predicate with signature: `(string) => bool`, to validate the given string + * @param callable(string): ValidationFail[] $errorsFactory a factory function with signature `(string) => array` to emit failures when the predicate fails */ public function __construct(callable $predicate, callable $errorsFactory) { $this->predicate = $predicate; diff --git a/src/Validation/Validation.php b/src/Validation/Validation.php index 19144b7..f1392e6 100644 --- a/src/Validation/Validation.php +++ b/src/Validation/Validation.php @@ -10,7 +10,7 @@ class Validation { * Validate a value from validators, appending failures in the given errors array. * @param mixed $val the value to validate * @param string $valName the name of the value - * @param array $failures array to push when a validator fails + * @param ValidationFail[] $failures array to push when a validator fails * @param Validator ...$validators given validators * @return bool true if any of the given validators did fail */ diff --git a/src/Validation/Validator.php b/src/Validation/Validator.php index 4ad6131..8227e46 100644 --- a/src/Validation/Validator.php +++ b/src/Validation/Validator.php @@ -7,7 +7,7 @@ abstract class Validator { * validates a variable string * @param string $name the name of the tested value * @param mixed $val the value to validate - * @return array the errors the validator has reported + * @return ValidationFail[] the errors the validator has reported */ abstract public function validate(string $name, $val): array; diff --git a/src/Validation/Validators.php b/src/Validation/Validators.php index 6f72cf2..94aea23 100644 --- a/src/Validation/Validators.php +++ b/src/Validation/Validators.php @@ -9,7 +9,7 @@ class Validators { /** * @return Validator a validator that validates a given regex */ - public static function regex(string $regex, string $msg = null): Validator { + public static function regex(string $regex, ?string $msg = null): Validator { return new SimpleFunctionValidator( fn(string $str) => preg_match($regex, $str), fn(string $name) => [new FieldValidationFail($name, $msg == null ? "field does not validates pattern $regex" : $msg)] @@ -19,7 +19,7 @@ class Validators { /** * @return Validator a validator that validates strings that only contains numbers, letters, accents letters, `-` and `_`. */ - public static function name(string $msg = null): Validator { + public static function name(?string $msg = null): Validator { return self::regex("/^[0-9a-zA-Zà-üÀ-Ü_-]*$/", $msg); } From d47bf6bd7df24970fdf9fc3a2813c15e70c102f1 Mon Sep 17 00:00:00 2001 From: Vivien DUFOUR Date: Tue, 21 Nov 2023 10:40:15 +0100 Subject: [PATCH 17/18] visualizer/bootstrap (#14) Basic Visualizer for oral presentation Co-authored-by: vidufour1 Co-authored-by: vivien.dufour Reviewed-on: https://codefirst.iut.uca.fr/git/IQBall/Application-Web/pulls/14 --- front/style/visualizer.css | 30 +++++++++++++++++++++++ front/views/Visualizer.tsx | 24 +++++++++++++++++++ public/index.php | 3 +++ sql/.guard | 0 src/Controller/VisualizerController.php | 32 +++++++++++++++++++++++++ 5 files changed, 89 insertions(+) create mode 100644 front/style/visualizer.css create mode 100644 front/views/Visualizer.tsx create mode 100644 sql/.guard create mode 100644 src/Controller/VisualizerController.php diff --git a/front/style/visualizer.css b/front/style/visualizer.css new file mode 100644 index 0000000..2d1a73f --- /dev/null +++ b/front/style/visualizer.css @@ -0,0 +1,30 @@ +#main { + height: 100vh; + width: 100%; + display: flex; + flex-direction: column; +} + +#topbar { + display: flex; + background-color: var(--main-color); + justify-content: center; + align-items: center; +} + +h1 { + text-align: center; + margin-top: 0; +} + +#court-container { + flex: 1; + display: flex; + justify-content: center; + background-color: var(--main-color); +} + +#court { + max-width: 80%; + max-height: 80%; +} diff --git a/front/views/Visualizer.tsx b/front/views/Visualizer.tsx new file mode 100644 index 0000000..ddf7fe2 --- /dev/null +++ b/front/views/Visualizer.tsx @@ -0,0 +1,24 @@ +import React, { CSSProperties, useState } from "react" +import "../style/visualizer.css" +import Court from "../assets/basketball_court.svg" + + +export default function Visualizer({id, name}: { id: number; name: string }) { + const [style, setStyle] = useState({}); + + return ( +
+
+

{name}

+
+
+ Basketball Court +
+
+ ); +} diff --git a/public/index.php b/public/index.php index e81d098..7f19f63 100644 --- a/public/index.php +++ b/public/index.php @@ -16,6 +16,7 @@ use App\Model\TacticModel; use Twig\Loader\FilesystemLoader; use App\Validation\ValidationFail; use App\Controller\ErrorController; +use App\Controller\VisualizerController; $loader = new FilesystemLoader('../src/Views/'); $twig = new \Twig\Environment($loader); @@ -29,6 +30,7 @@ $router->setBasePath($basePath); $sampleFormController = new SampleFormController(new FormResultGateway($con)); $editorController = new EditorController(new TacticModel(new TacticInfoGateway($con))); +$visualizerController = new VisualizerController(new TacticModel(new TacticInfoGateway($con))); $router->map("GET", "/", fn() => $sampleFormController->displayFormReact()); @@ -37,6 +39,7 @@ $router->map("GET", "/twig", fn() => $sampleFormController->displayFormTwig()); $router->map("POST", "/submit-twig", fn() => $sampleFormController->submitFormTwig($_POST)); $router->map("GET", "/tactic/new", fn() => $editorController->makeNew()); $router->map("GET", "/tactic/[i:id]/edit", fn(int $id) => $editorController->openEditorFor($id)); +$router->map("GET", "/tactic/[i:id]", fn(int $id) => $visualizerController->openVisualizer($id)); $match = $router->match(); diff --git a/sql/.guard b/sql/.guard new file mode 100644 index 0000000..e69de29 diff --git a/src/Controller/VisualizerController.php b/src/Controller/VisualizerController.php new file mode 100644 index 0000000..3c8b55e --- /dev/null +++ b/src/Controller/VisualizerController.php @@ -0,0 +1,32 @@ +tacticModel = $tacticModel; + } + + public function openVisualizer(int $id): HttpResponse { + $tactic = $this->tacticModel->get($id); + + if ($tactic == null) { + return new JsonHttpResponse("la tactique " . $id . " n'existe pas", HttpCodes::NOT_FOUND); + } + + return ViewHttpResponse::react("views/Visualizer.tsx", ["name" => $tactic->getName()]); + + } +} From 9333288705bfb0be8b20b8dc1771c15da95c1df2 Mon Sep 17 00:00:00 2001 From: Samuel BERION Date: Tue, 21 Nov 2023 11:13:56 +0100 Subject: [PATCH 18/18] Add register and login of authentification actions (#12) Co-authored-by: Samuel Co-authored-by: samuel Reviewed-on: https://codefirst.iut.uca.fr/git/IQBall/Application-Web/pulls/12 --- Documentation/models.puml | 26 +++++++ front/views/Visualizer.tsx | 7 +- public/index.php | 8 ++ sql/setup-tables.sql | 6 ++ src/Controller/AuthController.php | 94 ++++++++++++++++++++++++ src/Gateway/AuthGateway.php | 47 ++++++++++++ src/Model/AuthModel.php | 80 ++++++++++++++++++++ src/Views/display_auth_confirm.html.twig | 46 ++++++++++++ src/Views/display_login.html.twig | 85 +++++++++++++++++++++ src/Views/display_register.html.twig | 88 ++++++++++++++++++++++ src/Views/display_results.html.twig | 2 +- src/Views/sample_form.html.twig | 1 - 12 files changed, 484 insertions(+), 6 deletions(-) create mode 100644 src/Controller/AuthController.php create mode 100644 src/Gateway/AuthGateway.php create mode 100644 src/Model/AuthModel.php create mode 100644 src/Views/display_auth_confirm.html.twig create mode 100644 src/Views/display_login.html.twig create mode 100644 src/Views/display_register.html.twig diff --git a/Documentation/models.puml b/Documentation/models.puml index 1f4877a..d95343c 100755 --- a/Documentation/models.puml +++ b/Documentation/models.puml @@ -68,4 +68,30 @@ class Color { + getValue(): int } +class AuthController{ + + + displayRegister() : HttpResponse + + displayBadFields(viewName : string, fails : array) : HttpResponse + + confirmRegister(request : array) : HttpResponse + + displayLogin() : HttpResponse + + confirmLogin() : HttpResponse +} +AuthController --> "- model" AuthModel + +class AuthModel{ + + + register(username : string, password : string, confirmPassword : string, email : string): array + + getUserFields(email : string):array + + login(email : string, password : string) +} +AuthModel --> "- gateway" AuthGateway + +class AuthGateway{ + -con : Connection + + + mailExist(email : string) : bool + + insertAccount(username : string, hash : string, email : string) + + getUserHash(email : string):string + + getUserFields (email : string): array +} @enduml \ No newline at end of file diff --git a/front/views/Visualizer.tsx b/front/views/Visualizer.tsx index ddf7fe2..541da09 100644 --- a/front/views/Visualizer.tsx +++ b/front/views/Visualizer.tsx @@ -2,9 +2,8 @@ import React, { CSSProperties, useState } from "react" import "../style/visualizer.css" import Court from "../assets/basketball_court.svg" - -export default function Visualizer({id, name}: { id: number; name: string }) { - const [style, setStyle] = useState({}); +export default function Visualizer({ id, name }: { id: number; name: string }) { + const [style, setStyle] = useState({}) return (
@@ -20,5 +19,5 @@ export default function Visualizer({id, name}: { id: number; name: string }) { />
- ); + ) } diff --git a/public/index.php b/public/index.php index 7f19f63..edc4cb3 100644 --- a/public/index.php +++ b/public/index.php @@ -14,6 +14,8 @@ use App\Http\JsonHttpResponse; use App\Http\ViewHttpResponse; use App\Model\TacticModel; use Twig\Loader\FilesystemLoader; +use App\Gateway\AuthGateway; +use App\Controller\AuthController; use App\Validation\ValidationFail; use App\Controller\ErrorController; use App\Controller\VisualizerController; @@ -29,6 +31,8 @@ $router = new AltoRouter(); $router->setBasePath($basePath); $sampleFormController = new SampleFormController(new FormResultGateway($con)); +$authGateway = new AuthGateway($con); +$authController = new \App\Controller\AuthController(new \App\Model\AuthModel($authGateway)); $editorController = new EditorController(new TacticModel(new TacticInfoGateway($con))); $visualizerController = new VisualizerController(new TacticModel(new TacticInfoGateway($con))); @@ -37,6 +41,10 @@ $router->map("GET", "/", fn() => $sampleFormController->displayFormReact()); $router->map("POST", "/submit", fn() => $sampleFormController->submitFormReact($_POST)); $router->map("GET", "/twig", fn() => $sampleFormController->displayFormTwig()); $router->map("POST", "/submit-twig", fn() => $sampleFormController->submitFormTwig($_POST)); +$router->map("GET", "/register", fn() => $authController->displayRegister()); +$router->map("POST", "/register", fn() => $authController->confirmRegister($_POST)); +$router->map("GET", "/login", fn() => $authController->displayLogin()); +$router->map("POST", "/login", fn() => $authController->confirmLogin($_POST)); $router->map("GET", "/tactic/new", fn() => $editorController->makeNew()); $router->map("GET", "/tactic/[i:id]/edit", fn(int $id) => $editorController->openEditorFor($id)); $router->map("GET", "/tactic/[i:id]", fn(int $id) => $visualizerController->openVisualizer($id)); diff --git a/sql/setup-tables.sql b/sql/setup-tables.sql index 068c2e1..108b62a 100644 --- a/sql/setup-tables.sql +++ b/sql/setup-tables.sql @@ -1,9 +1,15 @@ -- drop tables here DROP TABLE IF EXISTS FormEntries; +DROP TABLE IF EXISTS AccountUser; DROP TABLE IF EXISTS TacticInfo; CREATE TABLE FormEntries(name varchar, description varchar); +CREATE TABLE AccountUser( + username varchar, + hash varchar, + email varchar unique +); CREATE TABLE TacticInfo( id integer PRIMARY KEY AUTOINCREMENT, diff --git a/src/Controller/AuthController.php b/src/Controller/AuthController.php new file mode 100644 index 0000000..e42c27d --- /dev/null +++ b/src/Controller/AuthController.php @@ -0,0 +1,94 @@ +model = $model; + } + + public function displayRegister(): HttpResponse { + return ViewHttpResponse::twig("display_register.html.twig", []); + } + + /** + * @param string $viewName + * @param ValidationFail[] $fails + * @return HttpResponse + */ + private function displayBadFields(string $viewName, array $fails): HttpResponse { + $bad_fields = []; + foreach ($fails as $err) { + if ($err instanceof FieldValidationFail) { + $bad_fields[] = $err->getFieldName(); + } + } + return ViewHttpResponse::twig($viewName, ['bad_fields' => $bad_fields]); + } + + /** + * @param mixed[] $request + * @return HttpResponse + */ + public function confirmRegister(array $request): HttpResponse { + $fails = []; + $request = HttpRequest::from($request, $fails, [ + "username" => [Validators::name(), Validators::lenBetween(2, 32)], + "password" => [Validators::lenBetween(6, 256)], + "confirmpassword" => [Validators::lenBetween(6, 256)], + "email" => [Validators::regex("/^\\S+@\\S+\\.\\S+$/"),Validators::lenBetween(5, 256)], + ]); + if (!empty($fails)) { + return $this->displayBadFields("display_register.html.twig", $fails); + } + $fails = $this->model->register($request['username'], $request["password"], $request['confirmpassword'], $request['email']); + if (empty($fails)) { + $results = $this->model->getUserFields($request['email']); + return ViewHttpResponse::twig("display_auth_confirm.html.twig", ['username' => $results['username'], 'email' => $results['email']]); + } + return $this->displayBadFields("display_register.html.twig", $fails); + } + + + public function displayLogin(): HttpResponse { + return ViewHttpResponse::twig("display_login.html.twig", []); + } + + /** + * @param mixed[] $request + * @return HttpResponse + */ + public function confirmLogin(array $request): HttpResponse { + $fails = []; + $request = HttpRequest::from($request, $fails, [ + "password" => [Validators::lenBetween(6, 256)], + "email" => [Validators::regex("/^\\S+@\\S+\\.\\S+$/"),Validators::lenBetween(5, 256)], + ]); + if (!empty($fails)) { + return $this->displayBadFields("display_login.html.twig", $fails); + } + + $fails = $this->model->login($request['email'], $request['password']); + if (empty($fails)) { + $results = $this->model->getUserFields($request['email']); + return ViewHttpResponse::twig("display_auth_confirm.html.twig", ['username' => $results['username'], 'email' => $results['email']]); + } + return $this->displayBadFields("display_login.html.twig", $fails); + } + +} diff --git a/src/Gateway/AuthGateway.php b/src/Gateway/AuthGateway.php new file mode 100644 index 0000000..5acc01c --- /dev/null +++ b/src/Gateway/AuthGateway.php @@ -0,0 +1,47 @@ +con = $con; + } + + + public function mailExist(string $email): bool { + return $this->getUserFields($email) != null; + } + + + public function insertAccount(string $username, string $hash, string $email): void { + $this->con->exec("INSERT INTO AccountUser VALUES (:username,:hash,:email)", [':username' => [$username, PDO::PARAM_STR],':hash' => [$hash, PDO::PARAM_STR],':email' => [$email, PDO::PARAM_STR]]); + } + + public function getUserHash(string $email): string { + $results = $this->con->fetch("SELECT hash FROM AccountUser WHERE email = :email", [':email' => [$email, PDO::PARAM_STR]]); + return $results[0]['hash']; + } + + + /** + * @param string $email + * @return array|null + */ + public function getUserFields(string $email): ?array { + $results = $this->con->fetch("SELECT username,email FROM AccountUser WHERE email = :email", [':email' => [$email, PDO::PARAM_STR]]); + $firstRow = $results[0] ?? null; + return $firstRow; + } + + + + +} diff --git a/src/Model/AuthModel.php b/src/Model/AuthModel.php new file mode 100644 index 0000000..45b63e4 --- /dev/null +++ b/src/Model/AuthModel.php @@ -0,0 +1,80 @@ +gateway = $gateway; + } + + + /** + * @param string $username + * @param string $password + * @param string $confirmPassword + * @param string $email + * @return ValidationFail[] + */ + public function register(string $username, string $password, string $confirmPassword, string $email): array { + $errors = []; + + if ($password != $confirmPassword) { + $errors[] = new FieldValidationFail("confirmpassword", "password and password confirmation are not equals"); + } + + if ($this->gateway->mailExist($email)) { + $errors[] = new FieldValidationFail("email", "email already exist"); + } + + if(empty($errors)) { + $hash = password_hash($password, PASSWORD_DEFAULT); + $this->gateway->insertAccount($username, $hash, $email); + } + + return $errors; + } + + /** + * @param string $email + * @return array|null + */ + public function getUserFields(string $email): ?array { + return $this->gateway->getUserFields($email); + } + + + /** + * @param string $email + * @param string $password + * @return ValidationFail[] $errors + */ + public function login(string $email, string $password): array { + $errors = []; + + if (!$this->gateway->mailExist($email)) { + $errors[] = new FieldValidationFail("email", "email doesnt exists"); + return $errors; + } + $hash = $this->gateway->getUserHash($email); + + if (!password_verify($password, $hash)) { + $errors[] = new FieldValidationFail("password", "invalid password"); + } + + return $errors; + } + + + + + +} diff --git a/src/Views/display_auth_confirm.html.twig b/src/Views/display_auth_confirm.html.twig new file mode 100644 index 0000000..60c63b2 --- /dev/null +++ b/src/Views/display_auth_confirm.html.twig @@ -0,0 +1,46 @@ + + + + + Profil Utilisateur + + + + + + + + \ No newline at end of file diff --git a/src/Views/display_login.html.twig b/src/Views/display_login.html.twig new file mode 100644 index 0000000..33b2385 --- /dev/null +++ b/src/Views/display_login.html.twig @@ -0,0 +1,85 @@ + + + + Connexion + + + + + +
+

Se connecter

+
+
+ + + + + + +
+
+ +
+
+
+ + + + \ No newline at end of file diff --git a/src/Views/display_register.html.twig b/src/Views/display_register.html.twig new file mode 100644 index 0000000..40199a0 --- /dev/null +++ b/src/Views/display_register.html.twig @@ -0,0 +1,88 @@ + + + + S'enregistrer + + + + + +
+

S'enregistrer

+
+
+ + + + + + + + + +
+
+ +
+
+
+ + + + \ No newline at end of file diff --git a/src/Views/display_results.html.twig b/src/Views/display_results.html.twig index 6d2aef0..a33546b 100644 --- a/src/Views/display_results.html.twig +++ b/src/Views/display_results.html.twig @@ -1,4 +1,3 @@ - @@ -14,5 +13,6 @@

description: {{ v.description }}

{% endfor %} + \ No newline at end of file diff --git a/src/Views/sample_form.html.twig b/src/Views/sample_form.html.twig index bcb958e..6f4a9b5 100644 --- a/src/Views/sample_form.html.twig +++ b/src/Views/sample_form.html.twig @@ -1,4 +1,3 @@ -