From d268905f2d0c40faee4b6f9fb1eebe1b31977c52 Mon Sep 17 00:00:00 2001 From: jessy-david-dev Date: Sat, 11 Apr 2026 22:25:34 +0200 Subject: [PATCH] style: apply prettier formatting across codebase --- app/about/page.tsx | 10 +- app/admin/page.tsx | 331 +++++++++++++++++++----- app/components/ArticleView.tsx | 109 +++++--- app/components/AuthModal.tsx | 105 ++++++-- app/components/BlitzScreen.tsx | 301 ++++++++++++++-------- app/components/Breadcrumbs.tsx | 16 +- app/components/DailyScreen.tsx | 190 ++++++++++---- app/components/GameScreen.tsx | 337 +++++++++++++++++-------- app/components/LeaderboardScreen.tsx | 82 ++++-- app/components/ProfileScreen.tsx | 143 ++++++++--- app/components/PublicProfileScreen.tsx | 98 +++++-- app/components/ScreenRouter.tsx | 219 ++++++++++------ app/components/SoloScreen.tsx | 203 ++++++++++----- app/mentions-legales/page.tsx | 4 +- app/page.tsx | 51 +++- lib/session.ts | 10 +- lib/types.ts | 10 +- lib/useBlitzGame.ts | 94 ++++--- lib/useDailyGame.ts | 137 ++++++---- lib/useGameEffects.ts | 26 +- lib/useGameHandlers.ts | 61 ++++- lib/useMultiGame.ts | 234 ++++++++++++----- lib/useSearchAllowed.ts | 3 +- lib/useSoloGame.ts | 128 +++++++--- lib/useTimer.ts | 7 +- lib/utils.ts | 4 +- lib/wiki.ts | 7 +- 27 files changed, 2092 insertions(+), 828 deletions(-) diff --git a/app/about/page.tsx b/app/about/page.tsx index b157bcf..f460e21 100644 --- a/app/about/page.tsx +++ b/app/about/page.tsx @@ -150,10 +150,16 @@ export default function AboutPage() { { emoji: "🎯", label: "500 parties et plus", range: "500+" }, { emoji: "⚡", label: "1 000 parties et plus", range: "1 000+" }, { emoji: "🔥", label: "5 000 parties et plus", range: "5 000+" }, - { emoji: "🚀", label: "10 000 parties et plus", range: "10 000+" }, + { + emoji: "🚀", + label: "10 000 parties et plus", + range: "10 000+", + }, ].map(({ emoji, label, range }) => (
- {emoji} + + {emoji} + {label} {range}
diff --git a/app/admin/page.tsx b/app/admin/page.tsx index 175c793..8053e35 100644 --- a/app/admin/page.tsx +++ b/app/admin/page.tsx @@ -62,15 +62,21 @@ function MiniBarChart({ data }: { data: ActivityDay[] }) {
{data.map((d) => { const pct = (d.count / max) * 100; - const day = new Date(d.date + "T12:00:00").toLocaleDateString("fr-FR", { day: "numeric", month: "short" }); + const day = new Date(d.date + "T12:00:00").toLocaleDateString("fr-FR", { + day: "numeric", + month: "short", + }); return ( -
+
- {day} — {d.count} + {day} - {d.count}
); @@ -83,19 +89,32 @@ export default function AdminPage() { const [data, setData] = useState(null); const [dailyPuzzles, setDailyPuzzles] = useState(null); const [loading, setLoading] = useState(true); - const [tab, setTab] = useState<"overview" | "users" | "games" | "daily">("overview"); + const [tab, setTab] = useState<"overview" | "users" | "games" | "daily">( + "overview", + ); // Daily puzzle form - const [puzzleForm, setPuzzleForm] = useState({ date: "", startArticle: "", targetArticle: "" }); + const [puzzleForm, setPuzzleForm] = useState({ + date: "", + startArticle: "", + targetArticle: "", + }); const [editingPuzzle, setEditingPuzzle] = useState(null); const [puzzleSaving, setPuzzleSaving] = useState(false); // Confirm delete - const [confirmDelete, setConfirmDelete] = useState<{ type: "user" | "puzzle"; id: string; label: string } | null>(null); + const [confirmDelete, setConfirmDelete] = useState<{ + type: "user" | "puzzle"; + id: string; + label: string; + } | null>(null); function refreshStats() { return fetch("/api/admin/stats") - .then((r) => { if (!r.ok) throw new Error(); return r.json(); }) + .then((r) => { + if (!r.ok) throw new Error(); + return r.json(); + }) .then(setData); } @@ -139,7 +158,10 @@ export default function AdminPage() { await fetch(`/api/admin/daily/${editingPuzzle.id}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ startArticle: puzzleForm.startArticle, targetArticle: puzzleForm.targetArticle }), + body: JSON.stringify({ + startArticle: puzzleForm.startArticle, + targetArticle: puzzleForm.targetArticle, + }), }); } else { await fetch("/api/admin/daily", { @@ -158,15 +180,22 @@ export default function AdminPage() { function startEditPuzzle(p: DailyPuzzle) { setEditingPuzzle(p); - setPuzzleForm({ date: p.date, startArticle: p.startArticle, targetArticle: p.targetArticle }); + setPuzzleForm({ + date: p.date, + startArticle: p.startArticle, + targetArticle: p.targetArticle, + }); } const card = "bg-[#1a1a1a] border border-[#2e2e2e] rounded-xl p-4"; const tabBtn = (active: boolean) => `px-3 py-2 rounded-lg text-xs sm:text-sm font-semibold transition-colors cursor-pointer ${ - active ? "bg-[#7c3aed] text-white" : "bg-[#1a1a1a] border border-[#2e2e2e] text-[#888] hover:text-[#f0f0f0]" + active + ? "bg-[#7c3aed] text-white" + : "bg-[#1a1a1a] border border-[#2e2e2e] text-[#888] hover:text-[#f0f0f0]" }`; - const inputCls = "w-full min-h-10 px-3 bg-[#0f0f0f] border border-[#2e2e2e] rounded-xl text-sm text-[#f0f0f0] outline-none focus:border-[#7c3aed] transition-colors"; + const inputCls = + "w-full min-h-10 px-3 bg-[#0f0f0f] border border-[#2e2e2e] rounded-xl text-sm text-[#f0f0f0] outline-none focus:border-[#7c3aed] transition-colors"; return (
@@ -174,27 +203,53 @@ export default function AdminPage() {

Admin

-

WikiRush — panneau d'administration

+

+ WikiRush - panneau d'administration +

- + ← Accueil
- {loading &&
Chargement...
} - {!loading && !data &&
Accès refusé ou erreur serveur.
} + {loading && ( +
Chargement...
+ )} + {!loading && !data && ( +
+ Accès refusé ou erreur serveur. +
+ )} {/* Confirm dialog */} {confirmDelete && (

Confirmer la suppression

-

Supprimer {confirmDelete.label} ? Cette action est irréversible.

+

+ Supprimer{" "} + + {confirmDelete.label} + {" "} + ? Cette action est irréversible. +

- + @@ -207,12 +262,31 @@ export default function AdminPage() { <> {/* Tabs */}
- - + + + - -
{/* Overview */} @@ -220,41 +294,80 @@ export default function AdminPage() {
{[ - { label: "Utilisateurs", value: data.totalUsers.toLocaleString("fr-FR") }, - { label: "Parties totales", value: data.totalGames.toLocaleString("fr-FR") }, - { label: "Parties aujourd'hui", value: data.todayGames.toLocaleString("fr-FR") }, + { + label: "Utilisateurs", + value: data.totalUsers.toLocaleString("fr-FR"), + }, + { + label: "Parties totales", + value: data.totalGames.toLocaleString("fr-FR"), + }, + { + label: "Parties aujourd'hui", + value: data.todayGames.toLocaleString("fr-FR"), + }, ].map(({ label, value }) => (
-

{label}

-

{value}

+

+ {label} +

+

+ {value} +

))}
{/* Graphique activité */}
-

Activité — 14 derniers jours

+

+ Activité - 14 derniers jours +

- {new Date(data.activity[0]?.date + "T12:00:00").toLocaleDateString("fr-FR", { day: "numeric", month: "short" })} - aujourd'hui + + {new Date( + data.activity[0]?.date + "T12:00:00", + ).toLocaleDateString("fr-FR", { + day: "numeric", + month: "short", + })} + + + aujourd'hui +
{/* Modes */}
-

Parties par mode

+

+ Parties par mode +

- {data.modeStats.sort((a, b) => b._count.id - a._count.id).map((m) => ( -
- {m.mode} -
- {m._count.id.toLocaleString("fr-FR")} parties - {m._avg.clicks !== null && ~{Math.round(m._avg.clicks)} clics} - {m._avg.timeSeconds !== null && ~{fmt(m._avg.timeSeconds)}} + {data.modeStats + .sort((a, b) => b._count.id - a._count.id) + .map((m) => ( +
+ + {m.mode} + +
+ + {m._count.id.toLocaleString("fr-FR")} parties + + {m._avg.clicks !== null && ( + ~{Math.round(m._avg.clicks)} clics + )} + {m._avg.timeSeconds !== null && ( + ~{fmt(m._avg.timeSeconds)} + )} +
-
- ))} + ))}
@@ -263,20 +376,33 @@ export default function AdminPage() { {/* Users */} {tab === "users" && (
-

Utilisateurs

+

+ Utilisateurs +

{data.recentUsers.map((u) => ( -
+

{u.name} - {u.banned && Banni} + {u.banned && ( + + Banni + + )}

{u.email}

-

{u._count.games} parties

-

{new Date(u.createdAt).toLocaleDateString("fr-FR")}

+

+ {u._count.games} parties +

+

+ {new Date(u.createdAt).toLocaleDateString("fr-FR")} +

{editingPuzzle && ( +
+
e.stopPropagation()} + > +
{(["login", "register"] as Mode[]).map((m) => ( @@ -56,24 +88,61 @@ export function AuthModal({ onClose, onSuccess }: { onClose: () => void; onSucce
{mode === "register" && ( - setName(e.target.value)} required maxLength={30} /> + setName(e.target.value)} + required + maxLength={30} + /> )} - setEmail(e.target.value)} required /> - setPassword(e.target.value)} required minLength={6} /> + setEmail(e.target.value)} + required + /> + setPassword(e.target.value)} + required + minLength={6} + /> {error && ( -
{error}
+
+ {error} +
)} - {mode === "register" && (

En créant un compte, vous acceptez notre{" "} - + politique de confidentialité - . + + .

)}
diff --git a/app/components/BlitzScreen.tsx b/app/components/BlitzScreen.tsx index d20c289..acd398d 100644 --- a/app/components/BlitzScreen.tsx +++ b/app/components/BlitzScreen.tsx @@ -15,122 +15,186 @@ export function BlitzScreen({ onBack }: { onBack: () => void }) { const game = useBlitzGame(); const { allowed: searchAllowed, toggle: toggleSearch } = useSearchAllowed(); - - const btnPrimary = "w-full min-h-11 rounded-xl text-sm font-semibold bg-[#7c3aed] text-white hover:bg-[#6d28d9] disabled:opacity-50 cursor-pointer transition-colors"; - const btnGhost = "w-full min-h-11 rounded-xl text-sm font-semibold bg-[#242424] border border-[#2e2e2e] text-[#f0f0f0] hover:bg-[#1a1a1a] cursor-pointer transition-colors"; + const btnPrimary = + "w-full min-h-11 rounded-xl text-sm font-semibold bg-[#7c3aed] text-white hover:bg-[#6d28d9] disabled:opacity-50 cursor-pointer transition-colors"; + const btnGhost = + "w-full min-h-11 rounded-xl text-sm font-semibold bg-[#242424] border border-[#2e2e2e] text-[#f0f0f0] hover:bg-[#1a1a1a] cursor-pointer transition-colors"; const danger = game.timeLeft < 30; - if (game.phase === "setup") return ( -
- -
-

Mode Blitz

-

- Tu as 2 minutes{" "}pour naviguer de l'article de départ jusqu'à l'article cible en cliquant sur les liens Wikipedia. Plus tu es rapide, mieux c'est ! -

- - -
- ); - - if (game.phase === "won") return ( -
-
-
🎉
-

Article trouvé !

- {game.puzzle && ( -

- {game.puzzle.start} - {" → "} - {game.puzzle.target} -

- )} -
-
- {fmtLeft(game.timeLeft)} - temps restant -
-
- {game.clicks} - clics -
-
- {game.history.length} - articles -
-
-
- {game.history.map((t, i) => ( - - {i > 0 && } - {t} - - ))} -
- -
- - -
+ if (game.phase === "setup") + return ( +
+ +
+

Mode Blitz

+

+ Tu as 2 minutes pour + naviguer de l'article de départ jusqu'à l'article cible + en cliquant sur les liens Wikipedia. Plus tu es rapide, mieux + c'est ! +

+ +
-
- ); + ); - if (game.phase === "lost") return ( -
-
-
⏱️
-

Temps écoulé !

- {game.puzzle && ( -

- L'objectif était d'atteindre{" "} - {game.puzzle.target} -

- )} -
-
- {game.clicks} - clics + if (game.phase === "won") + return ( +
+
+
🎉
+

Article trouvé !

+ {game.puzzle && ( +

+ + {game.puzzle.start} + + {" → "} + + {game.puzzle.target} + +

+ )} +
+
+ + {fmtLeft(game.timeLeft)} + + temps restant +
+
+ + {game.clicks} + + clics +
+
+ + {game.history.length} + + articles +
-
- {game.history.length} - articles visités -
-
- {game.history.length > 0 && (
{game.history.map((t, i) => ( {i > 0 && } - {t} + + {t} + ))}
- )} -
- - + +
+ + +
-
- ); + ); + + if (game.phase === "lost") + return ( +
+
+
⏱️
+

Temps écoulé !

+ {game.puzzle && ( +

+ L'objectif était d'atteindre{" "} + + {game.puzzle.target} + +

+ )} +
+
+ + {game.clicks} + + clics +
+
+ + {game.history.length} + + articles visités +
+
+ {game.history.length > 0 && ( +
+ {game.history.map((t, i) => ( + + {i > 0 && } + {t} + + ))} +
+ )} +
+ + +
+
+
+ ); // Playing return ( @@ -139,18 +203,27 @@ export function BlitzScreen({ onBack }: { onBack: () => void }) {
{game.title &&

{game.title}

} {game.loading && ( -
Chargement...
+
+
Chargement... +
)} {game.loadError && (

{game.loadError}

-
)} {!game.loading && !game.loadError && game.html && ( - + )}
@@ -160,20 +233,30 @@ export function BlitzScreen({ onBack }: { onBack: () => void }) {
- Clics - {game.clicks} + + Clics + + + {game.clicks} +
{/* Timer */} -
+
{fmtLeft(game.timeLeft)}
{game.puzzle && (
-
Cible
-
{game.puzzle.target}
+
+ Cible +
+
+ {game.puzzle.target} +
)}
diff --git a/app/components/Breadcrumbs.tsx b/app/components/Breadcrumbs.tsx index 92c23ac..94c72eb 100644 --- a/app/components/Breadcrumbs.tsx +++ b/app/components/Breadcrumbs.tsx @@ -2,13 +2,25 @@ import { useRef } from "react"; -export function Breadcrumbs({ history, endRef }: { history: string[]; endRef: React.RefObject }) { +export function Breadcrumbs({ + history, + endRef, +}: { + history: string[]; + endRef: React.RefObject; +}) { return (
{history.map((title, i) => ( {i > 0 && } - + {title} diff --git a/app/components/DailyScreen.tsx b/app/components/DailyScreen.tsx index d4ffb9f..89da643 100644 --- a/app/components/DailyScreen.tsx +++ b/app/components/DailyScreen.tsx @@ -9,16 +9,32 @@ import { ShareBar } from "./ShareBar"; const MEDALS = ["🥇", "🥈", "🥉"]; -type LeaderboardEntry = { rank: number; name: string; clicks: number; timeSeconds: number; userId: string }; +type LeaderboardEntry = { + rank: number; + name: string; + clicks: number; + timeSeconds: number; + userId: string; +}; -export function DailyScreen({ onBack, currentUserId }: { onBack: () => void; currentUserId?: string }) { +export function DailyScreen({ + onBack, + currentUserId, +}: { + onBack: () => void; + currentUserId?: string; +}) { const game = useDailyGame(); const breadcrumbEndRef = useRef(null); const [leaderboard, setLeaderboard] = useState([]); const [loadingLb, setLoadingLb] = useState(false); useEffect(() => { - breadcrumbEndRef.current?.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "end" }); + breadcrumbEndRef.current?.scrollIntoView({ + behavior: "smooth", + block: "nearest", + inline: "end", + }); }, [game.history]); function fetchLeaderboard() { @@ -30,47 +46,78 @@ export function DailyScreen({ onBack, currentUserId }: { onBack: () => void; cur } useEffect(() => { - if (game.phase === "won" || game.phase === "gave_up" || game.phase === "already_played") { + if ( + game.phase === "won" || + game.phase === "gave_up" || + game.phase === "already_played" + ) { fetchLeaderboard(); } }, [game.phase]); - const btnPrimary = "w-full min-h-11 rounded-xl text-sm font-semibold bg-[#7c3aed] text-white hover:bg-[#6d28d9] disabled:opacity-50 cursor-pointer transition-colors"; - const btnGhost = "w-full min-h-11 rounded-xl text-sm font-semibold bg-[#242424] border border-[#2e2e2e] text-[#f0f0f0] hover:bg-[#1a1a1a] cursor-pointer transition-colors"; + const btnPrimary = + "w-full min-h-11 rounded-xl text-sm font-semibold bg-[#7c3aed] text-white hover:bg-[#6d28d9] disabled:opacity-50 cursor-pointer transition-colors"; + const btnGhost = + "w-full min-h-11 rounded-xl text-sm font-semibold bg-[#242424] border border-[#2e2e2e] text-[#f0f0f0] hover:bg-[#1a1a1a] cursor-pointer transition-colors"; // Loading initial - if (game.phase === "loading") return ( -
-
-
- ); + if (game.phase === "loading") + return ( +
+
+
+ ); // Erreur initiale - if (game.loadError) return ( -
-

{game.loadError}

- -
- ); + if (game.loadError) + return ( +
+

{game.loadError}

+ +
+ ); // Résultat (won / gave_up / already_played) - if (game.phase === "won" || game.phase === "gave_up" || game.phase === "already_played") { + if ( + game.phase === "won" || + game.phase === "gave_up" || + game.phase === "already_played" + ) { const result = game.myResult; return (
-
-
Défi du jour - {game.puzzle?.date}
+
+ Défi du jour - {game.puzzle?.date} +
- {game.phase === "won" ? "🎉 Réussi !" : game.phase === "gave_up" ? "😔 Abandonné" : "✅ Déjà joué"} + {game.phase === "won" + ? "🎉 Réussi !" + : game.phase === "gave_up" + ? "😔 Abandonné" + : "✅ Déjà joué"}
- {game.puzzle?.startArticle} + + {game.puzzle?.startArticle} + - {game.puzzle?.targetArticle} + + {game.puzzle?.targetArticle} +
@@ -78,11 +125,15 @@ export function DailyScreen({ onBack, currentUserId }: { onBack: () => void; cur
-
{result.clicks}
+
+ {result.clicks} +
clics
-
{fmt(result.timeSeconds)}
+
+ {fmt(result.timeSeconds)} +
temps
@@ -91,7 +142,15 @@ export function DailyScreen({ onBack, currentUserId }: { onBack: () => void; cur {result.path.map((t, i) => ( {i > 0 && } - {t} + + {t} + ))}
@@ -101,18 +160,33 @@ export function DailyScreen({ onBack, currentUserId }: { onBack: () => void; cur {/* Classement du jour */}
-

Classement du jour

+

+ Classement du jour +

{loadingLb ? ( -
+
+
+
) : leaderboard.length === 0 ? ( -

Aucun résultat pour l'instant.

+

+ Aucun résultat pour l'instant. +

) : (
{leaderboard.map((entry, i) => ( -
- {MEDALS[i] ?? `#${entry.rank}`} - {entry.name} - {entry.clicks} clics · {fmt(entry.timeSeconds)} +
+ + {MEDALS[i] ?? `#${entry.rank}`} + + + {entry.name} + + + {entry.clicks} clics · {fmt(entry.timeSeconds)} +
))}
@@ -120,9 +194,13 @@ export function DailyScreen({ onBack, currentUserId }: { onBack: () => void; cur
{game.phase === "won" && result && ( - + )} - +
); } @@ -134,18 +212,27 @@ export function DailyScreen({ onBack, currentUserId }: { onBack: () => void; cur
{game.title &&

{game.title}

} {game.loading && ( -
Chargement...
+
+
Chargement... +
)} {game.loadError && (

{game.loadError}

-
)} {!game.loading && !game.loadError && game.html && ( - + )}
@@ -154,22 +241,37 @@ export function DailyScreen({ onBack, currentUserId }: { onBack: () => void; cur
- 🗓 Défi du jour · cible - {game.puzzle?.targetArticle} + + 🗓 Défi du jour · cible + + + {game.puzzle?.targetArticle} +
- Clics - {game.clicks} + + Clics + + + {game.clicks} +
{game.canGoBack && ( - )} -
diff --git a/app/components/GameScreen.tsx b/app/components/GameScreen.tsx index 66fdd6d..58ef36a 100644 --- a/app/components/GameScreen.tsx +++ b/app/components/GameScreen.tsx @@ -25,8 +25,21 @@ type GameScreenProps = { }; export function GameScreen({ - room, playerId, html, title, loading, loadError, history, clicks, elapsed, - countdown, onNavigate, onRetry, onNextRound, onResetGame, onSurrender, + room, + playerId, + html, + title, + loading, + loadError, + history, + clicks, + elapsed, + countdown, + onNavigate, + onRetry, + onNextRound, + onResetGame, + onSurrender, }: GameScreenProps) { const breadcrumbEndRef = useRef(null); useCtrlFBlock(room.searchAllowed); @@ -34,125 +47,198 @@ export function GameScreen({ const isHost = myPlayer?.isHost ?? false; const myFinished = myPlayer?.hasWon || myPlayer?.hasSurrendered; const sortedPlayers = [...room.players].sort((a, b) => b.score - a.score); - const winner = room.roundWinner ? room.players.find((p) => p.id === room.roundWinner) : null; + const winner = room.roundWinner + ? room.players.find((p) => p.id === room.roundWinner) + : null; useEffect(() => { - breadcrumbEndRef.current?.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "end" }); + breadcrumbEndRef.current?.scrollIntoView({ + behavior: "smooth", + block: "nearest", + inline: "end", + }); }, [history]); - const btnPrimary = "w-full min-h-11 px-5 rounded-xl text-sm font-semibold bg-[#7c3aed] text-white hover:bg-[#6d28d9] cursor-pointer transition-colors"; - const btnGhost = "w-full min-h-11 px-5 rounded-xl text-sm font-semibold bg-[#242424] border border-[#2e2e2e] text-[#f0f0f0] hover:bg-[#1a1a1a] cursor-pointer transition-colors"; + const btnPrimary = + "w-full min-h-11 px-5 rounded-xl text-sm font-semibold bg-[#7c3aed] text-white hover:bg-[#6d28d9] cursor-pointer transition-colors"; + const btnGhost = + "w-full min-h-11 px-5 rounded-xl text-sm font-semibold bg-[#242424] border border-[#2e2e2e] text-[#f0f0f0] hover:bg-[#1a1a1a] cursor-pointer transition-colors"; // Results - if (room.phase === "results") return ( -
-
-
- {winner ? ( - <> -
🏆
-
{winner.name} a gagné la manche !
- - ) : ( -
Manche terminée !
- )} -
-
- {room.startArticle} - - {room.targetArticle} -
-
-

Classement

-
    - {sortedPlayers.map((p, i) => ( -
  • -
    - #{i + 1} - {p.name} - {p.hasWon && ✓ Trouvé} - {p.hasSurrendered && Forfait} - {p.score} pts + if (room.phase === "results") + return ( +
    +
    +
    + {winner ? ( + <> +
    🏆
    +
    + {winner.name} a gagné la manche !
    - {p.path && p.path.length > 0 && ( -
    - {p.path.map((t, j) => ( - - {j > 0 && } - {t} - - ))} - ({p.path.length - 1} clic{p.path.length - 1 > 1 ? "s" : ""}) -
    - )} -
  • - ))} -
-
-
- Manche {room.round}/{room.totalRounds} -
- {isHost ? ( -
- {room.round < room.totalRounds - ? - : - } - {room.round < room.totalRounds && ( - + + ) : ( +
+ Manche terminée ! +
)}
- ) : ( -

En attente de l'hôte...

- )} +
+ {room.startArticle} + + + {room.targetArticle} + +
+
+

+ Classement +

+
    + {sortedPlayers.map((p, i) => ( +
  • +
    + + #{i + 1} + + + {p.name} + + {p.hasWon && ( + + ✓ Trouvé + + )} + {p.hasSurrendered && ( + + Forfait + + )} + + {p.score} pts + +
    + {p.path && p.path.length > 0 && ( +
    + {p.path.map((t, j) => ( + + {j > 0 && } + + {t} + + + ))} + + ({p.path.length - 1} clic + {p.path.length - 1 > 1 ? "s" : ""}) + +
    + )} +
  • + ))} +
+
+
+ Manche {room.round}/{room.totalRounds} +
+ {isHost ? ( +
+ {room.round < room.totalRounds ? ( + + ) : ( + + )} + {room.round < room.totalRounds && ( + + )} +
+ ) : ( +

+ En attente de l'hôte... +

+ )} +
-
- ); + ); // Entre les manches : attente que l'hôte démarre la suivante - if (room.phase === "waiting") return ( -
-
-
Manche {room.round}/{room.totalRounds} terminée
-

- {isHost ? "Lance la manche suivante quand tu veux." : "En attente que l'hôte démarre la prochaine manche..."} -

- {isHost && ( -
- - + if (room.phase === "waiting") + return ( +
+
+
+ Manche {room.round}/{room.totalRounds} terminée
- )} +

+ {isHost + ? "Lance la manche suivante quand tu veux." + : "En attente que l'hôte démarre la prochaine manche..."} +

+ {isHost && ( +
+ + +
+ )} +
-
- ); + ); // Countdown - if (room.phase === "countdown") return ( -
-
-
-
- Départ - {room.startArticle} + if (room.phase === "countdown") + return ( +
+
+
+
+ + Départ + + + {room.startArticle} + +
+ + → + +
+ + Cible + + + {room.targetArticle} + +
- -
- Cible - {room.targetArticle} +
+ {countdown !== null && countdown > 0 ? countdown : "Partez !"}
-
- {countdown !== null && countdown > 0 ? countdown : "Partez !"} -
-
- ); + ); // Playing return ( @@ -161,8 +247,12 @@ export function GameScreen({
- Cible - {room.targetArticle} + + Cible + + + {room.targetArticle} +
@@ -181,10 +271,14 @@ export function GameScreen({ )} {myFinished && myPlayer?.hasWon && ( - ✓ Trouvé ! + + ✓ Trouvé ! + )} {myFinished && myPlayer?.hasSurrendered && ( - Forfait + + Forfait + )}
@@ -200,7 +294,10 @@ export function GameScreen({ {loadError && (

{loadError}

-
@@ -208,21 +305,39 @@ export function GameScreen({ {!loading && !loadError && html && (

{title}

- +
)}
{/* Sidebar - desktop seulement */}
{loading ? ( -
+
+
+
) : sorted.length === 0 ? ( -

Aucune partie jouée pour le moment.

+

+ Aucune partie jouée pour le moment. +

) : (
{sorted.map((row, i) => { - const games = mode === "solo" ? row.soloGames : mode === "multi" ? row.multiGames : row.totalGames; - const wins = mode === "solo" ? row.soloWins : mode === "multi" ? row.multiWins : row.wins; + const games = + mode === "solo" + ? row.soloGames + : mode === "multi" + ? row.multiGames + : row.totalGames; + const wins = + mode === "solo" + ? row.soloWins + : mode === "multi" + ? row.multiWins + : row.wins; return ( -
setViewingUserId(row.id)} className={`flex items-center gap-2.5 px-3.5 py-3 rounded-xl border cursor-pointer hover:brightness-110 transition-all ${i < 3 ? "border-[#7c3aed] bg-[#7c3aed]/8" : "border-[#2e2e2e] bg-[#1a1a1a]"}`}> - {MEDALS[i] ?? `#${i + 1}`} +
setViewingUserId(row.id)} + className={`flex items-center gap-2.5 px-3.5 py-3 rounded-xl border cursor-pointer hover:brightness-110 transition-all ${i < 3 ? "border-[#7c3aed] bg-[#7c3aed]/8" : "border-[#2e2e2e] bg-[#1a1a1a]"}`} + > + + {MEDALS[i] ?? `#${i + 1}`} +
{row.name}
- {wins} victoires - {games} parties + + {wins}{" "} + victoires + + + {games}{" "} + parties + {row.avgClicks != null && ( - {row.avgClicks} clics moy. + + + {row.avgClicks} + {" "} + clics moy. + )} {row.bestTime != null && ( - {fmt(row.bestTime)} meilleur + + + {fmt(row.bestTime)} + {" "} + meilleur + )}
diff --git a/app/components/ProfileScreen.tsx b/app/components/ProfileScreen.tsx index e48dae2..8d15513 100644 --- a/app/components/ProfileScreen.tsx +++ b/app/components/ProfileScreen.tsx @@ -35,14 +35,24 @@ function computeStats(games: Game[]): Stats { return { total: games.length, won: won.length, - avgClicks: won.length ? Math.round(won.reduce((s, g) => s + g.clicks, 0) / won.length) : 0, - avgTime: won.length ? won.reduce((s, g) => s + g.timeSeconds, 0) / won.length : 0, + avgClicks: won.length + ? Math.round(won.reduce((s, g) => s + g.clicks, 0) / won.length) + : 0, + avgTime: won.length + ? won.reduce((s, g) => s + g.timeSeconds, 0) / won.length + : 0, bestClicks: won.length ? Math.min(...won.map((g) => g.clicks)) : 0, bestTime: won.length ? Math.min(...won.map((g) => g.timeSeconds)) : 0, }; } -export function ProfileScreen({ userName, onBack }: { userName: string; onBack: () => void }) { +export function ProfileScreen({ + userName, + onBack, +}: { + userName: string; + onBack: () => void; +}) { const [games, setGames] = useState([]); const [loading, setLoading] = useState(true); const [filter, setFilter] = useState<"all" | "solo" | "multi">("all"); @@ -63,22 +73,29 @@ export function ProfileScreen({ userName, onBack }: { userName: string; onBack: .finally(() => setLoading(false)); }, []); - const filtered = filter === "all" ? games : games.filter((g) => g.mode === filter); + const filtered = + filter === "all" ? games : games.filter((g) => g.mode === filter); const stats = computeStats(filtered); - const statCard = "bg-[#1a1a1a] border border-[#2e2e2e] rounded-xl p-3 sm:p-3.5 text-center flex flex-col gap-1"; - const btnGhost = "min-h-9 px-3 rounded-lg text-xs sm:text-sm font-semibold bg-[#242424] border border-[#2e2e2e] text-[#f0f0f0] hover:bg-[#1a1a1a] cursor-pointer transition-colors"; + const statCard = + "bg-[#1a1a1a] border border-[#2e2e2e] rounded-xl p-3 sm:p-3.5 text-center flex flex-col gap-1"; + const btnGhost = + "min-h-9 px-3 rounded-lg text-xs sm:text-sm font-semibold bg-[#242424] border border-[#2e2e2e] text-[#f0f0f0] hover:bg-[#1a1a1a] cursor-pointer transition-colors"; return (
{/* Header */}
- +
{userName[0].toUpperCase()} -

{userName}

+

+ {userName} +

@@ -132,22 +171,39 @@ export function ProfileScreen({ userName, onBack }: { userName: string; onBack: {/* Game list */}
{loading && ( -
Chargement...
+
+
Chargement... +
)} {!loading && filtered.length === 0 && ( -

Aucune partie enregistrée.

+

+ Aucune partie enregistrée. +

)} {filtered.map((g) => ( -
+
- {g.mode === "solo" ? "Solo" : "Multi"} - {new Date(g.playedAt).toLocaleDateString("fr-FR")} - {g.won ? "Victoire" : "Abandon"} + + {g.mode === "solo" ? "Solo" : "Multi"} + + + {new Date(g.playedAt).toLocaleDateString("fr-FR")} + + + {g.won ? "Victoire" : "Abandon"} +
{g.startArticle} - {g.targetArticle} + + {g.targetArticle} +
{g.won && (
@@ -161,7 +217,15 @@ export function ProfileScreen({ userName, onBack }: { userName: string; onBack: {g.path.map((t, i) => ( {i > 0 && } - {t} + + {t} + ))}
@@ -173,15 +237,23 @@ export function ProfileScreen({ userName, onBack }: { userName: string; onBack: {/* Mes données */}
-

Mes données

-

Télécharge toutes tes données personnelles (RGPD).

+

+ Mes données +

+

+ Télécharge toutes tes données personnelles (RGPD). +

- + @@ -192,10 +264,14 @@ export function ProfileScreen({ userName, onBack }: { userName: string; onBack: {/* Danger zone */}
-

Zone dangereuse

+

+ Zone dangereuse +

{!confirmDelete ? (
-

Supprime définitivement ton compte et toutes tes parties.

+

+ Supprime définitivement ton compte et toutes tes parties. +

+ {profile && (
@@ -70,64 +79,109 @@ export function PublicProfileScreen({

{profile.name}

- Membre depuis {new Date(profile.createdAt).toLocaleDateString("fr-FR", { month: "long", year: "numeric" })} + Membre depuis{" "} + {new Date(profile.createdAt).toLocaleDateString("fr-FR", { + month: "long", + year: "numeric", + })}

)}
- {loading &&
Chargement...
} - {notFound &&

Joueur introuvable.

} + {loading && ( +
+ Chargement... +
+ )} + {notFound && ( +

+ Joueur introuvable. +

+ )} {profile && (
{/* Stats principales */}
- {profile.stats.total} - Parties + + {profile.stats.total} + + + Parties +
- {profile.stats.wins} - Victoires + + {profile.stats.wins} + + + Victoires +
- {profile.stats.winRate}% - Win rate + + {profile.stats.winRate}% + + + Win rate +
{profile.stats.avgClicks ?? "-"} - Clics moy. + + Clics moy. +
{profile.stats.bestClicks ?? "-"} - Meilleur clics + + Meilleur clics +
{profile.stats.bestTime ? fmt(profile.stats.bestTime) : "-"} - Meilleur temps + + Meilleur temps +
{/* Stats par mode */}
-

Par mode

+

+ Par mode +

{(["solo", "multi", "daily", "blitz"] as const).map((m) => { const s = profile.stats[m]; if (s.games === 0) return null; return ( -
- {MODE_LABELS[m]} +
+ + {MODE_LABELS[m]} +
- {s.wins} victoires - {s.games} parties + + {s.wins}{" "} + victoires + + + + {s.games} + {" "} + parties +
); diff --git a/app/components/ScreenRouter.tsx b/app/components/ScreenRouter.tsx index aff4f25..99495dc 100644 --- a/app/components/ScreenRouter.tsx +++ b/app/components/ScreenRouter.tsx @@ -42,95 +42,146 @@ type Props = { }; export function ScreenRouter({ - screen, setScreen, session, solo, multi, handlers, - playerName, setPlayerName, joinCode, setJoinCode, - maxPlayers, setMaxPlayers, - totalRounds, setTotalRounds, - gameMode, setGameMode, - error, setError, loading, showAuth, setShowAuth, + screen, + setScreen, + session, + solo, + multi, + handlers, + playerName, + setPlayerName, + joinCode, + setJoinCode, + maxPlayers, + setMaxPlayers, + totalRounds, + setTotalRounds, + gameMode, + setGameMode, + error, + setError, + loading, + showAuth, + setShowAuth, }: Props) { - if (screen === "profile") return ( - setScreen("home")} /> - ); - - if (screen === "leaderboard") return ( - setScreen("home")} /> - ); - - if (screen === "daily") return ( - setScreen("home")} currentUserId={session?.user?.id ?? undefined} /> - ); - - if (screen === "blitz") return ( - setScreen("home")} /> - ); - - if (screen === "home") return ( - <> - setShowAuth(true)} - onShowProfile={() => setScreen("profile")} - onShowLeaderboard={() => setScreen("leaderboard")} - onDaily={() => setScreen("daily")} - onBlitz={() => setScreen("blitz")} + if (screen === "profile") + return ( + setScreen("home")} /> - {showAuth && setShowAuth(false)} onSuccess={() => setShowAuth(false)} />} - - ); + ); - if (screen === "solo") return ( - { solo.reset(); setScreen("home"); }} - onNewGame={solo.start} - onRetry={solo.retryLoad} - /> - ); + if (screen === "leaderboard") + return setScreen("home")} />; - if (screen === "lobby" && multi.room && multi.playerId) return ( - multi.setSearchAllowed(v)} - /> - ); + if (screen === "daily") + return ( + setScreen("home")} + currentUserId={session?.user?.id ?? undefined} + /> + ); - if (screen === "game" && multi.room && multi.playerId) return ( - - ); + if (screen === "blitz") + return setScreen("home")} />; + + if (screen === "home") + return ( + <> + setShowAuth(true)} + onShowProfile={() => setScreen("profile")} + onShowLeaderboard={() => setScreen("leaderboard")} + onDaily={() => setScreen("daily")} + onBlitz={() => setScreen("blitz")} + /> + {showAuth && ( + setShowAuth(false)} + onSuccess={() => setShowAuth(false)} + /> + )} + + ); + + if (screen === "solo") + return ( + { + solo.reset(); + setScreen("home"); + }} + onNewGame={solo.start} + onRetry={solo.retryLoad} + /> + ); + + if (screen === "lobby" && multi.room && multi.playerId) + return ( + multi.setSearchAllowed(v)} + /> + ); + + if (screen === "game" && multi.room && multi.playerId) + return ( + + ); return null; } diff --git a/app/components/SoloScreen.tsx b/app/components/SoloScreen.tsx index 2f7fb55..8b3944c 100644 --- a/app/components/SoloScreen.tsx +++ b/app/components/SoloScreen.tsx @@ -29,75 +29,120 @@ type SoloScreenProps = { }; export function SoloScreen({ - phase, puzzle, html, title, loading, loadError, history, clicks, - elapsedDisplay, canGoBack, onStart, onNavigate, onBack, onQuit, onNewGame, onRetry, + phase, + puzzle, + html, + title, + loading, + loadError, + history, + clicks, + elapsedDisplay, + canGoBack, + onStart, + onNavigate, + onBack, + onQuit, + onNewGame, + onRetry, }: SoloScreenProps) { const breadcrumbEndRef = useRef(null); const { allowed: searchAllowed, toggle: toggleSearch } = useSearchAllowed(); useEffect(() => { - - breadcrumbEndRef.current?.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "end" }); + breadcrumbEndRef.current?.scrollIntoView({ + behavior: "smooth", + block: "nearest", + inline: "end", + }); }, [history]); - const btnPrimary = "w-full min-h-11 rounded-xl text-sm font-semibold bg-[#7c3aed] text-white hover:bg-[#6d28d9] disabled:opacity-50 cursor-pointer transition-colors"; - const btnGhost = "w-full min-h-11 rounded-xl text-sm font-semibold bg-[#242424] border border-[#2e2e2e] text-[#f0f0f0] hover:bg-[#1a1a1a] cursor-pointer transition-colors"; + const btnPrimary = + "w-full min-h-11 rounded-xl text-sm font-semibold bg-[#7c3aed] text-white hover:bg-[#6d28d9] disabled:opacity-50 cursor-pointer transition-colors"; + const btnGhost = + "w-full min-h-11 rounded-xl text-sm font-semibold bg-[#242424] border border-[#2e2e2e] text-[#f0f0f0] hover:bg-[#1a1a1a] cursor-pointer transition-colors"; - if (phase === "setup") return ( -
- -

Mode Solo

-

- Deux articles aléatoires seront choisis. Atteins l'article cible en cliquant uniquement sur les liens ! -

- - -
- ); + if (phase === "setup") + return ( +
+ +

Mode Solo

+

+ Deux articles aléatoires seront choisis. Atteins l'article cible + en cliquant uniquement sur les liens ! +

+ + +
+ ); - if (phase === "won") return ( -
-
-
🎉
-

Article atteint !

-
-
- {clicks} - clics + if (phase === "won") + return ( +
+
+
🎉
+

Article atteint !

+
+
+ + {clicks} + + clics +
+
+ + {elapsedDisplay} + + temps +
-
- {elapsedDisplay} - temps +
+ {history.map((t, i) => ( + + {i > 0 && } + + {t} + + + ))} +
+ +
+ +
-
-
- {history.map((t, i) => ( - - {i > 0 && } - {t} - - ))} -
- -
- -
-
- ); + ); // Playing return ( @@ -113,13 +158,20 @@ export function SoloScreen({ {loadError && (

{loadError}

-
)} {!loading && !loadError && html && ( - + )}
@@ -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 {