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": {
"@nextui-org/react": "^2.2.10",
"@tanstack/react-query": "^5.27.5",
"country-region-data": "^3.0.0",
"framer-motion": "^11.0.12",
"ky": "^1.2.2",
"luxon": "^3.4.4",

@ -11,7 +11,9 @@ export const SearchPage: Component = () => {
return (
<>
<Search initialQueryValue={query || undefined} />
<div className="container mx-auto py-4">
<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,47 +5,95 @@ import { useClient } from "@/hooks/useClient";
import { useQuery } from "@tanstack/react-query";
import { Button } from "@nextui-org/button";
import { CircularProgress } from "@nextui-org/progress";
import { Spacer } from "@nextui-org/spacer";
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 = ({}) => {
const client = useClient();
const { isLoading, error, refetch, data } = useQuery({
queryKey: ["trending"],
queryFn: () => client.getTrending("NL")
const searchParams = useSearchParams();
const validRegions = useMemo(() => getRegionCodes(), []);
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 (
<div className="container px-4 mx-auto min-h-screen">
{isLoading && !data && (
<div className="flex items-center justify-center h-screen">
<CircularProgress aria-label="Loading trending page..." />
</div>
)}
{error && (
<div className="flex items-center justify-center h-screen">
<div className="text-center">
<h1 className="text-xl">
An error occurred loading the trending page
</h1>
<h2 className="text-lg">{error.toString()}</h2>
<Spacer y={2} />
<Button color="primary" onClick={() => refetch()}>
Retry
</Button>
<>
{isLoading && !data && <LoadingPage />}
{!isLoading && (
<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>
{error && (
<div className="flex items-center justify-center h-screen">
<div className="text-center">
<h1 className="text-xl">
An error occurred loading the trending page
</h1>
<h2 className="text-lg">{error.toString()}</h2>
<Spacer y={2} />
<Button color="primary" onClick={() => refetch()}>
Retry
</Button>
</div>
</div>
)}
{data && error === null && (
<div className="grid gap-4 py-4 grid-cols-1 md:grid-cols-2 lg:grid-cols-3">
{data.map((video) => (
<VideoCard key={video.id} data={video} />
))}
</div>
)}
</div>
)}
{data && (
<div className="grid gap-4 py-4 grid-cols-1 md:grid-cols-2 lg:grid-cols-3">
{data.map((video) => (
<VideoCard key={video.id} data={video} />
))}
</div>
)}
</div>
</>
);
};

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

@ -1,9 +1,13 @@
import { Suspense } from "react";
import { Trending } from "./Trending";
import { LoadingPage } from "@/components/LoadingPage";
export default function Page() {
return (
<>
<Trending />
<Suspense fallback={<LoadingPage />}>
<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 [searchQuery, setSearchQuery] = useState(initialQueryValue ?? "");
const [searchQuery, setSearchQuery] = useState("");
const [searchQueryDebounced] = useDebounce(searchQuery, 500);
@ -23,8 +23,6 @@ export const Search: Component<{ initialQueryValue?: string }> = ({
});
const handleSubmit: FormEventHandler = (e) => {
// e.preventDefault();
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:
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:
version "7.0.3"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"

Loading…
Cancel
Save