diff --git a/src/main/webapp/src/hooks/useExternalWindow.jsx b/src/main/webapp/src/hooks/useExternalWindow.jsx index 6603d47..1c455a0 100644 --- a/src/main/webapp/src/hooks/useExternalWindow.jsx +++ b/src/main/webapp/src/hooks/useExternalWindow.jsx @@ -1,6 +1,6 @@ import {createContext, useContext, useReducer} from "react"; -const PubAffContext = createContext({next: [], c1: undefined, c2: undefined, showScore: true}); +const PubAffContext = createContext({next: [], c1: undefined, c2: undefined, showScore: true, timeCb: undefined}); const PubAffDispatchContext = createContext(() => { }); @@ -8,6 +8,12 @@ function reducer(state, action) { switch (action.type) { case 'SET_DATA': return {...state, ...action.payload} + case 'CALL_TIME': + if (state.timeCb) + state.timeCb(action.payload) + return state + case 'CLEAR_CB_TIME': + return {...state, timeCb: undefined} default: return state } diff --git a/src/main/webapp/src/pages/competition/editor/CMTChronoPanel.jsx b/src/main/webapp/src/pages/competition/editor/CMTChronoPanel.jsx new file mode 100644 index 0000000..f779c92 --- /dev/null +++ b/src/main/webapp/src/pages/competition/editor/CMTChronoPanel.jsx @@ -0,0 +1,177 @@ +import React, {useEffect, useRef, useState} from "react"; +import {usePubAffDispatch} from "../../../hooks/useExternalWindow.jsx"; +import {timePrint} from "../../../utils/Tools.js"; + +export function ChronoPanel() { + const [config, setConfig] = useState({ + time: Number(sessionStorage.getItem("chronoTime") || "90999"), + pause: Number(sessionStorage.getItem("chronoPause") || "60999") + }) + const [chrono, setChrono] = useState({time: 0, startTime: 0}) + const chronoText = useRef(null) + const state = useRef({chronoState: 0, countBlink: 20, lastColor: "black", lastTimeStr: "00:00"}) + const publicAffDispatch = usePubAffDispatch(); + + const addTime = (time) => setChrono(prev => ({...prev, time: prev.time - time})) + const isRunning = () => chrono.startTime !== 0 + + const getTime = () => { + if (chrono.startTime === 0) + return chrono.time + return chrono.time + Date.now() - chrono.startTime + } + + useEffect(() => { + const blinkRfDuration = 20 + const state_ = state.current + const text_ = chronoText.current + + const timer = setInterval(() => { + let currentDuration = config.time + let color = "black" + if (state_.chronoState === 1) { + color = (state_.countBlink < blinkRfDuration) ? "black" : "red" + } else if (state_.chronoState === 2) { + currentDuration = (state_.chronoState === 0) ? 10000 : config.pause + color = (state_.countBlink < blinkRfDuration) ? "green" : "red" + } + const timeStr = timePrint(currentDuration - getTime()) + + if (timeStr !== state_.lastTimeStr || color !== state_.lastColor) + publicAffDispatch({type: 'CALL_TIME', payload: {timeStr: timeStr, timeColor: color}}) + + if (timeStr !== state_.lastTimeStr) { + text_.textContent = timePrint(currentDuration - getTime()) + state_.lastTimeStr = timeStr + } + if (color !== state_.lastColor) { + text_.style.color = color + state_.lastColor = color + } + + if (state_.chronoState === 0 && isRunning()) { + state_.chronoState = 1 + } else if (state_.chronoState === 1 && getTime() >= config.time) { + setChrono(prev => ({...prev, time: 0, startTime: Date.now()})) + state_.chronoState = 2 + } else if (state_.chronoState === 2 && getTime() >= config.pause) { + setChrono(prev => ({...prev, time: 0, startTime: Date.now()})) + state_.chronoState = 1 + } + + if (isRunning()) { + state_.countBlink = 19 + } else { + state_.countBlink++ + if (state_.countBlink > 40) + state_.countBlink = 0 + } + if (state_.chronoState === 0) { + clearInterval(timer) + } + }, 50); + return () => clearInterval(timer) + }, [chrono, config]) + + const handleSubmit = (e) => { + e.preventDefault(); + const form = e.target; + const timeStr = form[0].value; + const pauseStr = form[1].value; + + const parseTime = (str) => { + const parts = str.split(":").map(part => parseInt(part, 10)); + if (parts.length === 1) { + return parts[0] * 1000; + } else if (parts.length === 2) { + return (parts[0] * 60 + parts[1]) * 1000; + } else { + return 0; + } + } + + const newTime = parseTime(timeStr) + 999; + const newPause = parseTime(pauseStr) + 999; + + sessionStorage.setItem("chronoPause", newPause); + sessionStorage.setItem("chronoTime", newTime); + + setConfig({time: newTime, pause: newPause}); + } + + return
+
+ + +
+
+
+

{state.current.lastTimeStr}

+
+
+
+ + +
+
+ + +
+
+ +
+
+
+
+
+
Temps: {timePrint(config.time)}, pause: {timePrint(config.pause)}
+
+ +
+ + +
+} diff --git a/src/main/webapp/src/pages/competition/editor/CMTable.jsx b/src/main/webapp/src/pages/competition/editor/CMTable.jsx index e030238..9929bc2 100644 --- a/src/main/webapp/src/pages/competition/editor/CMTable.jsx +++ b/src/main/webapp/src/pages/competition/editor/CMTable.jsx @@ -15,6 +15,7 @@ import {PubAffProvider, usePubAffDispatch} from "../../../hooks/useExternalWindo import {faDisplay} from "@fortawesome/free-solid-svg-icons"; import {PubAffWindow} from "./PubAffWindow.jsx"; import {SimpleIconsScore} from "../../../assets/SimpleIconsScore.ts"; +import {ChronoPanel} from "./CMTChronoPanel.jsx"; function CupImg() { return
-
- A +
+
Chronomètre
+
+ +
B
-
+
Matches
@@ -94,6 +98,7 @@ function Menu() { setShowPubAff(false); externalWindow.current.close(); externalWindow.current = null; + publicAffDispatch({type: 'CLEAR_CB_TIME', payload: null}); sessionStorage.removeItem(windowName + "_open"); }); setShowPubAff(true); @@ -279,7 +284,11 @@ function MatchList({matches, cat}) { } else { publicAffDispatch({ type: 'SET_DATA', - payload: {c1: match.c1, c2: match.c2, next: marches2.filter(m => !m.end && m.poule === lice && m.id !== activeMatch).map(m => ({c1: m.c1, c2: m.c2}))} + payload: { + c1: match.c1, + c2: match.c2, + next: marches2.filter(m => !m.end && m.poule === lice && m.id !== activeMatch).map(m => ({c1: m.c1, c2: m.c2})) + } }); } }, [match]); @@ -333,7 +342,8 @@ function MatchList({matches, cat}) { {marches2.map((m, index) => ( - setActiveMatch(m.id)}> + setActiveMatch(m.id)}> {liceName[(index - firstIndex) % liceName.length]} {m.poule} diff --git a/src/main/webapp/src/pages/competition/editor/PubAffWindow.jsx b/src/main/webapp/src/pages/competition/editor/PubAffWindow.jsx index e2a0895..052b9de 100644 --- a/src/main/webapp/src/pages/competition/editor/PubAffWindow.jsx +++ b/src/main/webapp/src/pages/competition/editor/PubAffWindow.jsx @@ -1,7 +1,7 @@ import {useCombs} from "../../../hooks/useComb.jsx"; import {usePubAffState} from "../../../hooks/useExternalWindow.jsx"; import {SmartLogoBackgroundMemo} from "../../../components/SmartLogoBackground.jsx"; -import {useMemo} from 'react'; +import {useMemo, useRef} from 'react'; const vite_url = import.meta.env.VITE_URL; @@ -13,18 +13,27 @@ const text1Style = {fontSize: "min(2.25vw, 8vh)", fontWeight: "bold", marginLeft const text2Style = {fontSize: "min(1.7vw, 7vh)", fontWeight: "bold"}; export function PubAffWindow({document}) { + const chronoText = useRef(null) + const state2 = useRef({lastColor: "white", lastTimeStr: "01:30"}) const state = usePubAffState(); document.title = "A React portal window" document.body.className = "bg-dark text-white overflow-hidden"; + state.timeCb = (payload) => { + state2.current = {lastColor: payload.timeColor === "black" ? "white" : payload.timeColor, lastTimeStr: payload.timeStr} + chronoText.current.textContent = payload.timeStr + chronoText.current.style.color = payload.timeColor === "black" ? "white" : payload.timeColor + } + const showScore = state.showScore ?? true; return <>
-
01:30
+
{state2.current.lastTimeStr}
{showScore &&
diff --git a/src/main/webapp/src/utils/Tools.js b/src/main/webapp/src/utils/Tools.js index 21e9232..5821030 100644 --- a/src/main/webapp/src/utils/Tools.js +++ b/src/main/webapp/src/utils/Tools.js @@ -130,3 +130,26 @@ export function scorePrint(s1) { return String(s1) } } + +export function timePrint(time, negSign = false) { + if (time === null || time === undefined) + return "" + const neg = time < 0 + if (neg){ + if (!negSign) + return "00:00" + time = -time + } + + const ms = time % 1000 + time = (time - ms) / 1000 + const sec = time % 60 + time = (time - sec) / 60 + const min = time % 60 + const hr = (time - min) / 60 + + return (neg ? "-" : "") + + (hr > 0 ? String(hr).padStart(2, '0') + ":" : "") + + String(min).padStart(2, '0') + ":" + + String(sec).padStart(2, '0') +}