Merge branch 'master' of codefirst.iut.uca.fr:IQBall/Application-Web into editor/ball-assign
continuous-integration/drone/push Build is passing Details

pull/40/head
Override-6 1 year ago
commit 79b9e0811c
Signed by untrusted user who does not match committer: maxime.batista
GPG Key ID: 8002CC4B4DD9ECA5

@ -1 +1,2 @@
VITE_API_ENDPOINT=/api
VITE_API_ENDPOINT=/api
VITE_BASE=

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

@ -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')

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

@ -1,7 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
version="1.1"
width="100%"
width="100"
height="50"
viewBox="7.5 18.5 85.5 56"
style="enable-background:new 7.5 18.5 85.5 56;"
xml:space="preserve">

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

@ -0,0 +1,22 @@
<svg width="100" height="50" viewBox="0 0 80 58" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.74466 0.676824V28.6768V56.6768L76.7447 56.6768V28.6768V0.676824L2.74466 0.676824Z" stroke="black" stroke-miterlimit="10"/>
<path d="M76.8393 0.876801L3.21608 0.876801" stroke="black" stroke-miterlimit="10"/>
<path d="M12.0393 56.8768V47.2768C12.0393 33.8101 24.6232 23.0101 39.9554 23.0101C55.2875 23.0101 67.8715 33.9435 67.8715 47.2768V56.8768" stroke="black" stroke-miterlimit="10"/>
<path d="M49.3571 56.8768V31.5435C49.3571 26.7435 45.1625 23.0101 40.1 23.0101C35.0375 23.0101 30.8429 26.8768 30.8429 31.5435V56.8768" stroke="black" stroke-miterlimit="10"/>
<path d="M49.3571 31.5435H30.6982" stroke="black" stroke-miterlimit="10"/>
<path d="M49.2125 31.1435C49.2125 31.5435 49.2125 31.8101 49.2125 32.0768" stroke="black" stroke-miterlimit="10"/>
<path d="M48.6339 33.9435C47.3322 37.2768 43.8607 39.6768 39.8107 39.6768C35.4715 39.6768 31.7107 36.7435 30.8429 33.0102" stroke="black" stroke-miterlimit="10" stroke-dasharray="1.44 1.44"/>
<path d="M30.8429 32.0768C30.8429 31.8101 30.8429 31.4101 30.8429 31.1435" stroke="black" stroke-miterlimit="10"/>
<path d="M30.6982 46.0768H29.2518" stroke="black" stroke-miterlimit="10"/>
<path d="M30.6982 41.9435H29.2518" stroke="black" stroke-width="0.5" stroke-miterlimit="10"/>
<path d="M30.6982 37.8102H29.2518" stroke="black" stroke-width="0.5" stroke-miterlimit="10"/>
<path d="M30.6982 33.4102H29.2518" stroke="black" stroke-width="0.5" stroke-miterlimit="10"/>
<path d="M50.8036 46.2101H49.3572" stroke="black" stroke-miterlimit="10"/>
<path d="M50.8036 42.0768H49.3572" stroke="black" stroke-width="0.5" stroke-miterlimit="10"/>
<path d="M50.8036 37.9435H49.3572" stroke="black" stroke-width="0.5" stroke-miterlimit="10"/>
<path d="M50.8036 33.5435H49.3572" stroke="black" stroke-width="0.5" stroke-miterlimit="10"/>
<path d="M49.3571 31.5435H30.6982" stroke="black" stroke-miterlimit="10"/>
<path d="M6.25357 19.4102H0.323216" stroke="black" stroke-width="0.5" stroke-miterlimit="10"/>
<path d="M79.8768 19.4102H73.9464" stroke="black" stroke-width="0.5" stroke-miterlimit="10"/>
<path d="M30.7447 0.67682C30.7447 5.64738 34.998 9.67682 40.2447 9.67682C45.4914 9.67682 49.7447 5.64738 49.7447 0.67682" stroke="black"/>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

