WIP: tree-slider
continuous-integration/drone/push Build is failing Details

maxime.batista 1 year ago
parent 4fe1ddfbd2
commit a8a00ea687

@ -0,0 +1,66 @@
import {ReactNode, useCallback, useEffect, useRef, useState} from "react";
export interface SlideLayoutProps {
children: ReactNode[2]
}
export default function CurtainLayout({children}: SlideLayoutProps) {
const [rightWidth, setRightWidth] = useState(80)
const curtainRef = useRef<HTMLDivElement>(null)
const sliderRef = useRef<HTMLDivElement>(null)
const resize = useCallback((e: MouseEvent) => {
const sliderPosX = e.clientX
const curtainWidth = curtainRef.current!.getBoundingClientRect().width
setRightWidth((sliderPosX / curtainWidth) * 100)
}, [curtainRef, setRightWidth])
const [resizing, setResizing] = useState(false)
useEffect(() => {
const curtain = curtainRef.current!
const slider = sliderRef.current!
if (resizing) {
const handleMouseUp = () => setResizing(false)
curtain.addEventListener('mousemove', resize)
curtain.addEventListener('mouseup', handleMouseUp)
return () => {
curtain.removeEventListener('mousemove', resize)
curtain.removeEventListener('mouseup', handleMouseUp)
}
}
const handleMouseDown = () => setResizing(true)
slider.addEventListener('mousedown', handleMouseDown)
return () => {
slider.removeEventListener('mousedown', handleMouseDown)
}
}, [sliderRef, curtainRef, resizing, setResizing])
return (
<div className={"curtain"} ref={curtainRef} style={{display: "flex"}}>
<div className={"curtain-left"} style={{width: `${rightWidth}%`}}>
{children[0]}
</div>
<div ref={sliderRef}
style={{
width: 2,
height: "100%",
backgroundColor: "grey",
cursor: "col-resize"
}}>
</div>
<div className={"curtain-right"} style={{width: `${100 - rightWidth}%`}}>
{children[1]}
</div>
</div>
)
}

