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

pull/11/head
maxime.batista 1 year ago committed by Override-6
parent ef80aa3192
commit c53a1b024c
Signed by untrusted user who does not match committer: maxime.batista
GPG Key ID: 8002CC4B4DD9ECA5

@ -1,40 +1,39 @@
import {Dispatch, ReactElement, RefObject, SetStateAction, useRef} from "react"; import {ReactElement, useRef} from "react";
import Draggable from "react-draggable"; import Draggable from "react-draggable";
export interface RackInput { export interface RackProps<E extends {key: string | number}> {
id: string, id: string,
objects: [ReactElement[], Dispatch<SetStateAction<ReactElement[]>>], objects: E[],
canDetach: (ref: RefObject<HTMLDivElement>) => boolean, onChange: (objects: E[]) => void,
onElementDetached: (ref: RefObject<HTMLDivElement>, el: ReactElement) => void, canDetach: (ref: HTMLDivElement) => boolean,
onElementDetached: (ref: HTMLDivElement, el: E) => void,
render: (e: E) => ReactElement,
} }
interface RackItemInput { interface RackItemProps<E extends {key: string | number}> {
item: ReactElement, item: E,
onTryDetach: (ref: RefObject<HTMLDivElement>, el: ReactElement) => void onTryDetach: (ref: HTMLDivElement, el: E) => void,
render: (e: E) => ReactElement,
} }
/** /**
* A container of draggable objects * A container of draggable objects
* */ * */
export function Rack({id, objects, canDetach, onElementDetached}: RackInput) { export function Rack<E extends {key: string | number}>({id, objects, onChange, canDetach, onElementDetached, render}: RackProps<E>) {
const [rackObjects, setRackObjects] = objects
return ( return (
<div id={id} style={{ <div id={id} style={{
display: "flex" display: "flex"
}}> }}>
{rackObjects.map(element => ( {objects.map(element => (
<RackItem key={element.key} <RackItem key={element.key}
item={element} item={element}
render={render}
onTryDetach={(ref, element) => { onTryDetach={(ref, element) => {
if (!canDetach(ref)) if (!canDetach(ref))
return return
setRackObjects(objects => { const index = objects.findIndex(o => o.key === element.key)
const index = objects.findIndex(o => o.key === element.key) onChange(objects.toSpliced(index, 1))
return objects.toSpliced(index, 1);
})
onElementDetached(ref, element) onElementDetached(ref, element)
}}/> }}/>
@ -43,16 +42,16 @@ export function Rack({id, objects, canDetach, onElementDetached}: RackInput) {
) )
} }
function RackItem({item, onTryDetach}: RackItemInput) { function RackItem<E extends {key: string | number}>({item, onTryDetach, render}: RackItemProps<E>) {
const divRef = useRef<HTMLDivElement>(null); const divRef = useRef<HTMLDivElement>(null);
return ( return (
<Draggable <Draggable
position={{x: 0, y: 0}} position={{x: 0, y: 0}}
nodeRef={divRef} nodeRef={divRef}
onStop={() => onTryDetach(divRef, item)}> onStop={() => onTryDetach(divRef.current!, item)}>
<div ref={divRef}> <div ref={divRef}>
{item} {render(item)}
</div> </div>
</Draggable> </Draggable>
) )

@ -1,35 +1,27 @@
import CourtSvg from '../../assets/basketball_court.svg'; import CourtSvg from '../../assets/basketball_court.svg?react';
import '../../style/basket_court.css'; import '../../style/basket_court.css';
import {MouseEvent, ReactElement, useEffect, useRef, useState} from "react"; import {useRef} from "react";
import CourtPlayer from "./CourtPlayer"; import CourtPlayer from "./CourtPlayer";
import {Player} from "../../data/Player"; import {Player} from "../../data/Player";
export function BasketCourt({players, onPlayerRemove}: { players: Player[], onPlayerRemove: (Player) => void }) { export interface BasketCourtProps {
const [courtPlayers, setCourtPlayers] = useState<ReactElement[]>([]) players: Player[],
const divRef = useRef<HTMLDivElement>(null); onPlayerRemove: (p: Player) => void,
}
useEffect(() => { export function BasketCourt({players, onPlayerRemove}: BasketCourtProps) {
const bounds = divRef.current!.getBoundingClientRect(); const divRef = useRef<HTMLDivElement>(null);
setCourtPlayers(players.map(player => {
return (
<CourtPlayer key={player.id}
pos={player.position}
team={player.team}
x={(bounds.width * player.right_percentage)}
y={(bounds.height * player.bottom_percentage)}
bounds={{bottom: bounds.height, top: 0, left: 0, right: bounds.width}}
onRemove={() => onPlayerRemove(player)}
/>
)
}))
}, [players, divRef]);
return ( return (
<div id="court-container" ref={divRef}> <div id="court-container" ref={divRef} style={{position: "relative"}}>
<CourtSvg id="court-svg"/> <CourtSvg id="court-svg"/>
{courtPlayers} {players.map(player => {
return <CourtPlayer key={player.id}
player={player}
onRemove={() => onPlayerRemove(player)}
/>
})}
</div> </div>
) )
} }

@ -1,35 +1,37 @@
import React, {useRef} from "react"; import {useRef} from "react";
import "../../style/player.css"; import "../../style/player.css";
import RemoveIcon from "../../assets/icon/remove.svg"; import RemoveIcon from "../../assets/icon/remove.svg?react";
import Draggable, {DraggableBounds} from "react-draggable"; import Draggable from "react-draggable";
import {PlayerPiece} from "./PlayerPiece"; import {PlayerPiece} from "./PlayerPiece";
import {Player} from "../../data/Player";
export interface PlayerOptions { export interface PlayerProps {
pos: string, player: Player,
team: string,
x: number,
y: number,
bounds: DraggableBounds,
onRemove: () => void onRemove: () => void
} }
/** /**
* 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({pos, team, x, y, bounds, onRemove}: PlayerOptions) { export default function CourtPlayer({player, onRemove}: PlayerProps) {
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
const x = player.rightRatio;
const y = player.bottomRatio;
return ( return (
<Draggable <Draggable
handle={".player-piece"} handle={".player-piece"}
nodeRef={ref} nodeRef={ref}
bounds={bounds} bounds="parent"
defaultPosition={{x: x, y: y}}
> >
<div ref={ref} <div ref={ref}
className={"player"} className={"player"}
style={{ style={{
position: "absolute", position: "absolute",
left: `${x * 100}%`,
top: `${y * 100}%`,
}}> }}>
<div tabIndex={0} <div tabIndex={0}
@ -41,9 +43,9 @@ export default function CourtPlayer({pos, team, x, y, bounds, onRemove}: PlayerO
<div className="player-selection-tab"> <div className="player-selection-tab">
<RemoveIcon <RemoveIcon
className="player-selection-tab-remove" className="player-selection-tab-remove"
onClick={() => onRemove()}/> onClick={onRemove}/>
</div> </div>
<PlayerPiece team={team} text={pos}/> <PlayerPiece team={player.team} text={player.role}/>
</div> </div>
</div> </div>

@ -1,8 +1,9 @@
import React from "react"; import React from "react";
import '../../style/player.css' import '../../style/player.css'
import {Team} from "../../data/Team";
export function PlayerPiece({team, text}: { team: string, text: string }) { export function PlayerPiece({team, text}: { team: Team, text: string }) {
return ( return (
<div className={`player-piece ${team}`}> <div className={`player-piece ${team}`}>
<p>{text}</p> <p>{text}</p>

@ -1,3 +1,5 @@
import {Team} from "./Team";
export interface Player { export interface Player {
/** /**
* unique identifier of the player. * unique identifier of the player.
@ -8,20 +10,20 @@ export interface Player {
/** /**
* the player's team * the player's team
* */ * */
team: "allies" | "opponents", team: Team,
/** /**
* player's position * player's position
* */ * */
position: string, role: string,
/** /**
* Percentage of the player's position to the bottom (0 means top, 1 means bottom, 0.5 means middle) * Percentage of the player's position to the bottom (0 means top, 1 means bottom, 0.5 means middle)
*/ */
bottom_percentage: number bottomRatio: number
/** /**
* Percentage of the player's position to the right (0 means left, 1 means right, 0.5 means middle) * Percentage of the player's position to the right (0 means left, 1 means right, 0.5 means middle)
*/ */
right_percentage: number, rightRatio: number,
} }

@ -0,0 +1,4 @@
export enum Team {
Allies = "allies",
Opponents = "opponents"
}

@ -3,6 +3,7 @@
#court-container { #court-container {
display: flex; display: flex;
background-color: var(--main-color); background-color: var(--main-color);
} }

@ -52,3 +52,8 @@
#court-div-bounds { #court-div-bounds {
width: 60%; width: 60%;
} }
.react-draggable {
z-index: 2;
}

@ -9,15 +9,11 @@ on the court.
} }
.player-content { .player-content {
/*apply a translation to center the player piece when placed*/
transform: translate(-29%, -46%);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-content: center; align-content: center;
align-items: center; align-items: center;
outline: none; outline: none;
} }
.player-piece { .player-piece {
@ -44,14 +40,18 @@ on the court.
.player-selection-tab { .player-selection-tab {
display: flex; display: flex;
position: absolute;
margin-bottom: 10%; margin-bottom: 10%;
justify-content: center; justify-content: center;
visibility: hidden; visibility: hidden;
width: 100%;
transform: translateY(-20px);
} }
.player-selection-tab-remove { .player-selection-tab-remove {
pointer-events: all; pointer-events: all;
width: 25%;
height: 25%; height: 25%;
} }

@ -7,27 +7,36 @@ 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 {Player} from "../data/Player";
import {Team} from "../data/Team";
const ERROR_STYLE: CSSProperties = { const ERROR_STYLE: CSSProperties = {
borderColor: "red" borderColor: "red"
} }
/**
* information about a player that is into a rack
*/
interface RackedPlayer {
team: Team,
key: string,
}
export default function Editor({id, name}: { id: number, name: string }) { export default function Editor({id, name}: { id: number, name: string }) {
const [style, setStyle] = useState<CSSProperties>({}); const [style, setStyle] = useState<CSSProperties>({});
const positions = ["1", "2", "3", "4", "5"] const positions = ["1", "2", "3", "4", "5"]
const [allies, setAllies] = useState( const [allies, setAllies] = useState(
positions.map(pos => <PlayerPiece team="allies" key={pos} text={pos}/>) positions.map(key => ({team: Team.Allies, key}))
) )
const [opponents, setOpponents] = useState( const [opponents, setOpponents] = useState(
positions.map(pos => <PlayerPiece team="opponents" key={pos} text={pos}/>) positions.map(key => ({team: Team.Opponents, key}))
) )
const [players, setPlayers] = useState<Player[]>([]); const [players, setPlayers] = useState<Player[]>([]);
const courtDivContentRef = useRef<HTMLDivElement>(null); const courtDivContentRef = useRef<HTMLDivElement>(null);
const canDetach = (ref: RefObject<HTMLDivElement>) => { const canDetach = (ref: HTMLDivElement) => {
const refBounds = ref.current!.getBoundingClientRect(); const refBounds = ref.getBoundingClientRect();
const courtBounds = courtDivContentRef.current!.getBoundingClientRect(); const courtBounds = courtDivContentRef.current!.getBoundingClientRect();
// check if refBounds overlaps courtBounds // check if refBounds overlaps courtBounds
@ -39,23 +48,23 @@ export default function Editor({id, name}: { id: number, name: string }) {
); );
} }
const onElementDetach = (ref: RefObject<HTMLDivElement>, element: ReactElement) => { const onPieceDetach = (ref: HTMLDivElement, element: RackedPlayer) => {
const refBounds = ref.current!.getBoundingClientRect(); const refBounds = ref.getBoundingClientRect();
const courtBounds = courtDivContentRef.current!.getBoundingClientRect(); const courtBounds = courtDivContentRef.current!.getBoundingClientRect();
const relativeXPixels = refBounds.x - courtBounds.x; const relativeXPixels = refBounds.x - courtBounds.x;
const relativeYPixels = refBounds.y - courtBounds.y; const relativeYPixels = refBounds.y - courtBounds.y;
const xPercent = relativeXPixels / courtBounds.width; const xRatio = relativeXPixels / courtBounds.width;
const yPercent = relativeYPixels / courtBounds.height; const yRatio = relativeYPixels / courtBounds.height;
setPlayers(players => { setPlayers(players => {
return [...players, { return [...players, {
id: players.length, id: players.length,
team: element.props.team, team: element.team,
position: element.props.text, role: element.key,
right_percentage: xPercent, rightRatio: xRatio,
bottom_percentage: yPercent bottomRatio: yRatio
}] }]
}) })
} }
@ -87,13 +96,17 @@ export default function Editor({id, name}: { id: number, name: string }) {
<div id="edit-div"> <div id="edit-div">
<div id="racks"> <div id="racks">
<Rack id="allies-rack" <Rack id="allies-rack"
objects={[allies, setAllies]} objects={allies}
onChange={setAllies}
canDetach={canDetach} canDetach={canDetach}
onElementDetached={onElementDetach}/> onElementDetached={onPieceDetach}
render={({team, key}) => <PlayerPiece team={team} text={key} key={key}/>}/>
<Rack id="opponent-rack" <Rack id="opponent-rack"
objects={[opponents, setOpponents]} objects={opponents}
onChange={setOpponents}
canDetach={canDetach} canDetach={canDetach}
onElementDetached={onElementDetach}/> onElementDetached={onPieceDetach}
render={({team, key}) => <PlayerPiece team={team} text={key} key={key}/>}/>
</div> </div>
<div id="court-div"> <div id="court-div">
<div id="court-div-bounds" ref={courtDivContentRef}> <div id="court-div-bounds" ref={courtDivContentRef}>
@ -104,16 +117,23 @@ export default function Editor({id, name}: { id: number, name: string }) {
const idx = players.indexOf(player) const idx = players.indexOf(player)
return players.toSpliced(idx, 1) return players.toSpliced(idx, 1)
}) })
const piece = <PlayerPiece team={player.team} key={player.position} text={player.position}/>
switch (player.team) { switch (player.team) {
case "opponents": case Team.Opponents:
setOpponents(opponents => ( setOpponents(opponents => (
[...opponents, piece] [...opponents, {
team: player.team,
pos: player.role,
key: player.role
}]
)) ))
break break
case "allies": case Team.Allies:
setAllies(allies => ( setAllies(allies => (
[...allies, piece] [...allies, {
team: player.team,
pos: player.role,
key: player.role
}]
)) ))
} }
}}/> }}/>

@ -6,7 +6,7 @@
"dom.iterable", "dom.iterable",
"esnext" "esnext"
], ],
"types": ["vite/client"], "types": ["vite/client", "vite-plugin-svgr/client"],
"allowJs": true, "allowJs": true,
"skipLibCheck": true, "skipLibCheck": true,
"esModuleInterop": true, "esModuleInterop": true,

@ -41,7 +41,7 @@ export default defineConfig({
relativeCSSInjection: true, relativeCSSInjection: true,
}), }),
svgr({ svgr({
include: "**/*.svg" include: "**/*.svg?react"
}) })
] ]
}) })

Loading…
Cancel
Save