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

@ -174,6 +174,22 @@ export default function FlashcardsPage() {
const [newGroupName, setNewGroupName] = useState(""); const [newGroupName, setNewGroupName] = useState("");
const [editingGroupId, setEditingGroupId] = useState<string | null>(null); const [editingGroupId, setEditingGroupId] = useState<string | null>(null);
const [editGroupName, setEditGroupName] = useState(""); const [editGroupName, setEditGroupName] = useState("");
const [collapsedGroups, setCollapsedGroups] = useState<Record<string, boolean>>({});
useEffect(() => {
const saved = localStorage.getItem('flashcards_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('flashcards_collapsed_groups', JSON.stringify(next));
return next;
});
}
const fetchAll = useCallback(async (cId: string) => { const fetchAll = useCallback(async (cId: string) => {
try { try {
@ -430,7 +446,7 @@ export default function FlashcardsPage() {
<div className="space-y-8"> <div className="space-y-8">
{groupedDecks.map(group => ( {groupedDecks.map(group => (
<div key={group.id} className="bg-bg-surface-alt/30 p-4 rounded-xl border border-border-light"> <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 ? ( {editingGroupId === group.id ? (
<div className="flex items-center gap-2"> <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" /> <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" />
@ -438,7 +454,12 @@ export default function FlashcardsPage() {
<button onClick={() => setEditingGroupId(null)} className="text-xs px-2 py-1 bg-bg-surface text-text-heading rounded border border-border-light">Cancel</button> <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> </div>
) : ( ) : (
<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> <h3 className="text-lg font-semibold text-text-heading">{group.name}</h3>
</div>
)} )}
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@ -448,6 +469,7 @@ export default function FlashcardsPage() {
</div> </div>
</div> </div>
<div className={collapsedGroups[group.id] ? "hidden" : "block"}>
<SortableContext id={group.id} items={group.decks.map(d => d.id)} strategy={verticalListSortingStrategy}> <SortableContext id={group.id} items={group.decks.map(d => d.id)} strategy={verticalListSortingStrategy}>
<DroppableContainer id={group.id}> <DroppableContainer id={group.id}>
{group.decks.length === 0 ? ( {group.decks.length === 0 ? (
@ -460,11 +482,19 @@ export default function FlashcardsPage() {
</DroppableContainer> </DroppableContainer>
</SortableContext> </SortableContext>
</div> </div>
</div>
))} ))}
{/* Uncategorized */} {/* Uncategorized */}
<div className="bg-bg-surface-alt/30 p-4 rounded-xl border border-border-light"> <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> <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={uncategorizedDecks.map(d => d.id)} strategy={verticalListSortingStrategy}> <SortableContext id="uncategorized" items={uncategorizedDecks.map(d => d.id)} strategy={verticalListSortingStrategy}>
<DroppableContainer id="uncategorized"> <DroppableContainer id="uncategorized">
{uncategorizedDecks.length === 0 ? ( {uncategorizedDecks.length === 0 ? (
@ -478,6 +508,7 @@ export default function FlashcardsPage() {
</SortableContext> </SortableContext>
</div> </div>
</div> </div>
</div>
<DragOverlay> <DragOverlay>
{activeDeck ? ( {activeDeck ? (

View file

@ -130,6 +130,22 @@ export default function QuizzesPage() {
const [newGroupName, setNewGroupName] = useState(""); const [newGroupName, setNewGroupName] = useState("");
const [editingGroupId, setEditingGroupId] = useState<string | null>(null); const [editingGroupId, setEditingGroupId] = useState<string | null>(null);
const [editGroupName, setEditGroupName] = useState(""); 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) => { const fetchAll = useCallback(async (cId: string) => {
try { try {
@ -403,7 +419,7 @@ export default function QuizzesPage() {
<div className="space-y-8"> <div className="space-y-8">
{groupedQuizzes.map(group => ( {groupedQuizzes.map(group => (
<div key={group.id} className="bg-bg-surface-alt/30 p-4 rounded-xl border border-border-light"> <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 ? ( {editingGroupId === group.id ? (
<div className="flex items-center gap-2"> <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" /> <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> <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> </div>
) : ( ) : (
<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> <h3 className="text-lg font-semibold text-text-heading">{group.name}</h3>
</div>
)} )}
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@ -421,6 +442,7 @@ export default function QuizzesPage() {
</div> </div>
</div> </div>
<div className={collapsedGroups[group.id] ? "hidden" : "block"}>
<SortableContext id={group.id} items={group.quizzes.map(q => q.id)} strategy={verticalListSortingStrategy}> <SortableContext id={group.id} items={group.quizzes.map(q => q.id)} strategy={verticalListSortingStrategy}>
<DroppableContainer id={group.id}> <DroppableContainer id={group.id}>
{group.quizzes.length === 0 ? ( {group.quizzes.length === 0 ? (
@ -433,11 +455,19 @@ export default function QuizzesPage() {
</DroppableContainer> </DroppableContainer>
</SortableContext> </SortableContext>
</div> </div>
</div>
))} ))}
{/* Uncategorized */} {/* Uncategorized */}
<div className="bg-bg-surface-alt/30 p-4 rounded-xl border border-border-light"> <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> <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}> <SortableContext id="uncategorized" items={uncategorizedQuizzes.map(q => q.id)} strategy={verticalListSortingStrategy}>
<DroppableContainer id="uncategorized"> <DroppableContainer id="uncategorized">
{uncategorizedQuizzes.length === 0 ? ( {uncategorizedQuizzes.length === 0 ? (
@ -451,6 +481,7 @@ export default function QuizzesPage() {
</SortableContext> </SortableContext>
</div> </div>
</div> </div>
</div>
<DragOverlay> <DragOverlay>
{activeQuiz ? ( {activeQuiz ? (