@ -14,10 +14,10 @@ import TitleInput from "../components/TitleInput"
import PlainCourt from "../assets/court/full_court.svg?react" import PlainCourt from "../assets/court/full_court.svg?react"
import HalfCourt from "../assets/court/half_court.svg?react" import HalfCourt from "../assets/court/half_court.svg?react"
import { BallPiece } from "../components/editor/BallPiece" import {BallPiece} from "../components/editor/BallPiece"
import { Rack } from "../components/Rack" import {Rack} from "../components/Rack"
import { PlayerPiece } from "../components/editor/PlayerPiece" import {PlayerPiece} from "../components/editor/PlayerPiece"
import { import {
ComponentId, ComponentId,
@ -27,17 +27,17 @@ import {
TacticComponent, TacticComponent,
TacticInfo, TacticInfo,
} from "../model/tactic/Tactic" } from "../model/tactic/Tactic"
import { fetchAPI, fetchAPIGet } from "../Fetcher" import {fetchAPI, fetchAPIGet} from "../Fetcher"
import SavingState, { import SavingState, {
SaveState, SaveState,
SaveStates, SaveStates,
} from "../components/editor/SavingState" } from "../components/editor/SavingState"
import { BALL_TYPE } from "../model/tactic/CourtObjects" import {BALL_TYPE} from "../model/tactic/CourtObjects"
import { CourtAction } from "../components/editor/CourtAction" import {CourtAction} from "../components/editor/CourtAction"
import { ActionPreview, BasketCourt } from "../components/editor/BasketCourt" import {ActionPreview, BasketCourt} from "../components/editor/BasketCourt"
import { overlaps } from "../geo/Box" import {overlaps} from "../geo/Box"
import { import {
computeTerminalState, computeTerminalState,
@ -61,7 +61,7 @@ import {
PlayerTeam, PlayerTeam,
} from "../model/tactic/Player" } from "../model/tactic/Player"
import { RackedCourtObject, RackedPlayer } from "../editor/RackedItems" import {RackedCourtObject, RackedPlayer} from "../editor/RackedItems"
import { import {
CourtPlayer, CourtPlayer,
EditableCourtPlayer, EditableCourtPlayer,
@ -74,16 +74,16 @@ import {
spreadNewStateFromOriginStateChange, spreadNewStateFromOriginStateChange,
} from "../editor/ActionsDomains" } from "../editor/ActionsDomains"
import ArrowAction from "../components/actions/ArrowAction" import ArrowAction from "../components/actions/ArrowAction"
import { middlePos, Pos, ratioWithinBase } from "../geo/Pos" import {middlePos, Pos, ratioWithinBase} from "../geo/Pos"
import { Action, ActionKind } from "../model/tactic/Action" import {Action, ActionKind} from "../model/tactic/Action"
import BallAction from "../components/actions/BallAction" import BallAction from "../components/actions/BallAction"
import { import {
computePhantomPositioning, computePhantomPositioning,
getOrigin, getOrigin,
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 {useNavigate, useParams} from "react-router-dom"
import StepsTree from "../components/editor/StepsTree" import StepsTree from "../components/editor/StepsTree"
import { import {
addStepNode, addStepNode,
@ -92,6 +92,7 @@ import {
getStepNode, getStepNode,
removeStepNode, removeStepNode,
} from "../editor/StepsDomain" } from "../editor/StepsDomain"
import CurtainLayout from "../components/CurtainLayout";
const ERROR_STYLE: CSSProperties = { const ERROR_STYLE: CSSProperties = {
borderColor: "red", borderColor: "red",
@ -121,12 +122,12 @@ export interface EditorPageProps {
guestMode: boolean guestMode: boolean
} }
export default function Editor({ guestMode }: EditorPageProps) { export default function Editor({guestMode}: EditorPageProps) {
return <EditorPortal guestMode={guestMode} /> return <EditorPortal guestMode={guestMode}/>
} }
function EditorPortal({ guestMode }: EditorPageProps) { function EditorPortal({guestMode}: EditorPageProps) {
return guestMode ? <GuestModeEditor /> : <UserModeEditor /> return guestMode ? <GuestModeEditor/> : <UserModeEditor/>
} }
function GuestModeEditor() { function GuestModeEditor() {
@ -136,7 +137,7 @@ function GuestModeEditor() {
const stepInitialContent: StepContent = { const stepInitialContent: StepContent = {
...(storageContent == null ...(storageContent == null
? { components: [] } ? {components: []}
: JSON.parse(storageContent)), : JSON.parse(storageContent)),
} }
@ -148,7 +149,7 @@ function GuestModeEditor() {
if (storageContent == null) { if (storageContent == null) {
localStorage.setItem( localStorage.setItem(
GUEST_MODE_STEP_ROOT_NODE_INFO_STORAGE_KEY, GUEST_MODE_STEP_ROOT_NODE_INFO_STORAGE_KEY,
JSON.stringify({ id: GUEST_MODE_ROOT_STEP_ID, children: [] }), JSON.stringify({id: GUEST_MODE_ROOT_STEP_ID, children: []}),
) )
localStorage.setItem( localStorage.setItem(
GUEST_MODE_STEP_CONTENT_STORAGE_KEY + GUEST_MODE_ROOT_STEP_ID, GUEST_MODE_STEP_CONTENT_STORAGE_KEY + GUEST_MODE_ROOT_STEP_ID,
@ -186,7 +187,7 @@ function GuestModeEditor() {
const content = JSON.parse( const content = JSON.parse(
localStorage.getItem( localStorage.getItem(
GUEST_MODE_STEP_CONTENT_STORAGE_KEY + GUEST_MODE_STEP_CONTENT_STORAGE_KEY +
stepId, stepId,
)!, )!,
) )
const courtBounds = const courtBounds =
@ -195,7 +196,7 @@ function GuestModeEditor() {
courtBounds, courtBounds,
content, content,
) )
return { content, relativePositions } return {content, relativePositions}
}, },
async (stepId, content) => async (stepId, content) =>
localStorage.setItem( localStorage.setItem(
@ -249,7 +250,7 @@ function GuestModeEditor() {
) )
const nodeId = getAvailableId(root) const nodeId = getAvailableId(root)
const node = { id: nodeId, children: [] } const node = {id: nodeId, children: []}
const resultTree = addStepNode(root, parent, node) const resultTree = addStepNode(root, parent, node)
localStorage.setItem( localStorage.setItem(
@ -284,7 +285,7 @@ function UserModeEditor() {
id: -1, id: -1,
children: [], children: [],
}) })
const { tacticId: idStr } = useParams() const {tacticId: idStr} = useParams()
const tacticId = parseInt(idStr!) const tacticId = parseInt(idStr!)
const navigation = useNavigate() const navigation = useNavigate()
@ -295,7 +296,7 @@ function UserModeEditor() {
async (content: StepContent) => { async (content: StepContent) => {
const response = await fetchAPI( const response = await fetchAPI(
`tactics/${tacticId}/steps/${stepId}`, `tactics/${tacticId}/steps/${stepId}`,
{ content }, {content},
"PUT", "PUT",
) )
@ -326,7 +327,7 @@ function UserModeEditor() {
async (id, content) => { async (id, content) => {
const response = await fetchAPI( const response = await fetchAPI(
`tactics/${tacticId}/steps/${id}`, `tactics/${tacticId}/steps/${id}`,
{ content }, {content},
"PUT", "PUT",
) )
if (!response.ok) { if (!response.ok) {
@ -344,7 +345,7 @@ function UserModeEditor() {
const [stepContent, setStepContent, saveState] = const [stepContent, setStepContent, saveState] =
useContentState<StepContent>( useContentState<StepContent>(
{ components: [] }, {components: []},
SaveStates.Ok, SaveStates.Ok,
useMemo(() => debounceAsync(saveContent, 250), [saveContent]), useMemo(() => debounceAsync(saveContent, 250), [saveContent]),
) )
@ -357,8 +358,8 @@ function UserModeEditor() {
const infoResponse = await infoResponsePromise const infoResponse = await infoResponsePromise
const treeResponse = await treeResponsePromise const treeResponse = await treeResponsePromise
const { name, courtType } = await infoResponse.json() const {name, courtType} = await infoResponse.json()
const { root } = await treeResponse.json() const {root} = await treeResponse.json()
if ( if (
infoResponse.status == 401 || infoResponse.status == 401 ||
@ -382,7 +383,7 @@ function UserModeEditor() {
const content = await contentResponse.json() const content = await contentResponse.json()
setTactic({ id: tacticId, name, courtType }) setTactic({id: tacticId, name, courtType})
setStepsTree(root) setStepsTree(root)
setStepId(root.id) setStepId(root.id)
setStepContent(content, false) setStepContent(content, false)
@ -393,7 +394,7 @@ function UserModeEditor() {
const onNameChange = useCallback( const onNameChange = useCallback(
(name: string) => (name: string) =>
fetchAPI(`tactics/${tacticId}/name`, { name }, "PUT").then( fetchAPI(`tactics/${tacticId}/name`, {name}, "PUT").then(
(r) => r.ok, (r) => r.ok,
), ),
[tacticId], [tacticId],
@ -418,8 +419,8 @@ function UserModeEditor() {
content, content,
}) })
if (!response.ok) return null if (!response.ok) return null
const { stepId } = await response.json() const {stepId} = await response.json()
const child = { id: stepId, children: [] } const child = {id: stepId, children: []}
setStepsTree(addStepNode(stepsTree, parent, child)) setStepsTree(addStepNode(stepsTree, parent, child))
return child return child
}, },
@ -439,7 +440,7 @@ function UserModeEditor() {
[tacticId, stepsTree], [tacticId, stepsTree],
) )
if (!tactic) return <EditorLoadingScreen /> if (!tactic) return <EditorLoadingScreen/>
return ( return (
<EditorPage <EditorPage
@ -485,18 +486,18 @@ export interface EditorViewProps {
} }
function EditorPage({ function EditorPage({
tactic: { name, rootStepNode: initialStepsNode, courtType }, tactic: {name, rootStepNode: initialStepsNode, courtType},
currentStepId, currentStepId,
setCurrentStepContent: setContent, setCurrentStepContent: setContent,
currentStepContent: content, currentStepContent: content,
saveState, saveState,
onNameChange, onNameChange,
selectStep, selectStep,
onRemoveStep, onRemoveStep,
onAddStep, onAddStep,
courtRef, courtRef,
}: EditorViewProps) { }: EditorViewProps) {
const [titleStyle, setTitleStyle] = useState<CSSProperties>({}) const [titleStyle, setTitleStyle] = useState<CSSProperties>({})
const [rootStepsNode, setRootStepsNode] = useState(initialStepsNode) const [rootStepsNode, setRootStepsNode] = useState(initialStepsNode)
@ -505,7 +506,7 @@ function EditorPage({
const opponents = getRackPlayers(PlayerTeam.Opponents, content.components) const opponents = getRackPlayers(PlayerTeam.Opponents, content.components)
const [objects, setObjects] = useState<RackedCourtObject[]>(() => const [objects, setObjects] = useState<RackedCourtObject[]>(() =>
isBallOnCourt(content) ? [] : [{ key: "ball" }], isBallOnCourt(content) ? [] : [{key: "ball"}],
) )
const [previewAction, setPreviewAction] = useState<ActionPreview | null>( const [previewAction, setPreviewAction] = useState<ActionPreview | null>(
@ -535,12 +536,12 @@ function EditorPage({
} }
useEffect(() => { useEffect(() => {
setObjects(isBallOnCourt(content) ? [] : [{ key: "ball" }]) setObjects(isBallOnCourt(content) ? [] : [{key: "ball"}])
}, [setObjects, content]) }, [setObjects, content])
const insertRackedPlayer = (player: Player) => { const insertRackedPlayer = (player: Player) => {
if (player.ballState == BallState.HOLDS_BY_PASS) { if (player.ballState == BallState.HOLDS_BY_PASS) {
setObjects([{ key: "ball" }]) setObjects([{key: "ball"}])
} }
} }
@ -635,15 +636,15 @@ function EditorPage({
/> />
), ),
!isFrozen && !isFrozen &&
(info.ballState === BallState.HOLDS_ORIGIN || (info.ballState === BallState.HOLDS_ORIGIN ||
info.ballState === BallState.PASSED_ORIGIN) && ( info.ballState === BallState.PASSED_ORIGIN) && (
<BallAction <BallAction
key={2} key={2}
onDrop={(ballBounds) => { onDrop={(ballBounds) => {
doMoveBall(ballBounds, player) doMoveBall(ballBounds, player)
}} }}
/> />
), ),
] ]
}, },
[content, courtRef, doMoveBall, previewAction?.isInvalid, setContent], [content, courtRef, doMoveBall, previewAction?.isInvalid, setContent],
@ -744,7 +745,7 @@ function EditorPage({
setContent((content) => removeBall(content)) setContent((content) => removeBall(content))
setObjects((objects) => [ setObjects((objects) => [
...objects, ...objects,
{ key: "ball" }, {key: "ball"},
]) ])
}} }}
/> />
@ -777,11 +778,111 @@ function EditorPage({
[courtRef, doDeleteAction, doUpdateAction], [courtRef, doDeleteAction, doUpdateAction],
) )
const contentNode = <div id="content-div">
<div id="racks">
<PlayerRack
id={"allies"}
objects={allies}
setComponents={setComponents}
courtRef={courtRef}
/>
<Rack
id={"objects"}
objects={objects}
onChange={setObjects}
canDetach={useCallback(
(div) =>
overlaps(
courtBounds(),
div.getBoundingClientRect(),
),
[courtBounds],
)}
onElementDetached={useCallback(
(r, e: RackedCourtObject) =>
setContent((content) =>
placeObjectAt(
r.getBoundingClientRect(),
courtBounds(),
e,
content,
),
),
[courtBounds, setContent],
)}
render={renderCourtObject}
/>
<PlayerRack
id={"opponents"}
objects={opponents}
setComponents={setComponents}
courtRef={courtRef}
/>
</div>
<div id="court-div">
<div id="court-div-bounds">
<BasketCourt
components={content.components}
courtImage={<Court courtType={courtType}/>}
courtRef={courtRef}
previewAction={previewAction}
renderComponent={renderComponent}
renderActions={renderActions}
/>
</div>
</div>
</div>
const stepsTreeNode = <EditorStepsTree
isVisible={isStepsTreeVisible}
selectedStepId={currentStepId}
root={rootStepsNode}
onAddChildren={useCallback(
async (parent) => {
const addedNode = await onAddStep(
parent,
computeTerminalState(
content,
relativePositions,
),
)
if (addedNode == null) {
console.error(
"could not add step : onAddStep returned null node",
)
return
}
selectStep(addedNode.id)
setRootStepsNode((root) =>
addStepNode(root, parent, addedNode),
)
},
[content, onAddStep, selectStep, relativePositions],
)}
onRemoveNode={useCallback(
async (removed) => {
const isOk = await onRemoveStep(removed)
selectStep(getParent(rootStepsNode, removed)!.id)
if (isOk)
setRootStepsNode(
(root) => removeStepNode(root, removed)!,
)
},
[rootStepsNode, onRemoveStep, selectStep],
)}
onStepSelected={useCallback(
(node) => selectStep(node.id),
[selectStep],
)}
/>
return ( return (
<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={saveState}/>
</div> </div>
<div id="title-input-div"> <div id="title-input-div">
<TitleInput <TitleInput
@ -804,111 +905,19 @@ function EditorPage({
</div> </div>
</div> </div>
<div id="editor-div"> <div id="editor-div">
<div id="content-div"> {isStepsTreeVisible
<div id="racks"> ? <CurtainLayout>
<PlayerRack {contentNode}
id={"allies"} {stepsTreeNode}
objects={allies} </CurtainLayout>
setComponents={setComponents} : {contentNode}
courtRef={courtRef} }
/>
<Rack
id={"objects"}
objects={objects}
onChange={setObjects}
canDetach={useCallback(
(div) =>
overlaps(
courtBounds(),
div.getBoundingClientRect(),
),
[courtBounds],
)}
onElementDetached={useCallback(
(r, e: RackedCourtObject) =>
setContent((content) =>
placeObjectAt(
r.getBoundingClientRect(),
courtBounds(),
e,
content,
),
),
[courtBounds, setContent],
)}
render={renderCourtObject}
/>
<PlayerRack
id={"opponents"}
objects={opponents}
setComponents={setComponents}
courtRef={courtRef}
/>
</div>
<div id="court-div">
<div id="court-div-bounds">
<BasketCourt
components={content.components}
courtImage={<Court courtType={courtType} />}
courtRef={courtRef}
previewAction={previewAction}
renderComponent={renderComponent}
renderActions={renderActions}
/>
</div>
</div>
</div>
<EditorStepsTree
isVisible={isStepsTreeVisible}
selectedStepId={currentStepId}
root={rootStepsNode}
onAddChildren={useCallback(
async (parent) => {
const addedNode = await onAddStep(
parent,
computeTerminalState(
content,
relativePositions,
),
)
if (addedNode == null) {
console.error(
"could not add step : onAddStep returned null node",
)
return
}
selectStep(addedNode.id)
setRootStepsNode((root) =>
addStepNode(root, parent, addedNode),
)
},
[content, onAddStep, selectStep, relativePositions],
)}
onRemoveNode={useCallback(
async (removed) => {
const isOk = await onRemoveStep(removed)
selectStep(getParent(rootStepsNode, removed)!.id)
if (isOk)
setRootStepsNode(
(root) => removeStepNode(root, removed)!,
)
},
[rootStepsNode, onRemoveStep, selectStep],
)}
onStepSelected={useCallback(
(node) => selectStep(node.id),
[selectStep],
)}
/>
</div> </div>
</div> </div>
) )
} }
interface EditorStepsTreeProps { interface EditorStepsTreeProps {
isVisible: boolean
selectedStepId: number selectedStepId: number
root: StepInfoNode root: StepInfoNode
onAddChildren: (parent: StepInfoNode) => void onAddChildren: (parent: StepInfoNode) => void
@ -917,19 +926,15 @@ interface EditorStepsTreeProps {
} }
function EditorStepsTree({ function EditorStepsTree({
isVisible, selectedStepId,
selectedStepId, root,
root, onAddChildren,
onAddChildren, onRemoveNode,
onRemoveNode, onStepSelected,
onStepSelected, }: EditorStepsTreeProps) {
}: EditorStepsTreeProps) {
return ( return (
<div <div
id="steps-div" id="steps-div">
style={{
transform: isVisible ? "translateX(0)" : "translateX(100%)",
}}>
<StepsTree <StepsTree
root={root} root={root}
selectedStepId={selectedStepId} selectedStepId={selectedStepId}
@ -952,12 +957,12 @@ interface PlayerRackProps {
} }
function PlayerRack({ function PlayerRack({
id, id,
objects, objects,
setObjects, setObjects,
courtRef, courtRef,
setComponents, setComponents,
}: PlayerRackProps) { }: PlayerRackProps) {
const courtBounds = useCallback( const courtBounds = useCallback(
() => courtRef.current!.getBoundingClientRect(), () => courtRef.current!.getBoundingClientRect(),
[courtRef], [courtRef],
@ -985,7 +990,7 @@ function PlayerRack({
[courtBounds, setComponents], [courtBounds, setComponents],
)} )}
render={useCallback( render={useCallback(
({ team, key }: { team: PlayerTeam; key: string }) => ( ({team, key}: { team: PlayerTeam; key: string }) => (
<PlayerPiece <PlayerPiece
team={team} team={team}
text={key} text={key}
@ -1011,15 +1016,15 @@ interface CourtPlayerArrowActionProps {
} }
function CourtPlayerArrowAction({ function CourtPlayerArrowAction({
playerInfo, playerInfo,
player, player,
isInvalid, isInvalid,
content, content,
setContent, setContent,
setPreviewAction, setPreviewAction,
courtRef, courtRef,
}: CourtPlayerArrowActionProps) { }: CourtPlayerArrowActionProps) {
const courtBounds = useCallback( const courtBounds = useCallback(
() => courtRef.current!.getBoundingClientRect(), () => courtRef.current!.getBoundingClientRect(),
[courtRef], [courtRef],
@ -1074,7 +1079,7 @@ function CourtPlayerArrowAction({
} }
setContent((content) => { setContent((content) => {
let { createdAction, newContent } = createAction( let {createdAction, newContent} = createAction(
player, player,
courtBounds(), courtBounds(),
headRect, headRect,
@ -1127,18 +1132,18 @@ function isBallOnCourt(content: StepContent) {
function renderCourtObject(courtObject: RackedCourtObject) { function renderCourtObject(courtObject: RackedCourtObject) {
if (courtObject.key == "ball") { if (courtObject.key == "ball") {
return <BallPiece /> return <BallPiece/>
} }
throw new Error("unknown racked court object " + courtObject.key) throw new Error("unknown racked court object " + courtObject.key)
} }
function Court({ courtType }: { courtType: string }) { function Court({courtType}: { courtType: string }) {
return ( return (
<div id="court-image-div"> <div id="court-image-div">
{courtType == "PLAIN" ? ( {courtType == "PLAIN" ? (
<PlainCourt id="court-image" /> <PlainCourt id="court-image"/>
) : ( ) : (
<HalfCourt id="court-image" /> <HalfCourt id="court-image"/>
)} )}
</div> </div>
) )
@ -1243,7 +1248,7 @@ async function updateStepContents(
} }
} }
const { content, relativePositions } = await getStepContent(stepId) const {content, relativePositions} = await getStepContent(stepId)
const startNode = getStepNode(stepsTree!, stepId)! const startNode = getStepNode(stepsTree!, stepId)!
await updateSteps(startNode, content, relativePositions) await updateSteps(startNode, content, relativePositions)

@ -50,28 +50,21 @@
} }
#content-div, #content-div,
#editor-div { #editor-div,
#steps-div {
height: 100%; height: 100%;
width: 100%; width: 100%;
} }
#content-div { .curtain {
width: 100%; width: 100%;
} }
#steps-div { #steps-div {
background-color: var(--editor-tree-background); background-color: var(--editor-tree-background);
width: 20%;
transform: translateX(100%);
transition: transform 500ms;
overflow: scroll; overflow: scroll;
} }
#steps-div::-webkit-scrollbar {
display: none;
}
#allies-rack, #allies-rack,
#opponent-rack { #opponent-rack {
width: 125px; width: 125px;

Loading…
Cancel
Save