From 738d2f94107a10e94f87f4b060a4a98b78c8c99d Mon Sep 17 00:00:00 2001 From: "maxime.batista" Date: Fri, 17 Nov 2023 14:34:38 +0100 Subject: [PATCH 1/6] prepare front to persistable tactics --- front/Fetcher.ts | 13 +++ front/components/editor/BasketCourt.tsx | 37 ++++----- front/components/editor/CourtPlayer.tsx | 60 ++++++++------ front/{data => tactic}/Player.ts | 2 +- front/tactic/Tactic.ts | 11 +++ front/{data => tactic}/Team.tsx | 0 front/views/Editor.tsx | 93 ++++++++++++++-------- public/api/index.php | 3 +- src/Api/Controller/APITacticController.php | 9 +++ 9 files changed, 149 insertions(+), 79 deletions(-) create mode 100644 front/Fetcher.ts rename front/{data => tactic}/Player.ts (95%) create mode 100644 front/tactic/Tactic.ts rename front/{data => tactic}/Team.tsx (100%) diff --git a/front/Fetcher.ts b/front/Fetcher.ts new file mode 100644 index 0000000..59b15c8 --- /dev/null +++ b/front/Fetcher.ts @@ -0,0 +1,13 @@ +import {API} from "./Constants"; + + +export function fetchAPI(url: string, payload: object, method = "POST"): Promise { + return fetch(`${API}/${url}`, { + method, + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }, + body: JSON.stringify(payload) + }) +} diff --git a/front/components/editor/BasketCourt.tsx b/front/components/editor/BasketCourt.tsx index 9f4cb5d..7eab38c 100644 --- a/front/components/editor/BasketCourt.tsx +++ b/front/components/editor/BasketCourt.tsx @@ -1,26 +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 "../../tactic/Player"; export interface BasketCourtProps { - players: Player[] - onPlayerRemove: (p: Player) => void + players: Player[], + onPlayerRemove: (p: Player) => void, + onPlayerChange: (p: Player) => void } -export function BasketCourt({ players, onPlayerRemove }: BasketCourtProps) { +export function BasketCourt({players, onPlayerRemove, onPlayerChange}: BasketCourtProps) { + const divRef = useRef(null); + 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 9b08e7b..b8ff341 100644 --- a/front/components/editor/CourtPlayer.tsx +++ b/front/components/editor/CourtPlayer.tsx @@ -1,40 +1,52 @@ -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 "../../tactic/Player"; export interface PlayerProps { - player: Player + player: Player, + onChange: (p: Player) => void, 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) { - const ref = useRef(null) + +export default function CourtPlayer({player, onChange, onRemove}: PlayerProps) { const x = player.rightRatio const y = player.bottomRatio return ( - -
-
{ - if (e.key == "Delete") onRemove() - }}> + onChange({ + id: player.id, + rightRatio: player.rightRatio, + bottomRatio: player.bottomRatio, + team: player.team, + role: player.role + })} + > +
+ +
{ + if (e.key == "Delete") + onRemove() + }}>
Promise, + onNameChange: (name: string) => Promise +} + /** * information about a player that is into a rack */ @@ -21,8 +29,21 @@ interface RackedPlayer { key: string } -export default function Editor({ id, name }: { id: number; name: string }) { - const [style, setStyle] = useState({}) +export default function Editor({tactic}: { tactic: Tactic }) { + return ( + fetchAPI(`tactic/${tactic.id}/save`, {content}) + .then((r) => r.ok) + )} + onNameChange={(name: string) => ( + fetchAPI(`tactic/${tactic.id}/edit/name`, {name}) + .then((r) => r.ok) + )}/> +} + +function EditorView({tactic: {name, content}, onContentChange, onNameChange}: EditorViewProps) { + const [style, setStyle] = useState({}); + const positions = ["1", "2", "3", "4", "5"] const [allies, setAllies] = useState( positions.map((key) => ({ team: Team.Allies, key })), @@ -31,8 +52,17 @@ export default function Editor({ id, name }: { id: number; name: string }) { positions.map((key) => ({ team: Team.Opponents, key })), ) - const [players, setPlayers] = useState([]) - const courtDivContentRef = useRef(null) + + const [players, setPlayers] = useState(content.players); + const courtDivContentRef = useRef(null); + + useEffect(() => { + onContentChange({players}) + .then(success => { + if (!success) + alert("error when saving changes.") + }) + }, [players]) const canDetach = (ref: HTMLDivElement) => { const refBounds = ref.getBoundingClientRect() @@ -75,28 +105,15 @@ 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, - }), - }).then((response) => { - if (response.ok) { - setStyle({}) - } else { - setStyle(ERROR_STYLE) - } - }) - }} - /> + { + onNameChange(new_name).then(success => { + if (success) { + setStyle({}) + } else { + setStyle(ERROR_STYLE) + } + }) + }}/>
RIGHT
@@ -126,6 +143,12 @@ export default function Editor({ id, name }: { id: number; name: string }) {
{ + setPlayers(players => { + const idx = players.indexOf(player) + return players.toSpliced(idx, 1, player) + }) + }} onPlayerRemove={(player) => { setPlayers((players) => { const idx = players.indexOf(player) diff --git a/public/api/index.php b/public/api/index.php index fd39bfa..8e1b9ef 100644 --- a/public/api/index.php +++ b/public/api/index.php @@ -29,8 +29,9 @@ function getRoutes(): AltoRouter { $router = new AltoRouter(); $router->setBasePath(get_public_path(__DIR__)); - $router->map("POST", "/tactic/[i:id]/edit/name", Action::auth(fn(int $id, Account $acc) => getTacticController()->updateName($id, $acc))); $router->map("POST", "/auth", Action::noAuth(fn() => getAuthController()->authorize())); + $router->map("POST", "/tactic/[i:id]/edit/name", Action::auth(fn(int $id, Account $acc) => getTacticController()->updateName($id, $acc))); + $router->map("POST", "/tactic/[i:id]/save", Action::auth(fn(int $id) => getTacticController()->saveContent($id))); return $router; } diff --git a/src/Api/Controller/APITacticController.php b/src/Api/Controller/APITacticController.php index e8a1731..0edfbb3 100644 --- a/src/Api/Controller/APITacticController.php +++ b/src/Api/Controller/APITacticController.php @@ -45,4 +45,13 @@ class APITacticController { return HttpResponse::fromCode(HttpCodes::OK); }); } + + /** + * @param int $id + * @param Account $account + * @return HttpResponse + */ + public function saveContent(int $id, Account $account): HttpResponse { + return HttpResponse::fromCode(HttpCodes::OK); + } } -- 2.36.3 From 8dd6688cf85191b5e875d46e3768559af3086273 Mon Sep 17 00:00:00 2001 From: "maxime.batista" Date: Mon, 20 Nov 2023 09:20:26 +0100 Subject: [PATCH 2/6] persist player position --- front/Utils.ts | 9 +++ front/components/editor/BasketCourt.tsx | 1 + front/components/editor/CourtPlayer.tsx | 82 +++++++++++++++---------- front/views/Editor.tsx | 29 ++++----- 4 files changed, 72 insertions(+), 49 deletions(-) create mode 100644 front/Utils.ts diff --git a/front/Utils.ts b/front/Utils.ts new file mode 100644 index 0000000..5b604a0 --- /dev/null +++ b/front/Utils.ts @@ -0,0 +1,9 @@ +export function calculateRatio(it: { x: number, y: number }, parent: DOMRect): { x: number, y: number } { + const relativeXPixels = it.x - parent.x; + const relativeYPixels = it.y - parent.y; + + const xRatio = relativeXPixels / parent.width; + const yRatio = relativeYPixels / parent.height; + + return {x: xRatio, y: yRatio} +} diff --git a/front/components/editor/BasketCourt.tsx b/front/components/editor/BasketCourt.tsx index 7eab38c..4ae8c5f 100644 --- a/front/components/editor/BasketCourt.tsx +++ b/front/components/editor/BasketCourt.tsx @@ -21,6 +21,7 @@ export function BasketCourt({players, onPlayerRemove, onPlayerChange}: BasketCou player={player} onChange={onPlayerChange} onRemove={() => onPlayerRemove(player)} + parentRef={divRef} /> })}
diff --git a/front/components/editor/CourtPlayer.tsx b/front/components/editor/CourtPlayer.tsx index b8ff341..a94f957 100644 --- a/front/components/editor/CourtPlayer.tsx +++ b/front/components/editor/CourtPlayer.tsx @@ -1,52 +1,70 @@ -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 "../../tactic/Player"; +import { MutableRefObject, useEffect, useRef, useState } 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 "../../tactic/Player" +import { calculateRatio } from "../../Utils" export interface PlayerProps { player: Player, onChange: (p: Player) => void, onRemove: () => void + parentRef: MutableRefObject } /** * A player that is placed on the court, which can be selected, and moved in the associated bounds * */ +export default function CourtPlayer({ + player, + onChange, + onRemove, + parentRef, +}: PlayerProps) { + const pieceRef = useRef(null) -export default function CourtPlayer({player, onChange, onRemove}: PlayerProps) { - - const x = player.rightRatio - const y = player.bottomRatio + const [x, setX] = useState(player.rightRatio) + const [y, setY] = useState(player.bottomRatio) return ( onChange({ - id: player.id, - rightRatio: player.rightRatio, - bottomRatio: player.bottomRatio, - team: player.team, - role: player.role - })} - > -
- -
{ - if (e.key == "Delete") - onRemove() - }}> + position={{ x, y }} + onStop={() => { + const pieceBounds = pieceRef.current!.getBoundingClientRect() + const parentBounds = parentRef.current!.getBoundingClientRect() + + const { x, y } = calculateRatio(pieceBounds, parentBounds) + + setX(x) + setY(y) + + onChange({ + id: player.id, + rightRatio: x, + bottomRatio: y, + team: player.team, + role: player.role, + }) + }}> +
+
{ + if (e.key == "Delete") onRemove() + }}>
{ - return [ - ...players, - { - id: players.length, - team: element.team, - role: element.key, - rightRatio: xRatio, - bottomRatio: yRatio, - }, - ] + + const {x, y} = calculateRatio(refBounds, courtBounds) + + setPlayers(players => { + return [...players, { + id: players.length, + team: element.team, + role: element.key, + rightRatio: x, + bottomRatio: y + }] }) } -- 2.36.3 From 6731b02c6c25030f9ad78b8eb1facd1a964553e8 Mon Sep 17 00:00:00 2001 From: "maxime.batista" Date: Mon, 20 Nov 2023 09:56:05 +0100 Subject: [PATCH 3/6] add save state information in topbar --- Documentation/models.puml | 2 + front/Fetcher.ts | 15 +- front/Utils.ts | 15 +- front/components/TitleInput.tsx | 2 +- front/components/editor/BasketCourt.tsx | 43 ++--- front/components/editor/CourtPlayer.tsx | 9 +- front/components/editor/PlayerPiece.tsx | 2 +- front/components/editor/SavingState.tsx | 27 ++++ front/style/editor.css | 34 +++- front/style/title_input.css | 4 +- front/tactic/Tactic.ts | 8 +- front/views/Editor.tsx | 173 ++++++++++++++------- public/api/index.php | 2 +- sql/setup-tables.sql | 18 ++- src/Api/Controller/APITacticController.php | 7 +- src/App/Controller/EditorController.php | 7 +- src/Core/Data/TacticInfo.php | 19 ++- src/Core/Gateway/TacticInfoGateway.php | 22 ++- src/Core/Model/TacticModel.php | 9 +- 19 files changed, 292 insertions(+), 126 deletions(-) create mode 100644 front/components/editor/SavingState.tsx diff --git a/Documentation/models.puml b/Documentation/models.puml index c0629d4..86ca699 100755 --- a/Documentation/models.puml +++ b/Documentation/models.puml @@ -5,11 +5,13 @@ class TacticInfo { - name: string - creationDate: string - ownerId: string + - content: string + getId(): int + getOwnerId(): int + getCreationTimestamp(): int + getName(): string + + getContent(): string } class Account { diff --git a/front/Fetcher.ts b/front/Fetcher.ts index 59b15c8..7c81fdd 100644 --- a/front/Fetcher.ts +++ b/front/Fetcher.ts @@ -1,13 +1,16 @@ -import {API} from "./Constants"; +import { API } from "./Constants" - -export function fetchAPI(url: string, payload: object, method = "POST"): Promise { +export function fetchAPI( + url: string, + payload: object, + method = "POST", +): Promise { return fetch(`${API}/${url}`, { method, headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json' + Accept: "application/json", + "Content-Type": "application/json", }, - body: JSON.stringify(payload) + body: JSON.stringify(payload), }) } diff --git a/front/Utils.ts b/front/Utils.ts index 5b604a0..523d813 100644 --- a/front/Utils.ts +++ b/front/Utils.ts @@ -1,9 +1,12 @@ -export function calculateRatio(it: { x: number, y: number }, parent: DOMRect): { x: number, y: number } { - const relativeXPixels = it.x - parent.x; - const relativeYPixels = it.y - parent.y; +export function calculateRatio( + it: { x: number; y: number }, + parent: DOMRect, +): { x: number; y: number } { + const relativeXPixels = it.x - parent.x + const relativeYPixels = it.y - parent.y - const xRatio = relativeXPixels / parent.width; - const yRatio = relativeYPixels / parent.height; + const xRatio = relativeXPixels / parent.width + const yRatio = relativeYPixels / parent.height - return {x: xRatio, y: yRatio} + return { x: xRatio, y: yRatio } } diff --git a/front/components/TitleInput.tsx b/front/components/TitleInput.tsx index 6e4acb0..8da1c65 100644 --- a/front/components/TitleInput.tsx +++ b/front/components/TitleInput.tsx @@ -17,7 +17,7 @@ export default function TitleInput({ return ( void, + players: Player[] + onPlayerRemove: (p: Player) => void onPlayerChange: (p: Player) => void } -export function BasketCourt({players, onPlayerRemove, onPlayerChange}: BasketCourtProps) { - const divRef = useRef(null); +export function BasketCourt({ + players, + onPlayerRemove, + onPlayerChange, +}: BasketCourtProps) { + const divRef = useRef(null) return ( -
- - {players.map(player => { - return onPlayerRemove(player)} - parentRef={divRef} - /> +
+ + {players.map((player) => { + return ( + onPlayerRemove(player)} + parentRef={divRef} + /> + ) })}
) diff --git a/front/components/editor/CourtPlayer.tsx b/front/components/editor/CourtPlayer.tsx index a94f957..9f0c9e4 100644 --- a/front/components/editor/CourtPlayer.tsx +++ b/front/components/editor/CourtPlayer.tsx @@ -1,5 +1,4 @@ - -import { MutableRefObject, useEffect, useRef, useState } from "react" +import { RefObject, useRef, useState } from "react" import "../../style/player.css" import RemoveIcon from "../../assets/icon/remove.svg?react" import Draggable from "react-draggable" @@ -8,10 +7,10 @@ import { Player } from "../../tactic/Player" import { calculateRatio } from "../../Utils" export interface PlayerProps { - player: Player, - onChange: (p: Player) => void, + player: Player + onChange: (p: Player) => void onRemove: () => void - parentRef: MutableRefObject + parentRef: RefObject } /** diff --git a/front/components/editor/PlayerPiece.tsx b/front/components/editor/PlayerPiece.tsx index 08bf36d..69b38c2 100644 --- a/front/components/editor/PlayerPiece.tsx +++ b/front/components/editor/PlayerPiece.tsx @@ -1,6 +1,6 @@ import React from "react" import "../../style/player.css" -import { Team } from "../../data/Team" +import { Team } from "../../tactic/Team" export function PlayerPiece({ team, text }: { team: Team; text: string }) { return ( diff --git a/front/components/editor/SavingState.tsx b/front/components/editor/SavingState.tsx new file mode 100644 index 0000000..df03628 --- /dev/null +++ b/front/components/editor/SavingState.tsx @@ -0,0 +1,27 @@ +export interface SaveState { + className: string + message: string +} + +export class SaveStates { + static readonly Ok: SaveState = { + className: "save-state-ok", + message: "saved", + } + static readonly Saving: SaveState = { + className: "save-state-saving", + message: "saving...", + } + static readonly Err: SaveState = { + className: "save-state-error", + message: "could not save tactic.", + } +} + +export default function SavingState({ state }: { state: SaveState }) { + return ( +
+
{state.message}
+
+ ) +} diff --git a/front/style/editor.css b/front/style/editor.css index b586a36..d832fc3 100644 --- a/front/style/editor.css +++ b/front/style/editor.css @@ -9,9 +9,21 @@ flex-direction: column; } +#topbar-left { + width: 100%; + display: flex; +} + +#topbar-right { + width: 100%; + display: flex; + flex-direction: row-reverse; +} + #topbar-div { display: flex; background-color: var(--main-color); + margin-bottom: 3px; justify-content: space-between; align-items: stretch; @@ -22,8 +34,9 @@ justify-content: space-between; } -.title_input { +.title-input { width: 25ch; + align-self: center; } #edit-div { @@ -56,3 +69,22 @@ .react-draggable { z-index: 2; } + +.save-state { + display: flex; + align-items: center; + margin-left: 20%; + font-family: monospace; +} + +.save-state-error { + color: red; +} + +.save-state-ok { + color: green; +} + +.save-state-saving { + color: gray; +} diff --git a/front/style/title_input.css b/front/style/title_input.css index 1b6be10..6d28238 100644 --- a/front/style/title_input.css +++ b/front/style/title_input.css @@ -1,4 +1,4 @@ -.title_input { +.title-input { background: transparent; border-top: none; border-right: none; @@ -9,7 +9,7 @@ border-bottom-color: transparent; } -.title_input:focus { +.title-input:focus { outline: none; border-bottom-color: blueviolet; diff --git a/front/tactic/Tactic.ts b/front/tactic/Tactic.ts index cef5c2a..bb2cd37 100644 --- a/front/tactic/Tactic.ts +++ b/front/tactic/Tactic.ts @@ -1,11 +1,11 @@ -import {Player} from "./Player"; +import { Player } from "./Player" export interface Tactic { - id: number, - name: string, + id: number + name: string content: TacticContent } export interface TacticContent { players: Player[] -} \ No newline at end of file +} diff --git a/front/views/Editor.tsx b/front/views/Editor.tsx index 61f1176..8f6483c 100644 --- a/front/views/Editor.tsx +++ b/front/views/Editor.tsx @@ -1,24 +1,25 @@ -import {CSSProperties, useEffect, useRef, useState} from "react"; -import "../style/editor.css"; -import TitleInput from "../components/TitleInput"; -import {BasketCourt} from "../components/editor/BasketCourt"; +import React, { CSSProperties, useEffect, useRef, useState } from "react" +import "../style/editor.css" +import TitleInput from "../components/TitleInput" +import { BasketCourt } from "../components/editor/BasketCourt" -import {Rack} from "../components/Rack"; -import {PlayerPiece} from "../components/editor/PlayerPiece"; +import { Rack } from "../components/Rack" +import { PlayerPiece } from "../components/editor/PlayerPiece" -import {Player} from "../tactic/Player"; -import {Tactic, TacticContent} from "../tactic/Tactic"; -import {fetchAPI} from "../Fetcher"; -import {Team} from "../tactic/Team"; -import {calculateRatio} from "../Utils"; +import { Player } from "../tactic/Player" +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" const ERROR_STYLE: CSSProperties = { borderColor: "red", } export interface EditorViewProps { - tactic: Tactic, - onContentChange: (tactic: TacticContent) => Promise, + tactic: Tactic + onContentChange: (tactic: TacticContent) => Promise onNameChange: (name: string) => Promise } @@ -30,39 +31,78 @@ interface RackedPlayer { key: string } -export default function Editor({tactic}: { tactic: Tactic }) { - return ( - fetchAPI(`tactic/${tactic.id}/save`, {content}) - .then((r) => r.ok) - )} - onNameChange={(name: string) => ( - fetchAPI(`tactic/${tactic.id}/edit/name`, {name}) - .then((r) => r.ok) - )}/> +export default function Editor({ + id, + name, + content, +}: { + id: number + name: string + content: string +}) { + return ( + + fetchAPI(`tactic/${id}/save`, { content }).then((r) => r.ok) + } + onNameChange={(name: string) => + fetchAPI(`tactic/${id}/edit/name`, { name }).then((r) => r.ok) + } + /> + ) } -function EditorView({tactic: {name, content}, onContentChange, onNameChange}: EditorViewProps) { - const [style, setStyle] = useState({}); +function EditorView({ + tactic: { name, content }, + onContentChange, + onNameChange, +}: EditorViewProps) { + const [style, setStyle] = useState({}) + const [saveState, setSaveState] = useState(SaveStates.Ok) const positions = ["1", "2", "3", "4", "5"] const [allies, setAllies] = useState( - positions.map((key) => ({ team: Team.Allies, key })), + positions + .filter( + (role) => + content.players.findIndex( + (p) => p.team == Team.Allies && p.role == role, + ) == -1, + ) + .map((key) => ({ team: Team.Allies, key })), ) const [opponents, setOpponents] = useState( - positions.map((key) => ({ team: Team.Opponents, key })), + positions + .filter( + (role) => + content.players.findIndex( + (p) => p.team == Team.Opponents && p.role == role, + ) == -1, + ) + .map((key) => ({ team: Team.Opponents, key })), ) + const [players, setPlayers] = useState(content.players) + const courtDivContentRef = useRef(null) - 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(() => { - onContentChange({players}) - .then(success => { - if (!success) - alert("error when saving changes.") + 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) => { @@ -82,34 +122,45 @@ function EditorView({tactic: {name, content}, onContentChange, onNameChange}: Ed const refBounds = ref.getBoundingClientRect() const courtBounds = courtDivContentRef.current!.getBoundingClientRect() - - const {x, y} = calculateRatio(refBounds, courtBounds) - - setPlayers(players => { - return [...players, { - id: players.length, - team: element.team, - role: element.key, - rightRatio: x, - bottomRatio: y - }] + const { x, y } = calculateRatio(refBounds, courtBounds) + + setPlayers((players) => { + return [ + ...players, + { + id: players.length, + team: element.team, + role: element.key, + rightRatio: x, + bottomRatio: y, + }, + ] }) } return (
-
LEFT
- { - onNameChange(new_name).then(success => { - if (success) { - setStyle({}) - } else { - setStyle(ERROR_STYLE) - } - }) - }}/> -
RIGHT
+
+ LEFT + +
+
+ { + onNameChange(new_name).then((success) => { + if (success) { + setStyle({}) + } else { + setStyle(ERROR_STYLE) + } + }) + }} + /> +
+
RIGHT
@@ -139,14 +190,18 @@ function EditorView({tactic: {name, content}, onContentChange, onNameChange}: Ed { - setPlayers(players => { - const idx = players.indexOf(player) + setPlayers((players) => { + const idx = players.findIndex( + (p) => p.id === player.id, + ) return players.toSpliced(idx, 1, player) }) }} onPlayerRemove={(player) => { setPlayers((players) => { - const idx = players.indexOf(player) + const idx = players.findIndex( + (p) => p.id === player.id, + ) return players.toSpliced(idx, 1) }) switch (player.team) { diff --git a/public/api/index.php b/public/api/index.php index 8e1b9ef..5734571 100644 --- a/public/api/index.php +++ b/public/api/index.php @@ -31,7 +31,7 @@ function getRoutes(): AltoRouter { $router->map("POST", "/auth", Action::noAuth(fn() => getAuthController()->authorize())); $router->map("POST", "/tactic/[i:id]/edit/name", Action::auth(fn(int $id, Account $acc) => getTacticController()->updateName($id, $acc))); - $router->map("POST", "/tactic/[i:id]/save", Action::auth(fn(int $id) => getTacticController()->saveContent($id))); + $router->map("POST", "/tactic/[i:id]/save", Action::auth(fn(int $id, Account $acc) => getTacticController()->saveContent($id, $acc))); return $router; } diff --git a/sql/setup-tables.sql b/sql/setup-tables.sql index 324fb39..5a2eecf 100644 --- a/sql/setup-tables.sql +++ b/sql/setup-tables.sql @@ -19,26 +19,32 @@ CREATE TABLE Tactic name varchar NOT NULL, creation_date timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL, owner integer NOT NULL, + content varchar DEFAULT '{"players": []}' NOT NULL, FOREIGN KEY (owner) REFERENCES Account ); -CREATE TABLE FormEntries(name varchar, description varchar); +CREATE TABLE FormEntries +( + name varchar, + description varchar +); CREATE TABLE Team ( - id integer PRIMARY KEY AUTOINCREMENT, - name varchar, - picture varchar, + id integer PRIMARY KEY AUTOINCREMENT, + name varchar, + picture varchar, main_color varchar, second_color varchar ); -CREATE TABLE Member( +CREATE TABLE Member +( id_team integer, id_user integer, - role char(1) CHECK (role IN ('Coach', 'Player')), + role char(1) 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 0edfbb3..606e49b 100644 --- a/src/Api/Controller/APITacticController.php +++ b/src/Api/Controller/APITacticController.php @@ -52,6 +52,11 @@ class APITacticController { * @return HttpResponse */ public function saveContent(int $id, Account $account): HttpResponse { - return HttpResponse::fromCode(HttpCodes::OK); + return Control::runChecked([ + "content" => [], + ], function (HttpRequest $req) use ($id) { + $this->model->updateContent($id, json_encode($req["content"])); + return HttpResponse::fromCode(HttpCodes::OK); + }); } } diff --git a/src/App/Controller/EditorController.php b/src/App/Controller/EditorController.php index bba3214..d2a2bc4 100644 --- a/src/App/Controller/EditorController.php +++ b/src/App/Controller/EditorController.php @@ -8,7 +8,6 @@ use IQBall\App\ViewHttpResponse; use IQBall\Core\Data\TacticInfo; use IQBall\Core\Http\HttpCodes; use IQBall\Core\Model\TacticModel; -use IQBall\Core\Validation\ValidationFail; class EditorController { private TacticModel $model; @@ -22,7 +21,11 @@ class EditorController { * @return ViewHttpResponse the editor view for given tactic */ private function openEditorFor(TacticInfo $tactic): ViewHttpResponse { - return ViewHttpResponse::react("views/Editor.tsx", ["name" => $tactic->getName(), "id" => $tactic->getId()]); + return ViewHttpResponse::react("views/Editor.tsx", [ + "id" => $tactic->getId(), + "name" => $tactic->getName(), + "content" => $tactic->getContent(), + ]); } /** diff --git a/src/Core/Data/TacticInfo.php b/src/Core/Data/TacticInfo.php index 04f592a..2565f93 100644 --- a/src/Core/Data/TacticInfo.php +++ b/src/Core/Data/TacticInfo.php @@ -8,17 +8,28 @@ class TacticInfo { private int $creationDate; private int $ownerId; + private string $content; + /** * @param int $id * @param string $name * @param int $creationDate * @param int $ownerId + * @param string $content */ - public function __construct(int $id, string $name, int $creationDate, int $ownerId) { + public function __construct(int $id, string $name, int $creationDate, int $ownerId, string $content) { $this->id = $id; $this->name = $name; $this->ownerId = $ownerId; $this->creationDate = $creationDate; + $this->content = $content; + } + + /** + * @return string + */ + public function getContent(): string { + return $this->content; } public function getId(): int { @@ -36,8 +47,10 @@ class TacticInfo { return $this->ownerId; } - public function getCreationTimestamp(): int { + /** + * @return int + */ + public function getCreationDate(): int { return $this->creationDate; } - } diff --git a/src/Core/Gateway/TacticInfoGateway.php b/src/Core/Gateway/TacticInfoGateway.php index f3d2432..3e0a0d0 100644 --- a/src/Core/Gateway/TacticInfoGateway.php +++ b/src/Core/Gateway/TacticInfoGateway.php @@ -33,7 +33,7 @@ class TacticInfoGateway { $row = $res[0]; - return new TacticInfo($id, $row["name"], strtotime($row["creation_date"]), $row["owner"]); + return new TacticInfo($id, $row["name"], strtotime($row["creation_date"]), $row["owner"], $row['content']); } @@ -57,9 +57,9 @@ class TacticInfoGateway { /** * @param string $name * @param int $owner - * @return TacticInfo + * @return int inserted tactic id */ - public function insert(string $name, int $owner): TacticInfo { + public function insert(string $name, int $owner): int { $this->con->exec( "INSERT INTO Tactic(name, owner) VALUES(:name, :owner)", [ @@ -67,11 +67,7 @@ class TacticInfoGateway { ":owner" => [$owner, PDO::PARAM_INT], ] ); - $row = $this->con->fetch( - "SELECT id, creation_date, owner FROM Tactic WHERE :id = id", - [':id' => [$this->con->lastInsertId(), PDO::PARAM_INT]] - )[0]; - return new TacticInfo(intval($row["id"]), $name, strtotime($row["creation_date"]), $row["owner"]); + return intval($this->con->lastInsertId()); } /** @@ -90,4 +86,14 @@ class TacticInfoGateway { ); } + 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], + ] + ); + } + } diff --git a/src/Core/Model/TacticModel.php b/src/Core/Model/TacticModel.php index 64c6ca3..0560568 100644 --- a/src/Core/Model/TacticModel.php +++ b/src/Core/Model/TacticModel.php @@ -2,9 +2,9 @@ namespace IQBall\Core\Model; +use IQBall\Core\Data\TacticInfo; use IQBall\Core\Gateway\TacticInfoGateway; use IQBall\Core\Validation\ValidationFail; -use IQBall\Core\Data\TacticInfo; class TacticModel { public const TACTIC_DEFAULT_NAME = "Nouvelle tactique"; @@ -26,7 +26,8 @@ class TacticModel { * @return TacticInfo */ public function makeNew(string $name, int $ownerId): TacticInfo { - return $this->tactics->insert($name, $ownerId); + $id = $this->tactics->insert($name, $ownerId); + return $this->tactics->get($id); } /** @@ -79,4 +80,8 @@ class TacticModel { return []; } + public function updateContent(int $id, string $json): void { + $this->tactics->updateContent($id, $json); + } + } -- 2.36.3 From 44513a50499b22337f2b51f898ebe072e1058fa1 Mon Sep 17 00:00:00 2001 From: Override-6 Date: Sun, 26 Nov 2023 14:09:02 +0100 Subject: [PATCH 4/6] fix player keys in BasketCourt --- front/components/editor/BasketCourt.tsx | 2 +- front/components/editor/CourtPlayer.tsx | 1 - front/tactic/Player.ts | 6 ------ front/views/Editor.tsx | 15 +++++++-------- 4 files changed, 8 insertions(+), 16 deletions(-) diff --git a/front/components/editor/BasketCourt.tsx b/front/components/editor/BasketCourt.tsx index 92722c7..115c41b 100644 --- a/front/components/editor/BasketCourt.tsx +++ b/front/components/editor/BasketCourt.tsx @@ -23,7 +23,7 @@ export function BasketCourt({ {players.map((player) => { return ( onPlayerRemove(player)} diff --git a/front/components/editor/CourtPlayer.tsx b/front/components/editor/CourtPlayer.tsx index 9f0c9e4..22e1bbe 100644 --- a/front/components/editor/CourtPlayer.tsx +++ b/front/components/editor/CourtPlayer.tsx @@ -43,7 +43,6 @@ export default function CourtPlayer({ setY(y) onChange({ - id: player.id, rightRatio: x, bottomRatio: y, team: player.team, diff --git a/front/tactic/Player.ts b/front/tactic/Player.ts index e7553a8..6530612 100644 --- a/front/tactic/Player.ts +++ b/front/tactic/Player.ts @@ -1,12 +1,6 @@ import { Team } from "./Team" export interface Player { - /** - * unique identifier of the player. - * This identifier must be unique to the associated court. - */ - id: number - /** * the player's team * */ diff --git a/front/views/Editor.tsx b/front/views/Editor.tsx index 8f6483c..df566ff 100644 --- a/front/views/Editor.tsx +++ b/front/views/Editor.tsx @@ -128,7 +128,6 @@ function EditorView({ return [ ...players, { - id: players.length, team: element.team, role: element.key, rightRatio: x, @@ -151,11 +150,7 @@ function EditorView({ default_value={name} on_validated={(new_name) => { onNameChange(new_name).then((success) => { - if (success) { - setStyle({}) - } else { - setStyle(ERROR_STYLE) - } + success ? setStyle({}) : setStyle(ERROR_STYLE) }) }} /> @@ -192,7 +187,9 @@ function EditorView({ onPlayerChange={(player) => { setPlayers((players) => { const idx = players.findIndex( - (p) => p.id === player.id, + (p) => + p.team === player.team && + p.role === player.role, ) return players.toSpliced(idx, 1, player) }) @@ -200,7 +197,9 @@ function EditorView({ onPlayerRemove={(player) => { setPlayers((players) => { const idx = players.findIndex( - (p) => p.id === player.id, + (p) => + p.team === player.team && + p.role === player.role, ) return players.toSpliced(idx, 1) }) -- 2.36.3 From cb24dd53a9fcaba4afbc30bbb21e7dee475ef495 Mon Sep 17 00:00:00 2001 From: "maxime.batista" Date: Tue, 28 Nov 2023 16:52:30 +0100 Subject: [PATCH 5/6] Apply suggestions --- front/Fetcher.ts | 2 +- front/views/Editor.tsx | 193 ++++++++++++--------- sql/setup-tables.sql | 2 +- src/Api/Controller/APITacticController.php | 5 +- src/Core/Connection.php | 10 +- src/Core/Gateway/TacticInfoGateway.php | 38 ++-- src/Core/Model/TacticModel.php | 11 +- src/Core/Validation/ValidationFail.php | 4 + 8 files changed, 154 insertions(+), 111 deletions(-) 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); + } + } -- 2.36.3 From 6ebf3737af246a69f3ff687812bb0e4308788df6 Mon Sep 17 00:00:00 2001 From: Override-6 Date: Tue, 28 Nov 2023 20:04:01 +0100 Subject: [PATCH 6/6] apply suggestions --- front/components/editor/CourtPlayer.tsx | 7 ++----- front/views/Editor.tsx | 4 ++-- src/Api/Controller/APITacticController.php | 2 +- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/front/components/editor/CourtPlayer.tsx b/front/components/editor/CourtPlayer.tsx index 22e1bbe..12083d3 100644 --- a/front/components/editor/CourtPlayer.tsx +++ b/front/components/editor/CourtPlayer.tsx @@ -24,8 +24,8 @@ export default function CourtPlayer({ }: PlayerProps) { const pieceRef = useRef(null) - const [x, setX] = useState(player.rightRatio) - const [y, setY] = useState(player.bottomRatio) + const x = player.rightRatio + const y = player.bottomRatio return ( { onNameChange(new_name).then((success) => { - success ? setStyle({}) : setStyle(ERROR_STYLE) + setStyle(success ? {} : ERROR_STYLE) }) }} /> @@ -239,7 +239,7 @@ function useContentState( } return state }) - }, []) + }, [saveStateCallback]) return [content, setContentSynced, savingState] } diff --git a/src/Api/Controller/APITacticController.php b/src/Api/Controller/APITacticController.php index 51ca531..79e766c 100644 --- a/src/Api/Controller/APITacticController.php +++ b/src/Api/Controller/APITacticController.php @@ -57,7 +57,7 @@ class APITacticController { "content" => [], ], function (HttpRequest $req) use ($id) { if ($fail = $this->model->updateContent($id, json_encode($req["content"]))) { - return new JsonHttpResponse([$fail]); + return new JsonHttpResponse([$fail], HttpCodes::BAD_REQUEST); } return HttpResponse::fromCode(HttpCodes::OK); }); -- 2.36.3