add save state information in topbar

pull/23/head
maxime.batista 2 years ago
parent 8dd6688cf8
commit 6731b02c6c

@ -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 {

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

@ -1,9 +1,12 @@
export function calculateRatio(it: { x: number, y: number }, parent: DOMRect): { x: number, y: number } { export function calculateRatio(
const relativeXPixels = it.x - parent.x; it: { x: number; y: number },
const relativeYPixels = it.y - parent.y; parent: DOMRect,
): { x: number; y: number } {
const relativeXPixels = it.x - parent.x
const relativeYPixels = it.y - parent.y
const xRatio = relativeXPixels / parent.width; const xRatio = relativeXPixels / parent.width
const yRatio = relativeYPixels / parent.height; const yRatio = relativeYPixels / parent.height
return {x: xRatio, y: yRatio} 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"

@ -1,28 +1,35 @@
import CourtSvg from '../../assets/basketball_court.svg?react'; 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 "../../tactic/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 onPlayerChange: (p: Player) => void
} }
export function BasketCourt({players, onPlayerRemove, onPlayerChange}: BasketCourtProps) { export function BasketCourt({
const divRef = useRef<HTMLDivElement>(null); players,
onPlayerRemove,
onPlayerChange,
}: BasketCourtProps) {
const divRef = useRef<HTMLDivElement>(null)
return ( return (
<div id="court-container" ref={divRef} 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 <CourtPlayer key={player.id} return (
player={player} <CourtPlayer
onChange={onPlayerChange} key={player.id}
onRemove={() => onPlayerRemove(player)} player={player}
parentRef={divRef} onChange={onPlayerChange}
/> onRemove={() => onPlayerRemove(player)}
parentRef={divRef}
/>
)
})} })}
</div> </div>
) )

