diff --git a/src/main/webapp/src/components/Nav.jsx b/src/main/webapp/src/components/Nav.jsx
index 1230802..0410cb4 100644
--- a/src/main/webapp/src/components/Nav.jsx
+++ b/src/main/webapp/src/components/Nav.jsx
@@ -55,6 +55,7 @@ function CompMenu() {
- Inscription
- Mes résultats
+ - Compétitions Manager
}
diff --git a/src/main/webapp/src/hooks/useWS.jsx b/src/main/webapp/src/hooks/useWS.jsx
new file mode 100644
index 0000000..e475704
--- /dev/null
+++ b/src/main/webapp/src/hooks/useWS.jsx
@@ -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
+ {children}
+
+}
+
+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,
+ }
+}
diff --git a/src/main/webapp/src/pages/competition/editor/CompetitionManagerRoot.jsx b/src/main/webapp/src/pages/competition/editor/CompetitionManagerRoot.jsx
new file mode 100644
index 0000000..1b2f7d4
--- /dev/null
+++ b/src/main/webapp/src/pages/competition/editor/CompetitionManagerRoot.jsx
@@ -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 <>
+
Compétition manager
+
+
+ }/>
+ }/>
+
+
+ >
+}
+
+function Home() {
+ const nav = useNavigate();
+ return
+
Home
+
+
+}
+
+function HomeComp() {
+ let {compUuid} = useParams();
+ const [perm, setPerm] = useState("")
+
+ const messageHandler = msg => {
+ console.log("Received WS message:", msg);
+ }
+
+ return
+
+
+ }/>
+ }/>
+
+
+}
+
+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
+}
+
+function Home2({perm}) {
+ const nav = useNavigate();
+ const {sendRequest} = useWS();
+
+ return
+
Sélectionne les modes d'affichage
+
+ {perm === "admin" && <>
+
+
+ >}
+ {perm === "table" && <>
+
+ >}
+
+
+
+
+
+}
+
+function Test2() {
+ let {compUuid} = useParams();
+ const nav = useNavigate();
+ return
+
Product ID: {compUuid}
+
+
+}
diff --git a/src/main/webapp/vite.config.js b/src/main/webapp/vite.config.js
index 6b5b768..7233a9d 100644
--- a/src/main/webapp/vite.config.js
+++ b/src/main/webapp/vite.config.js
@@ -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,