Files
chinese-family-tree-2/components/dashboard/photo-card.tsx
T
freedakgmail e9822f5c92 0.9.0.0
2025-12-22 07:52:55 +08:00

166 lines
5.6 KiB
TypeScript

'use client'
import React, { useCallback } from 'react'
import Link from 'next/link'
import { format } from 'date-fns'
import { Switch } from '@/components/ui/switch'
import { MemberNameWithStatus } from '@/components/member-name-with-status'
import { Play, Video } from 'lucide-react'
interface PhotoCardProps {
photo: {
url: string
caption?: string
uploadedAt: string
memberId: string
memberName: string
isDead: boolean
adminVisibleOverride: boolean
visibleInOverview: boolean
}
isOwner: boolean | null
currentTree?: { id?: string } | null
onSelect: (url: string) => void
onToggle: (memberId: string, photoUrl: string, value: boolean) => void
isLoading: boolean
}
const isVideoFile = (url: string) => {
const videoExtensions = ['.mp4', '.webm', '.ogg', '.mov', '.avi', '.mkv']
return videoExtensions.some(ext => url.toLowerCase().endsWith(ext))
}
export const PhotoCard: React.FC<PhotoCardProps> = ({
photo,
isOwner,
currentTree,
onSelect,
onToggle,
isLoading
}) => {
const handleToggle = useCallback((checked: boolean) => {
onToggle(photo.memberId, photo.url, checked)
}, [photo.memberId, photo.url, onToggle])
if (photo.adminVisibleOverride === false) {
return (
<div className="rounded-lg overflow-hidden bg-card shadow-sm hover:shadow-lg transition-all duration-300 hover:-translate-y-1 border border-border/50">
<div className="p-4 space-y-2">
<div className="flex items-center justify-between gap-1.5 text-xs">
<div className="flex items-center gap-1 text-muted-foreground">
<span></span>
<Link
href={`/members/${photo.memberId}${currentTree?.id ? `?treeId=${currentTree.id}` : ''}`}
className="hover:text-primary hover:underline"
>
<MemberNameWithStatus
name={photo.memberName}
isDead={photo.isDead}
className="text-foreground font-medium"
/>
</Link>
</div>
<span className="text-muted-foreground/70 text-[10px]">
{format(new Date(photo.uploadedAt), 'MM-dd')}
</span>
</div>
{isOwner && (
<div className="flex items-center justify-between gap-1.5 text-[11px] text-muted-foreground">
<span></span>
<Switch
checked={photo.adminVisibleOverride ?? true}
onCheckedChange={handleToggle}
disabled={isLoading}
/>
</div>
)}
{!isOwner && (
<p className="text-[11px] text-muted-foreground"></p>
)}
</div>
</div>
)
}
return (
<div className="rounded-lg overflow-hidden bg-card shadow-sm hover:shadow-lg transition-all duration-300 hover:-translate-y-1 border border-border/50">
{/* 媒体区域 */}
<div
className="relative cursor-zoom-in"
onClick={() => onSelect(photo.url)}
>
{isVideoFile(photo.url) ? (
<div className="relative">
<video
src={photo.url}
className="w-full h-auto object-cover"
muted
preload="metadata"
/>
<div className="absolute inset-0 flex items-center justify-center bg-black/30">
<div className="w-12 h-12 rounded-full bg-white/90 flex items-center justify-center">
<Play className="h-6 w-6 text-black ml-1" />
</div>
</div>
<div className="absolute top-2 left-2 bg-black/70 text-white text-xs px-2 py-1 rounded flex items-center gap-1">
<Video className="h-3 w-3" />
</div>
</div>
) : (
<img
src={photo.url}
alt={photo.caption || `${photo.memberName}的照片`}
className="w-full h-auto object-cover"
loading="lazy"
/>
)}
</div>
{/* 底部信息 */}
<div className="px-3 py-2 bg-card border-t border-border/30 space-y-1">
{/* 照片说明 */}
<div className="text-xs line-clamp-2">
{photo.caption ? (
<span className="text-foreground">{photo.caption}</span>
) : (
<span className="text-muted-foreground/70"></span>
)}
</div>
{/* 分享人和时间 */}
<div className="flex items-center justify-between gap-1.5 text-xs">
<div className="flex items-center gap-1">
<span className="text-muted-foreground"></span>
<Link
href={`/members/${photo.memberId}${currentTree?.id ? `?treeId=${currentTree.id}` : ''}`}
className="hover:text-primary hover:underline"
>
<MemberNameWithStatus
name={photo.memberName}
isDead={photo.isDead}
className="text-foreground font-medium"
/>
</Link>
</div>
<span className="text-muted-foreground/70 text-[10px]">
{format(new Date(photo.uploadedAt), 'MM-dd')}
</span>
</div>
{/* 管理员权限开关 */}
{isOwner && (
<div className="mt-2 flex items-center justify-between gap-1.5 text-[11px] text-muted-foreground">
<span></span>
<Switch
checked={photo.adminVisibleOverride ?? true}
onCheckedChange={handleToggle}
disabled={isLoading}
/>
</div>
)}
</div>
</div>
)
}