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