Compare commits
8 Commits
Author | SHA1 | Date |
---|---|---|
|
b941d6530c | 1 year ago |
|
1faa8168f9 | 1 year ago |
|
0de42db300 | 1 year ago |
![]() |
262cf97445 | 1 year ago |
![]() |
47d81bb665 | 1 year ago |
![]() |
eef1e16830 | 1 year ago |
![]() |
98eed72af6 | 1 year ago |
![]() |
7289a956b3 | 1 year ago |
@ -1,2 +1,2 @@
|
|||||||
#VITE_API_ENDPOINT=https://iqball.maxou.dev/api/dotnet-master
|
VITE_API_ENDPOINT=https://iqball.maxou.dev/api/dotnet-master
|
||||||
VITE_API_ENDPOINT=http://localhost:5254
|
#VITE_API_ENDPOINT=http://localhost:5254
|
||||||
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 9.9 KiB |
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 5.6 KiB |
Before Width: | Height: | Size: 507 B |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 575 B |
After Width: | Height: | Size: 781 B |
After Width: | Height: | Size: 1.6 KiB |
@ -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<TacticInfo> {
|
||||||
|
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<Tactic> {
|
||||||
|
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 {
|
||||||
|
content: contentResult,
|
||||||
|
children: await Promise.all(
|
||||||
|
stepInfoNode.children.map(transformToStep),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: context.name,
|
||||||
|
courtType: context.courtType,
|
||||||
|
root: await transformToStep(tree),
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,118 @@
|
|||||||
|
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 { loadPlainTactic } from "../../domains/TacticPersistenceDomain.ts"
|
||||||
|
|
||||||
|
export interface ExportTacticPopupProps {
|
||||||
|
service: TacticService
|
||||||
|
onHide: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ExportTacticPopup({
|
||||||
|
service,
|
||||||
|
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 (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">Exporter en JSON</p>
|
||||||
|
<JsonIcon className="json-logo" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div className={"progressbar"} style={{ display: "flex", height: 3 }}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
backgroundColor: "#f5992b",
|
||||||
|
width: `${percentage}%`,
|
||||||
|
}}></div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
backgroundColor: "white",
|
||||||
|
width: `${100 - percentage}%`,
|
||||||
|
}}></div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@ -1,24 +1,33 @@
|
|||||||
.court-image-div {
|
|
||||||
position: relative;
|
|
||||||
background-color: white;
|
|
||||||
height: 80vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
.court-container {
|
.court-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-content: center;
|
align-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: 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 {
|
.court-image {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
background-color: white;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.court-image * {
|
.court-image * {
|
||||||
stroke: var(--selected-team-secondarycolor);
|
stroke: var(--selected-team-secondarycolor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.half-court {
|
||||||
|
max-height: 70vh;
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
@ -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;
|
||||||
|
}
|