[dyad] Added thumbnails to the clips overview page - wrote 3 file(s), executed 1 SQL queries
This commit is contained in:
@@ -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 });
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -118,16 +118,26 @@ 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,
|
||||||
|
|||||||
Reference in New Issue
Block a user