wip: public screen

This commit is contained in:
Thibaut Valentin 2025-12-10 00:08:14 +01:00
parent 354fbfede9
commit 09a51edd5f
7 changed files with 327 additions and 42 deletions

View File

@ -130,7 +130,7 @@ public class CompetitionWS {
@OnTextMessage
Multi<MessageOut> 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());

View File

@ -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 <PubAffContext.Provider value={state}>
<PubAffDispatchContext.Provider value={dispatch}>
{children}
</PubAffDispatchContext.Provider>
</PubAffContext.Provider>
}
export function usePubAffState() {
return useContext(PubAffContext)
}
export function usePubAffDispatch() {
return useContext(PubAffDispatchContext)
}

View File

@ -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)
}
}

View File

@ -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 <img decoding="async" loading="lazy" width={"16"} height={"16"} className="wp-image-1635"
@ -18,33 +23,90 @@ function CupImg() {
export function CMTable() {
const [catId, setCatId] = useState(-1);
const menuAction = useRef({});
return <div className="text-center">
<div className="row">
<div className="col-md-12 col-lg">
<div style={{backgroundColor: "#00c700"}}>
A
</div>
<div style={{backgroundColor: "#0099c7"}}>
B
</div>
</div>
<div className="col-md-12 col-xl-6 col-xxl-5">
<div className="card">
<div className="card-header">Matches</div>
<div className="card-body">
<CategorieSelect catId={catId} setCatId={setCatId}/>
return <PubAffProvider>
<div className="text-center">
<div className="row">
<div className="col-md-12 col-lg">
<div style={{backgroundColor: "#00c700"}}>
A
</div>
<div style={{backgroundColor: "#0099c7"}}>
B
</div>
</div>
<div style={{backgroundColor: "#c70000"}}>
D
<div className="col-md-12 col-xl-6 col-xxl-5">
<div className="card">
<div className="card-header">Matches</div>
<div className="card-body">
<CategorieSelect catId={catId} setCatId={setCatId} menuAction={menuAction}/>
</div>
</div>
<div style={{backgroundColor: "#c70000"}}>
D
</div>
</div>
</div>
<Menu menuAction={menuAction}/>
</div>
</div>
</PubAffProvider>
}
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(
<>
<div className="vr" style={{margin: "0 0.5em", height: "100%"}}></div>
<FontAwesomeIcon icon={faDisplay} size="xl" style={{color: showPubAff ? "#00c700" : "#6c757d", cursor: "pointer"}}
onClick={handlePubAff}/>
</>, document.getElementById("actionMenu"))}
{externalWindow.current && createPortal(<PubAffWindow document={externalWindow.current.document}/>, 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}) {
<option key={c.id} value={c.id}>{c.name}</option>))}
</select>
</div>
{catId !== -1 && <MatchPanel catId={catId} cat={cat}/>}
{catId !== -1 && <MatchPanel catId={catId} cat={cat} menuAction={menuAction}/>}
</>
}
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 <ListMatch cat={cat} matches={matches} trees={trees}/>
return <ListMatch cat={cat} matches={matches} trees={trees} menuAction={menuAction}/>
}
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 <div style={{marginTop: "1em"}}>
{cat.type === 3 && <>
{cat && cat.type === 3 && <>
<ul className="nav nav-tabs">
<li className="nav-item">
<div className={"nav-link" + (type === 1 ? " active" : "")} aria-current={(type === 1 ? " page" : "false")}
@ -184,12 +246,24 @@ function ListMatch({cat, matches, trees}) {
function MatchList({matches, cat}) {
const [activeMatch, setActiveMatch] = useState(null)
const publicAffDispatch = usePubAffDispatch();
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: 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}) {
</table>
</div>
{activeMatch && <LoadingProvider><ScorePanel matchId={activeMatch} matches={matches}/></LoadingProvider>}
{activeMatch && <LoadingProvider><ScorePanel matchId={activeMatch} match={match}/></LoadingProvider>}
</>
}
@ -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}/>
</div>
{currentMatch?.matchSelect && <LoadingProvider><ScorePanel matchId={currentMatch?.matchSelect} matches={matches}/></LoadingProvider>}
{currentMatch?.matchSelect && <LoadingProvider><ScorePanel matchId={currentMatch?.matchSelect} match={match}/></LoadingProvider>}
</div>
}
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;

View File

@ -74,8 +74,10 @@ function WSStatus({setPerm}) {
return <div className="row">
<h2 className="col">{name}</h2>
<div className="col-auto" style={{margin: "auto 0"}}>Serveur: <ColoredCircle color={isReady ? (inWait ? "#ffad32" : "#00c700") : "#e50000"}/>
<div className="col-auto" style={{margin: "auto 0", padding: 0}}>Serveur: <ColoredCircle
color={isReady ? (inWait ? "#ffad32" : "#00c700") : "#e50000"}/>
</div>
<div className="col-auto " id="actionMenu" style={{verticalAlign: "center", textAlign: "center", margin: "auto 0", padding: 0}}></div>
</div>
}

