|
|
@ -1,6 +1,5 @@
|
|
|
|
import {
|
|
|
|
import {
|
|
|
|
CSSProperties,
|
|
|
|
CSSProperties,
|
|
|
|
Dispatch,
|
|
|
|
|
|
|
|
RefObject,
|
|
|
|
RefObject,
|
|
|
|
SetStateAction,
|
|
|
|
SetStateAction,
|
|
|
|
useCallback,
|
|
|
|
useCallback,
|
|
|
@ -25,9 +24,7 @@ import {
|
|
|
|
StepContent,
|
|
|
|
StepContent,
|
|
|
|
StepInfoNode,
|
|
|
|
StepInfoNode,
|
|
|
|
TacticComponent,
|
|
|
|
TacticComponent,
|
|
|
|
TacticInfo,
|
|
|
|
|
|
|
|
} from "../model/tactic/Tactic"
|
|
|
|
} from "../model/tactic/Tactic"
|
|
|
|
import { fetchAPI, fetchAPIGet } from "../Fetcher"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
import SavingState, {
|
|
|
|
import SavingState, {
|
|
|
|
SaveState,
|
|
|
|
SaveState,
|
|
|
@ -83,28 +80,23 @@ import {
|
|
|
|
removePlayer,
|
|
|
|
removePlayer,
|
|
|
|
} from "../editor/PlayerDomains"
|
|
|
|
} from "../editor/PlayerDomains"
|
|
|
|
import { CourtBall } from "../components/editor/CourtBall"
|
|
|
|
import { CourtBall } from "../components/editor/CourtBall"
|
|
|
|
import { useNavigate, useParams } from "react-router-dom"
|
|
|
|
|
|
|
|
import StepsTree from "../components/editor/StepsTree"
|
|
|
|
import StepsTree from "../components/editor/StepsTree"
|
|
|
|
import {
|
|
|
|
import {
|
|
|
|
addStepNode,
|
|
|
|
addStepNode,
|
|
|
|
getAvailableId,
|
|
|
|
|
|
|
|
getParent,
|
|
|
|
getParent,
|
|
|
|
getStepNode,
|
|
|
|
getStepNode,
|
|
|
|
removeStepNode,
|
|
|
|
removeStepNode,
|
|
|
|
} from "../editor/StepsDomain"
|
|
|
|
} from "../editor/StepsDomain"
|
|
|
|
import CurtainLayout from "../components/CurtainLayout"
|
|
|
|
import CurtainLayout from "../components/CurtainLayout"
|
|
|
|
|
|
|
|
import { ServiceError, TacticService } from "../service/TacticService.ts"
|
|
|
|
|
|
|
|
import { LocalStorageTacticService } from "../service/LocalStorageTacticService.ts"
|
|
|
|
|
|
|
|
import { APITacticService } from "../service/APITacticService.ts"
|
|
|
|
|
|
|
|
import { useParams } from "react-router-dom"
|
|
|
|
|
|
|
|
|
|
|
|
const ERROR_STYLE: CSSProperties = {
|
|
|
|
const ERROR_STYLE: CSSProperties = {
|
|
|
|
borderColor: "red",
|
|
|
|
borderColor: "red",
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const GUEST_MODE_STEP_CONTENT_STORAGE_KEY = "guest_mode_step"
|
|
|
|
|
|
|
|
const GUEST_MODE_STEP_ROOT_NODE_INFO_STORAGE_KEY = "guest_mode_step_tree"
|
|
|
|
|
|
|
|
const GUEST_MODE_TITLE_STORAGE_KEY = "guest_mode_title"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// The step identifier the editor will always open on
|
|
|
|
|
|
|
|
const GUEST_MODE_ROOT_STEP_ID = 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
type ComputedRelativePositions = Map<ComponentId, Pos>
|
|
|
|
type ComputedRelativePositions = Map<ComponentId, Pos>
|
|
|
|
|
|
|
|
|
|
|
|
type ComputedStepContent = {
|
|
|
|
type ComputedStepContent = {
|
|
|
@ -112,12 +104,6 @@ type ComputedStepContent = {
|
|
|
|
relativePositions: ComputedRelativePositions
|
|
|
|
relativePositions: ComputedRelativePositions
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface TacticDto {
|
|
|
|
|
|
|
|
id: number
|
|
|
|
|
|
|
|
name: string
|
|
|
|
|
|
|
|
courtType: CourtType
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export interface EditorPageProps {
|
|
|
|
export interface EditorPageProps {
|
|
|
|
guestMode: boolean
|
|
|
|
guestMode: boolean
|
|
|
|
}
|
|
|
|
}
|
|
|
@ -126,193 +112,56 @@ export default function Editor({ guestMode }: EditorPageProps) {
|
|
|
|
return <EditorPortal guestMode={guestMode} />
|
|
|
|
return <EditorPortal guestMode={guestMode} />
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function EditorPortal({ guestMode }: EditorPageProps) {
|
|
|
|
interface EditorService {
|
|
|
|
return guestMode ? <GuestModeEditor /> : <UserModeEditor />
|
|
|
|
addStep(
|
|
|
|
}
|
|
|
|
parent: StepInfoNode,
|
|
|
|
|
|
|
|
content: StepContent,
|
|
|
|
|
|
|
|
): Promise<StepInfoNode | ServiceError>
|
|
|
|
|
|
|
|
|
|
|
|
function GuestModeEditor() {
|
|
|
|
removeStep(step: number): Promise<void | ServiceError>
|
|
|
|
const storageContent = localStorage.getItem(
|
|
|
|
|
|
|
|
GUEST_MODE_STEP_CONTENT_STORAGE_KEY + GUEST_MODE_ROOT_STEP_ID,
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const stepInitialContent: StepContent = {
|
|
|
|
selectStep(step: number): Promise<void | ServiceError>
|
|
|
|
...(storageContent == null
|
|
|
|
|
|
|
|
? { components: [] }
|
|
|
|
|
|
|
|
: JSON.parse(storageContent)),
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const rootStepNode: StepInfoNode = JSON.parse(
|
|
|
|
setContent(content: SetStateAction<StepContent>): void
|
|
|
|
localStorage.getItem(GUEST_MODE_STEP_ROOT_NODE_INFO_STORAGE_KEY)!,
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// initialize local storage if we launch in guest mode
|
|
|
|
setName(name: string): Promise<SaveState>
|
|
|
|
if (storageContent == null) {
|
|
|
|
|
|
|
|
localStorage.setItem(
|
|
|
|
|
|
|
|
GUEST_MODE_STEP_ROOT_NODE_INFO_STORAGE_KEY,
|
|
|
|
|
|
|
|
JSON.stringify({ id: GUEST_MODE_ROOT_STEP_ID, children: [] }),
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
localStorage.setItem(
|
|
|
|
|
|
|
|
GUEST_MODE_STEP_CONTENT_STORAGE_KEY + GUEST_MODE_ROOT_STEP_ID,
|
|
|
|
|
|
|
|
JSON.stringify(stepInitialContent),
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const tacticName =
|
|
|
|
function EditorPortal({ guestMode }: EditorPageProps) {
|
|
|
|
localStorage.getItem(GUEST_MODE_TITLE_STORAGE_KEY) ??
|
|
|
|
const { tacticId: idStr } = useParams()
|
|
|
|
"Nouvelle Tactique"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const courtRef = useRef<HTMLDivElement>(null)
|
|
|
|
|
|
|
|
const [stepId, setStepId] = useState(GUEST_MODE_ROOT_STEP_ID)
|
|
|
|
|
|
|
|
const [stepContent, setStepContent, saveState] = useContentState(
|
|
|
|
|
|
|
|
stepInitialContent,
|
|
|
|
|
|
|
|
SaveStates.Guest,
|
|
|
|
|
|
|
|
useMemo(
|
|
|
|
|
|
|
|
() =>
|
|
|
|
|
|
|
|
debounceAsync(async (content: StepContent) => {
|
|
|
|
|
|
|
|
localStorage.setItem(
|
|
|
|
|
|
|
|
GUEST_MODE_STEP_CONTENT_STORAGE_KEY + stepId,
|
|
|
|
|
|
|
|
JSON.stringify(content),
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const stepsTree: StepInfoNode = JSON.parse(
|
|
|
|
|
|
|
|
localStorage.getItem(
|
|
|
|
|
|
|
|
GUEST_MODE_STEP_ROOT_NODE_INFO_STORAGE_KEY,
|
|
|
|
|
|
|
|
)!,
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
await updateStepContents(
|
|
|
|
|
|
|
|
stepId,
|
|
|
|
|
|
|
|
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),
|
|
|
|
|
|
|
|
[stepId],
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function getStepContent(step: number): StepContent {
|
|
|
|
if (guestMode || !idStr) {
|
|
|
|
return JSON.parse(
|
|
|
|
return <EditorPageWrapper service={LocalStorageTacticService.init()} />
|
|
|
|
localStorage.getItem(GUEST_MODE_STEP_CONTENT_STORAGE_KEY + step)!,
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
return <EditorPageWrapper service={new APITacticService(parseInt(idStr))} />
|
|
|
|
<EditorPage
|
|
|
|
|
|
|
|
tactic={{
|
|
|
|
|
|
|
|
id: -1,
|
|
|
|
|
|
|
|
rootStepNode,
|
|
|
|
|
|
|
|
name: tacticName,
|
|
|
|
|
|
|
|
courtType: "PLAIN",
|
|
|
|
|
|
|
|
}}
|
|
|
|
|
|
|
|
courtRef={courtRef}
|
|
|
|
|
|
|
|
currentStepContent={stepContent}
|
|
|
|
|
|
|
|
setCurrentStepContent={(content) => setStepContent(content, true)}
|
|
|
|
|
|
|
|
saveState={saveState}
|
|
|
|
|
|
|
|
currentStepId={stepId}
|
|
|
|
|
|
|
|
onNameChange={useCallback(async (name) => {
|
|
|
|
|
|
|
|
localStorage.setItem(GUEST_MODE_TITLE_STORAGE_KEY, name)
|
|
|
|
|
|
|
|
return true //simulate that the name has been changed
|
|
|
|
|
|
|
|
}, [])}
|
|
|
|
|
|
|
|
selectStep={useCallback(
|
|
|
|
|
|
|
|
(step) => {
|
|
|
|
|
|
|
|
setStepId(step)
|
|
|
|
|
|
|
|
setStepContent(getStepContent(step), false)
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
[setStepContent],
|
|
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
onAddStep={useCallback(async (parent, content) => {
|
|
|
|
|
|
|
|
const root: StepInfoNode = JSON.parse(
|
|
|
|
|
|
|
|
localStorage.getItem(
|
|
|
|
|
|
|
|
GUEST_MODE_STEP_ROOT_NODE_INFO_STORAGE_KEY,
|
|
|
|
|
|
|
|
)!,
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const nodeId = getAvailableId(root)
|
|
|
|
|
|
|
|
const node = { id: nodeId, children: [] }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const resultTree = addStepNode(root, parent, node)
|
|
|
|
|
|
|
|
localStorage.setItem(
|
|
|
|
|
|
|
|
GUEST_MODE_STEP_ROOT_NODE_INFO_STORAGE_KEY,
|
|
|
|
|
|
|
|
JSON.stringify(resultTree),
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
localStorage.setItem(
|
|
|
|
|
|
|
|
GUEST_MODE_STEP_CONTENT_STORAGE_KEY + node.id,
|
|
|
|
|
|
|
|
JSON.stringify(content),
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
return node
|
|
|
|
|
|
|
|
}, [])}
|
|
|
|
|
|
|
|
onRemoveStep={useCallback(async (step) => {
|
|
|
|
|
|
|
|
const root: StepInfoNode = JSON.parse(
|
|
|
|
|
|
|
|
localStorage.getItem(
|
|
|
|
|
|
|
|
GUEST_MODE_STEP_ROOT_NODE_INFO_STORAGE_KEY,
|
|
|
|
|
|
|
|
)!,
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
localStorage.setItem(
|
|
|
|
|
|
|
|
GUEST_MODE_STEP_ROOT_NODE_INFO_STORAGE_KEY,
|
|
|
|
|
|
|
|
JSON.stringify(removeStepNode(root, step)),
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
return true
|
|
|
|
|
|
|
|
}, [])}
|
|
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function UserModeEditor() {
|
|
|
|
function EditorPageWrapper({ service }: { service: TacticService }) {
|
|
|
|
const [tactic, setTactic] = useState<TacticDto | null>(null)
|
|
|
|
const [panicMessage, setPanicMessage] = useState<string>()
|
|
|
|
const [stepsTree, setStepsTree] = useState<StepInfoNode>({
|
|
|
|
const [stepId, setStepId] = useState<number>()
|
|
|
|
id: -1,
|
|
|
|
const [tacticName, setTacticName] = useState<string>()
|
|
|
|
children: [],
|
|
|
|
const [courtType, setCourtType] = useState<CourtType>()
|
|
|
|
})
|
|
|
|
const [stepsTree, setStepsTree] = useState<StepInfoNode>()
|
|
|
|
const { tacticId: idStr } = useParams()
|
|
|
|
|
|
|
|
const tacticId = parseInt(idStr!)
|
|
|
|
|
|
|
|
const navigation = useNavigate()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const courtRef = useRef<HTMLDivElement>(null)
|
|
|
|
const courtRef = useRef<HTMLDivElement>(null)
|
|
|
|
const [stepId, setStepId] = useState(-1)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const saveContent = useCallback(
|
|
|
|
const saveContent = useCallback(
|
|
|
|
async (content: StepContent) => {
|
|
|
|
async (content: StepContent) => {
|
|
|
|
const response = await fetchAPI(
|
|
|
|
const result = await service.saveContent(stepId!, content)
|
|
|
|
`tactics/${tacticId}/steps/${stepId}`,
|
|
|
|
|
|
|
|
{ content },
|
|
|
|
if (typeof result === "string") return SaveStates.Err
|
|
|
|
"PUT",
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
await updateStepContents(
|
|
|
|
await updateStepContents(
|
|
|
|
stepId,
|
|
|
|
stepId!,
|
|
|
|
stepsTree,
|
|
|
|
stepsTree!,
|
|
|
|
async (id) => {
|
|
|
|
async (id) => {
|
|
|
|
const response = await fetchAPIGet(
|
|
|
|
const content = await service.getContent(id)
|
|
|
|
`tactics/${tacticId}/steps/${id}`,
|
|
|
|
if (typeof content === "string")
|
|
|
|
)
|
|
|
|
|
|
|
|
if (!response.ok)
|
|
|
|
|
|
|
|
throw new Error(
|
|
|
|
throw new Error(
|
|
|
|
"Error when retrieving children content",
|
|
|
|
"Error when retrieving children content",
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
const content = await response.json()
|
|
|
|
|
|
|
|
const courtBounds =
|
|
|
|
const courtBounds =
|
|
|
|
courtRef.current!.getBoundingClientRect()
|
|
|
|
courtRef.current!.getBoundingClientRect()
|
|
|
|
const relativePositions = computeRelativePositions(
|
|
|
|
const relativePositions = computeRelativePositions(
|
|
|
@ -325,22 +174,14 @@ function UserModeEditor() {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
},
|
|
|
|
async (id, content) => {
|
|
|
|
async (id, content) => {
|
|
|
|
const response = await fetchAPI(
|
|
|
|
const result = await service.saveContent(id, content)
|
|
|
|
`tactics/${tacticId}/steps/${id}`,
|
|
|
|
if (typeof result === "string")
|
|
|
|
{ content },
|
|
|
|
throw new Error("Error when updating children content")
|
|
|
|
"PUT",
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
|
|
|
throw new Error(
|
|
|
|
|
|
|
|
"Error when updated new children content",
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
},
|
|
|
|
},
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
return SaveStates.Ok
|
|
|
|
return response.ok ? SaveStates.Ok : SaveStates.Err
|
|
|
|
|
|
|
|
},
|
|
|
|
},
|
|
|
|
[tacticId, stepId, stepsTree],
|
|
|
|
[service, stepId, stepsTree],
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
const [stepContent, setStepContent, saveState] =
|
|
|
|
const [stepContent, setStepContent, saveState] =
|
|
|
@ -351,153 +192,122 @@ function UserModeEditor() {
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
useEffect(() => {
|
|
|
|
async function initialize() {
|
|
|
|
async function init() {
|
|
|
|
const infoResponsePromise = fetchAPIGet(`tactics/${tacticId}`)
|
|
|
|
const contextResult = await service.getContext()
|
|
|
|
const treeResponsePromise = fetchAPIGet(`tactics/${tacticId}/tree`)
|
|
|
|
if (typeof contextResult === "string") {
|
|
|
|
|
|
|
|
setPanicMessage(
|
|
|
|
const infoResponse = await infoResponsePromise
|
|
|
|
"There has been an error retrieving the editor initial context : " +
|
|
|
|
const treeResponse = await treeResponsePromise
|
|
|
|
contextResult,
|
|
|
|
|
|
|
|
)
|
|
|
|
const { name, courtType } = await infoResponse.json()
|
|
|
|
|
|
|
|
const { root } = await treeResponse.json()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (infoResponse.status == 401 || treeResponse.status == 401) {
|
|
|
|
|
|
|
|
navigation("/login")
|
|
|
|
|
|
|
|
return
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const stepId = contextResult.stepsTree.id
|
|
|
|
|
|
|
|
setStepsTree(contextResult.stepsTree)
|
|
|
|
|
|
|
|
setStepId(stepId)
|
|
|
|
|
|
|
|
setCourtType(contextResult.courtType)
|
|
|
|
|
|
|
|
setTacticName(contextResult.name)
|
|
|
|
|
|
|
|
|
|
|
|
const contentResponsePromise = fetchAPIGet(
|
|
|
|
const contentResult = await service.getContent(stepId)
|
|
|
|
`tactics/${tacticId}/steps/${root.id}`,
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const contentResponse = await contentResponsePromise
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (contentResponse.status == 401) {
|
|
|
|
if (typeof contentResult === "string") {
|
|
|
|
navigation("/login")
|
|
|
|
setPanicMessage(
|
|
|
|
|
|
|
|
"There has been an error retrieving the tactic's root step content : " +
|
|
|
|
|
|
|
|
contentResult,
|
|
|
|
|
|
|
|
)
|
|
|
|
return
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
setStepContent(contentResult, false)
|
|
|
|
const content = await contentResponse.json()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
setTactic({ id: tacticId, name, courtType })
|
|
|
|
|
|
|
|
setStepsTree(root)
|
|
|
|
|
|
|
|
setStepId(root.id)
|
|
|
|
|
|
|
|
setStepContent(content, false)
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (tactic === null) initialize()
|
|
|
|
init()
|
|
|
|
}, [tactic, tacticId, idStr, navigation, setStepContent])
|
|
|
|
}, [service, setStepContent, stepId])
|
|
|
|
|
|
|
|
|
|
|
|
const onNameChange = useCallback(
|
|
|
|
const editorService: EditorService = useMemo(
|
|
|
|
(name: string) =>
|
|
|
|
() => ({
|
|
|
|
fetchAPI(`tactics/${tacticId}/name`, { name }, "PUT").then(
|
|
|
|
async addStep(
|
|
|
|
(r) => r.ok,
|
|
|
|
parent: StepInfoNode,
|
|
|
|
),
|
|
|
|
content: StepContent,
|
|
|
|
[tacticId],
|
|
|
|
): Promise<StepInfoNode | ServiceError> {
|
|
|
|
)
|
|
|
|
const result = await service.addStep(parent, content)
|
|
|
|
|
|
|
|
if (typeof result !== "string")
|
|
|
|
|
|
|
|
setStepsTree(addStepNode(stepsTree!, parent, result))
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
async removeStep(step: number): Promise<void | ServiceError> {
|
|
|
|
|
|
|
|
const result = await service.removeStep(step)
|
|
|
|
|
|
|
|
if (typeof result !== "string")
|
|
|
|
|
|
|
|
setStepsTree(removeStepNode(stepsTree!, step))
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
const selectStep = useCallback(
|
|
|
|
setContent(content: StepContent) {
|
|
|
|
async (step: number) => {
|
|
|
|
setStepContent(content, true)
|
|
|
|
const response = await fetchAPIGet(
|
|
|
|
|
|
|
|
`tactics/${tacticId}/steps/${step}`,
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
if (!response.ok) return
|
|
|
|
|
|
|
|
setStepId(step)
|
|
|
|
|
|
|
|
setStepContent(await response.json(), false)
|
|
|
|
|
|
|
|
},
|
|
|
|
},
|
|
|
|
[tacticId, setStepContent],
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const onAddStep = useCallback(
|
|
|
|
async setName(name: string): Promise<SaveState> {
|
|
|
|
async (parent: StepInfoNode, content: StepContent) => {
|
|
|
|
const result = await service.setName(name)
|
|
|
|
const response = await fetchAPI(`tactics/${tacticId}/steps`, {
|
|
|
|
if (typeof result === "string") return SaveStates.Err
|
|
|
|
parentId: parent.id,
|
|
|
|
setTacticName(name)
|
|
|
|
content,
|
|
|
|
return SaveStates.Ok
|
|
|
|
})
|
|
|
|
|
|
|
|
if (!response.ok) return null
|
|
|
|
|
|
|
|
const { stepId } = await response.json()
|
|
|
|
|
|
|
|
const child = { id: stepId, children: [] }
|
|
|
|
|
|
|
|
setStepsTree(addStepNode(stepsTree, parent, child))
|
|
|
|
|
|
|
|
return child
|
|
|
|
|
|
|
|
},
|
|
|
|
},
|
|
|
|
[tacticId, stepsTree],
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const onRemoveStep = useCallback(
|
|
|
|
async selectStep(step: number): Promise<void | ServiceError> {
|
|
|
|
async (step: StepInfoNode) => {
|
|
|
|
const result = await service.getContent(step)
|
|
|
|
const response = await fetchAPI(
|
|
|
|
if (typeof result === "string") return result
|
|
|
|
`tactics/${tacticId}/steps/${step.id}`,
|
|
|
|
setStepId(step)
|
|
|
|
{},
|
|
|
|
setStepContent(result, false)
|
|
|
|
"DELETE",
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
setStepsTree(removeStepNode(stepsTree, step)!)
|
|
|
|
|
|
|
|
return response.ok
|
|
|
|
|
|
|
|
},
|
|
|
|
},
|
|
|
|
[tacticId, stepsTree],
|
|
|
|
}),
|
|
|
|
|
|
|
|
[service, setStepContent, stepsTree],
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if (!tactic) return <EditorLoadingScreen />
|
|
|
|
if (panicMessage) {
|
|
|
|
|
|
|
|
return <p>{panicMessage}</p>
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!tacticName || !stepId || !stepsTree || !courtType) {
|
|
|
|
|
|
|
|
return <p>Retrieving editor context. Please wait...</p>
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
return (
|
|
|
|
<EditorPage
|
|
|
|
<EditorPage
|
|
|
|
tactic={{
|
|
|
|
name={tacticName}
|
|
|
|
id: tacticId,
|
|
|
|
courtType={courtType}
|
|
|
|
name: tactic?.name ?? "",
|
|
|
|
stepId={stepId}
|
|
|
|
rootStepNode: stepsTree,
|
|
|
|
stepsTree={stepsTree}
|
|
|
|
courtType: tactic?.courtType,
|
|
|
|
contentSaveState={saveState}
|
|
|
|
}}
|
|
|
|
content={stepContent}
|
|
|
|
|
|
|
|
service={editorService}
|
|
|
|
courtRef={courtRef}
|
|
|
|
courtRef={courtRef}
|
|
|
|
currentStepId={stepId}
|
|
|
|
|
|
|
|
currentStepContent={stepContent}
|
|
|
|
|
|
|
|
setCurrentStepContent={(content) => setStepContent(content, true)}
|
|
|
|
|
|
|
|
saveState={saveState}
|
|
|
|
|
|
|
|
onNameChange={onNameChange}
|
|
|
|
|
|
|
|
selectStep={selectStep}
|
|
|
|
|
|
|
|
onAddStep={onAddStep}
|
|
|
|
|
|
|
|
onRemoveStep={onRemoveStep}
|
|
|
|
|
|
|
|
/>
|
|
|
|
/>
|
|
|
|
)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function EditorLoadingScreen() {
|
|
|
|
|
|
|
|
return <p>Loading Editor, Please wait...</p>
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export interface EditorViewProps {
|
|
|
|
export interface EditorViewProps {
|
|
|
|
tactic: TacticInfo
|
|
|
|
stepsTree: StepInfoNode
|
|
|
|
currentStepContent: StepContent
|
|
|
|
name: string
|
|
|
|
currentStepId: number
|
|
|
|
courtType: CourtType
|
|
|
|
saveState: SaveState
|
|
|
|
content: StepContent
|
|
|
|
setCurrentStepContent: Dispatch<SetStateAction<StepContent>>
|
|
|
|
contentSaveState: SaveState
|
|
|
|
|
|
|
|
stepId: number
|
|
|
|
|
|
|
|
|
|
|
|
courtRef: RefObject<HTMLDivElement>
|
|
|
|
courtRef: RefObject<HTMLDivElement>
|
|
|
|
|
|
|
|
|
|
|
|
selectStep: (stepId: number) => void
|
|
|
|
service: EditorService
|
|
|
|
onNameChange: (name: string) => Promise<boolean>
|
|
|
|
|
|
|
|
onRemoveStep: (step: StepInfoNode) => Promise<boolean>
|
|
|
|
|
|
|
|
onAddStep: (
|
|
|
|
|
|
|
|
parent: StepInfoNode,
|
|
|
|
|
|
|
|
content: StepContent,
|
|
|
|
|
|
|
|
) => Promise<StepInfoNode | null>
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function EditorPage({
|
|
|
|
function EditorPage({
|
|
|
|
tactic: { name, rootStepNode: initialStepsNode, courtType },
|
|
|
|
name,
|
|
|
|
currentStepId,
|
|
|
|
courtType,
|
|
|
|
setCurrentStepContent: setContent,
|
|
|
|
content,
|
|
|
|
currentStepContent: content,
|
|
|
|
stepId,
|
|
|
|
saveState,
|
|
|
|
contentSaveState,
|
|
|
|
onNameChange,
|
|
|
|
stepsTree,
|
|
|
|
selectStep,
|
|
|
|
|
|
|
|
onRemoveStep,
|
|
|
|
|
|
|
|
onAddStep,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
courtRef,
|
|
|
|
courtRef,
|
|
|
|
|
|
|
|
service,
|
|
|
|
}: EditorViewProps) {
|
|
|
|
}: EditorViewProps) {
|
|
|
|
const [titleStyle, setTitleStyle] = useState<CSSProperties>({})
|
|
|
|
const [titleStyle, setTitleStyle] = useState<CSSProperties>({})
|
|
|
|
|
|
|
|
|
|
|
|
const [rootStepsNode, setRootStepsNode] = useState(initialStepsNode)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const allies = getRackPlayers(PlayerTeam.Allies, content.components)
|
|
|
|
const allies = getRackPlayers(PlayerTeam.Allies, content.components)
|
|
|
|
const opponents = getRackPlayers(PlayerTeam.Opponents, content.components)
|
|
|
|
const opponents = getRackPlayers(PlayerTeam.Opponents, content.components)
|
|
|
|
|
|
|
|
|
|
|
@ -527,7 +337,7 @@ function EditorPage({
|
|
|
|
}, [content, courtRef])
|
|
|
|
}, [content, courtRef])
|
|
|
|
|
|
|
|
|
|
|
|
const setComponents = (action: SetStateAction<TacticComponent[]>) => {
|
|
|
|
const setComponents = (action: SetStateAction<TacticComponent[]>) => {
|
|
|
|
setContent((c) => ({
|
|
|
|
service.setContent((c) => ({
|
|
|
|
...c,
|
|
|
|
...c,
|
|
|
|
components:
|
|
|
|
components:
|
|
|
|
typeof action == "function" ? action(c.components) : action,
|
|
|
|
typeof action == "function" ? action(c.components) : action,
|
|
|
@ -546,15 +356,15 @@ function EditorPage({
|
|
|
|
|
|
|
|
|
|
|
|
const doRemovePlayer = useCallback(
|
|
|
|
const doRemovePlayer = useCallback(
|
|
|
|
(component: PlayerLike) => {
|
|
|
|
(component: PlayerLike) => {
|
|
|
|
setContent((c) => removePlayer(component, c))
|
|
|
|
service.setContent((c) => removePlayer(component, c))
|
|
|
|
if (component.type == "player") insertRackedPlayer(component)
|
|
|
|
if (component.type == "player") insertRackedPlayer(component)
|
|
|
|
},
|
|
|
|
},
|
|
|
|
[setContent],
|
|
|
|
[service],
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
const doMoveBall = useCallback(
|
|
|
|
const doMoveBall = useCallback(
|
|
|
|
(newBounds: DOMRect, from?: PlayerLike) => {
|
|
|
|
(newBounds: DOMRect, from?: PlayerLike) => {
|
|
|
|
setContent((content) => {
|
|
|
|
service.setContent((content) => {
|
|
|
|
if (from) {
|
|
|
|
if (from) {
|
|
|
|
content =
|
|
|
|
content =
|
|
|
|
spreadNewStateFromOriginStateChange(
|
|
|
|
spreadNewStateFromOriginStateChange(
|
|
|
@ -569,12 +379,12 @@ function EditorPage({
|
|
|
|
return content
|
|
|
|
return content
|
|
|
|
})
|
|
|
|
})
|
|
|
|
},
|
|
|
|
},
|
|
|
|
[courtBounds, setContent],
|
|
|
|
[courtBounds, service],
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
const validatePlayerPosition = useCallback(
|
|
|
|
const validatePlayerPosition = useCallback(
|
|
|
|
(player: PlayerLike, info: PlayerInfo, newPos: Pos) => {
|
|
|
|
(player: PlayerLike, info: PlayerInfo, newPos: Pos) => {
|
|
|
|
setContent((content) =>
|
|
|
|
service.setContent((content) =>
|
|
|
|
moveComponent(
|
|
|
|
moveComponent(
|
|
|
|
newPos,
|
|
|
|
newPos,
|
|
|
|
player,
|
|
|
|
player,
|
|
|
@ -589,7 +399,7 @@ function EditorPage({
|
|
|
|
),
|
|
|
|
),
|
|
|
|
)
|
|
|
|
)
|
|
|
|
},
|
|
|
|
},
|
|
|
|
[courtBounds, setContent],
|
|
|
|
[courtBounds, service],
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
const renderAvailablePlayerActions = useCallback(
|
|
|
|
const renderAvailablePlayerActions = useCallback(
|
|
|
@ -631,7 +441,7 @@ function EditorPage({
|
|
|
|
playerInfo={info}
|
|
|
|
playerInfo={info}
|
|
|
|
content={content}
|
|
|
|
content={content}
|
|
|
|
courtRef={courtRef}
|
|
|
|
courtRef={courtRef}
|
|
|
|
setContent={setContent}
|
|
|
|
setContent={service.setContent}
|
|
|
|
/>
|
|
|
|
/>
|
|
|
|
),
|
|
|
|
),
|
|
|
|
!isFrozen &&
|
|
|
|
!isFrozen &&
|
|
|
@ -646,7 +456,13 @@ function EditorPage({
|
|
|
|
),
|
|
|
|
),
|
|
|
|
]
|
|
|
|
]
|
|
|
|
},
|
|
|
|
},
|
|
|
|
[content, courtRef, doMoveBall, previewAction?.isInvalid, setContent],
|
|
|
|
[
|
|
|
|
|
|
|
|
content,
|
|
|
|
|
|
|
|
courtRef,
|
|
|
|
|
|
|
|
doMoveBall,
|
|
|
|
|
|
|
|
previewAction?.isInvalid,
|
|
|
|
|
|
|
|
service.setContent,
|
|
|
|
|
|
|
|
],
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
const renderPlayer = useCallback(
|
|
|
|
const renderPlayer = useCallback(
|
|
|
@ -713,14 +529,14 @@ function EditorPage({
|
|
|
|
|
|
|
|
|
|
|
|
const doDeleteAction = useCallback(
|
|
|
|
const doDeleteAction = useCallback(
|
|
|
|
(_: Action, idx: number, origin: TacticComponent) => {
|
|
|
|
(_: Action, idx: number, origin: TacticComponent) => {
|
|
|
|
setContent((content) => removeAction(origin, idx, content))
|
|
|
|
service.setContent((content) => removeAction(origin, idx, content))
|
|
|
|
},
|
|
|
|
},
|
|
|
|
[setContent],
|
|
|
|
[service],
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
const doUpdateAction = useCallback(
|
|
|
|
const doUpdateAction = useCallback(
|
|
|
|
(component: TacticComponent, action: Action, actionIndex: number) => {
|
|
|
|
(component: TacticComponent, action: Action, actionIndex: number) => {
|
|
|
|
setContent((content) =>
|
|
|
|
service.setContent((content) =>
|
|
|
|
updateComponent(
|
|
|
|
updateComponent(
|
|
|
|
{
|
|
|
|
{
|
|
|
|
...component,
|
|
|
|
...component,
|
|
|
@ -734,7 +550,7 @@ function EditorPage({
|
|
|
|
),
|
|
|
|
),
|
|
|
|
)
|
|
|
|
)
|
|
|
|
},
|
|
|
|
},
|
|
|
|
[setContent],
|
|
|
|
[service],
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
const renderComponent = useCallback(
|
|
|
|
const renderComponent = useCallback(
|
|
|
@ -749,7 +565,7 @@ function EditorPage({
|
|
|
|
ball={component}
|
|
|
|
ball={component}
|
|
|
|
onPosValidated={doMoveBall}
|
|
|
|
onPosValidated={doMoveBall}
|
|
|
|
onRemove={() => {
|
|
|
|
onRemove={() => {
|
|
|
|
setContent((content) => removeBall(content))
|
|
|
|
service.setContent((content) => removeBall(content))
|
|
|
|
setObjects((objects) => [
|
|
|
|
setObjects((objects) => [
|
|
|
|
...objects,
|
|
|
|
...objects,
|
|
|
|
{ key: "ball" },
|
|
|
|
{ key: "ball" },
|
|
|
@ -760,7 +576,7 @@ function EditorPage({
|
|
|
|
}
|
|
|
|
}
|
|
|
|
throw new Error("unknown tactic component " + component)
|
|
|
|
throw new Error("unknown tactic component " + component)
|
|
|
|
},
|
|
|
|
},
|
|
|
|
[renderPlayer, doMoveBall, setContent],
|
|
|
|
[service, renderPlayer, doMoveBall],
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
const renderActions = useCallback(
|
|
|
|
const renderActions = useCallback(
|
|
|
@ -782,7 +598,7 @@ function EditorPage({
|
|
|
|
/>
|
|
|
|
/>
|
|
|
|
)
|
|
|
|
)
|
|
|
|
}),
|
|
|
|
}),
|
|
|
|
[courtRef, doDeleteAction, doUpdateAction, editorContentCurtainWidth],
|
|
|
|
[courtRef, doDeleteAction, doUpdateAction],
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
const contentNode = (
|
|
|
|
const contentNode = (
|
|
|
@ -809,7 +625,7 @@ function EditorPage({
|
|
|
|
)}
|
|
|
|
)}
|
|
|
|
onElementDetached={useCallback(
|
|
|
|
onElementDetached={useCallback(
|
|
|
|
(r, e: RackedCourtObject) =>
|
|
|
|
(r, e: RackedCourtObject) =>
|
|
|
|
setContent((content) =>
|
|
|
|
service.setContent((content) =>
|
|
|
|
placeObjectAt(
|
|
|
|
placeObjectAt(
|
|
|
|
r.getBoundingClientRect(),
|
|
|
|
r.getBoundingClientRect(),
|
|
|
|
courtBounds(),
|
|
|
|
courtBounds(),
|
|
|
@ -817,7 +633,7 @@ function EditorPage({
|
|
|
|
content,
|
|
|
|
content,
|
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
[courtBounds, setContent],
|
|
|
|
[courtBounds, service],
|
|
|
|
)}
|
|
|
|
)}
|
|
|
|
render={renderCourtObject}
|
|
|
|
render={renderCourtObject}
|
|
|
|
/>
|
|
|
|
/>
|
|
|
@ -846,41 +662,32 @@ function EditorPage({
|
|
|
|
|
|
|
|
|
|
|
|
const stepsTreeNode = (
|
|
|
|
const stepsTreeNode = (
|
|
|
|
<EditorStepsTree
|
|
|
|
<EditorStepsTree
|
|
|
|
selectedStepId={currentStepId}
|
|
|
|
selectedStepId={stepId}
|
|
|
|
root={rootStepsNode}
|
|
|
|
root={stepsTree}
|
|
|
|
onAddChildren={useCallback(
|
|
|
|
onAddChildren={useCallback(
|
|
|
|
async (parent) => {
|
|
|
|
async (parent) => {
|
|
|
|
const addedNode = await onAddStep(
|
|
|
|
const addedNode = await service.addStep(
|
|
|
|
parent,
|
|
|
|
parent,
|
|
|
|
computeTerminalState(content, relativePositions),
|
|
|
|
computeTerminalState(content, relativePositions),
|
|
|
|
)
|
|
|
|
)
|
|
|
|
if (addedNode == null) {
|
|
|
|
if (typeof addedNode === "string") {
|
|
|
|
console.error(
|
|
|
|
console.error("could not add step : " + addedNode)
|
|
|
|
"could not add step : onAddStep returned null node",
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
return
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
selectStep(addedNode.id)
|
|
|
|
await service.selectStep(addedNode.id)
|
|
|
|
setRootStepsNode((root) =>
|
|
|
|
|
|
|
|
addStepNode(root, parent, addedNode),
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
},
|
|
|
|
},
|
|
|
|
[content, onAddStep, selectStep, relativePositions],
|
|
|
|
[service, content, relativePositions],
|
|
|
|
)}
|
|
|
|
)}
|
|
|
|
onRemoveNode={useCallback(
|
|
|
|
onRemoveNode={useCallback(
|
|
|
|
async (removed) => {
|
|
|
|
async (removed) => {
|
|
|
|
const isOk = await onRemoveStep(removed)
|
|
|
|
await service.removeStep(removed.id)
|
|
|
|
selectStep(getParent(rootStepsNode, removed)!.id)
|
|
|
|
await service.selectStep(getParent(stepsTree, removed)!.id)
|
|
|
|
if (isOk)
|
|
|
|
|
|
|
|
setRootStepsNode(
|
|
|
|
|
|
|
|
(root) => removeStepNode(root, removed)!,
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
},
|
|
|
|
},
|
|
|
|
[rootStepsNode, onRemoveStep, selectStep],
|
|
|
|
[service, stepsTree],
|
|
|
|
)}
|
|
|
|
)}
|
|
|
|
onStepSelected={useCallback(
|
|
|
|
onStepSelected={useCallback(
|
|
|
|
(node) => selectStep(node.id),
|
|
|
|
(node) => service.selectStep(node.id),
|
|
|
|
[selectStep],
|
|
|
|
[service],
|
|
|
|
)}
|
|
|
|
)}
|
|
|
|
/>
|
|
|
|
/>
|
|
|
|
)
|
|
|
|
)
|
|
|
@ -889,7 +696,7 @@ function EditorPage({
|
|
|
|
<div id="main-div">
|
|
|
|
<div id="main-div">
|
|
|
|
<div id="topbar-div">
|
|
|
|
<div id="topbar-div">
|
|
|
|
<div id="topbar-left">
|
|
|
|
<div id="topbar-left">
|
|
|
|
<SavingState state={saveState} />
|
|
|
|
<SavingState state={contentSaveState} />
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div id="title-input-div">
|
|
|
|
<div id="title-input-div">
|
|
|
|
<TitleInput
|
|
|
|
<TitleInput
|
|
|
@ -897,11 +704,15 @@ function EditorPage({
|
|
|
|
default_value={name}
|
|
|
|
default_value={name}
|
|
|
|
onValidated={useCallback(
|
|
|
|
onValidated={useCallback(
|
|
|
|
(new_name) => {
|
|
|
|
(new_name) => {
|
|
|
|
onNameChange(new_name).then((success) => {
|
|
|
|
service.setName(new_name).then((state) => {
|
|
|
|
setTitleStyle(success ? {} : ERROR_STYLE)
|
|
|
|
setTitleStyle(
|
|
|
|
|
|
|
|
state == SaveStates.Ok
|
|
|
|
|
|
|
|
? {}
|
|
|
|
|
|
|
|
: ERROR_STYLE,
|
|
|
|
|
|
|
|
)
|
|
|
|
})
|
|
|
|
})
|
|
|
|
},
|
|
|
|
},
|
|
|
|
[onNameChange],
|
|
|
|
[service],
|
|
|
|
)}
|
|
|
|
)}
|
|
|
|
/>
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|