Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,10 @@ export function AdminFlashcardsPage() {
}, [searchInput])

useEffect(() => {
void fetchData()
const t = window.setTimeout(() => {
void fetchData()
}, 0)
return () => window.clearTimeout(t)
}, [fetchData])

/** Deck scope from URL (?view=all&mainDeckId=…&deckSlug=… or ?deckScope=all to clear deck filters). */
Expand All @@ -269,26 +272,34 @@ export function AdminFlashcardsPage() {
const deckScopeAll = searchParams.get('deckScope') === 'all'

if (view === 'all' || mid || deckSlug || deckScopeAll) {
setActiveView('all')
queueMicrotask(() => setActiveView('all'))
}

if (deckScopeAll) {
setSelectedMainDeckId(null)
setSelectedSubdeckSlug(null)
queueMicrotask(() => {
setSelectedMainDeckId(null)
setSelectedSubdeckSlug(null)
})
return
}

if (mid && deckSlug) {
setSelectedMainDeckId(mid)
setSelectedSubdeckSlug(deckSlug)
queueMicrotask(() => {
setSelectedMainDeckId(mid)
setSelectedSubdeckSlug(deckSlug)
})
} else if (mid) {
setSelectedMainDeckId(mid)
setSelectedSubdeckSlug(null)
queueMicrotask(() => {
setSelectedMainDeckId(mid)
setSelectedSubdeckSlug(null)
})
} else if (deckSlug) {
setSelectedSubdeckSlug(deckSlug)
queueMicrotask(() => setSelectedSubdeckSlug(deckSlug))
} else {
setSelectedMainDeckId(null)
setSelectedSubdeckSlug(null)
queueMicrotask(() => {
setSelectedMainDeckId(null)
setSelectedSubdeckSlug(null)
})
}
}, [searchParams])

Expand All @@ -298,20 +309,24 @@ export function AdminFlashcardsPage() {
const mid = searchParams.get('mainDeckId')?.trim()
if (!deckSlug || mid) return
const row = deckRows.find((d) => d.slug === deckSlug && d.parentDeckId)
if (row?.parentDeckId) setSelectedMainDeckId(row.parentDeckId)
const parentDeckId = row?.parentDeckId ?? null
if (parentDeckId) queueMicrotask(() => setSelectedMainDeckId(parentDeckId))
}, [searchParams, deckRows])

useEffect(() => {
void fetchCoursesHierarchy()
const t = window.setTimeout(() => {
void fetchCoursesHierarchy()
}, 0)
return () => window.clearTimeout(t)
}, [fetchCoursesHierarchy])

// Reset tag pagination when toggling between main/all tags
useEffect(() => {
setTagPage(1)
queueMicrotask(() => setTagPage(1))
}, [showAllTags])

useEffect(() => {
setCardPage(1)
queueMicrotask(() => setCardPage(1))
}, [debouncedSearch, selectedTagSlugs, selectedSubdeckSlug, selectedMainDeckId, sortKey])

