results: improved folder structure, better fallback
continuous-integration/drone/push Build is passing
Details
continuous-integration/drone/push Build is passing
Details
parent
ff694c15c5
commit
cb54d9f991
@ -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 (
|
||||||
|
<div className="flex-1 flex items-center justify-center">
|
||||||
|
<div className="text-center">
|
||||||
|
<h1 className="text-xl">An error occurred loading the search page</h1>
|
||||||
|
<h2 className="text-lg">{error.toString()}</h2>
|
||||||
|
<Spacer y={2} />
|
||||||
|
<Button color="primary" onClick={() => refetch()}>
|
||||||
|
Retry
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -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 (
|
|
||||||
<>
|
|
||||||
<Container>
|
|
||||||
<div className="flex flex-row gap-2">
|
|
||||||
<div className="flex-1">
|
|
||||||
<SearchInput initialQueryValue={query ?? undefined} />
|
|
||||||
</div>
|
|
||||||
{canSearch && (
|
|
||||||
<div>
|
|
||||||
<Filter filter={filter} setFilter={setFilter} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{isLoadingInitialData && <LoadingPage />}
|
|
||||||
{error && (
|
|
||||||
<div className="flex-1 flex items-center justify-center">
|
|
||||||
<div className="text-center">
|
|
||||||
<h1 className="text-xl">
|
|
||||||
An error occurred loading the search page
|
|
||||||
</h1>
|
|
||||||
<h2 className="text-lg">{error.toString()}</h2>
|
|
||||||
<Spacer y={2} />
|
|
||||||
<Button color="primary" onClick={() => refetch()}>
|
|
||||||
Retry
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{hasLoadedData && (
|
|
||||||
<>
|
|
||||||
<div className="flex flex-col gap-4 mt-4">
|
|
||||||
{data?.pages.map((page, i) => {
|
|
||||||
return (
|
|
||||||
<Fragment key={i}>
|
|
||||||
{page.items.map((result) => {
|
|
||||||
switch (result.type) {
|
|
||||||
case "channel":
|
|
||||||
return <Channel key={result.id} data={result} />;
|
|
||||||
|
|
||||||
case "video":
|
|
||||||
return <Video key={result.id} data={result} />;
|
|
||||||
|
|
||||||
case "playlist":
|
|
||||||
return <Playlist key={result.id} data={result} />;
|
|
||||||
}
|
|
||||||
})}
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
<Loading
|
|
||||||
isFetching={isFetchingNextPage && !isPending}
|
|
||||||
onVisible={handleUserReachedPageEnd}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Container>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
@ -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 (
|
||||||
|
<>
|
||||||
|
<Container>
|
||||||
|
<SearchPageHeader query={query} filter={filter} />
|
||||||
|
{query && <SearchPageBody query={query} filter={filter} />}
|
||||||
|
</Container>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -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 && <ErrorPage data={error} refetch={refetch} />}
|
||||||
|
{isFetchingInitialData && (
|
||||||
|
<LoadingPage text={`Fetching search results for query \`${query}\``} />
|
||||||
|
)}
|
||||||
|
{error === null && data && (
|
||||||
|
<div className="flex flex-col gap-4 mt-4">
|
||||||
|
{data.pages.map((page, i) => {
|
||||||
|
return (
|
||||||
|
<Fragment key={i}>
|
||||||
|
{page.items.map((result) => {
|
||||||
|
switch (result.type) {
|
||||||
|
case "channel":
|
||||||
|
return <Channel key={result.id} data={result} />;
|
||||||
|
|
||||||
|
case "video":
|
||||||
|
return <Video key={result.id} data={result} />;
|
||||||
|
|
||||||
|
case "playlist":
|
||||||
|
return <Playlist key={result.id} data={result} />;
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<LoadingNextPage
|
||||||
|
isFetching={isFetchingNewPage}
|
||||||
|
onVisible={fetchNewData}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -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 (
|
||||||
|
<div className="flex flex-row gap-2">
|
||||||
|
<div className="flex-1">
|
||||||
|
<SearchInput query={query} setQuery={searchForQuery} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Filter filter={filter ?? "all"} setFilter={searchWithFilter} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -1,13 +1,16 @@
|
|||||||
"use client";
|
"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 (
|
return (
|
||||||
<div className="flex flex-1 justify-center items-center">
|
<div className="flex flex-1 justify-center items-center">
|
||||||
|
<div className="flex flex-col gap-2 items-center">
|
||||||
<CircularProgress aria-label="Loading page..." />
|
<CircularProgress aria-label="Loading page..." />
|
||||||
|
{text && <p className="text-xl">{text}</p>}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in new issue