This commit is contained in:
xujiang
2025-07-08 14:39:08 +08:00
commit d06caee35a
80 changed files with 7321 additions and 0 deletions

View File

@ -0,0 +1,185 @@
"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>
)
}