-
+
+
+
+ {
+ setPageState({
+ ...pageState,
+ searchTerm: term,
+ currentPage: 1,
+ });
+ }}
+ />
+
+ {
+ setPageState({
+ ...pageState,
+ sortBy,
+ currentPage: 1,
+ });
+ }}
+ />
- {isLoading ? (
-
- {new Array(21).fill(0).map((_, index) => (
-
- ))}
-
- ) : (
-
- {roadmaps?.length === 0 ? (
-
No roadmaps found
- ) : (
- <>
-
- {hasMorePages && (
-
-
-
- )}
- >
- )}
-
- )}
+ {loadingIndicator}
+ {emptyRoadmaps}
+ {roadmapsList}
);
}
diff --git a/src/components/ExploreAIRoadmap/ExploreAISearch.tsx b/src/components/ExploreAIRoadmap/ExploreAISearch.tsx
new file mode 100644
index 000000000..3c01718a9
--- /dev/null
+++ b/src/components/ExploreAIRoadmap/ExploreAISearch.tsx
@@ -0,0 +1,58 @@
+import { Search } from 'lucide-react';
+import { useEffect, useState } from 'react';
+import { useDebounceValue } from '../../hooks/use-debounce.ts';
+import { Spinner } from '../ReactIcons/Spinner.tsx';
+
+type ExploreAISearchProps = {
+ value: string;
+ isLoading: boolean;
+ onSubmit: (search: string) => void;
+};
+
+export function ExploreAISearch(props: ExploreAISearchProps) {
+ const { onSubmit, isLoading = false, value: defaultValue } = props;
+
+ const [term, setTerm] = useState(defaultValue);
+ const debouncedTerm = useDebounceValue(term, 500);
+
+ useEffect(() => {
+ setTerm(defaultValue);
+ }, [defaultValue]);
+
+ useEffect(() => {
+ if (debouncedTerm && debouncedTerm.length < 3) {
+ return;
+ }
+
+ if (debouncedTerm === defaultValue) {
+ return;
+ }
+
+ onSubmit(debouncedTerm);
+ }, [debouncedTerm]);
+
+ return (
+
+
+ setTerm(e.target.value)}
+ />
+ {isLoading && (
+
+
+
+ )}
+
+ );
+}
diff --git a/src/components/ExploreAIRoadmap/ExploreAISorting.tsx b/src/components/ExploreAIRoadmap/ExploreAISorting.tsx
new file mode 100644
index 000000000..47e11f0c6
--- /dev/null
+++ b/src/components/ExploreAIRoadmap/ExploreAISorting.tsx
@@ -0,0 +1,73 @@
+import { ArrowDownWideNarrow, Check, ChevronDown } from 'lucide-react';
+import { useRef, useState } from 'react';
+import { useOutsideClick } from '../../hooks/use-outside-click';
+
+export type SortByValues = 'viewCount' | 'createdAt' | '-createdAt';
+const sortingLabels: { label: string; value: SortByValues }[] = [
+ {
+ label: 'Most Viewed',
+ value: 'viewCount',
+ },
+ {
+ label: 'Newest',
+ value: 'createdAt',
+ },
+ {
+ label: 'Oldest',
+ value: '-createdAt',
+ },
+];
+
+type ExploreAISortingProps = {
+ sortBy: SortByValues;
+ onSortChange: (sortBy: SortByValues) => void;
+};
+
+export function ExploreAISorting(props: ExploreAISortingProps) {
+ const { sortBy, onSortChange } = props;
+
+ const [isOpen, setIsOpen] = useState(false);
+ const dropdownRef = useRef(null);
+
+ const selectedValue = sortingLabels.find((item) => item.value === sortBy);
+
+ useOutsideClick(dropdownRef, () => {
+ setIsOpen(false);
+ });
+
+ return (
+
+
+
+ {isOpen && (
+
+ {sortingLabels.map((item) => (
+
+ ))}
+
+ )}
+
+ );
+}
diff --git a/src/components/ExploreAIRoadmap/LoadingRoadmaps.tsx b/src/components/ExploreAIRoadmap/LoadingRoadmaps.tsx
new file mode 100644
index 000000000..b39d5b79c
--- /dev/null
+++ b/src/components/ExploreAIRoadmap/LoadingRoadmaps.tsx
@@ -0,0 +1,12 @@
+export function LoadingRoadmaps() {
+ return (
+
+ {new Array(21).fill(0).map((_, index) => (
+
+ ))}
+
+ );
+}
diff --git a/src/components/GenerateRoadmap/GenerateRoadmap.tsx b/src/components/GenerateRoadmap/GenerateRoadmap.tsx
index 875d36905..7d92323d6 100644
--- a/src/components/GenerateRoadmap/GenerateRoadmap.tsx
+++ b/src/components/GenerateRoadmap/GenerateRoadmap.tsx
@@ -314,7 +314,7 @@ export function GenerateRoadmap() {
data,
});
- setRoadmapTerm(title);
+ setRoadmapTerm(term);
setGeneratedRoadmapContent(data);
visitAIRoadmap(roadmapId);
};
diff --git a/src/components/Pagination/Pagination.tsx b/src/components/Pagination/Pagination.tsx
new file mode 100644
index 000000000..86f3ac4de
--- /dev/null
+++ b/src/components/Pagination/Pagination.tsx
@@ -0,0 +1,101 @@
+import { usePagination } from '../../hooks/use-pagination.ts';
+import { MoreHorizontal } from 'lucide-react';
+import { cn } from '../../lib/classname.ts';
+import { formatCommaNumber } from '../../lib/number.ts';
+
+type PaginationProps = {
+ variant?: 'minimal' | 'default';
+ totalPages: number;
+ currPage: number;
+ perPage: number;
+ totalCount: number;
+ isDisabled?: boolean;
+ onPageChange: (page: number) => void;
+};
+
+export function Pagination(props: PaginationProps) {
+ const {
+ variant = 'default',
+ onPageChange,
+ totalCount,
+ totalPages,
+ currPage,
+ perPage,
+ isDisabled = false,
+ } = props;
+
+ if (!totalPages || totalPages === 1) {
+ return null;
+ }
+
+ const pages = usePagination(currPage, totalPages, 5);
+
+ return (
+
+
+
+ {variant === 'default' && (
+ <>
+ {pages.map((page, counter) => {
+ if (page === 'more') {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+ );
+ })}
+ >
+ )}
+
+
+
+ Showing {formatCommaNumber((currPage - 1) * perPage)} to{' '}
+ {formatCommaNumber((currPage - 1) * perPage + perPage)} of{' '}
+ {formatCommaNumber(totalCount)} entries
+
+
+ );
+}
diff --git a/src/hooks/use-pagination.ts b/src/hooks/use-pagination.ts
new file mode 100644
index 000000000..3df4ff527
--- /dev/null
+++ b/src/hooks/use-pagination.ts
@@ -0,0 +1,36 @@
+import { useMemo } from 'react';
+
+export function usePagination(
+ currentPage: number,
+ totalPages: number,
+ maxPagesToShow: number,
+) {
+ return useMemo(() => {
+ const pages: Array
= [];
+ const half = Math.floor(maxPagesToShow / 2);
+ const start = Math.max(1, currentPage - half);
+ const end = Math.min(totalPages, currentPage + half);
+
+ if (start > 1) {
+ pages.push(1);
+ }
+
+ if (start > 2) {
+ pages.push('more');
+ }
+
+ for (let i = start; i <= end; i++) {
+ pages.push(i);
+ }
+
+ if (end < totalPages - 1) {
+ pages.push('more');
+ }
+
+ if (end < totalPages) {
+ pages.push(totalPages);
+ }
+
+ return pages;
+ }, [currentPage, totalPages, maxPagesToShow]);
+}
diff --git a/src/lib/number.ts b/src/lib/number.ts
new file mode 100644
index 000000000..6527e13c8
--- /dev/null
+++ b/src/lib/number.ts
@@ -0,0 +1,7 @@
+export const formatter = Intl.NumberFormat('en-US', {
+ useGrouping: true,
+});
+
+export function formatCommaNumber(number: number): string {
+ return formatter.format(number);
+}
diff --git a/src/pages/ai/explore.astro b/src/pages/ai/explore.astro
index c501f613f..052e09463 100644
--- a/src/pages/ai/explore.astro
+++ b/src/pages/ai/explore.astro
@@ -6,5 +6,4 @@ import AccountLayout from '../../layouts/AccountLayout.astro';
-