mirror of
https://github.com/yusufipk/OpenFrame.git
synced 2026-09-11 17:46:06 +00:00
feat(video-card): show deleting overlay and disable interactions during delete
This commit is contained in:
+106
-90
@@ -182,99 +182,115 @@ export function VideoCard({ video, projectId, onDeleted }: VideoCardProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Card className="group overflow-hidden transition-colors hover:bg-accent/50 cursor-pointer">
|
<div className="relative">
|
||||||
<Link href={`/projects/${projectId}/videos/${video.id}`}>
|
<Card
|
||||||
{/* Thumbnail */}
|
className={`group overflow-hidden transition-colors hover:bg-accent/50 cursor-pointer ${
|
||||||
<div className="relative aspect-video bg-muted overflow-hidden">
|
isDeleting ? 'pointer-events-none opacity-70' : ''
|
||||||
{imgError ? (
|
}`}
|
||||||
<div className="absolute inset-0 flex flex-col items-center justify-center bg-muted/80">
|
>
|
||||||
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground mb-2" />
|
<Link href={`/projects/${projectId}/videos/${video.id}`}>
|
||||||
<span className="text-xs text-muted-foreground font-medium">Processing...</span>
|
{/* Thumbnail */}
|
||||||
</div>
|
<div className="relative aspect-video bg-muted overflow-hidden">
|
||||||
) : (
|
{imgError ? (
|
||||||
// eslint-disable-next-line @next/next/no-img-element
|
<div className="absolute inset-0 flex flex-col items-center justify-center bg-muted/80">
|
||||||
<img
|
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground mb-2" />
|
||||||
src={`${video.thumbnailUrl?.replace('vz-thumbnail.b-cdn.net', 'vz-965f4f4a-fc1.b-cdn.net')}${retryKey ? `?t=${retryKey}` : ''}`}
|
<span className="text-xs text-muted-foreground font-medium">Processing...</span>
|
||||||
alt={video.title}
|
</div>
|
||||||
className="absolute inset-0 w-full h-full object-cover transition-transform group-hover:scale-105"
|
) : (
|
||||||
onError={() => {
|
// eslint-disable-next-line @next/next/no-img-element
|
||||||
setImgError(true);
|
<img
|
||||||
// Check again after 10 seconds in case Bunny is still processing
|
src={`${video.thumbnailUrl?.replace('vz-thumbnail.b-cdn.net', 'vz-965f4f4a-fc1.b-cdn.net')}${retryKey ? `?t=${retryKey}` : ''}`}
|
||||||
setTimeout(() => {
|
alt={video.title}
|
||||||
setRetryKey(Date.now());
|
className="absolute inset-0 w-full h-full object-cover transition-transform group-hover:scale-105"
|
||||||
setImgError(false);
|
onError={() => {
|
||||||
}, 10000);
|
setImgError(true);
|
||||||
}}
|
// Check again after 10 seconds in case Bunny is still processing
|
||||||
/>
|
setTimeout(() => {
|
||||||
)}
|
setRetryKey(Date.now());
|
||||||
{!imgError && (
|
setImgError(false);
|
||||||
<div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
|
}, 10000);
|
||||||
<Play className="h-12 w-12 text-white" fill="white" />
|
}}
|
||||||
</div>
|
/>
|
||||||
)}
|
)}
|
||||||
<Badge className="absolute bottom-2 right-2 bg-black/70">{video.duration}</Badge>
|
{!imgError && (
|
||||||
</div>
|
<div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
|
||||||
</Link>
|
<Play className="h-12 w-12 text-white" fill="white" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<Badge className="absolute bottom-2 right-2 bg-black/70">{video.duration}</Badge>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
|
||||||
<CardContent className="p-4">
|
<CardContent className="p-4">
|
||||||
<div className="flex items-start justify-between gap-2">
|
<div className="flex items-start justify-between gap-2">
|
||||||
<Link href={`/projects/${projectId}/videos/${video.id}`} className="min-w-0 flex-1">
|
<Link href={`/projects/${projectId}/videos/${video.id}`} className="min-w-0 flex-1">
|
||||||
<h3 className="font-medium truncate">{video.title}</h3>
|
<h3 className="font-medium truncate">{video.title}</h3>
|
||||||
<div className="flex items-center gap-3 mt-1 text-sm text-muted-foreground">
|
<div className="flex items-center gap-3 mt-1 text-sm text-muted-foreground">
|
||||||
<span className="flex items-center gap-1">
|
<span className="flex items-center gap-1">
|
||||||
<Badge variant="secondary" className="text-xs">
|
<Badge variant="secondary" className="text-xs">
|
||||||
v{video.currentVersion}
|
v{video.currentVersion}
|
||||||
</Badge>
|
</Badge>
|
||||||
</span>
|
</span>
|
||||||
<span className="flex items-center gap-1">
|
<span className="flex items-center gap-1">
|
||||||
<MessageSquare className="h-3.5 w-3.5" />
|
<MessageSquare className="h-3.5 w-3.5" />
|
||||||
{video.commentCount}
|
{video.commentCount}
|
||||||
</span>
|
</span>
|
||||||
<span className="flex items-center gap-1">
|
<span className="flex items-center gap-1">
|
||||||
<Clock className="h-3.5 w-3.5" />
|
<Clock className="h-3.5 w-3.5" />
|
||||||
{video.lastUpdated}
|
{video.lastUpdated}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="h-8 w-8 shrink-0"
|
className="h-8 w-8 shrink-0"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
disabled={isDeleting}
|
||||||
<MoreVertical className="h-4 w-4" />
|
>
|
||||||
</Button>
|
<MoreVertical className="h-4 w-4" />
|
||||||
</DropdownMenuTrigger>
|
</Button>
|
||||||
<DropdownMenuContent align="end">
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuItem asChild>
|
<DropdownMenuContent align="end">
|
||||||
<Link href={`/projects/${projectId}/videos/${video.id}/share`}>
|
<DropdownMenuItem asChild>
|
||||||
<Share2 className="mr-2 h-4 w-4" />
|
<Link href={`/projects/${projectId}/videos/${video.id}/share`}>
|
||||||
Share
|
<Share2 className="mr-2 h-4 w-4" />
|
||||||
</Link>
|
Share
|
||||||
</DropdownMenuItem>
|
</Link>
|
||||||
<DropdownMenuItem onSelect={() => setShowEditDialog(true)}>
|
</DropdownMenuItem>
|
||||||
<Pencil className="mr-2 h-4 w-4" />
|
<DropdownMenuItem onSelect={() => setShowEditDialog(true)}>
|
||||||
Edit
|
<Pencil className="mr-2 h-4 w-4" />
|
||||||
</DropdownMenuItem>
|
Edit
|
||||||
<DropdownMenuItem onSelect={() => setShowVersionDialog(true)}>
|
</DropdownMenuItem>
|
||||||
<Plus className="mr-2 h-4 w-4" />
|
<DropdownMenuItem onSelect={() => setShowVersionDialog(true)}>
|
||||||
Add Version
|
<Plus className="mr-2 h-4 w-4" />
|
||||||
</DropdownMenuItem>
|
Add Version
|
||||||
<DropdownMenuItem
|
</DropdownMenuItem>
|
||||||
className="text-destructive"
|
<DropdownMenuItem
|
||||||
onSelect={() => setShowDeleteDialog(true)}
|
className="text-destructive"
|
||||||
>
|
onSelect={() => setShowDeleteDialog(true)}
|
||||||
<Trash2 className="mr-2 h-4 w-4" />
|
>
|
||||||
Delete
|
<Trash2 className="mr-2 h-4 w-4" />
|
||||||
</DropdownMenuItem>
|
Delete
|
||||||
</DropdownMenuContent>
|
</DropdownMenuItem>
|
||||||
</DropdownMenu>
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{isDeleting && (
|
||||||
|
<div className="absolute inset-0 z-10 flex items-center justify-center rounded-xl bg-background/60 backdrop-blur-[1px]">
|
||||||
|
<div className="flex items-center gap-2 rounded-full border bg-card px-3 py-1.5 text-sm font-medium shadow-sm">
|
||||||
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
|
Deleting...
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
)}
|
||||||
</Card>
|
</div>
|
||||||
|
|
||||||
{/* Edit Dialog */}
|
{/* Edit Dialog */}
|
||||||
<Dialog open={showEditDialog} onOpenChange={setShowEditDialog}>
|
<Dialog open={showEditDialog} onOpenChange={setShowEditDialog}>
|
||||||
|
|||||||
Reference in New Issue
Block a user