Persist tactics #23

Merged
maxime.batista merged 6 commits from tactic/persistance into master 1 year ago

@ -5,11 +5,13 @@ class TacticInfo {
- name: string - name: string
- creationDate: string - creationDate: string
- ownerId: string - ownerId: string
- content: string
+ getId(): int + getId(): int
+ getOwnerId(): int + getOwnerId(): int
+ getCreationTimestamp(): int + getCreationTimestamp(): int
+ getName(): string + getName(): string
+ getContent(): string
} }
class Account { class Account {

@ -0,0 +1,16 @@
import { API } from "./Constants"
export function fetchAPI(
url: string,
payload: unknown,
method = "POST",
): Promise<Response> {
return fetch(`${API}/${url}`, {
method,
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
})
}

@ -0,0 +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
const xRatio = relativeXPixels / parent.width
const yRatio = relativeYPixels / parent.height
return { x: xRatio, y: yRatio }
}

@ -17,7 +17,7 @@ export default function TitleInput({
return ( return (
<input <input
className="title_input" className="title-input"
ref={ref} ref={ref}
style={style} style={style}
type="text" type="text"

@ -2,23 +2,32 @@ import CourtSvg from "../../assets/basketball_court.svg?react"
import "../../style/basket_court.css" import "../../style/basket_court.css"
import { useRef } from "react" import { useRef } from "react"
import CourtPlayer from "./CourtPlayer" import CourtPlayer from "./CourtPlayer"
import { Player } from "../../data/Player" import { Player } from "../../tactic/Player"
export interface BasketCourtProps { export interface BasketCourtProps {
players: Player[] players: Player[]
onPlayerRemove: (p: Player) => void onPlayerRemove: (p: Player) => void
onPlayerChange: (p: Player) => void
} }
export function BasketCourt({ players, onPlayerRemove }: BasketCourtProps) { export function BasketCourt({
players,
onPlayerRemove,
onPlayerChange,
}: BasketCourtProps) {
const divRef = useRef<HTMLDivElement>(null)
return ( return (
<div id="court-container" style={{ position: "relative" }}> <div id="court-container" ref={divRef} style={{ position: "relative" }}>
<CourtSvg id="court-svg" /> <CourtSvg id="court-svg" />
{players.map((player) => { {players.map((player) => {
return ( return (
<CourtPlayer <CourtPlayer
key={player.id} key={player.team + player.role}
player={player} player={player}
onChange={onPlayerChange}
onRemove={() => onPlayerRemove(player)} onRemove={() => onPlayerRemove(player)}
parentRef={divRef}
/> />
) )
})} })}

@ -1,28 +1,53 @@
import { useRef } from "react" import { RefObject, useRef, useState } from "react"
import "../../style/player.css" import "../../style/player.css"
import RemoveIcon from "../../assets/icon/remove.svg?react" import RemoveIcon from "../../assets/icon/remove.svg?react"
import Draggable from "react-draggable" import Draggable from "react-draggable"
import { PlayerPiece } from "./PlayerPiece" import { PlayerPiece } from "./PlayerPiece"
import { Player } from "../../data/Player" import { Player } from "../../tactic/Player"
import { calculateRatio } from "../../Utils"
export interface PlayerProps { export interface PlayerProps {
player: Player player: Player
onChange: (p: Player) => void
onRemove: () => void onRemove: () => void
parentRef: RefObject<HTMLDivElement>
} }
/** /**
* A player that is placed on the court, which can be selected, and moved in the associated bounds * 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({
const ref = useRef<HTMLDivElement>(null) player,
onChange,
onRemove,
parentRef,
}: PlayerProps) {
const pieceRef = useRef<HTMLDivElement>(null)
const x = player.rightRatio const x = player.rightRatio
const y = player.bottomRatio const y = player.bottomRatio
return ( return (
<Draggable handle={".player-piece"} nodeRef={ref} bounds="parent"> <Draggable
handle={".player-piece"}
nodeRef={pieceRef}
bounds="parent"
position={{ x, y }}
onStop={() => {
const pieceBounds = pieceRef.current!.getBoundingClientRect()
const parentBounds = parentRef.current!.getBoundingClientRect()
const { x, y } = calculateRatio(pieceBounds, parentBounds)
onChange({
rightRatio: x,
maxime.batista marked this conversation as resolved
Review
-                 setX(x)
-                 setY(y)
```diff - setX(x) - setY(y) ```
bottomRatio: y,
team: player.team,
role: player.role,
})
}}>
<div <div
ref={ref} ref={pieceRef}
className={"player"} className={"player"}
style={{ style={{
position: "absolute", position: "absolute",

@ -1,6 +1,6 @@
import React from "react" import React from "react"
import "../../style/player.css" import "../../style/player.css"
import { Team } from "../../data/Team" import { Team } from "../../tactic/Team"
export function PlayerPiece({ team, text }: { team: Team; text: string }) { export function PlayerPiece({ team, text }: { team: Team; text: string }) {
return ( return (

@ -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 (
<div className={"save-state"}>
<div className={state.className}>{state.message}</div>
</div>
)
}

@ -9,9 +9,21 @@
flex-direction: column; flex-direction: column;
} }
#topbar-left {
width: 100%;
display: flex;
}
#topbar-right {
width: 100%;
display: flex;
flex-direction: row-reverse;
}
#topbar-div { #topbar-div {
display: flex; display: flex;
background-color: var(--main-color); background-color: var(--main-color);
margin-bottom: 3px;
justify-content: space-between; justify-content: space-between;
align-items: stretch; align-items: stretch;
@ -22,8 +34,9 @@
justify-content: space-between; justify-content: space-between;
} }
.title_input { .title-input {
width: 25ch; width: 25ch;
align-self: center;
} }
#edit-div { #edit-div {
@ -56,3 +69,22 @@
.react-draggable { .react-draggable {
z-index: 2; 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;
}

@ -1,4 +1,4 @@
.title_input { .title-input {
background: transparent; background: transparent;
border-top: none; border-top: none;
border-right: none; border-right: none;
@ -9,7 +9,7 @@
border-bottom-color: transparent; border-bottom-color: transparent;
} }
.title_input:focus { .title-input:focus {
outline: none; outline: none;
border-bottom-color: blueviolet; border-bottom-color: blueviolet;

@ -1,19 +1,13 @@
import { Team } from "./Team" import { Team } from "./Team"
export interface Player { export interface Player {
/**
* unique identifier of the player.
* This identifier must be unique to the associated court.
*/
id: number
/** /**
* the player's team * the player's team
* */ * */
team: Team team: Team
/** /**
* player's position * player's role
* */ * */
role: string role: string

@ -0,0 +1,11 @@
import { Player } from "./Player"
export interface Tactic {
id: number
name: string
content: TacticContent
}
export interface TacticContent {
players: Player[]
}

@ -1,18 +1,38 @@
import { CSSProperties, 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 { API } from "../Constants"
import { BasketCourt } from "../components/editor/BasketCourt" import { BasketCourt } from "../components/editor/BasketCourt"
import { Rack } from "../components/Rack" import { Rack } from "../components/Rack"
import { PlayerPiece } from "../components/editor/PlayerPiece" import { PlayerPiece } from "../components/editor/PlayerPiece"
import { Player } from "../data/Player"
import { Team } from "../data/Team" 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, {
SaveState,
SaveStates,
} from "../components/editor/SavingState"
const ERROR_STYLE: CSSProperties = { const ERROR_STYLE: CSSProperties = {
borderColor: "red", borderColor: "red",
} }
export interface EditorViewProps {
tactic: Tactic
onContentChange: (tactic: TacticContent) => Promise<boolean>
onNameChange: (name: string) => Promise<boolean>
}
/** /**
* information about a player that is into a rack * information about a player that is into a rack
*/ */
@ -21,17 +41,48 @@ interface RackedPlayer {
key: string key: string
} }
export default function Editor({ id, name }: { id: number; name: string }) { export default function Editor({
id,
name,
content,
}: {
id: number
name: string
content: string
}) {
return (
<EditorView
tactic={{ name, id, content: JSON.parse(content) }}
onContentChange={(content: TacticContent) =>
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: initialContent },
onContentChange,
onNameChange,
}: EditorViewProps) {
const [style, setStyle] = useState<CSSProperties>({}) const [style, setStyle] = useState<CSSProperties>({})
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( const [allies, setAllies] = useState(
positions.map((key) => ({ team: Team.Allies, key })), getRackPlayers(Team.Allies, content.players),
) )
const [opponents, setOpponents] = useState( const [opponents, setOpponents] = useState(
positions.map((key) => ({ team: Team.Opponents, key })), getRackPlayers(Team.Opponents, content.players),
) )
const [players, setPlayers] = useState<Player[]>([])
const courtDivContentRef = useRef<HTMLDivElement>(null) const courtDivContentRef = useRef<HTMLDivElement>(null)
const canDetach = (ref: HTMLDivElement) => { const canDetach = (ref: HTMLDivElement) => {
@ -51,53 +102,42 @@ export default function Editor({ id, name }: { id: number; name: string }) {
const refBounds = ref.getBoundingClientRect() const refBounds = ref.getBoundingClientRect()
const courtBounds = courtDivContentRef.current!.getBoundingClientRect() const courtBounds = courtDivContentRef.current!.getBoundingClientRect()
const relativeXPixels = refBounds.x - courtBounds.x const { x, y } = calculateRatio(refBounds, courtBounds)
const relativeYPixels = refBounds.y - courtBounds.y
const xRatio = relativeXPixels / courtBounds.width setContent((content) => {
const yRatio = relativeYPixels / courtBounds.height return {
players: [
setPlayers((players) => { ...content.players,
return [
...players,
{ {
id: players.length,
team: element.team, team: element.team,
role: element.key, role: element.key,
rightRatio: xRatio, rightRatio: x,
bottomRatio: yRatio, bottomRatio: y,
}, },
] ],
}
}) })
} }
return ( return (
<div id="main-div"> <div id="main-div">
<div id="topbar-div"> <div id="topbar-div">
<div>LEFT</div> <div id="topbar-left">
LEFT
<SavingState state={saveState} />
</div>
<div id="title-input-div">
<TitleInput <TitleInput
style={style} style={style}
default_value={name} default_value={name}
on_validated={(new_name) => { on_validated={(new_name) => {
fetch(`${API}/tactic/${id}/edit/name`, { onNameChange(new_name).then((success) => {
method: "POST", setStyle(success ? {} : ERROR_STYLE)
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({
name: new_name,
}),
}).then((response) => {
if (response.ok) {
setStyle({})
} else {
setStyle(ERROR_STYLE)
}
}) })
}} }}
/> />
<div>RIGHT</div> </div>
<div id="topbar-right">RIGHT</div>
</div> </div>
<div id="edit-div"> <div id="edit-div">
<div id="racks"> <div id="racks">
@ -125,33 +165,40 @@ export default function Editor({ id, name }: { id: number; name: string }) {
<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) => {
setContent((content) => ({
players: toSplicedPlayers(
content.players,
player,
true,
),
}))
}}
onPlayerRemove={(player) => { onPlayerRemove={(player) => {
setPlayers((players) => { setContent((content) => ({
const idx = players.indexOf(player) players: toSplicedPlayers(
return players.toSpliced(idx, 1) content.players,
}) player,
false,
),
}))
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, }
setter((players) => [
...players,
{ {
team: player.team, team: player.team,
pos: player.role, pos: player.role,
key: player.role, key: player.role,
}, },
]) ])
}
}} }}
/> />
</div> </div>
@ -160,3 +207,51 @@ export default function Editor({ id, name }: { id: number; name: string }) {
</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
})
}, [saveStateCallback])
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] : []))
}

@ -29,8 +29,9 @@ function getRoutes(): AltoRouter {
$router = new AltoRouter(); $router = new AltoRouter();
$router->setBasePath(get_public_path(__DIR__)); $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", "/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, Account $acc) => getTacticController()->saveContent($id, $acc)));
return $router; return $router;
} }

@ -19,10 +19,15 @@ CREATE TABLE Tactic
name varchar NOT NULL, name varchar NOT NULL,
creation_date timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL, creation_date timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL,
owner integer NOT NULL, owner integer NOT NULL,
content varchar DEFAULT '{"players": []}' NOT NULL,
FOREIGN KEY (owner) REFERENCES Account FOREIGN KEY (owner) REFERENCES Account
); );
CREATE TABLE FormEntries(name varchar, description varchar); CREATE TABLE FormEntries
(
name varchar,
description varchar
);
CREATE TABLE Team CREATE TABLE Team
@ -35,10 +40,11 @@ CREATE TABLE Team
); );
CREATE TABLE Member( 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;
/** /**
@ -45,4 +46,20 @@ class APITacticController {
return HttpResponse::fromCode(HttpCodes::OK); return HttpResponse::fromCode(HttpCodes::OK);
}); });
} }
/**
* @param int $id
* @param Account $account
* @return HttpResponse
*/
public function saveContent(int $id, Account $account): HttpResponse {
return Control::runChecked([
"content" => [],
], function (HttpRequest $req) use ($id) {
if ($fail = $this->model->updateContent($id, json_encode($req["content"]))) {
return new JsonHttpResponse([$fail], HttpCodes::BAD_REQUEST);
}
return HttpResponse::fromCode(HttpCodes::OK);
});
}
} }

@ -8,7 +8,6 @@ use IQBall\App\ViewHttpResponse;
use IQBall\Core\Data\TacticInfo; use IQBall\Core\Data\TacticInfo;
use IQBall\Core\Http\HttpCodes; use IQBall\Core\Http\HttpCodes;
use IQBall\Core\Model\TacticModel; use IQBall\Core\Model\TacticModel;
use IQBall\Core\Validation\ValidationFail;
class EditorController { class EditorController {
private TacticModel $model; private TacticModel $model;
@ -22,7 +21,11 @@ class EditorController {
* @return ViewHttpResponse the editor view for given tactic * @return ViewHttpResponse the editor view for given tactic
*/ */
private function openEditorFor(TacticInfo $tactic): ViewHttpResponse { 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(),
]);
} }
/** /**

@ -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);
}
} }

@ -8,17 +8,28 @@ class TacticInfo {
private int $creationDate; private int $creationDate;
private int $ownerId; private int $ownerId;
private string $content;
/** /**
* @param int $id * @param int $id
* @param string $name * @param string $name
* @param int $creationDate * @param int $creationDate
* @param int $ownerId * @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->id = $id;
$this->name = $name; $this->name = $name;
$this->ownerId = $ownerId; $this->ownerId = $ownerId;
$this->creationDate = $creationDate; $this->creationDate = $creationDate;
$this->content = $content;
}
/**
* @return string
*/
public function getContent(): string {
return $this->content;
} }
public function getId(): int { public function getId(): int {
@ -36,8 +47,10 @@ class TacticInfo {
return $this->ownerId; return $this->ownerId;
} }
public function getCreationTimestamp(): int { /**
* @return int
*/
public function getCreationDate(): int {
return $this->creationDate; return $this->creationDate;
} }
} }

@ -33,7 +33,7 @@ class TacticInfoGateway {
$row = $res[0]; $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 string $name
* @param int $owner * @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( $this->con->exec(
"INSERT INTO Tactic(name, owner) VALUES(:name, :owner)", "INSERT INTO Tactic(name, owner) VALUES(:name, :owner)",
[ [
@ -67,27 +67,37 @@ class TacticInfoGateway {
":owner" => [$owner, PDO::PARAM_INT], ":owner" => [$owner, PDO::PARAM_INT],
] ]
); );
$row = $this->con->fetch( return intval($this->con->lastInsertId());
"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"]);
} }
/** /**
* 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;
); }
/***
* 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;
} }
} }

@ -2,9 +2,9 @@
namespace IQBall\Core\Model; namespace IQBall\Core\Model;
use IQBall\Core\Data\TacticInfo;
use IQBall\Core\Gateway\TacticInfoGateway; use IQBall\Core\Gateway\TacticInfoGateway;
use IQBall\Core\Validation\ValidationFail; use IQBall\Core\Validation\ValidationFail;
use IQBall\Core\Data\TacticInfo;
class TacticModel { class TacticModel {
public const TACTIC_DEFAULT_NAME = "Nouvelle tactique"; public const TACTIC_DEFAULT_NAME = "Nouvelle tactique";
@ -26,7 +26,8 @@ class TacticModel {
* @return TacticInfo * @return TacticInfo
*/ */
public function makeNew(string $name, int $ownerId): 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);
} }
/** /**
@ -75,8 +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): ?ValidationFail {
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