186 lines
7.0 KiB
TypeScript
186 lines
7.0 KiB
TypeScript
"use client"
|
|
|
|
import { useState } from "react"
|
|
import Image from "next/image"
|
|
import { TimelineStats } from "@/components/timeline-stats"
|
|
import { Card, CardContent } from "@/components/ui/card"
|
|
import { Badge } from "@/components/ui/badge"
|
|
import { Calendar, MapPin, Camera } 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 TimelineViewProps {
|
|
photos: Photo[]
|
|
onPhotoClick: (photo: Photo) => void
|
|
}
|
|
|
|
export function TimelineView({ photos, onPhotoClick }: TimelineViewProps) {
|
|
const [loadedImages, setLoadedImages] = useState<Set<number>>(new Set())
|
|
|
|
const handleImageLoad = (photoId: number) => {
|
|
setLoadedImages((prev) => new Set(prev).add(photoId))
|
|
}
|
|
|
|
// Group photos by year and month
|
|
const groupedPhotos = photos
|
|
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
|
|
.reduce(
|
|
(acc, photo) => {
|
|
const date = new Date(photo.date)
|
|
const year = date.getFullYear()
|
|
const month = date.getMonth()
|
|
|
|
if (!acc[year]) {
|
|
acc[year] = {}
|
|
}
|
|
if (!acc[year][month]) {
|
|
acc[year][month] = []
|
|
}
|
|
acc[year][month].push(photo)
|
|
|
|
return acc
|
|
},
|
|
{} as Record<number, Record<number, Photo[]>>,
|
|
)
|
|
|
|
const monthNames = [
|
|
"一月",
|
|
"二月",
|
|
"三月",
|
|
"四月",
|
|
"五月",
|
|
"六月",
|
|
"七月",
|
|
"八月",
|
|
"九月",
|
|
"十月",
|
|
"十一月",
|
|
"十二月",
|
|
]
|
|
|
|
return (
|
|
<div className="max-w-6xl mx-auto">
|
|
<TimelineStats photos={photos} />
|
|
|
|
<div className="relative">
|
|
{/* Timeline line */}
|
|
<div className="absolute left-8 md:left-1/2 top-0 bottom-0 w-0.5 bg-gray-200 transform md:-translate-x-0.5"></div>
|
|
|
|
{Object.entries(groupedPhotos).map(([year, months]) => (
|
|
<div key={year} className="mb-16">
|
|
{/* Year marker */}
|
|
<div className="sticky top-20 z-10 mb-8">
|
|
<div className="flex items-center">
|
|
<div className="relative z-10 bg-white border-4 border-gray-200 rounded-full p-4 left-4 md:left-1/2 md:transform md:-translate-x-1/2">
|
|
<span className="text-2xl font-light text-gray-900">{year}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{Object.entries(months).map(([monthIndex, monthPhotos]) => (
|
|
<div key={`${year}-${monthIndex}`} className="mb-12">
|
|
{/* Month marker */}
|
|
<div className="flex items-center mb-6">
|
|
<div className="relative z-10 bg-gray-100 rounded-full px-4 py-2 left-4 md:left-1/2 md:transform md:-translate-x-1/2">
|
|
<span className="text-sm font-medium text-gray-700">{monthNames[Number.parseInt(monthIndex)]}</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Photos for this month */}
|
|
<div className="space-y-8">
|
|
{monthPhotos.map((photo, index) => (
|
|
<div
|
|
key={photo.id}
|
|
className={`relative flex items-center ${
|
|
index % 2 === 0 ? "md:flex-row" : "md:flex-row-reverse"
|
|
}`}
|
|
>
|
|
{/* Timeline dot */}
|
|
<div className="absolute left-8 md:left-1/2 w-3 h-3 bg-gray-400 rounded-full transform -translate-x-1/2 z-10"></div>
|
|
|
|
{/* Photo card */}
|
|
<div
|
|
className={`w-full md:w-5/12 ml-16 md:ml-0 ${
|
|
index % 2 === 0 ? "md:mr-auto md:pr-8" : "md:ml-auto md:pl-8"
|
|
}`}
|
|
>
|
|
<Card
|
|
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="md:flex">
|
|
<div className="relative md:w-1/2 aspect-[4/3] md:aspect-auto overflow-hidden">
|
|
{!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 ${
|
|
loadedImages.has(photo.id) ? "opacity-100" : "opacity-0"
|
|
}`}
|
|
onLoad={() => handleImageLoad(photo.id)}
|
|
/>
|
|
</div>
|
|
|
|
<CardContent className="md:w-1/2 p-6">
|
|
<div className="space-y-4">
|
|
<div>
|
|
<h3 className="text-xl font-light text-gray-900 mb-2">{photo.title}</h3>
|
|
<p className="text-gray-600 text-sm leading-relaxed">{photo.description}</p>
|
|
</div>
|
|
|
|
<div className="flex flex-wrap gap-2">
|
|
{photo.tags.slice(0, 3).map((tag) => (
|
|
<Badge key={tag} variant="secondary" className="text-xs">
|
|
{tag}
|
|
</Badge>
|
|
))}
|
|
</div>
|
|
|
|
<div className="space-y-2 text-xs text-gray-500">
|
|
<div className="flex items-center gap-2">
|
|
<Calendar className="h-3 w-3" />
|
|
<span>{photo.date}</span>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<MapPin className="h-3 w-3" />
|
|
<span>{photo.exif.location}</span>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<Camera className="h-3 w-3" />
|
|
<span>{photo.exif.camera}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|