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 type { QuizFormat } from "@nerdware/shared" import { Button } from "@workspace/ui/components/button" import { Tooltip, TooltipContent, TooltipTrigger, } from "@workspace/ui/components/tooltip" 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 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)) 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)} onToggle={() => toggle.mutate({ id: q.id, active: !q.active })} deleting={remove.isPending} /> ))}
)}
) } function QuestionRow({ q, onDelete, onToggle, deleting, }: { q: AdminQuestion onDelete: () => void onToggle: () => void deleting: boolean }) { return (
  • {q.format === "image_reveal" && q.imageUrl && ( )}

    {q.prompt}

    {q.format} · {q.category ?? "—"} · diff. {q.difficulty} ·{" "} {q.lang.toUpperCase()} · {q.source} {!q.active && " · désactivée"}

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

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

    ) : (

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

    )}
    {q.active ? "Désactiver" : "Activer"}
  • ) } 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 [lang, setLang] = useState("fr") 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, lang } if (format === "free" || format === "rebus") { 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" || format === "rebus") && (