feat: update website layout, components, and assets
This commit is contained in:
@@ -450,7 +450,24 @@ export default function ProductsSection({ isLoaded = false }: ProductsSectionPro
|
||||
|
||||
// Desktop / tablet: pinned horizontal scroll
|
||||
mm.add('(min-width: 901px)', () => {
|
||||
const cards = gsap.utils.toArray<HTMLElement>('.products-card', track);
|
||||
const distance = () => Math.max(0, track.scrollWidth - section.clientWidth);
|
||||
const steps = Math.max(1, PRODUCTS.length - 1);
|
||||
|
||||
// Depth cue: the card nearest the viewport centre sits at full scale/
|
||||
// opacity, neighbours ease back slightly — makes the ride feel guided
|
||||
// rather than a flat strip sliding past, and the eased response
|
||||
// absorbs small scrub jitter instead of showing it as a raw jump.
|
||||
const updateDepth = () => {
|
||||
const centerX = section.clientWidth / 2;
|
||||
cards.forEach((card) => {
|
||||
const trackX = Number(gsap.getProperty(track, 'x', 'px'));
|
||||
const cardCenter = card.offsetLeft + card.offsetWidth / 2 + trackX;
|
||||
const dist = Math.abs(cardCenter - centerX);
|
||||
const norm = gsap.utils.clamp(0, 1, dist / (section.clientWidth * 0.62));
|
||||
gsap.set(card, { scale: 1 - norm * 0.07, opacity: 1 - norm * 0.32 });
|
||||
});
|
||||
};
|
||||
|
||||
const tween = gsap.to(track, {
|
||||
x: () => -distance(),
|
||||
@@ -460,30 +477,42 @@ export default function ProductsSection({ isLoaded = false }: ProductsSectionPro
|
||||
start: 'top top',
|
||||
end: () => `+=${distance()}`,
|
||||
pin: true,
|
||||
pinType: 'transform',
|
||||
pinSpacing: true,
|
||||
anticipatePin: 1,
|
||||
scrub: 1,
|
||||
scrub: 0.4,
|
||||
invalidateOnRefresh: true,
|
||||
refreshPriority: 5,
|
||||
snap: {
|
||||
snapTo: (value) => Math.round(value * steps) / steps,
|
||||
duration: { min: 0.2, max: 0.5 },
|
||||
ease: 'power2.inOut',
|
||||
},
|
||||
onUpdate: (self) => {
|
||||
if (progressRef.current) progressRef.current.style.transform = `scaleX(${self.progress})`;
|
||||
if (counterRef.current) {
|
||||
const idx = Math.min(PRODUCTS.length, Math.max(1, Math.round(self.progress * (PRODUCTS.length - 1)) + 1));
|
||||
const idx = Math.min(PRODUCTS.length, Math.max(1, Math.round(self.progress * steps) + 1));
|
||||
counterRef.current.textContent = `0${idx}`;
|
||||
}
|
||||
updateDepth();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
updateDepth();
|
||||
ScrollTrigger.refresh();
|
||||
return () => { tween.scrollTrigger?.kill(); tween.kill(); gsap.set(track, { clearProps: 'transform' }); };
|
||||
return () => {
|
||||
tween.scrollTrigger?.kill();
|
||||
tween.kill();
|
||||
gsap.set(track, { clearProps: 'transform' });
|
||||
gsap.set(cards, { clearProps: 'scale,opacity' });
|
||||
};
|
||||
});
|
||||
|
||||
return () => mm.revert();
|
||||
}, [isLoaded]);
|
||||
|
||||
return (
|
||||
<div className="products-section-wrapper">
|
||||
<section
|
||||
ref={sectionRef}
|
||||
id="products-section"
|
||||
@@ -496,24 +525,26 @@ export default function ProductsSection({ isLoaded = false }: ProductsSectionPro
|
||||
{/* Heading (fixed within the pinned viewport) */}
|
||||
<div className="products-heading">
|
||||
<div style={{ maxWidth: 520 }}>
|
||||
<div style={{ display: 'inline-flex', alignItems: 'center', gap: 9, marginBottom: 18, padding: '6px 16px 6px 8px', borderRadius: 100, background: '#ffffff', border: '1px solid rgba(244,190,40,0.3)', boxShadow: '0 1px 2px rgba(0,0,0,0.03), 0 12px 30px -18px rgba(23,21,18,0.2)' }}>
|
||||
<div style={{ display: 'inline-flex', alignItems: 'center', gap: 9, marginBottom: 20, padding: '6px 16px 6px 8px', borderRadius: 100, background: '#ffffff', border: '1px solid rgba(244,190,40,0.3)', boxShadow: '0 1px 2px rgba(0,0,0,0.03), 0 12px 30px -18px rgba(23,21,18,0.2)' }}>
|
||||
<span style={{ width: 24, height: 24, borderRadius: '50%', display: 'flex', alignItems: 'center', justifyContent: 'center', background: 'rgba(244,190,40,0.15)' }}>
|
||||
<img src="/images/loyalyai-logo.png" alt="" width={15} height={15} style={{ display: 'block', objectFit: 'contain' }} />
|
||||
</span>
|
||||
<span style={{ fontSize: 11, fontWeight: 800, letterSpacing: '0.18em', textTransform: 'uppercase', color: '#8a5a14', fontFamily: 'Sora, sans-serif' }}>Platform</span>
|
||||
<span className="pv-live-dot" style={{ width: 6, height: 6, borderRadius: '50%', background: '#4ade80', display: 'block', marginLeft: 2 }} />
|
||||
</div>
|
||||
<h2 style={{ margin: 0, fontSize: 'clamp(1.9rem, 3.6vw, 3.2rem)', fontWeight: 800, lineHeight: 0.94, letterSpacing: '-0.04em', color: '#111111', fontFamily: 'Sora, sans-serif' }}>
|
||||
Four products.<br />
|
||||
<span style={{ background: 'linear-gradient(105deg, #f4be28 0%, #FF9800 100%)', backgroundSize: '200% auto', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent', animation: 'prodShimmer 3s linear infinite' }}>One ecosystem.</span>
|
||||
<h2 style={{ margin: 0, padding: '0 0 clamp(10px, 1.6vw, 22px)', fontSize: 'clamp(1.6rem, 3vw, 2.7rem)', fontWeight: 800, lineHeight: 0.94, letterSpacing: '-0.04em', color: '#111111', fontFamily: 'Sora, sans-serif', whiteSpace: 'nowrap' }}>
|
||||
Four agents.{' '}
|
||||
<span style={{ background: 'linear-gradient(105deg, #f4be28 0%, #FF9800 100%)', backgroundSize: '200% auto', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent', animation: 'prodShimmer 3s linear infinite' }}>One AI.</span>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="products-heading-hint" style={{ maxWidth: 300, textAlign: 'right', borderRight: '2px solid rgba(244,190,40,0.3)', paddingRight: 'clamp(18px, 2.2vw, 28px)' }}>
|
||||
<p style={{ margin: '0 0 16px', fontSize: 'clamp(0.85rem, 1vw, 1rem)', lineHeight: 1.6, color: 'rgba(0,0,0,0.45)', fontFamily: 'Sora, sans-serif' }}>
|
||||
<div className="products-heading-hint" style={{ maxWidth: 320, textAlign: 'right', borderRight: '2px solid rgba(244,190,40,0.3)', paddingRight: 'clamp(18px, 2.2vw, 28px)' }}>
|
||||
<p style={{ margin: '0 0 18px', fontSize: 'clamp(0.85rem, 1vw, 1rem)', lineHeight: 1.6, color: 'rgba(0,0,0,0.45)', fontFamily: 'Sora, sans-serif' }}>
|
||||
Powering every stage of the retail journey with intelligent AI solutions.
|
||||
</p>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: 10 }}>
|
||||
<span style={{ fontFamily: 'Sora, sans-serif', fontWeight: 600, fontSize: '0.85rem', color: '#8a8072', textTransform: 'uppercase', letterSpacing: '0.08em' }}>Built as one connected platform</span>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'flex-end', gap: 10 }}>
|
||||
<span style={{ fontFamily: 'Sora, sans-serif', fontWeight: 600, fontSize: '0.78rem', color: '#8a8072', textTransform: 'uppercase', letterSpacing: '0.08em' }}>Unified into one platform</span>
|
||||
<span style={{ fontFamily: 'Sora, sans-serif', fontWeight: 800, fontSize: '1.6rem', color: '#f4be28', letterSpacing: '-0.02em' }}>04</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -604,11 +635,17 @@ export default function ProductsSection({ isLoaded = false }: ProductsSectionPro
|
||||
}
|
||||
}
|
||||
|
||||
/* Margin lives on this non-pinned wrapper, not on the pinned section
|
||||
itself — GSAP's ScrollTrigger pin-spacer can miscalculate the
|
||||
reserved space when the pinned element carries its own margin,
|
||||
which shows up as a jump/shake right as the pin engages. */
|
||||
.products-section-wrapper {
|
||||
margin: 20px;
|
||||
}
|
||||
.products-section-root {
|
||||
--pt: clamp(230px, 28vh, 310px);
|
||||
--pb: clamp(64px, 8vh, 96px);
|
||||
width: calc(100% - 40px);
|
||||
margin: 20px 20px;
|
||||
width: 100%;
|
||||
border-radius: 32px;
|
||||
background: #FAFAFA;
|
||||
height: 100vh;
|
||||
@@ -681,5 +718,6 @@ export default function ProductsSection({ isLoaded = false }: ProductsSectionPro
|
||||
}
|
||||
`}</style>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user