@@ -127,25 +179,44 @@ export function SoloScreen({
{/* Bottom bar */}
- Trouver
- {puzzle?.target}
+
+ Trouver
+
+
+ {puzzle?.target}
+
- Temps
- {elapsedDisplay}
+
+ Temps
+
+
+ {elapsedDisplay}
+
- Clics
- {clicks}
+
+ Clics
+
+
+ {clicks}
+
{canGoBack && (
-
diff --git a/app/mentions-legales/page.tsx b/app/mentions-legales/page.tsx
index b419a73..410c8ba 100644
--- a/app/mentions-legales/page.tsx
+++ b/app/mentions-legales/page.tsx
@@ -64,8 +64,8 @@ export default function MentionsLegalesPage() {
SAPINET
- SIREN : 899 483 457 (RCS
- de Nanterre)
+ SIREN : 899 483 457 (RCS de
+ Nanterre)
Adresse : 65 rue de la
diff --git a/app/page.tsx b/app/page.tsx
index 2b67fae..b293fc1 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -22,20 +22,49 @@ export default function WikiRush() {
const solo = useSoloGame();
const multi = useMultiGame();
- const { session } = useGameEffects({ solo, multi, screen, setScreen, setPlayerName });
- const handlers = useGameHandlers({ solo, multi, playerName, joinCode, maxPlayers, totalRounds, gameMode, setScreen, setError, setLoading });
+ const { session } = useGameEffects({
+ solo,
+ multi,
+ screen,
+ setScreen,
+ setPlayerName,
+ });
+ const handlers = useGameHandlers({
+ solo,
+ multi,
+ playerName,
+ joinCode,
+ maxPlayers,
+ totalRounds,
+ gameMode,
+ setScreen,
+ setError,
+ setLoading,
+ });
return (
);
}
diff --git a/lib/session.ts b/lib/session.ts
index b3dfe39..32edcf0 100644
--- a/lib/session.ts
+++ b/lib/session.ts
@@ -17,7 +17,9 @@ export type SessionData = {
export function saveSession(data: SessionData) {
try {
sessionStorage.setItem(KEY, JSON.stringify(data));
- } catch { /* ignore quota */ }
+ } catch {
+ /* ignore quota */
+ }
}
export function loadSession(): SessionData | null {
@@ -30,5 +32,9 @@ export function loadSession(): SessionData | null {
}
export function clearSession() {
- try { sessionStorage.removeItem(KEY); } catch { /* ignore */ }
+ try {
+ sessionStorage.removeItem(KEY);
+ } catch {
+ /* ignore */
+ }
}
diff --git a/lib/types.ts b/lib/types.ts
index d2c90f4..c40da75 100644
--- a/lib/types.ts
+++ b/lib/types.ts
@@ -1,4 +1,12 @@
-export type Screen = "home" | "lobby" | "game" | "solo" | "profile" | "leaderboard" | "daily" | "blitz";
+export type Screen =
+ | "home"
+ | "lobby"
+ | "game"
+ | "solo"
+ | "profile"
+ | "leaderboard"
+ | "daily"
+ | "blitz";
export type WikiArticle = {
title: string;
diff --git a/lib/useBlitzGame.ts b/lib/useBlitzGame.ts
index 56794fd..74e89dc 100644
--- a/lib/useBlitzGame.ts
+++ b/lib/useBlitzGame.ts
@@ -16,7 +16,10 @@ export function useBlitzGame() {
const [timeLeft, setTimeLeft] = useState(BLITZ_DURATION);
const [clicks, setClicks] = useState(0);
const [history, setHistory] = useState([]);
- const [puzzle, setPuzzle] = useState<{ start: string; target: string } | null>(null);
+ const [puzzle, setPuzzle] = useState<{
+ start: string;
+ target: string;
+ } | null>(null);
const clicksRef = useRef(0);
const pathRef = useRef([]);
@@ -26,7 +29,10 @@ export function useBlitzGame() {
const startTimeRef = useRef(0);
function stopTimer() {
- if (intervalRef.current) { clearInterval(intervalRef.current); intervalRef.current = null; }
+ if (intervalRef.current) {
+ clearInterval(intervalRef.current);
+ intervalRef.current = null;
+ }
}
function startTimer() {
@@ -46,7 +52,12 @@ export function useBlitzGame() {
useEffect(() => () => stopTimer(), []);
- async function saveGame(won: boolean, path: string[], clicks: number, timeSeconds: number) {
+ async function saveGame(
+ won: boolean,
+ path: string[],
+ clicks: number,
+ timeSeconds: number,
+ ) {
if (!puzzle) return;
fetch("/api/games", {
method: "POST",
@@ -70,7 +81,10 @@ export function useBlitzGame() {
const art = await fetchArticle(t);
setLoading(false);
loadingRef.current = false;
- if (!art) { setLoadError(`Impossible de charger "${t}".`); return null; }
+ if (!art) {
+ setLoadError(`Impossible de charger "${t}".`);
+ return null;
+ }
setHtml(art.html);
setTitle(art.title);
return art.title;
@@ -79,8 +93,10 @@ export function useBlitzGame() {
async function start() {
setLoading(true);
stopTimer();
- clicksRef.current = 0; setClicks(0);
- pathRef.current = []; setHistory([]);
+ clicksRef.current = 0;
+ setClicks(0);
+ pathRef.current = [];
+ setHistory([]);
endedRef.current = false;
setTimeLeft(BLITZ_DURATION);
setLoadError(null);
@@ -96,34 +112,43 @@ export function useBlitzGame() {
startTimer();
}
- const navigate = useCallback(async (t: string) => {
- if (loadingRef.current || endedRef.current) return;
- clicksRef.current += 1;
- setClicks(clicksRef.current);
+ const navigate = useCallback(
+ async (t: string) => {
+ if (loadingRef.current || endedRef.current) return;
+ clicksRef.current += 1;
+ setClicks(clicksRef.current);
- const canonical = await loadArticle(t);
- if (!canonical) return;
+ const canonical = await loadArticle(t);
+ if (!canonical) return;
- const newPath = [...pathRef.current, canonical];
- pathRef.current = newPath;
- setHistory(newPath);
- window.scrollTo({ top: 0, behavior: "smooth" });
+ const newPath = [...pathRef.current, canonical];
+ pathRef.current = newPath;
+ setHistory(newPath);
+ window.scrollTo({ top: 0, behavior: "smooth" });
- if (puzzle && normalizeTitle(canonical) === normalizeTitle(puzzle.target)) {
- endedRef.current = true;
- stopTimer();
- setPhase("won");
- saveGame(true, newPath, clicksRef.current, timeLeft);
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [puzzle, timeLeft]);
+ if (
+ puzzle &&
+ normalizeTitle(canonical) === normalizeTitle(puzzle.target)
+ ) {
+ endedRef.current = true;
+ stopTimer();
+ setPhase("won");
+ saveGame(true, newPath, clicksRef.current, timeLeft);
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ },
+ [puzzle, timeLeft],
+ );
function reset() {
stopTimer();
endedRef.current = false;
- clicksRef.current = 0; setClicks(0);
- pathRef.current = []; setHistory([]);
- setHtml(""); setTitle("");
+ clicksRef.current = 0;
+ setClicks(0);
+ pathRef.current = [];
+ setHistory([]);
+ setHtml("");
+ setTitle("");
setTimeLeft(BLITZ_DURATION);
setPuzzle(null);
setPhase("setup");
@@ -131,10 +156,19 @@ export function useBlitzGame() {
}
return {
- phase, puzzle, html, title, loading, loadError,
- timeLeft, clicks, history,
+ phase,
+ puzzle,
+ html,
+ title,
+ loading,
+ loadError,
+ timeLeft,
+ clicks,
+ history,
canGoBack: false, // pas de retour en blitz
- start, navigate, reset,
+ start,
+ navigate,
+ reset,
retryLoad: () => title && loadArticle(title),
};
}
diff --git a/lib/useDailyGame.ts b/lib/useDailyGame.ts
index 87913a6..d4c7c1e 100644
--- a/lib/useDailyGame.ts
+++ b/lib/useDailyGame.ts
@@ -4,7 +4,12 @@ import { useState, useCallback, useRef, useEffect } from "react";
import { fetchArticle, normalizeTitle } from "./wiki";
import { useTimer } from "./useTimer";
-export type DailyPhase = "loading" | "playing" | "won" | "gave_up" | "already_played";
+export type DailyPhase =
+ | "loading"
+ | "playing"
+ | "won"
+ | "gave_up"
+ | "already_played";
export type DailyPuzzleInfo = {
id: string;
@@ -42,22 +47,31 @@ export function useDailyGame() {
useEffect(() => {
fetch("/api/daily")
.then((r) => r.json())
- .then(async (data: { puzzle: DailyPuzzleInfo; alreadyPlayed: boolean; myResult: DailyResult | null }) => {
- setPuzzle(data.puzzle);
- if (data.alreadyPlayed && data.myResult) {
- setMyResult(data.myResult);
- setPhase("already_played");
- return;
- }
- // Charger l'article de départ
- const art = await fetchArticle(data.puzzle.startArticle);
- if (!art) { setLoadError("Impossible de charger l'article de départ."); return; }
- setHtml(art.html);
- setTitle(art.title);
- pathRef.current = [art.title];
- setHistory([art.title]);
- setPhase("playing");
- })
+ .then(
+ async (data: {
+ puzzle: DailyPuzzleInfo;
+ alreadyPlayed: boolean;
+ myResult: DailyResult | null;
+ }) => {
+ setPuzzle(data.puzzle);
+ if (data.alreadyPlayed && data.myResult) {
+ setMyResult(data.myResult);
+ setPhase("already_played");
+ return;
+ }
+ // Charger l'article de départ
+ const art = await fetchArticle(data.puzzle.startArticle);
+ if (!art) {
+ setLoadError("Impossible de charger l'article de départ.");
+ return;
+ }
+ setHtml(art.html);
+ setTitle(art.title);
+ pathRef.current = [art.title];
+ setHistory([art.title]);
+ setPhase("playing");
+ },
+ )
.catch(() => setLoadError("Impossible de charger le défi du jour."));
}, []);
@@ -68,7 +82,10 @@ export function useDailyGame() {
const art = await fetchArticle(t);
setLoading(false);
loadingRef.current = false;
- if (!art) { setLoadError(`Impossible de charger "${t}".`); return null; }
+ if (!art) {
+ setLoadError(`Impossible de charger "${t}".`);
+ return null;
+ }
setHtml(art.html);
setTitle(art.title);
return art.title;
@@ -86,44 +103,65 @@ export function useDailyGame() {
await fetch("/api/daily", {
method: "POST",
headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ puzzleId: puzzle.id, ...result, path: result.path }),
+ body: JSON.stringify({
+ puzzleId: puzzle.id,
+ ...result,
+ path: result.path,
+ }),
}).catch(() => {});
}
- const navigate = useCallback(async (t: string) => {
- if (loadingRef.current || gameEndedRef.current) return;
- clicksRef.current += 1;
- setClicks(clicksRef.current);
- if (!timerStartedRef.current) { timer.start(); timerStartedRef.current = true; }
+ const navigate = useCallback(
+ async (t: string) => {
+ if (loadingRef.current || gameEndedRef.current) return;
+ clicksRef.current += 1;
+ setClicks(clicksRef.current);
+ if (!timerStartedRef.current) {
+ timer.start();
+ timerStartedRef.current = true;
+ }
- const canonical = await loadArticle(t);
- if (!canonical) return;
- const newPath = [...pathRef.current, canonical];
- pathRef.current = newPath;
- setHistory(newPath);
- window.scrollTo({ top: 0, behavior: "smooth" });
+ const canonical = await loadArticle(t);
+ if (!canonical) return;
+ const newPath = [...pathRef.current, canonical];
+ pathRef.current = newPath;
+ setHistory(newPath);
+ window.scrollTo({ top: 0, behavior: "smooth" });
- if (puzzle && normalizeTitle(canonical) === normalizeTitle(puzzle.targetArticle)) {
- timer.stop();
- gameEndedRef.current = true;
- setPhase("won");
- await submitResult(true);
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [puzzle]);
+ if (
+ puzzle &&
+ normalizeTitle(canonical) === normalizeTitle(puzzle.targetArticle)
+ ) {
+ timer.stop();
+ gameEndedRef.current = true;
+ setPhase("won");
+ await submitResult(true);
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ },
+ [puzzle],
+ );
const goBack = useCallback(async () => {
- if (loadingRef.current || gameEndedRef.current || pathRef.current.length <= 1) return;
+ if (
+ loadingRef.current ||
+ gameEndedRef.current ||
+ pathRef.current.length <= 1
+ )
+ return;
clicksRef.current += 1;
setClicks(clicksRef.current);
- if (!timerStartedRef.current) { timer.start(); timerStartedRef.current = true; }
+ if (!timerStartedRef.current) {
+ timer.start();
+ timerStartedRef.current = true;
+ }
const newPath = pathRef.current.slice(0, -1);
const canonical = await loadArticle(newPath[newPath.length - 1]);
if (!canonical) return;
pathRef.current = newPath;
setHistory(newPath);
window.scrollTo({ top: 0, behavior: "smooth" });
- // eslint-disable-next-line react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
async function giveUp() {
@@ -134,11 +172,20 @@ export function useDailyGame() {
}
return {
- phase, puzzle, myResult,
- html, title, loading, loadError,
- history, clicks, elapsed: timer.elapsed,
+ phase,
+ puzzle,
+ myResult,
+ html,
+ title,
+ loading,
+ loadError,
+ history,
+ clicks,
+ elapsed: timer.elapsed,
canGoBack: pathRef.current.length > 1,
- navigate, goBack, giveUp,
+ navigate,
+ goBack,
+ giveUp,
retryLoad: () => title && loadArticle(title),
};
}
diff --git a/lib/useGameEffects.ts b/lib/useGameEffects.ts
index aaaf380..fcf41e3 100644
--- a/lib/useGameEffects.ts
+++ b/lib/useGameEffects.ts
@@ -37,21 +37,35 @@ export function useGameEffects({
const saved = loadSession();
if (!saved) return;
- if (saved.screen === "solo" && saved.soloPuzzle && saved.soloHistory?.length) {
+ if (
+ saved.screen === "solo" &&
+ saved.soloPuzzle &&
+ saved.soloHistory?.length
+ ) {
setScreen("solo");
- solo.restore(saved.soloPuzzle, saved.soloHistory, saved.soloClicks ?? 0)
- .then((ok) => { if (!ok) { clearSession(); setScreen("home"); } });
+ solo
+ .restore(saved.soloPuzzle, saved.soloHistory, saved.soloClicks ?? 0)
+ .then((ok) => {
+ if (!ok) {
+ clearSession();
+ setScreen("home");
+ }
+ });
} else if (
(saved.screen === "lobby" || saved.screen === "game") &&
- saved.multiRoomCode && saved.multiPlayerId
+ saved.multiRoomCode &&
+ saved.multiPlayerId
) {
if (saved.playerName) setPlayerName(saved.playerName);
multi.restore(saved.multiRoomCode, saved.multiPlayerId).then((ok) => {
if (ok) setScreen("lobby");
- else { clearSession(); setScreen("home"); }
+ else {
+ clearSession();
+ setScreen("home");
+ }
});
}
- // eslint-disable-next-line react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// Bloquer le bouton retour navigateur
diff --git a/lib/useGameHandlers.ts b/lib/useGameHandlers.ts
index a4c4800..e4647d3 100644
--- a/lib/useGameHandlers.ts
+++ b/lib/useGameHandlers.ts
@@ -20,29 +20,64 @@ type Handlers = {
};
export function useGameHandlers({
- solo, multi, playerName, joinCode, maxPlayers, totalRounds, gameMode, setScreen, setError, setLoading,
+ solo,
+ multi,
+ playerName,
+ joinCode,
+ maxPlayers,
+ totalRounds,
+ gameMode,
+ setScreen,
+ setError,
+ setLoading,
}: Handlers) {
async function handleCreateRoom() {
- if (!playerName.trim()) { setError("Entre ton pseudo !"); return; }
- setLoading(true); setError(null);
- const { error: err } = await multi.createRoom(playerName.trim(), maxPlayers, totalRounds, gameMode);
+ if (!playerName.trim()) {
+ setError("Entre ton pseudo !");
+ return;
+ }
+ setLoading(true);
+ setError(null);
+ const { error: err } = await multi.createRoom(
+ playerName.trim(),
+ maxPlayers,
+ totalRounds,
+ gameMode,
+ );
setLoading(false);
- if (err) { setError(err); return; }
+ if (err) {
+ setError(err);
+ return;
+ }
setScreen("lobby");
}
async function handleJoinRoom() {
- if (!playerName.trim()) { setError("Entre ton pseudo !"); return; }
- if (joinCode.trim().length !== 4) { setError("Le code doit faire 4 lettres"); return; }
- setLoading(true); setError(null);
- const { error: err } = await multi.joinRoom(playerName.trim(), joinCode.trim().toUpperCase());
+ if (!playerName.trim()) {
+ setError("Entre ton pseudo !");
+ return;
+ }
+ if (joinCode.trim().length !== 4) {
+ setError("Le code doit faire 4 lettres");
+ return;
+ }
+ setLoading(true);
+ setError(null);
+ const { error: err } = await multi.joinRoom(
+ playerName.trim(),
+ joinCode.trim().toUpperCase(),
+ );
setLoading(false);
- if (err) { setError(err); return; }
+ if (err) {
+ setError(err);
+ return;
+ }
setScreen("lobby");
}
async function handleStartGame() {
- setLoading(true); setError(null);
+ setLoading(true);
+ setError(null);
const { error: err } = await multi.startGame();
setLoading(false);
if (err) setError(err);
@@ -50,12 +85,12 @@ export function useGameHandlers({
async function handleNextRound() {
await multi.nextRound();
- // On reste sur "game" â le GameScreen affiche le lobby quand phase === "waiting"
+ // On reste sur "game" - le GameScreen affiche le lobby quand phase === "waiting"
}
async function handleResetGame() {
await multi.resetGame();
- // On reste sur "game" â le GameScreen affiche le lobby quand phase === "waiting"
+ // On reste sur "game" - le GameScreen affiche le lobby quand phase === "waiting"
}
function handleLeave() {
diff --git a/lib/useMultiGame.ts b/lib/useMultiGame.ts
index 115df23..e2e66a6 100644
--- a/lib/useMultiGame.ts
+++ b/lib/useMultiGame.ts
@@ -1,7 +1,13 @@
"use client";
import { useState, useCallback, useEffect, useRef } from "react";
-import { fetchArticle, pickTwoArticles, prefetchArticle, POLL_INTERVAL, COUNTDOWN_DURATION } from "./wiki";
+import {
+ fetchArticle,
+ pickTwoArticles,
+ prefetchArticle,
+ POLL_INTERVAL,
+ COUNTDOWN_DURATION,
+} from "./wiki";
import { useTimer } from "./useTimer";
import { saveSession, clearSession } from "./session";
import type { Room } from "../app/api/rooms/route";
@@ -32,11 +38,18 @@ export function useMultiGame() {
// Article loading
async function loadArticle(t: string): Promise {
- setLoading(true); loadingRef.current = true; setLoadError(null);
+ setLoading(true);
+ loadingRef.current = true;
+ setLoadError(null);
const art = await fetchArticle(t);
- setLoading(false); loadingRef.current = false;
- if (!art) { setLoadError(`Impossible de charger "${t}".`); return null; }
- setHtml(art.html); setTitle(art.title);
+ setLoading(false);
+ loadingRef.current = false;
+ if (!art) {
+ setLoadError(`Impossible de charger "${t}".`);
+ return null;
+ }
+ setHtml(art.html);
+ setTitle(art.title);
return art.title;
}
@@ -53,14 +66,20 @@ export function useMultiGame() {
}
function stopCountdown() {
- if (countdownRef.current) { clearInterval(countdownRef.current); countdownRef.current = null; }
+ if (countdownRef.current) {
+ clearInterval(countdownRef.current);
+ countdownRef.current = null;
+ }
setCountdown(null);
}
// Polling
function stopPolling() {
- if (pollRef.current) { clearInterval(pollRef.current); pollRef.current = null; }
+ if (pollRef.current) {
+ clearInterval(pollRef.current);
+ pollRef.current = null;
+ }
}
async function poll(code: string, pid: string) {
@@ -70,8 +89,10 @@ export function useMultiGame() {
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ action: "heartbeat", playerId: pid }),
});
- if (res.ok) setRoom((await res.json() as { room: Room }).room);
- } catch { /* ignore */ }
+ if (res.ok) setRoom(((await res.json()) as { room: Room }).room);
+ } catch {
+ /* ignore */
+ }
}
const pollCodeRef = useRef(null);
@@ -87,14 +108,18 @@ export function useMultiGame() {
// Relance le polling quand le tab redevient visible (les setInterval sont throttlés en arriÚre-plan)
useEffect(() => {
function onVisible() {
- if (document.visibilityState === "visible" && pollCodeRef.current && pollPidRef.current) {
+ if (
+ document.visibilityState === "visible" &&
+ pollCodeRef.current &&
+ pollPidRef.current
+ ) {
poll(pollCodeRef.current, pollPidRef.current);
startPolling(pollCodeRef.current, pollPidRef.current);
}
}
document.addEventListener("visibilitychange", onVisible);
return () => document.removeEventListener("visibilitychange", onVisible);
- // eslint-disable-next-line react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// Phase sync
@@ -107,8 +132,10 @@ export function useMultiGame() {
prevRoundRef.current = room.round;
if (room.phase === "countdown" && prevPhase !== "countdown") {
- setHtml(""); setLoadError(null);
- clicksRef.current = 0; setClicksDisplay(0);
+ setHtml("");
+ setLoadError(null);
+ clicksRef.current = 0;
+ setClicksDisplay(0);
timerStartedRef.current = false;
timer.reset();
startCountdown(room.countdownStart ?? Date.now());
@@ -125,7 +152,7 @@ export function useMultiGame() {
if (room.round !== prevRound && room.phase === "playing") {
loadArticle(room.startArticle);
}
- // eslint-disable-next-line react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [room]);
// Countdown -> playing transition
@@ -136,72 +163,117 @@ export function useMultiGame() {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ action: "play", playerId }),
- }).then((r) => r.json()).then((d) => {
- if ((d as { room: Room }).room) setRoom((d as { room: Room }).room);
- }).catch(() => {});
+ })
+ .then((r) => r.json())
+ .then((d) => {
+ if ((d as { room: Room }).room) setRoom((d as { room: Room }).room);
+ })
+ .catch(() => {});
}
}, [countdown, room, playerId]);
// Navigation
- const navigate = useCallback(async (t: string) => {
- if (!room || !playerId || loadingRef.current || room.phase !== "playing") return;
- clicksRef.current += 1; setClicksDisplay(clicksRef.current);
- if (!timerStartedRef.current) { timer.start(); timerStartedRef.current = true; }
+ const navigate = useCallback(
+ async (t: string) => {
+ if (!room || !playerId || loadingRef.current || room.phase !== "playing")
+ return;
+ clicksRef.current += 1;
+ setClicksDisplay(clicksRef.current);
+ if (!timerStartedRef.current) {
+ timer.start();
+ timerStartedRef.current = true;
+ }
- // Heartbeat optimiste pour éviter le kick pendant le chargement de l'article
- fetch(`/api/rooms/${room.code}`, {
- method: "PATCH",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ action: "heartbeat", playerId }),
- }).catch(() => {});
-
- const canonical = await loadArticle(t);
- if (!canonical) return;
-
- const newHistory = [...historyRef.current, canonical];
- historyRef.current = newHistory;
- setHistory(newHistory);
- window.scrollTo({ top: 0, behavior: "smooth" });
-
- try {
- const res = await fetch(`/api/rooms/${room.code}`, {
+ // Heartbeat optimiste pour éviter le kick pendant le chargement de l'article
+ fetch(`/api/rooms/${room.code}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ action: "navigate", playerId, article: canonical }),
- });
- if (res.ok) setRoom((await res.json() as { room: Room }).room);
- } catch { /* on continue localement */ }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [room, playerId]);
+ body: JSON.stringify({ action: "heartbeat", playerId }),
+ }).catch(() => {});
+
+ const canonical = await loadArticle(t);
+ if (!canonical) return;
+
+ const newHistory = [...historyRef.current, canonical];
+ historyRef.current = newHistory;
+ setHistory(newHistory);
+ window.scrollTo({ top: 0, behavior: "smooth" });
+
+ try {
+ const res = await fetch(`/api/rooms/${room.code}`, {
+ method: "PATCH",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ action: "navigate",
+ playerId,
+ article: canonical,
+ }),
+ });
+ if (res.ok) setRoom(((await res.json()) as { room: Room }).room);
+ } catch {
+ /* on continue localement */
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ },
+ [room, playerId],
+ );
// Room actions
- async function createRoom(playerName: string, maxPlayers = 16, totalRounds = 3, gameMode: "race" | "all_finish" = "race"): Promise<{ error?: string }> {
+ async function createRoom(
+ playerName: string,
+ maxPlayers = 16,
+ totalRounds = 3,
+ gameMode: "race" | "all_finish" = "race",
+ ): Promise<{ error?: string }> {
const res = await fetch("/api/rooms", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ playerName, maxPlayers, totalRounds, gameMode }),
});
- const data = await res.json() as { room?: Room; playerId?: string; error?: string };
+ const data = (await res.json()) as {
+ room?: Room;
+ playerId?: string;
+ error?: string;
+ };
if (!res.ok) return { error: data.error ?? "Erreur" };
- setRoom(data.room!); setPlayerId(data.playerId!);
+ setRoom(data.room!);
+ setPlayerId(data.playerId!);
startPolling(data.room!.code, data.playerId!);
- saveSession({ screen: "lobby", multiRoomCode: data.room!.code, multiPlayerId: data.playerId!, playerName });
+ saveSession({
+ screen: "lobby",
+ multiRoomCode: data.room!.code,
+ multiPlayerId: data.playerId!,
+ playerName,
+ });
return {};
}
- async function joinRoom(playerName: string, code: string): Promise<{ error?: string }> {
+ async function joinRoom(
+ playerName: string,
+ code: string,
+ ): Promise<{ error?: string }> {
const res = await fetch(`/api/rooms/${code}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ action: "join", playerName }),
});
- const data = await res.json() as { room?: Room; playerId?: string; error?: string };
+ const data = (await res.json()) as {
+ room?: Room;
+ playerId?: string;
+ error?: string;
+ };
if (!res.ok) return { error: data.error ?? "Impossible de rejoindre" };
- setRoom(data.room!); setPlayerId(data.playerId!);
+ setRoom(data.room!);
+ setPlayerId(data.playerId!);
startPolling(data.room!.code, data.playerId!);
- saveSession({ screen: "lobby", multiRoomCode: data.room!.code, multiPlayerId: data.playerId!, playerName });
+ saveSession({
+ screen: "lobby",
+ multiRoomCode: data.room!.code,
+ multiPlayerId: data.playerId!,
+ playerName,
+ });
return {};
}
@@ -211,9 +283,14 @@ export function useMultiGame() {
const res = await fetch(`/api/rooms/${room.code}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ action: "start", playerId, startArticle: puzzle.start, targetArticle: puzzle.target }),
+ body: JSON.stringify({
+ action: "start",
+ playerId,
+ startArticle: puzzle.start,
+ targetArticle: puzzle.target,
+ }),
});
- const data = await res.json() as { room?: Room; error?: string };
+ const data = (await res.json()) as { room?: Room; error?: string };
if (!res.ok) return { error: data.error ?? "Erreur" };
prefetchArticle(puzzle.start);
setRoom(data.room!);
@@ -227,7 +304,7 @@ export function useMultiGame() {
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ action: "setSearchAllowed", playerId, value }),
});
- if (res.ok) setRoom((await res.json() as { room: Room }).room);
+ if (res.ok) setRoom(((await res.json()) as { room: Room }).room);
}
async function surrender() {
@@ -237,7 +314,7 @@ export function useMultiGame() {
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ action: "surrender", playerId }),
});
- if (res.ok) setRoom((await res.json() as { room: Room }).room);
+ if (res.ok) setRoom(((await res.json()) as { room: Room }).room);
}
async function nextRound() {
@@ -247,7 +324,7 @@ export function useMultiGame() {
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ action: "nextRound", playerId }),
});
- if (res.ok) setRoom((await res.json() as { room: Room }).room);
+ if (res.ok) setRoom(((await res.json()) as { room: Room }).room);
}
async function resetGame() {
@@ -257,15 +334,21 @@ export function useMultiGame() {
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ action: "resetGame", playerId }),
});
- if (res.ok) setRoom((await res.json() as { room: Room }).room);
+ if (res.ok) setRoom(((await res.json()) as { room: Room }).room);
}
function leave() {
- stopPolling(); stopCountdown(); timer.stop();
- setRoom(null); setPlayerId(null);
- setHtml(""); setTitle("");
- setHistory([]); historyRef.current = [];
- clicksRef.current = 0; setClicksDisplay(0);
+ stopPolling();
+ stopCountdown();
+ timer.stop();
+ setRoom(null);
+ setPlayerId(null);
+ setHtml("");
+ setTitle("");
+ setHistory([]);
+ historyRef.current = [];
+ clicksRef.current = 0;
+ setClicksDisplay(0);
timerStartedRef.current = false;
clearSession();
}
@@ -279,7 +362,7 @@ export function useMultiGame() {
body: JSON.stringify({ action: "heartbeat", playerId: pid }),
});
if (!res.ok) return false;
- const data = await res.json() as { room: Room };
+ const data = (await res.json()) as { room: Room };
setRoom(data.room);
setPlayerId(pid);
startPolling(code, pid);
@@ -290,9 +373,26 @@ export function useMultiGame() {
}
return {
- room, playerId, html, title, loading, loadError,
- history, clicks: clicksDisplay, elapsed: timer.elapsed, countdown,
- createRoom, joinRoom, startGame, nextRound, resetGame, leave, navigate, surrender, setSearchAllowed, restore,
+ room,
+ playerId,
+ html,
+ title,
+ loading,
+ loadError,
+ history,
+ clicks: clicksDisplay,
+ elapsed: timer.elapsed,
+ countdown,
+ createRoom,
+ joinRoom,
+ startGame,
+ nextRound,
+ resetGame,
+ leave,
+ navigate,
+ surrender,
+ setSearchAllowed,
+ restore,
retryLoad: () => title && loadArticle(title),
};
}
diff --git a/lib/useSearchAllowed.ts b/lib/useSearchAllowed.ts
index e3abe24..7895af5 100644
--- a/lib/useSearchAllowed.ts
+++ b/lib/useSearchAllowed.ts
@@ -36,6 +36,7 @@ export function useCtrlFBlock(allowed: boolean) {
}
}
window.addEventListener("keydown", handleKeyDown, { capture: true });
- return () => window.removeEventListener("keydown", handleKeyDown, { capture: true });
+ return () =>
+ window.removeEventListener("keydown", handleKeyDown, { capture: true });
}, [allowed]);
}
diff --git a/lib/useSoloGame.ts b/lib/useSoloGame.ts
index 6c3a7c3..349dec6 100644
--- a/lib/useSoloGame.ts
+++ b/lib/useSoloGame.ts
@@ -33,7 +33,10 @@ export function useSoloGame() {
const art = await fetchArticle(t);
setLoading(false);
loadingRef.current = false;
- if (!art) { setLoadError(`Impossible de charger "${t}".`); return null; }
+ if (!art) {
+ setLoadError(`Impossible de charger "${t}".`);
+ return null;
+ }
setHtml(art.html);
setTitle(art.title);
return art.title;
@@ -41,8 +44,10 @@ export function useSoloGame() {
async function start() {
setLoading(true);
- clicksRef.current = 0; setClicksDisplay(0);
- pathRef.current = []; setHistory([]);
+ clicksRef.current = 0;
+ setClicksDisplay(0);
+ pathRef.current = [];
+ setHistory([]);
timerStartedRef.current = false;
gameEndedRef.current = false;
timer.reset();
@@ -55,38 +60,65 @@ export function useSoloGame() {
pathRef.current = [canonical];
setHistory([canonical]);
setPhase("playing");
- saveSession({ screen: "solo", soloPuzzle: p, soloHistory: [canonical], soloClicks: 0 });
+ saveSession({
+ screen: "solo",
+ soloPuzzle: p,
+ soloHistory: [canonical],
+ soloClicks: 0,
+ });
}
- const navigate = useCallback(async (t: string) => {
- if (loadingRef.current || gameEndedRef.current) return;
- clicksRef.current += 1;
- setClicksDisplay(clicksRef.current);
- if (!timerStartedRef.current) { timer.start(); timerStartedRef.current = true; }
+ const navigate = useCallback(
+ async (t: string) => {
+ if (loadingRef.current || gameEndedRef.current) return;
+ clicksRef.current += 1;
+ setClicksDisplay(clicksRef.current);
+ if (!timerStartedRef.current) {
+ timer.start();
+ timerStartedRef.current = true;
+ }
- const canonical = await loadArticle(t);
- if (!canonical) return;
- const newPath = [...pathRef.current, canonical];
- pathRef.current = newPath;
- setHistory(newPath);
- window.scrollTo({ top: 0, behavior: "smooth" });
+ const canonical = await loadArticle(t);
+ if (!canonical) return;
+ const newPath = [...pathRef.current, canonical];
+ pathRef.current = newPath;
+ setHistory(newPath);
+ window.scrollTo({ top: 0, behavior: "smooth" });
- if (puzzle && normalizeTitle(canonical) === normalizeTitle(puzzle.target)) {
- timer.stop();
- gameEndedRef.current = true;
- setPhase("won");
- clearSession();
- } else {
- saveSession({ screen: "solo", soloPuzzle: puzzle ?? undefined, soloHistory: newPath, soloClicks: clicksRef.current });
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [puzzle]);
+ if (
+ puzzle &&
+ normalizeTitle(canonical) === normalizeTitle(puzzle.target)
+ ) {
+ timer.stop();
+ gameEndedRef.current = true;
+ setPhase("won");
+ clearSession();
+ } else {
+ saveSession({
+ screen: "solo",
+ soloPuzzle: puzzle ?? undefined,
+ soloHistory: newPath,
+ soloClicks: clicksRef.current,
+ });
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ },
+ [puzzle],
+ );
const goBack = useCallback(async () => {
- if (loadingRef.current || gameEndedRef.current || pathRef.current.length <= 1) return;
+ if (
+ loadingRef.current ||
+ gameEndedRef.current ||
+ pathRef.current.length <= 1
+ )
+ return;
clicksRef.current += 1;
setClicksDisplay(clicksRef.current);
- if (!timerStartedRef.current) { timer.start(); timerStartedRef.current = true; }
+ if (!timerStartedRef.current) {
+ timer.start();
+ timerStartedRef.current = true;
+ }
const newPath = pathRef.current.slice(0, -1);
const canonical = await loadArticle(newPath[newPath.length - 1]);
@@ -94,16 +126,19 @@ export function useSoloGame() {
pathRef.current = newPath;
setHistory(newPath);
window.scrollTo({ top: 0, behavior: "smooth" });
- // eslint-disable-next-line react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
function reset() {
timer.reset();
- clicksRef.current = 0; setClicksDisplay(0);
- pathRef.current = []; setHistory([]);
+ clicksRef.current = 0;
+ setClicksDisplay(0);
+ pathRef.current = [];
+ setHistory([]);
timerStartedRef.current = false;
gameEndedRef.current = false;
- setHtml(""); setTitle("");
+ setHtml("");
+ setTitle("");
setPuzzle(null);
setPhase("setup");
setLoadError(null);
@@ -111,9 +146,14 @@ export function useSoloGame() {
}
// Expose une fonction pour restaurer une session sauvegardée
- async function restore(savedPuzzle: Puzzle, savedHistory: string[], savedClicks: number) {
+ async function restore(
+ savedPuzzle: Puzzle,
+ savedHistory: string[],
+ savedClicks: number,
+ ) {
setPuzzle(savedPuzzle);
- clicksRef.current = savedClicks; setClicksDisplay(savedClicks);
+ clicksRef.current = savedClicks;
+ setClicksDisplay(savedClicks);
const lastTitle = savedHistory[savedHistory.length - 1];
const canonical = await loadArticle(lastTitle);
if (!canonical) return false;
@@ -126,18 +166,26 @@ export function useSoloGame() {
}
return {
- phase, puzzle, html, title, loading, loadError, history,
- clicks: clicksDisplay, elapsed: timer.elapsed,
+ phase,
+ puzzle,
+ html,
+ title,
+ loading,
+ loadError,
+ history,
+ clicks: clicksDisplay,
+ elapsed: timer.elapsed,
canGoBack: pathRef.current.length > 1,
- start, navigate, goBack, reset, restore,
+ start,
+ navigate,
+ goBack,
+ reset,
+ restore,
retryLoad: () => title && loadArticle(title),
};
}
-export function useSoloKeyboard(
- active: boolean,
- goBack: () => void,
-) {
+export function useSoloKeyboard(active: boolean, goBack: () => void) {
useEffect(() => {
if (!active) return;
const onKey = (e: KeyboardEvent) => {
diff --git a/lib/useTimer.ts b/lib/useTimer.ts
index db652b0..5da4626 100644
--- a/lib/useTimer.ts
+++ b/lib/useTimer.ts
@@ -39,7 +39,12 @@ export function useTimer() {
const stop = useCallback(() => stopRef.current(), []);
const reset = useCallback(() => resetRef.current(), []);
- useEffect(() => () => { if (rafRef.current !== null) cancelAnimationFrame(rafRef.current); }, []);
+ useEffect(
+ () => () => {
+ if (rafRef.current !== null) cancelAnimationFrame(rafRef.current);
+ },
+ [],
+ );
return { elapsed, start, stop, reset };
}
diff --git a/lib/utils.ts b/lib/utils.ts
index c0df72c..2829c92 100644
--- a/lib/utils.ts
+++ b/lib/utils.ts
@@ -19,5 +19,7 @@ export async function saveGame(data: {
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
- } catch { /* silencieux - pas de compte ou hors ligne */ }
+ } catch {
+ /* silencieux - pas de compte ou hors ligne */
+ }
}
diff --git a/lib/wiki.ts b/lib/wiki.ts
index 760448e..48fb957 100644
--- a/lib/wiki.ts
+++ b/lib/wiki.ts
@@ -83,7 +83,9 @@ async function fetchRandomCandidates(): Promise {
});
const res = await fetch(`${WIKI_API_BASE}?${params}`);
if (!res.ok) throw new Error("Erreur reseau");
- const data = await res.json() as { query: { pages: Record } };
+ const data = (await res.json()) as {
+ query: { pages: Record };
+ };
return Object.values(data.query.pages)
.filter(isGoodArticle)
.map((p) => p.title);
@@ -96,7 +98,8 @@ export async function pickTwoArticles(): Promise {
const batch = await fetchRandomCandidates();
for (const title of batch) {
if (!collected.includes(title)) collected.push(title);
- if (collected.length >= 2) return { start: collected[0], target: collected[1] };
+ if (collected.length >= 2)
+ return { start: collected[0], target: collected[1] };
}
}
} catch {