@ -20,13 +20,13 @@ interface RackItemProps<E extends { key: string | number }> {
* A container of draggable objects
* */
export function Rack<E extends { key: string | number }>({
id,
objects,
onChange,
canDetach,
onElementDetached,
render,
}: RackProps<E>) {
id,
objects,
onChange,
canDetach,
onElementDetached,
render,
}: RackProps<E>) {
return (
<div
id={id}
@ -55,10 +55,10 @@ export function Rack<E extends { key: string | number }>({
}
function RackItem<E extends { key: string | number }>({
item,
onTryDetach,
render,
}: RackItemProps<E>) {
item,
onTryDetach,
render,
}: RackItemProps<E>) {
const divRef = useRef<HTMLDivElement>(null)
return (

@ -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<HTMLDivElement>
}
export function BallPiece({onDrop, pieceRef}: BallPieceProps) {
export function BallPiece({ onDrop, pieceRef }: BallPieceProps) {
return (
<Draggable
onStop={onDrop}
nodeRef={pieceRef}
position={{x: 0, y: 0}}
>
<Draggable onStop={onDrop} nodeRef={pieceRef} position={{ x: 0, y: 0 }}>
<div className={`ball-div`} ref={pieceRef}>
<Ball className={'ball'}/>
<Ball className={"ball"} />
</div>
</Draggable>
)
}
}

@ -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<HTMLDivElement>
}
export function BasketCourt({
@ -17,12 +18,15 @@ export function BasketCourt({
onPlayerRemove,
onBallDrop,
onPlayerChange,
courtImage,
courtRef,
}: BasketCourtProps) {
const divRef = useRef<HTMLDivElement>(null)
return (
<div id="court-container" ref={divRef} style={{ position: "relative" }}>
<CourtSvg id="court-svg" />
<div
id="court-container"
ref={courtRef}
style={{ position: "relative" }}>
<img src={courtImage} alt={"court"} id="court-svg" />
{players.map((player) => {
return (
<CourtPlayer
@ -31,7 +35,7 @@ export function BasketCourt({
onChange={onPlayerChange}
onRemove={() => onPlayerRemove(player)}
onBallDrop={onBallDrop}
parentRef={divRef}
parentRef={courtRef}
/>
)
})}

@ -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<HTMLDivElement>(null)
const ballPiece = useRef<HTMLDivElement>(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,
})
}}>
<div
@ -62,7 +61,8 @@ export default function CourtPlayer({
left: `${x * 100}%`,
top: `${y * 100}%`,
}}>
<div id={player.id}
<div
id={player.id}
tabIndex={0}
className="player-content"
onKeyUp={(e) => {
@ -73,9 +73,18 @@ export default function CourtPlayer({
className="player-selection-tab-remove"
onClick={onRemove}
/>
{hasBall && <BallPiece onDrop={() => onBallDrop(ballPiece.current!)} pieceRef={ballPiece}/>}
{hasBall && (
<BallPiece
onDrop={() => onBallDrop(ballPiece.current!)}
pieceRef={ballPiece}
/>
)}
</div>
<PlayerPiece team={player.team} text={player.role} hasBall={hasBall}/>
<PlayerPiece
team={player.team}
text={player.role}
hasBall={hasBall}
/>
</div>
</div>
</Draggable>

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

@ -2,7 +2,8 @@
fill: #c5520d;
}
.ball-div, .ball {
.ball-div,
.ball {
pointer-events: all;
width: 20px;
height: 20px;

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

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

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

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

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

@ -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,
}
right_percentage: number
}

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

@ -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<SaveState>
onNameChange: (name: string) => Promise<boolean>
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 (
<EditorView
tactic={{name: editorName, id, content: JSON.parse(editorContent)}}
tactic={{
name: editorName,
id,
content: JSON.parse(editorContent),
}}
onContentChange={async (content: TacticContent) => {
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<CSSProperties>({})
const [titleStyle, setTitleStyle] = useState<CSSProperties>({})
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<HTMLDivElement>(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 (
<div id="main-div">
<div id="topbar-div">
<div id="topbar-left">
LEFT
<SavingState state={saveState}/>
<SavingState state={saveState} />
</div>
<div id="title-input-div">
<TitleInput
style={style}
style={titleStyle}
default_value={name}
on_validated={(new_name) => {
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}) => (
<PlayerPiece team={team} text={key} key={key} hasBall={false}/>
render={({ team, key }) => (
<PlayerPiece
team={team}
text={key}
key={key}
hasBall={false}
/>
)}
/>
{showBall && <BallPiece onDrop={() => onBallDrop(ballPiece.current!)} pieceRef={ballPiece}/>}
{showBall && (
<BallPiece
onDrop={() => onBallDrop(ballPiece.current!)}
pieceRef={ballPiece}
/>
)}
<Rack
id="opponent-rack"
@ -225,16 +245,25 @@ function EditorView({
onChange={setOpponents}
canDetach={canDetach}
onElementDetached={onPieceDetach}
render={({team, key}) => (
<PlayerPiece team={team} text={key} key={key} hasBall={false}/>
render={({ team, key }) => (
<PlayerPiece
team={team}
text={key}
key={key}
hasBall={false}
/>
)}
/>
</div>
<div id="court-div">
<div id="court-div-bounds" ref={courtDivContentRef}>
<div id="court-div-bounds">
<BasketCourt
players={content.players}
onBallDrop={onBallDrop}
courtImage={
courtType == "PLAIN" ? plainCourt : halfCourt
}
courtRef={courtDivContentRef}
onPlayerChange={(player) => {
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({
},
])
}}
/>
</div>
</div>
@ -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<S>(

@ -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 (
<div id={"panel-root"}>
<div id={"panel-top"}>
<p>Select a basket court</p>
</div>
<div id={"panel-choices"}>
<div id={"panel-buttons"}>
<CourtKindButton
name="Plain"
details="Select a plain basketball court"
image={plainCourt}
redirect="/tactic/new/plain"
/>
<CourtKindButton
name="Half"
details="Select half a basketball court"
image={halfCourt}
redirect="/tactic/new/half"
/>
</div>
</div>
</div>
)
}
function CourtKindButton({
name,
image,
details,
redirect,
}: {
name: string
image: string
details: string
redirect: string
}) {
return (
<div
className="court-kind-button"
onClick={() => (location.href = BASE + redirect)}>
<div className="court-kind-button-details">{details}</div>
<div className="court-kind-button-top">
<div className="court-kind-button-image-div">
<img
src={image}
alt={name}
className="court-kind-button-image"
/>
</div>
</div>
<div className="court-kind-button-bottom">
<p className="court-kind-button-name">{name}</p>
</div>
</div>
)
}

@ -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<CSSProperties>({})

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

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

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

@ -0,0 +1,61 @@
<?php
namespace IQBall\Core\Data;
use InvalidArgumentException;
/**
* Enumeration class workaround
* As there is no enumerations in php 7.4, this class
* encapsulates an integer value and use it as a variant discriminant
*/
final class CourtType {
private const COURT_PLAIN = 0;
private const COURT_HALF = 1;
private int $value;
private function __construct(int $val) {
if ($val < self::COURT_PLAIN || $val > 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);
}
}

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

@ -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
*/

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

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

Loading…
Cancel
Save