2026-04-10 15:43:07 +02:00
|
|
|
// Route handler: POST /api/rooms - creer une room
|
|
|
|
|
// GET /api/rooms?code=XXXX - recuperer l'etat d'une room
|
|
|
|
|
|
|
|
|
|
import { NextRequest } from "next/server";
|
2026-04-11 17:37:56 +02:00
|
|
|
import { prisma } from "../../../lib/prisma";
|
2026-04-10 15:43:07 +02:00
|
|
|
|
|
|
|
|
// Types
|
|
|
|
|
|
|
|
|
|
export type Player = {
|
|
|
|
|
id: string;
|
|
|
|
|
name: string;
|
|
|
|
|
score: number;
|
|
|
|
|
currentArticle: string;
|
|
|
|
|
hasWon: boolean;
|
2026-04-11 21:58:35 +02:00
|
|
|
hasSurrendered: boolean;
|
2026-04-10 15:43:07 +02:00
|
|
|
isHost: boolean;
|
|
|
|
|
lastSeen: number; // timestamp ms
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export type Room = {
|
|
|
|
|
code: string;
|
|
|
|
|
players: Player[];
|
|
|
|
|
phase: "waiting" | "countdown" | "playing" | "results";
|
|
|
|
|
round: number;
|
|
|
|
|
totalRounds: number;
|
2026-04-11 13:47:05 +02:00
|
|
|
maxPlayers: number;
|
2026-04-11 21:58:35 +02:00
|
|
|
gameMode: "race" | "all_finish"; // race = 1er gagne, all_finish = tout le monde joue
|
2026-04-11 22:02:06 +02:00
|
|
|
searchAllowed: boolean;
|
2026-04-10 15:43:07 +02:00
|
|
|
startArticle: string;
|
|
|
|
|
targetArticle: string;
|
2026-04-11 17:37:56 +02:00
|
|
|
roundWinner: string | null;
|
|
|
|
|
countdownStart: number | null;
|
|
|
|
|
roundStart: number | null;
|
2026-04-10 15:43:07 +02:00
|
|
|
createdAt: number;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Helpers
|
|
|
|
|
|
|
|
|
|
function generateCode(): string {
|
|
|
|
|
const chars = "ABCDEFGHJKLMNPQRSTUVWXYZ";
|
|
|
|
|
let code = "";
|
|
|
|
|
for (let i = 0; i < 4; i++) {
|
|
|
|
|
code += chars[Math.floor(Math.random() * chars.length)];
|
|
|
|
|
}
|
|
|
|
|
return code;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function generatePlayerId(): string {
|
2026-04-11 13:37:50 +02:00
|
|
|
return crypto.randomUUID();
|
2026-04-10 15:43:07 +02:00
|
|
|
}
|
|
|
|
|
|
2026-04-11 17:37:56 +02:00
|
|
|
function dbToRoom(row: {
|
|
|
|
|
code: string;
|
|
|
|
|
players: unknown;
|
|
|
|
|
phase: string;
|
|
|
|
|
round: number;
|
|
|
|
|
totalRounds: number;
|
|
|
|
|
maxPlayers: number;
|
2026-04-11 21:58:35 +02:00
|
|
|
gameMode: string;
|
2026-04-11 22:02:06 +02:00
|
|
|
searchAllowed: boolean;
|
2026-04-11 17:37:56 +02:00
|
|
|
startArticle: string;
|
|
|
|
|
targetArticle: string;
|
|
|
|
|
roundWinner: string | null;
|
|
|
|
|
countdownStart: bigint | null;
|
|
|
|
|
roundStart: bigint | null;
|
|
|
|
|
createdAt: bigint;
|
|
|
|
|
}): Room {
|
|
|
|
|
return {
|
|
|
|
|
code: row.code,
|
|
|
|
|
players: row.players as Player[],
|
|
|
|
|
phase: row.phase as Room["phase"],
|
|
|
|
|
round: row.round,
|
|
|
|
|
totalRounds: row.totalRounds,
|
|
|
|
|
maxPlayers: row.maxPlayers,
|
2026-04-11 21:58:35 +02:00
|
|
|
gameMode: (row.gameMode ?? "race") as Room["gameMode"],
|
2026-04-11 22:02:06 +02:00
|
|
|
searchAllowed: row.searchAllowed ?? false,
|
2026-04-11 17:37:56 +02:00
|
|
|
startArticle: row.startArticle,
|
|
|
|
|
targetArticle: row.targetArticle,
|
|
|
|
|
roundWinner: row.roundWinner,
|
|
|
|
|
countdownStart: row.countdownStart !== null ? Number(row.countdownStart) : null,
|
|
|
|
|
roundStart: row.roundStart !== null ? Number(row.roundStart) : null,
|
|
|
|
|
createdAt: Number(row.createdAt),
|
|
|
|
|
};
|
2026-04-10 15:43:07 +02:00
|
|
|
}
|
|
|
|
|
|
2026-04-11 17:37:56 +02:00
|
|
|
// Nettoie les rooms inactives depuis plus de 2h
|
|
|
|
|
async function pruneOldRooms() {
|
|
|
|
|
const cutoff = BigInt(Date.now() - 2 * 60 * 60 * 1000);
|
|
|
|
|
await prisma.room.deleteMany({ where: { createdAt: { lt: cutoff } } });
|
|
|
|
|
}
|
2026-04-10 15:43:07 +02:00
|
|
|
|
|
|
|
|
// POST /api/rooms
|
|
|
|
|
// Body: { playerName: string }
|
|
|
|
|
// Response: { room: Room, playerId: string }
|
|
|
|
|
export async function POST(request: NextRequest) {
|
|
|
|
|
const body = await request.json();
|
2026-04-11 21:58:35 +02:00
|
|
|
const { playerName, maxPlayers, totalRounds, gameMode } = body as {
|
2026-04-11 15:20:29 +02:00
|
|
|
playerName: string;
|
|
|
|
|
maxPlayers?: number;
|
|
|
|
|
totalRounds?: number;
|
2026-04-11 21:58:35 +02:00
|
|
|
gameMode?: string;
|
2026-04-11 15:20:29 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
!playerName ||
|
|
|
|
|
typeof playerName !== "string" ||
|
|
|
|
|
playerName.trim() === ""
|
|
|
|
|
) {
|
2026-04-10 15:43:07 +02:00
|
|
|
return Response.json({ error: "Pseudo invalide" }, { status: 400 });
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-11 15:20:29 +02:00
|
|
|
const clampedMax = Math.min(
|
|
|
|
|
Math.max(typeof maxPlayers === "number" ? Math.floor(maxPlayers) : 16, 2),
|
|
|
|
|
16,
|
|
|
|
|
);
|
|
|
|
|
const clampedRounds = Math.min(
|
|
|
|
|
Math.max(typeof totalRounds === "number" ? Math.floor(totalRounds) : 3, 1),
|
|
|
|
|
10,
|
|
|
|
|
);
|
2026-04-11 13:47:05 +02:00
|
|
|
|
2026-04-11 17:37:56 +02:00
|
|
|
await pruneOldRooms();
|
2026-04-10 15:43:07 +02:00
|
|
|
|
|
|
|
|
// Generer un code unique
|
|
|
|
|
let code = generateCode();
|
|
|
|
|
let attempts = 0;
|
2026-04-11 17:37:56 +02:00
|
|
|
while (attempts < 20) {
|
|
|
|
|
const existing = await prisma.room.findUnique({ where: { code } });
|
|
|
|
|
if (!existing) break;
|
2026-04-10 15:43:07 +02:00
|
|
|
code = generateCode();
|
|
|
|
|
attempts++;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const playerId = generatePlayerId();
|
2026-04-11 17:37:56 +02:00
|
|
|
const now = BigInt(Date.now());
|
|
|
|
|
|
2026-04-11 21:58:35 +02:00
|
|
|
const clampedMode: Room["gameMode"] = gameMode === "all_finish" ? "all_finish" : "race";
|
|
|
|
|
|
2026-04-11 17:37:56 +02:00
|
|
|
const players: Player[] = [
|
|
|
|
|
{
|
|
|
|
|
id: playerId,
|
|
|
|
|
name: playerName.trim().slice(0, 20),
|
|
|
|
|
score: 0,
|
|
|
|
|
currentArticle: "",
|
|
|
|
|
hasWon: false,
|
2026-04-11 21:58:35 +02:00
|
|
|
hasSurrendered: false,
|
2026-04-11 17:37:56 +02:00
|
|
|
isHost: true,
|
|
|
|
|
lastSeen: Date.now(),
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const row = await prisma.room.create({
|
|
|
|
|
data: {
|
|
|
|
|
code,
|
|
|
|
|
players: players as object[],
|
|
|
|
|
phase: "waiting",
|
|
|
|
|
round: 0,
|
|
|
|
|
totalRounds: clampedRounds,
|
|
|
|
|
maxPlayers: clampedMax,
|
2026-04-11 21:58:35 +02:00
|
|
|
gameMode: clampedMode,
|
2026-04-11 22:02:06 +02:00
|
|
|
searchAllowed: false,
|
2026-04-11 17:37:56 +02:00
|
|
|
startArticle: "",
|
|
|
|
|
targetArticle: "",
|
|
|
|
|
roundWinner: null,
|
|
|
|
|
countdownStart: null,
|
|
|
|
|
roundStart: null,
|
|
|
|
|
createdAt: now,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const room = dbToRoom(row);
|
2026-04-10 15:43:07 +02:00
|
|
|
return Response.json({ room, playerId });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GET /api/rooms?code=XXXX
|
|
|
|
|
export async function GET(request: NextRequest) {
|
|
|
|
|
const { searchParams } = new URL(request.url);
|
|
|
|
|
const code = searchParams.get("code");
|
|
|
|
|
|
|
|
|
|
if (!code) {
|
|
|
|
|
return Response.json({ error: "Code manquant" }, { status: 400 });
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-11 17:37:56 +02:00
|
|
|
const row = await prisma.room.findUnique({ where: { code: code.toUpperCase() } });
|
2026-04-10 15:43:07 +02:00
|
|
|
|
2026-04-11 17:37:56 +02:00
|
|
|
if (!row) {
|
2026-04-10 15:43:07 +02:00
|
|
|
return Response.json({ error: "Room introuvable" }, { status: 404 });
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-11 17:37:56 +02:00
|
|
|
return Response.json({ room: dbToRoom(row) });
|
2026-04-10 15:43:07 +02:00
|
|
|
}
|