Compare commits
No commits in common. "4225b7b43e1eaf6581f33881143d1a107b0aac68" and "ed1747543bec5b6dd7f02bec60ae722aa0771377" have entirely different histories.
4225b7b43e
...
ed1747543b
15 changed files with 6 additions and 530 deletions
|
|
@ -1,2 +0,0 @@
|
|||
ALTER TABLE "quiz_question" ADD COLUMN "lang" text DEFAULT 'fr' NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "quiz_question" ADD COLUMN "active" boolean DEFAULT true NOT NULL;
|
||||
|
|
@ -1,302 +0,0 @@
|
|||
{
|
||||
"id": "7df3d3d1-dbf0-445e-a5c1-5ef4872a80de",
|
||||
"prevId": "6de8c2ee-3634-455b-ba4b-372368fc2e66",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
"public.game_history": {
|
||||
"name": "game_history",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"room_code": {
|
||||
"name": "room_code",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"played_at": {
|
||||
"name": "played_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"modes": {
|
||||
"name": "modes",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"results": {
|
||||
"name": "results",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.quiz_category": {
|
||||
"name": "quiz_category",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"quiz_category_name_unique": {
|
||||
"name": "quiz_category_name_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"name"
|
||||
]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.quiz_played": {
|
||||
"name": "quiz_played",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"question_id": {
|
||||
"name": "question_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"room_code": {
|
||||
"name": "room_code",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"played_at": {
|
||||
"name": "played_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"quiz_played_question_id_quiz_question_id_fk": {
|
||||
"name": "quiz_played_question_id_quiz_question_id_fk",
|
||||
"tableFrom": "quiz_played",
|
||||
"tableTo": "quiz_question",
|
||||
"columnsFrom": [
|
||||
"question_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {
|
||||
"quiz_played_question_id_room_code_pk": {
|
||||
"name": "quiz_played_question_id_room_code_pk",
|
||||
"columns": [
|
||||
"question_id",
|
||||
"room_code"
|
||||
]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.quiz_question": {
|
||||
"name": "quiz_question",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"category_id": {
|
||||
"name": "category_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"format": {
|
||||
"name": "format",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"prompt": {
|
||||
"name": "prompt",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"difficulty": {
|
||||
"name": "difficulty",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": 1
|
||||
},
|
||||
"source": {
|
||||
"name": "source",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"lang": {
|
||||
"name": "lang",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'fr'"
|
||||
},
|
||||
"active": {
|
||||
"name": "active",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": true
|
||||
},
|
||||
"choices": {
|
||||
"name": "choices",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"correct_index": {
|
||||
"name": "correct_index",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"accepted_answers": {
|
||||
"name": "accepted_answers",
|
||||
"type": "jsonb",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"image_url": {
|
||||
"name": "image_url",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"quiz_question_prompt_unique": {
|
||||
"name": "quiz_question_prompt_unique",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "prompt",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": true,
|
||||
"where": "\"quiz_question\".\"format\" <> 'image_reveal'",
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
},
|
||||
"quiz_question_image_unique": {
|
||||
"name": "quiz_question_image_unique",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "image_url",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": true,
|
||||
"where": "\"quiz_question\".\"image_url\" is not null",
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"quiz_question_category_id_quiz_category_id_fk": {
|
||||
"name": "quiz_question_category_id_quiz_category_id_fk",
|
||||
"tableFrom": "quiz_question",
|
||||
"tableTo": "quiz_category",
|
||||
"columnsFrom": [
|
||||
"category_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
}
|
||||
},
|
||||
"enums": {},
|
||||
"schemas": {},
|
||||
"sequences": {},
|
||||
"roles": {},
|
||||
"policies": {},
|
||||
"views": {},
|
||||
"_meta": {
|
||||
"columns": {},
|
||||
"schemas": {},
|
||||
"tables": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -15,13 +15,6 @@
|
|||
"when": 1781129455626,
|
||||
"tag": "0001_fancy_captain_stacy",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 2,
|
||||
"version": "7",
|
||||
"when": 1781185531830,
|
||||
"tag": "0002_lowly_tenebrous",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -19,7 +19,6 @@ interface CreateBody {
|
|||
prompt?: string
|
||||
category?: string
|
||||
difficulty?: number
|
||||
lang?: string
|
||||
choices?: string[]
|
||||
correctIndex?: number
|
||||
acceptedAnswers?: string[]
|
||||
|
|
@ -47,14 +46,7 @@ function validate(
|
|||
return { error: "Difficulté invalide (1 à 3)." }
|
||||
}
|
||||
|
||||
const base = {
|
||||
categoryId,
|
||||
format,
|
||||
prompt,
|
||||
difficulty,
|
||||
source: "manual",
|
||||
lang: body.lang === "en" ? "en" : "fr",
|
||||
}
|
||||
const base = { categoryId, format, prompt, difficulty, source: "manual" }
|
||||
|
||||
if (format === "free" || format === "image_reveal") {
|
||||
const answers = (body.acceptedAnswers ?? [])
|
||||
|
|
@ -146,8 +138,6 @@ export async function adminRoutes(app: FastifyInstance) {
|
|||
correctIndex: quizQuestion.correctIndex,
|
||||
acceptedAnswers: quizQuestion.acceptedAnswers,
|
||||
imageUrl: quizQuestion.imageUrl,
|
||||
lang: quizQuestion.lang,
|
||||
active: quizQuestion.active,
|
||||
category: quizCategory.name,
|
||||
})
|
||||
.from(quizQuestion)
|
||||
|
|
@ -184,25 +174,6 @@ export async function adminRoutes(app: FastifyInstance) {
|
|||
return reply.code(201).send({ id: inserted[0].id })
|
||||
})
|
||||
|
||||
app.patch<{ Params: { id: string }; Body: { active?: boolean } }>(
|
||||
"/questions/:id",
|
||||
async (req, reply) => {
|
||||
const active = req.body?.active
|
||||
if (typeof active !== "boolean") {
|
||||
return reply.code(400).send({ error: "Champ 'active' requis." })
|
||||
}
|
||||
const updated = await db!
|
||||
.update(quizQuestion)
|
||||
.set({ active })
|
||||
.where(eq(quizQuestion.id, req.params.id))
|
||||
.returning({ id: quizQuestion.id })
|
||||
if (updated.length === 0) {
|
||||
return reply.code(404).send({ error: "Question introuvable." })
|
||||
}
|
||||
return { ok: true }
|
||||
}
|
||||
)
|
||||
|
||||
app.delete<{ Params: { id: string } }>("/questions/:id", async (req, reply) => {
|
||||
const deleted = await db!
|
||||
.delete(quizQuestion)
|
||||
|
|
|
|||
|
|
@ -1,55 +0,0 @@
|
|||
// Historique de parties (table game_history). No-op si pas de DB.
|
||||
|
||||
import { desc, eq } from "drizzle-orm"
|
||||
import { db } from "./index"
|
||||
import { gameHistory } from "./schema"
|
||||
|
||||
export interface GameResultEntry {
|
||||
name: string
|
||||
score: number
|
||||
}
|
||||
|
||||
export async function saveGameHistory(
|
||||
roomCode: string,
|
||||
modes: string[],
|
||||
results: GameResultEntry[]
|
||||
): Promise<void> {
|
||||
if (!db) {
|
||||
return
|
||||
}
|
||||
await db.insert(gameHistory).values({ roomCode, modes, results })
|
||||
}
|
||||
|
||||
export interface GameHistoryRow {
|
||||
id: string
|
||||
playedAt: string
|
||||
modes: string[]
|
||||
results: GameResultEntry[]
|
||||
}
|
||||
|
||||
export async function listGameHistory(
|
||||
roomCode: string,
|
||||
limit = 10
|
||||
): Promise<GameHistoryRow[]> {
|
||||
if (!db) {
|
||||
return []
|
||||
}
|
||||
const rows = await db
|
||||
.select({
|
||||
id: gameHistory.id,
|
||||
playedAt: gameHistory.playedAt,
|
||||
modes: gameHistory.modes,
|
||||
results: gameHistory.results,
|
||||
})
|
||||
.from(gameHistory)
|
||||
.where(eq(gameHistory.roomCode, roomCode))
|
||||
.orderBy(desc(gameHistory.playedAt))
|
||||
.limit(limit)
|
||||
return rows.map((r) => ({
|
||||
id: r.id,
|
||||
playedAt:
|
||||
r.playedAt instanceof Date ? r.playedAt.toISOString() : String(r.playedAt),
|
||||
modes: (r.modes as string[]) ?? [],
|
||||
results: (r.results as GameResultEntry[]) ?? [],
|
||||
}))
|
||||
}
|
||||
|
|
@ -51,7 +51,6 @@ export async function loadQuizPool(
|
|||
.leftJoin(quizCategory, eq(quizQuestion.categoryId, quizCategory.id))
|
||||
.where(
|
||||
and(
|
||||
eq(quizQuestion.active, true),
|
||||
inArray(quizQuestion.format, formats),
|
||||
categories.length > 0
|
||||
? inArray(quizCategory.name, categories)
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
|
||||
import { sql } from "drizzle-orm"
|
||||
import {
|
||||
boolean,
|
||||
integer,
|
||||
jsonb,
|
||||
pgTable,
|
||||
|
|
@ -32,12 +31,8 @@ export const quizQuestion = pgTable(
|
|||
prompt: text("prompt").notNull(),
|
||||
/** 1 (facile) .. 3 (difficile). */
|
||||
difficulty: integer("difficulty").notNull().default(1),
|
||||
/** 'opentdb' | 'manual' | 'pokeapi' */
|
||||
/** 'opentdb' | 'manual' */
|
||||
source: text("source").notNull(),
|
||||
/** Langue de la question ('fr' | 'en'). */
|
||||
lang: text("lang").notNull().default("fr"),
|
||||
/** Désactivée (back-office) : exclue des parties sans être supprimée. */
|
||||
active: boolean("active").notNull().default(true),
|
||||
// Champs selon le format (NULL si non applicable) :
|
||||
choices: jsonb("choices").$type<string[]>(),
|
||||
correctIndex: integer("correct_index"),
|
||||
|
|
|
|||
|
|
@ -64,7 +64,6 @@ async function fetchPokemon(id: number): Promise<NewQuizQuestion | null> {
|
|||
prompt: PROMPT,
|
||||
difficulty: 2,
|
||||
source: "pokeapi",
|
||||
lang: "fr",
|
||||
acceptedAnswers: answers,
|
||||
imageUrl: art,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -113,7 +113,6 @@ function toQuestion(
|
|||
prompt,
|
||||
difficulty,
|
||||
source: "opentdb",
|
||||
lang: "en",
|
||||
choices: ["True", "False"],
|
||||
correctIndex: correct === "True" ? 0 : 1,
|
||||
}
|
||||
|
|
@ -129,7 +128,6 @@ function toQuestion(
|
|||
prompt,
|
||||
difficulty,
|
||||
source: "opentdb",
|
||||
lang: "en",
|
||||
choices,
|
||||
correctIndex: choices.indexOf(correct),
|
||||
}
|
||||
|
|
@ -179,7 +177,6 @@ async function seedManual(): Promise<number> {
|
|||
prompt: q.prompt,
|
||||
difficulty: q.difficulty,
|
||||
source: "manual",
|
||||
lang: "fr",
|
||||
choices: q.choices,
|
||||
correctIndex: q.correctIndex,
|
||||
acceptedAnswers: q.acceptedAnswers,
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import type {
|
|||
} from "@nerdware/shared"
|
||||
import type { IoServer } from "../socket"
|
||||
import type { RoomManager, ServerRoom } from "../rooms"
|
||||
import { saveGameHistory } from "../db/history-repo"
|
||||
import { createRound as defaultCreateRound, type RoundFactory } from "./registry"
|
||||
import type { GameRound, RoomGameController, RoundContext } from "./round"
|
||||
|
||||
|
|
@ -88,18 +87,6 @@ export class GameEngine implements RoomGameController {
|
|||
tracks: tracks.length > 0 ? tracks : undefined,
|
||||
recap: this.history.length > 0 ? this.history : undefined,
|
||||
})
|
||||
|
||||
// Historique (fire-and-forget, DB optionnelle).
|
||||
const modes = [...new Set(this.room.settings.rounds.map((r) => r.type))]
|
||||
const results = this.scoreboard()
|
||||
.map((s) => ({
|
||||
name: this.room.players.get(s.playerId)?.name ?? "?",
|
||||
score: s.score,
|
||||
}))
|
||||
.sort((a, b) => b.score - a.score)
|
||||
void saveGameHistory(this.room.code, modes, results).catch((err) =>
|
||||
console.error("[history] save failed", err)
|
||||
)
|
||||
}
|
||||
|
||||
/** Vote d'un joueur pendant une manche. Délègue à l'épreuve, puis check fin anticipée. */
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import { env, isDev } from "./env"
|
|||
import { createSocketServer } from "./socket"
|
||||
import { adminRoutes } from "./admin/routes"
|
||||
import { listCategories } from "./db/quiz-repo"
|
||||
import { listGameHistory } from "./db/history-repo"
|
||||
import "./game/modes" // enregistre les épreuves (registerRound)
|
||||
|
||||
const app = Fastify({
|
||||
|
|
@ -30,11 +29,6 @@ app.get("/health", async () => ({ status: "ok", uptime: process.uptime() }))
|
|||
// Catégories de quiz disponibles (public, pour le filtre du lobby).
|
||||
app.get("/api/categories", async () => listCategories())
|
||||
|
||||
// Historique des parties d'une room (public).
|
||||
app.get<{ Params: { code: string } }>("/api/history/:code", async (req) =>
|
||||
listGameHistory(req.params.code.toUpperCase())
|
||||
)
|
||||
|
||||
await app.register(adminRoutes, { prefix: "/api/admin" })
|
||||
|
||||
// Socket.IO se greffe sur le serveur HTTP sous-jacent de Fastify.
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ import {
|
|||
} from "lucide-react"
|
||||
import { SiYoutube } from "@icons-pack/react-simple-icons"
|
||||
import { fetchCategories } from "@/lib/categories"
|
||||
import { fetchHistory } from "@/lib/history"
|
||||
import type {
|
||||
BlindtestMode,
|
||||
GameType,
|
||||
|
|
@ -141,11 +140,6 @@ export function LobbyView({ snapshot }: { snapshot: RoomSnapshot }) {
|
|||
snapshot.settings
|
||||
const allCategories =
|
||||
useQuery({ queryKey: ["categories"], queryFn: fetchCategories }).data ?? []
|
||||
const history =
|
||||
useQuery({
|
||||
queryKey: ["history", snapshot.code],
|
||||
queryFn: () => fetchHistory(snapshot.code),
|
||||
}).data ?? []
|
||||
|
||||
const [quizCount, setQuizCount] = useState(5)
|
||||
const [imageCount, setImageCount] = useState(5)
|
||||
|
|
@ -439,38 +433,6 @@ export function LobbyView({ snapshot }: { snapshot: RoomSnapshot }) {
|
|||
)}
|
||||
|
||||
{error && <p className="text-destructive text-center text-sm">{error}</p>}
|
||||
|
||||
{history.length > 0 && (
|
||||
<details className="rounded-xl border text-left">
|
||||
<summary className="text-muted-foreground cursor-pointer p-3 text-sm font-medium select-none">
|
||||
Parties précédentes ({history.length})
|
||||
</summary>
|
||||
<ul className="flex flex-col gap-2 p-3 pt-0">
|
||||
{history.map((g) => {
|
||||
const top = [...g.results].sort((a, b) => b.score - a.score)
|
||||
return (
|
||||
<li key={g.id} className="bg-muted/40 rounded-lg p-2 text-xs">
|
||||
<p className="text-muted-foreground text-[10px] uppercase">
|
||||
{new Date(g.playedAt).toLocaleString("fr-FR", {
|
||||
dateStyle: "short",
|
||||
timeStyle: "short",
|
||||
})}
|
||||
{" · "}
|
||||
{g.modes.join(", ")}
|
||||
</p>
|
||||
<p>
|
||||
🏆 {top[0]?.name ?? "?"}
|
||||
<span className="text-muted-foreground">
|
||||
{" "}
|
||||
— {top.map((r) => `${r.name} ${r.score}`).join(" · ")}
|
||||
</span>
|
||||
</p>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</details>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -25,8 +25,6 @@ export interface AdminQuestion {
|
|||
correctIndex: number | null
|
||||
acceptedAnswers: string[] | null
|
||||
imageUrl: string | null
|
||||
lang: string
|
||||
active: boolean
|
||||
category: string | null
|
||||
}
|
||||
|
||||
|
|
@ -35,7 +33,6 @@ export interface NewQuestionInput {
|
|||
prompt: string
|
||||
category: string
|
||||
difficulty: number
|
||||
lang?: string
|
||||
choices?: string[]
|
||||
correctIndex?: number
|
||||
acceptedAnswers?: string[]
|
||||
|
|
@ -86,11 +83,6 @@ export const adminApi = {
|
|||
method: "POST",
|
||||
body: JSON.stringify(input),
|
||||
}),
|
||||
setActive: (id: string, active: boolean) =>
|
||||
request<{ ok: true }>(`/questions/${id}`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ active }),
|
||||
}),
|
||||
deleteQuestion: (id: string) =>
|
||||
request<{ ok: true }>(`/questions/${id}`, { method: "DELETE" }),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,22 +0,0 @@
|
|||
// Historique public des parties d'une room.
|
||||
|
||||
const SERVER_URL = import.meta.env.VITE_SERVER_URL ?? "http://localhost:3001"
|
||||
|
||||
export interface GameHistoryEntry {
|
||||
id: string
|
||||
playedAt: string
|
||||
modes: string[]
|
||||
results: { name: string; score: number }[]
|
||||
}
|
||||
|
||||
export async function fetchHistory(code: string): Promise<GameHistoryEntry[]> {
|
||||
try {
|
||||
const res = await fetch(`${SERVER_URL}/api/history/${code}`)
|
||||
if (!res.ok) {
|
||||
return []
|
||||
}
|
||||
return (await res.json()) as GameHistoryEntry[]
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { useRef, useState } from "react"
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
|
||||
import { Link } from "wouter"
|
||||
import { Eye, EyeOff, Trash2, Upload } from "lucide-react"
|
||||
import { Trash2, Upload } from "lucide-react"
|
||||
import type { QuizFormat } from "@nerdware/shared"
|
||||
import { Button } from "@workspace/ui/components/button"
|
||||
import {
|
||||
|
|
@ -86,12 +86,6 @@ function AdminPanel({ onLogout }: { onLogout: () => void }) {
|
|||
onSuccess: () => qc.invalidateQueries({ queryKey: ["admin-questions"] }),
|
||||
})
|
||||
|
||||
const toggle = useMutation({
|
||||
mutationFn: ({ id, active }: { id: string; active: boolean }) =>
|
||||
adminApi.setActive(id, active),
|
||||
onSuccess: () => qc.invalidateQueries({ queryKey: ["admin-questions"] }),
|
||||
})
|
||||
|
||||
const unauthorized =
|
||||
questions.isError && /401|autoris/i.test(String(questions.error))
|
||||
|
||||
|
|
@ -132,7 +126,6 @@ function AdminPanel({ onLogout }: { onLogout: () => void }) {
|
|||
key={q.id}
|
||||
q={q}
|
||||
onDelete={() => remove.mutate(q.id)}
|
||||
onToggle={() => toggle.mutate({ id: q.id, active: !q.active })}
|
||||
deleting={remove.isPending}
|
||||
/>
|
||||
))}
|
||||
|
|
@ -147,20 +140,14 @@ function AdminPanel({ onLogout }: { onLogout: () => void }) {
|
|||
function QuestionRow({
|
||||
q,
|
||||
onDelete,
|
||||
onToggle,
|
||||
deleting,
|
||||
}: {
|
||||
q: AdminQuestion
|
||||
onDelete: () => void
|
||||
onToggle: () => void
|
||||
deleting: boolean
|
||||
}) {
|
||||
return (
|
||||
<li
|
||||
className={`bg-muted/40 flex items-start justify-between gap-3 rounded-lg p-3 ${
|
||||
q.active ? "" : "opacity-50"
|
||||
}`}
|
||||
>
|
||||
<li className="bg-muted/40 flex items-start justify-between gap-3 rounded-lg p-3">
|
||||
{q.format === "image_reveal" && q.imageUrl && (
|
||||
<img
|
||||
src={assetUrl(q.imageUrl)}
|
||||
|
|
@ -171,9 +158,7 @@ function QuestionRow({
|
|||
<div className="min-w-0 flex-1">
|
||||
<p className="text-sm font-medium">{q.prompt}</p>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{q.format} · {q.category ?? "—"} · diff. {q.difficulty} ·{" "}
|
||||
{q.lang.toUpperCase()} · {q.source}
|
||||
{!q.active && " · désactivée"}
|
||||
{q.format} · {q.category ?? "—"} · diff. {q.difficulty} · {q.source}
|
||||
</p>
|
||||
{q.format === "free" || q.format === "image_reveal" ? (
|
||||
<p className="text-muted-foreground text-xs">
|
||||
|
|
@ -187,14 +172,6 @@ function QuestionRow({
|
|||
</p>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
size="icon-sm"
|
||||
variant="secondary"
|
||||
onClick={onToggle}
|
||||
title={q.active ? "Désactiver" : "Activer"}
|
||||
>
|
||||
{q.active ? <Eye /> : <EyeOff />}
|
||||
</Button>
|
||||
<Button
|
||||
size="icon-sm"
|
||||
variant="secondary"
|
||||
|
|
@ -214,7 +191,6 @@ function QuestionForm({ onCreated }: { onCreated: () => void }) {
|
|||
const [prompt, setPrompt] = useState("")
|
||||
const [category, setCategory] = useState("")
|
||||
const [difficulty, setDifficulty] = useState(1)
|
||||
const [lang, setLang] = useState("fr")
|
||||
const [choices, setChoices] = useState<string[]>(EMPTY_CHOICES)
|
||||
const [correctIndex, setCorrectIndex] = useState(0)
|
||||
const [answers, setAnswers] = useState("")
|
||||
|
|
@ -252,7 +228,7 @@ function QuestionForm({ onCreated }: { onCreated: () => void }) {
|
|||
}
|
||||
|
||||
function submit() {
|
||||
const input: NewQuestionInput = { format, prompt, category, difficulty, lang }
|
||||
const input: NewQuestionInput = { format, prompt, category, difficulty }
|
||||
if (format === "free") {
|
||||
input.acceptedAnswers = answers
|
||||
.split("\n")
|
||||
|
|
@ -301,14 +277,6 @@ function QuestionForm({ onCreated }: { onCreated: () => void }) {
|
|||
<option value={2}>Moyen</option>
|
||||
<option value={3}>Difficile</option>
|
||||
</select>
|
||||
<select
|
||||
className={inputClass}
|
||||
value={lang}
|
||||
onChange={(e) => setLang(e.target.value)}
|
||||
>
|
||||
<option value="fr">FR</option>
|
||||
<option value="en">EN</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<input
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue