"use client"; import React, { useState, useEffect } from "react"; import { RubricWithDetails, RubricCriterion, RubricLevel, cmsApi, CreateCriterionPayload, CreateLevelPayload } from "@/lib/api"; import { Plus, Trash2, Save, X, GripVertical, ChevronDown, ChevronUp, AlertCircle, Check } from "lucide-react"; import { clsx, type ClassValue } from "clsx"; import { twMerge } from "tailwind-merge"; function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } interface RubricEditorProps { rubricId: string; courseId: string; onClose: () => void; onSaved?: () => void; } export default function RubricEditor({ rubricId, courseId, onClose, onSaved }: RubricEditorProps) { const [rubric, setRubric] = useState(null); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [error, setError] = useState(null); useEffect(() => { loadRubric(); }, [rubricId]); const loadRubric = async () => { try { setLoading(true); const data = await cmsApi.getRubricWithDetails(rubricId); setRubric(data); } catch (err) { console.error("Failed to load rubric", err); setError("Failed to load rubric details."); } finally { setLoading(false); } }; const handleUpdateRubric = async (name: string, description: string) => { if (!rubric) return; try { const updated = await cmsApi.updateRubric(rubricId, { name, description: description || undefined }); setRubric({ ...rubric, ...updated }); } catch (err) { console.error("Failed to update rubric", err); } }; const handleAddCriterion = async () => { if (!rubric) return; const payload: CreateCriterionPayload = { name: "New Criterion", max_points: 10, position: rubric.criteria.length }; try { const newCriterion = await cmsApi.createCriterion(rubricId, payload); setRubric({ ...rubric, criteria: [...rubric.criteria, { ...newCriterion, levels: [] }] }); } catch (err) { console.error("Failed to add criterion", err); } }; const handleUpdateCriterion = async (criterionId: string, updates: Partial) => { if (!rubric) return; try { const payload = { ...updates, description: updates.description === null ? undefined : updates.description }; const updated = await cmsApi.updateCriterion(criterionId, payload); setRubric({ ...rubric, criteria: rubric.criteria.map(c => c.id === criterionId ? { ...c, ...updated } : c ) }); // Total points might have changed if (updates.max_points !== undefined) { const updatedRubric = await cmsApi.getRubricWithDetails(rubricId); setRubric(updatedRubric); } } catch (err) { console.error("Failed to update criterion", err); } }; const handleDeleteCriterion = async (criterionId: string) => { if (!confirm("Are you sure you want to delete this criterion?")) return; try { await cmsApi.deleteCriterion(criterionId); const updatedRubric = await cmsApi.getRubricWithDetails(rubricId); setRubric(updatedRubric); } catch (err) { console.error("Failed to delete criterion", err); } }; const handleAddLevel = async (criterionId: string) => { if (!rubric) return; const criterion = rubric.criteria.find(c => c.id === criterionId); if (!criterion) return; const payload: CreateLevelPayload = { name: "New Level", points: 0, position: criterion.levels.length }; try { const newLevel = await cmsApi.createLevel(criterionId, payload); setRubric({ ...rubric, criteria: rubric.criteria.map(c => c.id === criterionId ? { ...c, levels: [...c.levels, newLevel] } : c ) }); } catch (err) { console.error("Failed to add level", err); } }; const handleUpdateLevel = async (levelId: string, criterionId: string, updates: Partial) => { if (!rubric) return; try { const payload = { ...updates, description: updates.description === null ? undefined : updates.description }; const updated = await cmsApi.updateLevel(levelId, payload); setRubric({ ...rubric, criteria: rubric.criteria.map(c => c.id === criterionId ? { ...c, levels: c.levels.map(l => l.id === levelId ? updated : l) } : c ) }); } catch (err) { console.error("Failed to update level", err); } }; const handleDeleteLevel = async (levelId: string, criterionId: string) => { try { await cmsApi.deleteLevel(levelId); setRubric({ ...rubric!, criteria: rubric!.criteria.map(c => c.id === criterionId ? { ...c, levels: c.levels.filter(l => l.id !== levelId) } : c ) }); } catch (err) { console.error("Failed to delete level", err); } }; if (loading) return (
); if (error || !rubric) return (

{error || "Rubric not found"}

); return (
{/* Header */}
handleUpdateRubric(e.target.value, rubric.description || "")} placeholder="Rubric Name" className="bg-transparent text-3xl font-black text-slate-900 dark:text-white focus:outline-none focus:ring-1 focus:ring-blue-500/30 rounded px-2 w-full uppercase tracking-tighter" /> handleUpdateRubric(rubric.name, e.target.value)} placeholder="Add a description..." className="bg-transparent text-sm font-bold text-slate-400 dark:text-gray-500 focus:outline-none focus:ring-1 focus:ring-blue-500/30 rounded px-2 mt-2 w-full italic" />
Total Points {rubric.total_points}
{/* Content */}

Evaluation Criteria {rubric.criteria.length} items

{rubric.criteria.length === 0 ? (

Define your first grading criterion to start building the rubric.

) : ( rubric.criteria.map((item, idx) => (
handleUpdateCriterion(item.id, { name: e.target.value })} className="bg-transparent text-xl font-black text-slate-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500/20 rounded-lg px-2 w-full uppercase tracking-tight" /> handleUpdateCriterion(item.id, { description: e.target.value })} placeholder="Focus area (e.g. Critical Thinking, Technical Accuracy...)" className="bg-transparent text-sm font-medium text-slate-500 dark:text-gray-500 focus:outline-none w-full mt-2 italic px-2" />
handleUpdateCriterion(item.id, { max_points: parseInt(e.target.value) || 0 })} className="bg-white dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-xl px-3 py-2 w-20 text-center text-blue-600 dark:text-blue-400 font-black text-lg focus:outline-none focus:ring-2 focus:ring-blue-500/30 transition-all shadow-sm" />
{/* Levels */}

Achievement Levels

{item.levels.map(level => (
handleUpdateLevel(level.id, item.id, { name: e.target.value })} placeholder="Level (e.g. Master)" className="bg-transparent text-sm font-black text-slate-900 dark:text-gray-200 focus:outline-none uppercase tracking-tight" />