diff --git a/src/components/DiscoverRoadmaps/DiscoverRoadmapSorting.tsx b/src/components/DiscoverRoadmaps/DiscoverRoadmapSorting.tsx
new file mode 100644
index 000000000..c78daf3ff
--- /dev/null
+++ b/src/components/DiscoverRoadmaps/DiscoverRoadmapSorting.tsx
@@ -0,0 +1,77 @@
+import { ArrowDownWideNarrow, Check, ChevronDown } from 'lucide-react';
+import { useRef, useState } from 'react';
+import { useOutsideClick } from '../../hooks/use-outside-click';
+import type { SortByValues } from './DiscoverRoadmaps';
+
+const sortingLabels: { label: string; value: SortByValues }[] = [
+ {
+ label: 'Newest',
+ value: 'createdAt',
+ },
+ {
+ label: 'Oldest',
+ value: '-createdAt',
+ },
+ {
+ label: 'Highest Rated',
+ value: 'rating',
+ },
+ {
+ label: 'Lowest Rated',
+ value: '-rating',
+ },
+];
+
+type DiscoverRoadmapSortingProps = {
+ sortBy: SortByValues;
+ onSortChange: (sortBy: SortByValues) => void;
+};
+
+export function DiscoverRoadmapSorting(props: DiscoverRoadmapSortingProps) {
+ 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/DiscoverRoadmaps/DiscoverRoadmaps.tsx b/src/components/DiscoverRoadmaps/DiscoverRoadmaps.tsx
index 6dcee9d10..71faaa403 100644
--- a/src/components/DiscoverRoadmaps/DiscoverRoadmaps.tsx
+++ b/src/components/DiscoverRoadmaps/DiscoverRoadmaps.tsx
@@ -4,29 +4,133 @@ import { Pagination } from '../Pagination/Pagination';
import { SearchRoadmap } from './SearchRoadmap';
import { EmptyDiscoverRoadmaps } from './EmptyDiscoverRoadmaps';
import { Rating } from '../Rating/Rating';
+import { useEffect, useState } from 'react';
+import { deleteUrlParam, getUrlParams, setUrlParams } from '../../lib/browser';
+import { LoadingRoadmaps } from '../ExploreAIRoadmap/LoadingRoadmaps';
+import { httpGet } from '../../lib/http';
+import { useToast } from '../../hooks/use-toast';
+import { DiscoverRoadmapSorting } from './DiscoverRoadmapSorting';
-type DiscoverRoadmapsProps = {
- searchParams: string;
- roadmapsResponse: ListShowcaseRoadmapResponse;
+type DiscoverRoadmapsProps = {};
+
+export type SortByValues = 'rating' | '-rating' | 'createdAt' | '-createdAt';
+
+type QueryParams = {
+ q?: string;
+ s?: SortByValues;
+ p?: string;
+};
+
+type PageState = {
+ searchTerm: string;
+ sortBy: SortByValues;
+ currentPage: number;
};
export function DiscoverRoadmaps(props: DiscoverRoadmapsProps) {
- const { roadmapsResponse, searchParams: defaultSearchparams } = props;
+ const toast = useToast();
+
+ const [pageState, setPageState] = useState({
+ searchTerm: '',
+ sortBy: 'createdAt',
+ currentPage: 0,
+ });
+ const [isLoading, setIsLoading] = useState(true);
+ const [roadmapsResponse, setRoadmapsResponse] =
+ useState(null);
+
+ useEffect(() => {
+ const queryParams = getUrlParams() as QueryParams;
+
+ setPageState({
+ searchTerm: queryParams.q || '',
+ sortBy: queryParams.s || 'createdAt',
+ currentPage: +(queryParams.p || '1'),
+ });
+ }, []);
+
+ useEffect(() => {
+ setIsLoading(true);
+ if (!pageState.currentPage) {
+ return;
+ }
+
+ // only set the URL params if the user modified anything
+ if (
+ pageState.currentPage !== 1 ||
+ pageState.searchTerm !== '' ||
+ pageState.sortBy !== 'createdAt'
+ ) {
+ setUrlParams({
+ q: pageState.searchTerm,
+ s: pageState.sortBy,
+ p: String(pageState.currentPage),
+ });
+ } else {
+ deleteUrlParam('q');
+ deleteUrlParam('s');
+ deleteUrlParam('p');
+ }
+
+ loadAIRoadmaps(
+ pageState.currentPage,
+ pageState.searchTerm,
+ pageState.sortBy,
+ ).finally(() => {
+ setIsLoading(false);
+ });
+ }, [pageState]);
+
+ const loadAIRoadmaps = async (
+ currPage: number = 1,
+ searchTerm: string = '',
+ sortBy: SortByValues = 'createdAt',
+ ) => {
+ const { response, error } = await httpGet(
+ `${import.meta.env.PUBLIC_API_URL}/v1-list-showcase-roadmap`,
+ {
+ currPage,
+ ...(searchTerm && { term: searchTerm }),
+ ...(sortBy && { sortBy }),
+ },
+ );
+
+ if (error || !response) {
+ toast.error(error?.message || 'Something went wrong');
+ return;
+ }
+
+ setRoadmapsResponse(response);
+ };
const roadmaps = roadmapsResponse?.data || [];
- const searchParams = new URLSearchParams(defaultSearchparams);
- const titleQuery = searchParams.get('q') || '';
+ const loadingIndicator = isLoading && ;
return (
-
+
+ {}}
+ />
+
+ {
+ setPageState({
+ ...pageState,
+ sortBy,
+ });
+ }}
+ />
+
- {roadmaps.length === 0 && }
- {roadmaps.length > 0 && (
+ {loadingIndicator}
+ {roadmaps.length === 0 && !isLoading && }
+ {roadmaps.length > 0 && !isLoading && (
<>
{roadmaps.map((roadmap) => {
@@ -75,13 +179,10 @@ export function DiscoverRoadmaps(props: DiscoverRoadmapsProps) {
perPage={roadmapsResponse?.perPage || 0}
totalCount={roadmapsResponse?.totalCount || 0}
onPageChange={(page) => {
- const newSearchParams = new URLSearchParams();
- if (titleQuery) {
- newSearchParams.set('q', titleQuery);
- }
-
- newSearchParams.set('currPage', page.toString());
- window.location.href = `/discover?${newSearchParams.toString()}`;
+ setPageState({
+ ...pageState,
+ currentPage: page,
+ });
}}
/>
>
diff --git a/src/components/DiscoverRoadmaps/SearchRoadmap.tsx b/src/components/DiscoverRoadmaps/SearchRoadmap.tsx
index c76a397d4..89592f41c 100644
--- a/src/components/DiscoverRoadmaps/SearchRoadmap.tsx
+++ b/src/components/DiscoverRoadmaps/SearchRoadmap.tsx
@@ -1,18 +1,45 @@
import { Search } from 'lucide-react';
+import { useEffect, useState } from 'react';
+import { useDebounceValue } from '../../hooks/use-debounce';
+import { Spinner } from '../ReactIcons/Spinner';
type SearchRoadmapProps = {
value: string;
total: number;
+ isLoading: boolean;
+ onValueChange: (value: string) => void;
};
export function SearchRoadmap(props: SearchRoadmapProps) {
- const { total, value: defaultValue } = props;
+ const { total, value: defaultValue, onValueChange, isLoading } = 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;
+ }
+
+ onValueChange(debouncedTerm);
+ }, [debouncedTerm]);
return (
-
+
{total > 0 && (
diff --git a/src/pages/discover.astro b/src/pages/discover.astro
index dbfee1565..c7cf19ec0 100644
--- a/src/pages/discover.astro
+++ b/src/pages/discover.astro
@@ -1,27 +1,8 @@
---
-import { roadmapApi } from '../api/roadmap';
import BaseLayout from '../layouts/BaseLayout.astro';
import { DiscoverRoadmaps } from '../components/DiscoverRoadmaps/DiscoverRoadmaps';
-import { DiscoverError } from '../components/DiscoverRoadmaps/DiscoverError';
-
-export const prerender = false;
-
-const roadmapApiClient = roadmapApi(Astro);
-
-const { error, response: roadmaps } =
- await roadmapApiClient.listShowcaseRoadmap();
-const searchParams = Astro.url.searchParams.toString();
---
- {error && }
- {
- roadmaps && (
-
- )
- }
+