feat(video-search): add video search
This commit is contained in:
parent
bb9a2f538d
commit
6e304e7051
6 changed files with 336 additions and 7 deletions
190
ui/components/SearchVideos.tsx
Normal file
190
ui/components/SearchVideos.tsx
Normal file
|
@ -0,0 +1,190 @@
|
|||
/* eslint-disable @next/next/no-img-element */
|
||||
import { PlayCircle, PlayIcon, PlusIcon, VideoIcon } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import Lightbox, { GenericSlide, VideoSlide } from 'yet-another-react-lightbox';
|
||||
import 'yet-another-react-lightbox/styles.css';
|
||||
import { Message } from './ChatWindow';
|
||||
|
||||
type Video = {
|
||||
url: string;
|
||||
img_src: string;
|
||||
title: string;
|
||||
iframe_src: string;
|
||||
};
|
||||
|
||||
declare module 'yet-another-react-lightbox' {
|
||||
export interface VideoSlide extends GenericSlide {
|
||||
type: 'video-slide';
|
||||
src: string;
|
||||
iframe_src: string;
|
||||
}
|
||||
|
||||
interface SlideTypes {
|
||||
'video-slide': VideoSlide;
|
||||
}
|
||||
}
|
||||
|
||||
const Searchvideos = ({
|
||||
query,
|
||||
chat_history,
|
||||
}: {
|
||||
query: string;
|
||||
chat_history: Message[];
|
||||
}) => {
|
||||
const [videos, setVideos] = useState<Video[] | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [slides, setSlides] = useState<VideoSlide[]>([]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{!loading && videos === null && (
|
||||
<button
|
||||
onClick={async () => {
|
||||
setLoading(true);
|
||||
const res = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_API_URL}/videos`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query: query,
|
||||
chat_history: chat_history,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
const videos = data.videos;
|
||||
setVideos(videos);
|
||||
setSlides(
|
||||
videos.map((video: Video) => {
|
||||
return {
|
||||
type: 'video-slide',
|
||||
iframe_src: video.iframe_src,
|
||||
src: video.img_src,
|
||||
};
|
||||
}),
|
||||
);
|
||||
setLoading(false);
|
||||
}}
|
||||
className="border border-dashed border-[#1C1C1C] hover:bg-[#1c1c1c] active:scale-95 duration-200 transition px-4 py-2 flex flex-row items-center justify-between rounded-lg text-white text-sm w-full"
|
||||
>
|
||||
<div className="flex flex-row items-center space-x-2">
|
||||
<VideoIcon size={17} />
|
||||
<p>Search videos</p>
|
||||
</div>
|
||||
<PlusIcon className="text-[#24A0ED]" size={17} />
|
||||
</button>
|
||||
)}
|
||||
{loading && (
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{[...Array(4)].map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="bg-[#1C1C1C] h-32 w-full rounded-lg animate-pulse aspect-video object-cover"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{videos !== null && videos.length > 0 && (
|
||||
<>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{videos.length > 4
|
||||
? videos.slice(0, 3).map((video, i) => (
|
||||
<div
|
||||
onClick={() => {
|
||||
setOpen(true);
|
||||
setSlides([
|
||||
slides[i],
|
||||
...slides.slice(0, i),
|
||||
...slides.slice(i + 1),
|
||||
]);
|
||||
}}
|
||||
className="relative transition duration-200 active:scale-95 hover:scale-[1.02] cursor-pointer"
|
||||
key={i}
|
||||
>
|
||||
<img
|
||||
src={video.img_src}
|
||||
alt={video.title}
|
||||
className="relative h-full w-full aspect-video object-cover rounded-lg"
|
||||
/>
|
||||
<div className="absolute bg-black/70 text-white/70 px-2 py-1 flex flex-row items-center space-x-1 bottom-1 right-1 rounded-md">
|
||||
<PlayCircle size={15} />
|
||||
<p className="text-xs">Video</p>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
: videos.map((video, i) => (
|
||||
<div
|
||||
onClick={() => {
|
||||
setOpen(true);
|
||||
setSlides([
|
||||
slides[i],
|
||||
...slides.slice(0, i),
|
||||
...slides.slice(i + 1),
|
||||
]);
|
||||
}}
|
||||
className="relative transition duration-200 active:scale-95 hover:scale-[1.02] cursor-pointer"
|
||||
key={i}
|
||||
>
|
||||
<img
|
||||
src={video.img_src}
|
||||
alt={video.title}
|
||||
className="relative h-full w-full aspect-video object-cover rounded-lg"
|
||||
/>
|
||||
<div className="absolute bg-black/70 text-white/70 px-2 py-1 flex flex-row items-center space-x-1 bottom-1 right-1 rounded-md">
|
||||
<PlayCircle size={15} />
|
||||
<p className="text-xs">Video</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{videos.length > 4 && (
|
||||
<button
|
||||
onClick={() => setOpen(true)}
|
||||
className="bg-[#111111] hover:bg-[#1c1c1c] transition duration-200 active:scale-95 hover:scale-[1.02] h-auto w-full rounded-lg flex flex-col justify-between text-white p-2"
|
||||
>
|
||||
<div className="flex flex-row items-center space-x-1">
|
||||
{videos.slice(3, 6).map((video, i) => (
|
||||
<img
|
||||
key={i}
|
||||
src={video.img_src}
|
||||
alt={video.title}
|
||||
className="h-6 w-12 rounded-md lg:h-3 lg:w-6 lg:rounded-sm aspect-video object-cover"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-white/70 text-xs">
|
||||
View {videos.length - 3} more
|
||||
</p>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<Lightbox
|
||||
open={open}
|
||||
close={() => setOpen(false)}
|
||||
slides={slides}
|
||||
render={{
|
||||
slide: ({ slide }) =>
|
||||
slide.type === 'video-slide' ? (
|
||||
<div className="h-full w-full flex flex-row items-center justify-center">
|
||||
<iframe
|
||||
src={slide.iframe_src}
|
||||
className="aspect-video max-h-[95vh] w-[95vw] rounded-2xl md:w-[80vw]"
|
||||
allowFullScreen
|
||||
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
|
||||
/>
|
||||
</div>
|
||||
) : null,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Searchvideos;
|
Loading…
Add table
Add a link
Reference in a new issue