View File

@ -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 <>
<div className="row text-center"
style={{background: "linear-gradient(to bottom, #000000, #323232)", height: `calc(100vh - ${combHeight} * 2)`, ...noMP}}>
<div>
<div style={{fontSize: "30vh", lineHeight: "30vh"}}>01:30</div>
{showScore &&
<div className="row" style={noMP}>
<div className="col-4" style={noMP}>
<div style={{fontSize: "30vh", lineHeight: "30vh", color: "#ff1414"}}>0</div>
</div>
<div className="col-4" style={noMP}>
</div>
<div className="col-4" style={noMP}>
<div style={{fontSize: "30vh", lineHeight: "30vh", color: "#14adff"}}>0</div>
</div>
</div>}
</div>
</div>
<div className="row fixed-bottom text-center" style={noMP}>
</div>
<div className="fixed-bottom text-center" style={noMP}>
<div className="row" style={noMP}>
<div className="col" style={noMP}>
<CombDisplay combId={state?.c1} background={"red"}>
<span className="position-absolute top-0 start-0 translate-middle-y" style={text1Style}>Actuel</span>
</CombDisplay>
</div>
<div className="col" style={noMP}>
<CombDisplay combId={state?.c2} background={"blue"}>
<span className="position-absolute bottom-0 start-0 translate-middle-x" style={text2Style}>contre</span>
</CombDisplay>
</div>
</div>
<div className="row" style={noMP}>
<div className="col" style={noMP}>
<CombDisplay combId={state?.next?.[0]?.c1} background={"red"}>
<span className="position-absolute top-0 start-0 translate-middle-y" style={text1Style}>Suivant</span>
</CombDisplay>
</div>
<div className="col" style={noMP}>
<CombDisplay combId={state?.next?.[0]?.c2} background={"blue"}>
<span className="position-absolute bottom-0 start-0 translate-middle-x" style={text2Style}>contre</span>
</CombDisplay>
</div>
</div>
{!showScore && <div className="row" style={noMP}>
<MatchDisplay state={state}/>
</div>}
</div>
</>
}
function MatchDisplay({state}) {
const {getComb} = useCombs();
const combs = state?.next?.slice(1, 6) || [];
console.log("Rendering MatchDisplay for", combs);
return <div className="col-12 position-relative" style={{height: `calc(${combHeight} * 2)`}}>
<div className="position-absolute bottom-0 start-0" style={{height: "100%", background: redBackground, width: "50vw"}}/>
<div className="position-absolute bottom-0 start-50" style={{height: "100%", background: blueBackground, width: "50vw"}}/>
<div className="position-absolute top-0 start-0 w-100" style={{...noMP, height: "0.4vh", backgroundColor: "#646464AA"}}/>
<div className="position-absolute top-0 start-50" style={{
...noMP,
height: `calc(${combHeight} * 1.5)`,
width: "0.4vh",
backgroundColor: "#646464AA",
margin: `calc(${combHeight} * 0.25) 0`
}}/>
<div className="position-relative" style={{marginTop: `1vh`, color: "#dcdcdc"}}>
{combs.map((match, index) => {
const c1 = getComb(match.c1, "");
const c2 = getComb(match.c2, "");
return <div key={index} className="row" style={noMP}>
<div className="col" style={{fontSize: `3vh`, margin: "0.5vh 1vw"}}>
{c1.fname} {c1.lname}
</div>
<div className="col" style={{fontSize: `3vh`}}>
{c2.fname} {c2.lname}
</div>
{index !== combs.length - 1 && <div className="w-75" style={{...noMP, height: "0.2vh", margin: "0 12.5vw", backgroundColor: "#646464AA"}}/>}
</div>
})}
</div>
</div>
}
function CombDisplay({combId, background, children}) {
const {getComb} = useCombs();
const comb = getComb(combId, "");
//console.log("Rendering CombDisplay for", combId, comb);
return <div className="col position-relative"
style={{
height: combHeight,
background: background === "red" ? redBackground : blueBackground,
display: "flex",
flexDirection: background === "red" ? "row" : "row-reverse",
justifyContent: "space-between",
alignItems: "center",
}}>
{comb !== "" && <>
<img src={`/flags/svg/ad.svg`} alt={"fr"} style={{width: "6vw", height: "min(9vh, 6vw)", objectFit: "contain", margin: "0 .5vw"}}/>
<div style={{fontSize: "min(3.5vw, 10vh)"}}>{comb.fname} {comb.lname}</div>
<img src={`/flags/svg/${comb.country.toLowerCase()}.svg`} alt={comb.country}
style={{width: "4vw", height: "8vh", objectFit: "contain", margin: "0 1.25vw"}}/>
</>}
<div className="position-absolute top-0 start-0 w-100" style={{...noMP, height: "0.4vh", backgroundColor: "#646464AA"}}/>
{children}
</div>
}

View File

@ -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);
}
});
}