feat: add return of table state

This commit is contained in:
2026-01-27 22:11:49 +01:00
parent 189eb135bb
commit d749dea6f4
15 changed files with 576 additions and 28 deletions

View File

@@ -5,7 +5,6 @@
"actuel": "Current",
"administration": "Administration",
"adresseDuServeur": "Server address",
"advertisement": "",
"ajouter": "Add",
"ajouterDesCombattants": "Add fighters",
"ajouterUn": "Add one",
@@ -40,6 +39,7 @@
"config.obs.motDePasseDuServeur": "Server password",
"config.obs.warn1": "/! The password will be stored in plain text; it is recommended to use it only on OBS WebSocket and to change it between each competition",
"config.obs.ws": "ws://",
"configurationDuNomDeLaZone": "Zone name configuration",
"configurationObs": "OBS Configuration",
"confirm1": "This match already has results; are you sure you want to delete it?",
"confirm2.msg": "Do you really want to change the tournament tree size or the loser matches? This will modify existing matches (including possible deletions)!",
@@ -64,6 +64,7 @@
"err3": "At least one type (pool or tournament) must be selected.",
"erreurLorsDeLaCopieDansLePresse": "Error while copying to clipboard: ",
"erreurLorsDeLaCréationDesMatchs": "Error while creating matches: ",
"etatDesTablesDeMarque": "State of marque tables",
"exporter": "Export",
"fermer": "Close",
"finalesUniquement": "Finals only",
@@ -80,6 +81,7 @@
"neRienConserver": "Keep nothing",
"no": "No.",
"nom": "Name",
"nomDeLaZone": "Area name",
"nomDeLéquipe": "team name",
"nomDesZonesDeCombat": "Combat zone names <1>(separated by ';')</1>",
"nouvelle...": "New...",
@@ -146,7 +148,7 @@
"ttm.admin.obs": "Short click: Download resources. Long click: Create OBS configuration",
"ttm.admin.scripte": "Copy integration script",
"ttm.table.inverserLaPosition": "Reverse fighter positions on this screen",
"ttm.table.obs": "Short click: Load configuration and connect. Long click: Ring configuration",
"ttm.table.obs": "Short click: Load configuration and connect.",
"ttm.table.pub_aff": "Open public display",
"ttm.table.pub_score": "Show scores on public display",
"type": "Type",

View File

@@ -5,7 +5,6 @@
"actuel": "Actuel",
"administration": "Administration",
"adresseDuServeur": "Adresse du serveur",
"advertisement": "Advertisement",
"ajouter": "Ajouter",
"ajouterDesCombattants": "Ajouter des combattants",
"ajouterUn": "Ajouter un ",
@@ -40,6 +39,7 @@
"config.obs.motDePasseDuServeur": "Mot de passe du serveur",
"config.obs.warn1": "/! Le mot de passe va être stoker en claire, il est recommandé de ne l'utiliser que sur obs websocket et d'en changer entre chaque compétition",
"config.obs.ws": "ws://",
"configurationDuNomDeLaZone": "Configuration du nom de la zone",
"configurationObs": "Configuration OBS",
"confirm1": "Ce match a déjà des résultats, êtes-vous sûr de vouloir le supprimer ?",
"confirm2.msg": "Voulez-vous vraiment changer la taille de l'arbre du tournoi ou les matchs pour les perdants ? Cela va modifier les matchs existants (incluant des possibles suppressions)!",
@@ -64,6 +64,7 @@
"err3": "Au moins un type (poule ou tournoi) doit être sélectionné.",
"erreurLorsDeLaCopieDansLePresse": "Erreur lors de la copie dans le presse-papier : ",
"erreurLorsDeLaCréationDesMatchs": "Erreur lors de la création des matchs: ",
"etatDesTablesDeMarque": "Etat des tables de marque",
"exporter": "Exporter",
"fermer": "Fermer",
"finalesUniquement": "Finales uniquement",
@@ -80,6 +81,7 @@
"neRienConserver": "Ne rien conserver",
"no": "N°",
"nom": "Nom",
"nomDeLaZone": "Nom de la zone",
"nomDeLéquipe": "Nom de l'équipe",
"nomDesZonesDeCombat": "Nom des zones de combat <1>(séparée par des ';')</1>",
"nouvelle...": "Nouvelle...",
@@ -146,7 +148,7 @@
"ttm.admin.obs": "Clique court : Télécharger les ressources. Clique long : Créer la configuration obs",
"ttm.admin.scripte": "Copier le scripte d'intégration",
"ttm.table.inverserLaPosition": "Inverser la position des combattants sur cette écran",
"ttm.table.obs": "Clique court : Charger la configuration et se connecter. Clique long : Configuration de la lice",
"ttm.table.obs": "Clique court : Charger la configuration et se connecter.",
"ttm.table.pub_aff": "Ouvrir l'affichage public",
"ttm.table.pub_score": "Afficher les scores sur l'affichage public",
"type": "Type",

