From 09a51edd5f48f63db4fdf29821190cc60ce9bb60 Mon Sep 17 00:00:00 2001 From: Thibaut Valentin Date: Wed, 10 Dec 2025 00:08:14 +0100 Subject: [PATCH] wip: public screen --- .../fr/titionfire/ffsaf/ws/CompetitionWS.java | 2 +- .../webapp/src/hooks/useExternalWindow.jsx | 32 ++++ src/main/webapp/src/hooks/useWS.jsx | 24 +-- .../src/pages/competition/editor/CMTable.jsx | 152 ++++++++++++++---- .../editor/CompetitionManagerRoot.jsx | 4 +- .../pages/competition/editor/PubAffWindow.jsx | 134 +++++++++++++++ src/main/webapp/src/utils/copyStyles.js | 21 +++ 7 files changed, 327 insertions(+), 42 deletions(-) create mode 100644 src/main/webapp/src/hooks/useExternalWindow.jsx create mode 100644 src/main/webapp/src/pages/competition/editor/PubAffWindow.jsx create mode 100644 src/main/webapp/src/utils/copyStyles.js diff --git a/src/main/java/fr/titionfire/ffsaf/ws/CompetitionWS.java b/src/main/java/fr/titionfire/ffsaf/ws/CompetitionWS.java index b585820..e0cf3bd 100644 --- a/src/main/java/fr/titionfire/ffsaf/ws/CompetitionWS.java +++ b/src/main/java/fr/titionfire/ffsaf/ws/CompetitionWS.java @@ -130,7 +130,7 @@ public class CompetitionWS { @OnTextMessage Multi processAsync(WebSocketConnection connection, MessageIn message) { - + System.out.println(message); if (message.type() == MessageType.REPLY || message.type() == MessageType.ERROR) { try { JsonUni jsonUni = waitingResponse.get(connection).get(message.uuid()); diff --git a/src/main/webapp/src/hooks/useExternalWindow.jsx b/src/main/webapp/src/hooks/useExternalWindow.jsx new file mode 100644 index 0000000..cb144d2 --- /dev/null +++ b/src/main/webapp/src/hooks/useExternalWindow.jsx @@ -0,0 +1,32 @@ +import {createContext, useContext, useReducer} from "react"; + +const PubAffContext = createContext({next: [], c1: undefined, c2: undefined}); +const PubAffDispatchContext = createContext(() => { +}); + +function reducer(state, action) { + switch (action.type) { + case 'SET_DATA': + return {...state, ...action.payload} + default: + return state + } +} + +export function PubAffProvider({children}) { + const [state, dispatch] = useReducer(reducer, {}) + + return + + {children} + + +} + +export function usePubAffState() { + return useContext(PubAffContext) +} + +export function usePubAffDispatch() { + return useContext(PubAffDispatchContext) +} diff --git a/src/main/webapp/src/hooks/useWS.jsx b/src/main/webapp/src/hooks/useWS.jsx index 8d3fbd3..bd05b73 100644 --- a/src/main/webapp/src/hooks/useWS.jsx +++ b/src/main/webapp/src/hooks/useWS.jsx @@ -1,6 +1,7 @@ import {createContext, useContext, useEffect, useId, useReducer, useRef, useState} from "react"; import {apiAxios} from "../utils/Tools.js"; import {toast} from "react-toastify"; +import {useAuth} from "./useAuth.jsx"; function uuidv4() { return "10000000-1000-4000-8000-100000000000".replace(/[018]/g, c => @@ -42,6 +43,7 @@ const mountCounter = {}; export function WSProvider({url, onmessage, children}) { const id = useId(); + const {is_authenticated} = useAuth() const [isReady, setIsReady] = useState(false) const [state, dispatch] = useReducer(reducer, {listener: []}) const ws = useRef(null) @@ -71,17 +73,21 @@ export function WSProvider({url, onmessage, children}) { socket.onclose = () => { setIsReady(false) if (mountCounter[id] > 0) { - console.log("WSProvider: reconnecting to", url); setTimeout(() => { - try { - const newSocket = new WebSocket(url) - ws.current = newSocket - newSocket.onopen = socket.onopen - newSocket.onclose = socket.onclose - newSocket.onmessage = socket.onmessage - }catch (e) { + //if (is_authenticated){ + console.log("WSProvider: reconnecting to", url); + try { + const newSocket = new WebSocket(url) + ws.current = newSocket + newSocket.onopen = socket.onopen + newSocket.onclose = socket.onclose + newSocket.onmessage = socket.onmessage + }catch (e) { - } + } + //}else{ + // console.log("WSProvider: not reconnecting, user is not authenticated"); + //} }, 5000) } } diff --git a/src/main/webapp/src/pages/competition/editor/CMTable.jsx b/src/main/webapp/src/pages/competition/editor/CMTable.jsx index 37a999c..2e52036 100644 --- a/src/main/webapp/src/pages/competition/editor/CMTable.jsx +++ b/src/main/webapp/src/pages/competition/editor/CMTable.jsx @@ -9,6 +9,11 @@ import {faCircleQuestion} from "@fortawesome/free-regular-svg-icons"; import {DrawGraph} from "../../result/DrawGraph.jsx"; import {scorePrint, win} from "../../../utils/Tools.js"; import {toast} from "react-toastify"; +import {createPortal} from "react-dom"; +import {copyStyles} from "../../../utils/copyStyles.js"; +import {PubAffProvider, usePubAffDispatch, usePubAffState} from "../../../hooks/useExternalWindow.jsx"; +import {faDisplay} from "@fortawesome/free-solid-svg-icons"; +import {PubAffWindow} from "./PubAffWindow.jsx"; function CupImg() { return -
-
-
- A -
-
- B -
-
-
-
-
Matches
-
- + return +
+
+
+
+ A +
+
+ B
-
- D +
+
+
Matches
+
+ +
+
+
+ D +
+
-
+
} -function CategorieSelect({catId, setCatId}) { +const windowName = "FFSAFScorePublicWindow"; + +function Menu({menuAction}) { + const e = document.getElementById("actionMenu") + const [showPubAff, setShowPubAff] = useState(false) + + const externalWindow = useRef(null) + const containerEl = useRef(document.createElement("div")) + + useEffect(() => { + if (sessionStorage.getItem(windowName + "_open") === "true") { + handlePubAff(); + } + //return () => { + // if (!externalWindow.current) + // return; + // externalWindow.current.close(); + //} + }, []); + + const handlePubAff = __ => { + if (showPubAff === 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", () => { + setShowPubAff(false); + externalWindow.current.close(); + externalWindow.current = null; + sessionStorage.removeItem(windowName + "_open"); + }); + setShowPubAff(true); + sessionStorage.setItem(windowName + "_open", "true"); + } else { + externalWindow.current.focus(); + } + } + + if (!e) + return <>; + return <> + {createPortal( + <> +
+ + , document.getElementById("actionMenu"))} + {externalWindow.current && createPortal(, containerEl.current)} + +} + +function CategorieSelect({catId, setCatId, menuAction}) { const setLoading = useLoadingSwitcher() const {data: cats, setData: setCats} = useRequestWS('getAllCategory', {}, setLoading); const {dispatch} = useWS(); @@ -68,11 +130,11 @@ function CategorieSelect({catId, setCatId}) { ))}
- {catId !== -1 && } + {catId !== -1 && } } -function MatchPanel({catId, cat}) { +function MatchPanel({catId, cat, menuAction}) { const setLoading = useLoadingSwitcher() const {sendRequest, dispatch} = useWS(); const [trees, setTrees] = useState([]); @@ -144,10 +206,10 @@ function MatchPanel({catId, cat}) { } }, [catId]); - return + return } -function ListMatch({cat, matches, trees}) { +function ListMatch({cat, matches, trees, menuAction}) { const [type, setType] = useState(1); useEffect(() => { @@ -156,7 +218,7 @@ function ListMatch({cat, matches, trees}) { }, [cat]); return
- {cat.type === 3 && <> + {cat && cat.type === 3 && <>
  • m.categorie_ord !== -42) .sort((a, b) => a.categorie_ord - b.categorie_ord) .map(m => ({...m, win: win(m.scores)})) + const match = matches.find(m => m.id === activeMatch) + useEffect(() => { + if (!match) { + publicAffDispatch({type: 'SET_DATA', payload: {c1: undefined, c2: undefined, next: []}}); + } else { + publicAffDispatch({ + type: 'SET_DATA', + payload: {c1: match.c1, c2: match.c2, next: marches2.filter(m => !m.end && m.id !== activeMatch).map(m => ({c1: m.c1, c2: m.c2}))} + }); + } + }, [match]); //useEffect(() => { // if (activeMatch !== null) // setActiveMatch(null); @@ -239,7 +313,7 @@ function MatchList({matches, cat}) {
    - {activeMatch && } + {activeMatch && } } @@ -247,6 +321,24 @@ function BuildTree({treeData, matches}) { const scrollRef = useRef(null) const [currentMatch, setCurrentMatch] = useState(null) const {getComb} = useCombs() + const publicAffDispatch = usePubAffDispatch(); + + const match = matches.find(m => m.id === currentMatch?.matchSelect) + useEffect(() => { + if (!match) { + publicAffDispatch({type: 'SET_DATA', payload: {c1: undefined, c2: undefined}}); + } else { + publicAffDispatch({type: 'SET_DATA', payload: {c1: match.c1, c2: match.c2}}); + } + }, [match]); + const next_match = matches.find(m => m.id === currentMatch?.matchNext) + useEffect(() => { + if (!next_match) { + publicAffDispatch({type: 'SET_DATA', payload: {next: []}}); + } else { + publicAffDispatch({type: 'SET_DATA', payload: {next: [{c1: next_match.c1, c2: next_match.c2}]}}); + } + }, [next_match]); function parseTree(data_in) { if (data_in?.data == null) @@ -292,17 +384,15 @@ function BuildTree({treeData, matches}) { matchSelect={currentMatch?.matchSelect} matchNext={currentMatch?.matchNext} size={23}/>
- {currentMatch?.matchSelect && } + {currentMatch?.matchSelect && }
} -function ScorePanel({matchId, matches}) { +function ScorePanel({matchId, match}) { const {sendRequest} = useWS() const setLoading = useLoadingSwitcher() - const match = matches.find(m => m.id === matchId) - const [end, setEnd] = useState(match?.end || false) const [scoreIn, setScoreIn] = useState("") const inputRef = useRef(null) @@ -382,8 +472,8 @@ function ScorePanel({matchId, matches}) { if (!match || match?.end === end) return; - if (end){ - if (win(match?.scores) === 0 && match.categorie_ord === -42){ + if (end) { + if (win(match?.scores) === 0 && match.categorie_ord === -42) { toast.error("Impossible de terminer un match nul en tournois."); setEnd(false); return; diff --git a/src/main/webapp/src/pages/competition/editor/CompetitionManagerRoot.jsx b/src/main/webapp/src/pages/competition/editor/CompetitionManagerRoot.jsx index b125060..b238e19 100644 --- a/src/main/webapp/src/pages/competition/editor/CompetitionManagerRoot.jsx +++ b/src/main/webapp/src/pages/competition/editor/CompetitionManagerRoot.jsx @@ -74,8 +74,10 @@ function WSStatus({setPerm}) { return

{name}

-
Serveur: +
Serveur:
+
} diff --git a/src/main/webapp/src/pages/competition/editor/PubAffWindow.jsx b/src/main/webapp/src/pages/competition/editor/PubAffWindow.jsx new file mode 100644 index 0000000..64ddb7b --- /dev/null +++ b/src/main/webapp/src/pages/competition/editor/PubAffWindow.jsx @@ -0,0 +1,134 @@ +import {useCombs} from "../../../hooks/useComb.jsx"; +import {usePubAffState} from "../../../hooks/useExternalWindow.jsx"; + +const noMP = {margin: 0, padding: 0}; +const redBackground = "radial-gradient(circle, #C80000FF 0%, #000000FF 100%)" +const blueBackground = "radial-gradient(circle, #0000C8FF 0%, #000000FF 100%)" +const combHeight = "15vh"; +const text1Style = {fontSize: "min(2.25vw, 8vh)", fontWeight: "bold", marginLeft: "0.5em"}; +const text2Style = {fontSize: "min(1.7vw, 7vh)", fontWeight: "bold"}; + +export function PubAffWindow({document}) { + const state = usePubAffState(); + + document.title = "A React portal window" + document.body.className = "bg-dark text-white overflow-hidden"; + + const showScore = false; + + return <> +
+
+
01:30
+ {showScore && +
+
+
0
+
+
+
+
+
0
+
+
} +
+
+
+ +
+
+
+
+ + Actuel + +
+
+ + contre + +
+
+
+
+ + Suivant + +
+
+ + contre + +
+
+ {!showScore &&
+ +
} +
+ +} + +function MatchDisplay({state}) { + const {getComb} = useCombs(); + const combs = state?.next?.slice(1, 6) || []; + + console.log("Rendering MatchDisplay for", combs); + + return
+
+
+
+
+ +
+ {combs.map((match, index) => { + const c1 = getComb(match.c1, ""); + const c2 = getComb(match.c2, ""); + + return
+
+ {c1.fname} {c1.lname} +
+
+ {c2.fname} {c2.lname} +
+ {index !== combs.length - 1 &&
} + +
+ })} +
+
+} + +function CombDisplay({combId, background, children}) { + const {getComb} = useCombs(); + const comb = getComb(combId, ""); + + //console.log("Rendering CombDisplay for", combId, comb); + + return
+ {comb !== "" && <> + {"fr"} +
{comb.fname} {comb.lname}
+ {comb.country} + } +
+ {children} +
+} diff --git a/src/main/webapp/src/utils/copyStyles.js b/src/main/webapp/src/utils/copyStyles.js new file mode 100644 index 0000000..639ec97 --- /dev/null +++ b/src/main/webapp/src/utils/copyStyles.js @@ -0,0 +1,21 @@ +export function copyStyles(sourceDoc, targetDoc) { + Array.from(sourceDoc.styleSheets).forEach(styleSheet => { + if (styleSheet.cssRules) { + // true for inline styles + const newStyleEl = sourceDoc.createElement("style"); + + Array.from(styleSheet.cssRules).forEach(cssRule => { + newStyleEl.appendChild(sourceDoc.createTextNode(cssRule.cssText)); + }); + + targetDoc.head.appendChild(newStyleEl); + } else if (styleSheet.href) { + // true for stylesheets loaded from a URL + const newLinkEl = sourceDoc.createElement("link"); + + newLinkEl.rel = "stylesheet"; + newLinkEl.href = styleSheet.href; + targetDoc.head.appendChild(newLinkEl); + } + }); +}