diff --git a/src/components/editor/StepsTree.tsx b/src/components/editor/StepsTree.tsx
index b6443ed..f719066 100644
--- a/src/components/editor/StepsTree.tsx
+++ b/src/components/editor/StepsTree.tsx
@@ -1,7 +1,7 @@
-import "../../style/steps-tree.css"
+import "../../style/steps_tree.css"
import { StepInfoNode } from "../../model/tactic/Tactic"
import BendableArrow from "../arrows/BendableArrow"
-import { useRef } from "react"
+import { useRef, useState } from "react"
import AddSvg from "../../assets/icon/add.svg?react"
import RemoveSvg from "../../assets/icon/remove.svg?react"
@@ -13,19 +13,26 @@ export interface StepsTreeProps {
}
export default function StepsTree({
- root,
- onAddChildren,
- onRemoveNode,
- onStepSelected,
-}: StepsTreeProps) {
+ root,
+ onAddChildren,
+ onRemoveNode,
+ onStepSelected,
+ }: StepsTreeProps) {
+
+ const [selectedStepId, setSelectedStepId] = useState(root.id)
+
return (
{
+ setSelectedStepId(step.id)
+ onStepSelected(step)
+ }}
/>
)
@@ -34,36 +41,34 @@ export default function StepsTree({
interface StepsTreeContentProps {
node: StepInfoNode
isNodeRoot: boolean
+ selectedStepId: number,
onAddChildren: (parent: StepInfoNode) => void
onRemoveNode: (node: StepInfoNode) => void
onStepSelected: (node: StepInfoNode) => void
}
function StepsTreeNode({
- node,
- isNodeRoot,
- onAddChildren,
- onRemoveNode,
- onStepSelected,
-}: StepsTreeContentProps) {
+ node,
+ isNodeRoot,
+ selectedStepId,
+ onAddChildren,
+ onRemoveNode,
+ onStepSelected,
+ }: StepsTreeContentProps) {
const ref = useRef(null)
+
return (
-
-
onAddChildren(node)}
- onRemoveButtonClicked={
- isNodeRoot ? undefined : () => onRemoveNode(node)
- }
- onSelected={() => onStepSelected(node)}
- />
+
+
{node.children.map((child) => (
{}}
+ segments={[{next: "step-piece-" + child.id}]}
+ onSegmentsChanges={() => {
+ }}
forceStraight={true}
wavy={false}
//TODO remove magic constants
@@ -71,11 +76,19 @@ function StepsTreeNode({
endRadius={10}
/>
))}
+ onAddChildren(node)}
+ onRemoveButtonClicked={isNodeRoot ? undefined : () => onRemoveNode(node)}
+ onSelected={() => onStepSelected(node)}
+ />
{node.children.map((child) => (
void
onRemoveButtonClicked?: () => void
onSelected: () => void
}
function StepPiece({
- id,
- onAddButtonClicked,
- onRemoveButtonClicked,
- onSelected,
-}: StepPieceProps) {
+ id,
+ isSelected,
+ onAddButtonClicked,
+ onRemoveButtonClicked,
+ onSelected,
+ }: StepPieceProps) {
return (
{onAddButtonClicked && (
diff --git a/src/editor/StepsDomain.ts b/src/editor/StepsDomain.ts
index 46c7515..835b147 100644
--- a/src/editor/StepsDomain.ts
+++ b/src/editor/StepsDomain.ts
@@ -34,3 +34,25 @@ export function removeStepNode(
}),
}
}
+
+/**
+ * Returns an available identifier that is not already present into the given node tree
+ * @param root
+ */
+export function getAvailableId(root: StepInfoNode): number {
+ const acc = (root: StepInfoNode): number => Math.max(root.id, ...root.children.map(acc))
+ return acc(root) + 1
+}
+
+export function getParent(root: StepInfoNode, node: StepInfoNode): StepInfoNode | null {
+ if (root.children.find(n => n.id === node.id))
+ return root
+
+ for (const child of root.children) {
+ const result = getParent(child, node)
+ if (result != null) {
+ return result
+ }
+ }
+ return null
+}
\ No newline at end of file
diff --git a/src/pages/Editor.tsx b/src/pages/Editor.tsx
index 8d2da94..d05bcd7 100644
--- a/src/pages/Editor.tsx
+++ b/src/pages/Editor.tsx
@@ -78,22 +78,19 @@ import {
} from "../editor/PlayerDomains"
import { CourtBall } from "../components/editor/CourtBall"
import { useNavigate, useParams } from "react-router-dom"
-import { DEFAULT_TACTIC_NAME } from "./NewTacticPage.tsx"
import StepsTree from "../components/editor/StepsTree"
-import { addStepNode, removeStepNode } from "../editor/StepsDomain"
+import { addStepNode, getAvailableId, getParent, removeStepNode } from "../editor/StepsDomain"
const ERROR_STYLE: CSSProperties = {
borderColor: "red",
}
-const GUEST_MODE_CONTENT_STORAGE_KEY = "guest_mode_content"
+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"
-export interface EditorViewProps {
- tactic: TacticInfo
- onContentChange: (tactic: StepContent) => Promise
- onNameChange: (name: string) => Promise
-}
+// The step identifier the editor will always open on
+const DEFAULT_STEP_ID = 1
interface TacticDto {
id: number
@@ -103,34 +100,103 @@ interface TacticDto {
root: StepInfoNode
}
-interface EditorPageProps {
+export interface EditorPageProps {
guestMode: boolean
}
-export default function EditorPage({ guestMode }: EditorPageProps) {
- const [tactic, setTactic] = useState(() => {
- if (guestMode) {
- return {
- id: -1,
- courtType: "PLAIN",
- content: { components: [] },
- name: DEFAULT_TACTIC_NAME,
- root: { id: 1, children: [] },
- }
- }
- return null
+export default function Editor({ guestMode }: EditorPageProps) {
+ return
+}
+
+
+function EditorPortal({ guestMode }: EditorPageProps) {
+ return guestMode ? :
+}
+
+function GuestModeEditor() {
+ const storageContent = localStorage.getItem(GUEST_MODE_STEP_CONTENT_STORAGE_KEY + "0")
+
+ const stepInitialContent = ({
+ ...(storageContent == null ? { components: [] } : JSON.parse(storageContent)),
+ stepId: 0,
})
+
+ // initialize local storage if we launch in guest mode
+ if (storageContent == null) {
+ localStorage.setItem(GUEST_MODE_STEP_ROOT_NODE_INFO_STORAGE_KEY, JSON.stringify({ id: 0, children: [] }))
+ localStorage.setItem(
+ GUEST_MODE_STEP_CONTENT_STORAGE_KEY + stepInitialContent.stepId,
+ JSON.stringify(stepInitialContent),
+ )
+ }
+
+
+ const [stepId, setStepId] = useState(DEFAULT_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),
+ )
+ return SaveStates.Guest
+ }, 250), [stepId]),
+ )
+
+ return 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({ ...JSON.parse(localStorage.getItem(GUEST_MODE_STEP_CONTENT_STORAGE_KEY + step)!) }, false)
+ return
+ }, [setStepContent])}
+ onAddStep={useCallback(async parent => {
+ const root: StepInfoNode = JSON.parse(localStorage.getItem(GUEST_MODE_STEP_ROOT_NODE_INFO_STORAGE_KEY)!)
+
+ const nodeId = getAvailableId(root)
+ const node = { id: nodeId, children: [] }
+
+ localStorage.setItem(GUEST_MODE_STEP_ROOT_NODE_INFO_STORAGE_KEY, JSON.stringify(addStepNode(root, parent, node)))
+ localStorage.setItem(GUEST_MODE_STEP_CONTENT_STORAGE_KEY + node.id, JSON.stringify({
+ stepId: node.id,
+ components: [],
+ }))
+ 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() {
+ const [tactic, setTactic] = useState(null)
const { tacticId: idStr } = useParams()
- const id = guestMode ? -1 : parseInt(idStr!)
+ const id = parseInt(idStr!)
const navigation = useNavigate()
useEffect(() => {
- if (guestMode) return
async function initialize() {
const infoResponsePromise = fetchAPIGet(`tactics/${id}`)
const treeResponsePromise = fetchAPIGet(`tactics/${id}/tree`)
- const contentResponsePromise = fetchAPIGet(`tactics/${id}/steps/1`)
+ const contentResponsePromise = fetchAPIGet(`tactics/${id}/steps/${DEFAULT_STEP_ID}`)
const infoResponse = await infoResponsePromise
const treeResponse = await treeResponsePromise
@@ -153,160 +219,81 @@ export default function EditorPage({ guestMode }: EditorPageProps) {
}
initialize()
- }, [guestMode, id, idStr, navigation])
-
- if (tactic) {
- return (
-
- )
- }
-
- return
-}
-
-function EditorLoadingScreen() {
- return Loading Editor, please wait...
-}
-
-export interface EditorProps {
- id: number
- initialName: string
- courtType: "PLAIN" | "HALF"
- initialStepContent: StepContent
- initialStepId: number
- initialStepsNode: StepInfoNode
-}
-
-function Editor({
- id,
- initialName,
- courtType,
- initialStepContent,
- initialStepId,
- initialStepsNode,
-}: EditorProps) {
- const isInGuestMode = id == -1
- const navigate = useNavigate()
-
- const storageContent = localStorage.getItem(GUEST_MODE_CONTENT_STORAGE_KEY)
- const stepInitialContent = {
- ...(isInGuestMode && storageContent != null
- ? JSON.parse(storageContent)
- : initialStepContent),
- }
+ }, [id, idStr, navigation])
- const storage_name = localStorage.getItem(GUEST_MODE_TITLE_STORAGE_KEY)
- const editorName =
- isInGuestMode && storage_name != null ? storage_name : initialName
- const [stepId, setStepId] = useState(initialStepId)
+ const [stepId, setStepId] = useState(1)
const [stepContent, setStepContent, saveState] = useContentState(
- stepInitialContent,
- isInGuestMode ? SaveStates.Guest : SaveStates.Ok,
- useMemo(
- () =>
- debounceAsync(async (content: StepContent) => {
- if (isInGuestMode) {
- localStorage.setItem(
- GUEST_MODE_CONTENT_STORAGE_KEY,
- JSON.stringify(content),
- )
- return SaveStates.Guest
- }
- const response = await fetchAPI(
- `tactics/${id}/steps/${stepId}`,
- {
- content: {
- components: content.components,
- },
- },
- "PUT",
- )
- return response.ok ? SaveStates.Ok : SaveStates.Err
- }, 250),
- [id, isInGuestMode, stepId],
- ),
+ tactic?.content ?? { components: [] },
+ SaveStates.Ok,
+ useMemo(() => debounceAsync(async (content: StepContent) => {
+ const response = await fetchAPI(
+ `tactics/${id}/steps/${stepId}`,
+ {
+ content: {
+ components: content.components,
+ },
+ },
+ "PUT",
+ )
+ return response.ok ? SaveStates.Ok : SaveStates.Err
+ }, 250), [id, stepId]),
)
+ const onNameChange = useCallback((name: string) =>
+ fetchAPI(`tactics/${id}/edit/name`, { name })
+ .then((r) => r.ok)
+ , [id])
+
+ const selectStep = useCallback(async (step: number) => {
+ const response = await fetchAPIGet(`tactics/${id}/steps/${step}`)
+ if (!response.ok)
+ return
+ setStepId(step)
+ setStepContent({ ...await response.json() }, false)
+ }, [id, setStepContent])
+
+ const onAddStep = useCallback(async (parent: StepInfoNode) => {
+ const response = await fetchAPI(`tactics/${id}/steps`, {
+ parentId: parent.id,
+ })
+ if (!response.ok)
+ return null
+ const { stepId } = await response.json()
+ return { id: stepId, children: [] }
+ }, [id])
+
+ const onRemoveStep = useCallback((step: StepInfoNode) =>
+ fetchAPI(
+ `tactics/${id}/steps/${step.id}`,
+ {},
+ "DELETE",
+ ).then(r => r.ok)
+ , [id])
+
+
+ if (!tactic)
+ return
+
+ return setStepContent(content, true)}
+ saveState={saveState}
+ onNameChange={onNameChange}
+ selectStep={selectStep}
+ onAddStep={onAddStep}
+ onRemoveStep={onRemoveStep}
+ />
+}
- return (
- setStepContent(content, true)}
- courtType={courtType}
- saveState={saveState}
- onContentChange={async (content: StepContent) => {
- if (isInGuestMode) {
- localStorage.setItem(
- GUEST_MODE_CONTENT_STORAGE_KEY,
- JSON.stringify(content),
- )
- return SaveStates.Guest
- }
- const response = await fetchAPI(
- `tactics/${id}/steps/1`,
- { content },
- "PUT",
- )
- if (response.status == 401) {
- navigate("/login")
- }
- return response.ok ? SaveStates.Ok : SaveStates.Err
- }}
- onNameChange={async (name: string) => {
- if (isInGuestMode) {
- localStorage.setItem(GUEST_MODE_TITLE_STORAGE_KEY, name)
- return true //simulate that the name has been changed
- }
-
- const response = await fetchAPI(
- `tactics/${id}/name`,
- { name },
- "PUT",
- )
- if (response.status == 401) {
- navigate("/login")
- }
- return response.ok
- }}
- selectStep={async (step) => {
- const response = await fetchAPIGet(`tactics/${id}/steps/${step}`)
- if (!response.ok)
- return null
- setStepId(step)
- setStepContent({ ...await response.json() }, false)
- }}
- onAddStep={async (parent) => {
- const response = await fetchAPI(`tactics/${id}/steps`, {
- parentId: parent.id,
- })
- if (!response.ok) return null
- const { stepId } = await response.json()
- return { id: stepId, children: [] }
- }}
- onRemoveStep={async (step) => {
- const response = await fetchAPI(
- `tactics/${id}/steps/${step.id}`,
- {},
- "DELETE",
- )
- return response.ok
- }}
- />
- )
+function EditorLoadingScreen() {
+ return Loading Editor, Please wait...
}
export interface EditorViewProps {
@@ -320,20 +307,25 @@ export interface EditorViewProps {
onNameChange: (name: string) => Promise
onRemoveStep: (step: StepInfoNode) => Promise
onAddStep: (parent: StepInfoNode) => Promise
- courtType: "PLAIN" | "HALF"
}
-function EditorView({
- tactic: { name, rootStepNode: initialStepsNode },
- setCurrentStepContent: setContent,
- currentStepContent: content,
- saveState,
- onNameChange,
- selectStep,
- onRemoveStep,
- onAddStep,
- courtType,
-}: EditorViewProps) {
+
+
+function EditorPage({
+ tactic: {
+ name,
+ rootStepNode: initialStepsNode,
+ courtType,
+ },
+ setCurrentStepContent: setContent,
+ currentStepContent: content,
+ saveState,
+ onNameChange,
+ selectStep,
+ onRemoveStep,
+ onAddStep,
+ }: EditorViewProps) {
+
const [titleStyle, setTitleStyle] = useState({})
const [rootStepsNode, setRootStepsNode] = useState(initialStepsNode)
@@ -570,10 +562,10 @@ function EditorView({
const renderComponent = useCallback(
(component: TacticComponent) => {
- if (component.type == "player" || component.type == "phantom") {
+ if (component.type === "player" || component.type === "phantom") {
return renderPlayer(component)
}
- if (component.type == BALL_TYPE) {
+ if (component.type === BALL_TYPE) {
return (
{
const isOk = await onRemoveStep(removed)
- if (isOk)
- setRootStepsNode(
- (root) => removeStepNode(root, removed)!,
- )
+ selectStep(getParent(rootStepsNode, removed)!.id)
+ if (isOk) setRootStepsNode(
+ (root) => removeStepNode(root, removed)!,
+ )
},
- [onRemoveStep],
+ [rootStepsNode, onRemoveStep, selectStep],
)}
onStepSelected={useCallback(
(node) => selectStep(node.id),
@@ -879,7 +871,7 @@ function CourtPlayerArrowAction({
}))
}}
onHeadPicked={(headPos) => {
- ;(document.activeElement as HTMLElement).blur()
+ (document.activeElement as HTMLElement).blur()
setPreviewAction({
origin: playerInfo.id,
diff --git a/src/style/steps-tree.css b/src/style/steps_tree.css
similarity index 81%
rename from src/style/steps-tree.css
rename to src/style/steps_tree.css
index 00ff699..4b58090 100644
--- a/src/style/steps-tree.css
+++ b/src/style/steps_tree.css
@@ -18,18 +18,21 @@
user-select: none;
cursor: pointer;
+
+ border: 2px solid var(--editor-tree-background);
+}
+
+.step-piece-selected {
+ border: 2px solid var(--selection-color-light);
}
+.step-piece-selected,
.step-piece:focus,
.step-piece:hover {
background-color: var(--editor-tree-step-piece-hovered);
}
-.step-piece:focus-within .step-piece-actions {
- visibility: visible;
-}
-
-.step-piece:hover .step-piece-actions {
+.step-piece-selected .step-piece-actions, .step-piece:hover .step-piece-actions, .step-piece:focus-within .step-piece-actions {
visibility: visible;
}
diff --git a/src/style/theme/default.css b/src/style/theme/default.css
index 950450a..19702b0 100644
--- a/src/style/theme/default.css
+++ b/src/style/theme/default.css
@@ -13,6 +13,7 @@
--buttons-shadow-color: #a8a8a8;
--selection-color: #3f7fc4;
+ --selection-color-light: #acd8f8;
--border-color: #ffffff;