Skip to content

Commit ded9df9

Browse files
committed
nice looking photography page
1 parent f91d99f commit ded9df9

File tree

7 files changed

+291
-27
lines changed

7 files changed

+291
-27
lines changed

components.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"$schema": "https://ui.shadcn.com/schema.json",
3-
"style": "default",
3+
"style": "new-york",
44
"rsc": true,
55
"tsx": true,
66
"tailwind": {

package-lock.json

Lines changed: 93 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
"@radix-ui/react-avatar": "^1.1.0",
1313
"@radix-ui/react-dropdown-menu": "^2.1.1",
1414
"@radix-ui/react-hover-card": "^1.1.1",
15-
"@radix-ui/react-slot": "^1.1.0",
15+
"@radix-ui/react-slot": "^1.2.3",
1616
"@radix-ui/react-tooltip": "^1.1.2",
1717
"@vercel/analytics": "^1.3.1",
1818
"class-variance-authority": "^0.7.0",

src/app/layout.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export default function RootLayout({
2121
return (
2222
<html lang="en" suppressHydrationWarning>
2323
<body
24-
className={`${GeistSans.className} ${GeistMono.variable} bg-background dark:bg-neutral-800 lg:bg-neutral-350 lg:dark:bg-black`}
24+
className={`${GeistSans.className} ${GeistMono.variable} bg-background`}
2525
>
2626
<ThemeProvider
2727
attribute="class"

src/app/page.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@ import Image from "next/image";
1111
import { CardBody, CardContainer, CardItem } from "@/components/ui/3d-card";
1212
import Link from "next/link";
1313
import { LinkPreview } from "@/components/ui/link-preview";
14-
import { BackgroundGradient } from "@/components/ui/background-gradient";
15-
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
1614
import { Badge } from "@/components/ui/badge";
1715
import { Code, Github, Linkedin, MapPin, MapPinned, Mail } from "lucide-react";
1816
import { ModeToggle } from "@/components/mode-toggle";

src/app/photography/page.tsx

Lines changed: 86 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,15 @@ import { v2 as cloudinary, ResourceApiResponse } from "cloudinary";
33
import Image from 'next/image'
44
import { unstable_cache } from "next/cache";
55
import 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

716
export const metadata: Metadata = {
817
title: "Photography",
@@ -19,11 +28,11 @@ const getBase64 = async (src: ArrayBuffer, size: number) => {
1928
};
2029

2130
async 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

2938
async 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

4757
const 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+
4963
export 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

Comments
 (0)