- Add hydration state check to PhotoGallery component to prevent SSR/client mismatch - Update bun.lock with Express and CORS dependencies for mock API - Add placeholder image for better development experience
97 lines
3.3 KiB
TypeScript
97 lines
3.3 KiB
TypeScript
"use client"
|
|
|
|
import { useState, useEffect } from "react"
|
|
import Image from "next/image"
|
|
import { Card } from "@/components/ui/card"
|
|
import { Badge } from "@/components/ui/badge"
|
|
import { Calendar, MapPin } from "lucide-react"
|
|
|
|
interface Photo {
|
|
id: number
|
|
src: string
|
|
title: string
|
|
description: string
|
|
category: string
|
|
tags: string[]
|
|
date: string
|
|
exif: {
|
|
camera: string
|
|
lens: string
|
|
settings: string
|
|
location: string
|
|
}
|
|
}
|
|
|
|
interface PhotoGalleryProps {
|
|
photos: Photo[]
|
|
onPhotoClick: (photo: Photo) => void
|
|
}
|
|
|
|
export function PhotoGallery({ photos, onPhotoClick }: PhotoGalleryProps) {
|
|
const [loadedImages, setLoadedImages] = useState<Set<number>>(new Set())
|
|
const [isHydrated, setIsHydrated] = useState(false)
|
|
|
|
useEffect(() => {
|
|
setIsHydrated(true)
|
|
}, [])
|
|
|
|
const handleImageLoad = (photoId: number) => {
|
|
setLoadedImages((prev) => new Set(prev).add(photoId))
|
|
}
|
|
|
|
return (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
|
{photos.map((photo) => (
|
|
<Card
|
|
key={photo.id}
|
|
className="group cursor-pointer overflow-hidden border-0 shadow-sm hover:shadow-xl transition-all duration-300 hover:scale-[1.02]"
|
|
onClick={() => onPhotoClick(photo)}
|
|
>
|
|
<div className="relative aspect-[4/3] overflow-hidden">
|
|
{isHydrated && !loadedImages.has(photo.id) && <div className="absolute inset-0 bg-gray-100 animate-pulse" />}
|
|
<Image
|
|
src={photo.src || "/placeholder.svg"}
|
|
alt={photo.title}
|
|
fill
|
|
className={`object-cover transition-all duration-500 group-hover:scale-110 ${
|
|
isHydrated && loadedImages.has(photo.id) ? "opacity-100" : !isHydrated ? "opacity-100" : "opacity-0"
|
|
}`}
|
|
onLoad={() => handleImageLoad(photo.id)}
|
|
/>
|
|
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/20 transition-all duration-300" />
|
|
|
|
{/* Hover overlay */}
|
|
<div className="absolute inset-0 p-4 flex flex-col justify-end opacity-0 group-hover:opacity-100 transition-all duration-300">
|
|
<div className="text-white">
|
|
<h3 className="font-medium text-lg mb-1">{photo.title}</h3>
|
|
<p className="text-sm text-white/80 mb-2">{photo.description}</p>
|
|
<div className="flex items-center gap-2 text-xs text-white/70">
|
|
<Calendar className="h-3 w-3" />
|
|
<span>{photo.date}</span>
|
|
<MapPin className="h-3 w-3 ml-2" />
|
|
<span>{photo.exif.location}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Card content */}
|
|
<div className="p-4">
|
|
<h3 className="font-medium text-gray-900 mb-2">{photo.title}</h3>
|
|
<div className="flex flex-wrap gap-1 mb-3">
|
|
{photo.tags.slice(0, 2).map((tag) => (
|
|
<Badge key={tag} variant="secondary" className="text-xs">
|
|
{tag}
|
|
</Badge>
|
|
))}
|
|
</div>
|
|
<div className="text-xs text-gray-500">
|
|
{photo.exif.camera} • {photo.exif.settings}
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
)
|
|
}
|