From cc3a3429fdfab56414ceab1ee3e777be9a95e927 Mon Sep 17 00:00:00 2001 From: "maxime.batista" Date: Sat, 9 Mar 2024 09:53:54 +0100 Subject: [PATCH] WIP: tree-slider --- src/components/CurtainLayout.tsx | 66 ++++++ src/pages/Editor.tsx | 387 ++++++++++++++++--------------- src/style/editor.css | 13 +- 3 files changed, 265 insertions(+), 201 deletions(-) create mode 100644 src/components/CurtainLayout.tsx diff --git a/src/components/CurtainLayout.tsx b/src/components/CurtainLayout.tsx new file mode 100644 index 0000000..0c2d626 --- /dev/null +++ b/src/components/CurtainLayout.tsx @@ -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(null) + const sliderRef = useRef(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 ( +
+
+ {children[0]} +
+
+
+ +
+ {children[1]} +
+
+ + ) +} \ No newline at end of file diff --git a/src/pages/Editor.tsx b/src/pages/Editor.tsx index da6a3d2..d85433d 100644 --- a/src/pages/Editor.tsx +++ b/src/pages/Editor.tsx @@ -14,10 +14,10 @@ import TitleInput from "../components/TitleInput" import PlainCourt from "../assets/court/full_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 { PlayerPiece } from "../components/editor/PlayerPiece" +import {Rack} from "../components/Rack" +import {PlayerPiece} from "../components/editor/PlayerPiece" import { ComponentId, @@ -27,17 +27,17 @@ import { TacticComponent, TacticInfo, } from "../model/tactic/Tactic" -import { fetchAPI, fetchAPIGet } from "../Fetcher" +import {fetchAPI, fetchAPIGet} from "../Fetcher" import SavingState, { SaveState, SaveStates, } from "../components/editor/SavingState" -import { BALL_TYPE } from "../model/tactic/CourtObjects" -import { CourtAction } from "../components/editor/CourtAction" -import { ActionPreview, BasketCourt } from "../components/editor/BasketCourt" -import { overlaps } from "../geo/Box" +import {BALL_TYPE} from "../model/tactic/CourtObjects" +import {CourtAction} from "../components/editor/CourtAction" +import {ActionPreview, BasketCourt} from "../components/editor/BasketCourt" +import {overlaps} from "../geo/Box" import { computeTerminalState, @@ -61,7 +61,7 @@ import { PlayerTeam, } from "../model/tactic/Player" -import { RackedCourtObject, RackedPlayer } from "../editor/RackedItems" +import {RackedCourtObject, RackedPlayer} from "../editor/RackedItems" import { CourtPlayer, EditableCourtPlayer, @@ -74,16 +74,16 @@ import { spreadNewStateFromOriginStateChange, } from "../editor/ActionsDomains" import ArrowAction from "../components/actions/ArrowAction" -import { middlePos, Pos, ratioWithinBase } from "../geo/Pos" -import { Action, ActionKind } from "../model/tactic/Action" +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 { CourtBall } from "../components/editor/CourtBall" -import { useNavigate, useParams } from "react-router-dom" +import {CourtBall} from "../components/editor/CourtBall" +import {useNavigate, useParams} from "react-router-dom" import StepsTree from "../components/editor/StepsTree" import { addStepNode, @@ -92,6 +92,7 @@ import { getStepNode, removeStepNode, } from "../editor/StepsDomain" +import CurtainLayout from "../components/CurtainLayout"; const ERROR_STYLE: CSSProperties = { borderColor: "red", @@ -121,12 +122,12 @@ export interface EditorPageProps { guestMode: boolean } -export default function Editor({ guestMode }: EditorPageProps) { - return +export default function Editor({guestMode}: EditorPageProps) { + return } -function EditorPortal({ guestMode }: EditorPageProps) { - return guestMode ? : +function EditorPortal({guestMode}: EditorPageProps) { + return guestMode ? : } function GuestModeEditor() { @@ -136,7 +137,7 @@ function GuestModeEditor() { const stepInitialContent: StepContent = { ...(storageContent == null - ? { components: [] } + ? {components: []} : JSON.parse(storageContent)), } @@ -148,7 +149,7 @@ function GuestModeEditor() { if (storageContent == null) { localStorage.setItem( 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( GUEST_MODE_STEP_CONTENT_STORAGE_KEY + GUEST_MODE_ROOT_STEP_ID, @@ -186,7 +187,7 @@ function GuestModeEditor() { const content = JSON.parse( localStorage.getItem( GUEST_MODE_STEP_CONTENT_STORAGE_KEY + - stepId, + stepId, )!, ) const courtBounds = @@ -195,7 +196,7 @@ function GuestModeEditor() { courtBounds, content, ) - return { content, relativePositions } + return {content, relativePositions} }, async (stepId, content) => localStorage.setItem( @@ -249,7 +250,7 @@ function GuestModeEditor() { ) const nodeId = getAvailableId(root) - const node = { id: nodeId, children: [] } + const node = {id: nodeId, children: []} const resultTree = addStepNode(root, parent, node) localStorage.setItem( @@ -284,7 +285,7 @@ function UserModeEditor() { id: -1, children: [], }) - const { tacticId: idStr } = useParams() + const {tacticId: idStr} = useParams() const tacticId = parseInt(idStr!) const navigation = useNavigate() @@ -295,7 +296,7 @@ function UserModeEditor() { async (content: StepContent) => { const response = await fetchAPI( `tactics/${tacticId}/steps/${stepId}`, - { content }, + {content}, "PUT", ) @@ -326,7 +327,7 @@ function UserModeEditor() { async (id, content) => { const response = await fetchAPI( `tactics/${tacticId}/steps/${id}`, - { content }, + {content}, "PUT", ) if (!response.ok) { @@ -344,7 +345,7 @@ function UserModeEditor() { const [stepContent, setStepContent, saveState] = useContentState( - { components: [] }, + {components: []}, SaveStates.Ok, useMemo(() => debounceAsync(saveContent, 250), [saveContent]), ) @@ -357,8 +358,8 @@ function UserModeEditor() { const infoResponse = await infoResponsePromise const treeResponse = await treeResponsePromise - const { name, courtType } = await infoResponse.json() - const { root } = await treeResponse.json() + const {name, courtType} = await infoResponse.json() + const {root} = await treeResponse.json() if ( infoResponse.status == 401 || @@ -382,7 +383,7 @@ function UserModeEditor() { const content = await contentResponse.json() - setTactic({ id: tacticId, name, courtType }) + setTactic({id: tacticId, name, courtType}) setStepsTree(root) setStepId(root.id) setStepContent(content, false) @@ -393,7 +394,7 @@ function UserModeEditor() { const onNameChange = useCallback( (name: string) => - fetchAPI(`tactics/${tacticId}/name`, { name }, "PUT").then( + fetchAPI(`tactics/${tacticId}/name`, {name}, "PUT").then( (r) => r.ok, ), [tacticId], @@ -418,8 +419,8 @@ function UserModeEditor() { content, }) if (!response.ok) return null - const { stepId } = await response.json() - const child = { id: stepId, children: [] } + const {stepId} = await response.json() + const child = {id: stepId, children: []} setStepsTree(addStepNode(stepsTree, parent, child)) return child }, @@ -439,7 +440,7 @@ function UserModeEditor() { [tacticId, stepsTree], ) - if (!tactic) return + if (!tactic) return return ( ({}) const [rootStepsNode, setRootStepsNode] = useState(initialStepsNode) @@ -505,7 +506,7 @@ function EditorPage({ const opponents = getRackPlayers(PlayerTeam.Opponents, content.components) const [objects, setObjects] = useState(() => - isBallOnCourt(content) ? [] : [{ key: "ball" }], + isBallOnCourt(content) ? [] : [{key: "ball"}], ) const [previewAction, setPreviewAction] = useState( @@ -535,12 +536,12 @@ function EditorPage({ } useEffect(() => { - setObjects(isBallOnCourt(content) ? [] : [{ key: "ball" }]) + setObjects(isBallOnCourt(content) ? [] : [{key: "ball"}]) }, [setObjects, content]) const insertRackedPlayer = (player: Player) => { if (player.ballState == BallState.HOLDS_BY_PASS) { - setObjects([{ key: "ball" }]) + setObjects([{key: "ball"}]) } } @@ -635,15 +636,15 @@ function EditorPage({ /> ), !isFrozen && - (info.ballState === BallState.HOLDS_ORIGIN || - info.ballState === BallState.PASSED_ORIGIN) && ( - { - doMoveBall(ballBounds, player) - }} - /> - ), + (info.ballState === BallState.HOLDS_ORIGIN || + info.ballState === BallState.PASSED_ORIGIN) && ( + { + doMoveBall(ballBounds, player) + }} + /> + ), ] }, [content, courtRef, doMoveBall, previewAction?.isInvalid, setContent], @@ -744,7 +745,7 @@ function EditorPage({ setContent((content) => removeBall(content)) setObjects((objects) => [ ...objects, - { key: "ball" }, + {key: "ball"}, ]) }} /> @@ -777,11 +778,111 @@ function EditorPage({ [courtRef, doDeleteAction, doUpdateAction], ) + const contentNode =
+
+ + + + overlaps( + courtBounds(), + div.getBoundingClientRect(), + ), + [courtBounds], + )} + onElementDetached={useCallback( + (r, e: RackedCourtObject) => + setContent((content) => + placeObjectAt( + r.getBoundingClientRect(), + courtBounds(), + e, + content, + ), + ), + [courtBounds, setContent], + )} + render={renderCourtObject} + /> + + +
+
+
+ } + courtRef={courtRef} + previewAction={previewAction} + renderComponent={renderComponent} + renderActions={renderActions} + /> +
+
+
+ + const stepsTreeNode = { + 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 (
- +
-
-
- - - - overlaps( - courtBounds(), - div.getBoundingClientRect(), - ), - [courtBounds], - )} - onElementDetached={useCallback( - (r, e: RackedCourtObject) => - setContent((content) => - placeObjectAt( - r.getBoundingClientRect(), - courtBounds(), - e, - content, - ), - ), - [courtBounds, setContent], - )} - render={renderCourtObject} - /> - - -
-
-
- } - courtRef={courtRef} - previewAction={previewAction} - renderComponent={renderComponent} - renderActions={renderActions} - /> -
-
-
- { - 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], - )} - /> + {isStepsTreeVisible + ? + {contentNode} + {stepsTreeNode} + + : contentNode + }
) } interface EditorStepsTreeProps { - isVisible: boolean selectedStepId: number root: StepInfoNode onAddChildren: (parent: StepInfoNode) => void @@ -917,19 +926,15 @@ interface EditorStepsTreeProps { } function EditorStepsTree({ - isVisible, - selectedStepId, - root, - onAddChildren, - onRemoveNode, - onStepSelected, -}: EditorStepsTreeProps) { + selectedStepId, + root, + onAddChildren, + onRemoveNode, + onStepSelected, + }: EditorStepsTreeProps) { return (
+ id="steps-div"> courtRef.current!.getBoundingClientRect(), [courtRef], @@ -985,7 +990,7 @@ function PlayerRack({ [courtBounds, setComponents], )} render={useCallback( - ({ team, key }: { team: PlayerTeam; key: string }) => ( + ({team, key}: { team: PlayerTeam; key: string }) => ( courtRef.current!.getBoundingClientRect(), [courtRef], @@ -1074,7 +1079,7 @@ function CourtPlayerArrowAction({ } setContent((content) => { - let { createdAction, newContent } = createAction( + let {createdAction, newContent} = createAction( player, courtBounds(), headRect, @@ -1127,18 +1132,18 @@ function isBallOnCourt(content: StepContent) { function renderCourtObject(courtObject: RackedCourtObject) { if (courtObject.key == "ball") { - return + return } throw new Error("unknown racked court object " + courtObject.key) } -function Court({ courtType }: { courtType: string }) { +function Court({courtType}: { courtType: string }) { return (
{courtType == "PLAIN" ? ( - + ) : ( - + )}
) @@ -1243,7 +1248,7 @@ async function updateStepContents( } } - const { content, relativePositions } = await getStepContent(stepId) + const {content, relativePositions} = await getStepContent(stepId) const startNode = getStepNode(stepsTree!, stepId)! await updateSteps(startNode, content, relativePositions) diff --git a/src/style/editor.css b/src/style/editor.css index b66ed67..6b17af8 100644 --- a/src/style/editor.css +++ b/src/style/editor.css @@ -50,28 +50,21 @@ } #content-div, -#editor-div { +#editor-div, +#steps-div { height: 100%; width: 100%; } -#content-div { +.curtain { width: 100%; } #steps-div { background-color: var(--editor-tree-background); - width: 20%; - - transform: translateX(100%); - transition: transform 500ms; overflow: scroll; } -#steps-div::-webkit-scrollbar { - display: none; -} - #allies-rack, #opponent-rack { width: 125px;