diff --git a/front/Fetcher.ts b/front/Fetcher.ts index 7c81fdd..4c483e9 100644 --- a/front/Fetcher.ts +++ b/front/Fetcher.ts @@ -2,7 +2,7 @@ import { API } from "./Constants" export function fetchAPI( url: string, - payload: object, + payload: unknown, method = "POST", ): Promise { return fetch(`${API}/${url}`, { diff --git a/front/views/Editor.tsx b/front/views/Editor.tsx index df566ff..17da16d 100644 --- a/front/views/Editor.tsx +++ b/front/views/Editor.tsx @@ -1,4 +1,11 @@ -import React, { CSSProperties, useEffect, useRef, useState } from "react" +import { + CSSProperties, + Dispatch, + SetStateAction, + useCallback, + useRef, + useState, +} from "react" import "../style/editor.css" import TitleInput from "../components/TitleInput" import { BasketCourt } from "../components/editor/BasketCourt" @@ -11,7 +18,10 @@ import { Tactic, TacticContent } from "../tactic/Tactic" import { fetchAPI } from "../Fetcher" import { Team } from "../tactic/Team" import { calculateRatio } from "../Utils" -import SavingState, { SaveStates } from "../components/editor/SavingState" +import SavingState, { + SaveState, + SaveStates, +} from "../components/editor/SavingState" const ERROR_STYLE: CSSProperties = { borderColor: "red", @@ -54,57 +64,27 @@ export default function Editor({ } function EditorView({ - tactic: { name, content }, + tactic: { name, content: initialContent }, onContentChange, onNameChange, }: EditorViewProps) { const [style, setStyle] = useState({}) - const [saveState, setSaveState] = useState(SaveStates.Ok) - - const positions = ["1", "2", "3", "4", "5"] + const [content, setContent, saveState] = useContentState( + initialContent, + (content) => + onContentChange(content).then((success) => + success ? SaveStates.Ok : SaveStates.Err, + ), + ) const [allies, setAllies] = useState( - positions - .filter( - (role) => - content.players.findIndex( - (p) => p.team == Team.Allies && p.role == role, - ) == -1, - ) - .map((key) => ({ team: Team.Allies, key })), + getRackPlayers(Team.Allies, content.players), ) const [opponents, setOpponents] = useState( - positions - .filter( - (role) => - content.players.findIndex( - (p) => p.team == Team.Opponents && p.role == role, - ) == -1, - ) - .map((key) => ({ team: Team.Opponents, key })), + getRackPlayers(Team.Opponents, content.players), ) - const [players, setPlayers] = useState(content.players) const courtDivContentRef = useRef(null) - // The didMount ref is used to store a boolean flag in order to avoid calling 'onChange' when the editor is first rendered. - const didMount = useRef(false) - useEffect(() => { - if (!didMount.current) { - didMount.current = true - return - } - setSaveState(SaveStates.Saving) - onContentChange({ players }) - .then((success) => { - if (success) { - setSaveState(SaveStates.Ok) - } else { - setSaveState(SaveStates.Err) - } - }) - .catch(() => setSaveState(SaveStates.Err)) - }, [players]) - const canDetach = (ref: HTMLDivElement) => { const refBounds = ref.getBoundingClientRect() const courtBounds = courtDivContentRef.current!.getBoundingClientRect() @@ -124,16 +104,18 @@ function EditorView({ const { x, y } = calculateRatio(refBounds, courtBounds) - setPlayers((players) => { - return [ - ...players, - { - team: element.team, - role: element.key, - rightRatio: x, - bottomRatio: y, - }, - ] + setContent((content) => { + return { + players: [ + ...content.players, + { + team: element.team, + role: element.key, + rightRatio: x, + bottomRatio: y, + }, + ], + } }) } @@ -183,47 +165,40 @@ function EditorView({
{ - setPlayers((players) => { - const idx = players.findIndex( - (p) => - p.team === player.team && - p.role === player.role, - ) - return players.toSpliced(idx, 1, player) - }) + setContent((content) => ({ + players: toSplicedPlayers( + content.players, + player, + true, + ), + })) }} onPlayerRemove={(player) => { - setPlayers((players) => { - const idx = players.findIndex( - (p) => - p.team === player.team && - p.role === player.role, - ) - return players.toSpliced(idx, 1) - }) + setContent((content) => ({ + players: toSplicedPlayers( + content.players, + player, + false, + ), + })) + let setter switch (player.team) { case Team.Opponents: - setOpponents((opponents) => [ - ...opponents, - { - team: player.team, - pos: player.role, - key: player.role, - }, - ]) + setter = setOpponents break case Team.Allies: - setAllies((allies) => [ - ...allies, - { - team: player.team, - pos: player.role, - key: player.role, - }, - ]) + setter = setAllies } + setter((players) => [ + ...players, + { + team: player.team, + pos: player.role, + key: player.role, + }, + ]) }} />
@@ -232,3 +207,51 @@ function EditorView({
) } + +function getRackPlayers(team: Team, players: Player[]): RackedPlayer[] { + return ["1", "2", "3", "4", "5"] + .filter( + (role) => + players.findIndex((p) => p.team == team && p.role == role) == + -1, + ) + .map((key) => ({ team, key })) +} + +function useContentState( + initialContent: S, + saveStateCallback: (s: S) => Promise, +): [S, Dispatch>, SaveState] { + const [content, setContent] = useState(initialContent) + const [savingState, setSavingState] = useState(SaveStates.Ok) + + const setContentSynced = useCallback((newState: SetStateAction) => { + setContent((content) => { + const state = + typeof newState === "function" + ? (newState as (state: S) => S)(content) + : newState + if (state !== content) { + setSavingState(SaveStates.Saving) + saveStateCallback(state) + .then(setSavingState) + .catch(() => setSavingState(SaveStates.Err)) + } + return state + }) + }, []) + + return [content, setContentSynced, savingState] +} + +function toSplicedPlayers( + players: Player[], + player: Player, + replace: boolean, +): Player[] { + const idx = players.findIndex( + (p) => p.team === player.team && p.role === player.role, + ) + + return players.toSpliced(idx, 1, ...(replace ? [player] : [])) +} diff --git a/sql/setup-tables.sql b/sql/setup-tables.sql index 5a2eecf..eb74877 100644 --- a/sql/setup-tables.sql +++ b/sql/setup-tables.sql @@ -44,7 +44,7 @@ CREATE TABLE Member ( id_team integer, id_user integer, - role char(1) CHECK (role IN ('Coach', 'Player')), + role text CHECK (role IN ('Coach', 'Player')), FOREIGN KEY (id_team) REFERENCES Team (id), FOREIGN KEY (id_user) REFERENCES User (id) ); diff --git a/src/Api/Controller/APITacticController.php b/src/Api/Controller/APITacticController.php index 606e49b..51ca531 100644 --- a/src/Api/Controller/APITacticController.php +++ b/src/Api/Controller/APITacticController.php @@ -9,6 +9,7 @@ use IQBall\Core\Http\HttpRequest; use IQBall\Core\Http\HttpResponse; use IQBall\Core\Http\JsonHttpResponse; use IQBall\Core\Model\TacticModel; +use IQBall\Core\Validation\FieldValidationFail; use IQBall\Core\Validation\Validators; /** @@ -55,7 +56,9 @@ class APITacticController { return Control::runChecked([ "content" => [], ], function (HttpRequest $req) use ($id) { - $this->model->updateContent($id, json_encode($req["content"])); + if ($fail = $this->model->updateContent($id, json_encode($req["content"]))) { + return new JsonHttpResponse([$fail]); + } return HttpResponse::fromCode(HttpCodes::OK); }); } diff --git a/src/Core/Connection.php b/src/Core/Connection.php index 019b515..1dd559d 100644 --- a/src/Core/Connection.php +++ b/src/Core/Connection.php @@ -25,7 +25,7 @@ class Connection { * @return void */ public function exec(string $query, array $args) { - $stmnt = $this->prepare($query, $args); + $stmnt = $this->prep($query, $args); $stmnt->execute(); } @@ -36,7 +36,7 @@ class Connection { * @return array[] the returned rows of the request */ public function fetch(string $query, array $args): array { - $stmnt = $this->prepare($query, $args); + $stmnt = $this->prep($query, $args); $stmnt->execute(); return $stmnt->fetchAll(PDO::FETCH_ASSOC); } @@ -46,7 +46,7 @@ class Connection { * @param array> $args * @return \PDOStatement */ - private function prepare(string $query, array $args): \PDOStatement { + private function prep(string $query, array $args): \PDOStatement { $stmnt = $this->pdo->prepare($query); foreach ($args as $name => $value) { $stmnt->bindValue($name, $value[0], $value[1]); @@ -54,4 +54,8 @@ class Connection { return $stmnt; } + public function prepare(string $query): \PDOStatement { + return $this->pdo->prepare($query); + } + } diff --git a/src/Core/Gateway/TacticInfoGateway.php b/src/Core/Gateway/TacticInfoGateway.php index 3e0a0d0..6b66f2c 100644 --- a/src/Core/Gateway/TacticInfoGateway.php +++ b/src/Core/Gateway/TacticInfoGateway.php @@ -74,26 +74,30 @@ class TacticInfoGateway { * update name of given tactic identifier * @param int $id * @param string $name - * @return void + * @return bool */ - public function updateName(int $id, string $name): void { - $this->con->exec( - "UPDATE Tactic SET name = :name WHERE id = :id", - [ - ":name" => [$name, PDO::PARAM_STR], - ":id" => [$id, PDO::PARAM_INT], - ] - ); + public function updateName(int $id, string $name): bool { + $stmnt = $this->con->prepare("UPDATE Tactic SET name = :name WHERE id = :id"); + $stmnt->execute([ + ":name" => $name, + ":id" => $id, + ]); + return $stmnt->rowCount() == 1; } - public function updateContent(int $id, string $json): void { - $this->con->exec( - "UPDATE Tactic SET content = :content WHERE id = :id", - [ - ":content" => [$json, PDO::PARAM_STR], - ":id" => [$id, PDO::PARAM_INT], - ] - ); + /*** + * Updates a given tactics content + * @param int $id + * @param string $json + * @return bool + */ + public function updateContent(int $id, string $json): bool { + $stmnt = $this->con->prepare("UPDATE Tactic SET content = :content WHERE id = :id"); + $stmnt->execute([ + ":content" => $json, + ":id" => $id, + ]); + return $stmnt->rowCount() == 1; } } diff --git a/src/Core/Model/TacticModel.php b/src/Core/Model/TacticModel.php index 0560568..136b27d 100644 --- a/src/Core/Model/TacticModel.php +++ b/src/Core/Model/TacticModel.php @@ -76,12 +76,17 @@ class TacticModel { return [ValidationFail::unauthorized()]; } - $this->tactics->updateName($id, $name); + if (!$this->tactics->updateName($id, $name)) { + return [ValidationFail::error("Could not update name")]; + } return []; } - public function updateContent(int $id, string $json): void { - $this->tactics->updateContent($id, $json); + public function updateContent(int $id, string $json): ?ValidationFail { + if (!$this->tactics->updateContent($id, $json)) { + return ValidationFail::error("Could not update content"); + } + return null; } } diff --git a/src/Core/Validation/ValidationFail.php b/src/Core/Validation/ValidationFail.php index 9a714e5..9a74a03 100644 --- a/src/Core/Validation/ValidationFail.php +++ b/src/Core/Validation/ValidationFail.php @@ -49,4 +49,8 @@ class ValidationFail implements JsonSerializable { return new ValidationFail("Unauthorized", $message); } + public static function error(string $message): ValidationFail { + return new ValidationFail("Error", $message); + } + }