SolyQuiz/app/dashboard/quizzes/QuizzesClient.tsx
2026-02-27 00:40:57 +01:00

709 lines
34 KiB
TypeScript

'use client'
import { useState, useRef } from 'react'
import { useRouter } from 'next/navigation'
import {
Search,
ChevronDown,
ChevronUp,
Upload,
Plus as PlusIcon,
BookOpen,
Users,
Grid,
Edit2,
Play,
Trash2,
Check,
X,
Loader2,
FileJson,
RefreshCw,
} from 'lucide-react'
import { cn, formatDate } from '@/lib/utils'
interface Quiz {
id: string
title: string
updated_at: string
}
interface Subchapter {
id: string
name: string
quizzes: Quiz[]
}
interface Category {
id: string
name: string
description: string | null
subchapters: Subchapter[]
}
interface Props {
initialCategories: Category[]
stats: {
totalQuizzes: number
totalCategories: number
activeStudents: number
}
}
export default function QuizzesClient({ initialCategories, stats: initialStats }: Props) {
const router = useRouter()
const [categories, setCategories] = useState<Category[]>(initialCategories)
const [stats, setStats] = useState(initialStats)
const [expandedCategories, setExpandedCategories] = useState<Set<string>>(
new Set(initialCategories.slice(0, 1).map((c) => c.id))
)
const [search, setSearch] = useState('')
// Upload state — clé = subchapter_id
const [uploadingSubchapterId, setUploadingSubchapterId] = useState<string | null>(null)
const [uploadError, setUploadError] = useState<string | null>(null)
const fileInputRef = useRef<HTMLInputElement>(null)
const [pendingSubchapterId, setPendingSubchapterId] = useState<string | null>(null)
// Category CRUD
const [showNewCategoryModal, setShowNewCategoryModal] = useState(false)
const [newCategoryName, setNewCategoryName] = useState('')
const [newCategoryDesc, setNewCategoryDesc] = useState('')
const [creatingCategory, setCreatingCategory] = useState(false)
const [createCategoryError, setCreateCategoryError] = useState<string | null>(null)
const [editingCategoryId, setEditingCategoryId] = useState<string | null>(null)
const [editingCategoryName, setEditingCategoryName] = useState('')
const [savingCategoryId, setSavingCategoryId] = useState<string | null>(null)
const [confirmDeleteCategoryId, setConfirmDeleteCategoryId] = useState<string | null>(null)
const [deletingCategoryId, setDeletingCategoryId] = useState<string | null>(null)
// Subchapter CRUD
const [editingSubchapterId, setEditingSubchapterId] = useState<string | null>(null)
const [editingSubchapterName, setEditingSubchapterName] = useState('')
const [savingSubchapterId, setSavingSubchapterId] = useState<string | null>(null)
const [confirmDeleteSubchapterId, setConfirmDeleteSubchapterId] = useState<string | null>(null)
const [deletingSubchapterId, setDeletingSubchapterId] = useState<string | null>(null)
const [addingSubchapterForCategoryId, setAddingSubchapterForCategoryId] = useState<string | null>(null)
const [newSubchapterName, setNewSubchapterName] = useState('')
const [savingNewSubchapter, setSavingNewSubchapter] = useState(false)
const toggleCategory = (id: string) => {
setExpandedCategories((prev) => {
const next = new Set(prev)
if (next.has(id)) next.delete(id)
else next.add(id)
return next
})
}
const filteredCategories = categories.filter((cat) => {
if (!search) return true
const q = search.toLowerCase()
return (
cat.name.toLowerCase().includes(q) ||
cat.subchapters.some((s) => s.name.toLowerCase().includes(q))
)
})
// ── Upload quiz pour un chapitre spécifique ──
const triggerUpload = (subchapterId: string) => {
setPendingSubchapterId(subchapterId)
setUploadError(null)
fileInputRef.current?.click()
}
const handleFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]
if (!file || !pendingSubchapterId) return
const subchapterId = pendingSubchapterId
setUploadingSubchapterId(subchapterId)
setUploadError(null)
const formData = new FormData()
formData.append('file', file)
formData.append('subchapter_id', subchapterId)
try {
const res = await fetch('/api/upload-quiz', { method: 'POST', body: formData })
const data = await res.json()
if (!res.ok) {
setUploadError(data.error ?? "Erreur lors de l'import")
} else {
// Mettre à jour le state local immédiatement
const newQuiz: Quiz = {
id: data.quiz.id,
title: data.quiz.title,
updated_at: new Date().toISOString(),
}
setCategories((prev) =>
prev.map((cat) => ({
...cat,
subchapters: cat.subchapters.map((sub) =>
sub.id === subchapterId
? { ...sub, quizzes: [newQuiz] }
: sub
),
}))
)
if (!data.quiz.replaced) {
setStats((s) => ({ ...s, totalQuizzes: s.totalQuizzes + 1 }))
}
// Invalide le cache du router pour que les prochaines navigations
// récupèrent les données fraîches depuis le serveur
router.refresh()
}
} catch {
setUploadError('Erreur réseau')
} finally {
setUploadingSubchapterId(null)
setPendingSubchapterId(null)
if (fileInputRef.current) fileInputRef.current.value = ''
}
}
// ── Category handlers ──
const handleCreateCategory = async () => {
if (!newCategoryName.trim()) return
setCreatingCategory(true)
setCreateCategoryError(null)
try {
const res = await fetch('/api/categories/create', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: newCategoryName.trim(), description: newCategoryDesc.trim() || null }),
})
const data = await res.json()
if (!res.ok) {
setCreateCategoryError(data.error ?? 'Erreur')
} else {
const newCat: Category = { ...data.category, subchapters: [] }
setCategories((prev) => [...prev, newCat])
setStats((s) => ({ ...s, totalCategories: s.totalCategories + 1 }))
setShowNewCategoryModal(false)
setNewCategoryName('')
setNewCategoryDesc('')
setExpandedCategories((prev) => new Set([...prev, newCat.id]))
}
} catch {
setCreateCategoryError('Erreur réseau')
} finally {
setCreatingCategory(false)
}
}
const handleSaveCategory = async (id: string) => {
if (!editingCategoryName.trim()) return
setSavingCategoryId(id)
try {
const res = await fetch(`/api/categories/${id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: editingCategoryName.trim() }),
})
if (res.ok) {
setCategories((prev) =>
prev.map((c) => (c.id === id ? { ...c, name: editingCategoryName.trim() } : c))
)
setEditingCategoryId(null)
router.refresh()
}
} finally {
setSavingCategoryId(null)
}
}
const handleDeleteCategory = async (id: string) => {
setDeletingCategoryId(id)
try {
const res = await fetch(`/api/categories/${id}`, { method: 'DELETE' })
if (res.ok) {
const cat = categories.find((c) => c.id === id)
const removedQuizzes = cat?.subchapters.reduce((acc, s) => acc + (s.quizzes?.length ?? 0), 0) ?? 0
setCategories((prev) => prev.filter((c) => c.id !== id))
setStats((s) => ({
...s,
totalCategories: Math.max(0, s.totalCategories - 1),
totalQuizzes: Math.max(0, s.totalQuizzes - removedQuizzes),
}))
setConfirmDeleteCategoryId(null)
router.refresh()
}
} finally {
setDeletingCategoryId(null)
}
}
// ── Subchapter handlers ──
const handleSaveSubchapter = async (subId: string) => {
if (!editingSubchapterName.trim()) return
setSavingSubchapterId(subId)
try {
const res = await fetch(`/api/subchapters/${subId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: editingSubchapterName.trim() }),
})
if (res.ok) {
setCategories((prev) =>
prev.map((c) => ({
...c,
subchapters: c.subchapters.map((s) =>
s.id === subId ? { ...s, name: editingSubchapterName.trim() } : s
),
}))
)
setEditingSubchapterId(null)
router.refresh()
}
} finally {
setSavingSubchapterId(null)
}
}
const handleDeleteSubchapter = async (subId: string) => {
setDeletingSubchapterId(subId)
try {
const res = await fetch(`/api/subchapters/${subId}`, { method: 'DELETE' })
if (res.ok) {
let removedQuizzes = 0
setCategories((prev) =>
prev.map((c) => {
const sub = c.subchapters.find((s) => s.id === subId)
if (sub) removedQuizzes = sub.quizzes?.length ?? 0
return { ...c, subchapters: c.subchapters.filter((s) => s.id !== subId) }
})
)
setStats((s) => ({ ...s, totalQuizzes: Math.max(0, s.totalQuizzes - removedQuizzes) }))
setConfirmDeleteSubchapterId(null)
router.refresh()
}
} finally {
setDeletingSubchapterId(null)
}
}
const handleCreateSubchapter = async (categoryId: string) => {
if (!newSubchapterName.trim()) return
setSavingNewSubchapter(true)
try {
const res = await fetch('/api/subchapters/create', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ category_id: categoryId, name: newSubchapterName.trim() }),
})
const data = await res.json()
if (res.ok) {
const newSub: Subchapter = { ...data.subchapter, quizzes: [] }
setCategories((prev) =>
prev.map((c) =>
c.id === categoryId ? { ...c, subchapters: [...c.subchapters, newSub] } : c
)
)
setAddingSubchapterForCategoryId(null)
setNewSubchapterName('')
router.refresh()
}
} finally {
setSavingNewSubchapter(false)
}
}
const categoryIcons = ['🔴', '🔵', '🟢', '🟡', '🟣', '🟠']
return (
<div className="p-4 md:p-8">
{/* Input file caché — partagé, déclenché par triggerUpload() */}
<input
ref={fileInputRef}
type="file"
accept=".json"
className="hidden"
onChange={handleFileUpload}
/>
{/* Header */}
<div className="flex items-start justify-between mb-6 md:mb-8 gap-3">
<div>
<h1 className="text-2xl md:text-3xl font-bold text-text-primary mb-1 md:mb-2">Gestion des Quiz</h1>
<p className="text-text-secondary text-sm max-w-lg hidden sm:block">
Organisez votre curriculum, gérez les catégories et importez les quiz dans chaque chapitre.
</p>
</div>
<button
onClick={() => setShowNewCategoryModal(true)}
className="btn-secondary flex items-center gap-2 flex-shrink-0 text-sm"
>
<PlusIcon size={16} />
<span className="hidden sm:inline">Nouvelle </span>Catégorie
</button>
</div>
{uploadError && (
<div className="mb-6 bg-red-500/10 border border-red-500/30 text-red-400 px-4 py-3 rounded-lg text-sm flex items-center justify-between">
{uploadError}
<button onClick={() => setUploadError(null)}><X size={14} /></button>
</div>
)}
{/* Stats */}
<div className="grid grid-cols-3 gap-3 md:gap-4 mb-6 md:mb-8">
{[
{ icon: BookOpen, label: 'Total Quiz', value: stats.totalQuizzes, color: 'text-green-400' },
{ icon: Grid, label: 'Catégories', value: stats.totalCategories, color: 'text-blue-400' },
{ icon: Users, label: 'Étudiants Actifs', value: stats.activeStudents, color: 'text-purple-400' },
].map(({ icon: Icon, label, value, color }) => (
<div key={label} className="card p-3 md:p-5 flex items-center gap-3 md:gap-4">
<div className={cn('w-9 h-9 md:w-12 md:h-12 rounded-xl bg-background-elevated flex items-center justify-center flex-shrink-0', color)}>
<Icon size={18} className="md:hidden" />
<Icon size={22} className="hidden md:block" />
</div>
<div>
<p className="text-text-secondary text-xs md:text-sm">{label}</p>
<p className="text-xl md:text-2xl font-bold text-text-primary">{value}</p>
</div>
</div>
))}
</div>
{/* Categories */}
<div>
<div className="flex flex-col sm:flex-row sm:items-center justify-between mb-4 gap-3">
<h2 className="text-lg font-semibold text-text-primary">Catégories Actives</h2>
<div className="relative w-full sm:w-64">
<Search size={16} className="absolute left-3 top-1/2 -translate-y-1/2 text-text-muted" />
<input
type="text"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Rechercher un chapitre..."
className="input-field pl-9 py-2 text-sm w-full"
/>
</div>
</div>
<div className="space-y-3">
{filteredCategories.length === 0 ? (
<div className="card p-12 text-center">
<BookOpen size={32} className="mx-auto text-text-muted mb-3" />
<p className="text-text-secondary">Aucune catégorie trouvée</p>
</div>
) : (
filteredCategories.map((category, index) => {
const isExpanded = expandedCategories.has(category.id)
const totalQuizCount = category.subchapters.reduce((acc, s) => acc + (s.quizzes?.length ?? 0), 0)
const isEditingCat = editingCategoryId === category.id
const isConfirmingDelete = confirmDeleteCategoryId === category.id
const isDeletingCat = deletingCategoryId === category.id
return (
<div key={category.id} className="card overflow-hidden">
{/* Category header */}
<div className="flex items-center justify-between p-4">
<button
onClick={() => !isEditingCat && toggleCategory(category.id)}
className="flex items-center gap-3 flex-1 text-left"
>
<span className="text-xl">{categoryIcons[index % categoryIcons.length]}</span>
{isEditingCat ? (
<input
value={editingCategoryName}
onChange={(e) => setEditingCategoryName(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') handleSaveCategory(category.id)
if (e.key === 'Escape') setEditingCategoryId(null)
}}
onClick={(e) => e.stopPropagation()}
className="input-field text-sm py-1.5 w-56"
autoFocus
/>
) : (
<div>
<p className="font-semibold text-text-primary">{category.name}</p>
<p className="text-xs text-text-muted">
{category.subchapters.length} Chapitre{category.subchapters.length > 1 ? 's' : ''} · {totalQuizCount} Quiz
</p>
</div>
)}
</button>
<div className="flex items-center gap-2">
{isEditingCat ? (
<>
<button
onClick={() => handleSaveCategory(category.id)}
disabled={savingCategoryId === category.id}
className="p-1.5 bg-primary/10 hover:bg-primary/20 rounded-md text-primary transition-colors"
>
{savingCategoryId === category.id ? <Loader2 size={14} className="animate-spin" /> : <Check size={14} />}
</button>
<button onClick={() => setEditingCategoryId(null)} className="p-1.5 hover:bg-background-elevated rounded-md text-text-muted transition-colors">
<X size={14} />
</button>
</>
) : isConfirmingDelete ? (
<div className="flex items-center gap-2 bg-red-500/10 border border-red-500/20 rounded-lg px-3 py-1.5">
<span className="text-xs text-red-400">Supprimer ?</span>
<button onClick={() => handleDeleteCategory(category.id)} disabled={isDeletingCat} className="text-xs text-red-400 hover:text-red-300 font-medium">
{isDeletingCat ? <Loader2 size={12} className="animate-spin" /> : 'Oui'}
</button>
<button onClick={() => setConfirmDeleteCategoryId(null)} className="text-xs text-text-muted hover:text-text-primary">Non</button>
</div>
) : (
<>
<span className="text-xs px-2.5 py-1 rounded-full border bg-green-500/10 text-green-400 border-green-500/20 font-medium">Publié</span>
<button onClick={(e) => { e.stopPropagation(); setEditingCategoryId(category.id); setEditingCategoryName(category.name); setConfirmDeleteCategoryId(null) }} className="p-1.5 hover:bg-background-elevated rounded-md text-text-muted hover:text-text-primary transition-colors" title="Renommer">
<Edit2 size={14} />
</button>
<button onClick={(e) => { e.stopPropagation(); setConfirmDeleteCategoryId(category.id); setEditingCategoryId(null) }} className="p-1.5 hover:bg-red-500/10 rounded-md text-text-muted hover:text-red-400 transition-colors" title="Supprimer">
<Trash2 size={14} />
</button>
<button onClick={() => toggleCategory(category.id)}>
{isExpanded ? <ChevronUp size={18} className="text-text-muted" /> : <ChevronDown size={18} className="text-text-muted" />}
</button>
</>
)}
</div>
</div>
{/* Subchapters table */}
{isExpanded && (
<div className="border-t border-border overflow-x-auto">
<table className="w-full min-w-[600px]">
<thead>
<tr className="border-b border-border">
<th className="text-left text-xs font-medium text-text-muted px-6 py-3 uppercase tracking-wider">Chapitre</th>
<th className="text-left text-xs font-medium text-text-muted px-6 py-3 uppercase tracking-wider">Quiz</th>
<th className="text-left text-xs font-medium text-text-muted px-6 py-3 uppercase tracking-wider">Dernière Modif.</th>
<th className="text-right text-xs font-medium text-text-muted px-6 py-3 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody>
{category.subchapters.map((subchapter) => {
const isEditingSub = editingSubchapterId === subchapter.id
const isConfirmingDeleteSub = confirmDeleteSubchapterId === subchapter.id
const isDeletingSub = deletingSubchapterId === subchapter.id
const isUploading = uploadingSubchapterId === subchapter.id
const quiz = (subchapter.quizzes ?? [])[0] ?? null
return (
<tr key={subchapter.id} className="border-b border-border/50 hover:bg-background-elevated/30 transition-colors">
{/* Nom du chapitre */}
<td className="px-6 py-4 text-sm font-medium text-text-primary">
{isEditingSub ? (
<input
value={editingSubchapterName}
onChange={(e) => setEditingSubchapterName(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') handleSaveSubchapter(subchapter.id)
if (e.key === 'Escape') setEditingSubchapterId(null)
}}
className="input-field text-sm py-1 w-40 md:w-48"
autoFocus
/>
) : (
subchapter.name
)}
</td>
{/* Quiz associé */}
<td className="px-6 py-4 text-sm">
{quiz ? (
<span className="font-medium text-text-primary truncate max-w-[200px] block">{quiz.title}</span>
) : (
<span className="text-text-muted italic">Aucun quiz</span>
)}
</td>
{/* Date */}
<td className="px-6 py-4 text-sm text-text-secondary">
{quiz ? formatDate(quiz.updated_at) : '—'}
</td>
{/* Actions */}
<td className="px-6 py-4">
<div className="flex items-center justify-end gap-1.5">
{isEditingSub ? (
<>
<button onClick={() => handleSaveSubchapter(subchapter.id)} disabled={savingSubchapterId === subchapter.id} className="p-1.5 bg-primary/10 hover:bg-primary/20 rounded-md text-primary transition-colors">
{savingSubchapterId === subchapter.id ? <Loader2 size={13} className="animate-spin" /> : <Check size={13} />}
</button>
<button onClick={() => setEditingSubchapterId(null)} className="p-1.5 hover:bg-background-elevated rounded-md text-text-muted transition-colors">
<X size={13} />
</button>
</>
) : isConfirmingDeleteSub ? (
<div className="flex items-center gap-2 bg-red-500/10 border border-red-500/20 rounded-lg px-2.5 py-1">
<span className="text-xs text-red-400">Supprimer ?</span>
<button onClick={() => handleDeleteSubchapter(subchapter.id)} disabled={isDeletingSub} className="text-xs text-red-400 hover:text-red-300 font-medium">
{isDeletingSub ? <Loader2 size={11} className="animate-spin" /> : 'Oui'}
</button>
<button onClick={() => setConfirmDeleteSubchapterId(null)} className="text-xs text-text-muted hover:text-text-primary">Non</button>
</div>
) : (
<>
{/* Renommer le chapitre */}
<button
onClick={() => { setEditingSubchapterId(subchapter.id); setEditingSubchapterName(subchapter.name); setConfirmDeleteSubchapterId(null) }}
className="p-1.5 hover:bg-background-elevated rounded-md text-text-muted hover:text-text-primary transition-colors"
title="Renommer le chapitre"
>
<Edit2 size={14} />
</button>
{/* Éditer le quiz (si existant) */}
{quiz && (
<button
onClick={() => router.push(`/dashboard/quizzes/${quiz.id}/edit`)}
className="p-1.5 hover:bg-primary/10 rounded-md text-text-muted hover:text-primary transition-colors"
title="Éditer le quiz"
>
<BookOpen size={14} />
</button>
)}
{/* Importer / Remplacer le quiz */}
<button
onClick={() => triggerUpload(subchapter.id)}
disabled={isUploading}
className={cn(
'p-1.5 rounded-md transition-colors',
quiz
? 'hover:bg-amber-500/10 text-text-muted hover:text-amber-400'
: 'hover:bg-green-500/10 text-text-muted hover:text-green-400',
isUploading && 'opacity-50 cursor-not-allowed'
)}
title={quiz ? 'Remplacer le quiz (JSON)' : 'Importer un quiz (JSON)'}
>
{isUploading ? (
<Loader2 size={14} className="animate-spin" />
) : quiz ? (
<RefreshCw size={14} />
) : (
<FileJson size={14} />
)}
</button>
{/* Lancer une session */}
<button
onClick={() => quiz && router.push(`/dashboard/sessions/create?quiz_id=${quiz.id}`)}
disabled={!quiz}
className={cn(
'p-1.5 rounded-md transition-colors',
quiz
? 'hover:bg-primary/10 text-text-muted hover:text-primary'
: 'text-text-muted/30 cursor-not-allowed'
)}
title={quiz ? 'Lancer une session' : 'Importez un quiz pour lancer une session'}
>
<Play size={14} />
</button>
{/* Supprimer le chapitre */}
<button
onClick={() => { setConfirmDeleteSubchapterId(subchapter.id); setEditingSubchapterId(null) }}
className="p-1.5 hover:bg-red-500/10 rounded-md text-text-muted hover:text-red-400 transition-colors"
title="Supprimer le chapitre"
>
<Trash2 size={14} />
</button>
</>
)}
</div>
</td>
</tr>
)
})}
{/* Ajouter un chapitre */}
{addingSubchapterForCategoryId === category.id && (
<tr className="border-b border-border/50 bg-background-elevated/20">
<td className="px-6 py-3" colSpan={4}>
<div className="flex items-center gap-2">
<input
value={newSubchapterName}
onChange={(e) => setNewSubchapterName(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') handleCreateSubchapter(category.id)
if (e.key === 'Escape') { setAddingSubchapterForCategoryId(null); setNewSubchapterName('') }
}}
placeholder="Nom du chapitre..."
className="input-field text-sm py-1.5 flex-1 max-w-xs"
autoFocus
/>
<button onClick={() => handleCreateSubchapter(category.id)} disabled={savingNewSubchapter || !newSubchapterName.trim()} className="p-1.5 bg-primary/10 hover:bg-primary/20 rounded-md text-primary transition-colors disabled:opacity-40">
{savingNewSubchapter ? <Loader2 size={14} className="animate-spin" /> : <Check size={14} />}
</button>
<button onClick={() => { setAddingSubchapterForCategoryId(null); setNewSubchapterName('') }} className="p-1.5 hover:bg-background-elevated rounded-md text-text-muted transition-colors">
<X size={14} />
</button>
</div>
</td>
</tr>
)}
</tbody>
</table>
<div className="border-t border-border/50 p-3 flex justify-center">
<button
onClick={() => { setAddingSubchapterForCategoryId(category.id); setNewSubchapterName(''); setExpandedCategories((prev) => new Set([...prev, category.id])) }}
className="text-sm text-primary hover:text-primary-light transition-colors flex items-center gap-1"
>
<PlusIcon size={14} />
Ajouter un chapitre
</button>
</div>
</div>
)}
</div>
)
})
)}
</div>
</div>
{/* Légende des icônes */}
<div className="mt-6 flex items-center gap-6 text-xs text-text-muted">
<div className="flex items-center gap-1.5"><Edit2 size={12} /> Renommer</div>
<div className="flex items-center gap-1.5"><BookOpen size={12} /> Éditer le quiz</div>
<div className="flex items-center gap-1.5"><FileJson size={12} /> Importer JSON</div>
<div className="flex items-center gap-1.5"><RefreshCw size={12} /> Remplacer (JSON)</div>
<div className="flex items-center gap-1.5"><Play size={12} /> Lancer session</div>
<div className="flex items-center gap-1.5"><Trash2 size={12} /> Supprimer</div>
</div>
{/* Modale nouvelle catégorie */}
{showNewCategoryModal && (
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm z-50 flex items-center justify-center p-4">
<div className="bg-background-card border border-border rounded-2xl w-full max-w-md shadow-2xl shadow-black/50">
<div className="flex items-center justify-between p-6 border-b border-border">
<h2 className="text-lg font-bold text-text-primary">Nouvelle Catégorie</h2>
<button onClick={() => { setShowNewCategoryModal(false); setCreateCategoryError(null); setNewCategoryName(''); setNewCategoryDesc('') }} className="p-1.5 hover:bg-background-elevated rounded-lg transition-colors">
<X size={18} className="text-text-muted" />
</button>
</div>
<div className="p-6 space-y-4">
<div>
<label className="block text-sm text-text-secondary mb-1.5">Nom de la catégorie *</label>
<input value={newCategoryName} onChange={(e) => setNewCategoryName(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && handleCreateCategory()} placeholder="Ex: Développement Web" className="input-field" autoFocus />
</div>
<div>
<label className="block text-sm text-text-secondary mb-1.5">Description (optionnelle)</label>
<textarea value={newCategoryDesc} onChange={(e) => setNewCategoryDesc(e.target.value)} placeholder="Décrivez cette catégorie..." className="input-field resize-none h-20" />
</div>
{createCategoryError && <p className="text-sm text-red-400">{createCategoryError}</p>}
</div>
<div className="flex items-center justify-end gap-3 px-6 py-4 border-t border-border bg-background-secondary/30 rounded-b-2xl">
<button onClick={() => { setShowNewCategoryModal(false); setCreateCategoryError(null); setNewCategoryName(''); setNewCategoryDesc('') }} className="btn-secondary">Annuler</button>
<button onClick={handleCreateCategory} disabled={creatingCategory || !newCategoryName.trim()} className={cn('btn-primary', (creatingCategory || !newCategoryName.trim()) && 'opacity-60 cursor-not-allowed')}>
{creatingCategory ? <><Loader2 size={16} className="animate-spin" /> Création...</> : <><PlusIcon size={16} /> Créer la catégorie</>}
</button>
</div>
</div>
</div>
)}
</div>
)
}