trending: added region switcher
continuous-integration/drone/push Build is passing Details

nextui
Guus van Meerveld 8 months ago
parent 6de055dc50
commit e1c0a1082c

@ -12,6 +12,7 @@
"dependencies": { "dependencies": {
"@nextui-org/react": "^2.2.10", "@nextui-org/react": "^2.2.10",
"@tanstack/react-query": "^5.27.5", "@tanstack/react-query": "^5.27.5",
"country-region-data": "^3.0.0",
"framer-motion": "^11.0.12", "framer-motion": "^11.0.12",
"ky": "^1.2.2", "ky": "^1.2.2",
"luxon": "^3.4.4", "luxon": "^3.4.4",

@ -11,7 +11,9 @@ export const SearchPage: Component = () => {
return ( return (
<> <>
<div className="container mx-auto py-4">
<Search initialQueryValue={query || undefined} /> <Search initialQueryValue={query || undefined} />
</div>
</> </>
); );
}; };

@ -0,0 +1,33 @@
"use client";
import { Component } from "@/typings/component";
import { Region } from "@/utils/getRegionCodes";
import { Autocomplete, AutocompleteItem } from "@nextui-org/autocomplete";
import { useRouter } from "next/navigation";
export const RegionSwitcher: Component<{
regions: Region[];
currentRegion: Region | null;
}> = ({ currentRegion, regions }) => {
const router = useRouter();
return (
<Autocomplete
defaultItems={regions}
label="Region"
placeholder="Select your region"
isClearable={false}
selectedKey={currentRegion?.code}
onSelectionChange={(key) => {
if (typeof key === "string" && key.length != 0)
return router.push(`/trending?region=${key}`);
}}
className="max-w-xs"
>
{(item) => (
<AutocompleteItem key={item.code}>{item.name}</AutocompleteItem>
)}
</Autocomplete>
);
};

@ -5,26 +5,72 @@ import { useClient } from "@/hooks/useClient";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { Button } from "@nextui-org/button"; import { Button } from "@nextui-org/button";
import { CircularProgress } from "@nextui-org/progress";
import { Spacer } from "@nextui-org/spacer"; import { Spacer } from "@nextui-org/spacer";
import { VideoCard } from "./VideoCard"; import { VideoCard } from "./VideoCard";
import { LoadingPage } from "@/components/LoadingPage";
import { useSearchParams } from "next/navigation";
import { useMemo } from "react";
import getRegionCodes from "@/utils/getRegionCodes";
import { RegionSwitcher } from "./RegionSwitcher";
import { defaultRegion } from "@/constants";
export const Trending: Component = ({}) => { export const Trending: Component = ({}) => {
const client = useClient(); const client = useClient();
const { isLoading, error, refetch, data } = useQuery({ const searchParams = useSearchParams();
queryKey: ["trending"], const validRegions = useMemo(() => getRegionCodes(), []);
queryFn: () => client.getTrending("NL")
const specifiedRegion =
searchParams.get("region")?.toUpperCase() ?? defaultRegion;
const [region, regionError] = useMemo(() => {
const foundRegion = validRegions.find(
(validRegion) => validRegion.code === specifiedRegion
);
if (foundRegion === undefined)
return [null, new Error(`Region \`${specifiedRegion}\` is invalid`)];
return [foundRegion, null];
}, [specifiedRegion, validRegions]);
const {
isLoading,
error: fetchError,
refetch,
data
} = useQuery({
queryKey: ["trending", region],
queryFn: () => {
if (region === null) return;
return client.getTrending(region.code);
},
enabled: regionError === null
}); });
const noDataError = useMemo(() => {
if (data && data.length === 0)
return new Error(
`Could not find any trending video's in region \`${region?.name}\``
);
return null;
}, [data]);
const error: Error | null = regionError ?? fetchError ?? noDataError ?? null;
return ( return (
<div className="container px-4 mx-auto min-h-screen"> <>
{isLoading && !data && ( {isLoading && !data && <LoadingPage />}
<div className="flex items-center justify-center h-screen"> {!isLoading && (
<CircularProgress aria-label="Loading trending page..." /> <div className="container mx-auto px-4 min-h-screen">
<div className="flex items-center">
<RegionSwitcher currentRegion={region} regions={validRegions} />
<Spacer x={4} />
<h1 className="text-xl">Trending</h1>
</div> </div>
)}
{error && ( {error && (
<div className="flex items-center justify-center h-screen"> <div className="flex items-center justify-center h-screen">
<div className="text-center"> <div className="text-center">
@ -39,7 +85,7 @@ export const Trending: Component = ({}) => {
</div> </div>
</div> </div>
)} )}
{data && ( {data && error === null && (
<div className="grid gap-4 py-4 grid-cols-1 md:grid-cols-2 lg:grid-cols-3"> <div className="grid gap-4 py-4 grid-cols-1 md:grid-cols-2 lg:grid-cols-3">
{data.map((video) => ( {data.map((video) => (
<VideoCard key={video.id} data={video} /> <VideoCard key={video.id} data={video} />
@ -47,5 +93,7 @@ export const Trending: Component = ({}) => {
</div> </div>
)} )}
</div> </div>
)}
</>
); );
}; };

@ -10,9 +10,17 @@ import formatDuration from "@/utils/formatDuration";
export const VideoCard: Component<{ data: TrendingVideo }> = ({ export const VideoCard: Component<{ data: TrendingVideo }> = ({
data: video data: video
}) => { }) => {
const handleContextMenu = () => {};
return ( return (
<Link href={`/watch?v=${video.id}`}> <Link href={`/watch?v=${video.id}`}>
<Card radius="lg"> <Card
radius="lg"
onContextMenu={(e) => {
e.preventDefault();
handleContextMenu();
}}
>
<CardBody> <CardBody>
<Image <Image
alt={video.title} alt={video.title}

@ -1,9 +1,13 @@
import { Suspense } from "react";
import { Trending } from "./Trending"; import { Trending } from "./Trending";
import { LoadingPage } from "@/components/LoadingPage";
export default function Page() { export default function Page() {
return ( return (
<> <>
<Suspense fallback={<LoadingPage />}>
<Trending /> <Trending />
</Suspense>
</> </>
); );
} }

@ -0,0 +1,12 @@
"use client";
import { Component } from "@/typings/component";
import { CircularProgress } from "@nextui-org/progress";
export const LoadingPage: Component = () => {
return (
<div className="h-screen container mx-auto flex items-center justify-center">
<CircularProgress aria-label="Loading page..." />
</div>
);
};

@ -12,7 +12,7 @@ export const Search: Component<{ initialQueryValue?: string }> = ({
}) => { }) => {
const client = useClient(); const client = useClient();
const [searchQuery, setSearchQuery] = useState(initialQueryValue ?? ""); const [searchQuery, setSearchQuery] = useState("");
const [searchQueryDebounced] = useDebounce(searchQuery, 500); const [searchQueryDebounced] = useDebounce(searchQuery, 500);
@ -23,8 +23,6 @@ export const Search: Component<{ initialQueryValue?: string }> = ({
}); });
const handleSubmit: FormEventHandler = (e) => { const handleSubmit: FormEventHandler = (e) => {
// e.preventDefault();
console.log(searchQuery); console.log(searchQuery);
}; };

@ -0,0 +1 @@
export const defaultRegion = "US" as const;

@ -0,0 +1,15 @@
import { allCountries } from "country-region-data";
export interface Region {
code: string;
name: string;
}
const getRegionCodes = (): Region[] => {
return allCountries.map((country) => ({
name: country[0],
code: country[1]
}));
};
export default getRegionCodes;

@ -3676,6 +3676,11 @@ core-js-compat@^3.31.0, core-js-compat@^3.34.0:
dependencies: dependencies:
browserslist "^4.22.3" browserslist "^4.22.3"
country-region-data@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/country-region-data/-/country-region-data-3.0.0.tgz#9251cff57c22e450cbe96a7e50a3a23362d4304a"
integrity sha512-jpZwc6coXayi3aAv2HHTC9vhwRJB2zdur+coBlIZo1IVMonzylRR4Asf5j7evtUzdZPODdHrJ8CzEFK6MGUAgg==
cross-spawn@^7.0.0, cross-spawn@^7.0.2: cross-spawn@^7.0.0, cross-spawn@^7.0.2:
version "7.0.3" version "7.0.3"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"

Loading…
Cancel
Save