Next.js 14 website with standalone output configured for Docker deployment. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
119 lines
3.9 KiB
TypeScript
119 lines
3.9 KiB
TypeScript
"use client";
|
||
|
||
import { useState } from "react";
|
||
import Image from "next/image";
|
||
import { motion, AnimatePresence } from "framer-motion";
|
||
|
||
const galleryImages = [
|
||
{
|
||
src: "/images/hero_main.jpg",
|
||
alt: "Friends walking along a sun-drenched boulevard on Istanbul's Asian side",
|
||
},
|
||
{
|
||
src: "/images/manifesto_coastline.jpg",
|
||
alt: "Locals relaxing on the waterfront with the Marmara Sea and Princes' Islands beyond",
|
||
},
|
||
{
|
||
src: "/images/first_light.jpg",
|
||
alt: "Traditional Turkish breakfast spread with eggs, cheese, simit, and tea",
|
||
},
|
||
{
|
||
src: "/images/the_other_side.jpg",
|
||
alt: "Aerial view of Fenerbahçe Park, Kalamış Marina, and the Asian side coastline",
|
||
},
|
||
];
|
||
|
||
export function PhotoGallery() {
|
||
const [lightboxIndex, setLightboxIndex] = useState<number | null>(null);
|
||
|
||
return (
|
||
<section className="py-16 md:py-24 section-padding">
|
||
<div className="max-w-7xl mx-auto">
|
||
<h2 className="font-display text-3xl md:text-4xl font-bold text-deep-nazar mb-12 text-center">
|
||
Through the Lens
|
||
</h2>
|
||
|
||
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
|
||
{galleryImages.map((img, i) => (
|
||
<motion.button
|
||
key={i}
|
||
className={`relative overflow-hidden rounded-2xl ${
|
||
i === 0 ? "col-span-2 row-span-2 aspect-square" : "aspect-[4/3]"
|
||
}`}
|
||
whileHover={{ scale: 0.98 }}
|
||
onClick={() => setLightboxIndex(i)}
|
||
>
|
||
<Image
|
||
src={img.src}
|
||
alt={img.alt}
|
||
fill
|
||
className="object-cover hover:scale-110 transition-transform duration-700"
|
||
sizes={i === 0 ? "(max-width: 768px) 100vw, 66vw" : "(max-width: 768px) 50vw, 33vw"}
|
||
/>
|
||
</motion.button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Lightbox */}
|
||
<AnimatePresence>
|
||
{lightboxIndex !== null && (
|
||
<motion.div
|
||
initial={{ opacity: 0 }}
|
||
animate={{ opacity: 1 }}
|
||
exit={{ opacity: 0 }}
|
||
className="fixed inset-0 z-50 bg-black/90 flex items-center justify-center p-4"
|
||
onClick={() => setLightboxIndex(null)}
|
||
>
|
||
<button
|
||
className="absolute top-4 right-4 text-white/70 hover:text-white text-3xl z-10"
|
||
onClick={() => setLightboxIndex(null)}
|
||
aria-label="Close lightbox"
|
||
>
|
||
×
|
||
</button>
|
||
|
||
<button
|
||
className="absolute left-4 top-1/2 -translate-y-1/2 text-white/70 hover:text-white text-3xl z-10"
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
setLightboxIndex((lightboxIndex - 1 + galleryImages.length) % galleryImages.length);
|
||
}}
|
||
aria-label="Previous image"
|
||
>
|
||
‹
|
||
</button>
|
||
|
||
<button
|
||
className="absolute right-4 top-1/2 -translate-y-1/2 text-white/70 hover:text-white text-3xl z-10"
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
setLightboxIndex((lightboxIndex + 1) % galleryImages.length);
|
||
}}
|
||
aria-label="Next image"
|
||
>
|
||
›
|
||
</button>
|
||
|
||
<motion.div
|
||
key={lightboxIndex}
|
||
initial={{ scale: 0.9, opacity: 0 }}
|
||
animate={{ scale: 1, opacity: 1 }}
|
||
exit={{ scale: 0.9, opacity: 0 }}
|
||
className="relative w-full max-w-4xl aspect-[3/2]"
|
||
onClick={(e) => e.stopPropagation()}
|
||
>
|
||
<Image
|
||
src={galleryImages[lightboxIndex].src}
|
||
alt={galleryImages[lightboxIndex].alt}
|
||
fill
|
||
className="object-contain"
|
||
sizes="100vw"
|
||
/>
|
||
</motion.div>
|
||
</motion.div>
|
||
)}
|
||
</AnimatePresence>
|
||
</section>
|
||
);
|
||
}
|