From 5439fc6c47e49d1c432c69010a1a7fddf88203c7 Mon Sep 17 00:00:00 2001 From: Override-6 Date: Fri, 10 Nov 2023 00:14:39 +0100 Subject: [PATCH] drag and drop players and opponents from racks --- front/components/Rack.tsx | 59 +++++++++++++++ front/components/editor/BasketCourt.tsx | 73 ++++++++----------- .../editor/{Player.tsx => CourtPlayer.tsx} | 13 ++-- front/components/editor/PlayerPiece.tsx | 11 +++ front/data/Player.ts | 22 ++++++ front/style/editor.css | 13 ++++ front/style/player.css | 2 +- front/views/Editor.tsx | 69 +++++++++++++++++- 8 files changed, 207 insertions(+), 55 deletions(-) create mode 100644 front/components/Rack.tsx rename front/components/editor/{Player.tsx => CourtPlayer.tsx} (80%) create mode 100644 front/components/editor/PlayerPiece.tsx create mode 100644 front/data/Player.ts diff --git a/front/components/Rack.tsx b/front/components/Rack.tsx new file mode 100644 index 0000000..ad8f354 --- /dev/null +++ b/front/components/Rack.tsx @@ -0,0 +1,59 @@ +import {Dispatch, ReactElement, RefObject, SetStateAction, useRef} from "react"; +import Draggable from "react-draggable"; + +export interface RackInput { + id: string, + objects: [ReactElement[], Dispatch>], + canDetach: (ref: RefObject) => boolean, + onElementDetached: (ref: RefObject, el: ReactElement) => void, +} + +interface RackItemInput { + item: ReactElement, + onTryDetach: (ref: RefObject, el: ReactElement) => void +} + +/** + * A container of draggable objects + * */ +export function Rack({id, objects, canDetach, onElementDetached}: RackInput) { + + const [rackObjects, setRackObjects] = objects + + return ( +
+ {rackObjects.map(element => ( + { + if (!canDetach(ref)) + return + + setRackObjects(objects => { + const index = objects.findIndex(o => o.key === element.key) + return objects.toSpliced(index, 1); + }) + + onElementDetached(ref, element) + }}/> + ))} +
+ ) +} + +function RackItem({item, onTryDetach}: RackItemInput) { + const divRef = useRef(null); + + return ( + onTryDetach(divRef, item)}> +
+ {item} +
+
+ ) +} \ No newline at end of file diff --git a/front/components/editor/BasketCourt.tsx b/front/components/editor/BasketCourt.tsx index c54224c..feb4637 100644 --- a/front/components/editor/BasketCourt.tsx +++ b/front/components/editor/BasketCourt.tsx @@ -1,54 +1,39 @@ import CourtSvg from '../../assets/basketball_court.svg'; import '../../style/basket_court.css'; -import React, {MouseEvent, ReactElement, useEffect, useRef, useState} from "react"; -import Player from "./Player"; -import Draggable from "react-draggable"; +import {MouseEvent, ReactElement, useEffect, useRef, useState} from "react"; +import CourtPlayer from "./CourtPlayer"; +import {Player} from "../../data/Player"; -const TEAM_MAX_PLAYER = 5; - -export function BasketCourt() { - const [players, setPlayers] = useState([]) +export function BasketCourt({players}: { players: Player[] }) { + const [courtPlayers, setCourtPlayers] = useState([]) const divRef = useRef(null); + useEffect(() => { + const bounds = divRef.current!.getBoundingClientRect(); + setCourtPlayers(players.map(player => { + return ( + { + // setCourtPlayers(players => { + // // recompute the player's index as it may have been moved if + // // previous players were removed and added. + // const playerCurrentIndex = players.findIndex(p => p.key === playerIndex.toString()) + // return players.toSpliced(playerCurrentIndex, 1) + // }) + }} + /> + ) + })) + }, [players, divRef]); + return (
- { - const bounds = divRef.current!.getBoundingClientRect(); - - if (players.length >= TEAM_MAX_PLAYER) { - return; - } - - // find a valid number for the player to place. - let playerIndex = players.findIndex((v, i) => - v.key !== i.toString() - ); - - if (playerIndex == -1) { - playerIndex = players.length; - } - - const player = ( - { - setPlayers(players => { - // recompute the player's index as it may have been moved if - // previous players were removed and added. - const playerCurrentIndex = players.findIndex(p => p.key === playerIndex.toString()) - return players.toSpliced(playerCurrentIndex, 1) - }) - }} - /> - ); - setPlayers(players => players.toSpliced(playerIndex, 0, player)) - }}/> - {players} + + {courtPlayers}
) } diff --git a/front/components/editor/Player.tsx b/front/components/editor/CourtPlayer.tsx similarity index 80% rename from front/components/editor/Player.tsx rename to front/components/editor/CourtPlayer.tsx index 8d3f1ff..701be4f 100644 --- a/front/components/editor/Player.tsx +++ b/front/components/editor/CourtPlayer.tsx @@ -2,16 +2,20 @@ import React, {useRef} from "react"; import "../../style/player.css"; import RemoveIcon from "../../assets/icon/remove.svg"; import Draggable, {DraggableBounds} from "react-draggable"; +import {PlayerPiece} from "./PlayerPiece"; export interface PlayerOptions { - id: number, + pos: string, x: number, y: number, bounds: DraggableBounds, onRemove: () => void } -export default function Player({id, x, y, bounds, onRemove}: PlayerOptions) { +/** + * A player that is placed on the court, which can be selected, and moved in the associated bounds + * */ +export default function CourtPlayer({pos, x, y, bounds, onRemove}: PlayerOptions) { const ref = useRef(null); return ( @@ -38,10 +42,7 @@ export default function Player({id, x, y, bounds, onRemove}: PlayerOptions) { className="player-selection-tab-remove" onClick={() => onRemove()}/> -
-

{id}

-
+ diff --git a/front/components/editor/PlayerPiece.tsx b/front/components/editor/PlayerPiece.tsx new file mode 100644 index 0000000..17accac --- /dev/null +++ b/front/components/editor/PlayerPiece.tsx @@ -0,0 +1,11 @@ +import React from "react"; +import '../../style/player.css' + + +export function PlayerPiece({text}: { text: string }) { + return ( +
+

{text}

+
+ ) +} \ No newline at end of file diff --git a/front/data/Player.ts b/front/data/Player.ts new file mode 100644 index 0000000..da66ae3 --- /dev/null +++ b/front/data/Player.ts @@ -0,0 +1,22 @@ +export interface Player { + /** + * unique identifier of the player. + * This identifier must be unique to the associated court. + */ + id: number, + + /** + * player's position + * */ + position: string, + + /** + * Percentage of the player's position to the bottom (0 means top, 1 means bottom, 0.5 means middle) + */ + 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 diff --git a/front/style/editor.css b/front/style/editor.css index 62c4e9d..15bf69f 100644 --- a/front/style/editor.css +++ b/front/style/editor.css @@ -18,10 +18,23 @@ align-items: stretch; } +#racks { + display: flex; + justify-content: space-between; +} + .title_input { width: 25ch; } +#edit-div { + height: 100%; +} + +#team-rack .player-piece , #opponent-rack .player-piece { + margin-left: 5px; +} + #court-div { background-color: var(--background-color); height: 100%; diff --git a/front/style/player.css b/front/style/player.css index 8fbf487..ebd0462 100644 --- a/front/style/player.css +++ b/front/style/player.css @@ -10,7 +10,7 @@ on the court. .player-content { /*apply a translation to center the player piece when placed*/ - transform: translate(-50%, -75%); + transform: translate(-29%, -46%); display: flex; flex-direction: column; diff --git a/front/views/Editor.tsx b/front/views/Editor.tsx index 471a94d..89858f0 100644 --- a/front/views/Editor.tsx +++ b/front/views/Editor.tsx @@ -1,17 +1,64 @@ -import React, {CSSProperties, useState} from "react"; +import {CSSProperties, ReactElement, RefObject, useRef, useState} from "react"; import "../style/editor.css"; import TitleInput from "../components/TitleInput"; import {API} from "../Constants"; import {BasketCourt} from "../components/editor/BasketCourt"; +import {Rack} from "../components/Rack"; +import {PlayerPiece} from "../components/editor/PlayerPiece"; +import {Player} from "../data/Player"; + const ERROR_STYLE: CSSProperties = { borderColor: "red" } export default function Editor({id, name}: { id: number, name: string }) { - const [style, setStyle] = useState({}); + const positions = ["PG", "SG", "SF", "PF", "C"] + const [team, setTeams] = useState( + positions.map(pos => ) + ) + const [opponents, setOpponents] = useState( + positions.map(pos => ) + ) + + const [players, setPlayers] = useState([]); + const courtDivContentRef = useRef(null); + + const canDetach = (ref: RefObject) => { + const refBounds = ref.current!.getBoundingClientRect(); + const courtBounds = courtDivContentRef.current!.getBoundingClientRect(); + + // check if refBounds overlaps courtBounds + return !( + refBounds.top > courtBounds.bottom || + refBounds.right < courtBounds.left || + refBounds.bottom < courtBounds.top || + refBounds.left > courtBounds.right + ); + } + + const onElementDetach = (ref: RefObject, element: ReactElement) => { + const refBounds = ref.current!.getBoundingClientRect(); + const courtBounds = courtDivContentRef.current!.getBoundingClientRect(); + + const relativeXPixels = refBounds.x - courtBounds.x; + const relativeYPixels = refBounds.y - courtBounds.y; + + const xPercent = relativeXPixels / courtBounds.width; + const yPercent = relativeYPixels / courtBounds.height; + + setPlayers(players => { + return [...players, { + id: players.length, + position: element.props.text, + right_percentage: xPercent, + bottom_percentage: yPercent + }] + }) + } + return (
@@ -36,8 +83,22 @@ export default function Editor({id, name}: { id: number, name: string }) { }}/>
RIGHT
-
- +
+
+ + +
+
+
+ +
+
)