You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Application-Web/src/pages/HomePage.tsx

445 lines
13 KiB

import "../style/home/home.css"
import { useNavigate } from "react-router-dom"
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
name: string
creationDate: number
}
interface Team {
id: number
name: string
picture: string
main_color: string
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<HomePageStateAction>
}
const HomeStateContext = createContext<HomeStateContextMutable | null>(null)
function useHomeState() {
return useContext(HomeStateContext)
}
export default function HomePage() {
const [state, dispatch] = useReducer(homePageStateReducer, {
tactics: [],
teams: [],
})
const navigate = useNavigate()
const fetcher = useAppFetcher()
useEffect(() => {
async function initUserData() {
const response = await fetcher.fetchAPIGet("user-data")
if (response.status == 401) {
navigate("/login")
return // if unauthorized
}
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])
const tacticExportService = useMemo(
() =>
state.exportingTacticId
? new APITacticService(fetcher, state.exportingTacticId!)
: null,
[fetcher, state.exportingTacticId],
)
return (
<HomeStateContext.Provider value={{ state, dispatch }}>
{tacticExportService && (
<div id="exports-popup">
<ExportTacticPopup
service={tacticExportService}
onHide={() =>
dispatch({
type: HomePageStateActionKind.SET_EXPORTING_TACTIC,
tacticId: undefined,
})
}
/>
</div>
)}
<Home />
</HomeStateContext.Provider>
)
}
function Home() {
return (
<div id="main">
<Body />
</div>
)
}
function Body() {
const widthPersonalSpace = 78
const widthSideMenu = 100 - widthPersonalSpace
return (
<div id="body">
<PersonalSpace width={widthPersonalSpace} />
<SideMenu width={widthSideMenu} />
</div>
)
}
function SideMenu({ width }: { width: number }) {
return (
<div
id="side-menu"
style={{
width: width + "%",
}}>
<div id="side-menu-content">
<LastTeamsSideMenu />
<LastTacticsSideMenu />
<TacticImportArea />
</div>
</div>
)
}
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 (
<div id="tactic-import-area">
<FileUploader
handleChange={handleDrop}
types={["json"]}
hoverTitle="Déposez ici"
label="Séléctionnez ou déposez un fichier ici"/>
</div>
)
}
function PersonalSpace({ width }: { width: number }) {
return (
<div
id="personal-space"
style={{
width: width + "%",
}}>
<TitlePersonalSpace />
<BodyPersonalSpace />
</div>
)
}
function TitlePersonalSpace() {
return (
<div id="title-personal-space">
<h2>Espace Personnel</h2>
</div>
)
}
function TacticGrid({ tactics }: { tactics: Tactic[] }) {
return (
<div id={"tactics-grid"}>
{tactics.map((team) => (
<TacticCard key={team.id} tactic={team} />
))}
</div>
)
}
function TacticCard({ tactic }: { tactic: Tactic }) {
const navigate = useNavigate()
const fetcher = useAppFetcher()
const {
dispatch,
} = useHomeState()!
return (
<div
className={"tactic-card"}
onClick={() => navigate(`/tactic/${tactic.id}/edit`)}>
<div className={"tactic-card-preview"}>
<Visualizer
visualizerId={tactic.id.toString()}
tacticId={tactic.id}
/>
</div>
<div className="tactic-card-content">
<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
className="tactic-card-remove-btn"
onClick={async (e) => {
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,
})
}}
/>
<DuplicateSvg
className="tactic-card-duplicate-btn"
onClick={async (e) => {
e.stopPropagation()
const service = new APITacticService(
fetcher,
tactic.id,
)
const context = await service.getContext()
if (typeof context === "string")
throw Error(context)
const plainTactic = await loadPlainTactic(
context,
service,
)
const { name, id } = await importTactic(
fetcher,
plainTactic,
)
dispatch({
type: HomePageStateActionKind.ADD_TACTIC,
tactic: { name, id, creationDate: 0 },
})
}}
/>
</div>
</div>
</div>
)
}
function BodyPersonalSpace() {
const tactics = useHomeState()!.state.tactics
return (
<div id="body-personal-space">
{tactics.length == 0 ? (
<p>Aucune tactique créée !</p>
) : (
<TacticGrid tactics={tactics} />
)}
</div>
)
}
function LastTeamsSideMenu() {
const navigate = useNavigate()
return (
<div id="teams">
<div className="titre-side-menu">
<h2 className="title">Mes équipes</h2>
<button className="new" onClick={() => navigate("/team/new")}>
+
</button>
</div>
<SetButtonTeam />
</div>
)
}
function LastTacticsSideMenu() {
const navigate = useNavigate()
return (
<div id="tactic">
<div className="titre-side-menu">
<h2 className="title">Mes dernières tactiques</h2>
<button
className="new"
id="create-tactic"
onClick={() => navigate("/tactic/new")}>
+
</button>
</div>
<SetButtonTactic />
</div>
)
}
function SetButtonTactic() {
const tactics = useHomeState()!.state.tactics.slice(0, 5)
const lastTactics = tactics.map((tactic) => (
<LastTacticCard key={tactic.id} tactic={tactic} />
))
return <div className="set-button">{lastTactics}</div>
}
function SetButtonTeam() {
const teams = useHomeState()!.state.teams
const listTeam = teams.map((team) => <TeamCard key={team.id} team={team} />)
return <div className="set-button">{listTeam}</div>
}
function TeamCard({ team }: { team: Team }) {
const name = truncateString(team.name, 20)
const navigate = useNavigate()
return (
<div>
<div
id={"button-team" + team.id}
className="button-side-menu data"
onClick={() => {
navigate("/team/" + team.id)
}}>
{name}
</div>
</div>
)
}
function LastTacticCard({ tactic }: { tactic: Tactic }) {
const name = truncateString(tactic.name, 20)
const navigate = useNavigate()
return (
<div
id={"button" + tactic.id}
className="button-side-menu data"
onClick={() => {
navigate("/tactic/" + tactic.id + "/edit")
}}>
{name}
</div>
)
}
function truncateString(name: string, limit: number): string {
if (name.length > limit) {
name = name.substring(0, limit) + "..."
}
return name
}