updated section design
This commit is contained in:
@@ -13,7 +13,7 @@
|
||||
}
|
||||
|
||||
.scroll-stack-inner {
|
||||
padding: 20vh 0 20rem;
|
||||
padding: 8vh 0 20rem;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ interface ScrollStackProps {
|
||||
blurAmount?: number;
|
||||
hideBehind?: boolean;
|
||||
useWindowScroll?: boolean;
|
||||
disabled?: boolean;
|
||||
onStackComplete?: () => void;
|
||||
}
|
||||
|
||||
@@ -45,6 +46,7 @@ const ScrollStack = ({
|
||||
blurAmount = 0,
|
||||
hideBehind = false,
|
||||
useWindowScroll = false,
|
||||
disabled = false,
|
||||
onStackComplete,
|
||||
}: ScrollStackProps) => {
|
||||
const scrollerRef = useRef<HTMLDivElement>(null);
|
||||
@@ -288,6 +290,20 @@ const ScrollStack = ({
|
||||
cardsRef.current = cards;
|
||||
const transformsCache = lastTransformsRef.current;
|
||||
|
||||
// Disabled (e.g. on mobile): clear any pinning transforms so the cards
|
||||
// flow as a normal scrollable list, and skip the whole sticky-stack setup.
|
||||
if (disabled) {
|
||||
cards.forEach((card) => {
|
||||
card.style.transform = '';
|
||||
card.style.filter = '';
|
||||
card.style.opacity = '';
|
||||
card.style.marginBottom = '';
|
||||
card.style.willChange = '';
|
||||
});
|
||||
transformsCache.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
cards.forEach((card, i) => {
|
||||
if (i < cards.length - 1) {
|
||||
card.style.marginBottom = `${itemDistance}px`;
|
||||
@@ -323,7 +339,7 @@ const ScrollStack = ({
|
||||
isUpdatingRef.current = false;
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [itemDistance, itemScale, itemStackDistance, stackPosition, scaleEndPosition, baseScale, rotationAmount, blurAmount, useWindowScroll, onStackComplete, setupLenis, updateCardTransforms, measurePositions]);
|
||||
}, [itemDistance, itemScale, itemStackDistance, stackPosition, scaleEndPosition, baseScale, rotationAmount, blurAmount, useWindowScroll, disabled, onStackComplete, setupLenis, updateCardTransforms, measurePositions]);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -331,7 +347,7 @@ const ScrollStack = ({
|
||||
ref={scrollerRef}
|
||||
style={useWindowScroll ? { overflow: 'visible', height: 'auto', position: 'static' } : undefined}
|
||||
>
|
||||
<div className="scroll-stack-inner">
|
||||
<div className="scroll-stack-inner" style={disabled ? { padding: 0, minHeight: 0 } : undefined}>
|
||||
{children}
|
||||
{/* Spacer so the last pin can release cleanly */}
|
||||
<div className="scroll-stack-end" />
|
||||
|
||||
124
src/components/SectionEyebrow.tsx
Normal file
124
src/components/SectionEyebrow.tsx
Normal file
@@ -0,0 +1,124 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
/* ── Section eyebrow — the small uppercase label that sits above a section
|
||||
heading. One component, several distinct motifs, so each section on a page
|
||||
can carry a visually unique kicker while staying consistent in quality.
|
||||
|
||||
variant:
|
||||
'line' — a gold rule that fades out, then the label
|
||||
'bar' — a short vertical gradient bar + label
|
||||
'pill' — label inside a soft bordered gold pill
|
||||
'bracket' — label framed by two L-shaped corner brackets
|
||||
'dot' — a pulsing gold dot + label
|
||||
'number' — a large index figure + hairline divider + label
|
||||
|
||||
`index` is used by the 'number' variant (e.g. "01").
|
||||
Colours are gold-accented and read on both light and dark backgrounds. ── */
|
||||
|
||||
const ACCENT = '#f4be28';
|
||||
|
||||
export type EyebrowVariant = 'line' | 'bar' | 'pill' | 'bracket' | 'dot' | 'number';
|
||||
|
||||
export default function SectionEyebrow({
|
||||
children,
|
||||
variant = 'line',
|
||||
index,
|
||||
style,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
variant?: EyebrowVariant;
|
||||
index?: string;
|
||||
style?: React.CSSProperties;
|
||||
}) {
|
||||
const label: React.CSSProperties = {
|
||||
fontFamily: 'Sora, sans-serif', fontWeight: 700, textTransform: 'uppercase',
|
||||
fontSize: 13, letterSpacing: '0.22em', color: ACCENT, lineHeight: 1,
|
||||
whiteSpace: 'nowrap',
|
||||
};
|
||||
const row: React.CSSProperties = {
|
||||
display: 'inline-flex', alignItems: 'center', gap: 14, ...style,
|
||||
};
|
||||
|
||||
if (variant === 'pill') {
|
||||
return (
|
||||
<span style={{
|
||||
...row, gap: 0, padding: '10px 20px', borderRadius: 999,
|
||||
border: `1px solid ${ACCENT}55`, background: `${ACCENT}14`,
|
||||
boxShadow: `0 8px 24px -16px ${ACCENT}`,
|
||||
}}>
|
||||
<span style={{ ...label, fontSize: 12, letterSpacing: '0.2em' }}>{children}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (variant === 'bar') {
|
||||
return (
|
||||
<span style={row}>
|
||||
<span aria-hidden style={{
|
||||
width: 5, height: 24, borderRadius: 3,
|
||||
background: `linear-gradient(180deg, ${ACCENT} 0%, ${ACCENT}55 100%)`,
|
||||
}} />
|
||||
<span style={label}>{children}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (variant === 'bracket') {
|
||||
return (
|
||||
<span style={{ ...row, position: 'relative', gap: 0, padding: '9px 16px' }}>
|
||||
<span aria-hidden style={{ position: 'absolute', top: 0, left: 0, width: 12, height: 12, borderTop: `2px solid ${ACCENT}`, borderLeft: `2px solid ${ACCENT}` }} />
|
||||
<span aria-hidden style={{ position: 'absolute', bottom: 0, right: 0, width: 12, height: 12, borderBottom: `2px solid ${ACCENT}`, borderRight: `2px solid ${ACCENT}` }} />
|
||||
<span style={label}>{children}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (variant === 'dot') {
|
||||
return (
|
||||
<span style={row}>
|
||||
<span aria-hidden className="se-dot" style={{
|
||||
position: 'relative', width: 9, height: 9, borderRadius: '50%', background: ACCENT, flexShrink: 0,
|
||||
}} />
|
||||
<span style={label}>{children}</span>
|
||||
<style>{`
|
||||
.se-dot::after {
|
||||
content: ''; position: absolute; inset: -5px; border-radius: 50%;
|
||||
border: 1.5px solid ${ACCENT}; opacity: 0.6;
|
||||
animation: seDotPulse 2s ease-out infinite;
|
||||
}
|
||||
@keyframes seDotPulse {
|
||||
0% { transform: scale(0.6); opacity: 0.7; }
|
||||
100% { transform: scale(1.5); opacity: 0; }
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) { .se-dot::after { animation: none; } }
|
||||
`}</style>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (variant === 'number') {
|
||||
return (
|
||||
<span style={{ ...row, gap: 16 }}>
|
||||
<span aria-hidden style={{
|
||||
fontFamily: 'Sora, sans-serif', fontWeight: 800, fontSize: 26, lineHeight: 1,
|
||||
letterSpacing: '-0.02em', color: ACCENT,
|
||||
}}>{index}</span>
|
||||
<span aria-hidden style={{ width: 1, height: 22, background: `${ACCENT}66` }} />
|
||||
<span style={label}>{children}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
// 'line' (default)
|
||||
return (
|
||||
<span style={row}>
|
||||
<span aria-hidden style={{
|
||||
width: 46, height: 2, borderRadius: 2,
|
||||
background: `linear-gradient(90deg, ${ACCENT} 0%, ${ACCENT}00 100%)`,
|
||||
}} />
|
||||
<span style={label}>{children}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -3,7 +3,6 @@
|
||||
import React, { useEffect, useLayoutEffect, useRef } from 'react';
|
||||
import { gsap } from 'gsap';
|
||||
import { ScrollTrigger } from 'gsap/ScrollTrigger';
|
||||
import { PhoneMockupImage } from './PhoneMockup';
|
||||
import { FpTagCard, FpStatCard } from './FpCard';
|
||||
import { useCountUp, useStaggerReveal } from './fpMotion';
|
||||
|
||||
@@ -77,14 +76,66 @@ export default function EarnRewardsSection() {
|
||||
<div className="fp-shell" style={{ position: 'relative', zIndex: 1 }}>
|
||||
<div className="fp-split" style={{ gridTemplateColumns: '0.95fr 1.05fr' }}>
|
||||
|
||||
{/* Left — phone mockup, matches the in-app Earn / Home experience */}
|
||||
{/* Left — live LytsUp Earn preview embedded in a real phone frame */}
|
||||
<div ref={phoneRef} style={{ display: 'flex', justifyContent: 'center', opacity: 0, width: '100%' }}>
|
||||
<PhoneMockupImage
|
||||
src="/For_people/01_earn-cropped.png"
|
||||
alt="LytsUp Earn screen showing Lyts balance and activity feed"
|
||||
floatClassName="erm-phone-shell"
|
||||
aspectRatio={1684 / 3196}
|
||||
/>
|
||||
<div
|
||||
className="erm-phone-shell"
|
||||
style={{
|
||||
width: 300, borderRadius: 42, padding: 10,
|
||||
background: 'linear-gradient(160deg, #2a2a2e 0%, #0a0a0c 100%)',
|
||||
boxShadow: '0 60px 120px -24px rgba(0,0,0,0.7), 0 0 0 1px rgba(255,255,255,0.06)',
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
{/* Screen — locked to a real phone's aspect ratio (~9:19.5) */}
|
||||
<div style={{
|
||||
borderRadius: 32, overflow: 'hidden', background: '#ffffff',
|
||||
position: 'relative', aspectRatio: '9 / 19.5',
|
||||
display: 'flex', flexDirection: 'column',
|
||||
}}>
|
||||
{/* Dynamic island */}
|
||||
<div style={{
|
||||
position: 'absolute', top: 8, left: '50%', transform: 'translateX(-50%)',
|
||||
width: 82, height: 22, borderRadius: 14, background: '#000', zIndex: 5,
|
||||
}} />
|
||||
{/* Safe area — reserves the notch strip above the app's top bar */}
|
||||
<div style={{ height: 32, flexShrink: 0, background: '#ffffff' }} />
|
||||
{/* The embed is a Flutter web app built for a real phone's logical
|
||||
width (390px). Rendering it at 390 and scaling to fit keeps the
|
||||
UI proportioned as designed instead of cramped at ~280px. The
|
||||
iframe is overscanned ~16px past the right clip so Flutter's
|
||||
desktop-style list scrollbar falls outside the visible screen. */}
|
||||
<div className="erm-phone-viewport" style={{ position: 'relative', flex: 1, minHeight: 0, overflow: 'hidden' }}>
|
||||
<iframe
|
||||
src="https://anbarasu22004.github.io/lytsup_view/"
|
||||
title="LytsUp Earn screen live preview"
|
||||
loading="lazy"
|
||||
scrolling="no"
|
||||
className="erm-phone-iframe"
|
||||
style={{
|
||||
position: 'absolute', top: 0, left: 0,
|
||||
border: 'none', display: 'block', background: '#ffffff',
|
||||
transformOrigin: 'top left',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<style>{`
|
||||
@keyframes ermPhoneFloat { 0%,100% { transform: translateY(0); } 50% { transform: translateY(-10px); } }
|
||||
.erm-phone-shell { animation: ermPhoneFloat 7s ease-in-out infinite; }
|
||||
/* --pm-scale = screen width (frame - 2*10px padding) ÷ 390px design width */
|
||||
.erm-phone-shell { --pm-scale: calc(280 / 390); }
|
||||
.erm-phone-iframe {
|
||||
width: calc(100% / var(--pm-scale) + 16px);
|
||||
height: calc(100% / var(--pm-scale));
|
||||
transform: scale(var(--pm-scale));
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) { .erm-phone-shell { animation: none !important; } }
|
||||
@media (max-width: 900px) {
|
||||
.erm-phone-shell { width: 280px !important; --pm-scale: calc(260 / 390); }
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right — headline */}
|
||||
@@ -109,12 +160,12 @@ export default function EarnRewardsSection() {
|
||||
</div>
|
||||
|
||||
{/* Stat strip — same card system as the tags above, distinguished by a filled dark surface */}
|
||||
<div ref={statsRef} style={{ display: 'flex', gap: 10, marginBottom: 28, maxWidth: 460 }}>
|
||||
<FpStatCard theme="fill" layout="row" style={{ flex: '1 1 0' }} icon="⚡"
|
||||
<div ref={statsRef} style={{ display: 'flex', flexWrap: 'wrap', gap: 10, marginBottom: 28, maxWidth: 460 }}>
|
||||
<FpStatCard theme="fill" layout="row" style={{ flex: '1 1 130px', minWidth: 0 }} icon="⚡"
|
||||
value={<span ref={waysCountRef}>10</span>} label="ways to earn" />
|
||||
<FpStatCard theme="fill" layout="row" style={{ flex: '1 1 0' }} icon="🔥"
|
||||
<FpStatCard theme="fill" layout="row" style={{ flex: '1 1 130px', minWidth: 0 }} icon="🔥"
|
||||
value="24/7" label="always live" />
|
||||
<FpStatCard theme="fill" layout="row" style={{ flex: '1 1 0' }} icon="🚀"
|
||||
<FpStatCard theme="fill" layout="row" style={{ flex: '1 1 130px', minWidth: 0 }} icon="🚀"
|
||||
value="∞" label="no earn cap" />
|
||||
</div>
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import React, { useEffect, useLayoutEffect, useRef } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { gsap } from 'gsap';
|
||||
import { ScrollTrigger } from 'gsap/ScrollTrigger';
|
||||
import SectionEyebrow from '@/components/SectionEyebrow';
|
||||
|
||||
if (typeof window !== 'undefined') gsap.registerPlugin(ScrollTrigger);
|
||||
|
||||
@@ -144,7 +145,7 @@ export default function ProductsScrollSection() {
|
||||
|
||||
{/* Header */}
|
||||
<div style={{ position: 'relative', zIndex: 2, marginBottom: 'clamp(56px,8vh,96px)' }}>
|
||||
<div style={{ fontSize: 11, fontWeight: 700, letterSpacing: '0.2em', textTransform: 'uppercase', color: '#f4be28', fontFamily: 'Sora, sans-serif', marginBottom: 16 }}>Our Products</div>
|
||||
<SectionEyebrow variant="pill" style={{ marginBottom: 16 }}>Our Products</SectionEyebrow>
|
||||
<h2 style={{ fontSize: 'clamp(32px, 4.5vw, 64px)', fontWeight: 800, letterSpacing: '-0.05em', lineHeight: 0.92, color: '#fff', margin: 0, fontFamily: 'Sora, sans-serif', maxWidth: 640 }}>
|
||||
AI Powered Retail Intelligence.
|
||||
</h2>
|
||||
|
||||
Reference in New Issue
Block a user