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
All checks were successful
Automated Container Build / build-and-push (push) Successful in 52s
This commit is contained in:
parent
0295101e10
commit
ec8e601332
2 changed files with 113 additions and 35 deletions
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue