import {CSSProperties, Dispatch, SetStateAction, useCallback, useRef, useState,} from "react" import "../style/editor.css" import TitleInput from "../components/TitleInput" import { BasketCourt } from "../components/editor/BasketCourt" import plainCourt from "../assets/court/full_court.svg" import halfCourt from "../assets/court/half_court.svg" import { Rack } from "../components/Rack" import { PlayerPiece } from "../components/editor/PlayerPiece" import {BallPiece, CourtBall} from "../components/editor/BallPiece"; 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 {CourtObject} from "../tactic/CourtObjects"; import {Simulate} from "react-dom/test-utils"; const ERROR_STYLE: CSSProperties = { borderColor: "red", } const GUEST_MODE_CONTENT_STORAGE_KEY = "guest_mode_content" const GUEST_MODE_TITLE_STORAGE_KEY = "guest_mode_title" 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" } /** * information about a player that is into a rack */ interface RackedPlayer { team: Team key: string } type RackedCourtObject = { key: "ball" } export default function Editor({ id, name, courtType, content, }: EditorProps) { const isInGuestMode = id == -1 const storage_content = localStorage.getItem(GUEST_MODE_CONTENT_STORAGE_KEY) const editorContent = 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 return ( { if (isInGuestMode) { localStorage.setItem( GUEST_MODE_CONTENT_STORAGE_KEY, JSON.stringify(content), ) return SaveStates.Guest } return fetchAPI(`tactic/${id}/save`, { content }).then((r) => r.ok ? SaveStates.Ok : SaveStates.Err, ) }} onNameChange={async (name: string) => { if (isInGuestMode) { 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( (r) => r.ok, ) }} courtType={courtType}/> ) } function EditorView({ tactic: {id, name, content: initialContent}, onContentChange, onNameChange, courtType, }: EditorViewProps) { const isInGuestMode = id == -1 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), ) const [opponents, setOpponents] = useState( getRackPlayers(Team.Opponents, content.players), ) const [objects, setObjects] = useState(isBallOnCourt(content) ? [] : [{key: "ball"}]) const courtDivContentRef = useRef(null) const canDetach = (bounds: DOMRect) => { const courtBounds = courtDivContentRef.current!.getBoundingClientRect() // check if refBounds overlaps courtBounds return !( bounds.top > courtBounds.bottom || bounds.right < courtBounds.left || bounds.bottom < courtBounds.top || bounds.left > courtBounds.right ) } const onPieceDetach = (ref: HTMLDivElement, element: RackedPlayer) => { const refBounds = ref.getBoundingClientRect() const courtBounds = courtDivContentRef.current!.getBoundingClientRect() const { x, y } = calculateRatio(refBounds, courtBounds) setContent((content) => { return { ...content, players: [ ...content.players, { id: "player-" + content.players.length, team: element.team, role: element.key, rightRatio: x, bottomRatio: y, hasBall: false, }, ], } }) } const onObjectDetach = (ref: HTMLDivElement, rackedObject: RackedCourtObject) => { const refBounds = ref.getBoundingClientRect() const courtBounds = courtDivContentRef.current!.getBoundingClientRect() const {x, y} = calculateRatio(refBounds, courtBounds) let courtObject: CourtObject switch (rackedObject.key) { case "ball": const ballObj = content.objects.findIndex(o => o.type == "ball") const playerCollidedIdx = getPlayerCollided(refBounds, content.players) if(playerCollidedIdx != -1) { onBallDropOnPlayer(playerCollidedIdx) setContent((content) => { return{ ...content, objects : content.objects.toSpliced(ballObj, 1) } }) return } else { courtObject = { type: "ball", rightRatio: x, bottomRatio: y } } break default: throw new Error("unknown court object ", rackedObject.key) } setContent((content) => { return ({ ...content, objects: [ ...content.objects, courtObject, ] }) }) } const getPlayerCollided = (bounds: DOMRect, players: Player[]): number | -1 => { for (let i = 0; i < players.length; i++) { const player = players[i] const playerBounds = document.getElementById(player.id)!.getBoundingClientRect() const doesOverlap = !( bounds.top > playerBounds.bottom || bounds.right < playerBounds.left || bounds.bottom < playerBounds.top || bounds.left > playerBounds.right ) if(doesOverlap) { return i } } return -1 } const onBallDropOnPlayer = (playerCollidedIdx : number) => { setContent((content) => { const ballObj = content.objects.findIndex(o => o.type == "ball") let player = content.players.at(playerCollidedIdx) as Player return { ...content, players: content.players.toSpliced(playerCollidedIdx, 1, {...player, hasBall: true}), objects : content.objects.toSpliced(ballObj, 1) } }) } const onBallDrop = (refBounds: DOMRect) => { const playerCollidedIdx = getPlayerCollided(refBounds, content.players) if(playerCollidedIdx != -1) { setContent((content) => { return { ...content, players: content.players.map((player) => ({...player, hasBall: false})), } }) onBallDropOnPlayer(playerCollidedIdx) return } if(content.objects.findIndex(o => o.type == "ball") != -1) { return } const courtBounds = courtDivContentRef.current!.getBoundingClientRect() const {x, y} = calculateRatio(refBounds, courtBounds) let courtObject: CourtObject courtObject = { type: "ball", rightRatio: x, bottomRatio: y } setContent((content) => { return { ...content, players: content.players.map((player) => ({...player, hasBall: false})), objects: [ ...content.objects, courtObject, ] } }) } return (
{ onNameChange(new_name).then((success) => { setTitleStyle(success ? {} : ERROR_STYLE) }) }} />
canDetach(div.getBoundingClientRect())} onElementDetached={onPieceDetach} render={({ team, key }) => ( )} /> canDetach(div.getBoundingClientRect())} onElementDetached={onObjectDetach} render={renderCourtObject}/> canDetach(div.getBoundingClientRect())} onElementDetached={onPieceDetach} render={({ team, key }) => ( )} />
{ setContent((content) => ({ ...content, players: toSplicedPlayers( content.players, player, true, ), })) }} onPlayerRemove={(player) => { setContent((content) => ({ ...content, players: toSplicedPlayers( content.players, player, false, ), objects: [ ...content.objects, ] })) let setter switch (player.team) { case Team.Opponents: setter = setOpponents break case Team.Allies: setter = setAllies } if (player.hasBall) { setObjects([{key: "ball"}]) } setter((players) => [ ...players, { team: player.team, pos: player.role, key: player.role, }, ]) }} />
) } function isBallOnCourt(content : TacticContent) { if(content.players.findIndex(p => p.hasBall) != -1) { return true } return content.objects.findIndex(o => o.type == "ball") != -1 } function renderCourtObject(courtObject: RackedCourtObject) { if (courtObject.key == "ball") { return } throw new Error("unknown racked court object ", courtObject.key) } function getRackPlayers(team: Team, players: Player[]): RackedPlayer[] { return ["1", "2", "3", "4", "5"] .filter( (role) => players.findIndex((p) => p.team == team && p.role == role) == -1, ) .map((key) => ({ team, key })) } function useContentState( initialContent: S, initialSaveState: SaveState, saveStateCallback: (s: S) => Promise, ): [S, Dispatch>, SaveState] { const [content, setContent] = useState(initialContent) const [savingState, setSavingState] = useState(initialSaveState) const setContentSynced = useCallback( (newState: SetStateAction) => { setContent((content) => { const state = typeof newState === "function" ? (newState as (state: S) => S)(content) : newState if (state !== content) { setSavingState(SaveStates.Saving) saveStateCallback(state) .then(setSavingState) .catch(() => setSavingState(SaveStates.Err)) } return state }) }, [saveStateCallback], ) return [content, setContentSynced, savingState] } function toSplicedPlayers( players: Player[], player: Player, replace: boolean, ): Player[] { const idx = players.findIndex( (p) => p.team === player.team && p.role === player.role, ) return players.toSpliced(idx, 1, ...(replace ? [player] : [])) }