Skip to content

Commit

Permalink
Merge pull request #176 from CS3219-AY2425S1/feat/lazy_loading
Browse files Browse the repository at this point in the history
Add lazy loading to question fetching
  • Loading branch information
tyouwei authored Nov 13, 2024
2 parents 92d42e5 + 7f0018f commit bfc5632
Show file tree
Hide file tree
Showing 7 changed files with 217 additions and 94 deletions.
18 changes: 15 additions & 3 deletions peerprep-fe/src/app/(main)/components/Main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,15 @@ import ProblemTable from '../../../components/problems/ProblemTable';
import RejoinSession from './RejoinSession';

export default function MainComponent() {
const { problems, filters, updateFilter, removeFilter, isLoading } =
useFilteredProblems();
const {
problems,
filters,
updateFilter,
removeFilter,
isLoading,
hasMore,
loadMore,
} = useFilteredProblems();

return (
<div className="min-h-screen bg-gray-900 p-6 pt-24 text-gray-100">
Expand All @@ -17,7 +24,12 @@ export default function MainComponent() {
updateFilter={updateFilter}
removeFilter={removeFilter}
/>
<ProblemTable problems={problems} isLoading={isLoading} />
<ProblemTable
problems={problems}
isLoading={isLoading}
hasMore={hasMore}
onLoadMore={loadMore}
/>
</div>
</div>
);
Expand Down
14 changes: 9 additions & 5 deletions peerprep-fe/src/app/admin/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ function AdminPage() {
updateFilter,
removeFilter,
isLoading,
refetchFilter,
hasMore,
loadMore,
fetchProblems,
} = useFilteredProblems();

const validateEntries = (problem: Problem) => {
Expand All @@ -40,7 +42,7 @@ function AdminPage() {
if (res.status !== 200) {
throw new Error('Failed to delete problem');
}
refetchFilter();
fetchProblems(1, false);
return res;
};

Expand All @@ -59,7 +61,7 @@ function AdminPage() {
title: problem.title,
});

refetchFilter();
fetchProblems(1, false);
return res;
} catch (e: unknown) {
if (isAxiosError(e)) {
Expand Down Expand Up @@ -97,7 +99,7 @@ function AdminPage() {
title: problem.title,
});

refetchFilter();
fetchProblems(1, false);
toggleDialogOpen();
return res;
} catch (e: unknown) {
Expand Down Expand Up @@ -132,9 +134,11 @@ function AdminPage() {
<ProblemTable
problems={problems}
isLoading={isLoading}
showActions={true}
hasMore={hasMore}
onLoadMore={loadMore}
handleDelete={handleDelete}
handleEdit={handleEdit}
showActions={true}
/>
</div>
<ProblemInputDialog
Expand Down
4 changes: 3 additions & 1 deletion peerprep-fe/src/app/collaboration/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ function CollaborationPageContent() {
);
const searchParams = useSearchParams();
const matchId = searchParams.get('matchId');
const { problems, isLoading } = useFilteredProblems();
const { problems, isLoading, hasMore, loadMore } = useFilteredProblems();
const { setLastMatchId } = useCollaborationStore();

useEffect(() => {
Expand Down Expand Up @@ -99,6 +99,8 @@ function CollaborationPageContent() {
<ProblemTable
problems={problems}
isLoading={isLoading}
hasMore={hasMore}
onLoadMore={loadMore}
rowCallback={handleCallback}
/>
</>
Expand Down
7 changes: 5 additions & 2 deletions peerprep-fe/src/components/problems/ProblemRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -159,8 +159,8 @@ export default function ProblemRow({
title="Confirm Delete"
description={`Are you sure you want to delete \"${problem.title}\"?`}
callback={() => {
setIsDeleteDialogOpen(false);
handleDeleteClick();
setIsDeleteDialogOpen(false);
}}
callbackTitle="Delete"
/>
Expand All @@ -170,7 +170,10 @@ export default function ProblemRow({
isOpen={isEditDialogOpen}
onClose={() => setIsEditDialogOpen(false)}
problem={problem}
requestCallback={handleEditClick}
requestCallback={(problem) => {
handleEditClick(problem);
setIsEditDialogOpen(false);
}}
requestTitle="Update"
/>

Expand Down
86 changes: 58 additions & 28 deletions peerprep-fe/src/components/problems/ProblemTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@ import { Problem } from '@/types/types';
import ProblemRow from './ProblemRow';
import { Skeleton } from '@/components/ui/skeleton';
import { AxiosResponse } from 'axios';
import { useEffect, useRef } from 'react';

interface ProblemTableProps {
problems: Problem[];
isLoading: boolean;
showActions?: boolean;
hasMore: boolean; // Add this
onLoadMore: () => void; // Add this
handleDelete?:
| ((id: number) => Promise<AxiosResponse<unknown, unknown>>)
| undefined;
Expand All @@ -21,10 +24,35 @@ export default function ProblemTable({
problems,
isLoading,
showActions = false,
hasMore,
onLoadMore,
handleDelete,
handleEdit,
rowCallback,
}: ProblemTableProps) {
const observerTarget = useRef<HTMLDivElement>(null);

useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting && !isLoading && hasMore) {
onLoadMore();
console.log('Loaded 10 more entries');
}
},
{
rootMargin: '100px',
threshold: 0.1,
},
);

if (observerTarget.current) {
observer.observe(observerTarget.current);
}

return () => observer.disconnect();
}, [isLoading, hasMore, onLoadMore]);

return (
<div className="overflow-x-auto">
<table className="w-full border-collapse">
Expand All @@ -37,36 +65,38 @@ export default function ProblemTable({
</tr>
</thead>
<tbody>
{isLoading
? Array.from({ length: 5 }).map((_, index) => (
<tr key={index} className="border-b border-gray-800">
<td className="w-1/3 px-4 py-2">
<Skeleton className="h-6 w-full bg-gray-600" />
</td>
<td className="px-4 py-2">
<div className="flex flex-wrap">
<Skeleton className="mb-1 mr-1 h-6 w-16 rounded-full bg-gray-600" />
<Skeleton className="mb-1 mr-1 h-6 w-16 rounded-full bg-gray-600" />
<Skeleton className="mb-1 mr-1 h-6 w-16 rounded-full bg-gray-600" />
</div>
</td>
<td className="px-4 py-2">
<Skeleton className="h-6 w-16 bg-gray-600" />
</td>
</tr>
))
: problems.map((problem) => (
<ProblemRow
key={problem._id}
problem={problem}
showActions={showActions}
handleDelete={handleDelete}
handleEdit={handleEdit}
rowCallback={rowCallback}
/>
))}
{problems.map((problem) => (
<ProblemRow
key={problem._id}
problem={problem}
showActions={showActions}
handleDelete={handleDelete}
handleEdit={handleEdit}
rowCallback={rowCallback}
/>
))}
</tbody>
</table>

{/* Observer target and loading indicator */}
<div ref={observerTarget} className="w-full py-4">
{(isLoading || hasMore) && (
<div className="space-y-3">
{[...Array(3)].map((_, index) => (
<div key={index} className="flex items-center space-x-4">
<Skeleton className="h-12 w-full bg-gray-600" />
</div>
))}
</div>
)}
</div>

{/* End of list message */}
{!hasMore && problems.length > 0 && (
<div className="py-4 text-center text-gray-400">
No more problems to load
</div>
)}
</div>
);
}
117 changes: 87 additions & 30 deletions peerprep-fe/src/hooks/useFilteredProblems.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState, useCallback, useEffect } from 'react';
import { useState, useCallback, useEffect, useRef } from 'react';
import { axiosClient } from '@/network/axiosClient';
import { Problem } from '@/types/types';

Expand All @@ -9,7 +9,10 @@ export interface FilterState {
search: string | null;
}

const PAGE_SIZE = 20;

export function useFilteredProblems() {
// States for both filtering and pagination
const [problems, setProblems] = useState<Problem[]>([]);
const [filters, setFilters] = useState<FilterState>({
difficulty: null,
Expand All @@ -18,32 +21,75 @@ export function useFilteredProblems() {
search: null,
});
const [isLoading, setIsLoading] = useState(true);
const [page, setPage] = useState(1);
const [hasMore, setHasMore] = useState(true);
const seenIds = useRef(new Set<number>());

const fetchProblems = useCallback(async () => {
setIsLoading(true);
const params = new URLSearchParams();
if (filters.difficulty) params.append('difficulty', filters.difficulty);
if (filters.status) params.append('status', filters.status);
if (filters.topics)
filters.topics.forEach((topic) => params.append('topics', topic));
if (filters.search) params.append('search', filters.search);
try {
const url = params.toString()
? `/questions?${params.toString()}`
: '/questions';
const response = await axiosClient.get(url);
setProblems(response.data);
} catch (error) {
console.error('Error fetching problems:', error);
} finally {
setIsLoading(false);
}
}, [filters]);
const fetchProblems = useCallback(
async (pageNum: number, isLoadingMore = false) => {
if (!isLoadingMore) {
seenIds.current.clear();
}

useEffect(() => {
fetchProblems();
}, [fetchProblems]);
setIsLoading(true);

try {
const params = new URLSearchParams();
params.append('page', pageNum.toString());
params.append('limit', PAGE_SIZE.toString());

// Apply filters to query
if (filters.difficulty) params.append('difficulty', filters.difficulty);
if (filters.status) params.append('status', filters.status);
if (filters.topics?.length) {
filters.topics.forEach((topic) => params.append('topics', topic));
}
if (filters.search) params.append('search', filters.search);

const url = `/questions?${params.toString()}`;
const response = await axiosClient.get<Problem[]>(url);
const newProblems = response.data;

if (newProblems.length === 0) {
setHasMore(false);
return;
}

if (isLoadingMore) {
console.log('Fetching a page of 20 items');
const uniqueNewProblems: Problem[] = [];
let foundDuplicate = false;

for (const problem of newProblems) {
if (seenIds.current.has(problem._id)) {
foundDuplicate = true;
break;
}
seenIds.current.add(problem._id);
uniqueNewProblems.push(problem);
}

if (foundDuplicate || uniqueNewProblems.length === 0) {
setHasMore(false);
}

setProblems((prev) => [...prev, ...uniqueNewProblems]);
} else {
newProblems.forEach((problem) => seenIds.current.add(problem._id));
setProblems(newProblems);
setHasMore(newProblems.length === PAGE_SIZE);
}
} catch (error) {
console.error('Error fetching problems:', error);
setHasMore(false);
} finally {
setIsLoading(false);
}
},
[filters],
); // Note filters dependency

// Filter functions
const updateFilter = useCallback(
(key: keyof FilterState, value: string | string[] | null) => {
setFilters((prev) => ({
Expand All @@ -67,18 +113,29 @@ export function useFilteredProblems() {
}));
}, []);

const refetchFilter = useCallback(() => {
setFilters((prev) => ({
...prev,
}));
}, []);
// Reset and fetch when filters change
useEffect(() => {
setPage(1);
fetchProblems(1, false);
}, [filters, fetchProblems]);

// Load more function for infinite scroll
const loadMore = useCallback(() => {
if (!isLoading && hasMore) {
const nextPage = page + 1;
setPage(nextPage);
fetchProblems(nextPage, true);
}
}, [isLoading, hasMore, page, fetchProblems]);

return {
problems,
filters,
updateFilter,
removeFilter,
isLoading,
refetchFilter,
hasMore,
loadMore,
fetchProblems,
};
}
Loading

0 comments on commit bfc5632

Please sign in to comment.