From 4227c7a3eccd3644bcff3e02597d2531c8b01741 Mon Sep 17 00:00:00 2001 From: arcgod-design Date: Wed, 1 Jul 2026 00:42:48 +0530 Subject: [PATCH] feat: add server-side pagination for problems list (closes #12) --- app/src/api/content.ts | 9 +-- app/src/sections/Problems.tsx | 77 ++++++++++++++++---- backend/src/controllers/contentController.ts | 33 +++++++-- 3 files changed, 91 insertions(+), 28 deletions(-) diff --git a/app/src/api/content.ts b/app/src/api/content.ts index 82445dc..37ae5df 100644 --- a/app/src/api/content.ts +++ b/app/src/api/content.ts @@ -45,13 +45,8 @@ export const getProblemsByTopic = async (topicId: string) => { return response.data; }; -/** - * Fetches every problem in the system, ordered by index. - * - * @returns A promise resolving to an array of all problem objects. - */ -export const getAllProblems = async () => { - const response = await axios.get(`${API_BASE_URL}/api/content/problems`); +export const getAllProblems = async (page: number = 1, limit: number = 20) => { + const response = await axios.get(`${API_BASE_URL}/api/content/problems?page=${page}&limit=${limit}`); return response.data; }; diff --git a/app/src/sections/Problems.tsx b/app/src/sections/Problems.tsx index 727622c..15ae3d9 100644 --- a/app/src/sections/Problems.tsx +++ b/app/src/sections/Problems.tsx @@ -1,4 +1,4 @@ -import { useState, useMemo, useEffect } from 'react'; +import { useState, useMemo, useEffect, useCallback } from 'react'; import { useQuery } from '@tanstack/react-query'; import { motion } from 'framer-motion'; import { @@ -15,7 +15,8 @@ import { ChevronDown, X, Save, - Code2 + Code2, + Loader2 } from 'lucide-react'; import { getAllProblems, getAllTopics } from '@/api/content'; import { updateProblemStatus, toggleBookmark as apiToggleBookmark, getUserProgress, updateNotes } from '@/api/userActions'; @@ -23,12 +24,9 @@ import { useAuth } from '@/contexts/AuthContext'; import { Input } from '@/components/ui/input'; import { toast } from 'sonner'; -export function Problems() { - const { data: problemsData = [], isLoading: problemsLoading } = useQuery({ - queryKey: ['problems'], - queryFn: getAllProblems - }); +const PAGE_SIZE = 20; +export function Problems() { const { data: topicsData = [], isLoading: topicsLoading } = useQuery({ queryKey: ['topics'], queryFn: getAllTopics @@ -42,9 +40,15 @@ export function Problems() { enabled: !!user, }); - const allProblems = problemsData; + const [allProblems, setAllProblems] = useState([]); + const [page, setPage] = useState(1); + const [hasMore, setHasMore] = useState(true); + const [totalProblems, setTotalProblems] = useState(0); + const [loadingProblems, setLoadingProblems] = useState(true); + const [loadingMore, setLoadingMore] = useState(false); + const topics = topicsData; - const filtersLoading = problemsLoading || topicsLoading; + const filtersLoading = topicsLoading || loadingProblems; const [searchQuery, setSearchQuery] = useState(''); const [difficultyFilter, setDifficultyFilter] = useState<'all' | 'Easy' | 'Medium' | 'Hard'>('all'); @@ -58,12 +62,41 @@ export function Problems() { const [notesMap, setNotesMap] = useState>({}); const [savingNote, setSavingNote] = useState(false); + const fetchProblems = useCallback(async (pageNum: number, append: boolean = false) => { + if (append) setLoadingMore(true); + else setLoadingProblems(true); + try { + const data = await getAllProblems(pageNum, PAGE_SIZE); + setAllProblems(prev => append ? [...prev, ...data.problems] : data.problems); + setTotalProblems(data.total); + setHasMore(pageNum < data.totalPages); + } catch { + if (!append) setAllProblems([]); + } finally { + if (append) setLoadingMore(false); + else setLoadingProblems(false); + } + }, []); + + useEffect(() => { + setPage(1); + setAllProblems([]); + setHasMore(true); + fetchProblems(1, false); + }, [fetchProblems]); + + const loadMore = () => { + const nextPage = page + 1; + setPage(nextPage); + fetchProblems(nextPage, true); + }; + useEffect(() => { if (userProgressData) { const completed = new Set(); const bookmarked = new Set(); const notesData: Record = {}; - + userProgressData.forEach((p: any) => { if (p.status === 'SOLVED') completed.add(p.problem_id); if (p.is_bookmarked) bookmarked.add(p.problem_id); @@ -71,7 +104,7 @@ export function Problems() { notesData[p.problem_id] = p.notes; } }); - + setCompletedProblems(completed); setBookmarkedProblems(bookmarked); setNotesMap(notesData); @@ -189,7 +222,7 @@ export function Problems() { Problem Library

- Browse and practice from our collection of {stats.total}+ problems + Browse and practice from our collection of {totalProblems}+ problems

@@ -297,7 +330,7 @@ export function Problems() { {/* Results Count */}

- Showing {filteredProblems.length} of {allProblems.length} problems + Showing {filteredProblems.length} of {totalProblems} problems

@@ -424,6 +457,24 @@ export function Problems() { })} + {/* Load More */} + {hasMore && filteredProblems.length > 0 && ( +
+ +
+ )} + {filteredProblems.length === 0 && (
diff --git a/backend/src/controllers/contentController.ts b/backend/src/controllers/contentController.ts index 0bc2bcb..93e7c16 100644 --- a/backend/src/controllers/contentController.ts +++ b/backend/src/controllers/contentController.ts @@ -135,21 +135,38 @@ export const getProblemsByTopic = async (req: Request, res: Response) => { }; /** - * @desc Get all problems - * @route GET /api/content/problems + * @desc Get all problems (paginated) + * @route GET /api/content/problems?page=1&limit=20 * @access Public * - * Fetches every problem in the system, ordered by display index. + * Fetches a page of problems ordered by display index. Returns total count + * and page metadata so the frontend can render a "Load More" button. * - * @param req - Express request object. - * @param res - Express response. Returns a JSON array of all problem objects. + * @param req - Express request. Accepts optional `page` and `limit` query params. + * @param res - Express response. Returns `{ problems, total, page, limit, totalPages }`. */ export const getAllProblems = async (req: Request, res: Response) => { try { - const problems = await prisma.problem.findMany({ - orderBy: { order_index: 'asc' } + const page = Math.max(1, parseInt(req.query.page as string) || 1); + const limit = Math.min(100, Math.max(1, parseInt(req.query.limit as string) || 20)); + const skip = (page - 1) * limit; + + const [problems, total] = await Promise.all([ + prisma.problem.findMany({ + orderBy: { order_index: 'asc' }, + skip, + take: limit + }), + prisma.problem.count() + ]); + + res.json({ + problems, + total, + page, + limit, + totalPages: Math.ceil(total / limit) }); - res.json(problems); } catch (error) { res.status(500).json({ message: 'Server Error' }); }