parent
575e05d493
commit
56b327177b
2 changed files with 193 additions and 0 deletions
@ -0,0 +1,183 @@ |
||||
import { useEffect, useState, useCallback } from 'react'; |
||||
import { useToast } from '../../hooks/use-toast'; |
||||
import { httpGet } from '../../lib/http'; |
||||
import { getRelativeTimeString } from '../../lib/date'; |
||||
import { |
||||
BadgeCheck, |
||||
CalendarCheck, |
||||
Eye, |
||||
Loader2, |
||||
RefreshCcw, |
||||
Sparkles, |
||||
} from 'lucide-react'; |
||||
|
||||
export interface AIRoadmapDocument { |
||||
_id?: string; |
||||
topic: string; |
||||
data: string; |
||||
viewCount: number; |
||||
createdAt: Date; |
||||
updatedAt: Date; |
||||
} |
||||
|
||||
type ExploreRoadmapsResponse = { |
||||
data: AIRoadmapDocument[]; |
||||
totalCount: number; |
||||
totalPages: number; |
||||
currPage: number; |
||||
perPage: number; |
||||
}; |
||||
|
||||
export function ExploreAIRoadmap() { |
||||
const toast = useToast(); |
||||
|
||||
const [isLoading, setIsLoading] = useState(true); |
||||
const [isLoadingMore, setIsLoadingMore] = useState(false); |
||||
const [roadmaps, setRoadmaps] = useState<AIRoadmapDocument[]>([]); |
||||
const [currPage, setCurrPage] = useState(1); |
||||
const [totalPages, setTotalPages] = useState(1); |
||||
|
||||
const loadAIRoadamps = useCallback( |
||||
async (currPage: number) => { |
||||
const { response, error } = await httpGet<ExploreRoadmapsResponse>( |
||||
`${import.meta.env.PUBLIC_API_URL}/v1-explore-roadmaps`, |
||||
{ |
||||
currPage, |
||||
}, |
||||
); |
||||
|
||||
if (error || !response) { |
||||
toast.error(error?.message || 'Something went wrong'); |
||||
return; |
||||
} |
||||
|
||||
const newRoadmaps = [...roadmaps, ...response.data]; |
||||
if ( |
||||
JSON.stringify(roadmaps) === JSON.stringify(response.data) || |
||||
JSON.stringify(roadmaps) === JSON.stringify(newRoadmaps) |
||||
) { |
||||
return; |
||||
} |
||||
|
||||
setRoadmaps(newRoadmaps); |
||||
setCurrPage(response.currPage); |
||||
setTotalPages(response.totalPages); |
||||
}, |
||||
[currPage, roadmaps], |
||||
); |
||||
|
||||
useEffect(() => { |
||||
loadAIRoadamps(currPage).finally(() => { |
||||
setIsLoading(false); |
||||
}); |
||||
}, []); |
||||
|
||||
const hasMorePages = currPage < totalPages; |
||||
|
||||
return ( |
||||
<section className="container mx-auto"> |
||||
<div className="border-b pb-8 pt-10"> |
||||
<h2 className="text-base font-semibold text-gray-800 sm:text-lg"> |
||||
Explore AI Generated Roadmaps |
||||
</h2> |
||||
<p className="mb-2.5 mt-2 text-balance text-sm text-gray-800 sm:mb-1.5 sm:mt-1 sm:text-base"> |
||||
These roadmaps are generated by AI based on the data and the topic |
||||
community has provided. You can also create your own roadmap and share |
||||
it with the community. |
||||
</p> |
||||
<div className="flex flex-col items-start gap-2 sm:flex-row sm:items-center"> |
||||
<a |
||||
href="/roadmaps" |
||||
className="inline-flex items-center gap-1.5 text-sm font-semibold text-gray-700 underline-offset-2 hover:underline" |
||||
> |
||||
<BadgeCheck className="h-4 w-4 stroke-[2.5]" /> |
||||
Visit Official Roadmaps |
||||
</a> |
||||
<span className="hidden font-black text-gray-700 sm:block"> |
||||
· |
||||
</span> |
||||
<a |
||||
href="/ai" |
||||
className="inline-flex items-center gap-1.5 text-sm font-semibold text-blue-600 underline-offset-2 hover:underline" |
||||
> |
||||
<Sparkles className="h-4 w-4 stroke-[2.5]" /> |
||||
Generate Your Own Roadmap |
||||
</a> |
||||
</div> |
||||
</div> |
||||
{isLoading ? ( |
||||
<ul className="mt-8 grid grid-cols-3 gap-2"> |
||||
{new Array(6).fill(0).map((_, index) => ( |
||||
<li |
||||
key={index} |
||||
className="animate-pulse rounded-md border bg-gray-100" |
||||
> |
||||
<div className="h-10 w-full"></div> |
||||
<div className="flex items-center justify-between gap-2 border-t px-2.5 py-2"> |
||||
<span className="flex items-center gap-1.5 text-sm text-gray-600"> |
||||
<Eye size={15} className="inline-block" /> |
||||
<span>{index} views</span> |
||||
</span> |
||||
<span className="flex items-center gap-1.5 text-sm text-gray-600"> |
||||
<CalendarCheck size={15} className="inline-block" /> |
||||
<span>{index}h ago</span> |
||||
</span> |
||||
</div> |
||||
</li> |
||||
))} |
||||
</ul> |
||||
) : ( |
||||
<div className="mt-8"> |
||||
{roadmaps?.length === 0 ? ( |
||||
<div className="text-center text-gray-800">No roadmaps found</div> |
||||
) : ( |
||||
<ul className="grid grid-cols-3 gap-2"> |
||||
{roadmaps.map((roadmap) => ( |
||||
<li key={roadmap._id} className="rounded-md border"> |
||||
<h2 |
||||
className="truncate px-2.5 py-2.5 text-xl font-medium leading-none tracking-wide" |
||||
title={roadmap.topic} |
||||
> |
||||
{roadmap.topic} |
||||
</h2> |
||||
<div className="flex items-center justify-between gap-2 border-t px-2.5 py-2"> |
||||
<span className="flex items-center gap-1.5 text-sm text-gray-600"> |
||||
<Eye size={15} className="inline-block" /> |
||||
<span>{roadmap?.viewCount || 0} views</span> |
||||
</span> |
||||
<span className="flex items-center gap-1.5 text-sm text-gray-600"> |
||||
<CalendarCheck size={15} className="inline-block" /> |
||||
<span> |
||||
{getRelativeTimeString(String(roadmap?.createdAt))} |
||||
</span> |
||||
</span> |
||||
</div> |
||||
</li> |
||||
))} |
||||
{hasMorePages && ( |
||||
<li> |
||||
<button |
||||
onClick={async () => { |
||||
setIsLoadingMore(true); |
||||
await loadAIRoadamps(currPage + 1); |
||||
setIsLoadingMore(false); |
||||
}} |
||||
className="flex h-full min-h-[79px] w-full items-center justify-center gap-1.5 rounded-md border bg-gray-100 font-medium text-gray-900 hover:bg-gray-100/80 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> |
||||
</li> |
||||
)} |
||||
</ul> |
||||
)} |
||||
</div> |
||||
)} |
||||
</section> |
||||
); |
||||
} |
@ -0,0 +1,10 @@ |
||||
--- |
||||
import LoginPopup from '../../components/AuthenticationFlow/LoginPopup.astro'; |
||||
import { ExploreAIRoadmap } from '../../components/ExploreAIRoadmap/ExploreAIRoadmap'; |
||||
import AccountLayout from '../../layouts/AccountLayout.astro'; |
||||
--- |
||||
|
||||
<AccountLayout title='Explore Roadmap AI'> |
||||
<ExploreAIRoadmap client:load /> |
||||
<LoginPopup /> |
||||
</AccountLayout> |
Loading…
Reference in new issue