This commit is contained in:
Vula Builder
2026-08-05 18:14:37 +00:00
parent adbe43e74b
commit 18892746bf
@@ -0,0 +1,251 @@
'use client';
import { useState, useMemo, useEffect } from 'react';
import Image from 'next/image';
import { X, ChevronLeft, ChevronRight } from 'lucide-react';
import Modal from '@/components/ui/Modal';
interface Photo {
id: number;
src: string;
alt: string;
caption: string;
date: string;
category: 'Offices' | 'Attorneys' | 'Client Events' | 'Community Outreach';
}
const photos: Photo[] = [
{
id: 1,
src: 'https://images.pexels.com/photos/37726678/pexels-photo-37726678.jpeg?auto=compress&cs=tinysrgb&h=650&w=940',
alt: 'law legal atmosphere interior',
caption: 'Gumede & Partners main office, Pietermaritzburg',
date: '10 Jun 2025',
category: 'Offices',
},
{
id: 2,
src: 'https://images.pexels.com/photos/7875996/pexels-photo-7875996.jpeg?auto=compress&cs=tinysrgb&h=650&w=940',
alt: 'law professional',
caption: 'Senior partner Martha Gumede in consultation',
date: '18 May 2025',
category: 'Attorneys',
},
{
id: 3,
src: 'https://images.pexels.com/photos/5673483/pexels-photo-5673483.jpeg?auto=compress&cs=tinysrgb&h=650&w=940',
alt: 'filterable photo grid firm life law',
caption: 'Annual client appreciation braai',
date: '5 Apr 2025',
category: 'Client Events',
},
{
id: 4,
src: 'https://images.pexels.com/photos/2084277/pexels-photo-2084277.jpeg?auto=compress&cs=tinysrgb&h=650&w=940',
alt: 'filterable photo grid firm life',
caption: 'Pro bono legal clinic at Imbali township',
date: '22 Mar 2025',
category: 'Community Outreach',
},
{
id: 5,
src: 'https://images.pexels.com/photos/5673483/pexels-photo-5673483.jpeg?auto=compress&cs=tinysrgb&h=650&w=940',
alt: 'filterable photo grid firm life law',
caption: 'Boardroom where key deals are closed',
date: '2 Mar 2025',
category: 'Offices',
},
{
id: 6,
src: 'https://images.pexels.com/photos/2084277/pexels-photo-2084277.jpeg?auto=compress&cs=tinysrgb&h=650&w=940',
alt: 'filterable photo grid firm life',
caption: 'Associate Thabo Dlamini preparing for court',
date: '28 Feb 2025',
category: 'Attorneys',
},
{
id: 7,
src: 'https://images.pexels.com/photos/37726678/pexels-photo-37726678.jpeg?auto=compress&cs=tinysrgb&h=650&w=940',
alt: 'law legal atmosphere interior',
caption: 'Real estate seminar with local investors',
date: '15 Jan 2025',
category: 'Client Events',
},
{
id: 8,
src: 'https://images.pexels.com/photos/7875996/pexels-photo-7875996.jpeg?auto=compress&cs=tinysrgb&h=650&w=940',
alt: 'law professional',
caption: 'Wills Week free estate planning workshop',
date: '10 Dec 2024',
category: 'Community Outreach',
},
];
const categories = ['All', 'Offices', 'Attorneys', 'Client Events', 'Community Outreach'] as const;
export default function FirmLifeGallerySection() {
const [filter, setFilter] = useState<string>('All');
const [lightboxIndex, setLightboxIndex] = useState<number | null>(null);
const filteredPhotos = useMemo(() => {
if (filter === 'All') return photos;
return photos.filter((p) => p.category === filter);
}, [filter]);
const closeLightbox = () => setLightboxIndex(null);
const goToPrev = () => {
setLightboxIndex((prev) => {
if (prev === null || filteredPhotos.length === 0) return prev;
return (prev - 1 + filteredPhotos.length) % filteredPhotos.length;
});
};
const goToNext = () => {
setLightboxIndex((prev) => {
if (prev === null || filteredPhotos.length === 0) return prev;
return (prev + 1) % filteredPhotos.length;
});
};
useEffect(() => {
if (lightboxIndex === null) return;
const handleKey = (e: KeyboardEvent) => {
if (e.key === 'Escape') closeLightbox();
if (e.key === 'ArrowLeft') goToPrev();
if (e.key === 'ArrowRight') goToNext();
};
window.addEventListener('keydown', handleKey);
return () => window.removeEventListener('keydown', handleKey);
}, [lightboxIndex]); // eslint-disable-line react-hooks/exhaustive-deps
const currentPhoto = lightboxIndex !== null ? filteredPhotos[lightboxIndex] : null;
return (
<section
id="firm-life-gallery"
className="bg-[#F0F8FF] px-6 py-20"
>
<div className="mx-auto max-w-7xl">
{/* Section heading */}
<div className="mb-10">
<h2 className="text-3xl font-bold tracking-tight text-[#1A2B3C] lg:text-4xl font-serif">
We dont just practice law we{' '}
<em className="italic text-secondary">serve</em> people
</h2>
</div>
{/* Filter pills */}
<div className="mb-10 flex flex-wrap gap-3">
{categories.map((cat) => (
<button
key={cat}
onClick={() => setFilter(cat)}
className={`
min-w-[44px] px-5 py-2 rounded-full text-sm font-medium
transition-all duration-200 ease-out
${
filter === cat
? 'bg-primary text-primary-foreground border border-primary'
: 'bg-white text-[#1A2B3C] border border-[#1A2B3C]/15 hover:border-secondary hover:text-secondary'
}
`}
aria-pressed={filter === cat}
>
{cat}
</button>
))}
</div>
{/* Photo grid */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
{filteredPhotos.map((photo, index) => (
<article
key={photo.id}
role="button"
tabIndex={0}
onClick={() => setLightboxIndex(index)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
setLightboxIndex(index);
}
}}
className="group cursor-pointer rounded-lg border border-[#1A2B3C]/15 hover:border-secondary transition-all duration-200 ease-out overflow-hidden bg-white"
aria-label={`View ${photo.caption}`}
>
{/* Thumbnail */}
<div className="relative aspect-square overflow-hidden">
<Image
src={photo.src}
alt={photo.alt}
fill
className="object-cover group-hover:scale-105 transition-transform duration-500 ease-out"
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 25vw"
/>
</div>
{/* Caption & date */}
<div className="p-4">
<p className="text-sm font-medium text-[#1A2B3C] line-clamp-2">
{photo.caption}
</p>
<p className="mt-1 text-xs text-[#1A2B3C]/60">{photo.date}</p>
</div>
</article>
))}
</div>
</div>
{/* Lightbox modal */}
{currentPhoto && lightboxIndex !== null && (
<Modal isOpen={true} onClose={closeLightbox}>
<div className="relative bg-black flex items-center justify-center min-h-[80vh] p-4">
{/* Close */}
<button
onClick={closeLightbox}
className="absolute top-4 right-4 z-10 text-white p-2 rounded-full bg-black/50 hover:bg-black/70 transition-all duration-200"
aria-label="Close lightbox"
>
<X size={24} />
</button>
{/* Previous */}
<button
onClick={goToPrev}
className="absolute left-4 top-1/2 -translate-y-1/2 z-10 text-white p-2 rounded-full bg-black/50 hover:bg-black/70 transition-all duration-200"
aria-label="Previous image"
>
<ChevronLeft size={32} />
</button>
{/* Large image */}
<div className="relative w-full max-w-5xl h-[70vh]">
<Image
src={currentPhoto.src}
alt={currentPhoto.alt}
fill
className="object-contain"
sizes="90vw"
priority
/>
</div>
{/* Next */}
<button
onClick={goToNext}
className="absolute right-4 top-1/2 -translate-y-1/2 z-10 text-white p-2 rounded-full bg-black/50 hover:bg-black/70 transition-all duration-200"
aria-label="Next image"
>
<ChevronRight size={32} />
</button>
{/* Image details */}
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 text-white text-center px-4">
<p className="text-sm font-medium">{currentPhoto.caption}</p>
<p className="text-xs opacity-70 mt-1">{currentPhoto.date}</p>
</div>
</div>
</Modal>
)}
</section>
);
}