feat: Allow inline editing of both name and description for decks and quizzes
All checks were successful
Automated Container Build / build-and-push (push) Successful in 52s

This commit is contained in:
Elijah 2026-06-27 20:48:19 -07:00
parent 0295101e10
commit ec8e601332
2 changed files with 113 additions and 35 deletions

View file

@ -30,6 +30,7 @@ export default function FlashcardsPage() {
const [classId, setClassId] = useState<string>(""); const [classId, setClassId] = useState<string>("");
const [editingId, setEditingId] = useState<string | null>(null); const [editingId, setEditingId] = useState<string | null>(null);
const [editName, setEditName] = useState(""); const [editName, setEditName] = useState("");
const [editDescription, setEditDescription] = useState("");
const fetchDecks = useCallback(async (cId: string) => { const fetchDecks = useCallback(async (cId: string) => {
try { try {
@ -77,10 +78,10 @@ export default function FlashcardsPage() {
await fetch(`/api/decks/${id}`, { await fetch(`/api/decks/${id}`, {
method: "PATCH", method: "PATCH",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: editName.trim() }), body: JSON.stringify({ name: editName.trim(), description: editDescription.trim() || null }),
}); });
setDecks((prev) => setDecks((prev) =>
prev.map((d) => (d.id === id ? { ...d, name: editName.trim() } : d)) prev.map((d) => (d.id === id ? { ...d, name: editName.trim(), description: editDescription.trim() || null } : d))
); );
setEditingId(null); setEditingId(null);
} }
@ -148,39 +149,49 @@ export default function FlashcardsPage() {
className="group flex flex-col bg-bg-surface rounded-xl border border-border-light shadow-[var(--shadow-card)] hover:shadow-[var(--shadow-card-hover)] hover:border-primary/20 transition-all duration-300 h-full" className="group flex flex-col bg-bg-surface rounded-xl border border-border-light shadow-[var(--shadow-card)] hover:shadow-[var(--shadow-card-hover)] hover:border-primary/20 transition-all duration-300 h-full"
> >
<div className="p-6 flex flex-col flex-1 h-full"> <div className="p-6 flex flex-col flex-1 h-full">
{/* Name (editable) */} {/* Name and Description (editable) */}
{editingId === deck.id ? ( {editingId === deck.id ? (
<div className="flex gap-2 mb-3"> <div className="flex flex-col gap-3 mb-3">
<input <input
type="text" type="text"
value={editName} value={editName}
onChange={(e) => setEditName(e.target.value)} onChange={(e) => setEditName(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && handleRename(deck.id)} placeholder="Deck Name"
autoFocus autoFocus
className="flex-1 px-3 py-1.5 rounded-lg border border-border bg-bg-surface-alt/50 text-text-heading text-sm focus:outline-none focus:ring-2 focus:ring-primary/30" className="w-full px-3 py-1.5 rounded-lg border border-border bg-bg-surface-alt/50 text-text-heading text-sm font-semibold focus:outline-none focus:ring-2 focus:ring-primary/30"
/> />
<textarea
value={editDescription}
onChange={(e) => setEditDescription(e.target.value)}
placeholder="Description (optional)"
rows={2}
className="w-full px-3 py-1.5 rounded-lg border border-border bg-bg-surface-alt/50 text-text-heading text-sm focus:outline-none focus:ring-2 focus:ring-primary/30 resize-none"
/>
<div className="flex gap-2 mt-1">
<button <button
onClick={() => handleRename(deck.id)} onClick={() => handleRename(deck.id)}
className="text-sm text-primary hover:text-primary-hover cursor-pointer" className="px-3 py-1.5 bg-primary text-white text-xs font-medium rounded-md hover:bg-primary-hover cursor-pointer"
> >
Save Save Changes
</button> </button>
<button <button
onClick={() => setEditingId(null)} onClick={() => setEditingId(null)}
className="text-sm text-text-muted hover:text-text-heading cursor-pointer" className="px-3 py-1.5 bg-bg-surface-alt text-text-heading text-xs font-medium rounded-md hover:bg-border cursor-pointer transition-colors"
> >
Cancel Cancel
</button> </button>
</div> </div>
</div>
) : ( ) : (
<>
<h3 className="text-lg font-semibold text-text-heading mb-1"> <h3 className="text-lg font-semibold text-text-heading mb-1">
{deck.name} {deck.name}
</h3> </h3>
)}
{deck.description && ( {deck.description && (
<p className="text-sm text-text-secondary mb-3 line-clamp-2">{deck.description}</p> <p className="text-sm text-text-secondary mb-3 line-clamp-2">{deck.description}</p>
)} )}
</>
)}
{/* Stats */} {/* Stats */}
<div className="flex items-center gap-3 mt-auto mb-4"> <div className="flex items-center gap-3 mt-auto mb-4">
@ -231,9 +242,10 @@ export default function FlashcardsPage() {
onClick={() => { onClick={() => {
setEditingId(deck.id); setEditingId(deck.id);
setEditName(deck.name); setEditName(deck.name);
setEditDescription(deck.description || "");
}} }}
className="p-2 rounded-lg text-text-muted hover:text-text-heading hover:bg-bg-surface-alt transition-all duration-200 cursor-pointer" className="p-2 rounded-lg text-text-muted hover:text-text-heading hover:bg-bg-surface-alt transition-all duration-200 cursor-pointer"
title="Rename" title="Edit"
> >
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />

View file

@ -22,6 +22,9 @@ export default function QuizzesPage() {
const [animate] = useState(!cache[classSlug]); const [animate] = useState(!cache[classSlug]);
const [showImport, setShowImport] = useState(false); const [showImport, setShowImport] = useState(false);
const [classId, setClassId] = useState<string>(""); const [classId, setClassId] = useState<string>("");
const [editingId, setEditingId] = useState<string | null>(null);
const [editName, setEditName] = useState("");
const [editDescription, setEditDescription] = useState("");
const fetchQuizzes = useCallback(async (cId: string) => { const fetchQuizzes = useCallback(async (cId: string) => {
try { try {
@ -64,6 +67,19 @@ export default function QuizzesPage() {
setQuizzes((prev) => prev.filter((q) => q.id !== id)); setQuizzes((prev) => prev.filter((q) => q.id !== id));
} }
async function handleRename(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);
}
return ( return (
<div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8"> <div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Header */} {/* Header */}
@ -109,7 +125,42 @@ export default function QuizzesPage() {
key={quiz.id} key={quiz.id}
className="group bg-bg-surface rounded-xl border border-border-light shadow-[var(--shadow-card)] hover:shadow-[var(--shadow-card-hover)] hover:border-primary/20 transition-all duration-300" className="group bg-bg-surface rounded-xl border border-border-light shadow-[var(--shadow-card)] hover:shadow-[var(--shadow-card-hover)] hover:border-primary/20 transition-all duration-300"
> >
<div className="p-6"> <div className="p-6 flex flex-col flex-1 h-full">
{/* Name and Description (editable) */}
{editingId === quiz.id ? (
<div className="flex flex-col gap-3 mb-3">
<input
type="text"
value={editName}
onChange={(e) => setEditName(e.target.value)}
placeholder="Quiz Name"
autoFocus
className="w-full px-3 py-1.5 rounded-lg border border-border bg-bg-surface-alt/50 text-text-heading text-sm font-semibold focus:outline-none focus:ring-2 focus:ring-primary/30"
/>
<textarea
value={editDescription}
onChange={(e) => setEditDescription(e.target.value)}
placeholder="Description (optional)"
rows={2}
className="w-full px-3 py-1.5 rounded-lg border border-border bg-bg-surface-alt/50 text-text-heading text-sm focus:outline-none focus:ring-2 focus:ring-primary/30 resize-none"
/>
<div className="flex gap-2 mt-1">
<button
onClick={() => handleRename(quiz.id)}
className="px-3 py-1.5 bg-primary text-white text-xs font-medium rounded-md hover:bg-primary-hover cursor-pointer"
>
Save Changes
</button>
<button
onClick={() => setEditingId(null)}
className="px-3 py-1.5 bg-bg-surface-alt text-text-heading text-xs font-medium rounded-md hover:bg-border cursor-pointer transition-colors"
>
Cancel
</button>
</div>
</div>
) : (
<>
<h3 className="text-lg font-semibold text-text-heading mb-1"> <h3 className="text-lg font-semibold text-text-heading mb-1">
{quiz.name} {quiz.name}
</h3> </h3>
@ -118,7 +169,9 @@ export default function QuizzesPage() {
{quiz.description} {quiz.description}
</p> </p>
)} )}
<div className="flex items-center gap-3"> </>
)}
<div className="flex items-center gap-3 mt-auto">
<span className="inline-flex items-center gap-1 px-2.5 py-1 rounded-full bg-badge-bg text-badge-text text-xs font-medium"> <span className="inline-flex items-center gap-1 px-2.5 py-1 rounded-full bg-badge-bg text-badge-text text-xs font-medium">
{quiz._count.questions} {quiz._count.questions === 1 ? "question" : "questions"} {quiz._count.questions} {quiz._count.questions === 1 ? "question" : "questions"}
</span> </span>
@ -136,6 +189,19 @@ export default function QuizzesPage() {
> >
Start Quiz Start Quiz
</Link> </Link>
<button
onClick={() => {
setEditingId(quiz.id);
setEditName(quiz.name);
setEditDescription(quiz.description || "");
}}
className="p-2 rounded-lg text-text-muted hover:text-text-heading hover:bg-bg-surface-alt transition-all duration-200 cursor-pointer"
title="Edit"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
</button>
<button <button
onClick={() => handleDelete(quiz.id, quiz.name)} onClick={() => handleDelete(quiz.id, quiz.name)}
className="p-2 rounded-lg text-text-muted hover:text-error hover:bg-error-bg transition-all duration-200 cursor-pointer" className="p-2 rounded-lg text-text-muted hover:text-error hover:bg-error-bg transition-all duration-200 cursor-pointer"