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 [editingId, setEditingId] = useState<string | null>(null);
const [editName, setEditName] = useState("");
const [editDescription, setEditDescription] = useState("");
const fetchDecks = useCallback(async (cId: string) => {
try {
@ -77,10 +78,10 @@ export default function FlashcardsPage() {
await fetch(`/api/decks/${id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: editName.trim() }),
body: JSON.stringify({ name: editName.trim(), description: editDescription.trim() || null }),
});
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);
}
@ -148,38 +149,48 @@ 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"
>
<div className="p-6 flex flex-col flex-1 h-full">
{/* Name (editable) */}
{/* Name and Description (editable) */}
{editingId === deck.id ? (
<div className="flex gap-2 mb-3">
<div className="flex flex-col gap-3 mb-3">
<input
type="text"
value={editName}
onChange={(e) => setEditName(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && handleRename(deck.id)}
placeholder="Deck Name"
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"
/>
<button
onClick={() => handleRename(deck.id)}
className="text-sm text-primary hover:text-primary-hover cursor-pointer"
>
Save
</button>
<button
onClick={() => setEditingId(null)}
className="text-sm text-text-muted hover:text-text-heading cursor-pointer"
>
Cancel
</button>
<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(deck.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">
{deck.name}
</h3>
)}
{deck.description && (
<p className="text-sm text-text-secondary mb-3 line-clamp-2">{deck.description}</p>
<>
<h3 className="text-lg font-semibold text-text-heading mb-1">
{deck.name}
</h3>
{deck.description && (
<p className="text-sm text-text-secondary mb-3 line-clamp-2">{deck.description}</p>
)}
</>
)}
{/* Stats */}
@ -231,9 +242,10 @@ export default function FlashcardsPage() {
onClick={() => {
setEditingId(deck.id);
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"
title="Rename"
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" />

View file

@ -22,6 +22,9 @@ export default function QuizzesPage() {
const [animate] = useState(!cache[classSlug]);
const [showImport, setShowImport] = useState(false);
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) => {
try {
@ -64,6 +67,19 @@ export default function QuizzesPage() {
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 (
<div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Header */}
@ -109,16 +125,53 @@ export default function QuizzesPage() {
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"
>
<div className="p-6">
<h3 className="text-lg font-semibold text-text-heading mb-1">
{quiz.name}
</h3>
{quiz.description && (
<p className="text-sm text-text-secondary mb-3 line-clamp-2">
{quiz.description}
</p>
<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">
{quiz.name}
</h3>
{quiz.description && (
<p className="text-sm text-text-secondary mb-3 line-clamp-2">
{quiz.description}
</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">
{quiz._count.questions} {quiz._count.questions === 1 ? "question" : "questions"}
</span>
@ -136,6 +189,19 @@ export default function QuizzesPage() {
>
Start Quiz
</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
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"