add a steps visualizer (fake data)

maxime 1 year ago
parent fd9b5e2063
commit 30df4e74bf

@ -1,7 +1,7 @@
import { ReactElement, ReactNode, RefObject } from "react" import { ReactElement, ReactNode, RefObject } from "react"
import { Action } from "../../model/tactic/Action" import { Action } from "../../model/tactic/Action"
import { CourtAction } from "./CourtAction.tsx" import { CourtAction } from "./CourtAction"
import { ComponentId, TacticComponent } from "../../model/tactic/Tactic" import { ComponentId, TacticComponent } from "../../model/tactic/Tactic"
export interface BasketCourtProps { export interface BasketCourtProps {

@ -1,7 +1,7 @@
import { Action, ActionKind } from "../../model/tactic/Action" import { Action, ActionKind } from "../../model/tactic/Action"
import BendableArrow from "../../components/arrows/BendableArrow" import BendableArrow from "../arrows/BendableArrow"
import { RefObject } from "react" import { RefObject } from "react"
import { MoveToHead, ScreenHead } from "../../components/actions/ArrowAction" import { MoveToHead, ScreenHead } from "../actions/ArrowAction"
import { ComponentId } from "../../model/tactic/Tactic" import { ComponentId } from "../../model/tactic/Tactic"
export interface CourtActionProps { export interface CourtActionProps {

@ -0,0 +1,58 @@
import {StepInfoNode} from "../../model/tactic/Tactic";
import "../../style/steps-tree.css"
import BendableArrow from "../arrows/BendableArrow";
import {useRef} from "react";
export interface StepsTreeProps {
root: StepInfoNode
}
export default function StepsTree({root}: StepsTreeProps) {
return <div className="steps-tree">
<StepsTreeNode node={root}/>
</div>
}
interface StepsTreeContentProps {
node: StepInfoNode
}
function StepsTreeNode({node}: StepsTreeContentProps) {
const ref = useRef<HTMLDivElement>(null)
return (
<div ref={ref}
className={"step-group"}>
<StepPiece id={node.id}/>
{node.children.map(child => (
<BendableArrow
key={child.id}
area={ref}
startPos={"step-piece-" + node.id}
segments={[{next: "step-piece-" + child.id}]}
onSegmentsChanges={() => {
}}
forceStraight={true}
wavy={false}
//TODO remove magic constant
startRadius={10}
endRadius={10}
/>
))}
<div className={"step-children"}>
{node.children.map(child => <StepsTreeNode key={child.id} node={child}/>)}
</div>
</div>
)
}
interface StepPieceProps {
id: number
}
function StepPiece({id}: StepPieceProps) {
return (
<div id={"step-piece-" + id} className={"step-piece"}>
<p>{id}</p>
</div>
)
}

@ -4,15 +4,15 @@ import {
PlayerLike, PlayerLike,
PlayerPhantom, PlayerPhantom,
} from "../model/tactic/Player" } from "../model/tactic/Player"
import { ratioWithinBase } from "../geo/Pos" import {ratioWithinBase} from "../geo/Pos"
import { import {
ComponentId, ComponentId,
TacticComponent, TacticComponent,
TacticContent, StepContent,
} from "../model/tactic/Tactic" } from "../model/tactic/Tactic"
import { overlaps } from "../geo/Box" import {overlaps} from "../geo/Box"
import { Action, ActionKind, moves } from "../model/tactic/Action" import {Action, ActionKind, moves} from "../model/tactic/Action"
import { removeBall, updateComponent } from "./TacticContentDomains" import {removeBall, updateComponent} from "./TacticContentDomains"
import { import {
areInSamePath, areInSamePath,
changePlayerBallState, changePlayerBallState,
@ -22,7 +22,7 @@ import {
isNextInPath, isNextInPath,
removePlayer, removePlayer,
} from "./PlayerDomains" } from "./PlayerDomains"
import { BALL_TYPE } from "../model/tactic/CourtObjects" import {BALL_TYPE} from "../model/tactic/CourtObjects"
export function getActionKind( export function getActionKind(
target: TacticComponent | null, target: TacticComponent | null,
@ -31,12 +31,12 @@ export function getActionKind(
switch (ballState) { switch (ballState) {
case BallState.HOLDS_ORIGIN: case BallState.HOLDS_ORIGIN:
return target return target
? { kind: ActionKind.SHOOT, nextState: BallState.PASSED_ORIGIN } ? {kind: ActionKind.SHOOT, nextState: BallState.PASSED_ORIGIN}
: { kind: ActionKind.DRIBBLE, nextState: ballState } : {kind: ActionKind.DRIBBLE, nextState: ballState}
case BallState.HOLDS_BY_PASS: case BallState.HOLDS_BY_PASS:
return target return target
? { kind: ActionKind.SHOOT, nextState: BallState.PASSED } ? {kind: ActionKind.SHOOT, nextState: BallState.PASSED}
: { kind: ActionKind.DRIBBLE, nextState: ballState } : {kind: ActionKind.DRIBBLE, nextState: ballState}
case BallState.PASSED_ORIGIN: case BallState.PASSED_ORIGIN:
case BallState.PASSED: case BallState.PASSED:
case BallState.NONE: case BallState.NONE:
@ -212,8 +212,8 @@ export function createAction(
origin: PlayerLike, origin: PlayerLike,
courtBounds: DOMRect, courtBounds: DOMRect,
arrowHead: DOMRect, arrowHead: DOMRect,
content: TacticContent, content: StepContent,
): { createdAction: Action; newContent: TacticContent } { ): { createdAction: Action; newContent: StepContent } {
/** /**
* Creates a new phantom component. * Creates a new phantom component.
* Be aware that this function will reassign the `content` parameter. * Be aware that this function will reassign the `content` parameter.
@ -222,7 +222,7 @@ export function createAction(
forceHasBall: boolean, forceHasBall: boolean,
attachedTo?: ComponentId, attachedTo?: ComponentId,
): ComponentId { ): ComponentId {
const { x, y } = ratioWithinBase(arrowHead, courtBounds) const {x, y} = ratioWithinBase(arrowHead, courtBounds)
let itemIndex: number let itemIndex: number
let originPlayer: Player let originPlayer: Player
@ -320,13 +320,13 @@ export function createAction(
action = { action = {
target: toId, target: toId,
type: actionKind, type: actionKind,
segments: [{ next: toId }], segments: [{next: toId}],
} }
} else { } else {
action = { action = {
target: toId, target: toId,
type: actionKind, type: actionKind,
segments: [{ next: toId }], segments: [{next: toId}],
} }
} }
@ -355,7 +355,7 @@ export function createAction(
const action: Action = { const action: Action = {
target: phantomId, target: phantomId,
type: actionKind, type: actionKind,
segments: [{ next: phantomId }], segments: [{next: phantomId}],
} }
return { return {
newContent: updateComponent( newContent: updateComponent(
@ -371,8 +371,8 @@ export function createAction(
export function removeAllActionsTargeting( export function removeAllActionsTargeting(
componentId: ComponentId, componentId: ComponentId,
content: TacticContent, content: StepContent,
): TacticContent { ): StepContent {
const components = [] const components = []
for (let i = 0; i < content.components.length; i++) { for (let i = 0; i < content.components.length; i++) {
const component = content.components[i] const component = content.components[i]
@ -391,9 +391,10 @@ export function removeAllActionsTargeting(
export function removeAction( export function removeAction(
origin: TacticComponent, origin: TacticComponent,
actionIdx: number, actionIdx: number,
content: TacticContent, content: StepContent,
): TacticContent { ): StepContent {
const action = origin.actions[actionIdx] const action = origin.actions[actionIdx]
origin = { origin = {
...origin, ...origin,
actions: origin.actions.toSpliced(actionIdx, 1), actions: origin.actions.toSpliced(actionIdx, 1),
@ -462,8 +463,8 @@ export function removeAction(
export function spreadNewStateFromOriginStateChange( export function spreadNewStateFromOriginStateChange(
origin: PlayerLike, origin: PlayerLike,
newState: BallState, newState: BallState,
content: TacticContent, content: StepContent,
): TacticContent { ): StepContent {
if (origin.ballState === newState) { if (origin.ballState === newState) {
return content return content
} }
@ -534,7 +535,7 @@ export function spreadNewStateFromOriginStateChange(
i-- // step back i-- // step back
} else { } else {
// do not change the action type if it is a shoot action // do not change the action type if it is a shoot action
const { kind, nextState } = getActionKindBetween( const {kind, nextState} = getActionKindBetween(
origin, origin,
actionTarget, actionTarget,
newState, newState,

@ -1,30 +1,11 @@
import { import {BallState, Player, PlayerLike, PlayerPhantom,} from "../model/tactic/Player"
BallState, import {ComponentId, StepContent, TacticComponent,} from "../model/tactic/Tactic"
Player,
PlayerLike, import {removeComponent, updateComponent} from "./TacticContentDomains"
PlayerPhantom, import {removeAllActionsTargeting, spreadNewStateFromOriginStateChange,} from "./ActionsDomains"
} from "../model/tactic/Player" import {ActionKind} from "../model/tactic/Action"
import { import {add, minus, norm, Pos, posWithinBase, ratioWithinBase, relativeTo,} from "../geo/Pos.ts"
ComponentId, import {PLAYER_RADIUS_PIXELS} from "../components/editor/CourtPlayer.tsx"
TacticComponent,
TacticContent,
} from "../model/tactic/Tactic"
import { removeComponent, updateComponent } from "./TacticContentDomains"
import {
removeAllActionsTargeting,
spreadNewStateFromOriginStateChange,
} from "./ActionsDomains"
import { ActionKind } from "../model/tactic/Action"
import {
add,
minus,
norm,
Pos,
posWithinBase,
ratioWithinBase,
relativeTo,
} from "../geo/Pos.ts"
import { PLAYER_RADIUS_PIXELS } from "../components/editor/CourtPlayer.tsx"
export function getOrigin( export function getOrigin(
pathItem: PlayerPhantom, pathItem: PlayerPhantom,
@ -58,7 +39,7 @@ export function getPlayerNextTo(
// following another phantom and / or the origin of the phantom is another // following another phantom and / or the origin of the phantom is another
export function computePhantomPositioning( export function computePhantomPositioning(
phantom: PlayerPhantom, phantom: PlayerPhantom,
content: TacticContent, content: StepContent,
area: DOMRect, area: DOMRect,
): Pos { ): Pos {
const positioning = phantom.pos const positioning = phantom.pos
@ -171,8 +152,8 @@ export function isNextInPath(
export function clearPlayerPath( export function clearPlayerPath(
player: Player, player: Player,
content: TacticContent, content: StepContent,
): TacticContent { ): StepContent {
if (player.path == null) { if (player.path == null) {
return content return content
} }
@ -192,8 +173,8 @@ export function clearPlayerPath(
function removeAllPhantomsAttached( function removeAllPhantomsAttached(
to: ComponentId, to: ComponentId,
content: TacticContent, content: StepContent,
): TacticContent { ): StepContent {
let i = 0 let i = 0
while (i < content.components.length) { while (i < content.components.length) {
const component = content.components[i] const component = content.components[i]
@ -213,8 +194,8 @@ function removeAllPhantomsAttached(
export function removePlayer( export function removePlayer(
player: PlayerLike, player: PlayerLike,
content: TacticContent, content: StepContent,
): TacticContent { ): StepContent {
content = removeAllActionsTargeting(player.id, content) content = removeAllActionsTargeting(player.id, content)
content = removeAllPhantomsAttached(player.id, content) content = removeAllPhantomsAttached(player.id, content)
@ -266,8 +247,8 @@ export function removePlayer(
export function truncatePlayerPath( export function truncatePlayerPath(
player: Player, player: Player,
phantom: PlayerPhantom, phantom: PlayerPhantom,
content: TacticContent, content: StepContent,
): TacticContent { ): StepContent {
if (player.path == null) return content if (player.path == null) return content
const path = player.path! const path = player.path!
@ -300,7 +281,7 @@ export function truncatePlayerPath(
export function changePlayerBallState( export function changePlayerBallState(
player: PlayerLike, player: PlayerLike,
newState: BallState, newState: BallState,
content: TacticContent, content: StepContent,
): TacticContent { ): StepContent {
return spreadNewStateFromOriginStateChange(player, newState, content) return spreadNewStateFromOriginStateChange(player, newState, content)
} }

@ -1,4 +1,4 @@
import { Pos, ratioWithinBase } from "../geo/Pos" import {Pos, ratioWithinBase} from "../geo/Pos"
import { import {
BallState, BallState,
Player, Player,
@ -15,12 +15,13 @@ import {
import { import {
ComponentId, ComponentId,
TacticComponent, TacticComponent,
TacticContent, StepContent,
} from "../model/tactic/Tactic" } from "../model/tactic/Tactic"
import { overlaps } from "../geo/Box"
import { RackedCourtObject, RackedPlayer } from "./RackedItems" import {overlaps} from "../geo/Box"
import { changePlayerBallState, getComponent, getOrigin } from "./PlayerDomains" import {RackedCourtObject, RackedPlayer} from "./RackedItems"
import { ActionKind } from "../model/tactic/Action.ts" import {changePlayerBallState, getComponent, getOrigin} from "./PlayerDomains"
import {ActionKind} from "../model/tactic/Action.ts"
export function placePlayerAt( export function placePlayerAt(
refBounds: DOMRect, refBounds: DOMRect,
@ -45,14 +46,15 @@ export function placeObjectAt(
refBounds: DOMRect, refBounds: DOMRect,
courtBounds: DOMRect, courtBounds: DOMRect,
rackedObject: RackedCourtObject, rackedObject: RackedCourtObject,
content: TacticContent, content: StepContent,
): TacticContent { ): StepContent {
const pos = ratioWithinBase(refBounds, courtBounds) const pos = ratioWithinBase(refBounds, courtBounds)
let courtObject: CourtObject let courtObject: CourtObject
switch (rackedObject.key) { switch (rackedObject.key) {
case BALL_TYPE: case BALL_TYPE: {
const playerCollidedIdx = getComponentCollided( const playerCollidedIdx = getComponentCollided(
refBounds, refBounds,
content.components, content.components,
@ -69,7 +71,7 @@ export function placeObjectAt(
actions: [], actions: [],
} }
break break
}
default: default:
throw new Error("unknown court object " + rackedObject.key) throw new Error("unknown court object " + rackedObject.key)
} }
@ -82,9 +84,9 @@ export function placeObjectAt(
export function dropBallOnComponent( export function dropBallOnComponent(
targetedComponentIdx: number, targetedComponentIdx: number,
content: TacticContent, content: StepContent,
setAsOrigin: boolean, setAsOrigin: boolean,
): TacticContent { ): StepContent {
const component = content.components[targetedComponentIdx] const component = content.components[targetedComponentIdx]
if (component.type === "player" || component.type === "phantom") { if (component.type === "player" || component.type === "phantom") {
@ -101,7 +103,7 @@ export function dropBallOnComponent(
return removeBall(content) return removeBall(content)
} }
export function removeBall(content: TacticContent): TacticContent { export function removeBall(content: StepContent): StepContent {
const ballObjIdx = content.components.findIndex((o) => o.type == "ball") const ballObjIdx = content.components.findIndex((o) => o.type == "ball")
if (ballObjIdx == -1) { if (ballObjIdx == -1) {
@ -117,8 +119,8 @@ export function removeBall(content: TacticContent): TacticContent {
export function placeBallAt( export function placeBallAt(
refBounds: DOMRect, refBounds: DOMRect,
courtBounds: DOMRect, courtBounds: DOMRect,
content: TacticContent, content: StepContent,
): TacticContent { ): StepContent {
if (!overlaps(courtBounds, refBounds)) { if (!overlaps(courtBounds, refBounds)) {
return removeBall(content) return removeBall(content)
} }
@ -162,9 +164,9 @@ export function moveComponent(
component: TacticComponent, component: TacticComponent,
info: PlayerInfo, info: PlayerInfo,
courtBounds: DOMRect, courtBounds: DOMRect,
content: TacticContent, content: StepContent,
removed: (content: TacticContent) => TacticContent, removed: (content: StepContent) => StepContent,
): TacticContent { ): StepContent {
const playerBounds = document const playerBounds = document
.getElementById(info.id)! .getElementById(info.id)!
.getBoundingClientRect() .getBoundingClientRect()
@ -232,8 +234,8 @@ export function moveComponent(
export function removeComponent( export function removeComponent(
componentId: ComponentId, componentId: ComponentId,
content: TacticContent, content: StepContent,
): TacticContent { ): StepContent {
return { return {
...content, ...content,
components: content.components.filter((c) => c.id !== componentId), components: content.components.filter((c) => c.id !== componentId),
@ -242,8 +244,8 @@ export function removeComponent(
export function updateComponent( export function updateComponent(
component: TacticComponent, component: TacticComponent,
content: TacticContent, content: StepContent,
): TacticContent { ): StepContent {
return { return {
...content, ...content,
components: content.components.map((c) => components: content.components.map((c) =>
@ -285,5 +287,5 @@ export function getRackPlayers(
c.type == "player" && c.team == team && c.role == role, c.type == "player" && c.team == team && c.role == role,
) == -1, ) == -1,
) )
.map((key) => ({ team, key })) .map((key) => ({team, key}))
} }

@ -2,21 +2,27 @@ import { Player, PlayerPhantom } from "./Player"
import { Action } from "./Action" import { Action } from "./Action"
import { CourtObject } from "./CourtObjects" import { CourtObject } from "./CourtObjects"
export type CourtType = "HALF" | "PLAIN"
export interface Tactic { export interface Tactic {
id: number readonly id: number
name: string readonly name: string
courtType: CourtType readonly courtType: CourtType
content: TacticContent readonly currentStepContent: StepContent
readonly rootStepNode: StepInfoNode
}
export interface StepContent {
readonly stepId: number
readonly components: TacticComponent[]
} }
export interface TacticContent { export interface StepInfoNode {
components: TacticComponent[] readonly id: number
readonly children: StepInfoNode[]
} }
export type TacticComponent = Player | CourtObject | PlayerPhantom export type TacticComponent = Player | CourtObject | PlayerPhantom
export type ComponentId = string export type ComponentId = string
export type CourtType = "PLAIN" | "HALF"
export interface Component<T, Positioning> { export interface Component<T, Positioning> {
/** /**

@ -14,28 +14,25 @@ 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 {
CourtType, CourtType, StepContent, StepInfoNode,
Tactic, Tactic,
TacticComponent, TacticComponent,
TacticContent,
} from "../model/tactic/Tactic" } from "../model/tactic/Tactic"
import { fetchAPI, fetchAPIGet } from "../Fetcher" import { fetchAPI, fetchAPIGet } from "../Fetcher"
import SavingState, { import SavingState, {SaveState, SaveStates,} from "../components/editor/SavingState"
SaveState,
SaveStates,
} 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 {
dropBallOnComponent, dropBallOnComponent,
getComponentCollided, getComponentCollided,
@ -47,24 +44,14 @@ import {
removeBall, removeBall,
updateComponent, updateComponent,
} from "../editor/TacticContentDomains" } from "../editor/TacticContentDomains"
import {
BallState, import {BallState, Player, PlayerInfo, PlayerLike, PlayerTeam,} from "../model/tactic/Player"
Player, import {RackedCourtObject, RackedPlayer} from "../editor/RackedItems"
PlayerInfo,
PlayerLike,
PlayerTeam,
} from "../model/tactic/Player"
import { RackedCourtObject, RackedPlayer } from "../editor/RackedItems"
import CourtPlayer from "../components/editor/CourtPlayer" import CourtPlayer from "../components/editor/CourtPlayer"
import { import {createAction, getActionKind, isActionValid, removeAction,} from "../editor/ActionsDomains"
createAction,
getActionKind,
isActionValid,
removeAction,
} 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 {
changePlayerBallState, changePlayerBallState,
@ -75,6 +62,7 @@ import {
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 { DEFAULT_TACTIC_NAME } from "./NewTacticPage.tsx" import { DEFAULT_TACTIC_NAME } from "./NewTacticPage.tsx"
import StepsTree from "../components/editor/StepsTree"
const ERROR_STYLE: CSSProperties = { const ERROR_STYLE: CSSProperties = {
borderColor: "red", borderColor: "red",
@ -85,7 +73,7 @@ const GUEST_MODE_TITLE_STORAGE_KEY = "guest_mode_title"
export interface EditorViewProps { export interface EditorViewProps {
tactic: Tactic tactic: Tactic
onContentChange: (tactic: TacticContent) => Promise<SaveState> onContentChange: (tactic: StepContent) => Promise<SaveState>
onNameChange: (name: string) => Promise<boolean> onNameChange: (name: string) => Promise<boolean>
} }
interface TacticDto { interface TacticDto {
@ -93,6 +81,7 @@ interface TacticDto {
name: string name: string
courtType: CourtType courtType: CourtType
content: string content: string
root: StepInfoNode
} }
interface EditorPageProps { interface EditorPageProps {
@ -107,6 +96,7 @@ export default function EditorPage({ guestMode }: EditorPageProps) {
courtType: "PLAIN", courtType: "PLAIN",
content: '{"components": []}', content: '{"components": []}',
name: DEFAULT_TACTIC_NAME, name: DEFAULT_TACTIC_NAME,
root: {id: 1, children: []}
} }
} }
return null return null
@ -120,9 +110,11 @@ export default function EditorPage({ guestMode }: EditorPageProps) {
async function initialize() { async function initialize() {
const infoResponsePromise = fetchAPIGet(`tactics/${id}`) const infoResponsePromise = fetchAPIGet(`tactics/${id}`)
const treeResponsePromise = fetchAPIGet(`tactics/${id}/tree`)
const contentResponsePromise = fetchAPIGet(`tactics/${id}/steps/1`) const contentResponsePromise = fetchAPIGet(`tactics/${id}/steps/1`)
const infoResponse = await infoResponsePromise const infoResponse = await infoResponsePromise
const treeResponse = await treeResponsePromise
const contentResponse = await contentResponsePromise const contentResponse = await contentResponsePromise
if (infoResponse.status == 401 || contentResponse.status == 401) { if (infoResponse.status == 401 || contentResponse.status == 401) {
@ -132,8 +124,9 @@ export default function EditorPage({ guestMode }: EditorPageProps) {
const { name, courtType } = await infoResponse.json() const { name, courtType } = await infoResponse.json()
const content = await contentResponse.text() const content = await contentResponse.text()
const { root } = await treeResponse.json()
setTactic({ id, name, courtType, content }) setTactic({ id, name, courtType, content, root })
} }
initialize() initialize()
@ -143,6 +136,7 @@ export default function EditorPage({ guestMode }: EditorPageProps) {
return ( return (
<Editor <Editor
id={id} id={id}
rootStepNode={tactic.root}
courtType={tactic.courtType} courtType={tactic.courtType}
content={tactic.content} content={tactic.content}
name={tactic.name} name={tactic.name}
@ -161,14 +155,15 @@ export interface EditorProps {
id: number id: number
name: string name: string
content: string content: string
courtType: CourtType courtType: CourtType,
rootStepNode: StepInfoNode
} }
function Editor({ id, name, courtType, content }: EditorProps) { function Editor({ id, name, courtType, content, rootStepNode }: EditorProps) {
const isInGuestMode = id == -1 const isInGuestMode = id == -1
const storageContent = localStorage.getItem(GUEST_MODE_CONTENT_STORAGE_KEY) const storageContent = localStorage.getItem(GUEST_MODE_CONTENT_STORAGE_KEY)
const editorContent = const stepContent =
isInGuestMode && storageContent != null ? storageContent : content isInGuestMode && storageContent != null ? storageContent : content
const storageName = localStorage.getItem(GUEST_MODE_TITLE_STORAGE_KEY) const storageName = localStorage.getItem(GUEST_MODE_TITLE_STORAGE_KEY)
@ -182,9 +177,13 @@ function Editor({ id, name, courtType, content }: EditorProps) {
name: editorName, name: editorName,
id, id,
courtType, courtType,
content: JSON.parse(editorContent), currentStepContent: {
stepId: 1,
...JSON.parse(stepContent)
},
rootStepNode,
}} }}
onContentChange={async (content: TacticContent) => { onContentChange={async (content: StepContent) => {
if (isInGuestMode) { if (isInGuestMode) {
localStorage.setItem( localStorage.setItem(
GUEST_MODE_CONTENT_STORAGE_KEY, GUEST_MODE_CONTENT_STORAGE_KEY,
@ -196,6 +195,7 @@ function Editor({ id, name, courtType, content }: EditorProps) {
`tactics/${id}/steps/1`, `tactics/${id}/steps/1`,
{ content }, { content },
"PUT", "PUT",
) )
if (response.status == 401) { if (response.status == 401) {
navigate("/login") navigate("/login")
@ -212,21 +212,39 @@ function Editor({ id, name, courtType, content }: EditorProps) {
`tactics/${id}/name`, `tactics/${id}/name`,
{ name }, { name },
"PUT", "PUT",
) )
if (response.status == 401) { if (response.status == 401) {
navigate("/login") navigate("/login")
} }
return response.ok return response.ok
}} }}
onStepSelected={() => {
}}
stepsContentsRoot={rootStepNode}
courtType={courtType}
/> />
) )
} }
export interface EditorViewProps {
tactic: Tactic
onContentChange: (tactic: StepContent) => Promise<SaveState>
onStepSelected: (stepId: number) => void,
onNameChange: (name: string) => Promise<boolean>
stepsContentsRoot: StepInfoNode
courtType: "PLAIN" | "HALF"
}
function EditorView({ function EditorView({
tactic: { id, name, content: initialContent, courtType },
tactic: {id, name, currentStepContent: initialContent},
onContentChange, onContentChange,
onNameChange, onNameChange,
}: EditorViewProps) { onStepSelected,
stepsContentsRoot,
courtType,
}: EditorViewProps) {
const isInGuestMode = id == -1 const isInGuestMode = id == -1
const [titleStyle, setTitleStyle] = useState<CSSProperties>({}) const [titleStyle, setTitleStyle] = useState<CSSProperties>({})
@ -244,13 +262,15 @@ function EditorView({
) )
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>(
null, null,
) )
const [isStepsTreeVisible, setStepsTreeVisible] = useState(false)
const courtRef = useRef<HTMLDivElement>(null) const courtRef = useRef<HTMLDivElement>(null)
const setComponents = (action: SetStateAction<TacticComponent[]>) => { const setComponents = (action: SetStateAction<TacticComponent[]>) => {
@ -267,7 +287,7 @@ function EditorView({
) )
useEffect(() => { useEffect(() => {
setObjects(isBallOnCourt(content) ? [] : [{ key: "ball" }]) setObjects(isBallOnCourt(content) ? [] : [{key: "ball"}])
}, [setObjects, content]) }, [setObjects, content])
const insertRackedPlayer = (player: Player) => { const insertRackedPlayer = (player: Player) => {
@ -280,7 +300,7 @@ function EditorView({
setter = setAllies setter = setAllies
} }
if (player.ballState == BallState.HOLDS_BY_PASS) { if (player.ballState == BallState.HOLDS_BY_PASS) {
setObjects([{ key: "ball" }]) setObjects([{key: "ball"}])
} }
setter((players) => [ setter((players) => [
...players, ...players,
@ -479,7 +499,7 @@ function EditorView({
setContent((content) => removeBall(content)) setContent((content) => removeBall(content))
setObjects((objects) => [ setObjects((objects) => [
...objects, ...objects,
{ key: "ball" }, {key: "ball"},
]) ])
}} }}
/> />
@ -516,7 +536,7 @@ function EditorView({
<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
@ -532,9 +552,14 @@ function EditorView({
)} )}
/> />
</div> </div>
<div id="topbar-right" /> <div id="topbar-right">
<button onClick={() => setStepsTreeVisible((b) => !b)}>
STEPS
</button>
</div>
</div> </div>
<div id="edit-div"> <div id="editor-div">
<div id="content-div">
<div id="racks"> <div id="racks">
<PlayerRack <PlayerRack
id={"allies"} id={"allies"}
@ -583,7 +608,7 @@ function EditorView({
<div id="court-div-bounds"> <div id="court-div-bounds">
<BasketCourt <BasketCourt
components={content.components} components={content.components}
courtImage={<Court courtType={courtType} />} courtImage={<Court courtType={courtType}/>}
courtRef={courtRef} courtRef={courtRef}
previewAction={previewAction} previewAction={previewAction}
renderComponent={renderComponent} renderComponent={renderComponent}
@ -592,6 +617,69 @@ function EditorView({
</div> </div>
</div> </div>
</div> </div>
<EditorStepsTree isVisible={isStepsTreeVisible} root={stepsContentsRoot}/>
</div>
</div>
)
}
interface EditorStepsTreeProps {
isVisible: boolean
root: StepInfoNode
}
function EditorStepsTree({isVisible, root}: EditorStepsTreeProps) {
const fakeRoot: StepInfoNode = {
id: 0,
children: [
{
id: 1,
children: [
{
id: 2,
children: []
},
{
id: 3,
children: [{
id: 4,
children: []
}]
}
]
},
{
id: 5,
children: [
{
id: 6,
children: []
},
{
id: 7,
children: []
}
]
},
{
id: 8,
children: [
{
id: 9,
children: [
{
id: 10,
children: []
}
]
}
]
}
]
}
return (
<div id="steps-div" style={{transform: isVisible ? "translateX(0)" : "translateX(100%)"}}>
<StepsTree root={fakeRoot}/>
</div> </div>
) )
} }
@ -612,7 +700,7 @@ function PlayerRack({
setObjects, setObjects,
courtRef, courtRef,
setComponents, setComponents,
}: PlayerRackProps) { }: PlayerRackProps) {
const courtBounds = useCallback( const courtBounds = useCallback(
() => courtRef.current!.getBoundingClientRect(), () => courtRef.current!.getBoundingClientRect(),
[courtRef], [courtRef],
@ -640,7 +728,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}
@ -659,8 +747,8 @@ interface CourtPlayerArrowActionProps {
player: PlayerLike player: PlayerLike
isInvalid: boolean isInvalid: boolean
content: TacticContent content: StepContent
setContent: (state: SetStateAction<TacticContent>) => void setContent: (state: SetStateAction<StepContent>) => void
setPreviewAction: (state: SetStateAction<ActionPreview | null>) => void setPreviewAction: (state: SetStateAction<ActionPreview | null>) => void
courtRef: RefObject<HTMLDivElement> courtRef: RefObject<HTMLDivElement>
} }
@ -674,7 +762,7 @@ function CourtPlayerArrowAction({
setContent, setContent,
setPreviewAction, setPreviewAction,
courtRef, courtRef,
}: CourtPlayerArrowActionProps) { }: CourtPlayerArrowActionProps) {
const courtBounds = useCallback( const courtBounds = useCallback(
() => courtRef.current!.getBoundingClientRect(), () => courtRef.current!.getBoundingClientRect(),
[courtRef], [courtRef],
@ -729,7 +817,7 @@ function CourtPlayerArrowAction({
} }
setContent((content) => { setContent((content) => {
let { createdAction, newContent } = createAction( let {createdAction, newContent} = createAction(
player, player,
courtBounds(), courtBounds(),
headRect, headRect,
@ -768,7 +856,7 @@ function CourtPlayerArrowAction({
) )
} }
function isBallOnCourt(content: TacticContent) { function isBallOnCourt(content: StepContent) {
return ( return (
content.components.findIndex( content.components.findIndex(
(c) => (c) =>
@ -782,18 +870,18 @@ function isBallOnCourt(content: TacticContent) {
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>
) )

@ -26,13 +26,13 @@
width: 100%; width: 100%;
display: flex; display: flex;
background-color: var(--main-color); background-color: var(--main-color);
margin-bottom: 3px;
justify-content: space-between; justify-content: space-between;
align-items: stretch; align-items: stretch;
} }
#racks { #racks {
margin: 3px 6px 0 6px;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
@ -44,8 +44,28 @@
align-self: center; align-self: center;
} }
#edit-div { #editor-div {
display: flex;
flex-direction: row;
}
#content-div,
#editor-div {
height: 100%; height: 100%;
width: 100%;
}
#content-div {
width: 100%;
}
#steps-div {
background-color: var(--editor-tree-background);
overflow: hidden;
width: 20%;
transform: translateX(100%);
transition: transform 500ms;
} }
#allies-rack, #allies-rack,

@ -0,0 +1,48 @@
.step-piece {
font-family: monospace;
pointer-events: all;
background-color: var(--editor-tree-step-piece);
color: var(--selected-team-secondarycolor);
border-radius: 100px;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
user-select: none;
}
.step-children {
margin-top: 10vh;
display: flex;
flex-direction: row;
width: 100%;
height: 100%;
}
.step-group {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
height: 100%;
}
.steps-tree {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding-top: 10%;
height: 100%;
}

@ -29,4 +29,6 @@
--main-contrast-color: #e6edf3; --main-contrast-color: #e6edf3;
--font-title: Helvetica; --font-title: Helvetica;
--font-content: Helvetica; --font-content: Helvetica;
--editor-tree-background: #503636;
--editor-tree-step-piece: #0bd9d9;
} }

Loading…
Cancel
Save