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 [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>
|
||||||
) : (
|
) : (
|
||||||
<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">
|
<div className="flex items-center gap-2">
|
||||||
|
|
@ -448,34 +469,44 @@ export default function FlashcardsPage() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SortableContext id={group.id} items={group.decks.map(d => d.id)} strategy={verticalListSortingStrategy}>
|
<div className={collapsedGroups[group.id] ? "hidden" : "block"}>
|
||||||
<DroppableContainer id={group.id}>
|
<SortableContext id={group.id} items={group.decks.map(d => d.id)} strategy={verticalListSortingStrategy}>
|
||||||
{group.decks.length === 0 ? (
|
<DroppableContainer id={group.id}>
|
||||||
<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.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} />
|
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>
|
</DroppableContainer>
|
||||||
|
</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"}`}>
|
||||||
<SortableContext id="uncategorized" items={uncategorizedDecks.map(d => d.id)} strategy={verticalListSortingStrategy}>
|
<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"}>
|
||||||
<DroppableContainer id="uncategorized">
|
<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>
|
||||||
{uncategorizedDecks.length === 0 ? (
|
</button>
|
||||||
<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>
|
<h3 className="text-lg font-semibold text-text-heading">Uncategorized</h3>
|
||||||
) : (
|
</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} />
|
<div className={collapsedGroups["uncategorized"] ? "hidden" : "block"}>
|
||||||
))
|
<SortableContext id="uncategorized" items={uncategorizedDecks.map(d => d.id)} strategy={verticalListSortingStrategy}>
|
||||||
)}
|
<DroppableContainer id="uncategorized">
|
||||||
</DroppableContainer>
|
{uncategorizedDecks.length === 0 ? (
|
||||||
</SortableContext>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
) : (
|
) : (
|
||||||
<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">
|
<div className="flex items-center gap-2">
|
||||||
|
|
@ -421,34 +442,44 @@ export default function QuizzesPage() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SortableContext id={group.id} items={group.quizzes.map(q => q.id)} strategy={verticalListSortingStrategy}>
|
<div className={collapsedGroups[group.id] ? "hidden" : "block"}>
|
||||||
<DroppableContainer id={group.id}>
|
<SortableContext id={group.id} items={group.quizzes.map(q => q.id)} strategy={verticalListSortingStrategy}>
|
||||||
{group.quizzes.length === 0 ? (
|
<DroppableContainer id={group.id}>
|
||||||
<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.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} />
|
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>
|
</DroppableContainer>
|
||||||
|
</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"}`}>
|
||||||
<SortableContext id="uncategorized" items={uncategorizedQuizzes.map(q => q.id)} strategy={verticalListSortingStrategy}>
|
<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"}>
|
||||||
<DroppableContainer id="uncategorized">
|
<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>
|
||||||
{uncategorizedQuizzes.length === 0 ? (
|
</button>
|
||||||
<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>
|
<h3 className="text-lg font-semibold text-text-heading">Uncategorized</h3>
|
||||||
) : (
|
</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} />
|
<div className={collapsedGroups["uncategorized"] ? "hidden" : "block"}>
|
||||||
))
|
<SortableContext id="uncategorized" items={uncategorizedQuizzes.map(q => q.id)} strategy={verticalListSortingStrategy}>
|
||||||
)}
|
<DroppableContainer id="uncategorized">
|
||||||
</DroppableContainer>
|
{uncategorizedQuizzes.length === 0 ? (
|
||||||
</SortableContext>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue