From 306585ad98254c268c06ee5b5288049a387e36a1 Mon Sep 17 00:00:00 2001 From: Guus van Meerveld Date: Thu, 21 Mar 2024 19:24:39 +0100 Subject: [PATCH] search: added filter button --- src/app/results/Channel.tsx | 6 +- src/app/results/Filter.tsx | 56 ++++++++++++++ src/app/results/Search.tsx | 105 +++++++++++++++++---------- src/app/results/Video.tsx | 4 +- src/client/typings/search/options.ts | 8 +- src/components/Search.tsx | 6 +- src/components/Video.tsx | 4 +- src/hooks/useSearch.ts | 17 +++++ src/utils/formatBigNumber.ts | 26 +++++++ src/utils/formatViewCount.ts | 14 ---- 10 files changed, 183 insertions(+), 63 deletions(-) create mode 100644 src/app/results/Filter.tsx create mode 100644 src/hooks/useSearch.ts create mode 100644 src/utils/formatBigNumber.ts delete mode 100644 src/utils/formatViewCount.ts diff --git a/src/app/results/Channel.tsx b/src/app/results/Channel.tsx index 16b64de..0968c07 100644 --- a/src/app/results/Channel.tsx +++ b/src/app/results/Channel.tsx @@ -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 }) => {

{data.name}

-

{formatViewCount(data.subscribers)} subscribers

+

{formatBigNumber(data.subscribers)} subscribers

{data.videos !== 0 && ( -

{formatViewCount(data.videos)} videos

+

{formatBigNumber(data.videos)} videos

)}

{data.description}

diff --git a/src/app/results/Filter.tsx b/src/app/results/Filter.tsx new file mode 100644 index 0000000..330ead1 --- /dev/null +++ b/src/app/results/Filter.tsx @@ -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 ( + + + + + { + const selectedKeys = keys as Set; + + const selectedKey = Array.from(selectedKeys)[0]; + + if (!selectedKey) return; + + setFilter(selectedKey); + }} + > + {(item) => {item.label}} + + + ); +}; diff --git a/src/app/results/Search.tsx b/src/app/results/Search.tsx index 6523882..0f2ff65 100644 --- a/src/app/results/Search.tsx +++ b/src/app/results/Search.tsx @@ -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 ( <> - +
+
+ +
+
+ +
+
- {isFetching && } + {isFetching && !error && } {error && (
@@ -74,33 +106,30 @@ export const Search: Component = () => {
)}
- {data?.pages.map((page, i) => { - return ( - - {page.items.map((result) => { - switch (result.type) { - case "channel": - return ; - - case "video": - return - ); - })} + {!error && + data?.pages.map((page, i) => { + return ( + + {page.items.map((result) => { + switch (result.type) { + case "channel": + return ; + + case "video": + return + ); + })} - - {/* {!isFetching && !isFetchingNextPage && !error && ( - - )} */}
diff --git a/src/app/results/Video.tsx b/src/app/results/Video.tsx index daba460..dd49593 100644 --- a/src/app/results/Video.tsx +++ b/src/app/results/Video.tsx @@ -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 }) => {

{data.title}

-

{formatViewCount(data.views)} views

+

{formatBigNumber(data.views)} views

{formatUploadedTime(data.uploaded)}

; diff --git a/src/components/Search.tsx b/src/components/Search.tsx index 5bd1255..3fa8d99 100644 --- a/src/components/Search.tsx +++ b/src/components/Search.tsx @@ -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( diff --git a/src/components/Video.tsx b/src/components/Video.tsx index d08d458..d069f25 100644 --- a/src/components/Video.tsx +++ b/src/components/Video.tsx @@ -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 }) => {

- Views: {formatViewCount(data.views)} + Views: {formatBigNumber(data.views)}

diff --git a/src/hooks/useSearch.ts b/src/hooks/useSearch.ts new file mode 100644 index 0000000..9cface0 --- /dev/null +++ b/src/hooks/useSearch.ts @@ -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()); + }; +}; diff --git a/src/utils/formatBigNumber.ts b/src/utils/formatBigNumber.ts new file mode 100644 index 0000000..d1bc6ce --- /dev/null +++ b/src/utils/formatBigNumber.ts @@ -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; diff --git a/src/utils/formatViewCount.ts b/src/utils/formatViewCount.ts deleted file mode 100644 index 036f76d..0000000 --- a/src/utils/formatViewCount.ts +++ /dev/null @@ -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;