Skip to content

Commit bfc5632

Browse files
authored
Merge pull request #176 from CS3219-AY2425S1/feat/lazy_loading
Add lazy loading to question fetching
2 parents 92d42e5 + 7f0018f commit bfc5632

File tree

7 files changed

+217
-94
lines changed

7 files changed

+217
-94
lines changed

peerprep-fe/src/app/(main)/components/Main.tsx

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,15 @@ import ProblemTable from '../../../components/problems/ProblemTable';
55
import RejoinSession from './RejoinSession';
66

77
export default function MainComponent() {
8-
const { problems, filters, updateFilter, removeFilter, isLoading } =
9-
useFilteredProblems();
8+
const {
9+
problems,
10+
filters,
11+
updateFilter,
12+
removeFilter,
13+
isLoading,
14+
hasMore,
15+
loadMore,
16+
} = useFilteredProblems();
1017

1118
return (
1219
<div className="min-h-screen bg-gray-900 p-6 pt-24 text-gray-100">
@@ -17,7 +24,12 @@ export default function MainComponent() {
1724
updateFilter={updateFilter}
1825
removeFilter={removeFilter}
1926
/>
20-
<ProblemTable problems={problems} isLoading={isLoading} />
27+
<ProblemTable
28+
problems={problems}
29+
isLoading={isLoading}
30+
hasMore={hasMore}
31+
onLoadMore={loadMore}
32+
/>
2133
</div>
2234
</div>
2335
);

peerprep-fe/src/app/admin/page.tsx

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ function AdminPage() {
2020
updateFilter,
2121
removeFilter,
2222
isLoading,
23-
refetchFilter,
23+
hasMore,
24+
loadMore,
25+
fetchProblems,
2426
} = useFilteredProblems();
2527

2628
const validateEntries = (problem: Problem) => {
@@ -40,7 +42,7 @@ function AdminPage() {
4042
if (res.status !== 200) {
4143
throw new Error('Failed to delete problem');
4244
}
43-
refetchFilter();
45+
fetchProblems(1, false);
4446
return res;
4547
};
4648

@@ -59,7 +61,7 @@ function AdminPage() {
5961
title: problem.title,
6062
});
6163

62-
refetchFilter();
64+
fetchProblems(1, false);
6365
return res;
6466
} catch (e: unknown) {
6567
if (isAxiosError(e)) {
@@ -97,7 +99,7 @@ function AdminPage() {
9799
title: problem.title,
98100
});
99101

100-
refetchFilter();
102+
fetchProblems(1, false);
101103
toggleDialogOpen();
102104
return res;
103105
} catch (e: unknown) {
@@ -132,9 +134,11 @@ function AdminPage() {
132134
<ProblemTable
133135
problems={problems}
134136
isLoading={isLoading}
135-
showActions={true}
137+
hasMore={hasMore}
138+
onLoadMore={loadMore}
136139
handleDelete={handleDelete}
137140
handleEdit={handleEdit}
141+
showActions={true}
138142
/>
139143
</div>
140144
<ProblemInputDialog

peerprep-fe/src/app/collaboration/page.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ function CollaborationPageContent() {
3131
);
3232
const searchParams = useSearchParams();
3333
const matchId = searchParams.get('matchId');
34-
const { problems, isLoading } = useFilteredProblems();
34+
const { problems, isLoading, hasMore, loadMore } = useFilteredProblems();
3535
const { setLastMatchId } = useCollaborationStore();
3636

3737
useEffect(() => {
@@ -99,6 +99,8 @@ function CollaborationPageContent() {
9999
<ProblemTable
100100
problems={problems}
101101
isLoading={isLoading}
102+
hasMore={hasMore}
103+
onLoadMore={loadMore}
102104
rowCallback={handleCallback}
103105
/>
104106
</>

peerprep-fe/src/components/problems/ProblemRow.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,8 +159,8 @@ export default function ProblemRow({
159159
title="Confirm Delete"
160160
description={`Are you sure you want to delete \"${problem.title}\"?`}
161161
callback={() => {
162-
setIsDeleteDialogOpen(false);
163162
handleDeleteClick();
163+
setIsDeleteDialogOpen(false);
164164
}}
165165
callbackTitle="Delete"
166166
/>
@@ -170,7 +170,10 @@ export default function ProblemRow({
170170
isOpen={isEditDialogOpen}
171171
onClose={() => setIsEditDialogOpen(false)}
172172
problem={problem}
173-
requestCallback={handleEditClick}
173+
requestCallback={(problem) => {
174+
handleEditClick(problem);
175+
setIsEditDialogOpen(false);
176+
}}
174177
requestTitle="Update"
175178
/>
176179

peerprep-fe/src/components/problems/ProblemTable.tsx

Lines changed: 58 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,14 @@ import { Problem } from '@/types/types';
33
import ProblemRow from './ProblemRow';
44
import { Skeleton } from '@/components/ui/skeleton';
55
import { AxiosResponse } from 'axios';
6+
import { useEffect, useRef } from 'react';
67

78
interface ProblemTableProps {
89
problems: Problem[];
910
isLoading: boolean;
1011
showActions?: boolean;
12+
hasMore: boolean; // Add this
13+
onLoadMore: () => void; // Add this
1114
handleDelete?:
1215
| ((id: number) => Promise<AxiosResponse<unknown, unknown>>)
1316
| undefined;
@@ -21,10 +24,35 @@ export default function ProblemTable({
2124
problems,
2225
isLoading,
2326
showActions = false,
27+
hasMore,
28+
onLoadMore,
2429
handleDelete,
2530
handleEdit,
2631
rowCallback,
2732
}: ProblemTableProps) {
33+
const observerTarget = useRef<HTMLDivElement>(null);
34+
35+
useEffect(() => {
36+
const observer = new IntersectionObserver(
37+
(entries) => {
38+
if (entries[0].isIntersecting && !isLoading && hasMore) {
39+
onLoadMore();
40+
console.log('Loaded 10 more entries');
41+
}
42+
},
43+
{
44+
rootMargin: '100px',
45+
threshold: 0.1,
46+
},
47+
);
48+
49+
if (observerTarget.current) {
50+
observer.observe(observerTarget.current);
51+
}
52+
53+
return () => observer.disconnect();
54+
}, [isLoading, hasMore, onLoadMore]);
55+
2856
return (
2957
<div className="overflow-x-auto">
3058
<table className="w-full border-collapse">
@@ -37,36 +65,38 @@ export default function ProblemTable({
3765
</tr>
3866
</thead>
3967
<tbody>
40-
{isLoading
41-
? Array.from({ length: 5 }).map((_, index) => (
42-
<tr key={index} className="border-b border-gray-800">
43-
<td className="w-1/3 px-4 py-2">
44-
<Skeleton className="h-6 w-full bg-gray-600" />
45-
</td>
46-
<td className="px-4 py-2">
47-
<div className="flex flex-wrap">
48-
<Skeleton className="mb-1 mr-1 h-6 w-16 rounded-full bg-gray-600" />
49-
<Skeleton className="mb-1 mr-1 h-6 w-16 rounded-full bg-gray-600" />
50-
<Skeleton className="mb-1 mr-1 h-6 w-16 rounded-full bg-gray-600" />
51-
</div>
52-
</td>
53-
<td className="px-4 py-2">
54-
<Skeleton className="h-6 w-16 bg-gray-600" />
55-
</td>
56-
</tr>
57-
))
58-
: problems.map((problem) => (
59-
<ProblemRow
60-
key={problem._id}
61-
problem={problem}
62-
showActions={showActions}
63-
handleDelete={handleDelete}
64-
handleEdit={handleEdit}
65-
rowCallback={rowCallback}
66-
/>
67-
))}
68+
{problems.map((problem) => (
69+
<ProblemRow
70+
key={problem._id}
71+
problem={problem}
72+
showActions={showActions}
73+
handleDelete={handleDelete}
74+
handleEdit={handleEdit}
75+
rowCallback={rowCallback}
76+
/>
77+
))}
6878
</tbody>
6979
</table>
80+
81+
{/* Observer target and loading indicator */}
82+
<div ref={observerTarget} className="w-full py-4">
83+
{(isLoading || hasMore) && (
84+
<div className="space-y-3">
85+
{[...Array(3)].map((_, index) => (
86+
<div key={index} className="flex items-center space-x-4">
87+
<Skeleton className="h-12 w-full bg-gray-600" />
88+
</div>
89+
))}
90+
</div>
91+
)}
92+
</div>
93+
94+
{/* End of list message */}
95+
{!hasMore && problems.length > 0 && (
96+
<div className="py-4 text-center text-gray-400">
97+
No more problems to load
98+
</div>
99+
)}
70100
</div>
71101
);
72102
}
Lines changed: 87 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState, useCallback, useEffect } from 'react';
1+
import { useState, useCallback, useEffect, useRef } from 'react';
22
import { axiosClient } from '@/network/axiosClient';
33
import { Problem } from '@/types/types';
44

@@ -9,7 +9,10 @@ export interface FilterState {
99
search: string | null;
1010
}
1111

12+
const PAGE_SIZE = 20;
13+
1214
export function useFilteredProblems() {
15+
// States for both filtering and pagination
1316
const [problems, setProblems] = useState<Problem[]>([]);
1417
const [filters, setFilters] = useState<FilterState>({
1518
difficulty: null,
@@ -18,32 +21,75 @@ export function useFilteredProblems() {
1821
search: null,
1922
});
2023
const [isLoading, setIsLoading] = useState(true);
24+
const [page, setPage] = useState(1);
25+
const [hasMore, setHasMore] = useState(true);
26+
const seenIds = useRef(new Set<number>());
2127

22-
const fetchProblems = useCallback(async () => {
23-
setIsLoading(true);
24-
const params = new URLSearchParams();
25-
if (filters.difficulty) params.append('difficulty', filters.difficulty);
26-
if (filters.status) params.append('status', filters.status);
27-
if (filters.topics)
28-
filters.topics.forEach((topic) => params.append('topics', topic));
29-
if (filters.search) params.append('search', filters.search);
30-
try {
31-
const url = params.toString()
32-
? `/questions?${params.toString()}`
33-
: '/questions';
34-
const response = await axiosClient.get(url);
35-
setProblems(response.data);
36-
} catch (error) {
37-
console.error('Error fetching problems:', error);
38-
} finally {
39-
setIsLoading(false);
40-
}
41-
}, [filters]);
28+
const fetchProblems = useCallback(
29+
async (pageNum: number, isLoadingMore = false) => {
30+
if (!isLoadingMore) {
31+
seenIds.current.clear();
32+
}
4233

43-
useEffect(() => {
44-
fetchProblems();
45-
}, [fetchProblems]);
34+
setIsLoading(true);
35+
36+
try {
37+
const params = new URLSearchParams();
38+
params.append('page', pageNum.toString());
39+
params.append('limit', PAGE_SIZE.toString());
40+
41+
// Apply filters to query
42+
if (filters.difficulty) params.append('difficulty', filters.difficulty);
43+
if (filters.status) params.append('status', filters.status);
44+
if (filters.topics?.length) {
45+
filters.topics.forEach((topic) => params.append('topics', topic));
46+
}
47+
if (filters.search) params.append('search', filters.search);
48+
49+
const url = `/questions?${params.toString()}`;
50+
const response = await axiosClient.get<Problem[]>(url);
51+
const newProblems = response.data;
52+
53+
if (newProblems.length === 0) {
54+
setHasMore(false);
55+
return;
56+
}
57+
58+
if (isLoadingMore) {
59+
console.log('Fetching a page of 20 items');
60+
const uniqueNewProblems: Problem[] = [];
61+
let foundDuplicate = false;
62+
63+
for (const problem of newProblems) {
64+
if (seenIds.current.has(problem._id)) {
65+
foundDuplicate = true;
66+
break;
67+
}
68+
seenIds.current.add(problem._id);
69+
uniqueNewProblems.push(problem);
70+
}
71+
72+
if (foundDuplicate || uniqueNewProblems.length === 0) {
73+
setHasMore(false);
74+
}
4675

76+
setProblems((prev) => [...prev, ...uniqueNewProblems]);
77+
} else {
78+
newProblems.forEach((problem) => seenIds.current.add(problem._id));
79+
setProblems(newProblems);
80+
setHasMore(newProblems.length === PAGE_SIZE);
81+
}
82+
} catch (error) {
83+
console.error('Error fetching problems:', error);
84+
setHasMore(false);
85+
} finally {
86+
setIsLoading(false);
87+
}
88+
},
89+
[filters],
90+
); // Note filters dependency
91+
92+
// Filter functions
4793
const updateFilter = useCallback(
4894
(key: keyof FilterState, value: string | string[] | null) => {
4995
setFilters((prev) => ({
@@ -67,18 +113,29 @@ export function useFilteredProblems() {
67113
}));
68114
}, []);
69115

70-
const refetchFilter = useCallback(() => {
71-
setFilters((prev) => ({
72-
...prev,
73-
}));
74-
}, []);
116+
// Reset and fetch when filters change
117+
useEffect(() => {
118+
setPage(1);
119+
fetchProblems(1, false);
120+
}, [filters, fetchProblems]);
121+
122+
// Load more function for infinite scroll
123+
const loadMore = useCallback(() => {
124+
if (!isLoading && hasMore) {
125+
const nextPage = page + 1;
126+
setPage(nextPage);
127+
fetchProblems(nextPage, true);
128+
}
129+
}, [isLoading, hasMore, page, fetchProblems]);
75130

76131
return {
77132
problems,
78133
filters,
79134
updateFilter,
80135
removeFilter,
81136
isLoading,
82-
refetchFilter,
137+
hasMore,
138+
loadMore,
139+
fetchProblems,
83140
};
84141
}

0 commit comments

Comments
 (0)