[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;
title: string;
storage_path: string;
thumbnail_storage_path: string;
created_at: string;
};
@@ -31,7 +32,7 @@ export default function AccountPage() {
const { data, error } = await supabase
.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)
.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 { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';
import Link from 'next/link';
import Image from 'next/image';
type UserClipsProps = {
clips: Clip[];
@@ -61,7 +62,11 @@ export function UserClips({ clips, onClipDeleted, onClipUpdated }: UserClipsProp
const handleDeleteClip = async (clip: Clip) => {
setIsDeleting(true);
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;
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);
};
const getThumbnailUrl = (path: string) => {
const { data } = supabase.storage.from('clips').getPublicUrl(path);
return data.publicUrl;
}
return (
<>
<Toaster />
@@ -120,8 +130,20 @@ export function UserClips({ clips, onClipDeleted, onClipUpdated }: UserClipsProp
</CardHeader>
<CardContent className="flex-grow">
<Link href={`/clips/${clip.short_id}`}>
<div className="aspect-video w-full bg-muted rounded-md flex items-center justify-center text-muted-foreground">
Click to view
<div className="aspect-video w-full bg-muted rounded-md overflow-hidden relative">
{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>
</Link>
</CardContent>
@@ -139,7 +161,7 @@ export function UserClips({ clips, onClipDeleted, onClipUpdated }: UserClipsProp
<AlertDialogHeader>
<AlertDialogTitle>Are you sure?</AlertDialogTitle>
<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>
</AlertDialogHeader>
<AlertDialogFooter>

View File

@@ -119,15 +119,25 @@ export function VideoEditor() {
const data = await ffmpeg.readFile('output.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 storagePath = `${user.id}/${shortId}-${videoFile.name}`;
const thumbnailStoragePath = `${user.id}/${shortId}-thumbnail.jpg`;
const { error: uploadError } = await supabase.storage.from('clips').upload(storagePath, trimmedBlob);
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({
user_id: user.id,
storage_path: storagePath,
thumbnail_storage_path: thumbnailStoragePath,
short_id: shortId,
original_file_name: videoFile.name,
title: videoTitle,