diff --git a/ci/build_and_deploy_to.sh b/ci/build_and_deploy_to.sh index a5a6601..eb5d387 100755 --- a/ci/build_and_deploy_to.sh +++ b/ci/build_and_deploy_to.sh @@ -1,5 +1,4 @@ -set -e - +#!/usr/bin/env bash set -xeu export OUTPUT=$1 diff --git a/src/components/CurtainLayout.tsx b/src/components/CurtainLayout.tsx new file mode 100644 index 0000000..0be39fb --- /dev/null +++ b/src/components/CurtainLayout.tsx @@ -0,0 +1,76 @@ +import { ReactNode, useCallback, useEffect, useRef, useState } from "react" + +export interface SlideLayoutProps { + children: [ReactNode, ReactNode] + rightWidth: number + onRightWidthChange: (w: number) => void +} + +export default function CurtainLayout({ + children, + rightWidth, + onRightWidthChange, +}: SlideLayoutProps) { + const curtainRef = useRef(null) + const sliderRef = useRef(null) + + const resize = useCallback( + (e: MouseEvent) => { + const sliderPosX = e.clientX + const curtainWidth = + curtainRef.current!.getBoundingClientRect().width + + onRightWidthChange((sliderPosX / curtainWidth) * 100) + }, + [curtainRef, onRightWidthChange], + ) + + 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, resize]) + + return ( +
+
+ {children[0]} +
+
+ +
+ {children[1]} +
+
+ ) +} diff --git a/src/components/Rack.tsx b/src/components/Rack.tsx index fac282f..2e4e75a 100644 --- a/src/components/Rack.tsx +++ b/src/components/Rack.tsx @@ -20,13 +20,13 @@ interface RackItemProps { * A container of draggable objects * */ export function Rack({ - id, - objects, - onChange, - canDetach, - onElementDetached, - render, - }: RackProps) { + id, + objects, + onChange, + canDetach, + onElementDetached, + render, +}: RackProps) { return (
({ const index = objects.findIndex( (o) => o.key === element.key, ) - if (onChange) - onChange(objects.toSpliced(index, 1)) + if (onChange) onChange(objects.toSpliced(index, 1)) onElementDetached(ref, element) }} @@ -56,10 +55,10 @@ export function Rack({ } function RackItem({ - item, - onTryDetach, - render, - }: RackItemProps) { + item, + onTryDetach, + render, +}: RackItemProps) { const divRef = useRef(null) return ( diff --git a/src/components/arrows/BendableArrow.tsx b/src/components/arrows/BendableArrow.tsx index 18aeea3..a08204e 100644 --- a/src/components/arrows/BendableArrow.tsx +++ b/src/components/arrows/BendableArrow.tsx @@ -389,6 +389,7 @@ export default function BendableArrow({ useEffect(() => { const observer = new MutationObserver(update) const config = { attributes: true } + if (typeof startPos == "string") { observer.observe(document.getElementById(startPos)!, config) } @@ -402,6 +403,14 @@ export default function BendableArrow({ return () => observer.disconnect() }, [startPos, segments, update]) + useEffect(() => { + const observer = new ResizeObserver(update) + + observer.observe(area.current!, {}) + + return () => observer.disconnect() + }) + // Adds a selection handler // Also force an update when the window is resized useEffect(() => { diff --git a/src/components/editor/StepsTree.tsx b/src/components/editor/StepsTree.tsx index 5a4af5e..f9c9a07 100644 --- a/src/components/editor/StepsTree.tsx +++ b/src/components/editor/StepsTree.tsx @@ -63,7 +63,12 @@ function StepsTreeNode({ key={child.id} area={ref} startPos={"step-piece-" + stepId} - segments={[{ next: "step-piece-" + getStepName(rootNode, child.id)}]} + segments={[ + { + next: + "step-piece-" + getStepName(rootNode, child.id), + }, + ]} onSegmentsChanges={() => {}} forceStraight={true} wavy={false} @@ -77,7 +82,9 @@ function StepsTreeNode({ isSelected={selectedStepId === node.id} onAddButtonClicked={() => onAddChildren(node)} onRemoveButtonClicked={ - rootNode.id === node.id ? undefined : () => onRemoveNode(node) + rootNode.id === node.id + ? undefined + : () => onRemoveNode(node) } onSelected={() => onStepSelected(node)} /> diff --git a/src/editor/StepsDomain.ts b/src/editor/StepsDomain.ts index 5ae681e..15de927 100644 --- a/src/editor/StepsDomain.ts +++ b/src/editor/StepsDomain.ts @@ -19,14 +19,12 @@ export function addStepNode( } export function getStepName(root: StepInfoNode, step: number): string { - let ord = 1 const nodes = [root] while (nodes.length > 0) { const node = nodes.pop()! - if (node.id === step) - break + if (node.id === step) break ord++ nodes.push(...[...node.children].reverse()) diff --git a/src/editor/TacticContentDomains.ts b/src/editor/TacticContentDomains.ts index 9f05e14..de631ca 100644 --- a/src/editor/TacticContentDomains.ts +++ b/src/editor/TacticContentDomains.ts @@ -200,9 +200,9 @@ export function moveComponent( phantomIdx == 0 ? origin : getComponent( - originPathItems[phantomIdx - 1], - content.components, - ) + originPathItems[phantomIdx - 1], + content.components, + ) // detach the action from the screen target and transform it to a regular move action to the phantom. content = updateComponent( { @@ -210,18 +210,18 @@ export function moveComponent( actions: playerBeforePhantom.actions.map((a) => a.target === referent ? { - ...a, - segments: a.segments.toSpliced( - a.segments.length - 2, - 1, - { - ...a.segments[a.segments.length - 1], - next: component.id, - }, - ), - target: component.id, - type: ActionKind.MOVE, - } + ...a, + segments: a.segments.toSpliced( + a.segments.length - 2, + 1, + { + ...a.segments[a.segments.length - 1], + next: component.id, + }, + ), + target: component.id, + type: ActionKind.MOVE, + } : a, ), }, @@ -234,9 +234,9 @@ export function moveComponent( ...component, pos: isPhantom ? { - type: "fixed", - ...newPos, - } + type: "fixed", + ...newPos, + } : newPos, }, content, @@ -315,15 +315,15 @@ export function computeTerminalState( content.components.filter((c) => c.type !== "phantom") as ( | Player | CourtObject - )[] + )[] const componentsTargetedState = nonPhantomComponents.map((comp) => comp.type === "player" ? getPlayerTerminalState(comp, content, computedPositions) : { - ...comp, - frozen: true, - }, + ...comp, + frozen: true, + }, ) return { @@ -399,8 +399,6 @@ export function drainTerminalStateOnChildContent( parentTerminalState: StepContent, childContent: StepContent, ): StepContent | null { - - let gotUpdated = false for (const parentComponent of parentTerminalState.components) { @@ -435,11 +433,17 @@ export function drainTerminalStateOnChildContent( if (newContentResult) { gotUpdated = true childContent = newContentResult - childComponent = getComponent(childComponent.id, newContentResult?.components) + childComponent = getComponent( + childComponent.id, + newContentResult?.components, + ) } // update the position of the player if it has been moved // also force update if the child component is not frozen (the component was introduced previously by the child step but the parent added it afterward) - if (!childComponent.frozen || !equals(childComponent.pos, parentComponent.pos)) { + if ( + !childComponent.frozen || + !equals(childComponent.pos, parentComponent.pos) + ) { gotUpdated = true childContent = updateComponent( { @@ -460,7 +464,7 @@ export function drainTerminalStateOnChildContent( (comp) => comp.type === "phantom" || !comp.frozen || - tryGetComponent(comp.id, parentTerminalState.components) + tryGetComponent(comp.id, parentTerminalState.components), ), } diff --git a/src/pages/Editor.tsx b/src/pages/Editor.tsx index da6a3d2..158441a 100644 --- a/src/pages/Editor.tsx +++ b/src/pages/Editor.tsx @@ -92,6 +92,7 @@ import { getStepNode, removeStepNode, } from "../editor/StepsDomain" +import CurtainLayout from "../components/CurtainLayout" const ERROR_STYLE: CSSProperties = { borderColor: "red", @@ -360,10 +361,7 @@ function UserModeEditor() { const { name, courtType } = await infoResponse.json() const { root } = await treeResponse.json() - if ( - infoResponse.status == 401 || - treeResponse.status == 401 - ) { + if (infoResponse.status == 401 || treeResponse.status == 401) { navigation("/login") return } @@ -372,7 +370,6 @@ function UserModeEditor() { `tactics/${tacticId}/steps/${root.id}`, ) - const contentResponse = await contentResponsePromise if (contentResponse.status == 401) { @@ -519,6 +516,9 @@ function EditorPage({ [courtRef], ) + const [editorContentCurtainWidth, setEditorContentCurtainWidth] = + useState(80) + const relativePositions = useMemo(() => { const courtBounds = courtRef.current?.getBoundingClientRect() return courtBounds @@ -700,7 +700,15 @@ function EditorPage({ /> ) }, - [courtRef, content, relativePositions, courtBounds, renderAvailablePlayerActions, validatePlayerPosition, doRemovePlayer], + [ + courtRef, + content, + relativePositions, + courtBounds, + renderAvailablePlayerActions, + validatePlayerPosition, + doRemovePlayer, + ], ) const doDeleteAction = useCallback( @@ -774,7 +782,107 @@ function EditorPage({ /> ) }), - [courtRef, doDeleteAction, doUpdateAction], + [courtRef, doDeleteAction, doUpdateAction, editorContentCurtainWidth], + ) + + 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 ( @@ -798,117 +906,30 @@ function EditorPage({ />
-
-
-
- - - - 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,7 +938,6 @@ interface EditorStepsTreeProps { } function EditorStepsTree({ - isVisible, selectedStepId, root, onAddChildren, @@ -925,11 +945,7 @@ function EditorStepsTree({ onStepSelected, }: EditorStepsTreeProps) { return ( -
+
{ await login() - const response = await fetchAPI("tactics", { courtType: "PLAIN", name: "test tactic" }) + const response = await fetchAPI("tactics", { + courtType: "PLAIN", + name: "test tactic", + }) expect(response.status).toBe(200) }) test("spam step creation test", async () => { - const createTacticResponse = await fetchAPI("tactics", { courtType: "PLAIN", name: "test tactic" }) + const createTacticResponse = await fetchAPI("tactics", { + courtType: "PLAIN", + name: "test tactic", + }) expect(createTacticResponse.status).toBe(200) const { id } = await createTacticResponse.json() - const tasks = Array.from({length: 200}) - .map(async () => { - const response = await fetchAPI(`tactics/${id}/steps`, { parentId: 1, content: { components: [] } }) - expect(response.status).toBe(200) - const { stepId } = await response.json() - return stepId + const tasks = Array.from({ length: 200 }).map(async () => { + const response = await fetchAPI(`tactics/${id}/steps`, { + parentId: 1, + content: { components: [] }, }) + expect(response.status).toBe(200) + const { stepId } = await response.json() + return stepId + }) const steps = [] for (const task of tasks) { steps.push(await task) } steps.sort((a, b) => a - b) - const expected = Array.from({length: 200}, (_, i) => i + 2) + const expected = Array.from({ length: 200 }, (_, i) => i + 2) expect(steps).toEqual(expected) -}) \ No newline at end of file +}) diff --git a/vite.config.ts b/vite.config.ts index 4126e3b..8d932e7 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -9,7 +9,7 @@ export default defineConfig({ target: "es2021", }, test: { - environment: "jsdom" + environment: "jsdom", }, plugins: [ react(),