[dyad] Added thumbnails to the clips overview page - wrote 3 file(s), executed 1 SQL queries

This commit is contained in:
[dyad]
2026-01-30 09:06:50 +01:00
parent 957c9e3aaf
commit 3a3b1e5358
3 changed files with 38 additions and 5 deletions

View File

@@ -13,6 +13,7 @@ export type Clip = {
short_id: string; short_id: string;
title: string; title: string;
storage_path: string; storage_path: string;
thumbnail_storage_path: string;
created_at: string; created_at: string;
}; };
@@ -31,7 +32,7 @@ export default function AccountPage() {
const { data, error } = await supabase const { data, error } = await supabase
.from('clips') .from('clips')
.select('id, short_id, title, storage_path, created_at') .select('id, short_id, title, storage_path, thumbnail_storage_path, created_at')
.eq('user_id', user.id) .eq('user_id', user.id)
.order('created_at', { ascending: false }); .order('created_at', { ascending: false });

View File

@@ -15,6 +15,7 @@ import { formatDistanceToNow } from 'date-fns';
import { MoreVertical, Edit, Trash2, Loader2, Copy, Check } from 'lucide-react'; import { MoreVertical, Edit, Trash2, Loader2, Copy, Check } from 'lucide-react';
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';
import Link from 'next/link'; import Link from 'next/link';
import Image from 'next/image';
type UserClipsProps = { type UserClipsProps = {
clips: Clip[]; clips: Clip[];
@@ -61,7 +62,11 @@ export function UserClips({ clips, onClipDeleted, onClipUpdated }: UserClipsProp
const handleDeleteClip = async (clip: Clip) => { const handleDeleteClip = async (clip: Clip) => {
setIsDeleting(true); setIsDeleting(true);
try { try {
const { error: storageError } = await supabase.storage.from('clips').remove([clip.storage_path]); const pathsToRemove = [clip.storage_path];
if (clip.thumbnail_storage_path) {
pathsToRemove.push(clip.thumbnail_storage_path);
}
const { error: storageError } = await supabase.storage.from('clips').remove(pathsToRemove);
if (storageError) throw storageError; if (storageError) throw storageError;
const { error: dbError } = await supabase.from('clips').delete().eq('id', clip.id); const { error: dbError } = await supabase.from('clips').delete().eq('id', clip.id);
@@ -84,6 +89,11 @@ export function UserClips({ clips, onClipDeleted, onClipUpdated }: UserClipsProp
setTimeout(() => setHasCopied(null), 2000); setTimeout(() => setHasCopied(null), 2000);
}; };
const getThumbnailUrl = (path: string) => {
const { data } = supabase.storage.from('clips').getPublicUrl(path);
return data.publicUrl;
}
return ( return (
<> <>
<Toaster /> <Toaster />
@@ -120,8 +130,20 @@ export function UserClips({ clips, onClipDeleted, onClipUpdated }: UserClipsProp
</CardHeader> </CardHeader>
<CardContent className="flex-grow"> <CardContent className="flex-grow">
<Link href={`/clips/${clip.short_id}`}> <Link href={`/clips/${clip.short_id}`}>
<div className="aspect-video w-full bg-muted rounded-md flex items-center justify-center text-muted-foreground"> <div className="aspect-video w-full bg-muted rounded-md overflow-hidden relative">
Click to view {clip.thumbnail_storage_path ? (
<Image
src={getThumbnailUrl(clip.thumbnail_storage_path)}
alt={clip.title || 'Clip thumbnail'}
layout="fill"
objectFit="cover"
className="transition-transform duration-300 hover:scale-105"
/>
) : (
<div className="w-full h-full flex items-center justify-center text-muted-foreground">
No thumbnail
</div>
)}
</div> </div>
</Link> </Link>
</CardContent> </CardContent>
@@ -139,7 +161,7 @@ export function UserClips({ clips, onClipDeleted, onClipUpdated }: UserClipsProp
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle>Are you sure?</AlertDialogTitle> <AlertDialogTitle>Are you sure?</AlertDialogTitle>
<AlertDialogDescription> <AlertDialogDescription>
This action cannot be undone. This will permanently delete your clip and remove its data from our servers. This action cannot be undone. This will permanently delete your clip and its thumbnail from our servers.
</AlertDialogDescription> </AlertDialogDescription>
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogFooter> <AlertDialogFooter>

View File

@@ -119,15 +119,25 @@ export function VideoEditor() {
const data = await ffmpeg.readFile('output.mp4'); const data = await ffmpeg.readFile('output.mp4');
const trimmedBlob = new Blob([(data as any).buffer], { type: 'video/mp4' }); const trimmedBlob = new Blob([(data as any).buffer], { type: 'video/mp4' });
// Generate thumbnail
await ffmpeg.exec(['-i', 'output.mp4', '-ss', '00:00:00.001', '-vframes', '1', 'thumbnail.jpg']);
const thumbnailData = await ffmpeg.readFile('thumbnail.jpg');
const thumbnailBlob = new Blob([(thumbnailData as any).buffer], { type: 'image/jpeg' });
const shortId = Math.random().toString(36).substring(2, 8); const shortId = Math.random().toString(36).substring(2, 8);
const storagePath = `${user.id}/${shortId}-${videoFile.name}`; const storagePath = `${user.id}/${shortId}-${videoFile.name}`;
const thumbnailStoragePath = `${user.id}/${shortId}-thumbnail.jpg`;
const { error: uploadError } = await supabase.storage.from('clips').upload(storagePath, trimmedBlob); const { error: uploadError } = await supabase.storage.from('clips').upload(storagePath, trimmedBlob);
if (uploadError) throw new Error(`Storage Error: ${uploadError.message}`); if (uploadError) throw new Error(`Storage Error: ${uploadError.message}`);
const { error: thumbnailUploadError } = await supabase.storage.from('clips').upload(thumbnailStoragePath, thumbnailBlob);
if (thumbnailUploadError) throw new Error(`Thumbnail Upload Error: ${thumbnailUploadError.message}`);
const { error: dbError } = await supabase.from('clips').insert({ const { error: dbError } = await supabase.from('clips').insert({
user_id: user.id, user_id: user.id,
storage_path: storagePath, storage_path: storagePath,
thumbnail_storage_path: thumbnailStoragePath,
short_id: shortId, short_id: shortId,
original_file_name: videoFile.name, original_file_name: videoFile.name,
title: videoTitle, title: videoTitle,