Deploy
This commit is contained in:
@@ -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 don’t 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user