/** Load CMS subjects when the standalone deck form opens; reset combobox state. */
Expand All @@ -321,12 +336,14 @@ export function AdminFlashcardsPage() {
clearTimeout(standaloneSubjectBlurTimer.current)
standaloneSubjectBlurTimer.current = null
}
setStandaloneSubjectMenuOpen(false)
queueMicrotask(() => setStandaloneSubjectMenuOpen(false))
return
}
setStandaloneSubjectInput('')
setStandaloneSubjectId(null)
setStandaloneDeckTagIds([])
queueMicrotask(() => {
setStandaloneSubjectInput('')
setStandaloneSubjectId(null)
setStandaloneDeckTagIds([])
})
let cancelled = false
void (async () => {
try {
Expand Down Expand Up @@ -497,7 +514,7 @@ export function AdminFlashcardsPage() {

useEffect(() => {
const tp = totalFlashcardGridPages(filteredFlashcards.length)
setCardPage((p) => Math.min(p, tp))
queueMicrotask(() => setCardPage((p) => Math.min(p, tp)))
}, [filteredFlashcards.length])

const browseMode = activeView === 'all'
Expand Down Expand Up @@ -581,11 +598,13 @@ export function AdminFlashcardsPage() {

useEffect(() => {
if (coursesWithoutMainDeck.length === 0) {
setDeckCourseId('')
queueMicrotask(() => setDeckCourseId(''))
return
}
const allowed = new Set(coursesWithoutMainDeck.map((c) => c.id))
setDeckCourseId((prev) => (prev && allowed.has(prev) ? prev : coursesWithoutMainDeck[0]!.id))
queueMicrotask(() =>
setDeckCourseId((prev) => (prev && allowed.has(prev) ? prev : coursesWithoutMainDeck[0]!.id)),
)
}, [coursesWithoutMainDeck])

const usedModuleIds = useMemo(
Expand All @@ -598,7 +617,7 @@ export function AdminFlashcardsPage() {
[deckRows],
)

const subdecksByMain = useMemo(() => {
const subdecksByMain = (() => {
const grouped = new Map<string, DeckRow[]>()
for (const deck of deckRows) {
if (!deck.parentDeckId) continue
Expand All @@ -625,7 +644,7 @@ export function AdminFlashcardsPage() {
})
}
return grouped
Comment thread
Z4phxr marked this conversation as resolved.
}, [deckRows, moduleById])
})()

/** Subdecks under the selected main deck (toolbar filter). */
const subdecksForFilterMain = useMemo(() => {
Expand All @@ -637,7 +656,7 @@ export function AdminFlashcardsPage() {
if (!selectedSubdeckSlug || !selectedMainDeckId) return
const subs = subdecksByMain.get(selectedMainDeckId) ?? []
if (!subs.some((s) => s.slug === selectedSubdeckSlug)) {
setSelectedSubdeckSlug(null)
queueMicrotask(() => setSelectedSubdeckSlug(null))
}
}, [selectedMainDeckId, selectedSubdeckSlug, subdecksByMain])

Expand All @@ -647,18 +666,22 @@ export function AdminFlashcardsPage() {
mainDeckOptions.some((d) => d.id === selectedMainDeckId) ||
standaloneMainDeckOptions.some((d) => d.id === selectedMainDeckId)
if (!stillValid) {
setSelectedMainDeckId(null)
setSelectedSubdeckSlug(null)
queueMicrotask(() => {
setSelectedMainDeckId(null)
setSelectedSubdeckSlug(null)
})
}
}, [selectedMainDeckId, mainDeckOptions, standaloneMainDeckOptions])

useEffect(() => {
setInlineSubdeckMainId((cur) =>
cur != null && !expandedHierarchyMainIds.has(cur) ? null : cur,
)
setInlineStandaloneSubdeckMainId((cur) =>
cur != null && !expandedHierarchyMainIds.has(cur) ? null : cur,
)
queueMicrotask(() => {
setInlineSubdeckMainId((cur) =>
cur != null && !expandedHierarchyMainIds.has(cur) ? null : cur,
)
setInlineStandaloneSubdeckMainId((cur) =>
cur != null && !expandedHierarchyMainIds.has(cur) ? null : cur,
)
})
}, [expandedHierarchyMainIds])

const mainDeckDialogTitle = useMemo(
Expand Down Expand Up @@ -1907,7 +1930,6 @@ export function AdminFlashcardsPage() {

// Paginate: 15 tags per page
const TAGS_PER_PAGE = 15
const totalPages = Math.max(1, Math.ceil(arr.length / TAGS_PER_PAGE))
const startIdx = (tagPage - 1) * TAGS_PER_PAGE
const paginatedTags = arr.slice(startIdx, startIdx + TAGS_PER_PAGE)

Expand Down
41 changes: 6 additions & 35 deletions LearningPlatform/app/(admin)/admin/flashcards/tags/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,40 +41,6 @@ function truncate(text: string, max = 80): string {
return text.length <= max ? text : text.slice(0, max).trimEnd() + '…'
}

function stateConfig(state: FlashcardState): { label: string; cls: string } {
switch (state) {
case 'NEW': return { label: 'New', cls: 'bg-gray-100 text-gray-500 dark:bg-gray-800 dark:text-gray-400' }
case 'LEARNING': return { label: 'Learning', cls: 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/40 dark:text-yellow-300' }
case 'REVIEW': return { label: 'Review', cls: 'bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-300' }
case 'RELEARNING': return { label: 'Relearning', cls: 'bg-orange-100 text-orange-700 dark:bg-orange-900/40 dark:text-orange-300' }
case 'MASTERED': return { label: 'Mastered', cls: 'bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300' }
}
}

function sortByUrgency(cards: Flashcard[]): Flashcard[] {
const now = new Date()
const endOfDay = new Date(now); endOfDay.setHours(23, 59, 59, 999)
function urgency(c: Flashcard): number {
const due = c.nextReviewAt ? new Date(c.nextReviewAt) : null
if (c.state === 'RELEARNING') return due && due < now ? 1 : 3
if (c.state === 'REVIEW' || c.state === 'MASTERED') {
if (!due) return 4
if (due < now) return 2
if (due <= endOfDay) return 4
return 7
}
if (c.state === 'LEARNING') return due && due < now ? 1 : 3
if (c.state === 'NEW') return 5
return 6
}
return [...cards].sort((a, b) => {
const d = urgency(a) - urgency(b)
if (d !== 0) return d
return (a.nextReviewAt ? new Date(a.nextReviewAt).getTime() : 0) -
(b.nextReviewAt ? new Date(b.nextReviewAt).getTime() : 0)
})
}

// ─── Page ─────────────────────────────────────────────────────────────────────

export default function FlashcardsByTagPage({
Expand Down Expand Up @@ -126,7 +92,12 @@ export default function FlashcardsByTagPage({
}
}, [slug])

useEffect(() => { fetchData() }, [fetchData])
useEffect(() => {
const t = window.setTimeout(() => {
void fetchData()
}, 0)
return () => window.clearTimeout(t)
}, [fetchData])

// ── Open dialogs ─────────────────────────────────────────────────────────────

Expand Down
1 change: 0 additions & 1 deletion LearningPlatform/app/(admin)/admin/lessons/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { getPayload } from 'payload'
import config from '@payload-config'
import { unstable_cache } from 'next/cache'
import { Card, CardContent, CardHeader } from '@/components/ui/card'
import Link from 'next/link'
import { cn } from '@/lib/utils'
import { adminGlassCard, studentGlassPill } from '@/lib/student-glass-styles'
import { payloadTableExists } from '@/lib/payload-utils'
Expand Down
1 change: 0 additions & 1 deletion LearningPlatform/app/(admin)/admin/media/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { Card, CardContent } from '@/components/ui/card'
import { cn } from '@/lib/utils'
import { adminGlassCard } from '@/lib/student-glass-styles'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Image as ImageIcon, Video, FileText, ExternalLink, ChevronLeft, ChevronRight } from 'lucide-react'
import Link from 'next/link'
import { payloadTableExists } from '@/lib/payload-utils'
Expand Down
11 changes: 7 additions & 4 deletions LearningPlatform/app/(admin)/admin/subjects/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,6 @@ export default function AdminSubjectsPage() {
.replace(/-+/g, '-')
.replace(/^[-]+|[-]+$/g, '')

useEffect(() => {
fetchSubjects()
}, [])

const fetchSubjects = async () => {
try {
setLoading(true)
Expand All @@ -57,6 +53,13 @@ export default function AdminSubjectsPage() {
}
}

useEffect(() => {
const t = window.setTimeout(() => {
void fetchSubjects()
}, 0)
return () => window.clearTimeout(t)
}, [])

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setLoading(true)
Expand Down
7 changes: 6 additions & 1 deletion LearningPlatform/app/(admin)/admin/tags/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,12 @@ export default function AdminTagsPage() {
}
}, [])

useEffect(() => { void fetchTags() }, [fetchTags])
useEffect(() => {
const t = window.setTimeout(() => {
void fetchTags()
}, 0)
return () => window.clearTimeout(t)
}, [fetchTags])

// ── Filtered + sorted display list ─────────────────────────────────────────────

Expand Down
18 changes: 6 additions & 12 deletions LearningPlatform/app/(admin)/admin/tasks/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -165,14 +165,6 @@ export default function AdminTasksPage() {
const [tagSortDir, setTagSortDir] = useState<'asc' | 'desc'>('asc')
const [tagPage, setTagPage] = useState(1)

// Tag sort buttons (mirror /admin/tags controls)
const tagSortButtons: Array<{ key: string; icon: React.ReactNode; title: string; onClick: () => void }> = [
{ key: 'name-asc', icon: <ArrowUpAZ className="h-4 w-4" />, title: 'A → Z', onClick: () => { setTagSortField('name'); setTagSortDir('asc') } },
{ key: 'name-desc', icon: <ArrowDownAZ className="h-4 w-4" />, title: 'Z → A', onClick: () => { setTagSortField('name'); setTagSortDir('desc') } },
{ key: 'count-desc', icon: <ArrowDown01 className="h-4 w-4" />, title: 'Most appearances (tasks + flashcards)', onClick: () => { setTagSortField('count'); setTagSortDir('desc') } },
{ key: 'count-asc', icon: <ArrowUp01 className="h-4 w-4" />, title: 'Fewest appearances (tasks + flashcards)', onClick: () => { setTagSortField('count'); setTagSortDir('asc') } },
]

// Edit dialog
const [editDialogOpen, setEditDialogOpen] = useState(false)
const [editTask, setEditTask] = useState<Task | undefined>(undefined)
Expand Down Expand Up @@ -234,7 +226,10 @@ export default function AdminTasksPage() {

// Refetch when tag or task-type filters change (search is applied client-side on the loaded list).
useEffect(() => {
void fetchData()
const t = window.setTimeout(() => {
void fetchData()
}, 0)
return () => window.clearTimeout(t)
}, [fetchData])

function toggleTagSlug(slug: string) {
Expand Down Expand Up @@ -365,12 +360,12 @@ export default function AdminTasksPage() {

// Reset page to 1 whenever filters/sort or underlying tasks change
useEffect(() => {
setCurrentPage(1)
queueMicrotask(() => setCurrentPage(1))
}, [tasks, debouncedSearch, typeFilter, selectedTagSlugs, sortKey])

// Reset tag pagination when toggling between main/all tags
useEffect(() => {
setTagPage(1)
queueMicrotask(() => setTagPage(1))
}, [showAllTags])

// Paged tasks (50 per page)
Expand Down Expand Up @@ -618,7 +613,6 @@ export default function AdminTasksPage() {
{(() => {
// Paginate: 15 tags per page
const TAGS_PER_PAGE = 15
const totalPages = Math.max(1, Math.ceil(sortedTags.length / TAGS_PER_PAGE))
const startIdx = (tagPage - 1) * TAGS_PER_PAGE
const paginatedTags = sortedTags.slice(startIdx, startIdx + TAGS_PER_PAGE)
return paginatedTags.map((tag) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,14 @@ export default function StandaloneFlashcardBrowsePage() {
}, [])

useEffect(() => {
void load()
const t = window.setTimeout(() => {
void load()
}, 0)
return () => window.clearTimeout(t)
}, [load])

useEffect(() => {
setDeckListPage(1)
queueMicrotask(() => setDeckListPage(1))
}, [appliedSearch, appliedSubjectId, appliedSort])

const catalogSubjects = useMemo(() => {
Expand Down Expand Up @@ -125,7 +128,7 @@ export default function StandaloneFlashcardBrowsePage() {
const deckListTotalPages = Math.max(1, Math.ceil(filteredDecks.length / DECKS_PER_PAGE))

useEffect(() => {
setDeckListPage((p) => Math.min(p, deckListTotalPages))
queueMicrotask(() => setDeckListPage((p) => Math.min(p, deckListTotalPages)))
}, [deckListTotalPages])

const paginatedFilteredDecks = useMemo(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import { Label } from '@/components/ui/label'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { ArrowLeft, Loader2, Save, RotateCcw } from 'lucide-react'
import { LessonReadingSizeSettings } from '@/components/settings/lesson-reading-size'
import ThemeToggle from '@/components/theme-toggle'
import { studentGlassCard } from '@/lib/student-glass-styles'
import { cn } from '@/lib/utils'

Expand Down
Loading
Loading