View File

@@ -47,6 +47,7 @@ export function WSProvider({url, onmessage, children}) {
const [welcomeData, setWelcomeData] = useState({name: "", perm: "", show_blason: true, show_flag: false})
const [state, dispatch] = useReducer(reducer, {listener: []})
const ws = useRef(null)
const tableState = useRef({})
const listenersRef = useRef([])
const callbackRef = useRef({})
const isReadyRef = useRef(isReady)
@@ -216,14 +217,14 @@ export function WSProvider({url, onmessage, children}) {
}
const ret = {isReady, dispatch, send, wait_length: callbackRef, welcomeData}
const ret = {isReady, dispatch, send, wait_length: callbackRef, welcomeData, tableState}
return <WebsocketContext.Provider value={ret}>
{children}
</WebsocketContext.Provider>
}
export function useWS() {
const {isReady, dispatch, send, wait_length, welcomeData} = useContext(WebsocketContext)
const {isReady, dispatch, send, wait_length, welcomeData, tableState} = useContext(WebsocketContext)
return {
dispatch,
isReady,
@@ -247,6 +248,10 @@ export function useWS() {
send(uuidv4(), "error", "ERROR", data)
},
send,
setState: (newState) => {
tableState.current = {...tableState.current, ...newState}
},
tableState
}
}

View File

