nerdware/apps/server/src/db/seed-images.ts
AyoubBenziza b3efa27287 feat: game history + question lang/active management
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>
2026-06-11 17:12:07 +02:00

95 lines
3 KiB
TypeScript

// Seed du mode Images depuis PokéAPI : "devine le Pokémon" à partir de
// l'artwork officiel (URL distante, pas d'upload). Réponses acceptées FR + EN.
// bun run db:seed:images (gen 1, 151 Pokémon)
// IMAGE_SEED_COUNT=50 bun run db:seed:images
// Idempotent : déduplication par image_url (onConflictDoNothing).
import { eq } from "drizzle-orm"
import { drizzle } from "drizzle-orm/postgres-js"
import postgres from "postgres"
import { env } from "../env"
import { quizCategory, quizQuestion, type NewQuizQuestion } from "./schema"
if (!env.databaseUrl) {
console.error("DATABASE_URL manquant — impossible de seeder.")
process.exit(1)
}
const COUNT = Number(process.env.IMAGE_SEED_COUNT ?? 151)
const CATEGORY = "Pokémon"
const PROMPT = "Quel est ce Pokémon ?"
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms))
const client = postgres(env.databaseUrl, { max: 1 })
const db = drizzle(client, { schema: { quizCategory, quizQuestion } })
async function upsertCategory(name: string): Promise<string> {
await db.insert(quizCategory).values({ name }).onConflictDoNothing()
const [row] = await db
.select({ id: quizCategory.id })
.from(quizCategory)
.where(eq(quizCategory.name, name))
return row.id
}
interface Pokemon {
name: string
sprites: { other?: { ["official-artwork"]?: { front_default: string | null } } }
}
interface Species {
names: { name: string; language: { name: string } }[]
}
async function fetchPokemon(id: number): Promise<NewQuizQuestion | null> {
try {
const [pRes, sRes] = await Promise.all([
fetch(`https://pokeapi.co/api/v2/pokemon/${id}`),
fetch(`https://pokeapi.co/api/v2/pokemon-species/${id}`),
])
if (!pRes.ok || !sRes.ok) {
return null
}
const p = (await pRes.json()) as Pokemon
const s = (await sRes.json()) as Species
const art = p.sprites.other?.["official-artwork"]?.front_default
if (!art) {
return null
}
const fr = s.names.find((n) => n.language.name === "fr")?.name
const en = s.names.find((n) => n.language.name === "en")?.name ?? p.name
const answers = [...new Set([fr, en].filter(Boolean) as string[])]
return {
categoryId: "", // rempli plus bas
format: "image_reveal",
prompt: PROMPT,
difficulty: 2,
source: "pokeapi",
lang: "fr",
acceptedAnswers: answers,
imageUrl: art,
}
} catch {
return null
}
}
console.log(`Seed Images depuis PokéAPI (${COUNT} Pokémon)…`)
const categoryId = await upsertCategory(CATEGORY)
let inserted = 0
for (let id = 1; id <= COUNT; id++) {
const q = await fetchPokemon(id)
if (q) {
const res = await db
.insert(quizQuestion)
.values({ ...q, categoryId })
.onConflictDoNothing()
.returning({ id: quizQuestion.id })
inserted += res.length
}
if (id % 25 === 0) {
console.log(` ${id}/${COUNT}`)
}
await sleep(40) // courtoisie envers l'API
}
console.log(`Terminé : ${inserted} Pokémon ajoutés (mode Images).`)
await client.end()