updated section design

This commit is contained in:
R-Bharathraj
2026-07-13 18:05:59 +05:30
parent 03ed08d7dd
commit 4dbc330b1d
31 changed files with 513 additions and 121 deletions

View File

@@ -13,7 +13,7 @@
}
.scroll-stack-inner {
padding: 20vh 0 20rem;
padding: 8vh 0 20rem;
min-height: 100vh;
}

View File

@@ -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" />

View 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>
);
}

View File

@@ -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>

View File

@@ -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>