@@ -11,10 +11,12 @@ import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {SimpleIconsOBS} from "../../../assets/SimpleIconsOBS.ts";
import JSZip from "jszip";
import {detectOptimalBackground} from "../../../components/SmartLogoBackground.jsx";
import {faGlobe} from "@fortawesome/free-solid-svg-icons";
import {faGlobe, faTableCellsLarge} from "@fortawesome/free-solid-svg-icons";
import {Trans, useTranslation} from "react-i18next";
import i18n from "i18next";
import {getToastMessage} from "../../../utils/Tools.js";
import {copyStyles} from "../../../utils/copyStyles.js";
import {StateWindow} from "./StateWindow.jsx";
const vite_url = import.meta.env.VITE_URL;
@@ -161,12 +163,18 @@ async function downloadResourcesAsZip(resourceList) {
progressText.textContent = i18n.t('téléchargementTerminé!');
}
const windowName = "FFSAFTableStateWindow";
function Menu({menuActions, compUuid}) {
const e = document.getElementById("actionMenu")
const longPress = useRef({time: null, timer: null, button: null});
const obsModal = useRef(null);
const {t} = useTranslation("cm");
const [showStateWin, setShowStateWin] = useState(false)
const externalWindow = useRef(null)
const containerEl = useRef(document.createElement("div"))
for (const x of tto)
x.dispose();
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip2"]')
@@ -178,6 +186,32 @@ function Menu({menuActions, compUuid}) {
}
}
useEffect(() => {
if (sessionStorage.getItem(windowName + "_open") === "true") {
handleStateWin();
}
}, []);
const handleStateWin = __ => {
if (showStateWin === false || !externalWindow.current || externalWindow.current.closed) {
externalWindow.current = window.open("", windowName, "width=800,height=600,left=200,top=200")
externalWindow.current.document.body.innerHTML = ""
externalWindow.current.document.body.appendChild(containerEl.current)
copyStyles(document, externalWindow.current.document)
externalWindow.current.addEventListener("beforeunload", () => {
setShowStateWin(false);
externalWindow.current.close();
externalWindow.current = null;
sessionStorage.removeItem(windowName + "_open");
});
setShowStateWin(true);
sessionStorage.setItem(windowName + "_open", "true");
} else {
externalWindow.current.focus();
}
}
const longPressDown = (button) => {
longPress.current.button = button;
longPress.current.time = new Date();
@@ -251,7 +285,12 @@ function Menu({menuActions, compUuid}) {
onClick={() => copyScriptToClipboard()}
data-bs-toggle="tooltip2" data-bs-placement="top"
data-bs-title={t('ttm.admin.scripte')}/>
<FontAwesomeIcon icon={faTableCellsLarge} size="xl"
style={{color: showStateWin ? "#00c700" : "#6c757d", cursor: "pointer", marginRight: "0.25em"}}
onClick={handleStateWin}
data-bs-toggle="tooltip2" data-bs-placement="top" data-bs-title={t('etatDesTablesDeMarque')}/>
</>, document.getElementById("actionMenu"))}
{externalWindow.current && createPortal(<StateWindow document={externalWindow.current.document}/>, containerEl.current)}
<button ref={obsModal} type="button" className="btn btn-link" data-bs-toggle="modal" data-bs-target="#OBSModal" style={{display: 'none'}}>
Launch OBS Modal
@@ -500,7 +539,10 @@ function ModalContent({state, setCatId, setConfirm, confirmRef}) {
newTrees.push(trees2.at(i));
}
toast.promise(sendRequest('updateTrees', {categoryId: state.id, trees: newTrees}), getToastMessage("toast.updateTrees", "cm")
toast.promise(sendRequest('updateTrees', {
categoryId: state.id,
trees: newTrees
}), getToastMessage("toast.updateTrees", "cm")
).then(__ => {
toast.promise(sendRequest('updateCategory', newData), getToastMessage("toast.updateCategory", "cm"))
})

View File

@@ -2,6 +2,7 @@ import React, {useEffect, useRef, useState} from "react";
import {usePubAffDispatch} from "../../../hooks/useExternalWindow.jsx";
import {timePrint} from "../../../utils/Tools.js";
import {useTranslation} from "react-i18next";
import {useWS} from "../../../hooks/useWS.jsx";
export function ChronoPanel() {
const [config, setConfig] = useState({
@@ -10,6 +11,7 @@ export function ChronoPanel() {
})
const [chrono, setChrono] = useState({time: 0, startTime: 0})
const chronoText = useRef(null)
const [chronoState, setChronoState] = useState(0)
const state = useRef({chronoState: 0, countBlink: 20, lastColor: "#000000", lastTimeStr: "00:00"})
const publicAffDispatch = usePubAffDispatch();
const {t} = useTranslation("cm");
@@ -59,12 +61,15 @@ export function ChronoPanel() {
if (state_.chronoState === 0 && isRunning()) {
state_.chronoState = 1
setChronoState(1)
} else if (state_.chronoState === 1 && getTime() >= config.time) {
setChrono(prev => ({...prev, time: 0, startTime: Date.now()}))
state_.chronoState = 2
setChronoState(2)
} else if (state_.chronoState === 2 && getTime() >= config.pause) {
setChrono(prev => ({...prev, time: 0, startTime: Date.now()}))
state_.chronoState = 1
setChronoState(1)
}
if (isRunning()) {
@@ -117,6 +122,7 @@ export function ChronoPanel() {
<button className="btn btn-danger col" onClick={__ => {
setChrono(prev => ({...prev, time: 0, startTime: 0}))
state.current.chronoState = 0
setChronoState(0)
}}>{t('réinitialiser')}
</button>
</div>
@@ -181,5 +187,19 @@ export function ChronoPanel() {
</div>
</div>
</div>
<SendChrono chrono={chrono} config={config} chronoState={chronoState}/>
</div>
}
function SendChrono({chrono, config, chronoState}) {
const {sendNotify, setState} = useWS();
useEffect(() => {
setState({chronoState: {...chrono, configTime: config.time, configPause: config.pause, state: chronoState}});
sendNotify("sendCurentChrono", {...chrono, configTime: config.time, configPause: config.pause, state: chronoState});
}, [chrono]);
return <>
</>
}

View File

@@ -191,12 +191,13 @@ function MatchList({matches, cat, menuActions}) {
const [lice, setLice] = useState(localStorage.getItem("cm_lice") || "1")
const publicAffDispatch = usePubAffDispatch();
const {t} = useTranslation("cm");
const {cards, getHeightCardForCombInMatch} = useCards();
const {cards_v, getHeightCardForCombInMatch} = useCards();
const {sendNotify, setState} = useWS();
const liceName = (cat.liceName || "N/A").split(";");
const marches2 = matches.filter(m => m.categorie_ord !== -42)
.sort((a, b) => a.categorie_ord - b.categorie_ord)
.map(m => ({...m, ...win_end(m, Object.values(cards))}))
.map(m => ({...m, ...win_end(m, cards_v), end: m.end}))
const firstIndex = marches2.findLastIndex(m => m.poule === '-') + 1;
const isActiveMatch = (index) => {
@@ -221,10 +222,6 @@ function MatchList({matches, cat, menuActions}) {
});
}
}, [match]);
//useEffect(() => {
// if (activeMatch !== null)
// setActiveMatch(null);
//}, [cat])
useEffect(() => {
if (match && match.poule !== lice)
@@ -240,6 +237,11 @@ function MatchList({matches, cat, menuActions}) {
setActiveMatch(marches2.find((m, index) => !m.end && isActiveMatch(index))?.id);
}, [matches])
useEffect(() => {
setState({selectedMatch: activeMatch});
sendNotify("sendSelectMatch", activeMatch);
}, [activeMatch]);
const GetCard = ({combId, match, cat}) => {
const c = getHeightCardForCombInMatch(combId, match)
@@ -329,6 +331,7 @@ function BuildTree({treeData, matches, menuActions}) {
const {getComb} = useCombs()
const publicAffDispatch = usePubAffDispatch();
const {cards_v} = useCards();
const {sendNotify, setState} = useWS();
const match = matches.find(m => m.id === currentMatch?.matchSelect)
useEffect(() => {
@@ -390,6 +393,8 @@ function BuildTree({treeData, matches, menuActions}) {
const onMatchClick = (rect, matchId, __) => {
setCurrentMatch({matchSelect: matchId, matchNext: new TreeNode(matchId).nextMatchTree(trees.reverse())});
setState({selectedMatch: matchId});
sendNotify("sendSelectMatch", matchId);
}
const onClickVoid = () => {

View File

@@ -1,8 +1,9 @@
import {useEffect, useState} from "react";
import React, {useEffect, useState} from "react";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faChevronDown, faChevronUp} from "@fortawesome/free-solid-svg-icons";
import {usePubAffDispatch} from "../../../hooks/useExternalWindow.jsx";
import {useTranslation} from "react-i18next";
import {useWS} from "../../../hooks/useWS.jsx";
export function PointPanel({menuActions}) {
const [revers, setRevers] = useState(false)
@@ -49,5 +50,18 @@ export function PointPanel({menuActions}) {
<button className="btn btn-danger" onClick={handleReset}>{t('réinitialiser')}</button>
<button className="btn btn-success" onClick={handleSave}>{t('sauvegarder')}</button>
</div>
<SendScore scoreRouge={scoreRouge} scoreBleu={scoreBleu}/>
</div>
}
function SendScore({scoreRouge, scoreBleu}) {
const {sendNotify, setState} = useWS();
useEffect(() => {
setState({scoreState: {scoreRouge, scoreBleu}});
sendNotify("sendCurrentScore", {scoreRouge, scoreBleu});
}, [scoreRouge, scoreBleu]);
return <>
</>
}

View File

@@ -59,6 +59,7 @@ export function CMTable() {
</div>
<Menu menuActions={menuActions}/>
<ObsAutoSyncWhitPubAff/>
<SendCatId catId={catId}/>
</div>
</PubAffProvider>
</OBSProvider>
@@ -73,6 +74,7 @@ function Menu({menuActions}) {
const publicAffDispatch = usePubAffDispatch()
const [showPubAff, setShowPubAff] = useState(false)
const [showScore, setShowScore] = useState(true)
const [zone, setZone] = useState(sessionStorage.getItem("liceName") || "???")
const {connected, connect, disconnect} = useOBS();
const longPress = useRef({time: null, timer: null, button: null});
const obsModal = useRef(null);
@@ -124,7 +126,7 @@ function Menu({menuActions}) {
const longTimeAction = (button) => {
if (button === "obs") {
obsModal.current.click();
// obsModal.current.click();
}
}
@@ -169,12 +171,19 @@ function Menu({menuActions}) {
}
}
const handleOBSSubmit = (e) => {
const handleLiceSubmit = (e) => {
e.preventDefault();
const form = e.target;
const prefix = form[0].value;
sessionStorage.setItem("obs_prefix", prefix);
if (prefix === "") {
sessionStorage.removeItem("liceName");
setZone("???");
return;
}
sessionStorage.setItem("liceName", prefix);
setZone(prefix);
}
if (!e)
@@ -182,6 +191,8 @@ function Menu({menuActions}) {
return <>
{createPortal(
<>
<div className="vr" style={{margin: "0 0.5em", height: "100%"}}></div>
<span onClick={() => obsModal.current.click()} style={{cursor: "pointer"}}>Zone {zone}</span>
<div className="vr" style={{margin: "0 0.5em", height: "100%"}}></div>
<FontAwesomeIcon icon={faArrowRightArrowLeft} size="xl" style={{color: "#6c757d", cursor: "pointer", marginRight: "0.25em"}}
onClick={handleSwitchScore} data-bs-toggle="tooltip2" data-bs-placement="top"
@@ -203,23 +214,23 @@ function Menu({menuActions}) {
</>, document.getElementById("actionMenu"))}
{externalWindow.current && createPortal(<PubAffWindow document={externalWindow.current.document}/>, containerEl.current)}
<button ref={obsModal} type="button" className="btn btn-link" data-bs-toggle="modal" data-bs-target="#OBSModal" style={{display: 'none'}}>
Launch OBS Modal
<button ref={obsModal} type="button" className="btn btn-link" data-bs-toggle="modal" data-bs-target="#LiceNameModal"
style={{display: 'none'}}>
Launch Lice Name Modal
</button>
<div className="modal fade" id="OBSModal" tabIndex="-1" aria-labelledby="OBSModalLabel" aria-hidden="true">
<div className="modal fade" id="LiceNameModal" tabIndex="-1" aria-labelledby="LiceNameModalLabel" aria-hidden="true">
<div className="modal-dialog">
<div className="modal-content">
<div className="modal-header">
<h5 className="modal-title">Configuration OBS</h5>
<h5 className="modal-title">{t('configurationDuNomDeLaZone')}</h5>
<button type="button" className="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form onSubmit={handleOBSSubmit}>
<form onSubmit={handleLiceSubmit}>
<div className="modal-body">
<div className="input-group mb-3">
<span className="input-group-text">{t('obs.préfixDesSources')}</span>
<span className="input-group-text">sub</span>
<input type="text" className="form-control" placeholder="1" aria-label="" size={1} minLength={1} maxLength={1}
defaultValue={localStorage.getItem("obs_prefix") || "1"} required/>
<span className="input-group-text">{t('nomDeLaZone')}</span>
<input type="text" className="form-control" placeholder="1" aria-label="" size={1} minLength={0} maxLength={1}
defaultValue={sessionStorage.getItem("liceName") || "1"}/>
</div>
</div>
<div className="modal-footer">
@@ -230,6 +241,40 @@ function Menu({menuActions}) {
</div>
</div>
</div>
<SendLiceName name={zone}/>
</>
}
function SendLiceName({name}) {
const {sendNotify, setState} = useWS();
useEffect(() => {
setState({liceName: name});
sendNotify("sendLicenceName", name);
}, [name]);
return <>
</>
}
function SendCatId({catId}) {
const {sendNotify, setState, dispatch, tableState} = useWS();
useEffect(() => {
const welcomeInfo = () => {
sendNotify("sendState", tableState.current)
}
welcomeInfo();
dispatch({type: 'addListener', payload: {callback: welcomeInfo, code: 'welcomeInfo'}})
return () => dispatch({type: 'removeListener', payload: welcomeInfo});
}, []);
useEffect(() => {
setState({selectedCategory: catId});
sendNotify("sendSelectCategory", catId);
}, [catId]);
return <>
</>
}

View File

@@ -0,0 +1,211 @@
import {useWS} from "../../../hooks/useWS.jsx";
import {useEffect, useReducer, useRef, useState} from "react";
import {useCards, useCardsDispatch} from "../../../hooks/useCard.jsx";
import {from_sendTree} from "../../../utils/TreeUtils.js";
import {MarchReducer} from "../../../utils/MatchReducer.jsx";
import {CombName, useCombsDispatch} from "../../../hooks/useComb.jsx";
import {timePrint, win_end} from "../../../utils/Tools.js";
function reducer(state, action) {
switch (action.type) {
case 'SET':
return [...state.filter(s => s.id !== action.payload.id), action.payload]
case 'SET_ALL':
return action.payload
case 'REMOVE':
return state.filter(s => s.id !== action.payload)
default:
return state
}
}
export function StateWindow({document}) {
const {sendRequest, dispatch} = useWS();
const [state, dispatchState] = useReducer(reducer, [])
const subscribeToState = () => {
sendRequest("subscribeToState", true)
.then((data) => {
dispatchState({type: 'SET_ALL', payload: data});
})
}
useEffect(() => {
const sendStateFull = ({data}) => {
dispatchState({type: 'SET', payload: data});
}
const rmStateFull = ({data}) => {
dispatchState({type: 'REMOVE', payload: data});
}
const welcomeInfo = () => {
subscribeToState();
}
subscribeToState();
dispatch({type: 'addListener', payload: {callback: welcomeInfo, code: 'welcomeInfo'}})
dispatch({type: 'addListener', payload: {callback: sendStateFull, code: 'sendStateFull'}})
dispatch({type: 'addListener', payload: {callback: rmStateFull, code: 'rmStateFull'}})
return () => {
dispatch({type: 'removeListener', payload: welcomeInfo});
dispatch({type: 'removeListener', payload: sendStateFull});
dispatch({type: 'removeListener', payload: rmStateFull});
sendRequest("subscribeToState", false)
.then(() => {
});
}
}, [])
document.title = "État des tables de marque";
document.body.className = "overflow-hidden";
console.log(state)
return <>
<div className="d-flex flex-row flex-wrap justify-content-around align-items-center align-content-around h-100 p-2 overflow-auto">
{state.sort((a, b) => a.liceName.localeCompare(b.liceName)).map((table, index) =>
<div key={index} className="card d-inline-flex flex-grow-1 align-self-stretch" style={{minWidth: "25em", maxWidth: "30em"}}>
<ShowState table={table} dispatch={dispatchState}/>
</div>)
}
</div>
</>
}
function readAndConvertMatch(matches, data, combsToAdd) {
matches.push({
...data,
c1: data.c1?.id,
c2: data.c2?.id,
c1_cacheName: data.c1?.fname + " " + data.c1?.lname,
c2_cacheName: data.c2?.fname + " " + data.c2?.lname
})
if (data.c1)
combsToAdd.push(data.c1)
if (data.c2)
combsToAdd.push(data.c2)
}
function ShowState({table}) {
const cardDispatch = useCardsDispatch();
const {sendRequest, dispatch} = useWS();
const [matches, reducer] = useReducer(MarchReducer, []);
const combDispatch = useCombsDispatch();
const {cards_v} = useCards();
const [cat, setCat] = useState({id: -1, name: ""});
const marches2 = matches.filter(m => m.categorie === cat.id).map(m => ({...m, ...win_end(m, cards_v)}))
useEffect(() => {
const categoryListener = ({data}) => {
if (data.id !== cat.id)
return;
setCat({id: data.id, name: data.name});
}
const matchListener = ({data: datas}) => {
for (const data of datas) {
reducer({type: 'UPDATE_OR_ADD', payload: {...data, c1: data.c1?.id, c2: data.c2?.id}});
combDispatch({type: 'SET_ALL', payload: {source: "match", data: [data.c1, data.c2].filter(d => d != null)}});
}
}
const deleteMatch = ({data: datas}) => {
for (const data of datas)
reducer({type: 'REMOVE', payload: data});
}
dispatch({type: 'addListener', payload: {callback: categoryListener, code: 'sendCategory'}})
dispatch({type: 'addListener', payload: {callback: matchListener, code: 'sendMatch'}})
dispatch({type: 'addListener', payload: {callback: deleteMatch, code: 'sendDeleteMatch'}})
return () => {
dispatch({type: 'removeListener', payload: matchListener})
dispatch({type: 'removeListener', payload: deleteMatch})
dispatch({type: 'removeListener', payload: categoryListener})
}
}, []);
useEffect(() => {
if (table.selectedCategory !== cat.id) {
if (!table.selectedCategory || table.selectedCategory === -1) {
setCat({id: -1, name: ""});
return;
}
sendRequest('getFullCategory', table.selectedCategory)
.then((data) => {
setCat({id: data.id, name: data.name});
cardDispatch({type: 'SET_ALL', payload: data.cards});
let matches2 = [];
let combsToAdd = [];
data.trees.flatMap(d => from_sendTree(d, false).flat()).forEach((data_) => readAndConvertMatch(matches2, data_, combsToAdd));
data.matches.forEach((data_) => readAndConvertMatch(matches2, data_, combsToAdd));
reducer({type: 'REPLACE_ALL', payload: matches2});
combDispatch({type: 'SET_ALL', payload: {source: "match", data: combsToAdd}});
console.log(matches2);
})
}
}, [table]);
return <>
<div className="card-header">
Zone de combat {table.liceName}
</div>
<div className="card-body">
Catégorie : {cat.name}<br/>
Match terminés : {marches2.filter(m => m.end).length}/{marches2.length}<br/>
Matchs : <PrintMatch match={matches.find(m => m.id === table.selectedMatch)}/><br/>
Statue : {table?.state}<br/>
Score : {table?.scoreState?.scoreRouge} - {table?.scoreState?.scoreBleu}<br/>
Chronomètre : <PrintChrono chrono={table?.chronoState}/><br/>
</div>
</>
}
function PrintMatch({match}) {
return <>{match?.c1 && <CombName combId={match?.c1}/>} vs {match?.c2 && <CombName combId={match?.c2}/>}</>
}
function PrintChrono({chrono}) {
const chronoText = useRef(null)
const state = useRef({chronoState: 0, countBlink: 20, lastColor: "#000000", lastTimeStr: "00:00"})
const isRunning = () => chrono.startTime !== 0
const getTime = () => {
if (chrono.startTime === 0)
return chrono.time
return chrono.time + Date.now() - chrono.startTime
}
useEffect(() => {
if (!chrono || !chronoText.current)
return;
const state_ = state.current
const text_ = chronoText.current
const timer = setInterval(() => {
let currentDuration = chrono.configTime
if (chrono.state === 2) {
currentDuration = (chrono.state === 0) ? 10000 : chrono.configPause
}
const timeStr = (chrono.state === 1 ? " Match - " : " Pause - ") + timePrint(currentDuration - getTime()) + (isRunning() ? "" : " (arrêté)")
if (timeStr !== state_.lastTimeStr) {
text_.textContent = timeStr
state_.lastTimeStr = timeStr
}
if (chrono.chronoState === 0) {
clearInterval(timer)
}
}, 50);
return () => clearInterval(timer)
}, [chrono])
return <><span ref={chronoText}>{state.current.lastTimeStr}</span></>
}

View File

@@ -272,7 +272,6 @@ export function win_end(match, cards = []) {
export function virtual_end(match, cards) {
if (!cards)
return false
console.log(cards.filter(c => (c.comb === match?.c1 || c.comb === match?.c2) && hasEffectCard(c, match.id, match.categorie)))
return cards.some(c => (c.comb === match?.c1 || c.comb === match?.c2) && hasEffectCard(c, match.id, match.categorie))
}