parent
3a20912f0f
commit
da14f05079
11 changed files with 351 additions and 22 deletions
@ -0,0 +1,96 @@ |
|||||||
|
import { useListExploreAiCourses } from '../../queries/ai-course'; |
||||||
|
import { useEffect, useState } from 'react'; |
||||||
|
import { AlertCircle, Loader2 } from 'lucide-react'; |
||||||
|
import { AICourseCard } from '../GenerateCourse/AICourseCard'; |
||||||
|
|
||||||
|
type AIExploreCourseListingProps = {}; |
||||||
|
|
||||||
|
export function AIExploreCourseListing(props: AIExploreCourseListingProps) { |
||||||
|
const [isInitialLoading, setIsInitialLoading] = useState(true); |
||||||
|
|
||||||
|
const { |
||||||
|
data, |
||||||
|
error, |
||||||
|
fetchNextPage, |
||||||
|
hasNextPage, |
||||||
|
isFetching, |
||||||
|
isFetchingNextPage, |
||||||
|
status, |
||||||
|
isLoading: isExploreAiCoursesLoading, |
||||||
|
} = useListExploreAiCourses(); |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
setIsInitialLoading(false); |
||||||
|
}, [data]); |
||||||
|
|
||||||
|
const courses = data?.pages.flatMap((page) => page.data) ?? []; |
||||||
|
|
||||||
|
return ( |
||||||
|
<> |
||||||
|
<div className="mb-3 flex min-h-[35px] items-center justify-between max-sm:mb-1"> |
||||||
|
<div className="flex items-center gap-2"> |
||||||
|
<h2 className="text-lg font-semibold">Explore Courses</h2> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
{(isExploreAiCoursesLoading || isInitialLoading) && ( |
||||||
|
<div className="flex min-h-[152px] items-center justify-center gap-2 rounded-lg border border-gray-200 bg-white py-4"> |
||||||
|
<Loader2 |
||||||
|
className="size-4 animate-spin text-gray-400" |
||||||
|
strokeWidth={2.5} |
||||||
|
/> |
||||||
|
<p className="text-sm font-medium text-gray-600">Loading...</p> |
||||||
|
</div> |
||||||
|
)} |
||||||
|
|
||||||
|
{error && !isExploreAiCoursesLoading && !isInitialLoading && ( |
||||||
|
<div className="flex min-h-[152px] items-center justify-center gap-2 rounded-lg border border-gray-200 bg-white py-4"> |
||||||
|
<AlertCircle className="size-4 text-red-500" /> |
||||||
|
<p className="text-sm font-medium text-red-600"> |
||||||
|
{error?.message ?? 'Error loading courses.'} |
||||||
|
</p> |
||||||
|
</div> |
||||||
|
)} |
||||||
|
|
||||||
|
{!isExploreAiCoursesLoading && |
||||||
|
courses && |
||||||
|
courses.length > 0 && |
||||||
|
!error && ( |
||||||
|
<div className="flex flex-col gap-2"> |
||||||
|
{courses.map((course) => ( |
||||||
|
<AICourseCard |
||||||
|
key={course._id} |
||||||
|
course={course} |
||||||
|
showActions={false} |
||||||
|
showProgress={false} |
||||||
|
/> |
||||||
|
))} |
||||||
|
</div> |
||||||
|
)} |
||||||
|
|
||||||
|
{hasNextPage && !isFetchingNextPage && !error && ( |
||||||
|
<div className="mt-4 flex items-center justify-center"> |
||||||
|
<button |
||||||
|
onClick={() => fetchNextPage()} |
||||||
|
disabled={isFetchingNextPage} |
||||||
|
className="rounded-md border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 transition-colors hover:bg-gray-100 disabled:opacity-50" |
||||||
|
> |
||||||
|
Load more |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
)} |
||||||
|
|
||||||
|
{isFetchingNextPage && !error && ( |
||||||
|
<div className="mt-4 flex items-center justify-center gap-2"> |
||||||
|
<Loader2 |
||||||
|
className="size-4 animate-spin text-gray-400" |
||||||
|
strokeWidth={2.5} |
||||||
|
/> |
||||||
|
<p className="text-sm font-medium text-gray-600"> |
||||||
|
Loading more courses... |
||||||
|
</p> |
||||||
|
</div> |
||||||
|
)} |
||||||
|
</> |
||||||
|
); |
||||||
|
} |
@ -0,0 +1,98 @@ |
|||||||
|
import { useQuery } from '@tanstack/react-query'; |
||||||
|
import { |
||||||
|
listUserAiCoursesOptions, |
||||||
|
type ListUserAiCoursesQuery, |
||||||
|
} from '../../queries/ai-course'; |
||||||
|
import { queryClient } from '../../stores/query-client'; |
||||||
|
import { useEffect, useState } from 'react'; |
||||||
|
import { Loader2 } from 'lucide-react'; |
||||||
|
import { getUrlParams, setUrlParams, deleteUrlParam } from '../../lib/browser'; |
||||||
|
import { AICourseCard } from '../GenerateCourse/AICourseCard'; |
||||||
|
|
||||||
|
type AIFeaturedCoursesListingProps = {}; |
||||||
|
|
||||||
|
export function AIFeaturedCoursesListing(props: AIFeaturedCoursesListingProps) { |
||||||
|
const [isInitialLoading, setIsInitialLoading] = useState(true); |
||||||
|
|
||||||
|
const [pageState, setPageState] = useState<ListUserAiCoursesQuery>({ |
||||||
|
perPage: '10', |
||||||
|
currPage: '1', |
||||||
|
query: '', |
||||||
|
}); |
||||||
|
|
||||||
|
const { data: userAiCourses, isFetching: isUserAiCoursesLoading } = useQuery( |
||||||
|
listUserAiCoursesOptions(pageState), |
||||||
|
queryClient, |
||||||
|
); |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
setIsInitialLoading(false); |
||||||
|
}, [userAiCourses]); |
||||||
|
|
||||||
|
const courses = userAiCourses?.data ?? []; |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
const queryParams = getUrlParams(); |
||||||
|
|
||||||
|
setPageState({ |
||||||
|
...pageState, |
||||||
|
currPage: queryParams?.p || '1', |
||||||
|
query: queryParams?.q || '', |
||||||
|
}); |
||||||
|
}, []); |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
if (pageState?.currPage !== '1' || pageState?.query !== '') { |
||||||
|
setUrlParams({ |
||||||
|
p: pageState?.currPage || '1', |
||||||
|
q: pageState?.query || '', |
||||||
|
}); |
||||||
|
} else { |
||||||
|
deleteUrlParam('p'); |
||||||
|
deleteUrlParam('q'); |
||||||
|
} |
||||||
|
}, [pageState]); |
||||||
|
|
||||||
|
return ( |
||||||
|
<> |
||||||
|
<div className="mb-3 flex min-h-[35px] items-center justify-between max-sm:mb-1"> |
||||||
|
<div className="flex items-center gap-2"> |
||||||
|
<h2 className="text-lg font-semibold">Stuff Picks</h2> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
{(isUserAiCoursesLoading || isInitialLoading) && ( |
||||||
|
<div className="flex min-h-[152px] items-center justify-center gap-2 rounded-lg border border-gray-200 bg-white py-4"> |
||||||
|
<Loader2 |
||||||
|
className="size-4 animate-spin text-gray-400" |
||||||
|
strokeWidth={2.5} |
||||||
|
/> |
||||||
|
<p className="text-sm font-medium text-gray-600">Loading...</p> |
||||||
|
</div> |
||||||
|
)} |
||||||
|
|
||||||
|
{!isUserAiCoursesLoading && courses && courses.length > 0 && ( |
||||||
|
<div className="flex flex-col gap-2"> |
||||||
|
{courses.map((course) => ( |
||||||
|
<AICourseCard |
||||||
|
key={course._id} |
||||||
|
course={course} |
||||||
|
showActions={false} |
||||||
|
showProgress={false} |
||||||
|
/> |
||||||
|
))} |
||||||
|
</div> |
||||||
|
)} |
||||||
|
|
||||||
|
{!isUserAiCoursesLoading && |
||||||
|
(userAiCourses?.data?.length || 0 > 0) && |
||||||
|
courses.length === 0 && ( |
||||||
|
<div className="flex min-h-[114px] items-center justify-center rounded-lg border border-gray-200 bg-white py-4"> |
||||||
|
<p className="text-sm text-gray-600"> |
||||||
|
No courses match your search. |
||||||
|
</p> |
||||||
|
</div> |
||||||
|
)} |
||||||
|
</> |
||||||
|
); |
||||||
|
} |
@ -0,0 +1,17 @@ |
|||||||
|
import { AITutorSidebar, type AITutorTab } from './AITutorSidebar'; |
||||||
|
|
||||||
|
type AITutorLayoutProps = { |
||||||
|
children: React.ReactNode; |
||||||
|
activeTab: AITutorTab; |
||||||
|
}; |
||||||
|
|
||||||
|
export function AITutorLayout(props: AITutorLayoutProps) { |
||||||
|
const { children, activeTab } = props; |
||||||
|
|
||||||
|
return ( |
||||||
|
<div className="flex flex-grow flex-row"> |
||||||
|
<AITutorSidebar activeTab={activeTab} /> |
||||||
|
<div className="flex flex-grow flex-col">{children}</div> |
||||||
|
</div> |
||||||
|
); |
||||||
|
} |
@ -0,0 +1,21 @@ |
|||||||
|
--- |
||||||
|
import { UserCoursesList } from '../../components/GenerateCourse/UserCoursesList'; |
||||||
|
import SkeletonLayout from '../../layouts/SkeletonLayout.astro'; |
||||||
|
import { AITutorLayout } from '../../components/AITutor/AITutorLayout'; |
||||||
|
const ogImage = 'https://roadmap.sh/og-images/ai-tutor.png'; |
||||||
|
--- |
||||||
|
|
||||||
|
<SkeletonLayout |
||||||
|
title='Roadmap AI' |
||||||
|
noIndex={true} |
||||||
|
ogImageUrl={ogImage} |
||||||
|
description='Learn anything with AI Tutor. Pick a topic, choose a difficulty level and the AI will guide you through the learning process.' |
||||||
|
> |
||||||
|
<AITutorLayout activeTab='courses' client:load> |
||||||
|
<section class='flex grow flex-col bg-gray-100'> |
||||||
|
<div class='container mx-auto flex max-w-3xl flex-col py-10 max-sm:py-4'> |
||||||
|
<UserCoursesList client:load /> |
||||||
|
</div> |
||||||
|
</section> |
||||||
|
</AITutorLayout> |
||||||
|
</SkeletonLayout> |
@ -0,0 +1,21 @@ |
|||||||
|
--- |
||||||
|
import { AIExploreCourseListing } from '../../components/AITutor/AIExploreCourseListing'; |
||||||
|
import SkeletonLayout from '../../layouts/SkeletonLayout.astro'; |
||||||
|
import { AITutorLayout } from '../../components/AITutor/AITutorLayout'; |
||||||
|
const ogImage = 'https://roadmap.sh/og-images/ai-tutor.png'; |
||||||
|
--- |
||||||
|
|
||||||
|
<SkeletonLayout |
||||||
|
title='Roadmap AI' |
||||||
|
noIndex={true} |
||||||
|
ogImageUrl={ogImage} |
||||||
|
description='Learn anything with AI Tutor. Pick a topic, choose a difficulty level and the AI will guide you through the learning process.' |
||||||
|
> |
||||||
|
<AITutorLayout activeTab='explore' client:load> |
||||||
|
<section class='flex grow flex-col bg-gray-100'> |
||||||
|
<div class='container mx-auto flex max-w-3xl flex-col py-10 max-sm:py-4'> |
||||||
|
<AIExploreCourseListing client:load /> |
||||||
|
</div> |
||||||
|
</section> |
||||||
|
</AITutorLayout> |
||||||
|
</SkeletonLayout> |
@ -0,0 +1,21 @@ |
|||||||
|
--- |
||||||
|
import { AIFeaturedCoursesListing } from '../../components/AITutor/AIFeaturedCoursesListing'; |
||||||
|
import SkeletonLayout from '../../layouts/SkeletonLayout.astro'; |
||||||
|
import { AITutorLayout } from '../../components/AITutor/AITutorLayout'; |
||||||
|
const ogImage = 'https://roadmap.sh/og-images/ai-tutor.png'; |
||||||
|
--- |
||||||
|
|
||||||
|
<SkeletonLayout |
||||||
|
title='Roadmap AI' |
||||||
|
noIndex={true} |
||||||
|
ogImageUrl={ogImage} |
||||||
|
description='Learn anything with AI Tutor. Pick a topic, choose a difficulty level and the AI will guide you through the learning process.' |
||||||
|
> |
||||||
|
<AITutorLayout activeTab='stuff-picks' client:load> |
||||||
|
<section class='flex grow flex-col bg-gray-100'> |
||||||
|
<div class='container mx-auto flex max-w-3xl flex-col py-10 max-sm:py-4'> |
||||||
|
<AIFeaturedCoursesListing client:load /> |
||||||
|
</div> |
||||||
|
</section> |
||||||
|
</AITutorLayout> |
||||||
|
</SkeletonLayout> |
Loading…
Reference in new issue