From eef1e16830c9f75e13b49e333b79e5555fdd9b95 Mon Sep 17 00:00:00 2001 From: maxime Date: Tue, 26 Mar 2024 00:35:31 +0100 Subject: [PATCH] add JSON export --- src/App.tsx | 5 +- src/assets/icon/account.png | Bin 507 -> 0 bytes src/assets/icon/export.svg | 3 + src/assets/icon/json.svg | 1 + src/components/Visualizer.tsx | 2 +- src/components/editor/BasketCourt.tsx | 27 +-- src/components/editor/CourtAction.tsx | 2 +- src/components/editor/StepsTree.tsx | 2 +- src/domains/ActionsDomains.ts | 2 +- src/domains/PlayerDomains.ts | 2 +- src/domains/StepsDomain.ts | 6 +- src/domains/TacticContentDomains.ts | 2 +- src/editor/ContentVersions.ts | 2 +- src/model/tactic/Action.ts | 2 +- src/model/tactic/CourtObjects.ts | 2 +- src/model/tactic/Player.ts | 2 +- src/model/tactic/{Tactic.ts => TacticInfo.ts} | 7 + src/pages/Editor.tsx | 36 +++- src/pages/HomePage.tsx | 138 +++++++++++----- src/pages/NewTacticPage.tsx | 2 +- src/pages/VisualizerPage.tsx | 33 +++- src/pages/popup/ExportTacticPopup.tsx | 154 ++++++++++++++++++ src/service/APITacticService.ts | 2 +- src/service/LocalStorageTacticService.ts | 2 +- src/service/MutableTacticService.ts | 6 +- src/style/court.css | 2 +- src/style/editor.css | 11 +- src/style/export_tactic_popup.css | 65 ++++++++ src/style/home/home.css | 52 ++++-- src/style/visualizer.css | 12 +- src/visualizer/VisualizerState.ts | 6 +- 31 files changed, 487 insertions(+), 103 deletions(-) delete mode 100644 src/assets/icon/account.png create mode 100644 src/assets/icon/export.svg create mode 100644 src/assets/icon/json.svg rename src/model/tactic/{Tactic.ts => TacticInfo.ts} (86%) create mode 100644 src/pages/popup/ExportTacticPopup.tsx create mode 100644 src/style/export_tactic_popup.css diff --git a/src/App.tsx b/src/App.tsx index f1ab122..2901b45 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -23,7 +23,6 @@ import { import { BASE } from "./Constants.ts" import { Authentication, Fetcher } from "./app/Fetcher.ts" import { User } from "./model/User.ts" -import { VisualizerPage } from "./pages/VisualizerPage.tsx" const HomePage = lazy(() => import("./pages/HomePage.tsx")) const LoginPage = lazy(() => import("./pages/LoginPage.tsx")) @@ -33,6 +32,7 @@ const CreateTeamPage = lazy(() => import("./pages/CreateTeamPage.tsx")) const TeamPanelPage = lazy(() => import("./pages/TeamPanel.tsx")) const NewTacticPage = lazy(() => import("./pages/NewTacticPage.tsx")) const Editor = lazy(() => import("./pages/Editor.tsx")) +const VisualizerPage = lazy(() => import("./pages/VisualizerPage.tsx")) const Settings = lazy(() => import("./pages/Settings.tsx")) const TOKEN_REFRESH_INTERVAL_MS = 60 * 1000 @@ -148,14 +148,13 @@ export default function App() { element={suspense( - , , )} /> , + )} /> 25Zg zH(#4B7cD{=|_c`Ze4&X!=@5HN#C|jLhiPtH;F@Sq1U)Y5Y zec9tJ#vni|@uhg*Wat3YqZB|i&IR^40)$AR2LtW&hXADI#ZC|PCD+i78Q~lv9e{t^ zp8CAm`qa+Lw?+^F9G&pm0N@cIJ2v@7-<5b6f!Cr- zUj+EE%c;&v)qs>?a7aBnr2yoeqQ@>G9o|I|WE5a5MF}TIMWpRh+(nlva>?TwL&z4H zs?(+vv+HB6ku&TnD=fy*X^i8rL(V%&wy9tAhlcWhh8^ia|0_7M^^)VL@HCVTJJ-*S z3Y6loBMsjBZeaLDhZI7S!_F^95W0&!BZRa;qywWA)!)LFaTKS1I2fiTIU?r(ty4-R xzf9@^Oy1~O&WHv5`c*S^2f6S`~nTSY#9m*AXNYW002ovPDHLkV1k67-3$N# diff --git a/src/assets/icon/export.svg b/src/assets/icon/export.svg new file mode 100644 index 0000000..d87d8dc --- /dev/null +++ b/src/assets/icon/export.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icon/json.svg b/src/assets/icon/json.svg new file mode 100644 index 0000000..0761585 --- /dev/null +++ b/src/assets/icon/json.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/Visualizer.tsx b/src/components/Visualizer.tsx index 691c53f..7a6dad4 100644 --- a/src/components/Visualizer.tsx +++ b/src/components/Visualizer.tsx @@ -12,7 +12,7 @@ import { StepContent, StepInfoNode, TacticComponent, -} from "../model/tactic/Tactic.ts" +} from "../model/tactic/TacticInfo.ts" import { getParent } from "../domains/StepsDomain.ts" import { computeRelativePositions, diff --git a/src/components/editor/BasketCourt.tsx b/src/components/editor/BasketCourt.tsx index b728ecb..37596c0 100644 --- a/src/components/editor/BasketCourt.tsx +++ b/src/components/editor/BasketCourt.tsx @@ -6,10 +6,12 @@ import { ComponentId, CourtType, TacticComponent, -} from "../../model/tactic/Tactic" +} from "../../model/tactic/TacticInfo.ts" import PlainCourt from "../../assets/court/full_court.svg?react" import HalfCourt from "../../assets/court/half_court.svg?react" +import "../../style/court.css" + export interface BasketCourtProps { components: TacticComponent[] parentComponents: TacticComponent[] | null @@ -28,16 +30,16 @@ export interface ActionPreview extends Action { } export function BasketCourt({ - components, - parentComponents, - previewAction, + components, + parentComponents, + previewAction, - renderComponent, - renderActions, + renderComponent, + renderActions, - courtImage, - courtRef, -}: BasketCourtProps) { + courtImage, + courtRef, + }: BasketCourtProps) { const [court, setCourt] = useState(courtRef.current) //force update once the court reference is set @@ -73,10 +75,9 @@ export function BasketCourt({ export function Court({ courtType }: { courtType: CourtType }) { const CourtSvg = courtType === "PLAIN" ? PlainCourt : HalfCourt - const courtSpecificClassName = courtType === "PLAIN" ? "plain-court" : "half-court" + const courtSpecificClassName = + courtType === "PLAIN" ? "plain-court" : "half-court" return ( -
- -
+ ) } diff --git a/src/components/editor/CourtAction.tsx b/src/components/editor/CourtAction.tsx index 54a6be2..0c20c26 100644 --- a/src/components/editor/CourtAction.tsx +++ b/src/components/editor/CourtAction.tsx @@ -2,7 +2,7 @@ import { Action, ActionKind } from "../../model/tactic/Action" import BendableArrow from "../arrows/BendableArrow" import { RefObject } from "react" import { MoveToHead, ScreenHead } from "../actions/ArrowAction" -import { ComponentId } from "../../model/tactic/Tactic" +import { ComponentId } from "../../model/tactic/TacticInfo.ts" export interface CourtActionProps { origin: ComponentId diff --git a/src/components/editor/StepsTree.tsx b/src/components/editor/StepsTree.tsx index 0029a5c..1729e59 100644 --- a/src/components/editor/StepsTree.tsx +++ b/src/components/editor/StepsTree.tsx @@ -1,5 +1,5 @@ import "../../style/steps_tree.css" -import { StepInfoNode } from "../../model/tactic/Tactic" +import { StepInfoNode } from "../../model/tactic/TacticInfo.ts" import BendableArrow from "../arrows/BendableArrow" import { ReactNode, useMemo, useRef } from "react" import AddSvg from "../../assets/icon/add.svg?react" diff --git a/src/domains/ActionsDomains.ts b/src/domains/ActionsDomains.ts index 2fa3229..02364a4 100644 --- a/src/domains/ActionsDomains.ts +++ b/src/domains/ActionsDomains.ts @@ -9,7 +9,7 @@ import { ComponentId, StepContent, TacticComponent, -} from "../model/tactic/Tactic.ts" +} from "../model/tactic/TacticInfo.ts" import { overlaps } from "../geo/Box.ts" import { Action, ActionKind, moves } from "../model/tactic/Action.ts" import { removeBall, updateComponent } from "./TacticContentDomains.ts" diff --git a/src/domains/PlayerDomains.ts b/src/domains/PlayerDomains.ts index 8c13c64..0dc565e 100644 --- a/src/domains/PlayerDomains.ts +++ b/src/domains/PlayerDomains.ts @@ -9,7 +9,7 @@ import { ComponentId, StepContent, TacticComponent, -} from "../model/tactic/Tactic.ts" +} from "../model/tactic/TacticInfo.ts" import { removeComponent, updateComponent } from "./TacticContentDomains.ts" import { diff --git a/src/domains/StepsDomain.ts b/src/domains/StepsDomain.ts index a120c23..25b21d8 100644 --- a/src/domains/StepsDomain.ts +++ b/src/domains/StepsDomain.ts @@ -1,4 +1,4 @@ -import { StepInfoNode } from "../model/tactic/Tactic.ts" +import { StepInfoNode } from "../model/tactic/TacticInfo.ts" export function addStepNode( root: StepInfoNode, @@ -110,3 +110,7 @@ export function getParent( } return null } + +export function countSteps(tree: StepInfoNode): number { + return 1 + tree.children.reduce((tot, node) => tot + countSteps(node), 0) +} diff --git a/src/domains/TacticContentDomains.ts b/src/domains/TacticContentDomains.ts index b2aebb1..af8d2dd 100644 --- a/src/domains/TacticContentDomains.ts +++ b/src/domains/TacticContentDomains.ts @@ -18,7 +18,7 @@ import { ComponentId, StepContent, TacticComponent, -} from "../model/tactic/Tactic.ts" +} from "../model/tactic/TacticInfo.ts" import { overlaps } from "../geo/Box.ts" import { RackedCourtObject, RackedPlayer } from "../editor/RackedItems.ts" diff --git a/src/editor/ContentVersions.ts b/src/editor/ContentVersions.ts index 26cf6f1..3521eab 100644 --- a/src/editor/ContentVersions.ts +++ b/src/editor/ContentVersions.ts @@ -1,4 +1,4 @@ -import { StepContent } from "../model/tactic/Tactic.ts" +import { StepContent } from "../model/tactic/TacticInfo.ts" export class ContentVersions { private index = 0 diff --git a/src/model/tactic/Action.ts b/src/model/tactic/Action.ts index d4e459f..b26ecae 100644 --- a/src/model/tactic/Action.ts +++ b/src/model/tactic/Action.ts @@ -1,5 +1,5 @@ import { Pos } from "../../geo/Pos" -import { ComponentId } from "./Tactic" +import { ComponentId } from "./TacticInfo.ts" export enum ActionKind { SCREEN = "SCREEN", diff --git a/src/model/tactic/CourtObjects.ts b/src/model/tactic/CourtObjects.ts index 55eaf6f..6c46a64 100644 --- a/src/model/tactic/CourtObjects.ts +++ b/src/model/tactic/CourtObjects.ts @@ -1,4 +1,4 @@ -import { Component, Frozable } from "./Tactic" +import { Component, Frozable } from "./TacticInfo.ts" import { Pos } from "../../geo/Pos.ts" export const BALL_ID = "ball" diff --git a/src/model/tactic/Player.ts b/src/model/tactic/Player.ts index bd781ac..fe32ef3 100644 --- a/src/model/tactic/Player.ts +++ b/src/model/tactic/Player.ts @@ -1,4 +1,4 @@ -import { Component, ComponentId, Frozable } from "./Tactic" +import { Component, ComponentId, Frozable } from "./TacticInfo.ts" import { Pos } from "../../geo/Pos.ts" export type PlayerId = string diff --git a/src/model/tactic/Tactic.ts b/src/model/tactic/TacticInfo.ts similarity index 86% rename from src/model/tactic/Tactic.ts rename to src/model/tactic/TacticInfo.ts index ccf6d43..0138351 100644 --- a/src/model/tactic/Tactic.ts +++ b/src/model/tactic/TacticInfo.ts @@ -2,6 +2,12 @@ import { Player, PlayerPhantom } from "./Player" import { Action } from "./Action" import { CourtObject } from "./CourtObjects" +export interface Tactic { + readonly name: string + readonly courtType: CourtType + readonly root: TacticStep +} + export interface TacticInfo { readonly id: number readonly name: string @@ -12,6 +18,7 @@ export interface TacticInfo { export interface TacticStep { readonly stepId: number readonly content: StepContent + readonly children: TacticStep[] } export interface StepContent { diff --git a/src/pages/Editor.tsx b/src/pages/Editor.tsx index 0f80915..aaad380 100644 --- a/src/pages/Editor.tsx +++ b/src/pages/Editor.tsx @@ -22,7 +22,7 @@ import { StepContent, StepInfoNode, TacticComponent, -} from "../model/tactic/Tactic" +} from "../model/tactic/TacticInfo.ts" import SavingState, { SaveState, @@ -97,12 +97,14 @@ import SplitLayout from "../components/SplitLayout.tsx" import { MutableTacticService, ServiceError, + TacticService, } from "../service/MutableTacticService.ts" import { LocalStorageTacticService } from "../service/LocalStorageTacticService.ts" import { APITacticService } from "../service/APITacticService.ts" import { useNavigate, useParams } from "react-router-dom" import { ContentVersions } from "../editor/ContentVersions.ts" import { useAppFetcher } from "../App.tsx" +import ExportTacticPopup from "./popup/ExportTacticPopup.tsx" const ERROR_STYLE: CSSProperties = { borderColor: "red", @@ -132,6 +134,8 @@ interface EditorService { setName(name: string): Promise openVisualizer(): Promise + + getTacticService(): TacticService } export default function Editor({ guestMode }: EditorProps) { @@ -356,6 +360,10 @@ function EditorPageWrapper({ async openVisualizer(): Promise { openVisualizer() }, + + getTacticService(): TacticService { + return service + }, } }, [stepsTree, service, stepsVersions, setStepContent, openVisualizer]) @@ -423,10 +431,7 @@ function EditorPage({ const [isStepsTreeVisible, setStepsTreeVisible] = useState(true) - const courtBounds = useCallback( - () => courtRef.current!.getBoundingClientRect(), - [courtRef], - ) + const [showExportPopup, setShowExportPopup] = useState(false) const [editorContentCurtainWidth, setEditorContentCurtainWidth] = useState(80) @@ -438,6 +443,11 @@ function EditorPage({ : new Map() }, [content, courtRef]) + const courtBounds = useCallback( + () => courtRef.current!.getBoundingClientRect(), + [courtRef], + ) + const setComponents = (action: SetStateAction) => { service.setContent((c) => ({ ...c, @@ -809,6 +819,15 @@ function EditorPage({ return (
+ {showExportPopup && ( +
+ setShowExportPopup(false)} + /> +
+ )}
@@ -834,10 +853,15 @@ function EditorPage({ VISUALISER +
diff --git a/src/pages/HomePage.tsx b/src/pages/HomePage.tsx index e99a32e..413e88b 100644 --- a/src/pages/HomePage.tsx +++ b/src/pages/HomePage.tsx @@ -1,9 +1,18 @@ import "../style/home/home.css" import { useNavigate } from "react-router-dom" -import { createContext, Dispatch, useContext, useEffect, useReducer } from "react" +import { + createContext, + Dispatch, + useContext, + useEffect, useMemo, + useReducer, +} from "react" import { useAppFetcher } from "../App.tsx" import { Visualizer } from "../components/Visualizer.tsx" import BinSvg from "../assets/icon/bin.svg?react" +import ExportSvg from "../assets/icon/export.svg?react" +import ExportTacticPopup from "./popup/ExportTacticPopup.tsx" +import { APITacticService } from "../service/APITacticService.ts" interface Tactic { id: number @@ -22,26 +31,40 @@ interface Team { enum HomePageStateActionKind { UPDATE_TACTICS = "UPDATE_TACTICS", UPDATE_TEAMS = "UPDATE_TEAMS", + SET_EXPORTING_TACTIC = "SET_EXPORTING_TACTIC", INIT = "INIT" } -type HomePageStateAction = { - type: HomePageStateActionKind.UPDATE_TACTICS, +type HomePageStateAction = + | { + type: HomePageStateActionKind.UPDATE_TACTICS tactics: Tactic[] -} | { - type: HomePageStateActionKind.UPDATE_TEAMS, +} + | { + type: HomePageStateActionKind.UPDATE_TEAMS teams: Team[] -} | { - type: HomePageStateActionKind.INIT, +} + | { + type: HomePageStateActionKind.INIT state: HomePageState +} | { + type: HomePageStateActionKind.SET_EXPORTING_TACTIC, + tacticId: number | undefined } interface HomePageState { tactics: Tactic[] teams: Team[] + /** + * The home page displays a popup to export a certains tactic + */ + exportingTacticId?: number } -function homePageStateReducer(state: HomePageState, action: HomePageStateAction): HomePageState { +function homePageStateReducer( + state: HomePageState, + action: HomePageStateAction, +): HomePageState { switch (action.type) { case HomePageStateActionKind.UPDATE_TACTICS: return { ...state!, tactics: action.tactics } @@ -49,13 +72,16 @@ function homePageStateReducer(state: HomePageState, action: HomePageStateAction) case HomePageStateActionKind.UPDATE_TEAMS: return { ...state!, teams: action.teams } + case HomePageStateActionKind.SET_EXPORTING_TACTIC: + return { ...state!, exportingTacticId: action.tacticId } + case HomePageStateActionKind.INIT: return action.state } } interface HomeStateContextMutable { - state: HomePageState, + state: HomePageState dispatch: Dispatch } @@ -66,8 +92,10 @@ function useHomeState() { } export default function HomePage() { - const [state, dispatch] = useReducer(homePageStateReducer, { tactics: [], teams: [] }) - + const [state, dispatch] = useReducer(homePageStateReducer, { + tactics: [], + teams: [], + }) const navigate = useNavigate() const fetcher = useAppFetcher() @@ -80,18 +108,32 @@ export default function HomePage() { navigate("/login") return // if unauthorized } - type UserDataResponse = { teams: Team[], tactics: Tactic[] } + type UserDataResponse = { teams: Team[]; tactics: Tactic[] } const { teams, tactics }: UserDataResponse = await response.json() tactics.sort((a, b) => b.creationDate - a.creationDate) - dispatch({ type: HomePageStateActionKind.INIT, state: { teams, tactics } }) + dispatch({ + type: HomePageStateActionKind.INIT, + state: { teams, tactics }, + }) } initUserData() }, [fetcher, navigate]) + const tacticExportService = useMemo(() => + state.exportingTacticId ? new APITacticService(fetcher, state.exportingTacticId!) : null + , [fetcher, state.exportingTacticId], + ) return ( + {tacticExportService &&
+ dispatch({ type: HomePageStateActionKind.SET_EXPORTING_TACTIC, tacticId: undefined })} + /> +
}
) @@ -111,9 +153,7 @@ function Body() { return (
- +
) } @@ -133,9 +173,7 @@ function SideMenu({ width }: { width: number }) { ) } -function PersonalSpace({ - width, - }: { width: number }) { +function PersonalSpace({ width }: { width: number }) { return (
+
navigate(`/tactic/${tactic.id}/edit`)} + >
navigate(`/tactic/${tactic.id}/edit`)}> + className={"tactic-card-preview"}>

{tactic.name}

- { - const response = await fetcher.fetchAPI(`tactics/${tactic.id}`, {}, "DELETE") - if (!response.ok) { - throw Error(`Cannot delete tactic ${tactic.id}!`) - } - dispatch({ - type: HomePageStateActionKind.UPDATE_TACTICS, - tactics: tactics.filter(t => t.id !== tactic.id), - }) - }} - /> +
+ { + e.stopPropagation() + dispatch({ + type: HomePageStateActionKind.SET_EXPORTING_TACTIC, + tacticId: tactic.id, + }) + }} + /> + { + e.stopPropagation() + const response = await fetcher.fetchAPI( + `tactics/${tactic.id}`, + {}, + "DELETE", + ) + if (!response.ok) { + throw Error(`Cannot delete tactic ${tactic.id}!`) + } + dispatch({ + type: HomePageStateActionKind.UPDATE_TACTICS, + tactics: tactics.filter((t) => t.id !== tactic.id), + }) + }} + /> + +
+
) @@ -258,9 +320,7 @@ function SetButtonTactic() { function SetButtonTeam() { const teams = useHomeState()!.state.teams - const listTeam = teams.map((team) => ( - - )) + const listTeam = teams.map((team) => ) return
{listTeam}
} diff --git a/src/pages/NewTacticPage.tsx b/src/pages/NewTacticPage.tsx index 68e4be1..8d83f43 100644 --- a/src/pages/NewTacticPage.tsx +++ b/src/pages/NewTacticPage.tsx @@ -3,7 +3,7 @@ import "../style/new_tactic_panel.css" import plainCourt from "../assets/court/full_court.svg" import halfCourt from "../assets/court/half_court.svg" -import { CourtType } from "../model/tactic/Tactic.ts" +import { CourtType } from "../model/tactic/TacticInfo.ts" import { useCallback } from "react" import { useAppFetcher, useUser } from "../App.tsx" import { useNavigate } from "react-router-dom" diff --git a/src/pages/VisualizerPage.tsx b/src/pages/VisualizerPage.tsx index 9a66766..f27e7ad 100644 --- a/src/pages/VisualizerPage.tsx +++ b/src/pages/VisualizerPage.tsx @@ -1,15 +1,15 @@ import { ServiceError, TacticService } from "../service/MutableTacticService.ts" import { useNavigate, useParams } from "react-router-dom" -import { useCallback, useEffect, useMemo, useState } from "react" import { useVisualizer, VisualizerState, VisualizerStateActionKind, } from "../visualizer/VisualizerState.ts" +import { useCallback, useEffect, useMemo, useState } from "react" import { getParent } from "../domains/StepsDomain.ts" import { mapToParentContent } from "../domains/TacticContentDomains.ts" import StepsTree from "../components/editor/StepsTree.tsx" -import { StepInfoNode } from "../model/tactic/Tactic.ts" +import { StepInfoNode } from "../model/tactic/TacticInfo.ts" import SplitLayout from "../components/SplitLayout.tsx" import { LocalStorageTacticService } from "../service/LocalStorageTacticService.ts" import { APITacticService } from "../service/APITacticService.ts" @@ -17,12 +17,13 @@ import { APITacticService } from "../service/APITacticService.ts" import "../style/visualizer.css" import { VisualizerFrame } from "../components/Visualizer.tsx" import { useAppFetcher } from "../App.tsx" +import ExportTacticPopup from "./popup/ExportTacticPopup.tsx" export interface VisualizerPageProps { guestMode: boolean } -export function VisualizerPage({ guestMode }: VisualizerPageProps) { +export default function VisualizerPage({ guestMode }: VisualizerPageProps) { const { tacticId: idStr } = useParams() const navigate = useNavigate() @@ -48,6 +49,8 @@ interface VisualizerService { selectStep(step: number): Promise openEditor(): Promise + + getTacticService(): TacticService } interface ServedVisualizerPageProps { @@ -101,7 +104,7 @@ function ServedVisualizerPage({ } if (state === null) init() - }, [service, state]) + }, [dispatch, service, state]) const visualizerService: VisualizerService = useMemo( () => ({ @@ -125,8 +128,12 @@ function ServedVisualizerPage({ async openEditor() { openEditor() }, + + getTacticService(): TacticService { + return service + }, }), - [openEditor, service, state], + [dispatch, openEditor, service, state], ) if (panicMessage) { @@ -162,6 +169,8 @@ function VisualizerPageContent({ const [editorContentCurtainWidth, setEditorContentCurtainWidth] = useState(80) + const [showExportPopup, setShowExportPopup] = useState(false) + const stepsTreeNode = (
+ {showExportPopup && ( +
+ setShowExportPopup(false)} + /> +
+ )}

{tacticName}

+