wip: implement ws base

This commit is contained in:
2025-11-20 23:39:02 +01:00
parent 22fa896ee0
commit a83088387b
17 changed files with 657 additions and 9 deletions

View File

@@ -1,4 +1,4 @@
import {useEffect, useRef} from 'react'
import {lazy, Suspense, useEffect, useRef} from 'react'
import {Nav} from "./components/Nav.jsx";
import {createBrowserRouter, Outlet, RouterProvider, useLocation, useRouteError} from "react-router-dom";
import {Home} from "./pages/Homepage.jsx";
@@ -59,6 +59,11 @@ const router = createBrowserRouter([
element: <ResultRoot/>,
children: getResultChildren()
},
{
path: 'competition-manager/*',
element: <GetCompetitionMangerLazy/>,
//children: getCompetitionManagerChildren()
},
{
path: 'me',
element: <MePage/>
@@ -71,6 +76,17 @@ const router = createBrowserRouter([
}
])
function GetCompetitionMangerLazy() {
const CMLazy = lazy(() => import('./pages/competition/editor/CompetitionManagerRoot.jsx'))
return <Suspense
fallback={<div>
<h1>Compétition manager</h1>
<p>Chargement...</p>
</div>}>
<CMLazy/>
</Suspense>
}
function PageError() {
const error = useRouteError()
return <div style={{textAlign: 'center'}}>

View File

@@ -55,6 +55,7 @@ function CompMenu() {
<ul className="dropdown-menu">
<li className="nav-item"><NavLink className="nav-link" to="/competition">Inscription</NavLink></li>
<li className="nav-item"><NavLink className="nav-link" to="/result">Mes résultats</NavLink></li>
<li className="nav-item"><NavLink className="nav-link" to="/competition-manager">Compétitions Manager</NavLink></li>
</ul>
</li>
}

View File

@@ -0,0 +1,154 @@
import {createContext, useContext, useEffect, useReducer, useRef, useState} from "react";
function uuidv4() {
return "10000000-1000-4000-8000-100000000000".replace(/[018]/g, c =>
(+c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> +c / 4).toString(16)
);
}
const WebsocketContext = createContext([false, () => {
}])
function reducer(state, action) {
switch (action.type) {
case 'addListener':
return {
...state,
listener: [...state.listener, {callback: action.payload.callback, code: action.payload.code}]
}
case 'removeListener':
return {
...state,
listener: state.listener.filter(l => l.callback !== action.payload)
}
case 'clearListeners':
return {
...state,
listener: state.listener.filter(l => l.code !== action.payload)
}
case 'clearAllListeners':
return {
...state,
listener: []
}
default:
return state
}
}
export function WSProvider({url, onmessage, children}) {
const [isReady, setIsReady] = useState(false)
const [state, dispatch] = useReducer(reducer, {listener: []})
const ws = useRef(null)
const listenersRef = useRef([])
const callbackRef = useRef({})
useEffect(() => {
listenersRef.current = state.listener
}, [state.listener])
useEffect(() => {
console.log("WSProvider: connecting to", url);
const socket = new WebSocket(url)
socket.onopen = () => setIsReady(true)
socket.onclose = () => setIsReady(false)
socket.onmessage = (event) => {
const msg = JSON.parse(event.data)
if (msg.type === "REPLY" || msg.type === "ERROR") {
const cb = callbackRef.current[msg.uuid];
if (cb) {
if (msg.type === "REPLY")
cb.onReply(msg.data);
else
cb.onError(msg.data);
delete callbackRef.current[msg.uuid]
}
return;
}
let isHandled = false;
listenersRef.current
.filter(l => l.code === msg.code)
.forEach(l => {
try {
l.callback({...msg})
isHandled = true;
} catch (err) {
console.error("Listener callback error:", err)
}
});
if (!isHandled && onmessage)
onmessage(JSON.parse(event.data))
}
ws.current = socket
return () => {
socket.close()
}
}, [])
const send = (uuid, code, type, data, onReply = () => {
}, onError = () => {
}) => {
if (!isReady) {
onError("WebSocket is not connected");
return;
}
if (type === "REQUEST") {
callbackRef.current[uuid] = {onReply: onReply, onError: onError}
}
ws.current?.send(JSON.stringify({
uuid: uuid,
code: code,
type: type,
data: data
}))
}
const ret = {isReady, dispatch, send, wait_length: callbackRef}
return <WebsocketContext.Provider value={ret}>
{children}
</WebsocketContext.Provider>
}
export function useWS() {
const {isReady, dispatch, send, wait_length} = useContext(WebsocketContext)
return {
dispatch,
isReady,
wait_length,
sendRequest: (code, data) => {
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject("timeout");
}, 1000);
send(uuidv4(), code, "REQUEST", data, (data) => {
clearTimeout(timeout);
resolve(data);
}, (data) => {
clearTimeout(timeout);
reject(data);
});
})
},
sendNotify: (code, data) => {
send(uuidv4(), code, "NOTIFY", data)
},
sendReply: (message, data) => {
send(message.uuid, message.code, "REPLY", data)
},
sendReplyError: (message, data) => {
send(message.uuid, message.code, "ERROR", data)
},
sendError: (data) => {
send(uuidv4(), "error", "ERROR", data)
},
send,
}
}

