diff --git a/src/components/GenerateCourse/AICourseCard.tsx b/src/components/GenerateCourse/AICourseCard.tsx index 4048dd73f..329969ade 100644 --- a/src/components/GenerateCourse/AICourseCard.tsx +++ b/src/components/GenerateCourse/AICourseCard.tsx @@ -1,9 +1,9 @@ -import type { AICourseListItem } from '../../queries/ai-course'; +import type { AICourseWithLessonCount } from '../../queries/ai-course'; import type { DifficultyLevel } from './AICourse'; import { BookOpen } from 'lucide-react'; type AICourseCardProps = { - course: AICourseListItem; + course: AICourseWithLessonCount; }; export function AICourseCard(props: AICourseCardProps) { diff --git a/src/components/GenerateCourse/AICourseSearch.tsx b/src/components/GenerateCourse/AICourseSearch.tsx new file mode 100644 index 000000000..bd0f68a76 --- /dev/null +++ b/src/components/GenerateCourse/AICourseSearch.tsx @@ -0,0 +1,46 @@ +import { SearchIcon } from 'lucide-react'; +import { useEffect, useState } from 'react'; +import { useDebounceValue } from '../../hooks/use-debounce'; + +type AICourseSearchProps = { + value: string; + onChange: (value: string) => void; +}; + +export function AICourseSearch(props: AICourseSearchProps) { + const { value: defaultValue, onChange } = props; + + const [searchTerm, setSearchTerm] = useState(defaultValue); + const debouncedSearchTerm = useDebounceValue(searchTerm, 500); + + useEffect(() => { + setSearchTerm(defaultValue); + }, [defaultValue]); + + useEffect(() => { + if (debouncedSearchTerm && debouncedSearchTerm.length < 3) { + return; + } + + if (debouncedSearchTerm === defaultValue) { + return; + } + + onChange(debouncedSearchTerm); + }, [debouncedSearchTerm]); + + return ( +
+
+ +
+ setSearchTerm(e.target.value)} + /> +
+ ); +} diff --git a/src/components/GenerateCourse/UserCoursesList.tsx b/src/components/GenerateCourse/UserCoursesList.tsx index 92ecab6f7..a7f1e1bfb 100644 --- a/src/components/GenerateCourse/UserCoursesList.tsx +++ b/src/components/GenerateCourse/UserCoursesList.tsx @@ -2,24 +2,33 @@ import { useQuery } from '@tanstack/react-query'; import { getAiCourseLimitOptions, listUserAiCoursesOptions, + type ListUserAiCoursesQuery, } from '../../queries/ai-course'; import { queryClient } from '../../stores/query-client'; import { AICourseCard } from './AICourseCard'; import { useEffect, useState } from 'react'; -import { Gift, Loader2, Search, User2 } from 'lucide-react'; +import { Gift, Loader2, User2 } from 'lucide-react'; import { isLoggedIn } from '../../lib/jwt'; import { showLoginPopup } from '../../lib/popup'; import { cn } from '../../lib/classname'; import { useIsPaidUser } from '../../queries/billing'; import { UpgradeAccountModal } from '../Billing/UpgradeAccountModal'; +import { getUrlParams, setUrlParams, deleteUrlParam } from '../../lib/browser'; +import { AICourseSearch } from './AICourseSearch'; +import { Pagination } from '../Pagination/Pagination'; type UserCoursesListProps = {}; export function UserCoursesList(props: UserCoursesListProps) { - const [searchTerm, setSearchTerm] = useState(''); const [isInitialLoading, setIsInitialLoading] = useState(true); const [showUpgradePopup, setShowUpgradePopup] = useState(false); + const [pageState, setPageState] = useState({ + perPage: '10', + currPage: '1', + query: '', + }); + const { data: limits, isLoading: isLimitsLoading } = useQuery( getAiCourseLimitOptions(), queryClient, @@ -29,7 +38,7 @@ export function UserCoursesList(props: UserCoursesListProps) { const { isPaidUser, isLoading: isPaidUserLoading } = useIsPaidUser(); const { data: userAiCourses, isFetching: isUserAiCoursesLoading } = useQuery( - listUserAiCoursesOptions(), + listUserAiCoursesOptions(pageState), queryClient, ); @@ -37,21 +46,31 @@ export function UserCoursesList(props: UserCoursesListProps) { setIsInitialLoading(false); }, [userAiCourses]); - const filteredCourses = userAiCourses?.filter((course) => { - if (!searchTerm.trim()) { - return true; - } + const courses = userAiCourses?.data ?? []; + const isAuthenticated = isLoggedIn(); + const limitUsedPercentage = Math.round((used / limit) * 100); - const searchLower = searchTerm.toLowerCase(); + useEffect(() => { + const queryParams = getUrlParams(); - return ( - course.title.toLowerCase().includes(searchLower) || - course.keyword.toLowerCase().includes(searchLower) - ); - }); + setPageState({ + ...pageState, + currPage: queryParams?.p || '1', + query: queryParams?.q || '', + }); + }, []); - const isAuthenticated = isLoggedIn(); - const limitUsedPercentage = Math.round((used / limit) * 100); + useEffect(() => { + if (pageState?.currPage !== '1' || pageState?.query !== '') { + setUrlParams({ + p: pageState?.currPage || '1', + q: pageState?.query || '', + }); + } else { + deleteUrlParam('p'); + deleteUrlParam('q'); + } + }, [pageState]); return ( <> @@ -69,9 +88,9 @@ export function UserCoursesList(props: UserCoursesListProps) { {used > 0 && limit > 0 && !isPaidUserLoading && (
@@ -95,18 +114,16 @@ export function UserCoursesList(props: UserCoursesListProps) {
)} -
-
- -
- setSearchTerm(e.target.value)} - /> -
+ { + setPageState({ + ...pageState, + query: value, + currPage: '1', + }); + }} + /> @@ -127,15 +144,13 @@ export function UserCoursesList(props: UserCoursesListProps) { )} - {!isUserAiCoursesLoading && - !isInitialLoading && - userAiCourses?.length === 0 && ( -
-

- You haven't generated any courses yet. -

-
- )} + {!isUserAiCoursesLoading && !isInitialLoading && courses.length === 0 && ( +
+

+ You haven't generated any courses yet. +

+
+ )} {(isUserAiCoursesLoading || isInitialLoading) && (
@@ -147,19 +162,28 @@ export function UserCoursesList(props: UserCoursesListProps) {
)} - {!isUserAiCoursesLoading && - filteredCourses && - filteredCourses.length > 0 && ( -
- {filteredCourses.map((course) => ( - - ))} -
- )} + {!isUserAiCoursesLoading && courses && courses.length > 0 && ( +
+ {courses.map((course) => ( + + ))} + + { + setPageState({ ...pageState, currPage: String(page) }); + }} + className="rounded-lg border border-gray-200 bg-white p-4" + /> +
+ )} {!isUserAiCoursesLoading && - (userAiCourses?.length || 0 > 0) && - filteredCourses?.length === 0 && ( + (userAiCourses?.data?.length || 0 > 0) && + courses.length === 0 && (

No courses match your search. diff --git a/src/components/Pagination/Pagination.tsx b/src/components/Pagination/Pagination.tsx index 86f3ac4de..3f4c8d604 100644 --- a/src/components/Pagination/Pagination.tsx +++ b/src/components/Pagination/Pagination.tsx @@ -11,6 +11,7 @@ type PaginationProps = { totalCount: number; isDisabled?: boolean; onPageChange: (page: number) => void; + className?: string; }; export function Pagination(props: PaginationProps) { @@ -22,6 +23,7 @@ export function Pagination(props: PaginationProps) { currPage, perPage, isDisabled = false, + className, } = props; if (!totalPages || totalPages === 1) { @@ -32,10 +34,14 @@ export function Pagination(props: PaginationProps) { return (