feat: structure 2
This commit is contained in:
125
frontend/src/components/timeline/PhotoThumbnail.tsx
Normal file
125
frontend/src/components/timeline/PhotoThumbnail.tsx
Normal file
@@ -0,0 +1,125 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Star, Check, X } from 'lucide-react'
|
||||
import clsx from 'clsx'
|
||||
|
||||
interface Photo {
|
||||
id: string
|
||||
path: string
|
||||
filename: string
|
||||
width: number
|
||||
height: number
|
||||
date_taken: string | null
|
||||
rating: number
|
||||
flag: string | null
|
||||
hash: string
|
||||
}
|
||||
|
||||
interface PhotoThumbnailProps {
|
||||
photo: Photo
|
||||
size: number
|
||||
isSelected: boolean
|
||||
onClick: (e: React.MouseEvent) => void
|
||||
}
|
||||
|
||||
export function PhotoThumbnail({ photo, size, isSelected, onClick }: PhotoThumbnailProps) {
|
||||
const [imageError, setImageError] = useState(false)
|
||||
const [imageLoaded, setImageLoaded] = useState(false)
|
||||
|
||||
// Generate thumbnail URL - assuming backend serves thumbnails at /api/photos/{id}/thumbnail
|
||||
const thumbnailUrl = `http://localhost:8001/api/v1/photos/${photo.id}/thumb/medium`
|
||||
|
||||
// Calculate aspect ratio for proper sizing
|
||||
const aspectRatio = photo.height / photo.width
|
||||
const displayHeight = size * Math.min(aspectRatio, 1.5) // Cap height at 1.5x width
|
||||
|
||||
const handleImageLoad = () => {
|
||||
setImageLoaded(true)
|
||||
}
|
||||
|
||||
const handleImageError = () => {
|
||||
setImageError(true)
|
||||
}
|
||||
|
||||
// Reset state when photo changes
|
||||
useEffect(() => {
|
||||
setImageError(false)
|
||||
setImageLoaded(false)
|
||||
}, [photo.id])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'relative cursor-pointer overflow-hidden rounded-sm transition-all duration-200',
|
||||
'hover:ring-2 hover:ring-primary/50',
|
||||
isSelected && 'ring-2 ring-primary shadow-lg',
|
||||
!imageLoaded && 'bg-surface animate-pulse'
|
||||
)}
|
||||
style={{
|
||||
width: size,
|
||||
height: displayHeight,
|
||||
}}
|
||||
onClick={onClick}
|
||||
>
|
||||
{/* Thumbnail Image */}
|
||||
{!imageError ? (
|
||||
<img
|
||||
src={thumbnailUrl}
|
||||
alt={photo.filename}
|
||||
className={clsx(
|
||||
'h-full w-full object-cover transition-opacity duration-200',
|
||||
imageLoaded ? 'opacity-100' : 'opacity-0'
|
||||
)}
|
||||
onLoad={handleImageLoad}
|
||||
onError={handleImageError}
|
||||
loading="lazy"
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-full w-full items-center justify-center bg-surface text-text-muted">
|
||||
<div className="text-center text-xs">
|
||||
<div>Unable to load</div>
|
||||
<div className="mt-1 font-mono text-[10px]">{photo.filename}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Selection Indicator */}
|
||||
{isSelected && (
|
||||
<div className="absolute left-1 top-1 flex h-6 w-6 items-center justify-center rounded-full bg-primary text-white">
|
||||
<Check className="h-4 w-4" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Rating Stars */}
|
||||
{photo.rating > 0 && (
|
||||
<div className="absolute bottom-1 left-1 flex gap-0.5">
|
||||
{Array.from({ length: photo.rating }).map((_, i) => (
|
||||
<Star
|
||||
key={i}
|
||||
className="h-3 w-3 fill-star text-star"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Flag Indicators */}
|
||||
{photo.flag && (
|
||||
<div className="absolute bottom-1 right-1">
|
||||
{photo.flag === 'pick' && (
|
||||
<Check className="h-4 w-4 text-pick" />
|
||||
)}
|
||||
{photo.flag === 'reject' && (
|
||||
<X className="h-4 w-4 text-reject" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* File Type Badge for RAW/Video */}
|
||||
{(photo.path.toLowerCase().match(/\.(raw|arw|cr2|cr3|nef|orf|rw2|dng)$/i) ||
|
||||
photo.path.toLowerCase().match(/\.(mov|mp4|avi|mkv)$/i)) && (
|
||||
<div className="absolute right-1 top-1 rounded bg-black/50 px-1 py-0.5 text-[10px] font-medium text-white">
|
||||
{photo.path.toLowerCase().match(/\.(mov|mp4|avi|mkv)$/i) ? 'VIDEO' : 'RAW'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user