"use client"; import { useState, useEffect, useCallback } from "react"; import Link from "next/link"; import { useParams } from "next/navigation"; import { ImportModal } from "@/components/import/ImportModal"; import { ShareMenu } from "@/components/ui/ShareMenu"; import { DndContext, pointerWithin, KeyboardSensor, PointerSensor, useSensor, useSensors, DragEndEvent, DragOverEvent, DragStartEvent, DragOverlay, useDroppable, useDndContext, } from "@dnd-kit/core"; import { arrayMove, SortableContext, sortableKeyboardCoordinates, verticalListSortingStrategy, useSortable, } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; interface QuizItem { id: string; name: string; description: string | null; groupId: string | null; sortOrder: number; _count: { questions: number; attempts: number }; } interface MaterialGroup { id: string; name: string; sortOrder: number; } const cache: Record = {}; function SortableQuizCard({ quiz, onEdit, onDelete, classSlug }: any) { const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: quiz.id }); const style = { transform: CSS.Transform.toString(transform), transition, opacity: isDragging ? 0.4 : 1, }; return (

{quiz.name}

{quiz.description &&

{quiz.description}

}
{quiz._count.questions} {quiz._count.questions === 1 ? "question" : "questions"} {quiz._count.attempts > 0 && ( {quiz._count.attempts} {quiz._count.attempts === 1 ? "attempt" : "attempts"} )}
Start Quiz
); } function DroppableContainer({ id, children }: { id: string; children: React.ReactNode }) { const { setNodeRef } = useDroppable({ id }); const { over } = useDndContext(); const isOverContainer = over?.id === id; const isOverChild = over?.data?.current?.sortable?.containerId === id; const isOver = isOverContainer || isOverChild; return (
{children}
); } export default function QuizzesPage() { const params = useParams(); const classSlug = params.classSlug as string; const [quizzes, setQuizzes] = useState(cache[classSlug]?.quizzes || []); const [groups, setGroups] = useState(cache[classSlug]?.groups || []); const [loading, setLoading] = useState(!cache[classSlug]); const [animate] = useState(!cache[classSlug]); const [showImport, setShowImport] = useState(false); const [classId, setClassId] = useState(""); const [editingId, setEditingId] = useState(null); const [editName, setEditName] = useState(""); const [editDescription, setEditDescription] = useState(""); const [isCreatingGroup, setIsCreatingGroup] = useState(false); const [newGroupName, setNewGroupName] = useState(""); const [editingGroupId, setEditingGroupId] = useState(null); const [editGroupName, setEditGroupName] = useState(""); const [collapsedGroups, setCollapsedGroups] = useState>({}); useEffect(() => { const saved = localStorage.getItem('quizzes_collapsed_groups'); if (saved) { try { setCollapsedGroups(JSON.parse(saved)); } catch(e) {} } }, []); function toggleGroup(id: string) { setCollapsedGroups(prev => { const next = { ...prev, [id]: !prev[id] }; localStorage.setItem('quizzes_collapsed_groups', JSON.stringify(next)); return next; }); } const fetchAll = useCallback(async (cId: string) => { try { const [quizRes, groupRes] = await Promise.all([ fetch(`/api/quizzes/list?classId=${cId}`), fetch(`/api/material-groups?classId=${cId}&type=QUIZ`), ]); const qs = quizRes.ok ? await quizRes.json() : []; const gs = groupRes.ok ? await groupRes.json() : []; setQuizzes(qs); setGroups(gs); cache[classSlug] = { quizzes: qs, groups: gs }; } catch { setQuizzes([]); setGroups([]); } finally { setLoading(false); } }, [classSlug]); useEffect(() => { async function init() { try { const classRes = await fetch("/api/classes"); const classes = await classRes.json(); const cls = classes.find((c: { slug: string }) => c.slug === classSlug); if (!cls) { setLoading(false); return; } setClassId(cls.id); fetchAll(cls.id); } catch { setLoading(false); } } init(); }, [classSlug, fetchAll]); // Group Management async function handleCreateGroup() { if (!newGroupName.trim()) return; const res = await fetch("/api/material-groups", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ classId, name: newGroupName.trim(), type: "QUIZ" }), }); if (res.ok) { const g = await res.json(); setGroups((prev) => [...prev, g]); setIsCreatingGroup(false); setNewGroupName(""); } } async function handleRenameGroup(id: string) { if (!editGroupName.trim()) return; await fetch(`/api/material-groups/${id}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: editGroupName.trim() }), }); setGroups((prev) => prev.map((g) => (g.id === id ? { ...g, name: editGroupName.trim() } : g))); setEditingGroupId(null); } async function handleDeleteGroup(id: string, name: string) { if (!confirm(`Delete group "${name}"? Quizzes inside will be moved to Uncategorized.`)) return; await fetch(`/api/material-groups/${id}`, { method: "DELETE" }); setGroups((prev) => prev.filter((g) => g.id !== id)); setQuizzes((prev) => prev.map((q) => (q.groupId === id ? { ...q, groupId: null } : q))); } // Quiz Management async function handleDeleteQuiz(quiz: QuizItem) { if (!confirm(`Delete "${quiz.name}" and all its questions and attempts?`)) return; await fetch(`/api/quizzes/${quiz.id}`, { method: "DELETE" }); setQuizzes((prev) => prev.filter((q) => q.id !== quiz.id)); } async function handleRenameQuiz(id: string) { if (!editName.trim()) return; await fetch(`/api/quizzes/${id}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: editName.trim(), description: editDescription.trim() || null }), }); setQuizzes((prev) => prev.map((q) => (q.id === id ? { ...q, name: editName.trim(), description: editDescription.trim() || null } : q)) ); setEditingId(null); } // Drag and drop setup const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 8 } }), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }) ); const [activeId, setActiveId] = useState(null); function handleDragStart(event: DragStartEvent) { setActiveId(event.active.id as string); } function handleDragOver(event: DragOverEvent) { const { active, over } = event; if (!over) return; const activeId = active.id as string; const overId = over.id as string; if (activeId === overId) return; // Is it dropping over a container directly? const isOverContainer = over.data.current?.sortable?.containerId || overId; // ... we handle layout updates in dragEnd to keep it simple, or here for smooth lists. // For simplicity, we just handle the final move in dragEnd. } async function handleDragEnd(event: DragEndEvent) { setActiveId(null); const { active, over } = event; if (!over) return; const activeId = active.id as string; const overId = over.id as string; const activeQuiz = quizzes.find((q) => q.id === activeId); if (!activeQuiz) return; let targetGroupId: string | null = null; let targetIndex = 0; // Check if over a group container directly const overContainerId = over.data.current?.sortable?.containerId; if (overContainerId) { targetGroupId = overContainerId === "uncategorized" ? null : overContainerId; } else if (groups.find(g => g.id === overId) || overId === "uncategorized") { targetGroupId = overId === "uncategorized" ? null : overId; } else { const overQuiz = quizzes.find((q) => q.id === overId); if (overQuiz) { targetGroupId = overQuiz.groupId; } } if (targetGroupId !== undefined) { setQuizzes((items) => { const oldIndex = items.findIndex((q) => q.id === activeId); const overIndex = items.findIndex((q) => q.id === overId); let newItems = [...items]; newItems[oldIndex].groupId = targetGroupId; if (overIndex >= 0 && overIndex !== oldIndex) { newItems = arrayMove(newItems, oldIndex, overIndex); } else { // just moved to end of a group const oldItem = newItems.splice(oldIndex, 1)[0]; newItems.push(oldItem); } // Re-calculate sortOrder for the affected groups to persist const affectedGroups = new Set([activeQuiz.groupId, targetGroupId]); const updates: any[] = []; affectedGroups.forEach(gId => { const gItems = newItems.filter(q => q.groupId === gId); gItems.forEach((item, index) => { item.sortOrder = index; updates.push({ id: item.id, sortOrder: index, groupId: item.groupId }); }); }); // Fire API fetch("/api/quizzes/reorder", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ items: updates }), }); return newItems; }); } } // Derive grouped quizzes const groupedQuizzes = groups.map(g => ({ ...g, quizzes: quizzes.filter(q => q.groupId === g.id).sort((a,b) => a.sortOrder - b.sortOrder) })); const uncategorizedQuizzes = quizzes.filter(q => q.groupId === null).sort((a,b) => a.sortOrder - b.sortOrder); const activeQuiz = quizzes.find(q => q.id === activeId); return (
{/* Header */}

Practice

Practice quizzes

{isCreatingGroup && (
setNewGroupName(e.target.value)} placeholder="e.g. Exam 1 Quizzes" className="flex-1 px-3 py-2 rounded-lg border border-border bg-bg-surface-alt/50 text-text-heading focus:outline-none focus:ring-2 focus:ring-primary/30" />
)} {/* Editing quiz modal/inline - simplified as a modal-like overlay for simplicity when editing */} {editingId && (

Edit quiz

setEditName(e.target.value)} placeholder="Quiz Name" autoFocus className="w-full px-3 py-2 mb-3 rounded-lg border border-border bg-bg-surface-alt/50 text-text-heading focus:outline-none focus:ring-2 focus:ring-primary/30" />