|
|
@ -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,39 +28,33 @@ 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 = async (currPage: number) => { |
|
|
|
|
|
|
|
setIsLoading(true); |
|
|
|
const loadAIRoadmaps = useCallback( |
|
|
|
const { response, error } = await httpGet<ExploreRoadmapsResponse>( |
|
|
|
async (currPage: number) => { |
|
|
|
`${import.meta.env.PUBLIC_API_URL}/v1-list-ai-roadmaps`, |
|
|
|
const { response, error } = await httpGet<ExploreRoadmapsResponse>( |
|
|
|
{ |
|
|
|
`${import.meta.env.PUBLIC_API_URL}/v1-list-ai-roadmaps`, |
|
|
|
currPage, |
|
|
|
{ |
|
|
|
}, |
|
|
|
currPage, |
|
|
|
); |
|
|
|
}, |
|
|
|
|
|
|
|
); |
|
|
|
if (error || !response) { |
|
|
|
|
|
|
|
toast.error(error?.message || 'Something went wrong'); |
|
|
|
if (error || !response) { |
|
|
|
return; |
|
|
|
toast.error(error?.message || 'Something went wrong'); |
|
|
|
} |
|
|
|
return; |
|
|
|
|
|
|
|
} |
|
|
|
setRoadmapsResponse(response); |
|
|
|
|
|
|
|
}; |
|
|
|
const newRoadmaps = [...roadmaps, ...response.data]; |
|
|
|
|
|
|
|
if ( |
|
|
|
const currPage = roadmapsResponse?.currPage || 1; |
|
|
|
JSON.stringify(roadmaps) === JSON.stringify(response.data) || |
|
|
|
const totalPages = roadmapsResponse?.totalPages || 1; |
|
|
|
JSON.stringify(roadmaps) === JSON.stringify(newRoadmaps) |
|
|
|
const totalCount = roadmapsResponse?.totalCount || 0; |
|
|
|
) { |
|
|
|
|
|
|
|
return; |
|
|
|
const perPage = roadmapsResponse?.perPage || 0; |
|
|
|
} |
|
|
|
const hasNextPage = currPage < totalPages; |
|
|
|
|
|
|
|
const hasPrevPage = currPage > 1; |
|
|
|
setRoadmaps(newRoadmaps); |
|
|
|
|
|
|
|
setCurrPage(response.currPage); |
|
|
|
|
|
|
|
setTotalPages(response.totalPages); |
|
|
|
|
|
|
|
}, |
|
|
|
|
|
|
|
[currPage, roadmaps], |
|
|
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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} |
|
|
|
|
|
|
|
> |
|
|
|
|
|
|
|
← |
|
|
|
|
|
|
|
</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} |
|
|
|
|
|
|
|
> |
|
|
|
|
|
|
|
→ |
|
|
|
|
|
|
|
</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> |
|
|
|