lightbox
All checks were successful
Build & Deploy KLZ Cables / build-and-deploy (push) Successful in 3m46s
All checks were successful
Build & Deploy KLZ Cables / build-and-deploy (push) Successful in 3m46s
This commit is contained in:
@@ -3,6 +3,8 @@
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import Image from 'next/image';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { useRouter, useSearchParams, usePathname } from 'next/navigation';
|
||||
|
||||
interface LightboxProps {
|
||||
isOpen: boolean;
|
||||
@@ -12,6 +14,9 @@ interface LightboxProps {
|
||||
}
|
||||
|
||||
export default function Lightbox({ isOpen, images, initialIndex, onClose }: LightboxProps) {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const pathname = usePathname();
|
||||
const [currentIndex, setCurrentIndex] = useState(initialIndex);
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
@@ -20,19 +25,53 @@ export default function Lightbox({ isOpen, images, initialIndex, onClose }: Ligh
|
||||
return () => setMounted(false);
|
||||
}, []);
|
||||
|
||||
const updateUrl = useCallback((index: number | null) => {
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
if (index !== null) {
|
||||
params.set('photo', index.toString());
|
||||
} else {
|
||||
params.delete('photo');
|
||||
}
|
||||
router.replace(`${pathname}?${params.toString()}`, { scroll: false });
|
||||
}, [pathname, router, searchParams]);
|
||||
|
||||
const prevImage = useCallback(() => {
|
||||
setCurrentIndex((prev) => (prev === 0 ? images.length - 1 : prev - 1));
|
||||
}, [images.length]);
|
||||
setCurrentIndex((prev) => {
|
||||
const next = prev === 0 ? images.length - 1 : prev - 1;
|
||||
updateUrl(next);
|
||||
return next;
|
||||
});
|
||||
}, [images.length, updateUrl]);
|
||||
|
||||
const nextImage = useCallback(() => {
|
||||
setCurrentIndex((prev) => (prev === images.length - 1 ? 0 : prev + 1));
|
||||
}, [images.length]);
|
||||
setCurrentIndex((prev) => {
|
||||
const next = prev === images.length - 1 ? 0 : prev + 1;
|
||||
updateUrl(next);
|
||||
return next;
|
||||
});
|
||||
}, [images.length, updateUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
const photoParam = searchParams.get('photo');
|
||||
if (photoParam !== null) {
|
||||
const index = parseInt(photoParam, 10);
|
||||
if (!isNaN(index) && index >= 0 && index < images.length) {
|
||||
setCurrentIndex(index);
|
||||
}
|
||||
}
|
||||
}, [searchParams, images.length]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
updateUrl(currentIndex);
|
||||
}
|
||||
}, [isOpen, currentIndex, updateUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') onClose();
|
||||
if (e.key === 'Escape') handleClose();
|
||||
if (e.key === 'ArrowLeft') prevImage();
|
||||
if (e.key === 'ArrowRight') nextImage();
|
||||
};
|
||||
@@ -47,59 +86,119 @@ export default function Lightbox({ isOpen, images, initialIndex, onClose }: Ligh
|
||||
document.body.style.overflow = originalStyle;
|
||||
window.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, [isOpen, onClose, prevImage, nextImage]);
|
||||
}, [isOpen, prevImage, nextImage]);
|
||||
|
||||
if (!isOpen || !mounted) return null;
|
||||
if (!mounted) return null;
|
||||
|
||||
const handleClose = () => {
|
||||
updateUrl(null);
|
||||
onClose();
|
||||
};
|
||||
|
||||
return createPortal(
|
||||
<div
|
||||
className="fixed inset-0 z-[99999] bg-black/95 backdrop-blur-sm flex items-center justify-center p-4 animate-in fade-in duration-300"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="absolute top-6 right-6 text-white text-3xl hover:text-[#011dff] transition-colors z-[10000] rounded-full w-12 h-12 flex items-center justify-center hover:bg-white/10"
|
||||
aria-label="Close lightbox"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={prevImage}
|
||||
className="absolute left-6 top-1/2 -translate-y-1/2 text-white text-4xl hover:text-[#011dff] transition-colors w-12 h-12 flex items-center justify-center hover:bg-white/10 rounded-full z-[10000]"
|
||||
aria-label="Previous image"
|
||||
>
|
||||
‹
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={nextImage}
|
||||
className="absolute right-6 top-1/2 -translate-y-1/2 text-white text-4xl hover:text-[#011dff] transition-colors w-12 h-12 flex items-center justify-center hover:bg-white/10 rounded-full z-[10000]"
|
||||
aria-label="Next image"
|
||||
>
|
||||
›
|
||||
</button>
|
||||
<AnimatePresence>
|
||||
{isOpen && (
|
||||
<div className="fixed inset-0 z-[99999] flex items-center justify-center">
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
className="absolute inset-0 bg-primary/95 backdrop-blur-xl"
|
||||
onClick={handleClose}
|
||||
/>
|
||||
|
||||
<div className="relative max-w-6xl max-h-[90vh] w-full h-full flex items-center justify-center animate-in slide-in-from-bottom-4 duration-500">
|
||||
<Image
|
||||
src={images[currentIndex]}
|
||||
alt={`Gallery image ${currentIndex + 1}`}
|
||||
fill
|
||||
className="object-contain max-h-[90vh] max-w-full shadow-2xl"
|
||||
unoptimized
|
||||
/>
|
||||
<div className="absolute bottom-8 left-1/2 -translate-x-1/2 bg-black/60 backdrop-blur-md text-white px-6 py-3 rounded-full text-sm font-medium border border-white/10">
|
||||
{currentIndex + 1} / {images.length}
|
||||
<motion.button
|
||||
initial={{ opacity: 0, scale: 0.5 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.5 }}
|
||||
transition={{ delay: 0.1, duration: 0.4 }}
|
||||
onClick={handleClose}
|
||||
className="absolute top-6 right-6 text-white/60 hover:text-white transition-all duration-500 z-[10000] rounded-full w-14 h-14 flex items-center justify-center hover:bg-white/5 group border border-white/10"
|
||||
aria-label="Close lightbox"
|
||||
>
|
||||
<div className="relative w-full h-full flex items-center justify-center group-hover:rotate-90 transition-transform duration-500">
|
||||
<span className="text-3xl font-extralight leading-none mb-1">×</span>
|
||||
</div>
|
||||
</motion.button>
|
||||
|
||||
<motion.button
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: -20 }}
|
||||
transition={{ delay: 0.2, duration: 0.4 }}
|
||||
onClick={prevImage}
|
||||
className="absolute left-6 top-1/2 -translate-y-1/2 text-white/60 hover:text-white transition-all duration-500 w-14 h-14 flex items-center justify-center hover:bg-white/5 rounded-full z-[10000] group border border-white/10"
|
||||
aria-label="Previous image"
|
||||
>
|
||||
<span className="text-4xl font-extralight group-hover:-translate-x-1 transition-transform duration-500">‹</span>
|
||||
</motion.button>
|
||||
|
||||
<motion.button
|
||||
initial={{ opacity: 0, x: 20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: 20 }}
|
||||
transition={{ delay: 0.2, duration: 0.4 }}
|
||||
onClick={nextImage}
|
||||
className="absolute right-6 top-1/2 -translate-y-1/2 text-white/60 hover:text-white transition-all duration-500 w-14 h-14 flex items-center justify-center hover:bg-white/5 rounded-full z-[10000] group border border-white/10"
|
||||
aria-label="Next image"
|
||||
>
|
||||
<span className="text-4xl font-extralight group-hover:translate-x-1 transition-transform duration-500">›</span>
|
||||
</motion.button>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 40, scale: 0.95 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
exit={{ opacity: 0, y: 20, scale: 0.98 }}
|
||||
transition={{ duration: 0.6, ease: [0.25, 0.46, 0.45, 0.94] }}
|
||||
className="relative w-full h-full max-w-6xl max-h-[85vh] flex flex-col items-center justify-center p-4 md:p-12 z-20 pointer-events-none"
|
||||
>
|
||||
<div className="pointer-events-auto w-full h-full flex flex-col items-center justify-center">
|
||||
<div className="relative w-full h-full shadow-[0_40px_100px_-20px_rgba(0,0,0,0.6)] ring-1 ring-white/20 overflow-hidden bg-primary-dark/50 rounded-2xl flex items-center justify-center">
|
||||
<AnimatePresence mode="wait" initial={false}>
|
||||
<motion.div
|
||||
key={currentIndex}
|
||||
initial={{ opacity: 0, scale: 1.1, filter: 'blur(10px)' }}
|
||||
animate={{ opacity: 1, scale: 1, filter: 'blur(0px)' }}
|
||||
exit={{ opacity: 0, scale: 0.9, filter: 'blur(10px)' }}
|
||||
transition={{ duration: 0.7, ease: [0.25, 0.46, 0.45, 0.94] }}
|
||||
className="relative w-full h-full"
|
||||
>
|
||||
<Image
|
||||
src={images[currentIndex]}
|
||||
alt={`Gallery image ${currentIndex + 1}`}
|
||||
fill
|
||||
className="object-cover transition-transform duration-1000 hover:scale-[1.03]"
|
||||
unoptimized
|
||||
/>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Technical Detail: Subtle grid overlay to reinforce industrial precision */}
|
||||
<div className="absolute inset-0 pointer-events-none opacity-[0.03] bg-[url('/grid.svg')] bg-repeat z-10" />
|
||||
|
||||
{/* Premium Reflection: Subtle gradient to give material feel */}
|
||||
<div className="absolute inset-0 pointer-events-none bg-gradient-to-tr from-white/10 via-transparent to-transparent opacity-40 z-10" />
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: 10 }}
|
||||
transition={{ delay: 0.3, duration: 0.4 }}
|
||||
className="mt-8 flex items-center gap-4"
|
||||
>
|
||||
<div className="h-px w-12 bg-white/20" />
|
||||
<div className="bg-white/5 backdrop-blur-2xl text-white px-6 py-2 rounded-full border border-white/10 text-[11px] font-bold tracking-[0.2em] uppercase">
|
||||
{currentIndex + 1} <span className="text-accent mx-3">/</span> {images.length}
|
||||
</div>
|
||||
<div className="h-px w-12 bg-white/20" />
|
||||
</motion.div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Backdrop click to close */}
|
||||
<div
|
||||
className="absolute inset-0 z-[9998]"
|
||||
onClick={onClose}
|
||||
/>
|
||||
</div>,
|
||||
)}
|
||||
</AnimatePresence>,
|
||||
document.body
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import Image from 'next/image';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { Section, Container, Heading } from '../../components/ui';
|
||||
import Lightbox from '../Lightbox';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
|
||||
export default function GallerySection() {
|
||||
const t = useTranslations('Home.gallery');
|
||||
const searchParams = useSearchParams();
|
||||
const images = [
|
||||
'/uploads/2024/12/DSC07433-Large-600x400.webp',
|
||||
'/uploads/2024/12/DSC07460-Large-600x400.webp',
|
||||
@@ -20,6 +22,17 @@ export default function GallerySection() {
|
||||
const [lightboxOpen, setLightboxOpen] = useState(false);
|
||||
const [lightboxIndex, setLightboxIndex] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const photoParam = searchParams.get('photo');
|
||||
if (photoParam !== null) {
|
||||
const index = parseInt(photoParam, 10);
|
||||
if (!isNaN(index) && index >= 0 && index < images.length) {
|
||||
setLightboxIndex(index);
|
||||
setLightboxOpen(true);
|
||||
}
|
||||
}
|
||||
}, [searchParams, images.length]);
|
||||
|
||||
return (
|
||||
<Section className="bg-white text-white py-32">
|
||||
<Container>
|
||||
|
||||
Reference in New Issue
Block a user