Apply suggestions
continuous-integration/drone/push Build is passing Details

pull/23/head
maxime.batista 1 year ago
parent 44513a5049
commit cb24dd53a9

@ -2,7 +2,7 @@ import { API } from "./Constants"
export function fetchAPI( export function fetchAPI(
url: string, url: string,
payload: object, payload: unknown,
method = "POST", method = "POST",
): Promise<Response> { ): Promise<Response> {
return fetch(`${API}/${url}`, { return fetch(`${API}/${url}`, {

@ -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 "../style/editor.css"
import TitleInput from "../components/TitleInput" import TitleInput from "../components/TitleInput"
import { BasketCourt } from "../components/editor/BasketCourt" import { BasketCourt } from "../components/editor/BasketCourt"
@ -11,7 +18,10 @@ import { Tactic, TacticContent } from "../tactic/Tactic"
import { fetchAPI } from "../Fetcher" import { fetchAPI } from "../Fetcher"
import { Team } from "../tactic/Team" import { Team } from "../tactic/Team"
import { calculateRatio } from "../Utils" import { calculateRatio } from "../Utils"
import SavingState, { SaveStates } from "../components/editor/SavingState" import SavingState, {
SaveState,
SaveStates,
} from "../components/editor/SavingState"
const ERROR_STYLE: CSSProperties = { const ERROR_STYLE: CSSProperties = {
borderColor: "red", borderColor: "red",
@ -54,57 +64,27 @@ export default function Editor({
} }
function EditorView({ function EditorView({
tactic: { name, content }, tactic: { name, content: initialContent },
onContentChange, onContentChange,
onNameChange, onNameChange,
}: EditorViewProps) { }: EditorViewProps) {
const [style, setStyle] = useState<CSSProperties>({}) const [style, setStyle] = useState<CSSProperties>({})
const [saveState, setSaveState] = useState(SaveStates.Ok) const [content, setContent, saveState] = useContentState(
initialContent,
const positions = ["1", "2", "3", "4", "5"] (content) =>
onContentChange(content).then((success) =>
success ? SaveStates.Ok : SaveStates.Err,
),
)
const [allies, setAllies] = useState( const [allies, setAllies] = useState(
positions getRackPlayers(Team.Allies, content.players),
.filter(
(role) =>
content.players.findIndex(
(p) => p.team == Team.Allies && p.role == role,
) == -1,
)
.map((key) => ({ team: Team.Allies, key })),
) )
const [opponents, setOpponents] = useState( const [opponents, setOpponents] = useState(
positions getRackPlayers(Team.Opponents, content.players),
.filter(
(role) =>
content.players.findIndex(
(p) => p.team == Team.Opponents && p.role == role,
) == -1,
)
.map((key) => ({ team: Team.Opponents, key })),
) )
const [players, setPlayers] = useState<Player[]>(content.players)
const courtDivContentRef = useRef<HTMLDivElement>(null) const courtDivContentRef = useRef<HTMLDivElement>(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 canDetach = (ref: HTMLDivElement) => {
const refBounds = ref.getBoundingClientRect() const refBounds = ref.getBoundingClientRect()
const courtBounds = courtDivContentRef.current!.getBoundingClientRect() const courtBounds = courtDivContentRef.current!.getBoundingClientRect()
@ -124,16 +104,18 @@ function EditorView({
const { x, y } = calculateRatio(refBounds, courtBounds) const { x, y } = calculateRatio(refBounds, courtBounds)
setPlayers((players) => { setContent((content) => {
return [ return {
...players, players: [
{ ...content.players,
team: element.team, {
role: element.key, team: element.team,
rightRatio: x, role: element.key,
bottomRatio: y, rightRatio: x,
}, bottomRatio: y,
] },
],
}
}) })
} }
@ -183,47 +165,40 @@ function EditorView({
<div id="court-div"> <div id="court-div">
<div id="court-div-bounds" ref={courtDivContentRef}> <div id="court-div-bounds" ref={courtDivContentRef}>
<BasketCourt <BasketCourt
players={players} players={content.players}
onPlayerChange={(player) => { onPlayerChange={(player) => {
setPlayers((players) => { setContent((content) => ({
const idx = players.findIndex( players: toSplicedPlayers(
(p) => content.players,
p.team === player.team && player,
p.role === player.role, true,
) ),
return players.toSpliced(idx, 1, player) }))
})
}} }}
onPlayerRemove={(player) => { onPlayerRemove={(player) => {
setPlayers((players) => { setContent((content) => ({
const idx = players.findIndex( players: toSplicedPlayers(
(p) => content.players,
p.team === player.team && player,
p.role === player.role, false,
) ),
return players.toSpliced(idx, 1) }))
}) let setter
switch (player.team) { switch (player.team) {
case Team.Opponents: case Team.Opponents:
setOpponents((opponents) => [ setter = setOpponents
...opponents,
{
team: player.team,
pos: player.role,
key: player.role,
},
])
break break
case Team.Allies: case Team.Allies:
setAllies((allies) => [ setter = setAllies
...allies,
{
team: player.team,
pos: player.role,
key: player.role,
},
])
} }
setter((players) => [
...players,
{
team: player.team,
pos: player.role,
key: player.role,
},
])
}} }}
/> />
</div> </div>
@ -232,3 +207,51 @@ function EditorView({
</div> </div>
) )
} }
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<S>(
initialContent: S,
saveStateCallback: (s: S) => Promise<SaveState>,
): [S, Dispatch<SetStateAction<S>>, SaveState] {
const [content, setContent] = useState(initialContent)
const [savingState, setSavingState] = useState(SaveStates.Ok)
const setContentSynced = useCallback((newState: SetStateAction<S>) => {
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] : []))
}

@ -44,7 +44,7 @@ CREATE TABLE Member
( (
id_team integer, id_team integer,
id_user 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_team) REFERENCES Team (id),
FOREIGN KEY (id_user) REFERENCES User (id) FOREIGN KEY (id_user) REFERENCES User (id)
); );

@ -9,6 +9,7 @@ use IQBall\Core\Http\HttpRequest;
use IQBall\Core\Http\HttpResponse; use IQBall\Core\Http\HttpResponse;
use IQBall\Core\Http\JsonHttpResponse; use IQBall\Core\Http\JsonHttpResponse;
use IQBall\Core\Model\TacticModel; use IQBall\Core\Model\TacticModel;
use IQBall\Core\Validation\FieldValidationFail;
use IQBall\Core\Validation\Validators; use IQBall\Core\Validation\Validators;
/** /**
@ -55,7 +56,9 @@ class APITacticController {
return Control::runChecked([ return Control::runChecked([
"content" => [], "content" => [],
], function (HttpRequest $req) use ($id) { ], 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); return HttpResponse::fromCode(HttpCodes::OK);
}); });
} }

@ -25,7 +25,7 @@ class Connection {
* @return void * @return void
*/ */
public function exec(string $query, array $args) { public function exec(string $query, array $args) {
$stmnt = $this->prepare($query, $args); $stmnt = $this->prep($query, $args);
$stmnt->execute(); $stmnt->execute();
} }
@ -36,7 +36,7 @@ class Connection {
* @return array<string, mixed>[] the returned rows of the request * @return array<string, mixed>[] the returned rows of the request
*/ */
public function fetch(string $query, array $args): array { public function fetch(string $query, array $args): array {
$stmnt = $this->prepare($query, $args); $stmnt = $this->prep($query, $args);
$stmnt->execute(); $stmnt->execute();
return $stmnt->fetchAll(PDO::FETCH_ASSOC); return $stmnt->fetchAll(PDO::FETCH_ASSOC);
} }
@ -46,7 +46,7 @@ class Connection {
* @param array<string, array<mixed, int>> $args * @param array<string, array<mixed, int>> $args
* @return \PDOStatement * @return \PDOStatement
*/ */
private function prepare(string $query, array $args): \PDOStatement { private function prep(string $query, array $args): \PDOStatement {
$stmnt = $this->pdo->prepare($query); $stmnt = $this->pdo->prepare($query);
foreach ($args as $name => $value) { foreach ($args as $name => $value) {
$stmnt->bindValue($name, $value[0], $value[1]); $stmnt->bindValue($name, $value[0], $value[1]);
@ -54,4 +54,8 @@ class Connection {
return $stmnt; return $stmnt;
} }
public function prepare(string $query): \PDOStatement {
return $this->pdo->prepare($query);
}
} }

@ -74,26 +74,30 @@ class TacticInfoGateway {
* update name of given tactic identifier * update name of given tactic identifier
* @param int $id * @param int $id
* @param string $name * @param string $name
* @return void * @return bool
*/ */
public function updateName(int $id, string $name): void { public function updateName(int $id, string $name): bool {
$this->con->exec( $stmnt = $this->con->prepare("UPDATE Tactic SET name = :name WHERE id = :id");
"UPDATE Tactic SET name = :name WHERE id = :id", $stmnt->execute([
[ ":name" => $name,
":name" => [$name, PDO::PARAM_STR], ":id" => $id,
":id" => [$id, PDO::PARAM_INT], ]);
] return $stmnt->rowCount() == 1;
);
} }
public function updateContent(int $id, string $json): void { /***
$this->con->exec( * Updates a given tactics content
"UPDATE Tactic SET content = :content WHERE id = :id", * @param int $id
[ * @param string $json
":content" => [$json, PDO::PARAM_STR], * @return bool
":id" => [$id, PDO::PARAM_INT], */
] 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;
} }
} }

@ -76,12 +76,17 @@ class TacticModel {
return [ValidationFail::unauthorized()]; return [ValidationFail::unauthorized()];
} }
$this->tactics->updateName($id, $name); if (!$this->tactics->updateName($id, $name)) {
return [ValidationFail::error("Could not update name")];
}
return []; return [];
} }
public function updateContent(int $id, string $json): void { public function updateContent(int $id, string $json): ?ValidationFail {
$this->tactics->updateContent($id, $json); if (!$this->tactics->updateContent($id, $json)) {
return ValidationFail::error("Could not update content");
}
return null;
} }
} }

@ -49,4 +49,8 @@ class ValidationFail implements JsonSerializable {
return new ValidationFail("Unauthorized", $message); return new ValidationFail("Unauthorized", $message);
} }
public static function error(string $message): ValidationFail {
return new ValidationFail("Error", $message);
}
} }

Loading…
Cancel
Save