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 {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 SavingState, { SaveState, SaveStates, } from "../components/editor/SavingState" import Draggable from "react-draggable"; 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 } /** * information about a player that is into a rack */ interface RackedPlayer { team: Team key: string } export default function Editor({ id, name, content, }: { id: number name: string content: string }) { 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, ) }} /> ) } function EditorView({ tactic: {id, name, content: initialContent}, onContentChange, onNameChange, }: EditorViewProps) { const isInGuestMode = id == -1 const [style, setStyle] = 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 [showBall, setShowBall] = useState(true) const ballPiece = useRef(null) const courtDivContentRef = useRef(null) const canDetach = (ref: HTMLDivElement) => { const refBounds = ref.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 onPieceDetach = (ref: HTMLDivElement, element: RackedPlayer) => { const refBounds = ref.getBoundingClientRect() const courtBounds = courtDivContentRef.current!.getBoundingClientRect() const {x, y} = calculateRatio(refBounds, courtBounds) setContent((content) => { return { players: [ ...content.players, { id: "player-" + content.players.length, team: element.team, role: element.key, rightRatio: x, bottomRatio: y, hasBall: false }, ], } }) } const onBallDrop = (ref : HTMLDivElement) => { const ballBounds = ref.getBoundingClientRect() let showBall = true setContent(content => { const players = content.players.map(player => { 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) { showBall = false } return {...player, hasBall: doesOverlap} }) setShowBall(showBall) return {players: players} }) } return (
LEFT
{ onNameChange(new_name).then((success) => { setStyle(success ? {} : ERROR_STYLE) }) }} />
RIGHT
( )} /> {showBall && onBallDrop(ballPiece.current!)} pieceRef={ballPiece}/>} ( )} />
{ setContent((content) => ({ players: toSplicedPlayers( content.players, player, true, ), })) }} onPlayerRemove={(player) => { setContent((content) => ({ players: toSplicedPlayers( content.players, player, false, ), })) let setter switch (player.team) { case Team.Opponents: setter = setOpponents break case Team.Allies: setter = setAllies } setter((players) => [ ...players, { team: player.team, pos: player.role, key: player.role, }, ]) }} />
) } 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] : [])) }