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

maxime.batista 1 year ago
parent 4fe1ddfbd2
commit cc3a3429fd

@ -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 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 <EditorPortal guestMode={guestMode} />
export default function Editor({guestMode}: EditorPageProps) {
return <EditorPortal guestMode={guestMode}/>
}
function EditorPortal({ guestMode }: EditorPageProps) {
return guestMode ? <GuestModeEditor /> : <UserModeEditor />
function EditorPortal({guestMode}: EditorPageProps) {
return guestMode ? <GuestModeEditor/> : <UserModeEditor/>
}
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,
@ -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<StepContent>(
{ 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 <EditorLoadingScreen />
if (!tactic) return <EditorLoadingScreen/>
return (
<EditorPage
@ -485,7 +486,7 @@ export interface EditorViewProps {
}
function EditorPage({
tactic: { name, rootStepNode: initialStepsNode, courtType },
tactic: {name, rootStepNode: initialStepsNode, courtType},
currentStepId,
setCurrentStepContent: setContent,
currentStepContent: content,
@ -496,7 +497,7 @@ function EditorPage({
onAddStep,
courtRef,
}: EditorViewProps) {
}: EditorViewProps) {
const [titleStyle, setTitleStyle] = useState<CSSProperties>({})
const [rootStepsNode, setRootStepsNode] = useState(initialStepsNode)
@ -505,7 +506,7 @@ function EditorPage({
const opponents = getRackPlayers(PlayerTeam.Opponents, content.components)
const [objects, setObjects] = useState<RackedCourtObject[]>(() =>
isBallOnCourt(content) ? [] : [{ key: "ball" }],
isBallOnCourt(content) ? [] : [{key: "ball"}],
)
const [previewAction, setPreviewAction] = useState<ActionPreview | null>(
@ -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"}])
}
}
@ -744,7 +745,7 @@ function EditorPage({
setContent((content) => removeBall(content))
setObjects((objects) => [
...objects,
{ key: "ball" },
{key: "ball"},
])
}}
/>
@ -777,34 +778,7 @@ function EditorPage({
[courtRef, doDeleteAction, doUpdateAction],
)
return (
<div id="main-div">
<div id="topbar-div">
<div id="topbar-left">
<SavingState state={saveState} />
</div>
<div id="title-input-div">
<TitleInput
style={titleStyle}
default_value={name}
onValidated={useCallback(
(new_name) => {
onNameChange(new_name).then((success) => {
setTitleStyle(success ? {} : ERROR_STYLE)
})
},
[onNameChange],
)}
/>
</div>
<div id="topbar-right">
<button onClick={() => setStepsTreeVisible((b) => !b)}>
ETAPES
</button>
</div>
</div>
<div id="editor-div">
<div id="content-div">
const contentNode = <div id="content-div">
<div id="racks">
<PlayerRack
id={"allies"}
@ -851,7 +825,7 @@ function EditorPage({
<div id="court-div-bounds">
<BasketCourt
components={content.components}
courtImage={<Court courtType={courtType} />}
courtImage={<Court courtType={courtType}/>}
courtRef={courtRef}
previewAction={previewAction}
renderComponent={renderComponent}
@ -860,7 +834,8 @@ function EditorPage({
</div>
</div>
</div>
<EditorStepsTree
const stepsTreeNode = <EditorStepsTree
isVisible={isStepsTreeVisible}
selectedStepId={currentStepId}
root={rootStepsNode}
@ -902,13 +877,47 @@ function EditorPage({
[selectStep],
)}
/>
return (
<div id="main-div">
<div id="topbar-div">
<div id="topbar-left">
<SavingState state={saveState}/>
</div>
<div id="title-input-div">
<TitleInput
style={titleStyle}
default_value={name}
onValidated={useCallback(
(new_name) => {
onNameChange(new_name).then((success) => {
setTitleStyle(success ? {} : ERROR_STYLE)
})
},
[onNameChange],
)}
/>
</div>
<div id="topbar-right">
<button onClick={() => setStepsTreeVisible((b) => !b)}>
ETAPES
</button>
</div>
</div>
<div id="editor-div">
{isStepsTreeVisible
? <CurtainLayout>
{contentNode}
{stepsTreeNode}
</CurtainLayout>
: contentNode
}
</div>
</div>
)
}
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) {
}: EditorStepsTreeProps) {
return (
<div
id="steps-div"
style={{
transform: isVisible ? "translateX(0)" : "translateX(100%)",
}}>
id="steps-div">
<StepsTree
root={root}
selectedStepId={selectedStepId}
@ -957,7 +962,7 @@ function PlayerRack({
setObjects,
courtRef,
setComponents,
}: PlayerRackProps) {
}: PlayerRackProps) {
const courtBounds = useCallback(
() => 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 }) => (
<PlayerPiece
team={team}
text={key}
@ -1019,7 +1024,7 @@ function CourtPlayerArrowAction({
setContent,
setPreviewAction,
courtRef,
}: CourtPlayerArrowActionProps) {
}: CourtPlayerArrowActionProps) {
const courtBounds = useCallback(
() => 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 <BallPiece />
return <BallPiece/>
}
throw new Error("unknown racked court object " + courtObject.key)
}
function Court({ courtType }: { courtType: string }) {
function Court({courtType}: { courtType: string }) {
return (
<div id="court-image-div">
{courtType == "PLAIN" ? (
<PlainCourt id="court-image" />
<PlainCourt id="court-image"/>
) : (
<HalfCourt id="court-image" />
<HalfCourt id="court-image"/>
)}
</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)!
await updateSteps(startNode, content, relativePositions)

@ -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;

Loading…
Cancel
Save