Add minimize group feature with local storage persistence
All checks were successful
Automated Container Build / build-and-push (push) Successful in 1m10s

This commit is contained in:
Elijah 2026-07-10 16:15:51 -07:00
parent 7af0935657
commit 3cb9634524
2 changed files with 112 additions and 50 deletions

View file

@ -130,6 +130,22 @@ export default function QuizzesPage() {
const [newGroupName, setNewGroupName] = useState("");
const [editingGroupId, setEditingGroupId] = useState<string | null>(null);
const [editGroupName, setEditGroupName] = useState("");
const [collapsedGroups, setCollapsedGroups] = useState<Record<string, boolean>>({});
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 {
@ -403,7 +419,7 @@ export default function QuizzesPage() {
<div className="space-y-8">
{groupedQuizzes.map(group => (
<div key={group.id} className="bg-bg-surface-alt/30 p-4 rounded-xl border border-border-light">
<div className="flex items-center justify-between mb-4">
<div className={`flex items-center justify-between ${collapsedGroups[group.id] ? "" : "mb-4"}`}>
{editingGroupId === group.id ? (
<div className="flex items-center gap-2">
<input autoFocus value={editGroupName} onChange={e => setEditGroupName(e.target.value)} className="px-2 py-1 rounded border border-border bg-bg-surface text-text-heading focus:ring-1 focus:ring-primary" />
@ -411,7 +427,12 @@ export default function QuizzesPage() {
<button onClick={() => setEditingGroupId(null)} className="text-xs px-2 py-1 bg-bg-surface text-text-heading rounded border border-border-light">Cancel</button>
</div>
) : (
<h3 className="text-lg font-semibold text-text-heading">{group.name}</h3>
<div className="flex items-center gap-2">
<button onClick={() => toggleGroup(group.id)} className="p-1 rounded text-text-muted hover:text-text-heading hover:bg-bg-surface transition-colors cursor-pointer" title={collapsedGroups[group.id] ? "Expand" : "Collapse"}>
<svg className={`w-5 h-5 transition-transform duration-200 ${collapsedGroups[group.id] ? '-rotate-90' : ''}`} fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" /></svg>
</button>
<h3 className="text-lg font-semibold text-text-heading">{group.name}</h3>
</div>
)}
<div className="flex items-center gap-2">
@ -421,34 +442,44 @@ export default function QuizzesPage() {
</div>
</div>
<SortableContext id={group.id} items={group.quizzes.map(q => q.id)} strategy={verticalListSortingStrategy}>
<DroppableContainer id={group.id}>
{group.quizzes.length === 0 ? (
<div className="col-span-1 md:col-span-2 text-center py-4 text-text-muted text-sm border-2 border-dashed border-border-light rounded-lg">Drop quizzes here</div>
) : (
group.quizzes.map(quiz => (
<SortableQuizCard key={quiz.id} quiz={quiz} onEdit={(q: QuizItem) => { setEditingId(q.id); setEditName(q.name); setEditDescription(q.description || ""); }} onDelete={handleDeleteQuiz} classSlug={classSlug} />
))
)}
</DroppableContainer>
</SortableContext>
<div className={collapsedGroups[group.id] ? "hidden" : "block"}>
<SortableContext id={group.id} items={group.quizzes.map(q => q.id)} strategy={verticalListSortingStrategy}>
<DroppableContainer id={group.id}>
{group.quizzes.length === 0 ? (
<div className="col-span-1 md:col-span-2 text-center py-4 text-text-muted text-sm border-2 border-dashed border-border-light rounded-lg">Drop quizzes here</div>
) : (
group.quizzes.map(quiz => (
<SortableQuizCard key={quiz.id} quiz={quiz} onEdit={(q: QuizItem) => { setEditingId(q.id); setEditName(q.name); setEditDescription(q.description || ""); }} onDelete={handleDeleteQuiz} classSlug={classSlug} />
))
)}
</DroppableContainer>
</SortableContext>
</div>
</div>
))}
{/* Uncategorized */}
<div className="bg-bg-surface-alt/30 p-4 rounded-xl border border-border-light">
<h3 className="text-lg font-semibold text-text-heading mb-4">Uncategorized</h3>
<SortableContext id="uncategorized" items={uncategorizedQuizzes.map(q => q.id)} strategy={verticalListSortingStrategy}>
<DroppableContainer id="uncategorized">
{uncategorizedQuizzes.length === 0 ? (
<div className="col-span-1 md:col-span-2 text-center py-4 text-text-muted text-sm border-2 border-dashed border-border-light rounded-lg">No uncategorized quizzes</div>
) : (
uncategorizedQuizzes.map(quiz => (
<SortableQuizCard key={quiz.id} quiz={quiz} onEdit={(q: QuizItem) => { setEditingId(q.id); setEditName(q.name); setEditDescription(q.description || ""); }} onDelete={handleDeleteQuiz} classSlug={classSlug} />
))
)}
</DroppableContainer>
</SortableContext>
<div className={`flex items-center gap-2 ${collapsedGroups["uncategorized"] ? "" : "mb-4"}`}>
<button onClick={() => toggleGroup("uncategorized")} className="p-1 rounded text-text-muted hover:text-text-heading hover:bg-bg-surface transition-colors cursor-pointer" title={collapsedGroups["uncategorized"] ? "Expand" : "Collapse"}>
<svg className={`w-5 h-5 transition-transform duration-200 ${collapsedGroups["uncategorized"] ? '-rotate-90' : ''}`} fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" /></svg>
</button>
<h3 className="text-lg font-semibold text-text-heading">Uncategorized</h3>
</div>
<div className={collapsedGroups["uncategorized"] ? "hidden" : "block"}>
<SortableContext id="uncategorized" items={uncategorizedQuizzes.map(q => q.id)} strategy={verticalListSortingStrategy}>
<DroppableContainer id="uncategorized">
{uncategorizedQuizzes.length === 0 ? (
<div className="col-span-1 md:col-span-2 text-center py-4 text-text-muted text-sm border-2 border-dashed border-border-light rounded-lg">No uncategorized quizzes</div>
) : (
uncategorizedQuizzes.map(quiz => (
<SortableQuizCard key={quiz.id} quiz={quiz} onEdit={(q: QuizItem) => { setEditingId(q.id); setEditName(q.name); setEditDescription(q.description || ""); }} onDelete={handleDeleteQuiz} classSlug={classSlug} />
))
)}
</DroppableContainer>
</SortableContext>
</div>
</div>
</div>