fix: sync totalRounds changes to server, filter geographic admin articles, validate user exists before saving game
This commit is contained in:
@@ -20,6 +20,14 @@ export async function POST(req: NextRequest) {
|
|||||||
won: boolean;
|
won: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const userExists = await prisma.user.findUnique({
|
||||||
|
where: { id: session.user.id },
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
if (!userExists) {
|
||||||
|
return NextResponse.json({ error: "Utilisateur introuvable" }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
const game = await prisma.game.create({
|
const game = await prisma.game.create({
|
||||||
data: {
|
data: {
|
||||||
userId: session.user.id,
|
userId: session.user.id,
|
||||||
|
|||||||
@@ -69,7 +69,10 @@ function calcPlayerPoints(player: Player, room: Room): number {
|
|||||||
let timePts: number;
|
let timePts: number;
|
||||||
if (room.timeLimit > 0) {
|
if (room.timeLimit > 0) {
|
||||||
const timeLeft = room.timeLimit * 1000 - elapsed;
|
const timeLeft = room.timeLimit * 1000 - elapsed;
|
||||||
timePts = Math.max(0, Math.floor((timeLeft / (room.timeLimit * 1000)) * 10));
|
timePts = Math.max(
|
||||||
|
0,
|
||||||
|
Math.floor((timeLeft / (room.timeLimit * 1000)) * 10),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
timePts = Math.max(0, 10 - Math.floor(elapsed / 30000));
|
timePts = Math.max(0, 10 - Math.floor(elapsed / 30000));
|
||||||
}
|
}
|
||||||
@@ -355,7 +358,10 @@ export async function PATCH(
|
|||||||
case "setGameMode": {
|
case "setGameMode": {
|
||||||
const host = room.players.find((p) => p.id === playerId);
|
const host = room.players.find((p) => p.id === playerId);
|
||||||
if (!host?.isHost) {
|
if (!host?.isHost) {
|
||||||
return Response.json({ error: "Seul l'hôte peut modifier ce paramètre" }, { status: 403 });
|
return Response.json(
|
||||||
|
{ error: "Seul l'hôte peut modifier ce paramètre" },
|
||||||
|
{ status: 403 },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (value === "race" || value === "all_finish") {
|
if (value === "race" || value === "all_finish") {
|
||||||
room.gameMode = value;
|
room.gameMode = value;
|
||||||
@@ -364,18 +370,38 @@ export async function PATCH(
|
|||||||
return Response.json({ room });
|
return Response.json({ room });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hôte change le nombre de manches
|
||||||
|
case "setTotalRounds": {
|
||||||
|
const host = room.players.find((p) => p.id === playerId);
|
||||||
|
if (!host?.isHost) {
|
||||||
|
return Response.json(
|
||||||
|
{ error: "Seul l'hôte peut modifier ce paramètre" },
|
||||||
|
{ status: 403 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (typeof value === "number") {
|
||||||
|
room.totalRounds = Math.min(Math.max(Math.floor(value), 1), 10);
|
||||||
|
await saveRoom(room);
|
||||||
|
}
|
||||||
|
return Response.json({ room });
|
||||||
|
}
|
||||||
|
|
||||||
// Hôte change la limite de temps
|
// Hôte change la limite de temps
|
||||||
case "setTimeLimit": {
|
case "setTimeLimit": {
|
||||||
const host = room.players.find((p) => p.id === playerId);
|
const host = room.players.find((p) => p.id === playerId);
|
||||||
if (!host?.isHost) {
|
if (!host?.isHost) {
|
||||||
return Response.json({ error: "Seul l'hôte peut modifier ce paramètre" }, { status: 403 });
|
return Response.json(
|
||||||
|
{ error: "Seul l'hôte peut modifier ce paramètre" },
|
||||||
|
{ status: 403 },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
room.timeLimit = typeof value === "number" ? Math.max(0, Math.floor(value)) : 0;
|
room.timeLimit =
|
||||||
|
typeof value === "number" ? Math.max(0, Math.floor(value)) : 0;
|
||||||
await saveRoom(room);
|
await saveRoom(room);
|
||||||
return Response.json({ room });
|
return Response.json({ room });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fin du temps imparti — déclenché par le client qui détecte l'expiration
|
// Fin du temps imparti - déclenché par le client qui détecte l'expiration
|
||||||
case "timeUp": {
|
case "timeUp": {
|
||||||
if (room.phase !== "playing") {
|
if (room.phase !== "playing") {
|
||||||
return Response.json({ room });
|
return Response.json({ room });
|
||||||
|
|||||||
@@ -24,9 +24,7 @@ const FORBIDDEN_NAMESPACES = [
|
|||||||
"WP:",
|
"WP:",
|
||||||
];
|
];
|
||||||
|
|
||||||
const REMOVED_SECTION_IDS = [
|
const REMOVED_SECTION_IDS = ["Liens_externes"];
|
||||||
"Liens_externes",
|
|
||||||
];
|
|
||||||
|
|
||||||
function getHeadingId(el: Element): string {
|
function getHeadingId(el: Element): string {
|
||||||
// Nouvelle structure Wikipedia : <div class="mw-heading"><h2 id="...">
|
// Nouvelle structure Wikipedia : <div class="mw-heading"><h2 id="...">
|
||||||
@@ -72,7 +70,7 @@ function cleanWikiHtml(container: HTMLElement): void {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Supprimer les sections indésirables — supporte ancienne et nouvelle structure Wikipedia
|
// Supprimer les sections indésirables - supporte ancienne et nouvelle structure Wikipedia
|
||||||
// Nouvelle : <div class="mw-heading mw-heading2"> / Ancienne : <h2><span id="...">
|
// Nouvelle : <div class="mw-heading mw-heading2"> / Ancienne : <h2><span id="...">
|
||||||
const headingSelectors = "h2, h3, .mw-heading";
|
const headingSelectors = "h2, h3, .mw-heading";
|
||||||
container.querySelectorAll(headingSelectors).forEach((heading) => {
|
container.querySelectorAll(headingSelectors).forEach((heading) => {
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ type LobbyScreenProps = {
|
|||||||
gameMode: Room["gameMode"];
|
gameMode: Room["gameMode"];
|
||||||
setGameMode: (v: Room["gameMode"]) => void;
|
setGameMode: (v: Room["gameMode"]) => void;
|
||||||
onSetGameMode: (v: Room["gameMode"]) => void;
|
onSetGameMode: (v: Room["gameMode"]) => void;
|
||||||
|
onSetTotalRounds: (v: number) => void;
|
||||||
onSetSearchAllowed: (v: boolean) => void;
|
onSetSearchAllowed: (v: boolean) => void;
|
||||||
onSetTimeLimit: (v: number) => void;
|
onSetTimeLimit: (v: number) => void;
|
||||||
};
|
};
|
||||||
@@ -39,6 +40,7 @@ export function LobbyScreen({
|
|||||||
gameMode,
|
gameMode,
|
||||||
setGameMode,
|
setGameMode,
|
||||||
onSetGameMode,
|
onSetGameMode,
|
||||||
|
onSetTotalRounds,
|
||||||
onSetSearchAllowed,
|
onSetSearchAllowed,
|
||||||
onSetTimeLimit,
|
onSetTimeLimit,
|
||||||
}: LobbyScreenProps) {
|
}: LobbyScreenProps) {
|
||||||
@@ -180,8 +182,8 @@ export function LobbyScreen({
|
|||||||
<div className="flex-1 flex flex-col gap-1.5">
|
<div className="flex-1 flex flex-col gap-1.5">
|
||||||
<span className="text-xs text-[#888]">Manches</span>
|
<span className="text-xs text-[#888]">Manches</span>
|
||||||
<select
|
<select
|
||||||
value={totalRounds}
|
value={room.totalRounds}
|
||||||
onChange={(e) => setTotalRounds(Number(e.target.value))}
|
onChange={(e) => { const v = Number(e.target.value); setTotalRounds(v); onSetTotalRounds(v); }}
|
||||||
className="w-full min-h-11 px-2 bg-[#0f0f0f] border border-[#2e2e2e] rounded-xl text-sm text-[#f0f0f0] outline-none focus:border-[#7c3aed] cursor-pointer transition-colors"
|
className="w-full min-h-11 px-2 bg-[#0f0f0f] border border-[#2e2e2e] rounded-xl text-sm text-[#f0f0f0] outline-none focus:border-[#7c3aed] cursor-pointer transition-colors"
|
||||||
>
|
>
|
||||||
{[1, 2, 3, 4, 5, 7, 10].map((n) => (
|
{[1, 2, 3, 4, 5, 7, 10].map((n) => (
|
||||||
|
|||||||
@@ -159,6 +159,7 @@ export function ScreenRouter({
|
|||||||
gameMode={gameMode}
|
gameMode={gameMode}
|
||||||
setGameMode={setGameMode}
|
setGameMode={setGameMode}
|
||||||
onSetGameMode={(v) => multi.setGameMode(v)}
|
onSetGameMode={(v) => multi.setGameMode(v)}
|
||||||
|
onSetTotalRounds={(v) => multi.setTotalRounds(v)}
|
||||||
onSetSearchAllowed={(v) => multi.setSearchAllowed(v)}
|
onSetSearchAllowed={(v) => multi.setSearchAllowed(v)}
|
||||||
onSetTimeLimit={(v) => multi.setTimeLimit(v)}
|
onSetTimeLimit={(v) => multi.setTimeLimit(v)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
+51
-12
@@ -151,7 +151,12 @@ export function useMultiGame() {
|
|||||||
if (timeLimitTimerRef.current) clearInterval(timeLimitTimerRef.current);
|
if (timeLimitTimerRef.current) clearInterval(timeLimitTimerRef.current);
|
||||||
if (room.timeLimit > 0) {
|
if (room.timeLimit > 0) {
|
||||||
const tick = () => {
|
const tick = () => {
|
||||||
const left = Math.ceil(((room.roundStart ?? Date.now()) + room.timeLimit * 1000 - Date.now()) / 1000);
|
const left = Math.ceil(
|
||||||
|
((room.roundStart ?? Date.now()) +
|
||||||
|
room.timeLimit * 1000 -
|
||||||
|
Date.now()) /
|
||||||
|
1000,
|
||||||
|
);
|
||||||
setTimeLeft(left <= 0 ? 0 : left);
|
setTimeLeft(left <= 0 ? 0 : left);
|
||||||
};
|
};
|
||||||
tick();
|
tick();
|
||||||
@@ -162,7 +167,10 @@ export function useMultiGame() {
|
|||||||
}
|
}
|
||||||
if (room.phase === "results" && prevPhase !== "results") {
|
if (room.phase === "results" && prevPhase !== "results") {
|
||||||
timer.stop();
|
timer.stop();
|
||||||
if (timeLimitTimerRef.current) { clearInterval(timeLimitTimerRef.current); timeLimitTimerRef.current = null; }
|
if (timeLimitTimerRef.current) {
|
||||||
|
clearInterval(timeLimitTimerRef.current);
|
||||||
|
timeLimitTimerRef.current = null;
|
||||||
|
}
|
||||||
setTimeLeft(null);
|
setTimeLeft(null);
|
||||||
}
|
}
|
||||||
if (room.round !== prevRound && room.phase === "playing") {
|
if (room.round !== prevRound && room.phase === "playing") {
|
||||||
@@ -173,14 +181,24 @@ export function useMultiGame() {
|
|||||||
|
|
||||||
// Temps limité : envoie timeUp quand le compteur atteint 0
|
// Temps limité : envoie timeUp quand le compteur atteint 0
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (timeLeft !== 0 || !room || room.phase !== "playing" || !playerId || !room.timeLimit) return;
|
if (
|
||||||
|
timeLeft !== 0 ||
|
||||||
|
!room ||
|
||||||
|
room.phase !== "playing" ||
|
||||||
|
!playerId ||
|
||||||
|
!room.timeLimit
|
||||||
|
)
|
||||||
|
return;
|
||||||
fetch(`/api/rooms/${room.code}`, {
|
fetch(`/api/rooms/${room.code}`, {
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ action: "timeUp", playerId }),
|
body: JSON.stringify({ action: "timeUp", playerId }),
|
||||||
}).then((r) => r.json()).then((d) => {
|
})
|
||||||
|
.then((r) => r.json())
|
||||||
|
.then((d) => {
|
||||||
if ((d as { room: Room }).room) setRoom((d as { room: Room }).room);
|
if ((d as { room: Room }).room) setRoom((d as { room: Room }).room);
|
||||||
}).catch(() => {});
|
})
|
||||||
|
.catch(() => {});
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [timeLeft]);
|
}, [timeLeft]);
|
||||||
|
|
||||||
@@ -327,12 +345,16 @@ export function useMultiGame() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const goBack = useCallback(async () => {
|
const goBack = useCallback(async () => {
|
||||||
if (!room || !playerId || loadingRef.current || room.phase !== "playing") return;
|
if (!room || !playerId || loadingRef.current || room.phase !== "playing")
|
||||||
|
return;
|
||||||
if (historyRef.current.length <= 1) return;
|
if (historyRef.current.length <= 1) return;
|
||||||
|
|
||||||
clicksRef.current += 1;
|
clicksRef.current += 1;
|
||||||
setClicksDisplay(clicksRef.current);
|
setClicksDisplay(clicksRef.current);
|
||||||
if (!timerStartedRef.current) { timer.start(); timerStartedRef.current = true; }
|
if (!timerStartedRef.current) {
|
||||||
|
timer.start();
|
||||||
|
timerStartedRef.current = true;
|
||||||
|
}
|
||||||
|
|
||||||
const newHistory = historyRef.current.slice(0, -1);
|
const newHistory = historyRef.current.slice(0, -1);
|
||||||
const target = newHistory[newHistory.length - 1];
|
const target = newHistory[newHistory.length - 1];
|
||||||
@@ -354,10 +376,16 @@ export function useMultiGame() {
|
|||||||
const res = await fetch(`/api/rooms/${room.code}`, {
|
const res = await fetch(`/api/rooms/${room.code}`, {
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ action: "navigate", playerId, article: canonical }),
|
body: JSON.stringify({
|
||||||
|
action: "navigate",
|
||||||
|
playerId,
|
||||||
|
article: canonical,
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
if (res.ok) setRoom((await res.json() as { room: Room }).room);
|
if (res.ok) setRoom(((await res.json()) as { room: Room }).room);
|
||||||
} catch { /* on continue localement */ }
|
} catch {
|
||||||
|
/* on continue localement */
|
||||||
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [room, playerId]);
|
}, [room, playerId]);
|
||||||
|
|
||||||
@@ -368,7 +396,7 @@ export function useMultiGame() {
|
|||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ action: "setGameMode", playerId, value }),
|
body: JSON.stringify({ action: "setGameMode", 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 setTimeLimit(value: number) {
|
async function setTimeLimit(value: number) {
|
||||||
@@ -378,7 +406,17 @@ export function useMultiGame() {
|
|||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ action: "setTimeLimit", playerId, value }),
|
body: JSON.stringify({ action: "setTimeLimit", 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 setTotalRounds(value: number) {
|
||||||
|
if (!room || !playerId) return;
|
||||||
|
const res = await fetch(`/api/rooms/${room.code}`, {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ action: "setTotalRounds", playerId, value }),
|
||||||
|
});
|
||||||
|
if (res.ok) setRoom(((await res.json()) as { room: Room }).room);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setSearchAllowed(value: boolean) {
|
async function setSearchAllowed(value: boolean) {
|
||||||
@@ -479,6 +517,7 @@ export function useMultiGame() {
|
|||||||
surrender,
|
surrender,
|
||||||
timeLeft,
|
timeLeft,
|
||||||
setGameMode,
|
setGameMode,
|
||||||
|
setTotalRounds,
|
||||||
setTimeLimit,
|
setTimeLimit,
|
||||||
setSearchAllowed,
|
setSearchAllowed,
|
||||||
restore,
|
restore,
|
||||||
|
|||||||
+21
-3
@@ -3,8 +3,25 @@ import { getFallbackPuzzle } from "./puzzles";
|
|||||||
|
|
||||||
const WIKI_API_BASE = "https://fr.wikipedia.org/w/api.php";
|
const WIKI_API_BASE = "https://fr.wikipedia.org/w/api.php";
|
||||||
const MIN_ARTICLE_BYTES = 10000;
|
const MIN_ARTICLE_BYTES = 10000;
|
||||||
const BAD_TITLE_PREFIXES = ["Liste de", "Liste des", "Index de", "Portail:"];
|
const BAD_TITLE_PREFIXES = ["Index de", "Portail:"];
|
||||||
const BAD_TITLE_SUFFIXES = ["(homonymie)", "(disambiguation)"];
|
const BAD_TITLE_SUFFIXES = [
|
||||||
|
"(disambiguation)",
|
||||||
|
// Géographie administrative française
|
||||||
|
"(département)",
|
||||||
|
"(canton)",
|
||||||
|
"(commune)",
|
||||||
|
"(arrondissement)",
|
||||||
|
"(circonscription législative)",
|
||||||
|
"(région)",
|
||||||
|
];
|
||||||
|
const BAD_TITLE_PATTERNS = [
|
||||||
|
// Cantons français : "Canton de X", "Canton de X (Yvelines)", etc
|
||||||
|
/^Canton (de |d'|du )/i,
|
||||||
|
// Arrondissements
|
||||||
|
/^Arrondissement (de |d'|du )/i,
|
||||||
|
// Communes très petites ou listes géo
|
||||||
|
/^Communes (de |d'|du )/i,
|
||||||
|
];
|
||||||
|
|
||||||
// Cache de promesses module-level
|
// Cache de promesses module-level
|
||||||
const articleCache = new Map<string, Promise<WikiArticle | null>>();
|
const articleCache = new Map<string, Promise<WikiArticle | null>>();
|
||||||
@@ -66,7 +83,8 @@ function isGoodArticle(page: WikiPageInfo): boolean {
|
|||||||
return (
|
return (
|
||||||
page.length >= MIN_ARTICLE_BYTES &&
|
page.length >= MIN_ARTICLE_BYTES &&
|
||||||
!BAD_TITLE_PREFIXES.some((p) => t.startsWith(p)) &&
|
!BAD_TITLE_PREFIXES.some((p) => t.startsWith(p)) &&
|
||||||
!BAD_TITLE_SUFFIXES.some((s) => t.endsWith(s))
|
!BAD_TITLE_SUFFIXES.some((s) => t.endsWith(s)) &&
|
||||||
|
!BAD_TITLE_PATTERNS.some((r) => r.test(t))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user