import { useRef, useState } from "react" import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" import { Link } from "wouter" import { Trash2, Upload } from "lucide-react" import type { QuizFormat } from "@nerdware/shared" import { Button } from "@workspace/ui/components/button" import { adminApi, assetUrl, clearAdminToken, getAdminToken, setAdminToken, type AdminQuestion, type NewQuestionInput, } from "@/lib/admin-api" const inputClass = "border-input bg-background h-10 w-full rounded-md border px-3 py-2 text-sm focus-visible:ring-ring focus-visible:ring-2 focus-visible:outline-none" export function AdminPage() { const [token, setToken] = useState(getAdminToken()) if (!token) { return } return ( { clearAdminToken() setToken("") }} /> ) } function Login({ onAuth }: { onAuth: (token: string) => void }) { const [value, setValue] = useState("") return (

Back-office quiz

setValue(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter" && value.trim()) { setAdminToken(value.trim()) onAuth(value.trim()) } }} />
) } function AdminPanel({ onLogout }: { onLogout: () => void }) { const qc = useQueryClient() const questions = useQuery({ queryKey: ["admin-questions"], queryFn: adminApi.listQuestions, retry: false, }) const remove = useMutation({ mutationFn: adminApi.deleteQuestion, onSuccess: () => qc.invalidateQueries({ queryKey: ["admin-questions"] }), }) const unauthorized = questions.isError && /401|autoris/i.test(String(questions.error)) return (

Back-office quiz

{unauthorized ? (

Jeton invalide ou back-office indisponible.{" "}

) : (
qc.invalidateQueries({ queryKey: ["admin-questions"] }) } />

Questions ({questions.data?.length ?? 0})

{questions.isLoading && (

Chargement…

)}
    {questions.data?.map((q) => ( remove.mutate(q.id)} deleting={remove.isPending} /> ))}
)}
) } function QuestionRow({ q, onDelete, deleting, }: { q: AdminQuestion onDelete: () => void deleting: boolean }) { return (
  • {q.format === "image_reveal" && q.imageUrl && ( )}

    {q.prompt}

    {q.format} · {q.category ?? "—"} · diff. {q.difficulty} · {q.source}

    {q.format === "free" || q.format === "image_reveal" ? (

    ✔ {q.acceptedAnswers?.join(", ")}

    ) : (

    {q.choices ?.map((c, i) => (i === q.correctIndex ? `✔ ${c}` : c)) .join(" · ")}

    )}
  • ) } const EMPTY_CHOICES = ["", "", "", ""] function QuestionForm({ onCreated }: { onCreated: () => void }) { const [format, setFormat] = useState("mcq") const [prompt, setPrompt] = useState("") const [category, setCategory] = useState("") const [difficulty, setDifficulty] = useState(1) const [choices, setChoices] = useState(EMPTY_CHOICES) const [correctIndex, setCorrectIndex] = useState(0) const [answers, setAnswers] = useState("") const [imageUrl, setImageUrl] = useState(null) const [uploading, setUploading] = useState(false) const [uploadError, setUploadError] = useState(null) const fileInputRef = useRef(null) const create = useMutation({ mutationFn: (input: NewQuestionInput) => adminApi.createQuestion(input), onSuccess: () => { setPrompt("") setChoices(EMPTY_CHOICES) setCorrectIndex(0) setAnswers("") setImageUrl(null) onCreated() }, }) async function onPickImage(file: File | undefined) { if (!file) { return } setUploading(true) setUploadError(null) try { const { url } = await adminApi.uploadImage(file) setImageUrl(url) } catch (err) { setUploadError((err as { message?: string }).message ?? "Échec de l'upload") } finally { setUploading(false) } } function submit() { const input: NewQuestionInput = { format, prompt, category, difficulty } if (format === "free") { input.acceptedAnswers = answers .split("\n") .map((a) => a.trim()) .filter(Boolean) } else if (format === "image_reveal") { input.acceptedAnswers = answers .split("\n") .map((a) => a.trim()) .filter(Boolean) input.imageUrl = imageUrl ?? undefined } else if (format === "truefalse") { input.choices = ["Vrai", "Faux"] input.correctIndex = correctIndex } else { input.choices = choices.map((c) => c.trim()).filter(Boolean) input.correctIndex = correctIndex } create.mutate(input) } return (

    Nouvelle question

    setCategory(e.target.value)} /> setPrompt(e.target.value)} /> {format === "mcq" && (
    {choices.map((choice, i) => ( ))}

    Coche la bonne réponse. Les choix vides sont ignorés (min. 2).

    )} {format === "truefalse" && (
    {["Vrai", "Faux"].map((label, i) => ( ))}
    )} {format === "image_reveal" && (
    onPickImage(e.target.files?.[0])} /> {uploadError && (

    {uploadError}

    )} {imageUrl && ( )}
    )} {(format === "free" || format === "image_reveal") && (