DB:
- quiz_question gains `lang` ('fr' default) and `active` (true default); migration 0002
- only active questions are served to games (pool filter)
History:
- engine writes a game_history row at game end (modes + ranked results), fire-and-forget
- public GET /api/history/:code lists a room's recent games
- lobby shows a "Parties précédentes" collapsible (date, modes, winner/scores)
Back-office:
- create form has a FR/EN language select; lang stored
- per-question active toggle (PATCH /questions/:id) — disable without deleting;
inactive rows dimmed and labelled
- seeds set lang (opentdb=en, manual/pokemon=fr)
Verified e2e: migration applied; game played → /api/history returns it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
100 lines
2.8 KiB
TypeScript
100 lines
2.8 KiB
TypeScript
// Accès aux questions de quiz en base. No-op si pas de DB (db === null).
|
|
|
|
import { and, eq, inArray, notInArray, sql } from "drizzle-orm"
|
|
import { db } from "./index"
|
|
import { quizCategory, quizPlayed, quizQuestion } from "./schema"
|
|
import type { QuizQuestion } from "../game/modes/quiz/questions"
|
|
|
|
/**
|
|
* Pool de questions mcq/truefalse non encore jouées par cette room,
|
|
* mélangées et limitées. Tableau vide si pas de DB ou rien à servir.
|
|
*/
|
|
/** Catégories existantes (pour le filtre du lobby). */
|
|
export async function listCategories(): Promise<string[]> {
|
|
if (!db) {
|
|
return []
|
|
}
|
|
const rows = await db
|
|
.select({ name: quizCategory.name })
|
|
.from(quizCategory)
|
|
.orderBy(quizCategory.name)
|
|
return rows.map((r) => r.name)
|
|
}
|
|
|
|
export async function loadQuizPool(
|
|
roomCode: string,
|
|
limit: number,
|
|
formats: string[],
|
|
categories: string[] = []
|
|
): Promise<QuizQuestion[]> {
|
|
if (!db || formats.length === 0) {
|
|
return []
|
|
}
|
|
const alreadyPlayed = db
|
|
.select({ id: quizPlayed.questionId })
|
|
.from(quizPlayed)
|
|
.where(eq(quizPlayed.roomCode, roomCode))
|
|
|
|
const rows = await db
|
|
.select({
|
|
id: quizQuestion.id,
|
|
format: quizQuestion.format,
|
|
prompt: quizQuestion.prompt,
|
|
choices: quizQuestion.choices,
|
|
correctIndex: quizQuestion.correctIndex,
|
|
acceptedAnswers: quizQuestion.acceptedAnswers,
|
|
imageUrl: quizQuestion.imageUrl,
|
|
difficulty: quizQuestion.difficulty,
|
|
category: quizCategory.name,
|
|
})
|
|
.from(quizQuestion)
|
|
.leftJoin(quizCategory, eq(quizQuestion.categoryId, quizCategory.id))
|
|
.where(
|
|
and(
|
|
eq(quizQuestion.active, true),
|
|
inArray(quizQuestion.format, formats),
|
|
categories.length > 0
|
|
? inArray(quizCategory.name, categories)
|
|
: undefined,
|
|
notInArray(quizQuestion.id, alreadyPlayed)
|
|
)
|
|
)
|
|
.orderBy(sql`random()`)
|
|
.limit(limit)
|
|
|
|
return rows
|
|
.filter((r) => {
|
|
if (r.format === "free") {
|
|
return (r.acceptedAnswers?.length ?? 0) > 0
|
|
}
|
|
if (r.format === "image_reveal") {
|
|
return !!r.imageUrl && (r.acceptedAnswers?.length ?? 0) > 0
|
|
}
|
|
return r.choices && r.correctIndex !== null
|
|
})
|
|
.map((r) => ({
|
|
id: r.id,
|
|
format: r.format as QuizQuestion["format"],
|
|
prompt: r.prompt,
|
|
choices: r.choices ?? undefined,
|
|
correctIndex: r.correctIndex ?? undefined,
|
|
acceptedAnswers: r.acceptedAnswers ?? undefined,
|
|
imageUrl: r.imageUrl ?? undefined,
|
|
category: r.category ?? "Quiz",
|
|
difficulty: r.difficulty,
|
|
}))
|
|
}
|
|
|
|
/** Marque une question comme jouée par cette room (anti-répétition). */
|
|
export async function markQuestionPlayed(
|
|
roomCode: string,
|
|
questionId: string
|
|
): Promise<void> {
|
|
if (!db) {
|
|
return
|
|
}
|
|
await db
|
|
.insert(quizPlayed)
|
|
.values({ roomCode, questionId })
|
|
.onConflictDoNothing()
|
|
}
|