View File

@@ -0,0 +1,111 @@
import {Route, Routes, useNavigate, useParams} from "react-router-dom";
import {LoadingProvider} from "../../../hooks/useLoading.jsx";
import {useEffect, useState} from "react";
import {useWS, WSProvider} from "../../../hooks/useWS.jsx";
import {ColoredCircle} from "../../../components/ColoredCircle.jsx";
const vite_url = import.meta.env.VITE_URL;
export default function CompetitionManagerRoot() {
return <>
<h1>Compétition manager</h1>
<LoadingProvider>
<Routes>
<Route path="/" element={<Home/>}/>
<Route path="/:compUuid/*" element={<HomeComp/>}/>
</Routes>
</LoadingProvider>
</>
}
function Home() {
const nav = useNavigate();
return <div>
<h2>Home</h2>
<button onClick={() => nav("d3dc76a6-2058-423a-b34b-6d15d7ae5848")}>Go comp</button>
</div>
}
function HomeComp() {
let {compUuid} = useParams();
const [perm, setPerm] = useState("")
const messageHandler = msg => {
console.log("Received WS message:", msg);
}
return <WSProvider url={`${vite_url.replace('http', 'ws')}/api/ws/competition/${compUuid}`} onmessage={messageHandler}>
<WSStatus setPerm={setPerm}/>
<Routes>
<Route path="/" element={<Home2 perm={perm}/>}/>
<Route path="/test" element={<Test2/>}/>
</Routes>
</WSProvider>
}
function WSStatus({setPerm}) {
const [name, setName] = useState("")
const [inWait, setInWait] = useState(false)
const {isReady, wait_length, dispatch} = useWS();
useEffect(() => {
const timer = setInterval(() => {
setInWait(Object.keys(wait_length.current).length > 0);
}, 250);
return () => clearInterval(timer);
}, []);
useEffect(() => {
const welcomeListener = ({data}) => {
setName(data.name)
setPerm(data.perm)
}
dispatch({type: 'addListener', payload: {callback: welcomeListener, code: 'welcomeInfo'}})
return () => dispatch({type: 'removeListener', payload: welcomeListener})
}, [])
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>
</div>
}
function Home2({perm}) {
const nav = useNavigate();
const {sendRequest} = useWS();
return <div className="row">
<h4 className="col-auto" style={{margin: "auto 0"}}>Sélectionne les modes d'affichage</h4>
<div className="col">
{perm === "admin" && <>
<button className="btn btn-primary" onClick={() => nav("admin")}>Administration</button>
<button className="btn btn-primary ms-3" onClick={() => nav("table")}>Table de marque</button>
</>}
{perm === "table" && <>
<button className="btn btn-primary" onClick={() => nav("table")}>Table de marque</button>
</>}
</div>
<button onClick={() => {
sendRequest("getAllMatch", 1156)
.then(data => {
console.log("Received response for getAllMatch:", data);
})
.catch(err => {
console.error("Error in getAllMatch request:", err);
})
}}>Send Test
</button>
<button onClick={() => nav(-1)}>Go Back</button>
<button onClick={() => nav("test")}>Go Test</button>
</div>
}
function Test2() {
let {compUuid} = useParams();
const nav = useNavigate();
return <div>
<h2>Product ID: {compUuid}</h2>
<button onClick={() => nav(-1)}>Go Back</button>
</div>
}

View File

@@ -16,6 +16,7 @@ export default ({mode}) => {
"/api": {
target: process.env.VITE_API_URL,
changeOrigin: true,
ws: true,
},
"/q": {
target: process.env.VITE_API_URL,