Add minimize group feature with local storage persistence
All checks were successful
Automated Container Build / build-and-push (push) Successful in 1m10s
All checks were successful
Automated Container Build / build-and-push (push) Successful in 1m10s
This commit is contained in:
parent
7af0935657
commit
3cb9634524
2 changed files with 112 additions and 50 deletions
|
|
@ -174,6 +174,22 @@ export default function FlashcardsPage() {
|
|||
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('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) => {
|
||||
try {
|
||||
|
|
@ -430,7 +446,7 @@ export default function FlashcardsPage() {
|
|||
<div className="space-y-8">
|
||||
{groupedDecks.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" />
|
||||
|
|
@ -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>
|
||||
</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">
|
||||
|
|
@ -448,34 +469,44 @@ export default function FlashcardsPage() {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<SortableContext id={group.id} items={group.decks.map(d => d.id)} strategy={verticalListSortingStrategy}>
|
||||
<DroppableContainer id={group.id}>
|
||||
{group.decks.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 decks here</div>
|
||||
) : (
|
||||
group.decks.map(deck => (
|
||||
<SortableDeckCard key={deck.id} deck={deck} onEdit={(d: DeckItem) => { setEditingId(d.id); setEditName(d.name); setEditDescription(d.description || ""); }} onDelete={handleDeleteDeck} classSlug={classSlug} />
|
||||
))
|
||||
)}
|
||||
</DroppableContainer>
|
||||
</SortableContext>
|
||||
<div className={collapsedGroups[group.id] ? "hidden" : "block"}>
|
||||
<SortableContext id={group.id} items={group.decks.map(d => d.id)} strategy={verticalListSortingStrategy}>
|
||||
<DroppableContainer id={group.id}>
|
||||
{group.decks.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 decks here</div>
|
||||
) : (
|
||||
group.decks.map(deck => (
|
||||
<SortableDeckCard key={deck.id} deck={deck} onEdit={(d: DeckItem) => { setEditingId(d.id); setEditName(d.name); setEditDescription(d.description || ""); }} onDelete={handleDeleteDeck} 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={uncategorizedDecks.map(d => d.id)} strategy={verticalListSortingStrategy}>
|
||||
<DroppableContainer id="uncategorized">
|
||||
{uncategorizedDecks.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 decks</div>
|
||||
) : (
|
||||
uncategorizedDecks.map(deck => (
|
||||
<SortableDeckCard key={deck.id} deck={deck} onEdit={(d: DeckItem) => { setEditingId(d.id); setEditName(d.name); setEditDescription(d.description || ""); }} onDelete={handleDeleteDeck} 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={uncategorizedDecks.map(d => d.id)} strategy={verticalListSortingStrategy}>
|
||||
<DroppableContainer id="uncategorized">
|
||||
{uncategorizedDecks.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 decks</div>
|
||||
) : (
|
||||
uncategorizedDecks.map(deck => (
|
||||
<SortableDeckCard key={deck.id} deck={deck} onEdit={(d: DeckItem) => { setEditingId(d.id); setEditName(d.name); setEditDescription(d.description || ""); }} onDelete={handleDeleteDeck} classSlug={classSlug} />
|
||||
))
|
||||
)}
|
||||
</DroppableContainer>
|
||||
</SortableContext>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue