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/court.svg" import halfCourt from "../assets/court/half_court.svg" 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" 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 } 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, 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 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, { team: element.team, role: element.key, rightRatio: x, bottomRatio: y, }, ], } }) } return (
LEFT
{ onNameChange(new_name).then((success) => { setTitleStyle(success ? {} : ERROR_STYLE) }) }} />
RIGHT
( )} /> ( )} />
{ 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] : [])) }