added basic search page
continuous-integration/drone/push Build is passing
Details
continuous-integration/drone/push Build is passing
Details
parent
c7dd2ddd12
commit
5ac329296e
@ -0,0 +1,42 @@
|
|||||||
|
import { Component } from "@/typings/component";
|
||||||
|
|
||||||
|
import { ChannelResult as ChannelProps } from "@/client/typings/search";
|
||||||
|
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";
|
||||||
|
|
||||||
|
export const Channel: Component<{ data: ChannelProps }> = ({ data }) => {
|
||||||
|
const url = `/channel/${data.id}`;
|
||||||
|
|
||||||
|
const imageSize = 200;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link href={url}>
|
||||||
|
<Card>
|
||||||
|
<CardBody>
|
||||||
|
<div className="flex flex-row gap-4">
|
||||||
|
<Image
|
||||||
|
width={imageSize}
|
||||||
|
height={imageSize}
|
||||||
|
src={data.thumbnail}
|
||||||
|
alt={data.name}
|
||||||
|
as={NextImage}
|
||||||
|
unoptimized
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex-1 flex flex-col">
|
||||||
|
<h1 className="text-lg">{data.name}</h1>
|
||||||
|
<div className="flex flex-row gap-4 items-center font-semibold text-default-600">
|
||||||
|
<h1>Subscribers: {formatViewCount(data.subscribers)}</h1>
|
||||||
|
<h1>Videos: {formatViewCount(data.videos)}</h1>
|
||||||
|
</div>
|
||||||
|
<p className="text-default-600">{data.description}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,71 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Search as SearchInput } from "@/components/Search";
|
||||||
|
import { useClient } from "@/hooks/useClient";
|
||||||
|
import { Component } from "@/typings/component";
|
||||||
|
import { Spacer } from "@nextui-org/spacer";
|
||||||
|
import { useQuery } 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";
|
||||||
|
|
||||||
|
export const Search: Component = () => {
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
const query = searchParams.get("search_query");
|
||||||
|
|
||||||
|
const client = useClient();
|
||||||
|
|
||||||
|
const { isLoading, error, refetch, data } = useQuery({
|
||||||
|
queryKey: ["search", query],
|
||||||
|
queryFn: () => {
|
||||||
|
if (query === null) return;
|
||||||
|
|
||||||
|
return client.getSearch(query);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const results = data ?? [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Container>
|
||||||
|
<SearchInput initialQueryValue={query ?? undefined} />
|
||||||
|
<Spacer y={4} />
|
||||||
|
{isLoading && <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>
|
||||||
|
)}
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
{results.length != 0 &&
|
||||||
|
results.map((result) => {
|
||||||
|
switch (result.type) {
|
||||||
|
case "channel":
|
||||||
|
return <Channel key={result.id} data={result} />;
|
||||||
|
|
||||||
|
case "video":
|
||||||
|
return <Video key={result.id} data={result} />;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -1,19 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { Search } from "@/components/Search";
|
|
||||||
import { Component } from "@/typings/component";
|
|
||||||
import { useSearchParams } from "next/navigation";
|
|
||||||
|
|
||||||
export const SearchPage: Component = () => {
|
|
||||||
const searchParams = useSearchParams();
|
|
||||||
|
|
||||||
const query = searchParams.get("search_query");
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="container mx-auto py-4">
|
|
||||||
<Search initialQueryValue={query || undefined} />
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
@ -0,0 +1,42 @@
|
|||||||
|
import { Component } from "@/typings/component";
|
||||||
|
import { VideoResult as VideoProps } from "@/client/typings/search";
|
||||||
|
import { Card, CardBody } from "@nextui-org/card";
|
||||||
|
import { Image } from "@nextui-org/image";
|
||||||
|
|
||||||
|
import NextImage from "next/image";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
export const Video: Component<{ data: VideoProps }> = ({ data }) => {
|
||||||
|
const url = `/watch?v=${data.id}`;
|
||||||
|
|
||||||
|
const videoSize = 200;
|
||||||
|
const aspectRatio = 16 / 9;
|
||||||
|
|
||||||
|
const [width, height] = useMemo(() => {
|
||||||
|
return [videoSize * aspectRatio, videoSize];
|
||||||
|
}, [videoSize]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link href={url}>
|
||||||
|
<Card>
|
||||||
|
<CardBody>
|
||||||
|
<div className="flex flex-row gap-4">
|
||||||
|
<Image
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
src={data.thumbnail}
|
||||||
|
alt={data.title}
|
||||||
|
as={NextImage}
|
||||||
|
unoptimized
|
||||||
|
className="aspect-video"
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<h1 className="text-xl">{data.title}</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,6 @@
|
|||||||
|
export interface SearchOptions {
|
||||||
|
page?: number;
|
||||||
|
type?: SearchType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SearchType = "video" | "playlist" | "channel" | "all";
|
@ -0,0 +1,21 @@
|
|||||||
|
import { Component } from "@/typings/component";
|
||||||
|
import { navHeight } from "./Nav";
|
||||||
|
|
||||||
|
export const Container: Component<{ navbarOffset?: boolean }> = ({
|
||||||
|
children,
|
||||||
|
navbarOffset = true
|
||||||
|
}) => {
|
||||||
|
let height;
|
||||||
|
|
||||||
|
if (navbarOffset) height = `calc(100vh - ${navHeight}px)`;
|
||||||
|
else height = "100vh";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{ minHeight: height }}
|
||||||
|
className="container mx-auto py-4 px-2 flex flex-col"
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
Loading…
Reference in new issue