Skip to content
Open
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
9 changes: 2 additions & 7 deletions app/src/api/content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};

Expand Down
75 changes: 63 additions & 12 deletions app/src/sections/Problems.tsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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';
Expand All @@ -24,12 +25,9 @@ import { Input } from '@/components/ui/input';
import { toast } from 'sonner';
import { SOLVE_XP } from '@/utils/xpConfig';

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
Expand All @@ -43,9 +41,15 @@ export function Problems() {
enabled: !!user,
});

const allProblems = problemsData;
const [allProblems, setAllProblems] = useState<any[]>([]);
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');
Expand All @@ -59,6 +63,35 @@ export function Problems() {
const [notesMap, setNotesMap] = useState<Record<string, string>>({});
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<string>();
Expand All @@ -73,7 +106,7 @@ export function Problems() {
notesData[p.problem_id] = p.notes;
}
});

setCompletedProblems(completed);
setBookmarkedProblems(bookmarked);
setNotesMap(notesData);
Expand Down Expand Up @@ -195,7 +228,7 @@ export function Problems() {
Problem <span className="gradient-text">Library</span>
</h1>
<p className="text-white/60">
Browse and practice from our collection of {stats.total}+ problems
Browse and practice from our collection of {totalProblems}+ problems
</p>
</motion.div>

Expand Down Expand Up @@ -303,7 +336,7 @@ export function Problems() {
{/* Results Count */}
<div className="flex items-center justify-between mb-4">
<p className="text-white/60 text-sm">
Showing {filteredProblems.length} of {allProblems.length} problems
Showing {filteredProblems.length} of {totalProblems} problems
</p>
<div className="flex items-center gap-2 text-sm text-white/40">
<CheckCircle2 className="w-4 h-4" />
Expand Down Expand Up @@ -432,6 +465,24 @@ export function Problems() {
})}
</motion.div>

{/* Load More */}
{hasMore && filteredProblems.length > 0 && (
<div className="flex justify-center mt-6">
<button
onClick={loadMore}
disabled={loadingMore}
className="px-6 py-2.5 rounded-xl bg-white/5 hover:bg-white/10 text-white/60 hover:text-white text-sm font-medium transition-all disabled:opacity-50 disabled:cursor-not-allowed"
>
{loadingMore ? (
<span className="flex items-center gap-2">
<Loader2 className="w-4 h-4 animate-spin" />
Loading...
</span>
) : 'Load More'}
</button>
</div>
)}

{filteredProblems.length === 0 && (
<div className="text-center py-16">
<div className="w-16 h-16 rounded-full bg-white/5 flex items-center justify-center mx-auto mb-4">
Expand Down
33 changes: 25 additions & 8 deletions backend/src/controllers/contentController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' });
}
Expand Down
Loading