add JSON export
continuous-integration/drone/push Build is passing Details

pull/120/head
maxime 1 year ago
parent 98eed72af6
commit eef1e16830

@ -23,7 +23,6 @@ import {
import { BASE } from "./Constants.ts" import { BASE } from "./Constants.ts"
import { Authentication, Fetcher } from "./app/Fetcher.ts" import { Authentication, Fetcher } from "./app/Fetcher.ts"
import { User } from "./model/User.ts" import { User } from "./model/User.ts"
import { VisualizerPage } from "./pages/VisualizerPage.tsx"
const HomePage = lazy(() => import("./pages/HomePage.tsx")) const HomePage = lazy(() => import("./pages/HomePage.tsx"))
const LoginPage = lazy(() => import("./pages/LoginPage.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 TeamPanelPage = lazy(() => import("./pages/TeamPanel.tsx"))
const NewTacticPage = lazy(() => import("./pages/NewTacticPage.tsx")) const NewTacticPage = lazy(() => import("./pages/NewTacticPage.tsx"))
const Editor = lazy(() => import("./pages/Editor.tsx")) const Editor = lazy(() => import("./pages/Editor.tsx"))
const VisualizerPage = lazy(() => import("./pages/VisualizerPage.tsx"))
const Settings = lazy(() => import("./pages/Settings.tsx")) const Settings = lazy(() => import("./pages/Settings.tsx"))
const TOKEN_REFRESH_INTERVAL_MS = 60 * 1000 const TOKEN_REFRESH_INTERVAL_MS = 60 * 1000
@ -148,14 +148,13 @@ export default function App() {
element={suspense( element={suspense(
<LoggedInPage> <LoggedInPage>
<VisualizerPage guestMode={false} /> <VisualizerPage guestMode={false} />
,
</LoggedInPage>, </LoggedInPage>,
)} )}
/> />
<Route <Route
path={"/tactic/view-guest"} path={"/tactic/view-guest"}
element={suspense( element={suspense(
<VisualizerPage guestMode={true} />, <VisualizerPage guestMode={true} />
)} )}
/> />
<Route <Route

Binary file not shown.

Before

Width:  |  Height:  |  Size: 507 B

@ -0,0 +1,3 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M13 8.00421C13.5523 8.00421 14 8.45192 14 9.00418V12.0042C14 13.1088 13.1046 14.0042 12 14.0042H2C0.89543 14.0042 0 13.1088 0 12.0042V9.00418C0 8.45192 0.44772 8.00421 1 8.00421C1.55228 8.00421 2 8.45192 2 9.00418V12.0042H12V9.00418C12 8.45192 12.4477 8.00421 13 8.00421ZM7 0.589996L10.7071 4.2971C11.0976 4.68763 11.0976 5.32079 10.7071 5.71132C10.3166 6.10184 9.6834 6.10184 9.2929 5.71132L8 4.41842V9.00418C8 9.55648 7.55228 10.0042 7 10.0042C6.44772 10.0042 6 9.55648 6 9.00418V4.41842L4.70711 5.71132C4.31658 6.10184 3.68342 6.10184 3.29289 5.71132C2.90237 5.32079 2.90237 4.68763 3.29289 4.2971L7 0.589996Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 781 B

@ -0,0 +1 @@
<svg class="svg-icon" style="width: 1em; height: 1em;vertical-align: middle;fill: currentColor;overflow: hidden;" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M213.333333 128h85.333334v85.333333H213.333333v213.333334a85.333333 85.333333 0 0 1-85.333333 85.333333 85.333333 85.333333 0 0 1 85.333333 85.333333v213.333334h85.333334v85.333333H213.333333c-45.653333-11.52-85.333333-38.4-85.333333-85.333333v-170.666667a85.333333 85.333333 0 0 0-85.333333-85.333333H0v-85.333334h42.666667a85.333333 85.333333 0 0 0 85.333333-85.333333V213.333333a85.333333 85.333333 0 0 1 85.333333-85.333333m597.333334 0a85.333333 85.333333 0 0 1 85.333333 85.333333v170.666667a85.333333 85.333333 0 0 0 85.333333 85.333333h42.666667v85.333334h-42.666667a85.333333 85.333333 0 0 0-85.333333 85.333333v170.666667a85.333333 85.333333 0 0 1-85.333333 85.333333h-85.333334v-85.333333h85.333334v-213.333334a85.333333 85.333333 0 0 1 85.333333-85.333333 85.333333 85.333333 0 0 1-85.333333-85.333333V213.333333h-85.333334V128h85.333334m-298.666667 512a42.666667 42.666667 0 0 1 42.666667 42.666667 42.666667 42.666667 0 0 1-42.666667 42.666666 42.666667 42.666667 0 0 1-42.666667-42.666666 42.666667 42.666667 0 0 1 42.666667-42.666667m-170.666667 0a42.666667 42.666667 0 0 1 42.666667 42.666667 42.666667 42.666667 0 0 1-42.666667 42.666666 42.666667 42.666667 0 0 1-42.666666-42.666666 42.666667 42.666667 0 0 1 42.666666-42.666667m341.333334 0a42.666667 42.666667 0 0 1 42.666666 42.666667 42.666667 42.666667 0 0 1-42.666666 42.666666 42.666667 42.666667 0 0 1-42.666667-42.666666 42.666667 42.666667 0 0 1 42.666667-42.666667z" fill="" /></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

@ -12,7 +12,7 @@ import {
StepContent, StepContent,
StepInfoNode, StepInfoNode,
TacticComponent, TacticComponent,
} from "../model/tactic/Tactic.ts" } from "../model/tactic/TacticInfo.ts"
import { getParent } from "../domains/StepsDomain.ts" import { getParent } from "../domains/StepsDomain.ts"
import { import {
computeRelativePositions, computeRelativePositions,

@ -6,10 +6,12 @@ import {
ComponentId, ComponentId,
CourtType, CourtType,
TacticComponent, TacticComponent,
} from "../../model/tactic/Tactic" } from "../../model/tactic/TacticInfo.ts"
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 "../../style/court.css"
export interface BasketCourtProps { export interface BasketCourtProps {
components: TacticComponent[] components: TacticComponent[]
parentComponents: TacticComponent[] | null parentComponents: TacticComponent[] | null
@ -73,10 +75,9 @@ export function BasketCourt({
export function Court({ courtType }: { courtType: CourtType }) { export function Court({ courtType }: { courtType: CourtType }) {
const CourtSvg = courtType === "PLAIN" ? PlainCourt : HalfCourt const CourtSvg = courtType === "PLAIN" ? PlainCourt : HalfCourt
const courtSpecificClassName = courtType === "PLAIN" ? "plain-court" : "half-court" const courtSpecificClassName =
courtType === "PLAIN" ? "plain-court" : "half-court"
return ( return (
<div className="court-image-div">
<CourtSvg className={`court-image ${courtSpecificClassName}`} /> <CourtSvg className={`court-image ${courtSpecificClassName}`} />
</div>
) )
} }

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

@ -1,5 +1,5 @@
import "../../style/steps_tree.css" import "../../style/steps_tree.css"
import { StepInfoNode } from "../../model/tactic/Tactic" import { StepInfoNode } from "../../model/tactic/TacticInfo.ts"
import BendableArrow from "../arrows/BendableArrow" import BendableArrow from "../arrows/BendableArrow"
import { ReactNode, useMemo, useRef } from "react" import { ReactNode, useMemo, useRef } from "react"
import AddSvg from "../../assets/icon/add.svg?react" import AddSvg from "../../assets/icon/add.svg?react"

@ -9,7 +9,7 @@ import {
ComponentId, ComponentId,
StepContent, StepContent,
TacticComponent, TacticComponent,
} from "../model/tactic/Tactic.ts" } from "../model/tactic/TacticInfo.ts"
import { overlaps } from "../geo/Box.ts" import { overlaps } from "../geo/Box.ts"
import { Action, ActionKind, moves } from "../model/tactic/Action.ts" import { Action, ActionKind, moves } from "../model/tactic/Action.ts"
import { removeBall, updateComponent } from "./TacticContentDomains.ts" import { removeBall, updateComponent } from "./TacticContentDomains.ts"

@ -9,7 +9,7 @@ import {
ComponentId, ComponentId,
StepContent, StepContent,
TacticComponent, TacticComponent,
} from "../model/tactic/Tactic.ts" } from "../model/tactic/TacticInfo.ts"
import { removeComponent, updateComponent } from "./TacticContentDomains.ts" import { removeComponent, updateComponent } from "./TacticContentDomains.ts"
import { import {

@ -1,4 +1,4 @@
import { StepInfoNode } from "../model/tactic/Tactic.ts" import { StepInfoNode } from "../model/tactic/TacticInfo.ts"
export function addStepNode( export function addStepNode(
root: StepInfoNode, root: StepInfoNode,
@ -110,3 +110,7 @@ export function getParent(
} }
return null return null
} }
export function countSteps(tree: StepInfoNode): number {
return 1 + tree.children.reduce((tot, node) => tot + countSteps(node), 0)
}

@ -18,7 +18,7 @@ import {
ComponentId, ComponentId,
StepContent, StepContent,
TacticComponent, TacticComponent,
} from "../model/tactic/Tactic.ts" } from "../model/tactic/TacticInfo.ts"
import { overlaps } from "../geo/Box.ts" import { overlaps } from "../geo/Box.ts"
import { RackedCourtObject, RackedPlayer } from "../editor/RackedItems.ts" import { RackedCourtObject, RackedPlayer } from "../editor/RackedItems.ts"

@ -1,4 +1,4 @@
import { StepContent } from "../model/tactic/Tactic.ts" import { StepContent } from "../model/tactic/TacticInfo.ts"
export class ContentVersions { export class ContentVersions {
private index = 0 private index = 0

@ -1,5 +1,5 @@
import { Pos } from "../../geo/Pos" import { Pos } from "../../geo/Pos"
import { ComponentId } from "./Tactic" import { ComponentId } from "./TacticInfo.ts"
export enum ActionKind { export enum ActionKind {
SCREEN = "SCREEN", SCREEN = "SCREEN",

@ -1,4 +1,4 @@
import { Component, Frozable } from "./Tactic" import { Component, Frozable } from "./TacticInfo.ts"
import { Pos } from "../../geo/Pos.ts" import { Pos } from "../../geo/Pos.ts"
export const BALL_ID = "ball" export const BALL_ID = "ball"

@ -1,4 +1,4 @@
import { Component, ComponentId, Frozable } from "./Tactic" import { Component, ComponentId, Frozable } from "./TacticInfo.ts"
import { Pos } from "../../geo/Pos.ts" import { Pos } from "../../geo/Pos.ts"
export type PlayerId = string export type PlayerId = string

@ -2,6 +2,12 @@ import { Player, PlayerPhantom } from "./Player"
import { Action } from "./Action" import { Action } from "./Action"
import { CourtObject } from "./CourtObjects" import { CourtObject } from "./CourtObjects"
export interface Tactic {
readonly name: string
readonly courtType: CourtType
readonly root: TacticStep
}
export interface TacticInfo { export interface TacticInfo {
readonly id: number readonly id: number
readonly name: string readonly name: string
@ -12,6 +18,7 @@ export interface TacticInfo {
export interface TacticStep { export interface TacticStep {
readonly stepId: number readonly stepId: number
readonly content: StepContent readonly content: StepContent
readonly children: TacticStep[]
} }
export interface StepContent { export interface StepContent {

@ -22,7 +22,7 @@ import {
StepContent, StepContent,
StepInfoNode, StepInfoNode,
TacticComponent, TacticComponent,
} from "../model/tactic/Tactic" } from "../model/tactic/TacticInfo.ts"
import SavingState, { import SavingState, {
SaveState, SaveState,
@ -97,12 +97,14 @@ import SplitLayout from "../components/SplitLayout.tsx"
import { import {
MutableTacticService, MutableTacticService,
ServiceError, ServiceError,
TacticService,
} from "../service/MutableTacticService.ts" } from "../service/MutableTacticService.ts"
import { LocalStorageTacticService } from "../service/LocalStorageTacticService.ts" import { LocalStorageTacticService } from "../service/LocalStorageTacticService.ts"
import { APITacticService } from "../service/APITacticService.ts" import { APITacticService } from "../service/APITacticService.ts"
import { useNavigate, useParams } from "react-router-dom" import { useNavigate, useParams } from "react-router-dom"
import { ContentVersions } from "../editor/ContentVersions.ts" import { ContentVersions } from "../editor/ContentVersions.ts"
import { useAppFetcher } from "../App.tsx" import { useAppFetcher } from "../App.tsx"
import ExportTacticPopup from "./popup/ExportTacticPopup.tsx"
const ERROR_STYLE: CSSProperties = { const ERROR_STYLE: CSSProperties = {
borderColor: "red", borderColor: "red",
@ -132,6 +134,8 @@ interface EditorService {
setName(name: string): Promise<SaveState> setName(name: string): Promise<SaveState>
openVisualizer(): Promise<void> openVisualizer(): Promise<void>
getTacticService(): TacticService
} }
export default function Editor({ guestMode }: EditorProps) { export default function Editor({ guestMode }: EditorProps) {
@ -356,6 +360,10 @@ function EditorPageWrapper({
async openVisualizer(): Promise<void> { async openVisualizer(): Promise<void> {
openVisualizer() openVisualizer()
}, },
getTacticService(): TacticService {
return service
},
} }
}, [stepsTree, service, stepsVersions, setStepContent, openVisualizer]) }, [stepsTree, service, stepsVersions, setStepContent, openVisualizer])
@ -423,10 +431,7 @@ function EditorPage({
const [isStepsTreeVisible, setStepsTreeVisible] = useState(true) const [isStepsTreeVisible, setStepsTreeVisible] = useState(true)
const courtBounds = useCallback( const [showExportPopup, setShowExportPopup] = useState(false)
() => courtRef.current!.getBoundingClientRect(),
[courtRef],
)
const [editorContentCurtainWidth, setEditorContentCurtainWidth] = const [editorContentCurtainWidth, setEditorContentCurtainWidth] =
useState(80) useState(80)
@ -438,6 +443,11 @@ function EditorPage({
: new Map() : new Map()
}, [content, courtRef]) }, [content, courtRef])
const courtBounds = useCallback(
() => courtRef.current!.getBoundingClientRect(),
[courtRef],
)
const setComponents = (action: SetStateAction<TacticComponent[]>) => { const setComponents = (action: SetStateAction<TacticComponent[]>) => {
service.setContent((c) => ({ service.setContent((c) => ({
...c, ...c,
@ -809,6 +819,15 @@ function EditorPage({
return ( return (
<div id="main-div"> <div id="main-div">
{showExportPopup && (
<div id="exports-popup">
<ExportTacticPopup
service={service.getTacticService()}
show={showExportPopup}
onHide={() => setShowExportPopup(false)}
/>
</div>
)}
<div id="topbar-div"> <div id="topbar-div">
<div id="topbar-left"> <div id="topbar-left">
<SavingState state={contentSaveState} /> <SavingState state={contentSaveState} />
@ -834,10 +853,15 @@ function EditorPage({
VISUALISER VISUALISER
</button> </button>
<button <button
id={"show-steps-button"} id="show-steps-button"
onClick={() => setStepsTreeVisible((b) => !b)}> onClick={() => setStepsTreeVisible((b) => !b)}>
ETAPES ETAPES
</button> </button>
<button
id="show-exports-popup"
onClick={() => setShowExportPopup(true)}>
EXPORTER
</button>
</div> </div>
</div> </div>
<div id="editor-div"> <div id="editor-div">

@ -1,9 +1,18 @@
import "../style/home/home.css" import "../style/home/home.css"
import { useNavigate } from "react-router-dom" 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 { useAppFetcher } from "../App.tsx"
import { Visualizer } from "../components/Visualizer.tsx" import { Visualizer } from "../components/Visualizer.tsx"
import BinSvg from "../assets/icon/bin.svg?react" 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 { interface Tactic {
id: number id: number
@ -22,26 +31,40 @@ interface Team {
enum HomePageStateActionKind { enum HomePageStateActionKind {
UPDATE_TACTICS = "UPDATE_TACTICS", UPDATE_TACTICS = "UPDATE_TACTICS",
UPDATE_TEAMS = "UPDATE_TEAMS", UPDATE_TEAMS = "UPDATE_TEAMS",
SET_EXPORTING_TACTIC = "SET_EXPORTING_TACTIC",
INIT = "INIT" INIT = "INIT"
} }
type HomePageStateAction = { type HomePageStateAction =
type: HomePageStateActionKind.UPDATE_TACTICS, | {
type: HomePageStateActionKind.UPDATE_TACTICS
tactics: Tactic[] tactics: Tactic[]
} | { }
type: HomePageStateActionKind.UPDATE_TEAMS, | {
type: HomePageStateActionKind.UPDATE_TEAMS
teams: Team[] teams: Team[]
} | { }
type: HomePageStateActionKind.INIT, | {
type: HomePageStateActionKind.INIT
state: HomePageState state: HomePageState
} | {
type: HomePageStateActionKind.SET_EXPORTING_TACTIC,
tacticId: number | undefined
} }
interface HomePageState { interface HomePageState {
tactics: Tactic[] tactics: Tactic[]
teams: Team[] 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) { switch (action.type) {
case HomePageStateActionKind.UPDATE_TACTICS: case HomePageStateActionKind.UPDATE_TACTICS:
return { ...state!, tactics: action.tactics } return { ...state!, tactics: action.tactics }
@ -49,13 +72,16 @@ function homePageStateReducer(state: HomePageState, action: HomePageStateAction)
case HomePageStateActionKind.UPDATE_TEAMS: case HomePageStateActionKind.UPDATE_TEAMS:
return { ...state!, teams: action.teams } return { ...state!, teams: action.teams }
case HomePageStateActionKind.SET_EXPORTING_TACTIC:
return { ...state!, exportingTacticId: action.tacticId }
case HomePageStateActionKind.INIT: case HomePageStateActionKind.INIT:
return action.state return action.state
} }
} }
interface HomeStateContextMutable { interface HomeStateContextMutable {
state: HomePageState, state: HomePageState
dispatch: Dispatch<HomePageStateAction> dispatch: Dispatch<HomePageStateAction>
} }
@ -66,8 +92,10 @@ function useHomeState() {
} }
export default function HomePage() { export default function HomePage() {
const [state, dispatch] = useReducer(homePageStateReducer, { tactics: [], teams: [] }) const [state, dispatch] = useReducer(homePageStateReducer, {
tactics: [],
teams: [],
})
const navigate = useNavigate() const navigate = useNavigate()
const fetcher = useAppFetcher() const fetcher = useAppFetcher()
@ -80,18 +108,32 @@ export default function HomePage() {
navigate("/login") navigate("/login")
return // if unauthorized return // if unauthorized
} }
type UserDataResponse = { teams: Team[], tactics: Tactic[] } type UserDataResponse = { teams: Team[]; tactics: Tactic[] }
const { teams, tactics }: UserDataResponse = await response.json() const { teams, tactics }: UserDataResponse = await response.json()
tactics.sort((a, b) => b.creationDate - a.creationDate) tactics.sort((a, b) => b.creationDate - a.creationDate)
dispatch({ type: HomePageStateActionKind.INIT, state: { teams, tactics } }) dispatch({
type: HomePageStateActionKind.INIT,
state: { teams, tactics },
})
} }
initUserData() initUserData()
}, [fetcher, navigate]) }, [fetcher, navigate])
const tacticExportService = useMemo(() =>
state.exportingTacticId ? new APITacticService(fetcher, state.exportingTacticId!) : null
, [fetcher, state.exportingTacticId],
)
return ( return (
<HomeStateContext.Provider value={{ state, dispatch }}> <HomeStateContext.Provider value={{ state, dispatch }}>
{tacticExportService && <div id="exports-popup">
<ExportTacticPopup
service={tacticExportService}
show={true}
onHide={() => dispatch({ type: HomePageStateActionKind.SET_EXPORTING_TACTIC, tacticId: undefined })}
/>
</div>}
<Home /> <Home />
</HomeStateContext.Provider> </HomeStateContext.Provider>
) )
@ -111,9 +153,7 @@ function Body() {
return ( return (
<div id="body"> <div id="body">
<PersonalSpace width={widthPersonalSpace} /> <PersonalSpace width={widthPersonalSpace} />
<SideMenu <SideMenu width={widthSideMenu} />
width={widthSideMenu}
/>
</div> </div>
) )
} }
@ -133,9 +173,7 @@ function SideMenu({ width }: { width: number }) {
) )
} }
function PersonalSpace({ function PersonalSpace({ width }: { width: number }) {
width,
}: { width: number }) {
return ( return (
<div <div
id="personal-space" id="personal-space"
@ -169,12 +207,17 @@ function TacticGrid({ tactics }: { tactics: Tactic[] }) {
function TacticCard({ tactic }: { tactic: Tactic }) { function TacticCard({ tactic }: { tactic: Tactic }) {
const navigate = useNavigate() const navigate = useNavigate()
const fetcher = useAppFetcher() const fetcher = useAppFetcher()
const { state: { tactics }, dispatch } = useHomeState()! const {
state: { tactics },
dispatch,
} = useHomeState()!
return ( return (
<div className={"tactic-card"}>
<div <div
className={"tactic-card-preview"} className={"tactic-card"}
onClick={() => navigate(`/tactic/${tactic.id}/edit`)}> onClick={() => navigate(`/tactic/${tactic.id}/edit`)}
>
<div
className={"tactic-card-preview"}>
<Visualizer <Visualizer
visualizerId={tactic.id.toString()} visualizerId={tactic.id.toString()}
tacticId={tactic.id} tacticId={tactic.id}
@ -182,19 +225,38 @@ function TacticCard({ tactic }: { tactic: Tactic }) {
</div> </div>
<div className="tactic-card-content"> <div className="tactic-card-content">
<p className="tactic-card-title">{tactic.name}</p> <p className="tactic-card-title">{tactic.name}</p>
<div className="tactic-card-actions">
<ExportSvg
className="tactic-card-export-btn"
onClick={(e) => {
e.stopPropagation()
dispatch({
type: HomePageStateActionKind.SET_EXPORTING_TACTIC,
tacticId: tactic.id,
})
}}
/>
<BinSvg <BinSvg
className="tactic-card-remove-btn" className="tactic-card-remove-btn"
onClick={async () => { onClick={async (e) => {
const response = await fetcher.fetchAPI(`tactics/${tactic.id}`, {}, "DELETE") e.stopPropagation()
const response = await fetcher.fetchAPI(
`tactics/${tactic.id}`,
{},
"DELETE",
)
if (!response.ok) { if (!response.ok) {
throw Error(`Cannot delete tactic ${tactic.id}!`) throw Error(`Cannot delete tactic ${tactic.id}!`)
} }
dispatch({ dispatch({
type: HomePageStateActionKind.UPDATE_TACTICS, type: HomePageStateActionKind.UPDATE_TACTICS,
tactics: tactics.filter(t => t.id !== tactic.id), tactics: tactics.filter((t) => t.id !== tactic.id),
}) })
}} }}
/> />
</div>
</div> </div>
</div> </div>
) )
@ -258,9 +320,7 @@ function SetButtonTactic() {
function SetButtonTeam() { function SetButtonTeam() {
const teams = useHomeState()!.state.teams const teams = useHomeState()!.state.teams
const listTeam = teams.map((team) => ( const listTeam = teams.map((team) => <TeamCard key={team.id} team={team} />)
<TeamCard key={team.id} team={team} />
))
return <div className="set-button">{listTeam}</div> return <div className="set-button">{listTeam}</div>
} }

@ -3,7 +3,7 @@ import "../style/new_tactic_panel.css"
import plainCourt from "../assets/court/full_court.svg" import plainCourt from "../assets/court/full_court.svg"
import halfCourt from "../assets/court/half_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 { useCallback } from "react"
import { useAppFetcher, useUser } from "../App.tsx" import { useAppFetcher, useUser } from "../App.tsx"
import { useNavigate } from "react-router-dom" import { useNavigate } from "react-router-dom"

@ -1,15 +1,15 @@
import { ServiceError, TacticService } from "../service/MutableTacticService.ts" import { ServiceError, TacticService } from "../service/MutableTacticService.ts"
import { useNavigate, useParams } from "react-router-dom" import { useNavigate, useParams } from "react-router-dom"
import { useCallback, useEffect, useMemo, useState } from "react"
import { import {
useVisualizer, useVisualizer,
VisualizerState, VisualizerState,
VisualizerStateActionKind, VisualizerStateActionKind,
} from "../visualizer/VisualizerState.ts" } from "../visualizer/VisualizerState.ts"
import { useCallback, useEffect, useMemo, useState } from "react"
import { getParent } from "../domains/StepsDomain.ts" import { getParent } from "../domains/StepsDomain.ts"
import { mapToParentContent } from "../domains/TacticContentDomains.ts" import { mapToParentContent } from "../domains/TacticContentDomains.ts"
import StepsTree from "../components/editor/StepsTree.tsx" 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 SplitLayout from "../components/SplitLayout.tsx"
import { LocalStorageTacticService } from "../service/LocalStorageTacticService.ts" import { LocalStorageTacticService } from "../service/LocalStorageTacticService.ts"
import { APITacticService } from "../service/APITacticService.ts" import { APITacticService } from "../service/APITacticService.ts"
@ -17,12 +17,13 @@ import { APITacticService } from "../service/APITacticService.ts"
import "../style/visualizer.css" import "../style/visualizer.css"
import { VisualizerFrame } from "../components/Visualizer.tsx" import { VisualizerFrame } from "../components/Visualizer.tsx"
import { useAppFetcher } from "../App.tsx" import { useAppFetcher } from "../App.tsx"
import ExportTacticPopup from "./popup/ExportTacticPopup.tsx"
export interface VisualizerPageProps { export interface VisualizerPageProps {
guestMode: boolean guestMode: boolean
} }
export function VisualizerPage({ guestMode }: VisualizerPageProps) { export default function VisualizerPage({ guestMode }: VisualizerPageProps) {
const { tacticId: idStr } = useParams() const { tacticId: idStr } = useParams()
const navigate = useNavigate() const navigate = useNavigate()
@ -48,6 +49,8 @@ interface VisualizerService {
selectStep(step: number): Promise<void | ServiceError> selectStep(step: number): Promise<void | ServiceError>
openEditor(): Promise<void> openEditor(): Promise<void>
getTacticService(): TacticService
} }
interface ServedVisualizerPageProps { interface ServedVisualizerPageProps {
@ -101,7 +104,7 @@ function ServedVisualizerPage({
} }
if (state === null) init() if (state === null) init()
}, [service, state]) }, [dispatch, service, state])
const visualizerService: VisualizerService = useMemo( const visualizerService: VisualizerService = useMemo(
() => ({ () => ({
@ -125,8 +128,12 @@ function ServedVisualizerPage({
async openEditor() { async openEditor() {
openEditor() openEditor()
}, },
getTacticService(): TacticService {
return service
},
}), }),
[openEditor, service, state], [dispatch, openEditor, service, state],
) )
if (panicMessage) { if (panicMessage) {
@ -162,6 +169,8 @@ function VisualizerPageContent({
const [editorContentCurtainWidth, setEditorContentCurtainWidth] = const [editorContentCurtainWidth, setEditorContentCurtainWidth] =
useState(80) useState(80)
const [showExportPopup, setShowExportPopup] = useState(false)
const stepsTreeNode = ( const stepsTreeNode = (
<div id={"steps-div"}> <div id={"steps-div"}>
<StepsTree <StepsTree
@ -189,9 +198,23 @@ function VisualizerPageContent({
return ( return (
<div id="visualizer"> <div id="visualizer">
{showExportPopup && (
<div id="exports-popup">
<ExportTacticPopup
service={service.getTacticService()}
show={showExportPopup}
onHide={() => setShowExportPopup(false)}
/>
</div>
)}
<div id="header-page"> <div id="header-page">
<p id="title">{tacticName}</p> <p id="title">{tacticName}</p>
<div id="header-page-right"> <div id="header-page-right">
<button
id="show-exports-popup"
onClick={() => setShowExportPopup(true)}>
EXPORTER
</button>
<button <button
id={"show-steps-button"} id={"show-steps-button"}
onClick={() => setStepsTreeVisible((b) => !b)}> onClick={() => setStepsTreeVisible((b) => !b)}>

@ -0,0 +1,154 @@
import {
TacticContext,
TacticService,
} from "../../service/MutableTacticService.ts"
import { useEffect, useState } from "react"
import "../../style/export_tactic_popup.css"
import JsonIcon from "../../assets/icon/json.svg?react"
import {
StepInfoNode,
Tactic,
TacticStep,
} from "../../model/tactic/TacticInfo.ts"
import { countSteps } from "../../domains/StepsDomain.ts"
export interface ExportTacticPopupProps {
service: TacticService
show: boolean
onHide: () => void
}
export default function ExportTacticPopup({
service,
show,
onHide,
}: ExportTacticPopupProps) {
const [context, setContext] = useState<TacticContext>()
const [panicMessage, setPanicMessage] = useState<string>()
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 (!show) return <></>
if (panicMessage) return <p>{panicMessage}</p>
return (
<div className="popup" onClick={onHide}>
<div className="popup-card" onClick={(e) => e.stopPropagation()}>
<div className="popup-header">
<p>Exporting {context?.name ?? "Tactic"}</p>
</div>
<ProgressBar percentage={exportPercentage} />
<div className="popup-exports">
<div
className="export-card"
onClick={async () => {
await exportInJson(
context!,
service,
setExportPercentage,
)
setExportPercentage(0)
}}>
<p className="export-card-title">Export in JSON</p>
<JsonIcon className="json-logo" />
</div>
</div>
</div>
</div>
)
}
async function exportInJson(
context: TacticContext,
service: TacticService,
onProgress: (p: number) => void,
) {
const tree = context.stepsTree
const treeSize = countSteps(tree)
const totalStepsCompleted = new Uint16Array(1)
async function transformToStep(
stepInfoNode: StepInfoNode,
): Promise<TacticStep> {
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 {
stepId: stepInfoNode.id,
content: contentResult,
children: await Promise.all(
stepInfoNode.children.map(transformToStep),
),
}
}
const tactic: Tactic = {
name: context.name,
courtType: context.courtType,
root: await transformToStep(tree),
}
const e = document.createElement("a")
e.setAttribute(
"href",
"data:application/octet-stream," +
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 (
<div className={"progressbar"} style={{ display: "flex", height: 3 }}>
<div
style={{
backgroundColor: "#f5992b",
width: `${percentage}%`,
}}></div>
<div
style={{
backgroundColor: "white",
width: `${100 - percentage}%`,
}}></div>
</div>
)
}

@ -3,8 +3,8 @@ import {
ServiceError, ServiceError,
TacticContext, TacticContext,
} from "./MutableTacticService.ts" } from "./MutableTacticService.ts"
import { StepContent, StepInfoNode } from "../model/tactic/Tactic.ts"
import { Fetcher } from "../app/Fetcher.ts" import { Fetcher } from "../app/Fetcher.ts"
import { StepContent, StepInfoNode } from "../model/tactic/TacticInfo.ts"
export class APITacticService implements MutableTacticService { export class APITacticService implements MutableTacticService {
private readonly tacticId: number private readonly tacticId: number

@ -3,7 +3,7 @@ import {
ServiceError, ServiceError,
TacticContext, TacticContext,
} from "./MutableTacticService.ts" } from "./MutableTacticService.ts"
import { StepContent, StepInfoNode } from "../model/tactic/Tactic.ts" import { StepContent, StepInfoNode } from "../model/tactic/TacticInfo.ts"
import { import {
addStepNode, addStepNode,
getAvailableId, getAvailableId,

@ -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 { export interface TacticContext {
stepsTree: StepInfoNode stepsTree: StepInfoNode

@ -1,8 +1,8 @@
@import "theme/default.css"; @import "theme/default.css";
@import "court.css";
@import "tactic.css"; @import "tactic.css";
#main-div { #main-div {
position: relative;
display: flex; display: flex;
height: 100%; height: 100%;
width: 100%; width: 100%;
@ -103,6 +103,8 @@
#court-div { #court-div {
height: 100%; height: 100%;
padding-left: 10%;
padding-right: 10%;
display: flex; display: flex;
align-items: center; align-items: center;
@ -134,3 +136,10 @@
.save-state-guest { .save-state-guest {
color: gray; color: gray;
} }
#exports-popup {
position: absolute;
width: 100%;
height: 100%;
z-index: 1000;
}

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

@ -2,15 +2,8 @@
@import url(personnal_space.css); @import url(personnal_space.css);
@import url(side_menu.css); @import url(side_menu.css);
@import url(../template/header.css); @import url(../template/header.css);
@import url(../tactic.css);
body {
/* background-color: #303030; */
}
#main { #main {
/* margin-left : 10%;
margin-right: 10%; */
/* border : solid 1px #303030; */
display: flex; display: flex;
flex-direction: column; flex-direction: column;
font-family: var(--font-content); font-family: var(--font-content);
@ -51,6 +44,8 @@ body {
} }
.tactic-card { .tactic-card {
cursor: pointer;
position: relative; position: relative;
display: flex; display: flex;
@ -67,7 +62,6 @@ body {
align-items: center; align-items: center;
pointer-events: none; pointer-events: none;
z-index: 1000;
position: absolute; position: absolute;
} }
@ -78,24 +72,43 @@ body {
background: rgba(236, 235, 235, 0.87); background: rgba(236, 235, 235, 0.87);
} }
.tactic-card-remove-btn { .tactic-card-actions * {
pointer-events: all;
width: 30px; width: 30px;
}
.tactic-card-actions {
display: flex;
width: 100%;
height: 30px; height: 30px;
cursor: pointer; gap: 5px;
align-content: center;
align-items: center;
justify-content: center;
}
transition: scale .5s; .tactic-card-export-btn {
pointer-events: all;
height: 100%;
* {
fill: var(--accent-color);
}
} }
.tactic-card-remove-btn:hover { .tactic-card-remove-btn {
scale: 1.25; pointer-events: all;
height: 100%;
} }
.tactic-card-preview * {
pointer-events: none !important;
}
.tactic-card-preview { .tactic-card-preview {
pointer-events: none;
display: flex; display: flex;
justify-content: center; justify-content: center;
cursor: pointer;
overflow: hidden; overflow: hidden;
} }
@ -106,3 +119,10 @@ body {
.tactic-card-preview * { .tactic-card-preview * {
pointer-events: none; pointer-events: none;
} }
#exports-popup {
position: absolute;
width: 100%;
height: 100%;
z-index: 1000;
}

@ -1,9 +1,9 @@
@import "court.css";
@import "theme/default.css"; @import "theme/default.css";
@import "player.css"; @import "player.css";
@import "tactic.css"; @import "tactic.css";
#visualizer { #visualizer {
position: relative;
display: flex; display: flex;
height: 100%; height: 100%;
width: 100%; width: 100%;
@ -31,7 +31,6 @@
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
width: 50%;
height: 100%; height: 100%;
} }
@ -77,10 +76,17 @@
#court-div { #court-div {
height: 100%; height: 100%;
width: 70%; width: 80%;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
align-content: center; align-content: center;
} }
#exports-popup {
position: absolute;
width: 100%;
height: 100%;
z-index: 1000;
}

@ -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" import { useReducer } from "react"
export interface VisualizerState { export interface VisualizerState {

Loading…
Cancel
Save