diff --git a/.gitignore b/.gitignore index 780376a..b97bbea 100644 --- a/.gitignore +++ b/.gitignore @@ -22,4 +22,7 @@ dist-ssr *.sln *.sw? -package-lock.json \ No newline at end of file +package-lock.json + + +stats.html \ No newline at end of file diff --git a/package.json b/package.json index d89f82b..3220415 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "eslint-plugin-react-refresh": "^0.4.5", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-drag-drop-files": "^2.3.10", "react-draggable": "^4.4.6", "react-router-dom": "^6.22.0", "typescript": "^5.2.2", @@ -34,6 +35,7 @@ "eslint-plugin-react-hooks": "^4.6.0", "jsdom": "^24.0.0", "prettier": "^3.1.0", + "rollup-plugin-visualizer": "^5.12.0", "typescript": "^5.2.2", "vite-plugin-svgr": "^4.1.0", "vitest": "^1.3.1" diff --git a/src/App.tsx b/src/App.tsx index f1ab122..350928f 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,7 +148,6 @@ export default function App() { element={suspense( - , , )} /> diff --git a/src/assets/court/full_court.svg b/src/assets/court/full_court.svg index 5bfc0de..07fe19f 100644 --- a/src/assets/court/full_court.svg +++ b/src/assets/court/full_court.svg @@ -1,77 +1,77 @@ - - - - - - - - + + + + + + + + - - - - - - - - - - - + + + + + + + + + + + - - + + - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + - - + + - - - - - - - - - - - - - + + + + + + + + + + + + + - - - - - + + + + + - + @@ -79,9 +79,9 @@ - + - + @@ -89,9 +89,9 @@ - + - + @@ -99,9 +99,9 @@ - + - + @@ -109,9 +109,9 @@ - + - + @@ -119,9 +119,9 @@ - + - + @@ -129,7 +129,7 @@ - + diff --git a/src/assets/court/half_court.svg b/src/assets/court/half_court.svg index f621f93..e42536f 100644 --- a/src/assets/court/half_court.svg +++ b/src/assets/court/half_court.svg @@ -1,45 +1,45 @@ - - - - - - - + + + + + + + - - - - - - - - - - - + + + + + + + + + + + - - + + - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + - + @@ -47,9 +47,9 @@ - + - + @@ -57,9 +57,9 @@ - + - + @@ -67,7 +67,6 @@ - diff --git a/src/assets/icon/account.png b/src/assets/icon/account.png deleted file mode 100644 index 6ed3299..0000000 Binary files a/src/assets/icon/account.png and /dev/null differ diff --git a/src/assets/icon/bin.svg b/src/assets/icon/bin.svg new file mode 100644 index 0000000..1ef1c60 --- /dev/null +++ b/src/assets/icon/bin.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icon/duplicate.svg b/src/assets/icon/duplicate.svg new file mode 100644 index 0000000..515fa43 --- /dev/null +++ b/src/assets/icon/duplicate.svg @@ -0,0 +1 @@ + \ No newline at end of file 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 69a941a..2fefe0c 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, @@ -26,20 +26,26 @@ import { CourtAction } from "./editor/CourtAction.tsx" import { BasketCourt, Court } from "./editor/BasketCourt.tsx" import { TacticService } from "../service/MutableTacticService.ts" import { useAppFetcher } from "../App.tsx" +import { mapIdentifiers } from "../domains/TacticContentDomains.ts" export interface VisualizerProps { tacticId: number stepId?: number + visualizerId: string | number } -export function Visualizer({ tacticId, stepId }: VisualizerProps) { +export function Visualizer({ + visualizerId, + tacticId, + stepId, +}: VisualizerProps) { const [panicMessage, setPanicMessage] = useState(null) const [courtType, setCourtType] = useState() const [stepsTree, setStepsTree] = useState() const fetcher = useAppFetcher() const service = useMemo( () => new APITacticService(fetcher, tacticId), - [tacticId], + [fetcher, tacticId], ) const isNotInit = !stepsTree || !courtType @@ -68,6 +74,7 @@ export function Visualizer({ tacticId, stepId }: VisualizerProps) { return ( (null) const [content, setContent] = useState(null) - const [parentContent, setParentContent] = useState() + const [parentContent, setParentContent] = useState(null) - const isNotInit = !content || !parentContent + const isNotInit = !content useEffect(() => { async function init() { @@ -116,12 +125,21 @@ export function StepVisualizer({ parentContent = parentResult } - setContent(contentResult) - setParentContent(parentContent) + setContent( + mapIdentifiers(contentResult, (id) => `${id}-${visualizerId}`), + ) + if (parentContent) { + setParentContent( + mapIdentifiers( + parentContent, + (id) => `${id}-${visualizerId}-parent`, + ), + ) + } } if (isNotInit) init() - }, [isNotInit, service, stepId, stepsTree]) + }, [isNotInit, visualizerId, service, stepId, stepsTree]) if (panicMessage) { return

