@@ -3,6 +3,15 @@ import { v2 as cloudinary, ResourceApiResponse } from "cloudinary";
33import Image from 'next/image'
44import { unstable_cache } from "next/cache" ;
55import sharp from "sharp" ;
6+ import {
7+ Breadcrumb ,
8+ BreadcrumbItem ,
9+ BreadcrumbLink ,
10+ BreadcrumbList ,
11+ BreadcrumbPage ,
12+ BreadcrumbSeparator ,
13+ } from "@/components/ui/breadcrumb"
14+ import { Aperture , X } from "lucide-react" ;
615
716export const metadata : Metadata = {
817 title : "Photography" ,
@@ -19,11 +28,11 @@ const getBase64 = async (src: ArrayBuffer, size: number) => {
1928} ;
2029
2130async function getBlurPreviewDataUrl ( public_id : string ) {
22- const imageUrl = cloudinary . url ( public_id )
31+ const imageUrl = cloudinary . url ( public_id , { transformation : [ { crop : 'thumb' , width : 30 } ] } )
2332 // fetch image content and convert to data url
2433 const response = await fetch ( imageUrl ) ;
2534 const arrayBuffer = await response . arrayBuffer ( ) ;
26- return getBase64 ( arrayBuffer , 10 )
35+ return getBase64 ( arrayBuffer , 8 )
2736}
2837
2938async function getPhotos ( ) {
@@ -36,7 +45,8 @@ async function getPhotos() {
3645 return Promise . all ( res . resources . map ( async resource => ( {
3746 ...resource ,
3847 tags : resource . tags . filter ( t => t !== 'photography' ) ,
39- blurDataURL : await getBlurPreviewDataUrl ( resource . secure_url )
48+ blurDataURL : await getBlurPreviewDataUrl ( resource . secure_url ) ,
49+ metadata : ( await cloudinary . api . resource ( resource . public_id , { media_metadata : true } ) ) . media_metadata
4050 } ) ) ) ;
4151 } catch ( error ) {
4252 console . error ( "Error fetching photos from Cloudinary:" , error ) ;
@@ -46,30 +56,89 @@ async function getPhotos() {
4656
4757const cachedGetPhotos = unstable_cache ( getPhotos )
4858
59+ const getThumbUrl = ( public_id : string ) => {
60+ return cloudinary . url ( public_id , { transformation : [ { crop : 'thumb' , width : 1000 } , { quality : "auto:good" , format : 'auto' } ] } )
61+ }
62+
4963export default async function PhotographyPage ( ) {
5064 const photos = await cachedGetPhotos ( )
65+ console . log ( photos . map ( p => p ) )
5166 return (
5267 < div className = "container mx-auto px-4 py-8" >
53- < h1 className = "text-3xl font-bold mb-8 text-center" > Photography</ h1 >
68+ < Breadcrumb className = "pb-8" >
69+ < BreadcrumbList >
70+ < BreadcrumbItem >
71+ < BreadcrumbLink href = "/" > Ken Zhou</ BreadcrumbLink >
72+ </ BreadcrumbItem >
73+ < BreadcrumbSeparator />
74+ < BreadcrumbItem >
75+ < BreadcrumbPage className = "flex gap-2 items-center" > < Aperture className = "size-4" /> Photography</ BreadcrumbPage >
76+ </ BreadcrumbItem >
77+ </ BreadcrumbList >
78+ </ Breadcrumb >
5479
5580 { photos . length === 0 ? (
5681 < p className = "text-center text-gray-500" >
5782 No photos found. Check your Cloudinary configuration and tags.
5883 </ p >
5984 ) : (
60- < div className = "grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4" >
61- { photos . map ( ( photo ) => (
62- < Image
63- key = { photo . public_id }
64- src = { photo . secure_url }
65- alt = { `Photo - ${ photo . public_id } ` }
66- className = "w-full h-full object-cover rounded-lg shadow-md transition-transform duration-300 ease-in-out group-hover:scale-105"
67- width = { photo . width }
68- height = { photo . height }
69- placeholder = "blur"
70- blurDataURL = { photo . blurDataURL }
71- />
72- ) ) }
85+ < div className = "columns-1 sm:columns-2 md:columns-3 lg:columns-4 gap-4" >
86+ { photos . map ( ( photo ) => {
87+ const photoId = photo . public_id . replace ( / [ ^ \w - ] / g, '-' ) ;
88+ return (
89+ < div key = { photo . public_id } className = "group break-inside-avoid mb-4" >
90+ < a href = { `#${ photoId } ` } >
91+ < div className = "overflow-hidden outline outline-transparent hover:outline-border transition-colors" >
92+ < Image
93+ src = { getThumbUrl ( photo . public_id ) }
94+ alt = { `Photo - ${ photo . public_id } ` }
95+ className = "w-full h-auto object-cover transition-transform duration-300 ease-in-out"
96+ width = { photo . width }
97+ height = { photo . height }
98+ placeholder = "blur"
99+ blurDataURL = { photo . blurDataURL }
100+ />
101+ </ div >
102+ </ a >
103+ { photo . metadata && (
104+ < div className = "mt-2 text-xs" >
105+ < div className = "mb-1 flex flex-wrap gap-x-4 text-xs text-muted-foreground justify-between" >
106+ < span > { photo . metadata . FocalLength } </ span >
107+ < span > f/{ photo . metadata . FNumber } </ span >
108+ < span > { photo . metadata . ExposureTime } s</ span >
109+ < span > ISO { photo . metadata . ISO } </ span >
110+ < span > { Number ( photo . metadata . ExposureCompensation ) === 0 ? "±" : "" } { photo . metadata . ExposureCompensation } EV</ span >
111+ </ div >
112+ < div className = "flex gap-2 flex-wrap" >
113+ < span className = "font-semibold" > { photo . metadata . Make } { photo . metadata . Model } </ span >
114+ < span className = "text-muted-foreground" > { photo . metadata . LensModel } </ span >
115+ </ div >
116+
117+ </ div >
118+ ) }
119+ < div id = { photoId } className = "fixed inset-0 z-50 hidden items-center justify-center bg-background/80 backdrop-blur-sm target:flex" >
120+ < a href = "#" className = "absolute inset-0" aria-label = "Close modal" > </ a >
121+ < div className = "relative z-10 flex h-full w-full items-center justify-center p-4 lg:p-8 pointer-events-none" >
122+ < div
123+ className = "relative pointer-events-auto max-h-full max-w-full"
124+ style = { { aspectRatio : `${ photo . width } / ${ photo . height } ` } }
125+ >
126+ < Image
127+ src = { photo . secure_url }
128+ alt = { `Photo - ${ photo . public_id } ` }
129+ className = "block h-full w-full"
130+ width = { photo . width }
131+ height = { photo . height }
132+ placeholder = "blur"
133+ blurDataURL = { photo . blurDataURL }
134+ />
135+ </ div >
136+ </ div >
137+ < a href = "#" className = "absolute top-4 right-4 z-20 transition-colors" > < X /> </ a >
138+ </ div >
139+ </ div >
140+ )
141+ } ) }
73142 </ div >
74143 ) }
75144 </ div >
0 commit comments