search: added filter button
continuous-integration/drone/push Build is failing Details

nextui
Guus van Meerveld 1 month ago
parent ec1b131c3c
commit 306585ad98

@ -7,7 +7,7 @@ import { Card, CardBody } from "@nextui-org/card";
import { Image } from "@nextui-org/image";
import Link from "next/link";
import NextImage from "next/image";
import formatViewCount from "@/utils/formatViewCount";
import formatBigNumber from "@/utils/formatBigNumber";
export const Channel: Component<{ data: ChannelProps }> = ({ data }) => {
const url = `/channel/${data.id}`;
@ -32,9 +32,9 @@ export const Channel: Component<{ data: ChannelProps }> = ({ data }) => {
<div className="flex-1 flex flex-col justify-center">
<h1 className="text-lg">{data.name}</h1>
<div className="flex flex-row gap-4 items-center font-semibold text-default-600">
<h1>{formatViewCount(data.subscribers)} subscribers</h1>
<h1>{formatBigNumber(data.subscribers)} subscribers</h1>
{data.videos !== 0 && (
<h1>{formatViewCount(data.videos)} videos</h1>
<h1>{formatBigNumber(data.videos)} videos</h1>
)}
</div>
<p className="text-default-600">{data.description}</p>

@ -0,0 +1,56 @@
import { SearchType } from "@/client/typings/search/options";
import { Component } from "@/typings/component";
import { FiFilter as FilterIcon } from "react-icons/fi";
import { Button } from "@nextui-org/button";
import {
Dropdown,
DropdownTrigger,
DropdownMenu,
DropdownItem
} from "@nextui-org/dropdown";
import { useMemo } from "react";
export const Filter: Component<{
filter: SearchType;
setFilter: (filter: SearchType) => void;
}> = ({ setFilter, filter }) => {
const filterMenuItems: { key: SearchType; label: string }[] = useMemo(
() => [
{ key: "all", label: "All" },
{ key: "video", label: "Videos" },
{ key: "channel", label: "Channels" },
{ key: "playlist", label: "Playlists" }
],
[]
);
return (
<Dropdown>
<DropdownTrigger>
<Button className="h-full" variant="bordered" isIconOnly>
<FilterIcon className="text-xl" />
</Button>
</DropdownTrigger>
<DropdownMenu
aria-label="Static Actions"
items={filterMenuItems}
selectionMode="single"
selectedKeys={[filter]}
onSelectionChange={(keys) => {
const selectedKeys = keys as Set<SearchType>;
const selectedKey = Array.from(selectedKeys)[0];
if (!selectedKey) return;
setFilter(selectedKey);
}}
>
{(item) => <DropdownItem key={item.key}>{item.label}</DropdownItem>}
</DropdownMenu>
</Dropdown>
);
};

@ -5,49 +5,74 @@ import { useClient } from "@/hooks/useClient";
import { Component } from "@/typings/component";
import { Spacer } from "@nextui-org/spacer";
import { useInfiniteQuery } from "@tanstack/react-query";
import { useSearchParams } from "next/navigation";
import { Channel } from "./Channel";
import { Container } from "@/components/Container";
import { LoadingPage } from "@/components/LoadingPage";
import { Button } from "@nextui-org/button";
import { Video } from "./Video";
import { Playlist } from "./Playlist";
import { Fragment, useCallback } from "react";
import { CircularProgress } from "@nextui-org/progress";
import { useVisibility } from "reactjs-visibility";
import { Fragment, useCallback, useMemo } from "react";
import { Loading } from "./Loading";
import { Filter } from "./Filter";
import { useSearchParams } from "next/navigation";
import { SearchType, SearchTypeModel } from "@/client/typings/search/options";
import { useSearch } from "@/hooks/useSearch";
export const Search: Component = () => {
const client = useClient();
const searchParams = useSearchParams();
const query = searchParams.get("search_query");
const query = searchParams.get("search_query") as string;
const client = useClient();
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 {
data,
error,
error: fetchError,
fetchNextPage,
hasNextPage,
refetch,
isFetching,
isFetchingNextPage
} = useInfiniteQuery({
queryKey: ["search", query],
queryKey: ["search", query, filter],
queryFn: async ({ pageParam }) => {
return await client.getSearch(query ?? "", { pageParam: pageParam });
return await client.getSearch(query ?? "", {
pageParam: pageParam,
type: filter
});
},
enabled: query !== null,
enabled: !!invalidQuery || !!invalidFilter,
initialPageParam: "",
getNextPageParam: (lastPage, pages) => lastPage.nextCursor
});
const handleUserReachedPageEnd = useCallback(
(visiblity: boolean) => {
console.log(visiblity);
const error = invalidQuery ?? invalidFilter ?? fetchError ?? undefined;
console.log(visiblity, !isFetchingNextPage, hasNextPage);
const searchFor = useSearch();
const setFilter = useCallback(
(filter: SearchType) => {
searchFor(query, filter);
},
[query]
);
const handleUserReachedPageEnd = useCallback(
(visiblity: boolean) => {
if (visiblity && !isFetchingNextPage) fetchNextPage();
},
[hasNextPage, isFetchingNextPage]
@ -56,9 +81,16 @@ export const Search: Component = () => {
return (
<>
<Container>
<SearchInput initialQueryValue={query ?? undefined} />
<div className="flex flex-row gap-2">
<div className="flex-1">
<SearchInput initialQueryValue={query ?? undefined} />
</div>
<div>
<Filter filter={filter} setFilter={setFilter} />
</div>
</div>
<Spacer y={4} />
{isFetching && <LoadingPage />}
{isFetching && !error && <LoadingPage />}
{error && (
<div className="flex-1 flex items-center justify-center">
<div className="text-center">
@ -74,33 +106,30 @@ export const Search: Component = () => {
</div>
)}
<div className="flex flex-col gap-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>
);
})}
{!error &&
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>
);
})}
<Loading
isFetching={isFetchingNextPage}
onVisible={handleUserReachedPageEnd}
/>
{/* {!isFetching && !isFetchingNextPage && !error && (
<Button onClick={() => fetchNextPage()}>Load more</Button>
)} */}
</div>
</Container>
</>

@ -7,7 +7,7 @@ import { Image } from "@nextui-org/image";
import NextImage from "next/image";
import { useMemo } from "react";
import formatViewCount from "@/utils/formatViewCount";
import formatBigNumber from "@/utils/formatBigNumber";
import formatUploadedTime from "@/utils/formatUploadedTime";
import { Link } from "@nextui-org/link";
import NextLink from "next/link";
@ -47,7 +47,7 @@ export const Video: Component<{ data: VideoProps }> = ({ data }) => {
<div className="flex flex-col gap-2">
<h1 className="text-xl">{data.title}</h1>
<div className="flex flex-row gap-4 items-center font-semibold text-default-600">
<h1>{formatViewCount(data.views)} views</h1>
<h1>{formatBigNumber(data.views)} views</h1>
<h1>{formatUploadedTime(data.uploaded)}</h1>
</div>
<Link

@ -1,6 +1,12 @@
import z from "zod";
export interface SearchOptions {
pageParam?: string;
type?: SearchType;
}
export type SearchType = "video" | "playlist" | "channel" | "all";
export const searchTypes = ["video", "playlist", "channel", "all"] as const;
export const SearchTypeModel = z.enum(searchTypes);
export type SearchType = z.infer<typeof SearchTypeModel>;

@ -8,7 +8,7 @@ import { useCallback, useMemo, useState } from "react";
import { useDebounce } from "use-debounce";
import { FiSearch as SearchIcon } from "react-icons/fi";
import { useRouter } from "next/navigation";
import { useSearch } from "@/hooks/useSearch";
export const Search: Component<{
initialQueryValue?: string;
@ -17,7 +17,7 @@ export const Search: Component<{
const [searchQuery, setSearchQuery] = useState(initialQueryValue ?? "");
const router = useRouter();
const searchFor = useSearch();
const [searchQueryDebounced] = useDebounce(searchQuery, 250);
@ -31,7 +31,7 @@ export const Search: Component<{
});
const submit = useCallback((query: string) => {
router.push(`/results?search_query=${query}`);
searchFor(query);
}, []);
const suggestions = useMemo(

@ -4,7 +4,7 @@ import { Card, CardFooter, CardBody } from "@nextui-org/card";
import { Image } from "@nextui-org/image";
import { Divider } from "@nextui-org/divider";
import Link from "next/link";
import formatViewCount from "@/utils/formatViewCount";
import formatBigNumber from "@/utils/formatBigNumber";
import formatDuration from "@/utils/formatDuration";
import { ContextMenu } from "./ContextMenu";
import formatUploadedTime from "@/utils/formatUploadedTime";
@ -89,7 +89,7 @@ export const Video: Component<{ data: VideoProps }> = ({ data }) => {
</Tooltip>
<p className="text-small tracking-tight text-default-400">
Views: {formatViewCount(data.views)}
Views: {formatBigNumber(data.views)}
</p>
</div>
</div>

@ -0,0 +1,17 @@
import { SearchType } from "@/client/typings/search/options";
import { useRouter } from "next/navigation";
const searchPathname = "/results";
export const useSearch = (): ((query: string, filter?: SearchType) => void) => {
const router = useRouter();
return (query, filter = "all") => {
const params = new URLSearchParams();
params.set("search_query", query);
params.set("filter", filter);
router.push(searchPathname + "?" + params.toString());
};
};

@ -0,0 +1,26 @@
const billion = 1.0e9;
const million = 1.0e6;
const thousand = 1.0e3;
const formatBigNumber = (num: number): string => {
const abs = Math.abs(num);
// Nine Zeroes for Billions
if (abs >= billion) return (abs / billion).toPrecision(3) + "B";
if (abs >= million) {
if (abs >= million * 10) return (abs / million).toPrecision(3) + "M";
return (abs / million).toPrecision(2) + "M";
}
if (abs >= thousand) {
if (abs >= thousand * 10) return (abs / thousand).toPrecision(3) + "K";
return (abs / thousand).toPrecision(2) + "K";
}
return abs.toString();
};
export default formatBigNumber;

@ -1,14 +0,0 @@
const formatViewCount = (num: number): string => {
// Nine Zeroes for Billions
return Math.abs(num) >= 1.0e9
? (Math.abs(num) / 1.0e9).toPrecision(3) + "B"
: // Six Zeroes for Millions
Math.abs(num) >= 1.0e6
? (Math.abs(num) / 1.0e6).toPrecision(3) + "M"
: // Three Zeroes for Thousands
Math.abs(num) >= 1.0e3
? (Math.abs(num) / 1.0e3).toPrecision(3) + "K"
: Math.abs(num).toString();
};
export default formatViewCount;
Loading…
Cancel
Save