@ -1,5 +1,4 @@
import { RefObject, useRef, useState } from "react"
import { MutableRefObject, useEffect, 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"
@ -8,10 +7,10 @@ import { Player } from "../../tactic/Player"
import { calculateRatio } from "../../Utils" import { calculateRatio } from "../../Utils"
export interface PlayerProps { export interface PlayerProps {
player: Player, player: Player
onChange: (p: Player) => void, onChange: (p: Player) => void
onRemove: () => void onRemove: () => void
parentRef: MutableRefObject<HTMLElement> parentRef: RefObject<HTMLDivElement>
} }
/** /**

@ -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,11 +1,11 @@
import {Player} from "./Player"; import { Player } from "./Player"
export interface Tactic { export interface Tactic {
id: number, id: number
name: string, name: string
content: TacticContent content: TacticContent
} }
export interface TacticContent { export interface TacticContent {
players: Player[] players: Player[]
} }

@ -1,24 +1,25 @@
import {CSSProperties, useEffect, useRef, useState} from "react"; import React, { CSSProperties, useEffect, 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"
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 "../tactic/Player"; import { Player } from "../tactic/Player"
import {Tactic, TacticContent} from "../tactic/Tactic"; 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"
const ERROR_STYLE: CSSProperties = { const ERROR_STYLE: CSSProperties = {
borderColor: "red", borderColor: "red",
} }
export interface EditorViewProps { export interface EditorViewProps {
tactic: Tactic, tactic: Tactic
onContentChange: (tactic: TacticContent) => Promise<boolean>, onContentChange: (tactic: TacticContent) => Promise<boolean>
onNameChange: (name: string) => Promise<boolean> onNameChange: (name: string) => Promise<boolean>
} }
@ -30,39 +31,78 @@ interface RackedPlayer {
key: string key: string
} }
export default function Editor({tactic}: { tactic: Tactic }) { export default function Editor({
return <EditorView tactic={tactic} id,
onContentChange={(content: TacticContent) => ( name,
fetchAPI(`tactic/${tactic.id}/save`, {content}) content,
.then((r) => r.ok) }: {
)} id: number
onNameChange={(name: string) => ( name: string
fetchAPI(`tactic/${tactic.id}/edit/name`, {name}) content: string
.then((r) => r.ok) }) {
)}/> 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}, onContentChange, onNameChange}: EditorViewProps) { function EditorView({
const [style, setStyle] = useState<CSSProperties>({}); tactic: { name, content },
onContentChange,
onNameChange,
}: EditorViewProps) {
const [style, setStyle] = useState<CSSProperties>({})
const [saveState, setSaveState] = useState(SaveStates.Ok)
const positions = ["1", "2", "3", "4", "5"] const positions = ["1", "2", "3", "4", "5"]
const [allies, setAllies] = useState( 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( 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<Player[]>(content.players)
const courtDivContentRef = useRef<HTMLDivElement>(null)
const [players, setPlayers] = useState<Player[]>(content.players); // The didMount ref is used to store a boolean flag in order to avoid calling 'onChange' when the editor is first rendered.
const courtDivContentRef = useRef<HTMLDivElement>(null); const didMount = useRef(false)
useEffect(() => { useEffect(() => {
onContentChange({players}) if (!didMount.current) {
.then(success => { didMount.current = true
if (!success) return
alert("error when saving changes.") }
setSaveState(SaveStates.Saving)
onContentChange({ players })
.then((success) => {
if (success) {
setSaveState(SaveStates.Ok)
} else {
setSaveState(SaveStates.Err)
}
}) })
.catch(() => setSaveState(SaveStates.Err))
}, [players]) }, [players])
const canDetach = (ref: HTMLDivElement) => { const canDetach = (ref: HTMLDivElement) => {
@ -82,34 +122,45 @@ function EditorView({tactic: {name, content}, onContentChange, onNameChange}: Ed
const refBounds = ref.getBoundingClientRect() const refBounds = ref.getBoundingClientRect()
const courtBounds = courtDivContentRef.current!.getBoundingClientRect() const courtBounds = courtDivContentRef.current!.getBoundingClientRect()
const { x, y } = calculateRatio(refBounds, courtBounds)
const {x, y} = calculateRatio(refBounds, courtBounds)
setPlayers((players) => {
setPlayers(players => { return [
return [...players, { ...players,
id: players.length, {
team: element.team, id: players.length,
role: element.key, team: element.team,
rightRatio: x, role: element.key,
bottomRatio: y rightRatio: x,
}] 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">
<TitleInput style={style} default_value={name} on_validated={new_name => { LEFT
onNameChange(new_name).then(success => { <SavingState state={saveState} />
if (success) { </div>
setStyle({}) <div id="title-input-div">
} else { <TitleInput
setStyle(ERROR_STYLE) style={style}
} default_value={name}
}) on_validated={(new_name) => {
}}/> onNameChange(new_name).then((success) => {
<div>RIGHT</div> if (success) {
setStyle({})
} else {
setStyle(ERROR_STYLE)
}
})
}}
/>
</div>
<div id="topbar-right">RIGHT</div>
</div> </div>
<div id="edit-div"> <div id="edit-div">
<div id="racks"> <div id="racks">
@ -139,14 +190,18 @@ function EditorView({tactic: {name, content}, onContentChange, onNameChange}: Ed
<BasketCourt <BasketCourt
players={players} players={players}
onPlayerChange={(player) => { onPlayerChange={(player) => {
setPlayers(players => { setPlayers((players) => {
const idx = players.indexOf(player) const idx = players.findIndex(
(p) => p.id === player.id,
)
return players.toSpliced(idx, 1, player) return players.toSpliced(idx, 1, player)
}) })
}} }}
onPlayerRemove={(player) => { onPlayerRemove={(player) => {
setPlayers((players) => { setPlayers((players) => {
const idx = players.indexOf(player) const idx = players.findIndex(
(p) => p.id === player.id,
)
return players.toSpliced(idx, 1) return players.toSpliced(idx, 1)
}) })
switch (player.team) { switch (player.team) {

@ -31,7 +31,7 @@ function getRoutes(): AltoRouter {
$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]/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; return $router;
} }

@ -19,26 +19,32 @@ 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
( (
id integer PRIMARY KEY AUTOINCREMENT, id integer PRIMARY KEY AUTOINCREMENT,
name varchar, name varchar,
picture varchar, picture varchar,
main_color varchar, main_color varchar,
second_color varchar second_color varchar
); );
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 char(1) 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)
); );

@ -52,6 +52,11 @@ class APITacticController {
* @return HttpResponse * @return HttpResponse
*/ */
public function saveContent(int $id, Account $account): 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);
});
} }
} }

@ -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(),
]);
} }
/** /**

@ -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,11 +67,7 @@ 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"]);
} }
/** /**
@ -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],
]
);
}
} }

@ -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);
} }
/** /**
@ -79,4 +80,8 @@ class TacticModel {
return []; return [];
} }
public function updateContent(int $id, string $json): void {
$this->tactics->updateContent($id, $json);
}
} }

Loading…
Cancel
Save