diff --git a/src/app/results/ErrorPage.tsx b/src/app/results/ErrorPage.tsx
new file mode 100644
index 0000000..63ae427
--- /dev/null
+++ b/src/app/results/ErrorPage.tsx
@@ -0,0 +1,22 @@
+import { FC } from "react";
+
+import { Button } from "@nextui-org/button";
+import { Spacer } from "@nextui-org/spacer";
+
+export const ErrorPage: FC<{ data: Error; refetch: () => void }> = ({
+ data: error,
+ refetch
+}) => {
+ return (
+
+
+
An error occurred loading the search page
+ {error.toString()}
+
+
+
+
+ );
+};
diff --git a/src/app/results/Search.tsx b/src/app/results/Search.tsx
deleted file mode 100644
index eba280c..0000000
--- a/src/app/results/Search.tsx
+++ /dev/null
@@ -1,154 +0,0 @@
-"use client";
-
-import { useInfiniteQuery } from "@tanstack/react-query";
-import { useSearchParams } from "next/navigation";
-import { Fragment, useCallback, useMemo } from "react";
-
-import { Button } from "@nextui-org/button";
-import { Spacer } from "@nextui-org/spacer";
-
-import { useClient } from "@/hooks/useClient";
-import { useSearch } from "@/hooks/useSearch";
-
-import { SearchType, SearchTypeModel } from "@/client/typings/search/options";
-
-import { Container } from "@/components/Container";
-import { LoadingPage } from "@/components/LoadingPage";
-import { Search as SearchInput } from "@/components/Search";
-
-import { Channel } from "./Channel";
-import { Filter } from "./Filter";
-import { Loading } from "./Loading";
-import { Playlist } from "./Playlist";
-import { Video } from "./Video";
-
-import { Component } from "@/typings/component";
-
-export const Search: Component = () => {
- const client = useClient();
-
- const searchParams = useSearchParams();
-
- const query = searchParams.get("search_query") as string;
-
- const invalidQuery = useMemo(() => {
- if (query === null || query.length === 0)
- return new Error(`The required parameter 'query' is missing`);
- }, [query]);
-
- const filter = (searchParams.get("filter") ?? "all") as SearchType;
-
- const invalidFilter = useMemo(() => {
- const parsed = SearchTypeModel.safeParse(filter);
-
- if (!parsed.success)
- return new Error(`The provided filter \`${filter}\` is invalid`);
- }, [filter]);
-
- const canSearch = !(!!invalidQuery || !!invalidFilter);
-
- const {
- data,
- error: fetchError,
- fetchNextPage,
- refetch,
- isPending,
- isFetchingNextPage
- } = useInfiniteQuery({
- queryKey: ["search", query, filter],
- queryFn: async ({ pageParam }) => {
- return await client.getSearch(query ?? "", {
- pageParam: pageParam,
- type: filter
- });
- },
- enabled: canSearch,
- initialPageParam: "",
- getNextPageParam: (lastPage) => lastPage.nextCursor
- });
-
- const error = invalidFilter ?? fetchError ?? undefined;
-
- const searchFor = useSearch();
-
- const setFilter = useCallback(
- (filter: SearchType) => {
- searchFor(query, filter);
- },
- [query, searchFor]
- );
-
- const handleUserReachedPageEnd = useCallback(
- (visiblity: boolean) => {
- if (visiblity && !isFetchingNextPage) fetchNextPage();
- },
- [isFetchingNextPage, fetchNextPage]
- );
-
- const hasLoadedData =
- canSearch && data?.pages.flat().length
- ? data?.pages.flat().length !== 0
- : false;
-
- const isLoadingInitialData = canSearch && !isFetchingNextPage && isPending;
-
- return (
- <>
-
-
-
-
-
- {canSearch && (
-
-
-
- )}
-
- {isLoadingInitialData && }
- {error && (
-
-
-
- An error occurred loading the search page
-
- {error.toString()}
-
-
-
-
- )}
- {hasLoadedData && (
- <>
-
- {data?.pages.map((page, i) => {
- return (
-
- {page.items.map((result) => {
- switch (result.type) {
- case "channel":
- return ;
-
- case "video":
- return ;
-
- case "playlist":
- return ;
- }
- })}
-
- );
- })}
-
-
- >
- )}
-
- >
- );
-};
diff --git a/src/app/results/SearchPage.tsx b/src/app/results/SearchPage.tsx
new file mode 100644
index 0000000..da92e59
--- /dev/null
+++ b/src/app/results/SearchPage.tsx
@@ -0,0 +1,42 @@
+"use client";
+
+import { useSearchParams } from "next/navigation";
+import { FC, useMemo } from "react";
+
+import { SearchType, SearchTypeModel } from "@/client/typings/search/options";
+
+import { Container } from "@/components/Container";
+
+import { SearchPageBody } from "./SearchPageBody";
+import { SearchPageHeader } from "./SearchPageHeader";
+
+export const SearchPage: FC = () => {
+ const searchParams = useSearchParams();
+
+ const query = useMemo(() => {
+ const param = searchParams.get("search_query");
+
+ if (param === null || param.length === null) return;
+
+ return param;
+ }, [searchParams]);
+
+ const filter: SearchType = useMemo(() => {
+ const param = searchParams.get("filter");
+
+ const parsed = SearchTypeModel.safeParse(param);
+
+ if (!parsed.success) return "all";
+
+ return parsed.data;
+ }, [searchParams]);
+
+ return (
+ <>
+
+
+ {query && }
+
+ >
+ );
+};
diff --git a/src/app/results/Channel.tsx b/src/app/results/SearchPageBody/Channel.tsx
similarity index 90%
rename from src/app/results/Channel.tsx
rename to src/app/results/SearchPageBody/Channel.tsx
index 27de4d7..ab5561a 100644
--- a/src/app/results/Channel.tsx
+++ b/src/app/results/SearchPageBody/Channel.tsx
@@ -2,6 +2,7 @@
import NextImage from "next/image";
import Link from "next/link";
+import { FC } from "react";
import { Card, CardBody } from "@nextui-org/card";
import { Image } from "@nextui-org/image";
@@ -9,9 +10,7 @@ import { Image } from "@nextui-org/image";
import { ChannelItem } from "@/client/typings/item";
import formatBigNumber from "@/utils/formatBigNumber";
-import { Component } from "@/typings/component";
-
-export const Channel: Component<{ data: ChannelItem }> = ({ data }) => {
+export const Channel: FC<{ data: ChannelItem }> = ({ data }) => {
const url = `/channel/${data.id}`;
const imageSize = 200;
diff --git a/src/app/results/Loading.tsx b/src/app/results/SearchPageBody/LoadingNextPage.tsx
similarity index 92%
rename from src/app/results/Loading.tsx
rename to src/app/results/SearchPageBody/LoadingNextPage.tsx
index 260003a..dd6b09c 100644
--- a/src/app/results/Loading.tsx
+++ b/src/app/results/SearchPageBody/LoadingNextPage.tsx
@@ -4,7 +4,7 @@ import { CircularProgress } from "@nextui-org/progress";
import { Component } from "@/typings/component";
-export const Loading: Component<{
+export const LoadingNextPage: Component<{
isFetching: boolean;
onVisible: (visiblity: boolean) => void;
}> = ({ onVisible, isFetching }) => {
diff --git a/src/app/results/Playlist.tsx b/src/app/results/SearchPageBody/Playlist.tsx
similarity index 94%
rename from src/app/results/Playlist.tsx
rename to src/app/results/SearchPageBody/Playlist.tsx
index 8c27ba4..0f1a92f 100644
--- a/src/app/results/Playlist.tsx
+++ b/src/app/results/SearchPageBody/Playlist.tsx
@@ -2,6 +2,7 @@
import NextImage from "next/image";
import NextLink from "next/link";
+import { FC } from "react";
import { Card, CardBody } from "@nextui-org/card";
import { Image } from "@nextui-org/image";
@@ -12,9 +13,7 @@ import { PlaylistItem } from "@/client/typings/item";
import { videoUrl } from "@/utils/urls";
import { videoSize } from "@/utils/videoSize";
-import { Component } from "@/typings/component";
-
-export const Playlist: Component<{ data: PlaylistItem }> = ({ data }) => {
+export const Playlist: FC<{ data: PlaylistItem }> = ({ data }) => {
const url = `/playlist/${data.id}`;
const channelUrl = `/channel/${data.author.id}`;
diff --git a/src/app/results/Video.tsx b/src/app/results/SearchPageBody/Video.tsx
similarity index 94%
rename from src/app/results/Video.tsx
rename to src/app/results/SearchPageBody/Video.tsx
index f11c1c6..027c862 100644
--- a/src/app/results/Video.tsx
+++ b/src/app/results/SearchPageBody/Video.tsx
@@ -2,6 +2,7 @@
import NextImage from "next/image";
import NextLink from "next/link";
+import { FC } from "react";
import { Avatar } from "@nextui-org/avatar";
import { Card, CardBody } from "@nextui-org/card";
@@ -14,9 +15,7 @@ import formatDuration from "@/utils/formatDuration";
import formatUploadedTime from "@/utils/formatUploadedTime";
import { videoSize } from "@/utils/videoSize";
-import { Component } from "@/typings/component";
-
-export const Video: Component<{ data: VideoItem }> = ({ data }) => {
+export const Video: FC<{ data: VideoItem }> = ({ data }) => {
const url = `/watch?v=${data.id}`;
const channelUrl = `/channel/${data.author.id}`;
diff --git a/src/app/results/SearchPageBody/index.tsx b/src/app/results/SearchPageBody/index.tsx
new file mode 100644
index 0000000..90c31a4
--- /dev/null
+++ b/src/app/results/SearchPageBody/index.tsx
@@ -0,0 +1,84 @@
+import { useInfiniteQuery } from "@tanstack/react-query";
+import { FC, Fragment, useCallback } from "react";
+
+import { useClient } from "@/hooks/useClient";
+
+import { SearchType } from "@/client/typings/search/options";
+
+import { LoadingPage } from "@/components/LoadingPage";
+
+import { ErrorPage } from "../ErrorPage";
+import { Channel } from "./Channel";
+import { LoadingNextPage } from "./LoadingNextPage";
+import { Playlist } from "./Playlist";
+import { Video } from "./Video";
+
+export const SearchPageBody: FC<{ query: string; filter: SearchType }> = ({
+ filter,
+ query
+}) => {
+ const client = useClient();
+
+ const {
+ data,
+ error,
+ fetchNextPage,
+ refetch,
+ isPending: isFetchingInitialData,
+ isFetchingNextPage
+ } = useInfiniteQuery({
+ queryKey: ["search", query, filter],
+ queryFn: async ({ pageParam }) => {
+ return await client.getSearch(query, {
+ pageParam: pageParam,
+ type: filter
+ });
+ },
+ initialPageParam: "",
+ getNextPageParam: (lastPage) => lastPage.nextCursor
+ });
+
+ const isFetchingNewPage = isFetchingNextPage && !isFetchingInitialData;
+
+ const fetchNewData = useCallback(
+ (visiblity: boolean) => {
+ if (visiblity && !isFetchingNextPage) fetchNextPage();
+ },
+ [isFetchingNextPage, fetchNextPage]
+ );
+
+ return (
+ <>
+ {error !== null && }
+ {isFetchingInitialData && (
+
+ )}
+ {error === null && data && (
+
+ {data.pages.map((page, i) => {
+ return (
+
+ {page.items.map((result) => {
+ switch (result.type) {
+ case "channel":
+ return ;
+
+ case "video":
+ return ;
+
+ case "playlist":
+ return ;
+ }
+ })}
+
+ );
+ })}
+
+
+ )}
+ >
+ );
+};
diff --git a/src/app/results/SearchPageHeader.tsx b/src/app/results/SearchPageHeader.tsx
new file mode 100644
index 0000000..8bb9a2b
--- /dev/null
+++ b/src/app/results/SearchPageHeader.tsx
@@ -0,0 +1,37 @@
+"use client";
+
+import { FC } from "react";
+
+import { useSearch } from "@/hooks/useSearch";
+
+import { SearchType } from "@/client/typings/search/options";
+
+import { Search as SearchInput } from "@/components/Search";
+
+import { Filter } from "./Filter";
+
+export const SearchPageHeader: FC<{
+ query?: string;
+ filter?: SearchType;
+}> = ({ query, filter }) => {
+ const searchFor = useSearch();
+
+ const searchForQuery = (query: string): void => {
+ searchFor(query, filter);
+ };
+
+ const searchWithFilter = (filter: SearchType): void => {
+ if (query) searchFor(query, filter);
+ };
+
+ return (
+
+ );
+};
diff --git a/src/app/results/page.tsx b/src/app/results/page.tsx
index 330af9e..74f42e5 100644
--- a/src/app/results/page.tsx
+++ b/src/app/results/page.tsx
@@ -1,13 +1,22 @@
import { NextPage } from "next";
import { Suspense } from "react";
-import { Search } from "./Search";
+import { Container } from "@/components/Container";
+
+import { SearchPage } from "./SearchPage";
+import { SearchPageHeader } from "./SearchPageHeader";
const Page: NextPage = () => {
return (
<>
-
-
+
+
+
+ }
+ >
+
>
);
diff --git a/src/components/LoadingPage.tsx b/src/components/LoadingPage.tsx
index 00e073a..016e398 100644
--- a/src/components/LoadingPage.tsx
+++ b/src/components/LoadingPage.tsx
@@ -1,13 +1,16 @@
"use client";
-import { CircularProgress } from "@nextui-org/progress";
+import { FC } from "react";
-import { Component } from "@/typings/component";
+import { CircularProgress } from "@nextui-org/progress";
-export const LoadingPage: Component = () => {
+export const LoadingPage: FC<{ text?: string }> = ({ text }) => {
return (
);
};
diff --git a/src/components/Search.tsx b/src/components/Search.tsx
index c696f46..9cc0989 100644
--- a/src/components/Search.tsx
+++ b/src/components/Search.tsx
@@ -9,16 +9,14 @@ import { FiSearch as SearchIcon } from "react-icons/fi";
import { Autocomplete, AutocompleteItem } from "@nextui-org/autocomplete";
import { useClient } from "@/hooks/useClient";
-import { useSearch } from "@/hooks/useSearch";
export const Search: FC<{
- initialQueryValue?: string;
-}> = ({ initialQueryValue }) => {
+ query?: string;
+ setQuery: (query: string) => void;
+}> = ({ setQuery, query }) => {
const client = useClient();
- const [searchQuery, setSearchQuery] = useState(initialQueryValue ?? "");
-
- const searchFor = useSearch();
+ const [searchQuery, setSearchQuery] = useState(query ?? "");
const [searchQueryDebounced] = useDebounce(searchQuery, 250);
@@ -32,7 +30,7 @@ export const Search: FC<{
});
const submit = (query: string): void => {
- searchFor(query);
+ setQuery(query);
};
const suggestions = useMemo(
@@ -51,7 +49,7 @@ export const Search: FC<{
name="search_query"
value={searchQuery}
isLoading={isLoading}
- defaultInputValue={initialQueryValue}
+ defaultInputValue={query}
onValueChange={setSearchQuery}
onKeyDown={(e) => {
if (e.key === "Enter") {