search: added playlist card
continuous-integration/drone/push Build is failing Details

nextui
Guus van Meerveld 8 months ago
parent b1be90d190
commit c850857768

@ -1,3 +1,5 @@
"use client";
import { Component } from "@/typings/component"; import { Component } from "@/typings/component";
import { ChannelResult as ChannelProps } from "@/client/typings/search"; import { ChannelResult as ChannelProps } from "@/client/typings/search";

@ -0,0 +1,71 @@
"use client";
import { PlaylistResult as PlaylistProps } from "@/client/typings/search";
import { Component } from "@/typings/component";
import { Card, CardBody } from "@nextui-org/card";
import { Image } from "@nextui-org/image";
import NextLink from "next/link";
import NextImage from "next/image";
import { useMemo } from "react";
import { Link } from "@nextui-org/link";
export const Playlist: Component<{ data: PlaylistProps }> = ({ data }) => {
const url = `/playlist/${data.id}`;
const channelUrl = `/channel/${data.author.id}`;
const videoSize = 200;
const aspectRatio = 16 / 9;
const [width, height] = useMemo(() => {
return [videoSize * aspectRatio, videoSize];
}, [videoSize]);
return (
<NextLink href={url}>
<Card>
<CardBody>
<div className="flex flex-col lg:flex-row gap-4">
<div className="relative">
<Image
width={width}
height={height}
className="object-contain"
src={data.thumbnail}
alt={data.title}
as={NextImage}
unoptimized
/>
<p className="text-small rounded-md z-10 absolute bottom-2 right-2 bg-content2 p-1">
{data.numberOfVideos} videos
</p>
</div>
<div className="flex flex-col gap-2">
<div>
<h1 className="text-xl">{data.title}</h1>
<Link
as={NextLink}
href={channelUrl}
className="flex flex-row gap-2 items-center"
>
<h1 className="text-lg text-default-600">
{data.author.name}
</h1>
</Link>
</div>
{data.videos && (
<div className="flex flex-col gap-1">
{data.videos.map((video) => {
return <h1>{video.title}</h1>;
})}
</div>
)}
</div>
</div>
</CardBody>
</Card>
</NextLink>
);
};

@ -11,6 +11,7 @@ import { Container } from "@/components/Container";
import { LoadingPage } from "@/components/LoadingPage"; import { LoadingPage } from "@/components/LoadingPage";
import { Button } from "@nextui-org/button"; import { Button } from "@nextui-org/button";
import { Video } from "./Video"; import { Video } from "./Video";
import { Playlist } from "./Playlist";
export const Search: Component = () => { export const Search: Component = () => {
const searchParams = useSearchParams(); const searchParams = useSearchParams();
@ -60,8 +61,8 @@ export const Search: Component = () => {
case "video": case "video":
return <Video key={result.id} data={result} />; return <Video key={result.id} data={result} />;
default: case "playlist":
break; return <Playlist key={result.id} data={result} />;
} }
})} })}
</div> </div>

@ -1,3 +1,5 @@
"use client";
import { Component } from "@/typings/component"; import { Component } from "@/typings/component";
import { VideoResult as VideoProps } from "@/client/typings/search"; import { VideoResult as VideoProps } from "@/client/typings/search";
import { Card, CardBody } from "@nextui-org/card"; import { Card, CardBody } from "@nextui-org/card";

@ -18,6 +18,7 @@ export default class Transformer {
): string | null { ): string | null {
const thumbnail = thumbnails.find( const thumbnail = thumbnails.find(
(thumbnail) => (thumbnail) =>
thumbnail.quality == "maxresdefault" ||
thumbnail.quality == "default" || thumbnail.quality == "default" ||
thumbnail.quality == "medium" || thumbnail.quality == "medium" ||
thumbnail.quality == "middle" thumbnail.quality == "middle"
@ -88,7 +89,24 @@ export default class Transformer {
id: result.authorId id: result.authorId
}, },
id: result.playlistId, id: result.playlistId,
numberOfVideos: result.videoCount numberOfVideos: result.videoCount,
thumbnail: result.playlistThumbnail,
videos: result.videos.map((video) => {
const thumbnail = Transformer.findBestThumbnail(
video.videoThumbnails
);
if (thumbnail === null)
throw new Error(
`Invidious: Missing thumbnail for video with id ${video.videoId}`
);
return {
title: video.title,
id: video.videoId,
duration: video.lengthSeconds * 1000,
thumbnail: thumbnail
};
})
}; };
return playlist; return playlist;

@ -37,16 +37,18 @@ export const PlaylistResultModel = z.object({
authorUrl: z.string(), authorUrl: z.string(),
authorVerified: z.boolean(), authorVerified: z.boolean(),
videoCount: z.number(), videoCount: z.number(),
videos: z.object({ videos: z
title: z.string(), .object({
videoId: z.string(), title: z.string(),
lengthSeconds: z.number(), videoId: z.string(),
videoThumbnails: ThumbnailModel.array() lengthSeconds: z.number(),
}) videoThumbnails: ThumbnailModel.array()
})
.array()
}); });
export const SearchModel = z export const SearchModel = z
.union([VideoResultModel, ChannelResultModel, PlaylistResultModel]) .union([PlaylistResultModel, VideoResultModel, ChannelResultModel])
.array(); .array();
type Search = z.infer<typeof SearchModel>; type Search = z.infer<typeof SearchModel>;

@ -84,6 +84,7 @@ export default class Transformer {
name: result.uploaderName, name: result.uploaderName,
id: channelId id: channelId
}, },
thumbnail: result.thumbnail,
id: result.url, id: result.url,
numberOfVideos: result.videos numberOfVideos: result.videos
}; };

@ -21,6 +21,13 @@ export interface PlaylistResult {
id: string; id: string;
}; };
numberOfVideos: number; numberOfVideos: number;
thumbnail: string;
videos?: {
title: string;
id: string;
duration: number;
thumbnail: string;
}[];
} }
export type SearchResults = (VideoResult | ChannelResult | PlaylistResult)[]; export type SearchResults = (VideoResult | ChannelResult | PlaylistResult)[];

@ -46,7 +46,7 @@ export const Search: Component<{ initialQueryValue?: string }> = ({
return ( return (
<form onSubmit={() => submit(searchQuery)}> <form onSubmit={() => submit(searchQuery)}>
<Autocomplete <Autocomplete
isClearable isClearable={false}
name="search_query" name="search_query"
value={searchQuery} value={searchQuery}
isLoading={isLoading} isLoading={isLoading}

Loading…
Cancel
Save