{panicMessage}

diff --git a/src/components/arrows/BendableArrow.tsx b/src/components/arrows/BendableArrow.tsx index b730bbb..9443a9d 100644 --- a/src/components/arrows/BendableArrow.tsx +++ b/src/components/arrows/BendableArrow.tsx @@ -544,14 +544,22 @@ export default function BendableArrow({
{style?.head?.call(style)}
{style?.tail?.call(style)}
diff --git a/src/components/editor/BasketCourt.tsx b/src/components/editor/BasketCourt.tsx index 903767f..186d2b2 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 @@ -65,9 +67,6 @@ export function BasketCourt({ origin={previewAction.origin} color={previewAction.isInvalid ? "red" : "black"} isEditable={true} - //do nothing on interacted, not really possible as it's a preview arrow - onActionDeleted={() => {}} - onActionChanges={() => {}} /> )} @@ -76,9 +75,7 @@ export function BasketCourt({ export function Court({ courtType }: { courtType: CourtType }) { const CourtSvg = courtType === "PLAIN" ? PlainCourt : HalfCourt - return ( -
- -
- ) + 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..e8208f7 100644 --- a/src/domains/StepsDomain.ts +++ b/src/domains/StepsDomain.ts @@ -1,11 +1,11 @@ -import { StepInfoNode } from "../model/tactic/Tactic.ts" +import { StepInfoNode } from "../model/tactic/TacticInfo.ts" export function addStepNode( root: StepInfoNode, - parent: StepInfoNode, + parentId: number, child: StepInfoNode, ): StepInfoNode { - if (root.id === parent.id) { + if (root.id === parentId) { return { ...root, children: root.children.concat(child), @@ -14,7 +14,7 @@ export function addStepNode( return { ...root, - children: root.children.map((c) => addStepNode(c, parent, child)), + children: root.children.map((c) => addStepNode(c, parentId, child)), } } @@ -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 bd256c7..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" @@ -499,13 +499,20 @@ export function drainTerminalStateOnChildContent( } export function mapToParentContent(content: StepContent): StepContent { + return mapIdentifiers(content, (id) => id + "-parent") +} + +export function mapIdentifiers( + content: StepContent, + f: (id: string) => string, +): StepContent { function mapToParentActions(actions: Action[]): Action[] { return actions.map((a) => ({ ...a, - target: a.target + "-parent", + target: typeof a.target === "string" ? f(a.target) : a.target, segments: a.segments.map((s) => ({ ...s, - next: typeof s.next === "string" ? s.next + "-parent" : s.next, + next: typeof s.next === "string" ? f(s.next) : s.next, })), })) } @@ -517,10 +524,10 @@ export function mapToParentContent(content: StepContent): StepContent { if (p.type == "player") { return { ...p, - id: p.id + "-parent", + id: f(p.id), actions: mapToParentActions(p.actions), path: p.path && { - items: p.path.items.map((p) => p + "-parent"), + items: p.path.items.map(f), }, } } @@ -528,10 +535,10 @@ export function mapToParentContent(content: StepContent): StepContent { ...p, pos: p.pos.type == "follows" - ? { ...p.pos, attach: p.pos.attach + "-parent" } + ? { ...p.pos, attach: f(p.pos.attach) } : p.pos, - id: p.id + "-parent", - originPlayerId: p.originPlayerId + "-parent", + id: f(p.id), + originPlayerId: f(p.originPlayerId), actions: mapToParentActions(p.actions), } }), diff --git a/src/domains/TacticPersistenceDomain.ts b/src/domains/TacticPersistenceDomain.ts new file mode 100644 index 0000000..212b6cf --- /dev/null +++ b/src/domains/TacticPersistenceDomain.ts @@ -0,0 +1,109 @@ +import { Fetcher } from "../app/Fetcher.ts" +import { + StepInfoNode, + Tactic, + TacticInfo, + TacticStep, +} from "../model/tactic/TacticInfo.ts" +import { APITacticService } from "../service/APITacticService.ts" +import { + TacticContext, + TacticService, +} from "../service/MutableTacticService.ts" +import { countSteps } from "./StepsDomain.ts" + +export function importTacticFromFile( + fetcher: Fetcher, + blob: Blob, + onSuccess: (tactic: TacticInfo) => void, + onError: (e: Error | unknown) => void = console.error, +) { + const reader = new FileReader() + reader.onloadend = async (e) => { + const jsonString = e.target!.result as string + + let tactic + try { + tactic = await importTactic(fetcher, JSON.parse(jsonString)) + } catch (e) { + onError(e) + return + } + onSuccess(tactic) + } + reader.readAsText(blob) +} + +export async function importTactic( + fetcher: Fetcher, + tactic: Tactic, +): Promise { + const response = await fetcher.fetchAPI( + "tactics", + { + name: tactic.name, + courtType: tactic.courtType, + }, + "POST", + ) + + if (!response.ok) throw Error("Received unsuccessful response from API.") + + const { id, rootStepId } = await response.json() + + const service = new APITacticService(fetcher, id) + + await service.saveContent(rootStepId, tactic.root.content) + + async function importStepChildren(parent: TacticStep, parentId: number) { + return await Promise.all( + parent.children.map(async (child) => { + const result = await service.addStep(parentId, child.content) + if (typeof result === "string") throw Error(result) + await importStepChildren(child, result.id) + return result + }), + ) + } + + const rootStepNode: StepInfoNode = { + id: rootStepId, + children: await importStepChildren(tactic.root, rootStepId), + } + + return { courtType: tactic.courtType, name: tactic.name, id, rootStepNode } +} + +export async function loadPlainTactic( + context: TacticContext, + service: TacticService, + onProgress: (p: number) => void = () => {}, +): Promise { + const tree = context.stepsTree + + const treeSize = countSteps(tree) + const totalStepsCompleted = new Uint16Array(1) + + async function transformToStep( + stepInfoNode: StepInfoNode, + ): Promise { + const contentResult = await service.getContent(stepInfoNode.id) + if (typeof contentResult === "string") throw Error(contentResult) + + Atomics.add(totalStepsCompleted, 0, 1) + onProgress((Atomics.load(totalStepsCompleted, 0) / treeSize) * 100) + + return { + content: contentResult, + children: await Promise.all( + stepInfoNode.children.map(transformToStep), + ), + } + } + + return { + name: context.name, + courtType: context.courtType, + root: await transformToStep(tree), + } +} 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..1ec01c8 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 @@ -10,8 +16,8 @@ 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 7a9a929..8927e6b 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) { @@ -158,6 +162,7 @@ export default function Editor({ guestMode }: EditorProps) { interface EditorPageWrapperProps { service: MutableTacticService + openVisualizer(): void } @@ -305,11 +310,11 @@ function EditorPageWrapper({ parent: StepInfoNode, content: StepContent, ): Promise { - const result = await service.addStep(parent, content) + const result = await service.addStep(parent.id, content) if (typeof result !== "string") { internalStepsTree = addStepNode( internalStepsTree!, - parent, + parent.id, result, ) setStepsTree(internalStepsTree) @@ -355,6 +360,10 @@ function EditorPageWrapper({ async openVisualizer(): Promise { openVisualizer() }, + + getTacticService(): TacticService { + return service + }, } }, [stepsTree, service, stepsVersions, setStepContent, openVisualizer]) @@ -422,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) @@ -437,6 +443,11 @@ function EditorPage({ : new Map() }, [content, courtRef]) + const courtBounds = useCallback( + () => courtRef.current!.getBoundingClientRect(), + [courtRef], + ) + const setComponents = (action: SetStateAction) => { service.setContent((c) => ({ ...c, @@ -759,17 +770,15 @@ function EditorPage({ />
-
- } - courtRef={courtRef} - previewAction={previewAction} - renderComponent={renderComponent} - renderActions={renderActions} - /> -
+ } + courtRef={courtRef} + previewAction={previewAction} + renderComponent={renderComponent} + renderActions={renderActions} + />
) @@ -810,6 +819,14 @@ function EditorPage({ return (
+ {showExportPopup && ( +
+ setShowExportPopup(false)} + /> +
+ )}
@@ -835,10 +852,15 @@ function EditorPage({ VISUALISER +
diff --git a/src/pages/HomePage.tsx b/src/pages/HomePage.tsx index 1a0d656..57bacb5 100644 --- a/src/pages/HomePage.tsx +++ b/src/pages/HomePage.tsx @@ -1,8 +1,15 @@ import "../style/home/home.css" import { useNavigate } from "react-router-dom" -import { useEffect, useState } from "react" -import { User } from "../model/User.ts" +import { createContext, Dispatch, useCallback, 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 DuplicateSvg from "../assets/icon/duplicate.svg?react" +import ExportTacticPopup from "./popup/ExportTacticPopup.tsx" +import { APITacticService } from "../service/APITacticService.ts" +import { FileUploader } from "react-drag-drop-files" +import { importTactic, importTacticFromFile, loadPlainTactic } from "../domains/TacticPersistenceDomain.ts" interface Tactic { id: number @@ -18,9 +25,87 @@ interface Team { second_color: string } +enum HomePageStateActionKind { + UPDATE_TACTICS = "UPDATE_TACTICS", + ADD_TACTIC = "ADD_TACTIC", + REMOVE_TACTIC = "REMOVE_TACTIC", + UPDATE_TEAMS = "UPDATE_TEAMS", + SET_EXPORTING_TACTIC = "SET_EXPORTING_TACTIC", + INIT = "INIT", +} + +type HomePageStateAction = + | { + type: HomePageStateActionKind.UPDATE_TACTICS + tactics: Tactic[] +} + | { + type: HomePageStateActionKind.UPDATE_TEAMS + teams: Team[] +} + | { + type: HomePageStateActionKind.INIT + state: HomePageState +} + | { + type: HomePageStateActionKind.SET_EXPORTING_TACTIC + tacticId: number | undefined +} + | { + type: HomePageStateActionKind.REMOVE_TACTIC, + tacticId: number +} | { + type: HomePageStateActionKind.ADD_TACTIC, + tactic: Tactic +} + +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 { + switch (action.type) { + case HomePageStateActionKind.UPDATE_TACTICS: + return { ...state, tactics: action.tactics } + + case HomePageStateActionKind.UPDATE_TEAMS: + return { ...state, teams: action.teams } + + case HomePageStateActionKind.SET_EXPORTING_TACTIC: + return { ...state, exportingTacticId: action.tacticId } + + case HomePageStateActionKind.ADD_TACTIC: + return { ...state, tactics: [action.tactic, ...state.tactics] } + + case HomePageStateActionKind.REMOVE_TACTIC: + return { ...state, tactics: state.tactics.filter(t => t.id !== action.tacticId) } + + case HomePageStateActionKind.INIT: + return action.state + } +} + +interface HomeStateContextMutable { + state: HomePageState + dispatch: Dispatch +} + +const HomeStateContext = createContext(null) + +function useHomeState() { + return useContext(HomeStateContext) +} + export default function HomePage() { - type UserDataResponse = { user?: User; tactics: Tactic[]; teams: Team[] } - const [{ tactics, teams }, setInfo] = useState({ + const [state, dispatch] = useReducer(homePageStateReducer, { tactics: [], teams: [], }) @@ -31,76 +116,71 @@ export default function HomePage() { useEffect(() => { async function initUserData() { const response = await fetcher.fetchAPIGet("user-data") + if (response.status == 401) { navigate("/login") return // if unauthorized } - setInfo(await response.json()) + 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 }, + }) } initUserData() }, [fetcher, navigate]) - tactics!.sort((a, b) => b.creationDate - a.creationDate) + const tacticExportService = useMemo( + () => + state.exportingTacticId + ? new APITacticService(fetcher, state.exportingTacticId!) + : null, + [fetcher, state.exportingTacticId], + ) - const lastTactics = tactics.slice(0, 5) return ( - + + {tacticExportService && ( +
+ + dispatch({ + type: HomePageStateActionKind.SET_EXPORTING_TACTIC, + tacticId: undefined, + }) + } + /> +
+ )} + +
) } -function Home({ - lastTactics, - allTactics, - teams, -}: { - lastTactics: Tactic[] - allTactics: Tactic[] - teams: Team[] -}) { +function Home() { return (
- +
) } -function Body({ - lastTactics, - allTactics, - teams, -}: { - lastTactics: Tactic[] - allTactics: Tactic[] - teams: Team[] -}) { +function Body() { const widthPersonalSpace = 78 const widthSideMenu = 100 - widthPersonalSpace return (
- - + +
) } -function SideMenu({ - width, - lastTactics, - teams, -}: { - width: number - lastTactics: Tactic[] - teams: Team[] -}) { +function SideMenu({ width }: { width: number }) { return (
- - + + +
) } -function PersonalSpace({ - width, - allTactics, -}: { - width: number - allTactics: Tactic[] -}) { +function TacticImportArea() { + const fetcher = useAppFetcher() + const { dispatch } = useHomeState()! + + const handleDrop = useCallback( + async (file: File) => { + importTacticFromFile(fetcher, file, (tactic) => { + dispatch({ + type: HomePageStateActionKind.ADD_TACTIC, + tactic: { + name: tactic.name, + id: tactic.id, + creationDate: new Date().getDate(), + }, + }) + }) + }, + [dispatch, fetcher], + ) + + return ( +
+ +
+ ) +} + +function PersonalSpace({ width }: { width: number }) { return (
- +
) } @@ -142,69 +248,112 @@ function TitlePersonalSpace() { ) } -function TableData({ allTactics }: { allTactics: Tactic[] }) { - const nbRow = Math.floor(allTactics.length / 3) + 1 - const listTactic = Array(nbRow) - for (let i = 0; i < nbRow; i++) { - listTactic[i] = Array(0) - } - let i = 0 - let j = 0 - allTactics.forEach((tactic) => { - listTactic[i].push(tactic) - j++ - if (j === 3) { - i++ - j = 0 - } - }) +function TacticGrid({ tactics }: { tactics: Tactic[] }) { + return ( +
+ {tactics.map((team) => ( + + ))} +
+ ) +} +function TacticCard({ tactic }: { tactic: Tactic }) { const navigate = useNavigate() + const fetcher = useAppFetcher() + const { + dispatch, + } = useHomeState()! + return ( +
navigate(`/tactic/${tactic.id}/edit`)}> +
+ +
+
+

{tactic.name}

+
+ { + 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.REMOVE_TACTIC, + tacticId: tactic.id, + }) + }} + /> + { + e.stopPropagation() + const service = new APITacticService( + fetcher, + tactic.id, + ) + const context = await service.getContext() + if (typeof context === "string") + throw Error(context) - i = 0 - while (i < nbRow) { - listTactic[i] = listTactic[i].map((tactic: Tactic) => ( - { - navigate("/tactic/" + tactic.id + "/edit") - }}> - {truncateString(tactic.name, 25)} - - )) - i++ - } - if (nbRow == 1) { - if (listTactic[0].length < 3) { - for (let i = 0; i <= 3 - listTactic[0].length; i++) { - listTactic[0].push() - } - } - } + const plainTactic = await loadPlainTactic( + context, + service, + ) + const { name, id } = await importTactic( + fetcher, + plainTactic, + ) - return listTactic.map((tactic, rowIndex) => ( - {tactic} - )) + dispatch({ + type: HomePageStateActionKind.ADD_TACTIC, + tactic: { name, id, creationDate: 0 }, + }) + }} + /> +
+
+
+ ) } -function BodyPersonalSpace({ allTactics }: { allTactics: Tactic[] }) { +function BodyPersonalSpace() { + const tactics = useHomeState()!.state.tactics return (
- {allTactics.length == 0 ? ( + {tactics.length == 0 ? (

Aucune tactique créée !

) : ( - - - - -
+ )}
) } -function Team({ teams }: { teams: Team[] }) { +function LastTeamsSideMenu() { const navigate = useNavigate() return (
@@ -214,12 +363,12 @@ function Team({ teams }: { teams: Team[] }) { +
- +
) } -function Tactic({ lastTactics }: { lastTactics: Tactic[] }) { +function LastTacticsSideMenu() { const navigate = useNavigate() return ( @@ -233,26 +382,27 @@ function Tactic({ lastTactics }: { lastTactics: Tactic[] }) { +
- + ) } -function SetButtonTactic({ tactics }: { tactics: Tactic[] }) { +function SetButtonTactic() { + const tactics = useHomeState()!.state.tactics.slice(0, 5) const lastTactics = tactics.map((tactic) => ( - + )) return
{lastTactics}
} -function SetButtonTeam({ teams }: { teams: Team[] }) { - const listTeam = teams.map((team) => ( - - )) +function SetButtonTeam() { + const teams = useHomeState()!.state.teams + + const listTeam = teams.map((team) => ) return
{listTeam}
} -function ButtonTeam({ team }: { team: Team }) { +function TeamCard({ team }: { team: Team }) { const name = truncateString(team.name, 20) const navigate = useNavigate() @@ -270,7 +420,7 @@ function ButtonTeam({ team }: { team: Team }) { ) } -function ButtonLastTactic({ tactic }: { tactic: Tactic }) { +function LastTacticCard({ tactic }: { tactic: Tactic }) { const name = truncateString(tactic.name, 20) const navigate = useNavigate() 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 489dcab..37c0487 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 = (
- +
+ +
) return (
+ {showExportPopup && ( +
+ setShowExportPopup(false)} + /> +
+ )}

{tacticName}

+
-
+
{isStepsTreeVisible ? ( void +} + +export default function ExportTacticPopup({ + service, + onHide, +}: ExportTacticPopupProps) { + const [context, setContext] = useState() + const [panicMessage, setPanicMessage] = useState() + + const [exportPercentage, setExportPercentage] = useState(0) + + useEffect(() => { + async function init() { + const result = await service.getContext() + if (typeof result === "string") { + setPanicMessage("Could not retrieve tactic context") + return + } + setContext(result) + } + + if (!context) init() + }, [context, service]) + + useEffect(() => { + function onKeyUp(e: KeyboardEvent) { + if (e.key === "Escape") onHide() + } + + window.addEventListener("keyup", onKeyUp) + return () => window.removeEventListener("keyup", onKeyUp) + }, [onHide]) + + if (panicMessage) return

{panicMessage}

+ + return ( +
+
e.stopPropagation()}> +
+

Exporting {context?.name ?? "Tactic"}

+
+ +
+
{ + await exportInJson( + context!, + service, + setExportPercentage, + ) + setExportPercentage(0) + }}> +

Exporter en JSON

+ +
+
+
+
+ ) +} + +async function exportInJson( + context: TacticContext, + service: TacticService, + onProgress: (p: number) => void, +) { + const tactic = await loadPlainTactic(context, service, onProgress) + + const e = document.createElement("a") + e.setAttribute( + "href", + "data:application/json;charset=utf-8," + + encodeURIComponent(JSON.stringify(tactic, null, 2)), + ) + e.setAttribute("download", `${context.name}.json`) + e.style.display = "none" + + document.body.appendChild(e) + + e.click() + + document.body.removeChild(e) +} + +interface ProgressBarProps { + percentage: number +} + +function ProgressBar({ percentage }: ProgressBarProps) { + return ( +
+
+
+
+ ) +} diff --git a/src/service/APITacticService.ts b/src/service/APITacticService.ts index 718c22c..fe2f082 100644 --- a/src/service/APITacticService.ts +++ b/src/service/APITacticService.ts @@ -3,8 +3,8 @@ import { ServiceError, TacticContext, } from "./MutableTacticService.ts" -import { StepContent, StepInfoNode } from "../model/tactic/Tactic.ts" import { Fetcher } from "../app/Fetcher.ts" +import { StepContent, StepInfoNode } from "../model/tactic/TacticInfo.ts" export class APITacticService implements MutableTacticService { private readonly tacticId: number @@ -44,13 +44,13 @@ export class APITacticService implements MutableTacticService { } async addStep( - parent: StepInfoNode, + parentId: number, content: StepContent, ): Promise { const response = await this.fetcher.fetchAPI( `tactics/${this.tacticId}/steps`, { - parentId: parent.id, + parentId: parentId, content, }, ) diff --git a/src/service/LocalStorageTacticService.ts b/src/service/LocalStorageTacticService.ts index 32b14c2..d439145 100644 --- a/src/service/LocalStorageTacticService.ts +++ b/src/service/LocalStorageTacticService.ts @@ -3,7 +3,7 @@ import { ServiceError, TacticContext, } from "./MutableTacticService.ts" -import { StepContent, StepInfoNode } from "../model/tactic/Tactic.ts" +import { StepContent, StepInfoNode } from "../model/tactic/TacticInfo.ts" import { addStepNode, getAvailableId, @@ -55,7 +55,7 @@ export class LocalStorageTacticService implements MutableTacticService { } async addStep( - parent: StepInfoNode, + parentId: number, content: StepContent, ): Promise { const root: StepInfoNode = JSON.parse( @@ -65,7 +65,7 @@ export class LocalStorageTacticService implements MutableTacticService { const nodeId = getAvailableId(root) const node = { id: nodeId, children: [] } - const resultTree = addStepNode(root, parent, node) + const resultTree = addStepNode(root, parentId, node) localStorage.setItem( GUEST_MODE_STEP_ROOT_NODE_INFO_STORAGE_KEY, diff --git a/src/service/MutableTacticService.ts b/src/service/MutableTacticService.ts index b680d6b..c39618d 100644 --- a/src/service/MutableTacticService.ts +++ b/src/service/MutableTacticService.ts @@ -1,4 +1,8 @@ -import { CourtType, StepContent, StepInfoNode } from "../model/tactic/Tactic.ts" +import { + CourtType, + StepContent, + StepInfoNode, +} from "../model/tactic/TacticInfo.ts" export interface TacticContext { stepsTree: StepInfoNode @@ -19,7 +23,7 @@ export interface TacticService { export interface MutableTacticService extends TacticService { addStep( - parent: StepInfoNode, + parentId: number, content: StepContent, ): Promise diff --git a/src/style/actions/arrow_action.css b/src/style/actions/arrow_action.css index 77bfa4c..097a207 100644 --- a/src/style/actions/arrow_action.css +++ b/src/style/actions/arrow_action.css @@ -5,7 +5,7 @@ .arrow-action-icon { user-select: none; -moz-user-select: none; - -webkit-user-drag: none; + pointer-events: none; max-width: 17px; max-height: 17px; } diff --git a/src/style/court.css b/src/style/court.css index 77ca11c..a1f247e 100644 --- a/src/style/court.css +++ b/src/style/court.css @@ -1,24 +1,33 @@ -.court-image-div { - position: relative; - background-color: white; - height: 80vh; -} - .court-container { display: flex; align-content: center; align-items: center; justify-content: center; - background-color: black; + background-color: white; + padding: 5%; +} + +.court-container:has(.plain-court) { + width: 80%; +} + +.court-container:has(.half-court) { + height: 80%; } .court-image { height: 100%; width: 100%; + background-color: white; user-select: none; } .court-image * { stroke: var(--selected-team-secondarycolor); } + +.half-court { + max-height: 70vh; + width: fit-content; +} diff --git a/src/style/editor.css b/src/style/editor.css index 8c20276..4785ff2 100644 --- a/src/style/editor.css +++ b/src/style/editor.css @@ -1,8 +1,8 @@ @import "theme/default.css"; -@import "court.css"; @import "tactic.css"; #main-div { + position: relative; display: flex; height: 100%; width: 100%; @@ -102,10 +102,9 @@ } #court-div { - background-color: var(--background-color); - height: 100%; - width: 100%; + padding-left: 10%; + padding-right: 10%; display: flex; align-items: center; @@ -137,3 +136,10 @@ .save-state-guest { color: gray; } + +#exports-popup { + position: absolute; + width: 100%; + height: 100%; + z-index: 1000; +} diff --git a/src/style/export_tactic_popup.css b/src/style/export_tactic_popup.css new file mode 100644 index 0000000..367bb3d --- /dev/null +++ b/src/style/export_tactic_popup.css @@ -0,0 +1,65 @@ +.popup { + width: 100%; + height: 100%; + + display: flex; + align-items: center; + align-content: center; + justify-content: center; + + background: rgba(49, 36, 36, 0.53); + box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1); + backdrop-filter: blur(1.7px); + -webkit-backdrop-filter: blur(1.7px); +} + +.popup-card { + border: 1px solid rgba(71, 71, 86, 0.72); + + width: 50%; + height: 50%; + + display: flex; + flex-direction: column; + + overflow: hidden; + border-radius: 15px; +} + +.popup-exports { + display: flex; + align-content: center; + align-items: center; + justify-content: center; + + height: 100%; + background: rgba(49, 36, 36, 0.53); + backdrop-filter: blur(1.7px); + -webkit-backdrop-filter: blur(1.7px); +} + +.popup-header { + display: flex; + justify-content: center; + background-color: white; +} + +.export-card { + border-radius: 20px; + display: flex; + flex-direction: column; + background-color: white; + + align-items: center; + + cursor: pointer; +} + +.export-card .json-logo { + width: 80% !important; + height: 80% !important; +} + +.export-card .json-logo * { + fill: #f5992b; +} diff --git a/src/style/home/home.css b/src/style/home/home.css index 479773a..38821ba 100644 --- a/src/style/home/home.css +++ b/src/style/home/home.css @@ -2,15 +2,8 @@ @import url(personnal_space.css); @import url(side_menu.css); @import url(../template/header.css); - -body { - /* background-color: #303030; */ -} - +@import url(../tactic.css); #main { - /* margin-left : 10%; - margin-right: 10%; */ - /* border : solid 1px #303030; */ display: flex; flex-direction: column; font-family: var(--font-content); @@ -23,6 +16,7 @@ body { margin: 0; height: 100%; background-color: var(--home-second-color); + user-select: none; } .data { @@ -42,3 +36,100 @@ body { margin-left: 5%; margin-top: 5%; } + +#tactics-grid { + display: flex; + flex-direction: row; + flex-wrap: wrap; + gap: 6px; +} + +.tactic-card { + cursor: pointer; + + position: relative; + display: flex; + + align-items: center; + justify-content: center; + + flex-direction: column; + width: 32%; +} + +.tactic-card-content { + display: flex; + flex-direction: column; + align-items: center; + + pointer-events: none; + position: absolute; +} + +.tactic-card-title { + user-select: none; + padding: 0 10px 0 10px; + border-radius: 200px; + background: rgba(236, 235, 235, 0.87); +} + +.tactic-card-actions * { + width: 30px; +} + +.tactic-card-actions { + display: flex; + width: 100%; + height: 30px; + gap: 5px; + + align-content: center; + align-items: center; + justify-content: center; + + * { + pointer-events: all; + } +} + +.tactic-card-export-btn { + height: 100%; + + * { + fill: var(--accent-color); + } +} + +.tactic-card-remove-btn { + height: 100%; +} + +.tactic-card-preview * { + pointer-events: none !important; +} + +.tactic-card-duplicate-btn { + fill: #494949; +} + +.tactic-card-preview { + pointer-events: none; + display: flex; + justify-content: center; + overflow: hidden; +} + +.tactic-card-preview .court-container { + overflow: hidden; +} + +.tactic-card-preview * { + pointer-events: none; +} + +#exports-popup { + position: absolute; + width: 100%; + height: 100%; + z-index: 1000; +} diff --git a/src/style/home/personnal_space.css b/src/style/home/personnal_space.css index 6dc71b1..2f398d8 100644 --- a/src/style/home/personnal_space.css +++ b/src/style/home/personnal_space.css @@ -16,6 +16,13 @@ border: 3px var(--home-main-color) solid; border-radius: 0.5cap; align-self: center; + overflow-y: scroll; + -ms-overflow-style: none; + scrollbar-width: none; +} + +#body-personal-space::-webkit-scrollbar { + display: none; } #body-personal-space > p { diff --git a/src/style/home/side_menu.css b/src/style/home/side_menu.css index aeb50b4..4beede8 100644 --- a/src/style/home/side_menu.css +++ b/src/style/home/side_menu.css @@ -15,11 +15,19 @@ #side-menu-content { width: 90%; + height: 100%; + display: flex; + flex-direction: column; + gap: 25px; } + +#tactic-import-area { + height: 100%; +} + .titre-side-menu { border-bottom: var(--home-main-color) solid 3px; width: 100%; - margin-bottom: 3%; } #side-menu .title { @@ -30,7 +38,6 @@ text-transform: uppercase; background-color: var(--home-main-color); padding: 3%; - margin-bottom: 0px; margin-right: 3%; } diff --git a/src/style/template/header.css b/src/style/template/header.css index 6d64f46..78efc54 100644 --- a/src/style/template/header.css +++ b/src/style/template/header.css @@ -1,6 +1,7 @@ @import url(../theme/default.css); #header { + user-select: none; text-align: center; background-color: var(--home-main-color); margin: 0; @@ -19,6 +20,7 @@ width: 50px; height: 50px; border-radius: 20%; + -webkit-user-drag: none; } #header-left, diff --git a/src/style/visualizer.css b/src/style/visualizer.css index e992ea8..48335bb 100644 --- a/src/style/visualizer.css +++ b/src/style/visualizer.css @@ -1,9 +1,9 @@ -@import "court.css"; @import "theme/default.css"; @import "player.css"; @import "tactic.css"; #visualizer { + position: relative; display: flex; height: 100%; width: 100%; @@ -14,8 +14,12 @@ overflow: hidden; } -#editor-div { +#visualizer-div { height: 100%; + display: flex; + align-items: center; + justify-content: center; + align-content: center; } .curtain { @@ -27,7 +31,6 @@ display: flex; justify-content: center; align-items: center; - width: 100%; height: 100%; } @@ -70,3 +73,20 @@ overflow: scroll; height: 100%; } + +#court-div { + height: 100%; + width: 80%; + + display: flex; + align-items: center; + justify-content: center; + align-content: center; +} + +#exports-popup { + position: absolute; + width: 100%; + height: 100%; + z-index: 1000; +} diff --git a/src/visualizer/VisualizerState.ts b/src/visualizer/VisualizerState.ts index 6c80d20..95c2a18 100644 --- a/src/visualizer/VisualizerState.ts +++ b/src/visualizer/VisualizerState.ts @@ -1,4 +1,8 @@ -import { CourtType, StepContent, StepInfoNode } from "../model/tactic/Tactic.ts" +import { + CourtType, + StepContent, + StepInfoNode, +} from "../model/tactic/TacticInfo.ts" import { useReducer } from "react" export interface VisualizerState { diff --git a/vite.config.ts b/vite.config.ts index 8d932e7..9f40c89 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -2,11 +2,13 @@ import { defineConfig } from "vite" import react from "@vitejs/plugin-react" import cssInjectedByJsPlugin from "vite-plugin-css-injected-by-js" import svgr from "vite-plugin-svgr" +import { visualizer } from "rollup-plugin-visualizer" // https://vitejs.dev/config/ export default defineConfig({ build: { target: "es2021", + cssTarget: ["chrome112"] }, test: { environment: "jsdom", @@ -17,5 +19,6 @@ export default defineConfig({ relativeCSSInjection: true, }), svgr(), + visualizer() ], })