added video and channel cards to search page
continuous-integration/drone/push Build is passing Details

nextui
Guus van Meerveld 8 months ago
parent 5ac329296e
commit b1be90d190

@ -23,14 +23,17 @@ export const Channel: Component<{ data: ChannelProps }> = ({ data }) => {
src={data.thumbnail} src={data.thumbnail}
alt={data.name} alt={data.name}
as={NextImage} as={NextImage}
className="rounded-full"
unoptimized unoptimized
/> />
<div className="flex-1 flex flex-col"> <div className="flex-1 flex flex-col justify-center">
<h1 className="text-lg">{data.name}</h1> <h1 className="text-lg">{data.name}</h1>
<div className="flex flex-row gap-4 items-center font-semibold text-default-600"> <div className="flex flex-row gap-4 items-center font-semibold text-default-600">
<h1>Subscribers: {formatViewCount(data.subscribers)}</h1> <h1>{formatViewCount(data.subscribers)} subscribers</h1>
<h1>Videos: {formatViewCount(data.videos)}</h1> {data.videos !== 0 && (
<h1>{formatViewCount(data.videos)} videos</h1>
)}
</div> </div>
<p className="text-default-600">{data.description}</p> <p className="text-default-600">{data.description}</p>
</div> </div>

@ -5,10 +5,15 @@ import { Image } from "@nextui-org/image";
import NextImage from "next/image"; import NextImage from "next/image";
import { useMemo } from "react"; import { useMemo } from "react";
import Link from "next/link"; import formatViewCount from "@/utils/formatViewCount";
import formatUploadedTime from "@/utils/formatUploadedTime";
import { Link } from "@nextui-org/link";
import NextLink from "next/link";
import formatDuration from "@/utils/formatDuration";
export const Video: Component<{ data: VideoProps }> = ({ data }) => { export const Video: Component<{ data: VideoProps }> = ({ data }) => {
const url = `/watch?v=${data.id}`; const url = `/watch?v=${data.id}`;
const channelUrl = `/channel/${data.author.id}`;
const videoSize = 200; const videoSize = 200;
const aspectRatio = 16 / 9; const aspectRatio = 16 / 9;
@ -18,25 +23,57 @@ export const Video: Component<{ data: VideoProps }> = ({ data }) => {
}, [videoSize]); }, [videoSize]);
return ( return (
<Link href={url}> <NextLink href={url}>
<Card> <Card>
<CardBody> <CardBody>
<div className="flex flex-row gap-4"> <div className="flex flex-row gap-4">
<Image <div className="relative">
width={width} <Image
height={height} width={width}
src={data.thumbnail} height={height}
alt={data.title} src={data.thumbnail}
as={NextImage} alt={data.title}
unoptimized as={NextImage}
className="aspect-video" unoptimized
/> />
<p className="text-small rounded-md z-10 absolute bottom-2 right-2 bg-content2 p-1">
{formatDuration(data.duration)}
</p>
{data.live && (
<p className="text-small rounded-md z-10 absolute bottom-2 left-2 bg-danger p-1">
LIVE
</p>
)}
</div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<h1 className="text-xl">{data.title}</h1> <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>{formatUploadedTime(data.uploaded)}</h1>
</div>
<Link
as={NextLink}
href={channelUrl}
className="flex flex-row gap-2 items-center"
>
{data.author.avatar && (
<Image
width={64}
height={64}
src={data.author.avatar}
alt={data.author.name}
as={NextImage}
unoptimized
/>
)}
<h1 className="text-lg text-default-600">{data.author.name}</h1>
</Link>
<p className="text-default-600">{data.description}</p>
</div> </div>
</div> </div>
</CardBody> </CardBody>
</Card> </Card>
</Link> </NextLink>
); );
}; };

@ -32,7 +32,11 @@ export default class Transformer {
title: data.title, title: data.title,
description: "", description: "",
live: false, live: false,
author: { id: channelId, name: data.uploaderName } author: {
id: channelId,
name: data.uploaderName,
avatar: data.uploaderAvatar
}
}; };
} }

@ -4,6 +4,7 @@ export interface Video {
author: { author: {
name: string; name: string;
id: string; id: string;
avatar?: string;
}; };
thumbnail: string; thumbnail: string;
description: string; description: string;

@ -23,13 +23,16 @@ export const Search: Component<{ initialQueryValue?: string }> = ({
const { isLoading, error, data } = useQuery({ const { isLoading, error, data } = useQuery({
queryKey: ["search", "suggestions", searchQueryDebounced], queryKey: ["search", "suggestions", searchQueryDebounced],
queryFn: () => client.getSearchSuggestions(searchQueryDebounced), queryFn: () => {
enabled: searchQueryDebounced.length !== 0 if (searchQueryDebounced.length === 0) return [];
return client.getSearchSuggestions(searchQueryDebounced);
}
}); });
const handleSubmit = useCallback(() => { const submit = useCallback((query: string) => {
router.push(`/results?search_query=${searchQuery}`); router.push(`/results?search_query=${query}`);
}, [searchQuery]); }, []);
const suggestions = useMemo( const suggestions = useMemo(
() => () =>
@ -41,21 +44,26 @@ export const Search: Component<{ initialQueryValue?: string }> = ({
); );
return ( return (
<form onSubmit={handleSubmit}> <form onSubmit={() => submit(searchQuery)}>
<Autocomplete <Autocomplete
isClearable isClearable
name="search-bar" name="search_query"
value={searchQuery} value={searchQuery}
isLoading={isLoading} isLoading={isLoading}
defaultInputValue={initialQueryValue} defaultInputValue={initialQueryValue}
onValueChange={setSearchQuery} onValueChange={setSearchQuery}
onKeyDown={(e) => {
if (e.key === "Enter") {
submit(searchQuery);
}
}}
startContent={<SearchIcon className="text-xl" />} startContent={<SearchIcon className="text-xl" />}
defaultItems={suggestions} defaultItems={suggestions}
onSelectionChange={(key) => { onSelectionChange={(key) => {
if (key === null) return; if (key === null) return;
setSearchQuery(key.toString()); setSearchQuery(key.toString());
handleSubmit(); submit(key.toString());
}} }}
errorMessage={error !== null ? error.toString() : ""} errorMessage={error !== null ? error.toString() : ""}
isInvalid={error !== null} isInvalid={error !== null}

@ -7,8 +7,8 @@ export const useClient = () => {
const [client] = useState( const [client] = useState(
() => () =>
new Client([ new Client([
// { baseUrl: "https://invidious.drgns.space", type: ApiType.Invidious } { baseUrl: "https://invidious.drgns.space", type: ApiType.Invidious }
{ baseUrl: "https://pipedapi.kavin.rocks", type: ApiType.Piped } // { baseUrl: "https://pipedapi.kavin.rocks", type: ApiType.Piped }
]) ])
); );

@ -2,7 +2,7 @@ import { Duration } from "luxon";
const formatDuration = (ms: number): string => { const formatDuration = (ms: number): string => {
if (ms / (60 * 60 * 1000) >= 1) if (ms / (60 * 60 * 1000) >= 1)
return Duration.fromMillis(ms).toFormat("HH:mm:ss"); return Duration.fromMillis(ms).toFormat("hh:mm:ss");
else return Duration.fromMillis(ms).toFormat("mm:ss"); else return Duration.fromMillis(ms).toFormat("mm:ss");
}; };

Loading…
Cancel
Save