diff --git a/src/components/AITutor/AIExploreCourseListing.tsx b/src/components/AITutor/AIExploreCourseListing.tsx index c8b25f560..97ddd2f5b 100644 --- a/src/components/AITutor/AIExploreCourseListing.tsx +++ b/src/components/AITutor/AIExploreCourseListing.tsx @@ -1,31 +1,53 @@ -import { useListExploreAiCourses } from '../../queries/ai-course'; +import { useQuery } from '@tanstack/react-query'; import { useEffect, useState } from 'react'; -import { AlertCircle, Loader2 } from 'lucide-react'; +import { AlertCircle } from 'lucide-react'; import { AICourseCard } from '../GenerateCourse/AICourseCard'; import { AILoadingState } from './AILoadingState'; import { AITutorHeader } from './AITutorHeader'; import { UpgradeAccountModal } from '../Billing/UpgradeAccountModal'; +import { + listExploreAiCoursesOptions, + type ListExploreAiCoursesQuery, +} from '../../queries/ai-course'; +import { queryClient } from '../../stores/query-client'; +import { deleteUrlParam, getUrlParams, setUrlParams } from '../../lib/browser'; +import { Pagination } from '../Pagination/Pagination'; export function AIExploreCourseListing() { const [isInitialLoading, setIsInitialLoading] = useState(true); const [showUpgradePopup, setShowUpgradePopup] = useState(false); - const { - data, - error, - fetchNextPage, - hasNextPage, - isFetching, - isFetchingNextPage, - status, - isLoading: isExploreAiCoursesLoading, - } = useListExploreAiCourses(); + const [pageState, setPageState] = useState<ListExploreAiCoursesQuery>({ + perPage: '21', + currPage: '1', + }); + + const { data: exploreAiCourses, isFetching: isExploreAiCoursesLoading } = + useQuery(listExploreAiCoursesOptions(pageState), queryClient); useEffect(() => { setIsInitialLoading(false); - }, [data]); + }, [exploreAiCourses]); + + const courses = exploreAiCourses?.data ?? []; - const courses = data?.pages.flatMap((page) => page.data) ?? []; + useEffect(() => { + const queryParams = getUrlParams(); + setPageState({ + ...pageState, + currPage: queryParams?.p || '1', + }); + }, []); + + useEffect(() => { + if (pageState?.currPage !== '1') { + setUrlParams({ + p: pageState?.currPage || '1', + }); + } else { + deleteUrlParam('p'); + } + }, [pageState]); if (isInitialLoading || isExploreAiCoursesLoading) { return ( @@ -36,12 +58,12 @@ export function AIExploreCourseListing() { ); } - if (error) { + if (!exploreAiCourses?.data) { return ( <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.'} + Error loading courses. </p> </div> ); @@ -59,39 +81,34 @@ export function AIExploreCourseListing() { /> {courses && courses.length > 0 && ( - <div className="grid grid-cols-3 gap-2"> - {courses.map((course) => ( - <AICourseCard - key={course._id} - course={course} - showActions={false} - showProgress={false} - /> - ))} - </div> - )} + <div className="flex flex-col gap-2"> + <div className="grid grid-cols-3 gap-2"> + {courses.map((course) => ( + <AICourseCard + key={course._id} + course={course} + showActions={false} + showProgress={false} + /> + ))} + </div> - {hasNextPage && !isFetchingNextPage && ( - <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> + <Pagination + totalCount={exploreAiCourses?.totalCount || 0} + totalPages={exploreAiCourses?.totalPages || 0} + currPage={Number(exploreAiCourses?.currPage || 1)} + perPage={Number(exploreAiCourses?.perPage || 21)} + onPageChange={(page) => { + setPageState({ ...pageState, currPage: String(page) }); + }} + className="rounded-lg border border-gray-200 bg-white p-4" + /> </div> )} - {isFetchingNextPage && ( - <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> + {!isExploreAiCoursesLoading && 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 found.</p> </div> )} </> diff --git a/src/queries/ai-course.ts b/src/queries/ai-course.ts index 8b269124f..846bdb63e 100644 --- a/src/queries/ai-course.ts +++ b/src/queries/ai-course.ts @@ -1,7 +1,6 @@ import { httpGet } from '../lib/query-http'; import { isLoggedIn } from '../lib/jwt'; -import { queryOptions, useInfiniteQuery } from '@tanstack/react-query'; -import { queryClient } from '../stores/query-client'; +import { queryOptions } from '@tanstack/react-query'; export interface AICourseProgressDocument { _id: string; @@ -135,45 +134,32 @@ export function listFeaturedAiCoursesOptions( type ListExploreAiCoursesParams = {}; -type ListExploreAiCoursesQuery = { +export type ListExploreAiCoursesQuery = { perPage?: string; currPage?: string; }; type ListExploreAiCoursesResponse = { data: AICourseWithLessonCount[]; + totalCount: number; + totalPages: number; currPage: number; perPage: number; }; -export function useListExploreAiCourses() { - return useInfiniteQuery( - { - queryKey: ['explore-ai-courses'], - queryFn: ({ pageParam = 1 }) => { - return httpGet<ListExploreAiCoursesResponse>( - `/v1-list-explore-ai-courses`, - { - perPage: '21', - currPage: String(pageParam), - }, - ); - }, - getNextPageParam: (lastPage, pages, lastPageParam) => { - if (lastPage?.data?.length === 0) { - return undefined; - } - - return lastPageParam + 1; - }, - getPreviousPageParam: (firstPage, allPages, firstPageParam) => { - if (firstPageParam <= 1) { - return undefined; - } - return firstPageParam - 1; - }, - initialPageParam: 1, +export function listExploreAiCoursesOptions( + params: ListExploreAiCoursesQuery = { + perPage: '21', + currPage: '1', + }, +) { + return { + queryKey: ['explore-ai-courses', params], + queryFn: () => { + return httpGet<ListExploreAiCoursesResponse>( + `/v1-list-explore-ai-courses`, + params, + ); }, - queryClient, - ); + }; }