diff --git a/.env b/.env index 951db6b..98ae12d 100644 --- a/.env +++ b/.env @@ -1 +1,2 @@ -VITE_API_ENDPOINT=/api \ No newline at end of file +VITE_API_ENDPOINT=/api +VITE_BASE= \ No newline at end of file diff --git a/ci/.drone.yml b/ci/.drone.yml index 8b7058d..a282766 100644 --- a/ci/.drone.yml +++ b/ci/.drone.yml @@ -36,6 +36,7 @@ steps: - chmod +x /tmp/moshell_setup.sh - echo n | /tmp/moshell_setup.sh - echo "VITE_API_ENDPOINT=/IQBall/$DRONE_BRANCH/public/api" >> .env.PROD + - echo "VITE_BASE=/IQBall/$DRONE_BRANCH/public" >> .env.PROD - - /root/.local/bin/moshell ci/build_react.msh diff --git a/ci/build_react.msh b/ci/build_react.msh index 32a5923..3d3a8f0 100755 --- a/ci/build_react.msh +++ b/ci/build_react.msh @@ -9,8 +9,6 @@ val drone_branch = std::env("DRONE_BRANCH").unwrap() val base = "/IQBall/$drone_branch/public" npm run build -- --base=$base --mode PROD -npm run build -- --base=/IQBall/public --mode PROD - // Read generated mappings from build val result = $(jq -r 'to_entries|map(.key + " " +.value.file)|.[]' dist/manifest.json) val mappings = $result.split('\n') diff --git a/front/Constants.ts b/front/Constants.ts index 76b37c2..013db50 100644 --- a/front/Constants.ts +++ b/front/Constants.ts @@ -2,3 +2,8 @@ * This constant defines the API endpoint. */ export const API = import.meta.env.VITE_API_ENDPOINT + +/** + * This constant defines the base app's endpoint. + */ +export const BASE = import.meta.env.VITE_BASE diff --git a/front/assets/basketball_court.svg b/front/assets/court/court.svg similarity index 98% rename from front/assets/basketball_court.svg rename to front/assets/court/court.svg index e0df003..e01fd58 100644 --- a/front/assets/basketball_court.svg +++ b/front/assets/court/court.svg @@ -1,7 +1,8 @@ diff --git a/front/assets/court/half_court.svg b/front/assets/court/half_court.svg new file mode 100644 index 0000000..8e7640e --- /dev/null +++ b/front/assets/court/half_court.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/front/components/Rack.tsx b/front/components/Rack.tsx index 178ef24..2a7511f 100644 --- a/front/components/Rack.tsx +++ b/front/components/Rack.tsx @@ -20,13 +20,13 @@ interface RackItemProps { * A container of draggable objects * */ export function Rack({ - id, - objects, - onChange, - canDetach, - onElementDetached, - render, - }: RackProps) { + id, + objects, + onChange, + canDetach, + onElementDetached, + render, +}: RackProps) { return (
({ } function RackItem({ - item, - onTryDetach, - render, - }: RackItemProps) { + item, + onTryDetach, + render, +}: RackItemProps) { const divRef = useRef(null) return ( diff --git a/front/components/editor/BallPiece.tsx b/front/components/editor/BallPiece.tsx index 054f83c..baaba70 100644 --- a/front/components/editor/BallPiece.tsx +++ b/front/components/editor/BallPiece.tsx @@ -1,26 +1,21 @@ -import React, {RefObject} from "react"; +import React, { RefObject } from "react" -import "../../style/ball.css"; +import "../../style/ball.css" -import Ball from "../../assets/icon/ball.svg?react"; -import Draggable from "react-draggable"; +import Ball from "../../assets/icon/ball.svg?react" +import Draggable from "react-draggable" export interface BallPieceProps { onDrop: () => void pieceRef: RefObject } - -export function BallPiece({onDrop, pieceRef}: BallPieceProps) { +export function BallPiece({ onDrop, pieceRef }: BallPieceProps) { return ( - +
- +
) -} \ No newline at end of file +} diff --git a/front/components/editor/BasketCourt.tsx b/front/components/editor/BasketCourt.tsx index c15f02c..6229afd 100644 --- a/front/components/editor/BasketCourt.tsx +++ b/front/components/editor/BasketCourt.tsx @@ -1,15 +1,16 @@ - import CourtSvg from "../../assets/basketball_court.svg?react" import "../../style/basket_court.css" -import { useRef } from "react" +import { RefObject, useRef } from "react" import CourtPlayer from "./CourtPlayer" import { Player } from "../../tactic/Player" export interface BasketCourtProps { players: Player[] onPlayerRemove: (p: Player) => void - onBallDrop: (ref : HTMLDivElement) => void + onBallDrop: (ref: HTMLDivElement) => void onPlayerChange: (p: Player) => void + courtImage: string + courtRef: RefObject } export function BasketCourt({ @@ -17,12 +18,15 @@ export function BasketCourt({ onPlayerRemove, onBallDrop, onPlayerChange, + courtImage, + courtRef, }: BasketCourtProps) { - const divRef = useRef(null) - return ( -
- +
+ {"court"} {players.map((player) => { return ( onPlayerRemove(player)} onBallDrop={onBallDrop} - parentRef={divRef} + parentRef={courtRef} /> ) })} diff --git a/front/components/editor/CourtPlayer.tsx b/front/components/editor/CourtPlayer.tsx index 1d4961a..6aebdcb 100644 --- a/front/components/editor/CourtPlayer.tsx +++ b/front/components/editor/CourtPlayer.tsx @@ -1,7 +1,7 @@ import { RefObject, useRef, useState } from "react" import "../../style/player.css" import RemoveIcon from "../../assets/icon/remove.svg?react" -import {BallPiece} from "./BallPiece"; +import { BallPiece } from "./BallPiece" import Draggable from "react-draggable" import { PlayerPiece } from "./PlayerPiece" import { Player } from "../../tactic/Player" @@ -19,12 +19,12 @@ export interface PlayerProps { * 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, - onBallDrop, - parentRef, - }: PlayerProps) { + player, + onChange, + onRemove, + onBallDrop, + parentRef, +}: PlayerProps) { const pieceRef = useRef(null) const ballPiece = useRef(null) @@ -44,14 +44,13 @@ export default function CourtPlayer({ const { x, y } = calculateRatio(pieceBounds, parentBounds) - onChange({ - id : player.id, + id: player.id, rightRatio: x, bottomRatio: y, team: player.team, role: player.role, - hasBall: player.hasBall + hasBall: player.hasBall, }) }}>
-
{ @@ -73,9 +73,18 @@ export default function CourtPlayer({ className="player-selection-tab-remove" onClick={onRemove} /> - {hasBall && onBallDrop(ballPiece.current!)} pieceRef={ballPiece}/>} + {hasBall && ( + onBallDrop(ballPiece.current!)} + pieceRef={ballPiece} + /> + )}
- +
diff --git a/front/components/editor/PlayerPiece.tsx b/front/components/editor/PlayerPiece.tsx index 5756afb..e725d31 100644 --- a/front/components/editor/PlayerPiece.tsx +++ b/front/components/editor/PlayerPiece.tsx @@ -5,10 +5,10 @@ import { Team } from "../../tactic/Team" export interface PlayerPieceProps { team: Team text: string - hasBall : boolean + hasBall: boolean } -export function PlayerPiece({ team, text, hasBall}: PlayerPieceProps) { +export function PlayerPiece({ team, text, hasBall }: PlayerPieceProps) { let className = `player-piece ${team}` if (hasBall) { className += ` player-piece-has-ball` diff --git a/front/style/ball.css b/front/style/ball.css index 5bf15d1..c14c196 100644 --- a/front/style/ball.css +++ b/front/style/ball.css @@ -2,7 +2,8 @@ fill: #c5520d; } -.ball-div, .ball { +.ball-div, +.ball { pointer-events: all; width: 20px; height: 20px; diff --git a/front/style/basket_court.css b/front/style/basket_court.css index c001cc0..92a520c 100644 --- a/front/style/basket_court.css +++ b/front/style/basket_court.css @@ -1,11 +1,16 @@ #court-container { display: flex; + align-content: center; + align-items: center; + justify-content: center; + height: 100%; background-color: var(--main-color); } #court-svg { - margin: 5%; + margin: 35px 0 35px 0; + height: 87%; user-select: none; -webkit-user-drag: none; } diff --git a/front/style/editor.css b/front/style/editor.css index eefa561..45ce43d 100644 --- a/front/style/editor.css +++ b/front/style/editor.css @@ -1,4 +1,4 @@ -@import "colors.css"; +@import "theme/default.css"; #main-div { display: flex; @@ -63,7 +63,8 @@ } #court-div-bounds { - width: 60%; + padding: 20px 20px 20px 20px; + height: 75%; } .react-draggable { diff --git a/front/style/new_tactic_panel.css b/front/style/new_tactic_panel.css new file mode 100644 index 0000000..ff6a07e --- /dev/null +++ b/front/style/new_tactic_panel.css @@ -0,0 +1,122 @@ +#panel-root { + width: 100%; + height: 100%; + + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + align-content: center; +} + +#panel-top { + font-family: var(--text-main-font); +} + +#panel-choices { + width: 100%; + height: 100%; + + display: flex; + justify-content: center; + align-items: center; + align-content: center; + + background-color: var(--editor-court-selection-background); +} + +#panel-buttons { + width: 75%; + height: 20%; + display: flex; + justify-content: space-evenly; + align-items: stretch; + align-content: center; +} + +.court-kind-button { + position: relative; + + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + align-content: center; + + cursor: pointer; + + transition: scale 0.5s ease-out; + width: auto; +} + +.court-kind-button-bottom, +.court-kind-button-top { + border: solid; + border-color: var(--border-color); +} +.court-kind-button-bottom { + display: flex; + justify-content: center; + align-items: center; + align-content: center; + + height: 25%; + width: 100%; + + background-color: var(--editor-court-selection-buttons); + border-radius: 0 0 20px 20px; + border-width: 3px; +} + +.court-kind-button-top { + height: 30%; + background-color: var(--main-color); + border-radius: 20px 20px 0 0; + border-width: 3px 3px 0 3px; +} + +.court-kind-button:hover { + scale: 1.1; +} + +.court-kind-button-top, +.court-kind-button-image-div { + overflow: hidden; + display: flex; + height: 100%; + width: 100%; + justify-content: center; + align-items: center; + align-content: center; +} + +.court-kind-button-image { + height: 100%; + width: 150px; + user-select: none; + -webkit-user-drag: none; +} + +.court-kind-button-image-div { + height: 100%; + + padding: 0 10px 0 10px; + background-color: var(--second-color); +} + +.court-kind-button-name, +.court-kind-button-details { + user-select: none; + font-family: var(--text-main-font); +} + +.court-kind-button-details { + position: absolute; + z-index: -1; + top: 0; + transition: top 1s; +} + +.court-kind-button:hover .court-kind-button-details { + top: -20px; +} diff --git a/front/style/player.css b/front/style/player.css index d79cf46..6fa31a9 100644 --- a/front/style/player.css +++ b/front/style/player.css @@ -43,12 +43,11 @@ on the court. } .player-selection-tab { - display: flex; + display: none; position: absolute; margin-bottom: -20%; justify-content: center; - visibility: hidden; width: fit-content; transform: translateY(-20px); @@ -73,7 +72,7 @@ on the court. } .player:focus-within .player-selection-tab { - visibility: visible; + display: flex; } .player:focus-within .player-piece { diff --git a/front/style/theme/default.css b/front/style/theme/default.css new file mode 100644 index 0000000..a2894ee --- /dev/null +++ b/front/style/theme/default.css @@ -0,0 +1,21 @@ +@import url("https://fonts.googleapis.com/css2?family=Roboto:wght@300;400&display=swap"); +:root { + --main-color: #ffffff; + --second-color: #e8e8e8; + + --background-color: #d2cdd3; + + --selected-team-primarycolor: #ffffff; + --selected-team-secondarycolor: #000000; + + --buttons-shadow-color: #a8a8a8; + + --selection-color: #3f7fc4; + + --border-color: #ffffff; + + --editor-court-selection-background: #5f8fee; + --editor-court-selection-buttons: #acc4f3; + + --text-main-font: "Roboto", sans-serif; +} diff --git a/front/tactic/Ball.ts b/front/tactic/Ball.ts index 212823f..443e4f9 100644 --- a/front/tactic/Ball.ts +++ b/front/tactic/Ball.ts @@ -1,14 +1,11 @@ export interface Ball { - - /** * Percentage of the player's position to the bottom (0 means top, 1 means bottom, 0.5 means middle) */ - bottom_percentage: number, + bottom_percentage: number /** * Percentage of the player's position to the right (0 means left, 1 means right, 0.5 means middle) */ - right_percentage: number, - -} \ No newline at end of file + right_percentage: number +} diff --git a/front/tactic/Player.ts b/front/tactic/Player.ts index 32ed02d..553b85e 100644 --- a/front/tactic/Player.ts +++ b/front/tactic/Player.ts @@ -1,8 +1,7 @@ import { Team } from "./Team" export interface Player { - - id : string + id: string /** * the player's team * */ @@ -25,4 +24,3 @@ export interface Player { hasBall: boolean } - diff --git a/front/views/Editor.tsx b/front/views/Editor.tsx index 19b8a15..213607f 100644 --- a/front/views/Editor.tsx +++ b/front/views/Editor.tsx @@ -8,23 +8,24 @@ import { } from "react" import "../style/editor.css" import TitleInput from "../components/TitleInput" -import {BasketCourt} from "../components/editor/BasketCourt" - - -import {BallPiece} from "../components/editor/BallPiece"; -import {Ball} from "../tactic/Ball"; -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 { BasketCourt } from "../components/editor/BasketCourt" + +import plainCourt from "../assets/court/court.svg" +import halfCourt from "../assets/court/half_court.svg" + +import { Rack } from "../components/Rack" +import { PlayerPiece } from "../components/editor/PlayerPiece" + +import { BallPiece } from "../components/editor/BallPiece" +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" -import Draggable from "react-draggable"; const ERROR_STYLE: CSSProperties = { borderColor: "red", @@ -37,6 +38,14 @@ export interface EditorViewProps { tactic: Tactic onContentChange: (tactic: TacticContent) => Promise onNameChange: (name: string) => Promise + courtType: "PLAIN" | "HALF" +} + +export interface EditorProps { + id: number + name: string + content: string + courtType: "PLAIN" | "HALF" } /** @@ -47,15 +56,7 @@ interface RackedPlayer { key: string } -export default function Editor({ - id, - name, - content, - }: { - id: number - name: string - content: string -}) { +export default function Editor({ id, name, courtType, content }: EditorProps) { const isInGuestMode = id == -1 const storage_content = localStorage.getItem(GUEST_MODE_CONTENT_STORAGE_KEY) @@ -63,11 +64,16 @@ export default function Editor({ isInGuestMode && storage_content != null ? storage_content : content const storage_name = localStorage.getItem(GUEST_MODE_TITLE_STORAGE_KEY) - const editorName = isInGuestMode && storage_name != null ? storage_name : name + const editorName = + isInGuestMode && storage_name != null ? storage_name : name return ( { if (isInGuestMode) { localStorage.setItem( @@ -76,7 +82,7 @@ export default function Editor({ ) return SaveStates.Guest } - return fetchAPI(`tactic/${id}/save`, {content}).then((r) => + return fetchAPI(`tactic/${id}/save`, { content }).then((r) => r.ok ? SaveStates.Ok : SaveStates.Err, ) }} @@ -85,28 +91,30 @@ export default function Editor({ localStorage.setItem(GUEST_MODE_TITLE_STORAGE_KEY, name) return true //simulate that the name has been changed } - return fetchAPI(`tactic/${id}/edit/name`, {name}).then( + return fetchAPI(`tactic/${id}/edit/name`, { name }).then( (r) => r.ok, ) }} + courtType={courtType} /> ) } - function EditorView({ - tactic: {id, name, content: initialContent}, - onContentChange, - onNameChange, - }: EditorViewProps) { + tactic: { id, name, content: initialContent }, + onContentChange, + onNameChange, + courtType, +}: EditorViewProps) { const isInGuestMode = id == -1 - const [style, setStyle] = useState({}) + const [titleStyle, setTitleStyle] = useState({}) const [content, setContent, saveState] = useContentState( initialContent, isInGuestMode ? SaveStates.Guest : SaveStates.Ok, onContentChange, ) + const [allies, setAllies] = useState( getRackPlayers(Team.Allies, content.players), ) @@ -114,7 +122,9 @@ function EditorView({ getRackPlayers(Team.Opponents, content.players), ) - const [showBall, setShowBall] = useState(content.players.find(p => p.hasBall) == undefined) + const [showBall, setShowBall] = useState( + content.players.find((p) => p.hasBall) == undefined, + ) const ballPiece = useRef(null) @@ -133,12 +143,11 @@ function EditorView({ ) } - const onPieceDetach = (ref: HTMLDivElement, element: RackedPlayer) => { const refBounds = ref.getBoundingClientRect() const courtBounds = courtDivContentRef.current!.getBoundingClientRect() - const {x, y} = calculateRatio(refBounds, courtBounds) + const { x, y } = calculateRatio(refBounds, courtBounds) setContent((content) => { return { @@ -150,54 +159,55 @@ function EditorView({ role: element.key, rightRatio: x, bottomRatio: y, - hasBall: false + hasBall: false, }, ], } }) } - const onBallDrop = (ref : HTMLDivElement) => { + const onBallDrop = (ref: HTMLDivElement) => { const ballBounds = ref.getBoundingClientRect() let ballAssigned = false - setContent(content => { - const players = content.players.map(player => { - if(ballAssigned) { - return {...player, hasBall: false} + setContent((content) => { + const players = content.players.map((player) => { + if (ballAssigned) { + return { ...player, hasBall: false } } - const playerBounds = document.getElementById(player.id)!.getBoundingClientRect() + const playerBounds = document + .getElementById(player.id)! + .getBoundingClientRect() const doesOverlap = !( ballBounds.top > playerBounds.bottom || ballBounds.right < playerBounds.left || ballBounds.bottom < playerBounds.top || ballBounds.left > playerBounds.right ) - if(doesOverlap) { + if (doesOverlap) { ballAssigned = true } - return {...player, hasBall: doesOverlap} + return { ...player, hasBall: doesOverlap } }) setShowBall(!ballAssigned) - return {players: players} + return { players: players } }) } - return (
LEFT - +
{ onNameChange(new_name).then((success) => { - setStyle(success ? {} : ERROR_STYLE) + setTitleStyle(success ? {} : ERROR_STYLE) }) }} /> @@ -212,12 +222,22 @@ function EditorView({ onChange={setAllies} canDetach={canDetach} onElementDetached={onPieceDetach} - render={({team, key}) => ( - + render={({ team, key }) => ( + )} /> - {showBall && onBallDrop(ballPiece.current!)} pieceRef={ballPiece}/>} + {showBall && ( + onBallDrop(ballPiece.current!)} + pieceRef={ballPiece} + /> + )} ( - + render={({ team, key }) => ( + )} />
-
+
{ setContent((content) => ({ players: toSplicedPlayers( @@ -260,7 +289,7 @@ function EditorView({ case Team.Allies: setter = setAllies } - if(player.hasBall) { + if (player.hasBall) { setShowBall(true) } setter((players) => [ @@ -272,7 +301,6 @@ function EditorView({ }, ]) }} - />
@@ -288,7 +316,7 @@ function getRackPlayers(team: Team, players: Player[]): RackedPlayer[] { players.findIndex((p) => p.team == team && p.role == role) == -1, ) - .map((key) => ({team, key})) + .map((key) => ({ team, key })) } function useContentState( diff --git a/front/views/NewTacticPanel.tsx b/front/views/NewTacticPanel.tsx new file mode 100644 index 0000000..bd9badb --- /dev/null +++ b/front/views/NewTacticPanel.tsx @@ -0,0 +1,64 @@ +import "../style/theme/default.css" +import "../style/new_tactic_panel.css" + +import plainCourt from "../assets/court/court.svg" +import halfCourt from "../assets/court/half_court.svg" +import { BASE } from "../Constants" + +export default function NewTacticPanel() { + return ( +
+
+

Select a basket court

+
+
+
+ + +
+
+
+ ) +} + +function CourtKindButton({ + name, + image, + details, + redirect, +}: { + name: string + image: string + details: string + redirect: string +}) { + return ( +
(location.href = BASE + redirect)}> +
{details}
+
+
+ {name} +
+
+
+

{name}

+
+
+ ) +} diff --git a/front/views/Visualizer.tsx b/front/views/Visualizer.tsx index 541da09..5b09115 100644 --- a/front/views/Visualizer.tsx +++ b/front/views/Visualizer.tsx @@ -1,6 +1,6 @@ import React, { CSSProperties, useState } from "react" import "../style/visualizer.css" -import Court from "../assets/basketball_court.svg" +import Court from "../assets/court/court.svg" export default function Visualizer({ id, name }: { id: number; name: string }) { const [style, setStyle] = useState({}) diff --git a/public/index.php b/public/index.php index 78ee4d6..cd777ab 100644 --- a/public/index.php +++ b/public/index.php @@ -19,6 +19,7 @@ use IQBall\App\Session\SessionHandle; use IQBall\App\ViewHttpResponse; use IQBall\Core\Action; use IQBall\Core\Connection; +use IQBall\Core\Data\CourtType; use IQBall\Core\Gateway\AccountGateway; use IQBall\Core\Gateway\MemberGateway; use IQBall\Core\Gateway\TacticInfoGateway; @@ -90,7 +91,9 @@ function getRoutes(): AltoRouter { $ar->map("GET", "/tactic/[i:id]/edit", Action::auth(fn(int $id, SessionHandle $s) => getEditorController()->openEditor($id, $s))); // don't require an authentication to run this action. // If the user is not connected, the tactic will never save. - $ar->map("GET", "/tactic/new", Action::noAuth(fn(SessionHandle $s) => getEditorController()->createNew($s))); + $ar->map("GET", "/tactic/new", Action::noAuth(fn() => getEditorController()->createNew())); + $ar->map("GET", "/tactic/new/plain", Action::noAuth(fn(SessionHandle $s) => getEditorController()->createNewOfKind(CourtType::plain(), $s))); + $ar->map("GET", "/tactic/new/half", Action::noAuth(fn(SessionHandle $s) => getEditorController()->createNewOfKind(CourtType::half(), $s))); //team-related $ar->map("GET", "/team/new", Action::auth(fn(SessionHandle $s) => getTeamController()->displayCreateTeam($s))); diff --git a/sql/setup-tables.sql b/sql/setup-tables.sql index eb74877..efb20c6 100644 --- a/sql/setup-tables.sql +++ b/sql/setup-tables.sql @@ -16,35 +16,36 @@ CREATE TABLE Account CREATE TABLE Tactic ( id integer PRIMARY KEY AUTOINCREMENT, - name varchar NOT NULL, - creation_date timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL, - owner integer NOT NULL, - content varchar DEFAULT '{"players": []}' NOT NULL, + name varchar NOT NULL, + creation_date timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL, + owner integer NOT NULL, + content varchar DEFAULT '{"players": []}' NOT NULL, + court_type varchar CHECK ( court_type IN ('HALF', 'PLAIN')) NOT NULL, FOREIGN KEY (owner) REFERENCES Account ); CREATE TABLE FormEntries ( - name varchar, - description varchar + name varchar NOT NULL, + description varchar NOT NULL ); CREATE TABLE Team ( - id integer PRIMARY KEY AUTOINCREMENT, - name varchar, - picture varchar, - main_color varchar, - second_color varchar + id integer PRIMARY KEY AUTOINCREMENT NOT NULL, + name varchar NOT NULL, + picture varchar NOT NULL, + main_color varchar NOT NULL, + second_color varchar NOT NULL ); CREATE TABLE Member ( - id_team integer, - id_user integer, - role text CHECK (role IN ('Coach', 'Player')), + id_team integer NOT NULL, + id_user integer NOT NULL, + role text CHECK (role IN ('COACH', 'PLAYER')) NOT NULL, FOREIGN KEY (id_team) REFERENCES Team (id), - FOREIGN KEY (id_user) REFERENCES User (id) + FOREIGN KEY (id_user) REFERENCES Account (id) ); diff --git a/src/App/Controller/EditorController.php b/src/App/Controller/EditorController.php index 3994093..3bbbe61 100644 --- a/src/App/Controller/EditorController.php +++ b/src/App/Controller/EditorController.php @@ -5,9 +5,11 @@ namespace IQBall\App\Controller; use IQBall\App\Session\SessionHandle; use IQBall\App\Validator\TacticValidator; use IQBall\App\ViewHttpResponse; +use IQBall\Core\Data\CourtType; 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; @@ -25,17 +27,23 @@ class EditorController { "id" => $tactic->getId(), "name" => $tactic->getName(), "content" => $tactic->getContent(), + "courtType" => $tactic->getCourtType()->name(), ]); } + public function createNew(): ViewHttpResponse { + return ViewHttpResponse::react("views/NewTacticPanel.tsx", []); + } + /** * @return ViewHttpResponse the editor view for a test tactic. */ - private function openTestEditor(): ViewHttpResponse { + private function openTestEditor(CourtType $courtType): ViewHttpResponse { return ViewHttpResponse::react("views/Editor.tsx", [ "id" => -1, //-1 id means that the editor will not support saves - "content" => '{"players": []}', "name" => TacticModel::TACTIC_DEFAULT_NAME, + "content" => '{"players": []}', + "courtType" => $courtType->name(), ]); } @@ -44,15 +52,18 @@ class EditorController { * If the given session does not contain a connected account, * open a test editor. * @param SessionHandle $session + * @param CourtType $type * @return ViewHttpResponse the editor view */ - public function createNew(SessionHandle $session): ViewHttpResponse { - $account = $session->getAccount(); + public function createNewOfKind(CourtType $type, SessionHandle $session): ViewHttpResponse { + + $action = $session->getAccount(); - if ($account == null) { - return $this->openTestEditor(); + if ($action == null) { + return $this->openTestEditor($type); } - $tactic = $this->model->makeNewDefault($account->getId()); + + $tactic = $this->model->makeNewDefault($session->getAccount()->getId(), $type); return $this->openEditorFor($tactic); } diff --git a/src/Core/Data/CourtType.php b/src/Core/Data/CourtType.php new file mode 100755 index 0000000..caad45c --- /dev/null +++ b/src/Core/Data/CourtType.php @@ -0,0 +1,61 @@ + self::COURT_HALF) { + throw new InvalidArgumentException("Valeur du rĂ´le invalide"); + } + $this->value = $val; + } + + public static function plain(): CourtType { + return new CourtType(CourtType::COURT_PLAIN); + } + + public static function half(): CourtType { + return new CourtType(CourtType::COURT_HALF); + } + + public function name(): string { + switch ($this->value) { + case self::COURT_HALF: + return "HALF"; + case self::COURT_PLAIN: + return "PLAIN"; + } + die("unreachable"); + } + + public static function fromName(string $name): ?CourtType { + switch ($name) { + case "HALF": + return CourtType::half(); + case "PLAIN": + return CourtType::plain(); + default: + return null; + } + } + + public function isPlain(): bool { + return ($this->value == self::COURT_PLAIN); + } + + public function isHalf(): bool { + return ($this->value == self::COURT_HALF); + } + +} diff --git a/src/Core/Data/MemberRole.php b/src/Core/Data/MemberRole.php index 41b6b71..9606c0b 100755 --- a/src/Core/Data/MemberRole.php +++ b/src/Core/Data/MemberRole.php @@ -35,18 +35,18 @@ final class MemberRole { public function name(): string { switch ($this->value) { case self::ROLE_COACH: - return "Coach"; + return "COACH"; case self::ROLE_PLAYER: - return "Player"; + return "PLAYER"; } die("unreachable"); } public static function fromName(string $name): ?MemberRole { switch ($name) { - case "Coach": + case "COACH": return MemberRole::coach(); - case "Player": + case "PLAYER": return MemberRole::player(); default: return null; diff --git a/src/Core/Data/TacticInfo.php b/src/Core/Data/TacticInfo.php index 2565f93..c3b8667 100644 --- a/src/Core/Data/TacticInfo.php +++ b/src/Core/Data/TacticInfo.php @@ -7,7 +7,7 @@ class TacticInfo { private string $name; private int $creationDate; private int $ownerId; - + private CourtType $courtType; private string $content; /** @@ -15,13 +15,15 @@ class TacticInfo { * @param string $name * @param int $creationDate * @param int $ownerId + * @param CourtType $type * @param string $content */ - public function __construct(int $id, string $name, int $creationDate, int $ownerId, string $content) { + public function __construct(int $id, string $name, int $creationDate, int $ownerId, CourtType $type, string $content) { $this->id = $id; $this->name = $name; $this->ownerId = $ownerId; $this->creationDate = $creationDate; + $this->courtType = $type; $this->content = $content; } @@ -47,6 +49,10 @@ class TacticInfo { return $this->ownerId; } + public function getCourtType(): CourtType { + return $this->courtType; + } + /** * @return int */ diff --git a/src/Core/Gateway/TacticInfoGateway.php b/src/Core/Gateway/TacticInfoGateway.php index 447c7a5..67cffc4 100644 --- a/src/Core/Gateway/TacticInfoGateway.php +++ b/src/Core/Gateway/TacticInfoGateway.php @@ -3,6 +3,7 @@ namespace IQBall\Core\Gateway; use IQBall\Core\Connection; +use IQBall\Core\Data\CourtType; use IQBall\Core\Data\TacticInfo; use PDO; @@ -33,7 +34,8 @@ class TacticInfoGateway { $row = $res[0]; - return new TacticInfo($id, $row["name"], strtotime($row["creation_date"]), $row["owner"], $row['content']); + $type = CourtType::fromName($row['court_type']); + return new TacticInfo($id, $row["name"], strtotime($row["creation_date"]), $row["owner"], $type, $row['content']); } @@ -57,14 +59,16 @@ class TacticInfoGateway { /** * @param string $name * @param int $owner + * @param CourtType $type * @return int inserted tactic id */ - public function insert(string $name, int $owner): int { + public function insert(string $name, int $owner, CourtType $type): int { $this->con->exec( - "INSERT INTO Tactic(name, owner) VALUES(:name, :owner)", + "INSERT INTO Tactic(name, owner, court_type) VALUES(:name, :owner, :court_type)", [ ":name" => [$name, PDO::PARAM_STR], ":owner" => [$owner, PDO::PARAM_INT], + ":court_type" => [$type->name(), PDO::PARAM_STR], ] ); return intval($this->con->lastInsertId()); diff --git a/src/Core/Model/TacticModel.php b/src/Core/Model/TacticModel.php index 136b27d..51e5eb8 100644 --- a/src/Core/Model/TacticModel.php +++ b/src/Core/Model/TacticModel.php @@ -2,6 +2,7 @@ namespace IQBall\Core\Model; +use IQBall\Core\Data\CourtType; use IQBall\Core\Data\TacticInfo; use IQBall\Core\Gateway\TacticInfoGateway; use IQBall\Core\Validation\ValidationFail; @@ -23,20 +24,22 @@ class TacticModel { * creates a new empty tactic, with given name * @param string $name * @param int $ownerId + * @param CourtType $type * @return TacticInfo */ - public function makeNew(string $name, int $ownerId): TacticInfo { - $id = $this->tactics->insert($name, $ownerId); + public function makeNew(string $name, int $ownerId, CourtType $type): TacticInfo { + $id = $this->tactics->insert($name, $ownerId, $type); return $this->tactics->get($id); } /** * creates a new empty tactic, with a default name * @param int $ownerId + * @param CourtType $type * @return TacticInfo|null */ - public function makeNewDefault(int $ownerId): ?TacticInfo { - return $this->makeNew(self::TACTIC_DEFAULT_NAME, $ownerId); + public function makeNewDefault(int $ownerId, CourtType $type): ?TacticInfo { + return $this->makeNew(self::TACTIC_DEFAULT_NAME, $ownerId, $type); } /**