Skip to content

Commit 3917cfc

Browse files
committed
feat: add video feed dialog to library grid
1 parent e4da8c0 commit 3917cfc

2 files changed

Lines changed: 84 additions & 12 deletions

File tree

frontend/src/components/library/video-card.tsx

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
'use client';
22

33
import { useState } from 'react';
4-
import Link from 'next/link';
54
import { Play, Download, Clock, FileVideo, MoreVertical, Pencil, Trash2 } from 'lucide-react';
65
import { useQueryClient } from '@tanstack/react-query';
76
import { Card } from '@/components/ui/card';
@@ -42,6 +41,7 @@ import type { Video } from '@/lib/types/video';
4241

4342
interface VideoCardProps {
4443
video: Video;
44+
onWatch?: (videoId: string) => void;
4545
}
4646

4747
function parseFilename(filename: string) {
@@ -50,7 +50,7 @@ function parseFilename(filename: string) {
5050
return { baseName: filename.slice(0, lastDot), ext: filename.slice(lastDot) };
5151
}
5252

53-
export function VideoCard({ video }: VideoCardProps) {
53+
export function VideoCard({ video, onWatch }: VideoCardProps) {
5454
const queryClient = useQueryClient();
5555
const isPlayable = video.status !== 'UPLOADED';
5656

@@ -100,7 +100,11 @@ export function VideoCard({ video }: VideoCardProps) {
100100
<Card className="group overflow-hidden hover:-translate-y-1 hover:shadow-md">
101101
<div className="relative aspect-video bg-muted">
102102
{isPlayable ? (
103-
<Link href={`/watch/${video.id}`} className="block h-full">
103+
<button
104+
type="button"
105+
className="block h-full w-full text-left"
106+
onClick={() => onWatch?.(video.id)}
107+
>
104108
<video
105109
src={getStreamUrl(video.id)}
106110
className="h-full w-full object-cover"
@@ -121,7 +125,7 @@ export function VideoCard({ video }: VideoCardProps) {
121125
<Play className="h-6 w-6 text-foreground" fill="currentColor" />
122126
</div>
123127
</div>
124-
</Link>
128+
</button>
125129
) : (
126130
<div className="flex h-full items-center justify-center">
127131
<FileVideo className="h-10 w-10 text-muted-foreground/40" />
@@ -181,12 +185,15 @@ export function VideoCard({ video }: VideoCardProps) {
181185

182186
{isPlayable && (
183187
<div className="mt-3 flex gap-2">
184-
<Link href={`/watch/${video.id}`} className="flex-1">
185-
<Button variant="outline" size="sm" className="w-full gap-1.5">
186-
<Play className="h-3.5 w-3.5" />
187-
Watch
188-
</Button>
189-
</Link>
188+
<Button
189+
variant="outline"
190+
size="sm"
191+
className="flex-1 gap-1.5"
192+
onClick={() => onWatch?.(video.id)}
193+
>
194+
<Play className="h-3.5 w-3.5" />
195+
Watch
196+
</Button>
190197
{video.status === 'READY' && (
191198
<a href={getDownloadUrl(video.id)} download>
192199
<Button variant="ghost" size="sm" className="gap-1.5">

frontend/src/components/library/video-grid.tsx

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,23 @@
11
'use client';
22

3-
import { useState, useEffect, useRef } from 'react';
3+
import { useState, useEffect, useRef, useMemo, useCallback } from 'react';
44
import { Search } from 'lucide-react';
55
import { VideoCard } from './video-card';
66
import { EmptyState } from './empty-state';
7+
import { VideoFeed } from '@/components/feed/video-feed';
78
import { Skeleton } from '@/components/ui/skeleton';
89
import { Button } from '@/components/ui/button';
10+
import {
11+
Dialog,
12+
DialogPortal,
13+
DialogOverlay,
14+
DialogClose,
15+
} from '@/components/ui/dialog';
16+
import * as DialogPrimitive from '@radix-ui/react-dialog';
17+
import { X } from 'lucide-react';
918
import { useVideos } from '@/hooks/use-videos';
19+
import { getVideos } from '@/lib/api/videos';
20+
import type { Video } from '@/lib/types/video';
1021

1122
const STATUS_OPTIONS = [
1223
{ label: 'All', value: '' },
@@ -21,6 +32,7 @@ export function VideoGrid() {
2132
const [searchInput, setSearchInput] = useState('');
2233
const [debouncedSearch, setDebouncedSearch] = useState('');
2334
const [statusFilter, setStatusFilter] = useState('');
35+
const [watchVideoId, setWatchVideoId] = useState<string | null>(null);
2436
const debounceRef = useRef<ReturnType<typeof setTimeout>>(undefined);
2537

2638
useEffect(() => {
@@ -42,6 +54,29 @@ export function VideoGrid() {
4254
const total = data?.total ?? 0;
4355
const totalPages = Math.ceil(total / 12);
4456

57+
const feedVideos = useMemo(
58+
() => videos.filter((v) => v.status === 'READY'),
59+
[videos],
60+
);
61+
62+
const feedPageRef = useRef(page);
63+
feedPageRef.current = page;
64+
65+
const handleLoadMore = useCallback(async (): Promise<Video[]> => {
66+
feedPageRef.current += 1;
67+
try {
68+
const result = await getVideos(
69+
feedPageRef.current,
70+
12,
71+
debouncedSearch || undefined,
72+
statusFilter || undefined,
73+
);
74+
return result.data.filter((v) => v.status === 'READY');
75+
} catch {
76+
return [];
77+
}
78+
}, [debouncedSearch, statusFilter]);
79+
4580
return (
4681
<div className="space-y-6">
4782
<div className="flex flex-col gap-4 sm:flex-row">
@@ -105,7 +140,7 @@ export function VideoGrid() {
105140
key={video.id}
106141
style={{ animationDelay: `${index * 50}ms` }}
107142
>
108-
<VideoCard video={video} />
143+
<VideoCard video={video} onWatch={setWatchVideoId} />
109144
</div>
110145
))}
111146
</div>
@@ -135,6 +170,36 @@ export function VideoGrid() {
135170
)}
136171
</>
137172
)}
173+
174+
<Dialog
175+
open={watchVideoId !== null}
176+
onOpenChange={(open) => { if (!open) setWatchVideoId(null); }}
177+
>
178+
<DialogPortal>
179+
<DialogOverlay className="bg-black/20 backdrop-blur-[24px] saturate-[180%]" />
180+
<DialogPrimitive.Content
181+
className="fixed inset-0 z-50 flex items-center justify-center p-4 focus:outline-none"
182+
aria-describedby={undefined}
183+
>
184+
<DialogPrimitive.Title className="sr-only">
185+
Video Feed
186+
</DialogPrimitive.Title>
187+
<div className="relative h-full max-h-[85vh] w-full max-w-2xl overflow-hidden rounded-[0.625rem]">
188+
{watchVideoId && (
189+
<VideoFeed
190+
initialVideoId={watchVideoId}
191+
videos={feedVideos}
192+
onLoadMore={handleLoadMore}
193+
/>
194+
)}
195+
</div>
196+
<DialogClose className="absolute right-6 top-6 z-50 flex h-10 w-10 items-center justify-center rounded-full bg-black/50 text-white transition-colors hover:bg-black/70">
197+
<X className="h-5 w-5" />
198+
<span className="sr-only">Close</span>
199+
</DialogClose>
200+
</DialogPrimitive.Content>
201+
</DialogPortal>
202+
</Dialog>
138203
</div>
139204
);
140205
}

0 commit comments

Comments
 (0)