chore: implement roadmap pagination

ai/pagination
Arik Chakma 7 months ago
parent 7daf2e5d1d
commit 3a7dba8335
  1. 107
      src/components/ExploreAIRoadmap/ExploreAIRoadmap.tsx
  2. 7
      src/lib/number.ts

@ -1,9 +1,10 @@
import { useCallback, useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useToast } from '../../hooks/use-toast'; import { useToast } from '../../hooks/use-toast';
import { httpGet } from '../../lib/http'; import { httpGet } from '../../lib/http';
import { getRelativeTimeString } from '../../lib/date'; import { getRelativeTimeString } from '../../lib/date';
import { Eye, Loader2, RefreshCcw } from 'lucide-react'; import { Eye, Loader2, RefreshCcw } from 'lucide-react';
import { AIRoadmapAlert } from '../GenerateRoadmap/AIRoadmapAlert.tsx'; import { AIRoadmapAlert } from '../GenerateRoadmap/AIRoadmapAlert.tsx';
import { formatCommaNumber } from '../../lib/number.ts';
export interface AIRoadmapDocument { export interface AIRoadmapDocument {
_id?: string; _id?: string;
@ -27,13 +28,11 @@ export function ExploreAIRoadmap() {
const toast = useToast(); const toast = useToast();
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [isLoadingMore, setIsLoadingMore] = useState(false); const [roadmapsResponse, setRoadmapsResponse] =
const [roadmaps, setRoadmaps] = useState<AIRoadmapDocument[]>([]); useState<ExploreRoadmapsResponse | null>(null);
const [currPage, setCurrPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
const loadAIRoadmaps = useCallback( const loadAIRoadmaps = async (currPage: number) => {
async (currPage: number) => { setIsLoading(true);
const { response, error } = await httpGet<ExploreRoadmapsResponse>( const { response, error } = await httpGet<ExploreRoadmapsResponse>(
`${import.meta.env.PUBLIC_API_URL}/v1-list-ai-roadmaps`, `${import.meta.env.PUBLIC_API_URL}/v1-list-ai-roadmaps`,
{ {
@ -46,20 +45,16 @@ export function ExploreAIRoadmap() {
return; return;
} }
const newRoadmaps = [...roadmaps, ...response.data]; setRoadmapsResponse(response);
if ( };
JSON.stringify(roadmaps) === JSON.stringify(response.data) ||
JSON.stringify(roadmaps) === JSON.stringify(newRoadmaps)
) {
return;
}
setRoadmaps(newRoadmaps); const currPage = roadmapsResponse?.currPage || 1;
setCurrPage(response.currPage); const totalPages = roadmapsResponse?.totalPages || 1;
setTotalPages(response.totalPages); const totalCount = roadmapsResponse?.totalCount || 0;
},
[currPage, roadmaps], const perPage = roadmapsResponse?.perPage || 0;
); const hasNextPage = currPage < totalPages;
const hasPrevPage = currPage > 1;
useEffect(() => { useEffect(() => {
loadAIRoadmaps(currPage).finally(() => { loadAIRoadmaps(currPage).finally(() => {
@ -67,7 +62,51 @@ export function ExploreAIRoadmap() {
}); });
}, []); }, []);
const hasMorePages = currPage < totalPages; const roadmaps = roadmapsResponse?.data || [];
const paginationBar = (
<div className="mb-4 flex items-center justify-between">
<div className="flex items-center gap-2">
{hasPrevPage && (
<button
className="flex h-6 w-6 items-center justify-center rounded-md border disabled:cursor-not-allowed disabled:opacity-65"
onClick={() => {
loadAIRoadmaps(currPage - 1).finally(() => {
setIsLoading(false);
});
}}
disabled={isLoading}
>
&larr;
</button>
)}
{hasNextPage && (
<button
className="flex h-6 w-6 items-center justify-center rounded-md border disabled:cursor-not-allowed disabled:opacity-65"
onClick={() => {
loadAIRoadmaps(currPage + 1).finally(() => {
setIsLoading(false);
});
}}
disabled={isLoading}
>
&rarr;
</button>
)}
<p className="text-sm">
Showing {formatCommaNumber((currPage - 1) * perPage)} to{' '}
{formatCommaNumber((currPage - 1) * perPage + roadmaps.length)} of{' '}
{formatCommaNumber(totalCount)} entries
</p>
</div>
<div className="flex items-center text-sm">
<p>
Page {formatCommaNumber(currPage)} of {formatCommaNumber(totalPages)}
</p>
</div>
</div>
);
return ( return (
<section className="container mx-auto py-3 sm:py-6"> <section className="container mx-auto py-3 sm:py-6">
@ -75,6 +114,8 @@ export function ExploreAIRoadmap() {
<AIRoadmapAlert isListing /> <AIRoadmapAlert isListing />
</div> </div>
{paginationBar}
{isLoading ? ( {isLoading ? (
<ul className="grid grid-cols-1 gap-2 sm:grid-cols-2 lg:grid-cols-3"> <ul className="grid grid-cols-1 gap-2 sm:grid-cols-2 lg:grid-cols-3">
{new Array(21).fill(0).map((_, index) => ( {new Array(21).fill(0).map((_, index) => (
@ -90,7 +131,7 @@ export function ExploreAIRoadmap() {
<div className="text-center text-gray-800">No roadmaps found</div> <div className="text-center text-gray-800">No roadmaps found</div>
) : ( ) : (
<> <>
<ul className="grid grid-cols-1 gap-2 sm:grid-cols-2 lg:grid-cols-3"> <ul className="mb-4 grid grid-cols-1 gap-2 sm:grid-cols-2 lg:grid-cols-3">
{roadmaps.map((roadmap) => { {roadmaps.map((roadmap) => {
const roadmapLink = `/ai?id=${roadmap._id}`; const roadmapLink = `/ai?id=${roadmap._id}`;
return ( return (
@ -119,27 +160,7 @@ export function ExploreAIRoadmap() {
); );
})} })}
</ul> </ul>
{hasMorePages && ( {paginationBar}
<div className="my-5 flex items-center justify-center">
<button
onClick={() => {
setIsLoadingMore(true);
loadAIRoadmaps(currPage + 1).finally(() => {
setIsLoadingMore(false);
});
}}
className="inline-flex items-center gap-1.5 rounded-full bg-black px-3 py-1.5 text-sm font-medium text-white shadow-xl transition-colors focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
disabled={isLoadingMore}
>
{isLoadingMore ? (
<Loader2 className="h-4 w-4 animate-spin stroke-[2.5]" />
) : (
<RefreshCcw className="h-4 w-4 stroke-[2.5]" />
)}
Load More
</button>
</div>
)}
</> </>
)} )}
</div> </div>

@ -0,0 +1,7 @@
export const formatter = Intl.NumberFormat('en-US', {
useGrouping: true,
});
export function formatCommaNumber(number: number): string {
return formatter.format(number);
}
Loading…
Cancel
Save