|
|
|
@ -19,10 +19,20 @@ import { BallPiece } from "../components/editor/BallPiece"
|
|
|
|
|
import { Rack } from "../components/Rack"
|
|
|
|
|
import { PlayerPiece } from "../components/editor/PlayerPiece"
|
|
|
|
|
|
|
|
|
|
import { ComponentId, CourtType, StepContent, StepInfoNode, TacticComponent, TacticInfo } from "../model/tactic/Tactic"
|
|
|
|
|
import {
|
|
|
|
|
ComponentId,
|
|
|
|
|
CourtType,
|
|
|
|
|
StepContent,
|
|
|
|
|
StepInfoNode,
|
|
|
|
|
TacticComponent,
|
|
|
|
|
TacticInfo,
|
|
|
|
|
} from "../model/tactic/Tactic"
|
|
|
|
|
import { fetchAPI, fetchAPIGet } from "../Fetcher"
|
|
|
|
|
|
|
|
|
|
import SavingState, { SaveState, SaveStates } from "../components/editor/SavingState"
|
|
|
|
|
import SavingState, {
|
|
|
|
|
SaveState,
|
|
|
|
|
SaveStates,
|
|
|
|
|
} from "../components/editor/SavingState"
|
|
|
|
|
|
|
|
|
|
import { BALL_TYPE } from "../model/tactic/CourtObjects"
|
|
|
|
|
import { CourtAction } from "../components/editor/CourtAction"
|
|
|
|
@ -43,10 +53,19 @@ import {
|
|
|
|
|
updateComponent,
|
|
|
|
|
} from "../editor/TacticContentDomains"
|
|
|
|
|
|
|
|
|
|
import { BallState, Player, PlayerInfo, PlayerLike, PlayerTeam } from "../model/tactic/Player"
|
|
|
|
|
import {
|
|
|
|
|
BallState,
|
|
|
|
|
Player,
|
|
|
|
|
PlayerInfo,
|
|
|
|
|
PlayerLike,
|
|
|
|
|
PlayerTeam,
|
|
|
|
|
} from "../model/tactic/Player"
|
|
|
|
|
|
|
|
|
|
import { RackedCourtObject, RackedPlayer } from "../editor/RackedItems"
|
|
|
|
|
import { CourtPlayer, EditableCourtPlayer } from "../components/editor/CourtPlayer.tsx"
|
|
|
|
|
import {
|
|
|
|
|
CourtPlayer,
|
|
|
|
|
EditableCourtPlayer,
|
|
|
|
|
} from "../components/editor/CourtPlayer.tsx"
|
|
|
|
|
import {
|
|
|
|
|
createAction,
|
|
|
|
|
getActionKind,
|
|
|
|
@ -58,11 +77,21 @@ import ArrowAction from "../components/actions/ArrowAction"
|
|
|
|
|
import { middlePos, Pos, ratioWithinBase } from "../geo/Pos"
|
|
|
|
|
import { Action, ActionKind } from "../model/tactic/Action"
|
|
|
|
|
import BallAction from "../components/actions/BallAction"
|
|
|
|
|
import { computePhantomPositioning, getOrigin, removePlayer } from "../editor/PlayerDomains"
|
|
|
|
|
import {
|
|
|
|
|
computePhantomPositioning,
|
|
|
|
|
getOrigin,
|
|
|
|
|
removePlayer,
|
|
|
|
|
} from "../editor/PlayerDomains"
|
|
|
|
|
import { CourtBall } from "../components/editor/CourtBall"
|
|
|
|
|
import { useNavigate, useParams } from "react-router-dom"
|
|
|
|
|
import StepsTree from "../components/editor/StepsTree"
|
|
|
|
|
import { addStepNode, getAvailableId, getParent, getStepNode, removeStepNode } from "../editor/StepsDomain"
|
|
|
|
|
import {
|
|
|
|
|
addStepNode,
|
|
|
|
|
getAvailableId,
|
|
|
|
|
getParent,
|
|
|
|
|
getStepNode,
|
|
|
|
|
removeStepNode,
|
|
|
|
|
} from "../editor/StepsDomain"
|
|
|
|
|
|
|
|
|
|
const ERROR_STYLE: CSSProperties = {
|
|
|
|
|
borderColor: "red",
|
|
|
|
@ -105,13 +134,10 @@ function GuestModeEditor() {
|
|
|
|
|
GUEST_MODE_STEP_CONTENT_STORAGE_KEY + "0",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const stepInitialContent: ComputedStepContent = {
|
|
|
|
|
content: {
|
|
|
|
|
const stepInitialContent: StepContent = {
|
|
|
|
|
...(storageContent == null
|
|
|
|
|
? { components: [] }
|
|
|
|
|
: JSON.parse(storageContent)),
|
|
|
|
|
},
|
|
|
|
|
relativePositions: new Map(),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const rootStepNode: StepInfoNode = JSON.parse(
|
|
|
|
@ -130,6 +156,7 @@ function GuestModeEditor() {
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const courtRef = useRef<HTMLDivElement>(null)
|
|
|
|
|
const [stepId, setStepId] = useState(ROOT_STEP_ID)
|
|
|
|
|
const [stepContent, setStepContent, saveState] = useContentState(
|
|
|
|
|
stepInitialContent,
|
|
|
|
@ -137,46 +164,40 @@ function GuestModeEditor() {
|
|
|
|
|
useMemo(
|
|
|
|
|
() =>
|
|
|
|
|
debounceAsync(
|
|
|
|
|
async ({
|
|
|
|
|
content,
|
|
|
|
|
relativePositions,
|
|
|
|
|
}: ComputedStepContent) => {
|
|
|
|
|
async (content: StepContent) => {
|
|
|
|
|
localStorage.setItem(
|
|
|
|
|
GUEST_MODE_STEP_CONTENT_STORAGE_KEY + stepId,
|
|
|
|
|
JSON.stringify(content),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const terminalState = computeTerminalState(
|
|
|
|
|
content,
|
|
|
|
|
relativePositions,
|
|
|
|
|
)
|
|
|
|
|
const currentStepNode = getStepNode(
|
|
|
|
|
rootStepNode,
|
|
|
|
|
stepId,
|
|
|
|
|
)!
|
|
|
|
|
|
|
|
|
|
for (const child of currentStepNode.children) {
|
|
|
|
|
const childCurrentContent = getStepContent(child.id)
|
|
|
|
|
const childUpdatedContent =
|
|
|
|
|
drainTerminalStateOnChildContent(
|
|
|
|
|
terminalState,
|
|
|
|
|
childCurrentContent,
|
|
|
|
|
const stepsTree: StepInfoNode = JSON.parse(
|
|
|
|
|
localStorage.getItem(
|
|
|
|
|
GUEST_MODE_STEP_ROOT_NODE_INFO_STORAGE_KEY,
|
|
|
|
|
)!,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if (childUpdatedContent) {
|
|
|
|
|
localStorage.setItem(
|
|
|
|
|
GUEST_MODE_STEP_CONTENT_STORAGE_KEY +
|
|
|
|
|
await updateStepContents(
|
|
|
|
|
stepId,
|
|
|
|
|
JSON.stringify(childUpdatedContent),
|
|
|
|
|
stepsTree,
|
|
|
|
|
async (stepId) => {
|
|
|
|
|
const content = JSON.parse(localStorage.getItem(
|
|
|
|
|
GUEST_MODE_STEP_CONTENT_STORAGE_KEY + stepId,
|
|
|
|
|
)!)
|
|
|
|
|
const courtBounds = courtRef.current!.getBoundingClientRect()
|
|
|
|
|
const relativePositions = computeRelativePositions(courtBounds, content)
|
|
|
|
|
return { content, relativePositions }
|
|
|
|
|
},
|
|
|
|
|
async (stepId, content) => localStorage.setItem(
|
|
|
|
|
GUEST_MODE_STEP_CONTENT_STORAGE_KEY + stepId,
|
|
|
|
|
JSON.stringify(content),
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return SaveStates.Guest
|
|
|
|
|
},
|
|
|
|
|
250,
|
|
|
|
|
),
|
|
|
|
|
[rootStepNode, stepId],
|
|
|
|
|
[stepId],
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
@ -196,6 +217,7 @@ function GuestModeEditor() {
|
|
|
|
|
"Nouvelle Tactique",
|
|
|
|
|
courtType: "PLAIN",
|
|
|
|
|
}}
|
|
|
|
|
courtRef={courtRef}
|
|
|
|
|
currentStepContent={stepContent}
|
|
|
|
|
setCurrentStepContent={(content) => setStepContent(content, true)}
|
|
|
|
|
saveState={saveState}
|
|
|
|
@ -207,13 +229,7 @@ function GuestModeEditor() {
|
|
|
|
|
selectStep={useCallback(
|
|
|
|
|
(step) => {
|
|
|
|
|
setStepId(step)
|
|
|
|
|
setStepContent(
|
|
|
|
|
() => ({
|
|
|
|
|
content: getStepContent(step),
|
|
|
|
|
relativePositions: new Map(),
|
|
|
|
|
}),
|
|
|
|
|
false,
|
|
|
|
|
)
|
|
|
|
|
setStepContent(getStepContent(step), false)
|
|
|
|
|
return
|
|
|
|
|
},
|
|
|
|
|
[setStepContent],
|
|
|
|
@ -264,38 +280,41 @@ function UserModeEditor() {
|
|
|
|
|
const tacticId = parseInt(idStr!)
|
|
|
|
|
const navigation = useNavigate()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const courtRef = useRef<HTMLDivElement>(null)
|
|
|
|
|
const [stepId, setStepId] = useState(1)
|
|
|
|
|
|
|
|
|
|
const saveContent = useCallback(
|
|
|
|
|
async ({ content, relativePositions }: ComputedStepContent) => {
|
|
|
|
|
async (content: StepContent) => {
|
|
|
|
|
const response = await fetchAPI(
|
|
|
|
|
`tactics/${tacticId}/steps/${stepId}`,
|
|
|
|
|
{ content },
|
|
|
|
|
"PUT",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const terminalStateContent = computeTerminalState(
|
|
|
|
|
content,
|
|
|
|
|
relativePositions,
|
|
|
|
|
)
|
|
|
|
|
const currentNode = getStepNode(stepsTree!, stepId)!
|
|
|
|
|
|
|
|
|
|
const tasks = currentNode.children.map(async (child) => {
|
|
|
|
|
await updateStepContents(
|
|
|
|
|
stepId,
|
|
|
|
|
stepsTree,
|
|
|
|
|
async (id) => {
|
|
|
|
|
const response = await fetchAPIGet(
|
|
|
|
|
`tactics/${tacticId}/steps/${child.id}`,
|
|
|
|
|
`tactics/${tacticId}/steps/${id}`,
|
|
|
|
|
)
|
|
|
|
|
if (!response.ok)
|
|
|
|
|
throw new Error("Error when retrieving children content")
|
|
|
|
|
|
|
|
|
|
const childContent: StepContent = await response.json()
|
|
|
|
|
const childUpdatedContent = drainTerminalStateOnChildContent(
|
|
|
|
|
terminalStateContent,
|
|
|
|
|
childContent,
|
|
|
|
|
)
|
|
|
|
|
if (childUpdatedContent) {
|
|
|
|
|
|
|
|
|
|
const content = await response.json()
|
|
|
|
|
const courtBounds = courtRef.current!.getBoundingClientRect()
|
|
|
|
|
const relativePositions = computeRelativePositions(courtBounds, content)
|
|
|
|
|
return {
|
|
|
|
|
content,
|
|
|
|
|
relativePositions,
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
async (id, content) => {
|
|
|
|
|
const response = await fetchAPI(
|
|
|
|
|
`tactics/${tacticId}/steps/${child.id}`,
|
|
|
|
|
{ content: childUpdatedContent },
|
|
|
|
|
`tactics/${tacticId}/steps/${id}`,
|
|
|
|
|
{ content },
|
|
|
|
|
"PUT",
|
|
|
|
|
)
|
|
|
|
|
if (!response.ok) {
|
|
|
|
@ -303,12 +322,8 @@ function UserModeEditor() {
|
|
|
|
|
"Error when updated new children content",
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
for (const task of tasks) {
|
|
|
|
|
await task
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return response.ok ? SaveStates.Ok : SaveStates.Err
|
|
|
|
|
},
|
|
|
|
@ -316,11 +331,8 @@ function UserModeEditor() {
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const [stepContent, setStepContent, saveState] =
|
|
|
|
|
useContentState<ComputedStepContent>(
|
|
|
|
|
{
|
|
|
|
|
content: { components: [] },
|
|
|
|
|
relativePositions: new Map(),
|
|
|
|
|
},
|
|
|
|
|
useContentState<StepContent>(
|
|
|
|
|
{ components: [] },
|
|
|
|
|
SaveStates.Ok,
|
|
|
|
|
useMemo(() => debounceAsync(saveContent, 250), [saveContent]),
|
|
|
|
|
)
|
|
|
|
@ -352,7 +364,7 @@ function UserModeEditor() {
|
|
|
|
|
|
|
|
|
|
setTactic({ id: tacticId, name, courtType })
|
|
|
|
|
setStepsTree(root)
|
|
|
|
|
setStepContent({ content, relativePositions: new Map() }, false)
|
|
|
|
|
setStepContent(content, false)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (tactic === null) initialize()
|
|
|
|
@ -374,10 +386,7 @@ function UserModeEditor() {
|
|
|
|
|
if (!response.ok) return
|
|
|
|
|
setStepId(step)
|
|
|
|
|
setStepContent(
|
|
|
|
|
{
|
|
|
|
|
content: await response.json(),
|
|
|
|
|
relativePositions: new Map(),
|
|
|
|
|
},
|
|
|
|
|
await response.json(),
|
|
|
|
|
false,
|
|
|
|
|
)
|
|
|
|
|
},
|
|
|
|
@ -422,6 +431,7 @@ function UserModeEditor() {
|
|
|
|
|
rootStepNode: stepsTree,
|
|
|
|
|
courtType: tactic?.courtType,
|
|
|
|
|
}}
|
|
|
|
|
courtRef={courtRef}
|
|
|
|
|
currentStepId={stepId}
|
|
|
|
|
currentStepContent={stepContent}
|
|
|
|
|
setCurrentStepContent={(content) => setStepContent(content, true)}
|
|
|
|
@ -440,10 +450,12 @@ function EditorLoadingScreen() {
|
|
|
|
|
|
|
|
|
|
export interface EditorViewProps {
|
|
|
|
|
tactic: TacticInfo
|
|
|
|
|
currentStepContent: ComputedStepContent
|
|
|
|
|
currentStepContent: StepContent
|
|
|
|
|
currentStepId: number
|
|
|
|
|
saveState: SaveState
|
|
|
|
|
setCurrentStepContent: Dispatch<SetStateAction<ComputedStepContent>>
|
|
|
|
|
setCurrentStepContent: Dispatch<SetStateAction<StepContent>>
|
|
|
|
|
|
|
|
|
|
courtRef: RefObject<HTMLDivElement>
|
|
|
|
|
|
|
|
|
|
selectStep: (stepId: number) => void
|
|
|
|
|
onNameChange: (name: string) => Promise<boolean>
|
|
|
|
@ -457,13 +469,15 @@ export interface EditorViewProps {
|
|
|
|
|
function EditorPage({
|
|
|
|
|
tactic: { name, rootStepNode: initialStepsNode, courtType },
|
|
|
|
|
currentStepId,
|
|
|
|
|
setCurrentStepContent,
|
|
|
|
|
currentStepContent: { content, relativePositions },
|
|
|
|
|
setCurrentStepContent: setContent,
|
|
|
|
|
currentStepContent: content,
|
|
|
|
|
saveState,
|
|
|
|
|
onNameChange,
|
|
|
|
|
selectStep,
|
|
|
|
|
onRemoveStep,
|
|
|
|
|
onAddStep,
|
|
|
|
|
|
|
|
|
|
courtRef,
|
|
|
|
|
}: EditorViewProps) {
|
|
|
|
|
const [titleStyle, setTitleStyle] = useState<CSSProperties>({})
|
|
|
|
|
|
|
|
|
@ -486,33 +500,34 @@ function EditorPage({
|
|
|
|
|
|
|
|
|
|
const [isStepsTreeVisible, setStepsTreeVisible] = useState(false)
|
|
|
|
|
|
|
|
|
|
const courtRef = useRef<HTMLDivElement>(null)
|
|
|
|
|
const courtBounds = useCallback(
|
|
|
|
|
() => courtRef.current!.getBoundingClientRect(),
|
|
|
|
|
[courtRef],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const setContent = useCallback(
|
|
|
|
|
(newState: SetStateAction<StepContent>) => {
|
|
|
|
|
setCurrentStepContent((c) => {
|
|
|
|
|
const state =
|
|
|
|
|
typeof newState === "function"
|
|
|
|
|
? newState(c.content)
|
|
|
|
|
: newState
|
|
|
|
|
|
|
|
|
|
const relativePositions = useMemo(() => {
|
|
|
|
|
const courtBounds = courtRef.current?.getBoundingClientRect()
|
|
|
|
|
const relativePositions: ComputedRelativePositions = courtBounds
|
|
|
|
|
? computeRelativePositions(courtBounds, state)
|
|
|
|
|
: new Map()
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
content: state,
|
|
|
|
|
relativePositions,
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
},
|
|
|
|
|
[setCurrentStepContent],
|
|
|
|
|
)
|
|
|
|
|
return courtBounds ? computeRelativePositions(courtBounds, content) : new Map()
|
|
|
|
|
}, [content, courtRef])
|
|
|
|
|
|
|
|
|
|
// const setContent = useCallback(
|
|
|
|
|
// (newState: SetStateAction<StepContent>) => {
|
|
|
|
|
// setCurrentStepContent((c) => {
|
|
|
|
|
// const state =
|
|
|
|
|
// typeof newState === "function"
|
|
|
|
|
// ? newState(c.content)
|
|
|
|
|
// : newState
|
|
|
|
|
//
|
|
|
|
|
// const courtBounds = courtRef.current?.getBoundingClientRect()
|
|
|
|
|
// const relativePositions: ComputedRelativePositions = courtBounds
|
|
|
|
|
// ? computeRelativePositions(courtBounds, state)
|
|
|
|
|
// : new Map()
|
|
|
|
|
//
|
|
|
|
|
// return state
|
|
|
|
|
// })
|
|
|
|
|
// },
|
|
|
|
|
// [setCurrentStepContent],
|
|
|
|
|
// )
|
|
|
|
|
|
|
|
|
|
const setComponents = (action: SetStateAction<TacticComponent[]>) => {
|
|
|
|
|
setContent((c) => ({
|
|
|
|
@ -638,7 +653,8 @@ function EditorPage({
|
|
|
|
|
setContent={setContent}
|
|
|
|
|
/>
|
|
|
|
|
),
|
|
|
|
|
!isFrozen && (info.ballState === BallState.HOLDS_ORIGIN ||
|
|
|
|
|
!isFrozen &&
|
|
|
|
|
(info.ballState === BallState.HOLDS_ORIGIN ||
|
|
|
|
|
info.ballState === BallState.PASSED_ORIGIN) && (
|
|
|
|
|
<BallAction
|
|
|
|
|
key={2}
|
|
|
|
@ -674,11 +690,16 @@ function EditorPage({
|
|
|
|
|
info = component
|
|
|
|
|
|
|
|
|
|
if (component.frozen) {
|
|
|
|
|
return <CourtPlayer
|
|
|
|
|
return (
|
|
|
|
|
<CourtPlayer
|
|
|
|
|
key={component.id}
|
|
|
|
|
playerInfo={info}
|
|
|
|
|
className={"player"}
|
|
|
|
|
availableActions={() => renderAvailablePlayerActions(info, component)}
|
|
|
|
|
availableActions={() =>
|
|
|
|
|
renderAvailablePlayerActions(info, component)
|
|
|
|
|
}
|
|
|
|
|
/>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -1213,3 +1234,40 @@ function computeRelativePositions(courtBounds: DOMRect, content: StepContent) {
|
|
|
|
|
|
|
|
|
|
return relativePositionsCache
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function updateStepContents(stepId: number,
|
|
|
|
|
stepsTree: StepInfoNode,
|
|
|
|
|
getStepContent: (stepId: number) => Promise<ComputedStepContent>,
|
|
|
|
|
setStepContent: (stepId: number, content: StepContent) => Promise<void>,
|
|
|
|
|
) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function updateSteps(step: StepInfoNode, content: StepContent, relativePositions: ComputedRelativePositions) {
|
|
|
|
|
const terminalStateContent = computeTerminalState(
|
|
|
|
|
content,
|
|
|
|
|
relativePositions,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const tasks = step.children.map(async (child) => {
|
|
|
|
|
const { content: childContent, relativePositions: childRelativePositions } = await getStepContent(child.id)
|
|
|
|
|
const childUpdatedContent = drainTerminalStateOnChildContent(
|
|
|
|
|
terminalStateContent,
|
|
|
|
|
childContent,
|
|
|
|
|
)
|
|
|
|
|
if (childUpdatedContent) {
|
|
|
|
|
await setStepContent(child.id, childUpdatedContent)
|
|
|
|
|
await updateSteps(child, childUpdatedContent, childRelativePositions)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
for (const task of tasks) {
|
|
|
|
|
await task
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const { content, relativePositions } = await getStepContent(stepId)
|
|
|
|
|
const startNode = getStepNode(stepsTree!, stepId)!
|
|
|
|
|
|
|
|
|
|
await updateSteps(startNode, content, relativePositions)
|
|
|
|
|
}
|