320 lines
12 KiB
React
320 lines
12 KiB
React
import React, { useState } from 'react';
|
|
import { motion, AnimatePresence, useReducedMotion } from 'framer-motion';
|
|
import { ArrowRight, Clock } from 'lucide-react';
|
|
import { assetUrl } from '../lib/assetUrl';
|
|
|
|
export default function FeaturedGallery({ gallery }) {
|
|
const galleryData = gallery?.items || [];
|
|
const header = gallery || { eyebrow: 'Featured Gallery', title: '', subtitle: '' };
|
|
const [activeGalleryIndex, setActiveGalleryIndex] = useState(0);
|
|
const [isMobile, setIsMobile] = useState(false);
|
|
|
|
React.useEffect(() => {
|
|
const check = () => setIsMobile(window.innerWidth < 1024);
|
|
check();
|
|
window.addEventListener('resize', check);
|
|
return () => window.removeEventListener('resize', check);
|
|
}, []);
|
|
|
|
// Auto-advance card every 4 seconds
|
|
React.useEffect(() => {
|
|
if (!galleryData.length) return;
|
|
const timer = setInterval(() => {
|
|
setActiveGalleryIndex((prev) => (prev + 1) % galleryData.length);
|
|
}, 4000);
|
|
|
|
return () => clearInterval(timer);
|
|
}, [activeGalleryIndex, galleryData.length]);
|
|
|
|
return (
|
|
<section id="featured-gallery" style={{ padding: isMobile ? '4rem 1.25rem' : '6rem 8%', background: '#020710', position: 'relative' }}>
|
|
<div style={{ marginBottom: '3.5rem', position: 'relative', zIndex: 10 }}>
|
|
<span style={{ color: '#ffaa00', fontSize: '0.9rem', fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.1em' }}>
|
|
{header.eyebrow}
|
|
</span>
|
|
<h2 style={{ fontSize: 'clamp(2rem, 3vw, 2.5rem)', fontWeight: 800, marginTop: '0.5rem', color: '#ffffff' }}>
|
|
{header.title}
|
|
</h2>
|
|
<p style={{ color: '#94a3b8', fontSize: '1rem', marginTop: '0.75rem', maxWidth: '600px', lineHeight: '1.5' }}>
|
|
{header.subtitle}
|
|
</p>
|
|
</div>
|
|
|
|
<ResponsiveGalleryWrapper
|
|
galleryData={galleryData}
|
|
activeIdx={activeGalleryIndex}
|
|
setActiveIdx={setActiveGalleryIndex}
|
|
/>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
function ResponsiveGalleryWrapper({ galleryData, activeIdx, setActiveIdx }) {
|
|
const [isMobileSize, setIsMobileSize] = React.useState(false);
|
|
const shouldReduceMotion = useReducedMotion();
|
|
|
|
React.useEffect(() => {
|
|
const handleResize = () => {
|
|
setIsMobileSize(window.innerWidth < 1024);
|
|
};
|
|
handleResize();
|
|
window.addEventListener('resize', handleResize);
|
|
return () => window.removeEventListener('resize', handleResize);
|
|
}, []);
|
|
|
|
const getRevealProps = (index) => {
|
|
if (shouldReduceMotion) return {};
|
|
return {
|
|
initial: { opacity: 0, y: 40 },
|
|
whileInView: { opacity: 1, y: 0 },
|
|
viewport: { once: true, amount: 0.15 }
|
|
};
|
|
};
|
|
|
|
if (isMobileSize) {
|
|
return (
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}>
|
|
{galleryData.map((item, index) => {
|
|
const isFirst = index === 0;
|
|
const revealProps = getRevealProps(index);
|
|
return (
|
|
<motion.div
|
|
key={item.id}
|
|
className="glass-card"
|
|
{...revealProps}
|
|
transition={{
|
|
duration: 0.8,
|
|
ease: [0.22, 1, 0.36, 1],
|
|
delay: index * 0.1
|
|
}}
|
|
style={{
|
|
borderRadius: '24px',
|
|
overflow: 'hidden',
|
|
border: '1px solid rgba(255, 255, 255, 0.06)',
|
|
position: 'relative',
|
|
height: isFirst ? '320px' : '240px',
|
|
backgroundImage: `linear-gradient(to bottom, rgba(2, 7, 16, 0.3) 0%, rgba(2, 7, 16, 0.95) 100%), url(${assetUrl(item.image)})`,
|
|
backgroundSize: 'cover',
|
|
backgroundPosition: 'center',
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
justifyContent: 'flex-end',
|
|
padding: '2rem 1.5rem',
|
|
boxShadow: isFirst ? '0 10px 30px -10px rgba(255, 170, 0, 0.15)' : 'none',
|
|
borderColor: isFirst ? 'rgba(255, 170, 0, 0.3)' : 'rgba(255, 255, 255, 0.06)'
|
|
}}
|
|
>
|
|
<span style={{
|
|
color: '#ffaa00',
|
|
fontSize: '0.75rem',
|
|
fontWeight: 600,
|
|
textTransform: 'uppercase',
|
|
letterSpacing: '0.05em',
|
|
marginBottom: '0.25rem'
|
|
}}>
|
|
{item.category}
|
|
</span>
|
|
<h3 style={{ fontSize: '1.4rem', fontWeight: 700, color: '#ffffff', margin: '0 0 0.5rem 0', lineHeight: '1.3' }}>
|
|
{item.title}
|
|
</h3>
|
|
<p style={{ color: '#cbd5e1', fontSize: '0.88rem', margin: '0 0 1rem 0', lineHeight: '1.5', display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical', overflow: 'hidden' }}>
|
|
{item.excerpt}
|
|
</p>
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', borderTop: '1px solid rgba(255, 255, 255, 0.1)', paddingTop: '0.75rem' }}>
|
|
<span style={{ color: '#94a3b8', fontSize: '0.8rem', display: 'flex', alignItems: 'center', gap: '0.4rem' }}>
|
|
<Clock size={14} /> {item.readTime}
|
|
</span>
|
|
<span style={{ color: '#ffaa00', fontSize: '0.85rem', fontWeight: 600, display: 'flex', alignItems: 'center', gap: '0.25rem' }}>
|
|
Read More <ArrowRight size={14} />
|
|
</span>
|
|
</div>
|
|
</motion.div>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Desktop horizontal layout
|
|
return (
|
|
<div style={{
|
|
display: 'flex',
|
|
gap: '1.25rem',
|
|
width: '100%',
|
|
height: '460px',
|
|
alignItems: 'stretch',
|
|
position: 'relative'
|
|
}}>
|
|
{galleryData.map((item, index) => {
|
|
const isActive = activeIdx === index;
|
|
const revealProps = getRevealProps(index);
|
|
return (
|
|
<motion.div
|
|
key={item.id}
|
|
layout
|
|
onMouseEnter={() => setActiveIdx(index)}
|
|
className="glass-card"
|
|
{...revealProps}
|
|
transition={shouldReduceMotion ? { type: 'spring', stiffness: 350, damping: 32 } : {
|
|
layout: { type: 'spring', stiffness: 350, damping: 32 },
|
|
y: { duration: 0.8, ease: [0.22, 1, 0.36, 1], delay: index * 0.1 },
|
|
opacity: { duration: 0.8, ease: 'easeOut', delay: index * 0.1 }
|
|
}}
|
|
style={{
|
|
flex: isActive ? 2.8 : 1,
|
|
borderRadius: '24px',
|
|
overflow: 'hidden',
|
|
border: '1px solid rgba(255, 255, 255, 0.06)',
|
|
position: 'relative',
|
|
cursor: 'pointer',
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
justifyContent: 'space-between',
|
|
boxShadow: isActive ? '0 15px 35px -12px rgba(255, 170, 0, 0.2)' : '0 4px 20px rgba(0, 0, 0, 0.3)',
|
|
borderColor: isActive ? 'rgba(255, 170, 0, 0.3)' : 'rgba(255, 255, 255, 0.06)',
|
|
}}
|
|
>
|
|
{/* Background image container for smooth scale transform */}
|
|
<motion.div
|
|
style={{
|
|
position: 'absolute',
|
|
top: 0,
|
|
left: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
backgroundImage: `url(${assetUrl(item.image)})`,
|
|
backgroundSize: 'cover',
|
|
backgroundPosition: 'center',
|
|
zIndex: 1
|
|
}}
|
|
animate={{ scale: isActive ? 1.06 : 1 }}
|
|
transition={{ duration: 0.6 }}
|
|
/>
|
|
|
|
{/* Dark Overlay */}
|
|
<motion.div
|
|
style={{
|
|
position: 'absolute',
|
|
top: 0,
|
|
left: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
background: 'linear-gradient(to bottom, rgba(2, 7, 16, 0.3) 0%, rgba(2, 7, 16, 0.95) 100%)',
|
|
zIndex: 2
|
|
}}
|
|
animate={{ opacity: isActive ? 0.95 : 0.85 }}
|
|
transition={{ duration: 0.4 }}
|
|
/>
|
|
|
|
{/* Top Tag */}
|
|
<div style={{ position: 'relative', zIndex: 3, padding: '2rem 1.8rem 0 1.8rem', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
<span style={{
|
|
color: '#ffaa00',
|
|
fontSize: '0.75rem',
|
|
fontWeight: 700,
|
|
textTransform: 'uppercase',
|
|
letterSpacing: '0.08em',
|
|
background: 'rgba(255, 170, 0, 0.08)',
|
|
padding: '0.3rem 0.6rem',
|
|
borderRadius: '8px',
|
|
border: '1px solid rgba(255, 170, 0, 0.15)'
|
|
}}>
|
|
{item.category}
|
|
</span>
|
|
</div>
|
|
|
|
{/* Bottom Content Area */}
|
|
<div style={{ position: 'relative', zIndex: 3, padding: '0 1.8rem 2rem 1.8rem' }}>
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
|
|
<h3 style={{
|
|
fontSize: isActive ? '1.5rem' : '1.15rem',
|
|
fontWeight: 700,
|
|
color: '#ffffff',
|
|
margin: 0,
|
|
lineHeight: '1.3',
|
|
transition: 'font-size 0.3s ease',
|
|
whiteSpace: isActive ? 'normal' : 'nowrap',
|
|
overflow: isActive ? 'visible' : 'hidden',
|
|
textOverflow: isActive ? 'clip' : 'ellipsis',
|
|
maxWidth: '100%'
|
|
}}>
|
|
{item.title}
|
|
</h3>
|
|
|
|
{/* Excerpt - Animate visibility based on active state */}
|
|
<motion.div
|
|
initial={false}
|
|
animate={{
|
|
height: isActive ? 'auto' : 0,
|
|
opacity: isActive ? 1 : 0,
|
|
marginTop: isActive ? 6 : 0
|
|
}}
|
|
transition={{ duration: 0.4, ease: 'easeInOut' }}
|
|
style={{ overflow: 'hidden' }}
|
|
>
|
|
<p style={{ color: '#cbd5e1', fontSize: '0.88rem', margin: 0, lineHeight: '1.5', maxWidth: '90%' }}>
|
|
{item.excerpt}
|
|
</p>
|
|
</motion.div>
|
|
</div>
|
|
|
|
{/* Progress Line and Footer Area */}
|
|
<div style={{ marginTop: '1.5rem' }}>
|
|
<div style={{
|
|
width: '100%',
|
|
height: '2px',
|
|
backgroundColor: 'rgba(255, 255, 255, 0.12)',
|
|
borderRadius: '1px',
|
|
marginBottom: '1rem',
|
|
position: 'relative',
|
|
overflow: 'hidden'
|
|
}}>
|
|
{isActive ? (
|
|
<motion.div
|
|
key={`active-bar-${activeIdx}`}
|
|
style={{
|
|
height: '100%',
|
|
backgroundColor: '#ffaa00',
|
|
borderRadius: '1px'
|
|
}}
|
|
initial={{ width: '0%' }}
|
|
animate={{ width: '100%' }}
|
|
transition={{ duration: 4, ease: 'linear' }}
|
|
/>
|
|
) : (
|
|
<div
|
|
style={{
|
|
height: '100%',
|
|
width: '0%',
|
|
backgroundColor: '#ffaa00',
|
|
borderRadius: '1px'
|
|
}}
|
|
/>
|
|
)}
|
|
</div>
|
|
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', minHeight: '24px' }}>
|
|
<span style={{ color: '#94a3b8', fontSize: '0.8rem', display: 'flex', alignItems: 'center', gap: '0.4rem' }}>
|
|
<Clock size={13} style={{ color: '#ffaa00' }} /> {item.readTime}
|
|
</span>
|
|
|
|
{isActive && (
|
|
<motion.span
|
|
initial={{ opacity: 0, x: -10 }}
|
|
animate={{ opacity: 1, x: 0 }}
|
|
transition={{ duration: 0.3 }}
|
|
style={{ color: '#ffaa00', fontSize: '0.85rem', fontWeight: 600, display: 'flex', alignItems: 'center', gap: '0.25rem' }}
|
|
>
|
|
Read More <ArrowRight size={14} />
|
|
</motion.span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</motion.div>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
}
|