diff --git a/public/problems/1.png b/public/problems/1.png new file mode 100644 index 0000000..da909e9 Binary files /dev/null and b/public/problems/1.png differ diff --git a/public/problems/10.png b/public/problems/10.png new file mode 100644 index 0000000..56a8a85 Binary files /dev/null and b/public/problems/10.png differ diff --git a/public/problems/2.png b/public/problems/2.png new file mode 100644 index 0000000..94481b3 Binary files /dev/null and b/public/problems/2.png differ diff --git a/public/problems/3.png b/public/problems/3.png new file mode 100644 index 0000000..813086f Binary files /dev/null and b/public/problems/3.png differ diff --git a/public/problems/4.png b/public/problems/4.png new file mode 100644 index 0000000..23714d3 Binary files /dev/null and b/public/problems/4.png differ diff --git a/public/problems/5.png b/public/problems/5.png new file mode 100644 index 0000000..f0bdfa5 Binary files /dev/null and b/public/problems/5.png differ diff --git a/public/problems/6.png b/public/problems/6.png new file mode 100644 index 0000000..aa888d9 Binary files /dev/null and b/public/problems/6.png differ diff --git a/public/problems/7.png b/public/problems/7.png new file mode 100644 index 0000000..64a39e5 Binary files /dev/null and b/public/problems/7.png differ diff --git a/public/problems/8.png b/public/problems/8.png new file mode 100644 index 0000000..d1c781d Binary files /dev/null and b/public/problems/8.png differ diff --git a/public/problems/9.png b/public/problems/9.png new file mode 100644 index 0000000..8ecf261 Binary files /dev/null and b/public/problems/9.png differ diff --git a/public/retail_cycle/01_Earn_online.png b/public/retail_cycle/01_Earn_online.png new file mode 100644 index 0000000..b279d5b Binary files /dev/null and b/public/retail_cycle/01_Earn_online.png differ diff --git a/public/retail_cycle/02_unlock_deals.png b/public/retail_cycle/02_unlock_deals.png new file mode 100644 index 0000000..c2993d0 Binary files /dev/null and b/public/retail_cycle/02_unlock_deals.png differ diff --git a/public/retail_cycle/03_discover+visits.png b/public/retail_cycle/03_discover+visits.png new file mode 100644 index 0000000..262e30d Binary files /dev/null and b/public/retail_cycle/03_discover+visits.png differ diff --git a/public/retail_cycle/04_redeem+repeat.png b/public/retail_cycle/04_redeem+repeat.png new file mode 100644 index 0000000..ba5e35b Binary files /dev/null and b/public/retail_cycle/04_redeem+repeat.png differ diff --git a/public/shift/1.png b/public/shift/1.png new file mode 100644 index 0000000..6a339f5 Binary files /dev/null and b/public/shift/1.png differ diff --git a/public/shift/2.png b/public/shift/2.png new file mode 100644 index 0000000..f8abec5 Binary files /dev/null and b/public/shift/2.png differ diff --git a/public/shift/3.png b/public/shift/3.png new file mode 100644 index 0000000..878efc9 Binary files /dev/null and b/public/shift/3.png differ diff --git a/public/shift/4.png b/public/shift/4.png new file mode 100644 index 0000000..075076c Binary files /dev/null and b/public/shift/4.png differ diff --git a/public/shift/5.png b/public/shift/5.png new file mode 100644 index 0000000..00764dc Binary files /dev/null and b/public/shift/5.png differ diff --git a/public/shift/6.png b/public/shift/6.png new file mode 100644 index 0000000..56a7534 Binary files /dev/null and b/public/shift/6.png differ diff --git a/public/shift/7.png b/public/shift/7.png new file mode 100644 index 0000000..376cbe6 Binary files /dev/null and b/public/shift/7.png differ diff --git a/public/shift/8.png b/public/shift/8.png new file mode 100644 index 0000000..b664d5f Binary files /dev/null and b/public/shift/8.png differ diff --git a/src/app/contacts/page.tsx b/src/app/contacts/page.tsx index b07c162..91933fe 100644 --- a/src/app/contacts/page.tsx +++ b/src/app/contacts/page.tsx @@ -55,7 +55,7 @@ export default function ContactsPage() {
{[ { label: 'loyalydigital@gmail.com', href: 'mailto:loyalydigital@gmail.com' }, - { label: 'Coimbatore, TN', href: undefined }, + { label: 'Hyderabad, TG', href: undefined }, ].map((c) => { const inner = ( Loyal Customer

- Loyaly.ai turns everyday digital engagement into real world shopping rewards. Customers earn coins through games and daily activities, then redeem them at offline stores near them. + Loyaly.ai turns everyday digital engagement into real world shopping rewards. Customers earn LYT through games and daily activities, then redeem them at offline stores near them.

@@ -445,7 +445,7 @@ export default function IndexPage() { Loyal Customer

- Loyaly.ai turns everyday digital engagement into real world shopping rewards. Customers earn coins through games and daily activities, then redeem them at offline stores near them. + Loyaly.ai turns everyday digital engagement into real world shopping rewards. Customers earn LYT through games and daily activities, then redeem them at offline stores near them.

diff --git a/src/app/solutions_page/page.tsx b/src/app/solutions_page/page.tsx index af3fcc8..03c7d2a 100644 --- a/src/app/solutions_page/page.tsx +++ b/src/app/solutions_page/page.tsx @@ -5,6 +5,7 @@ import Link from 'next/link'; import Image from 'next/image'; import GSAPAnimator from '@/components/GSAPAnimator'; import { useReveal } from '@/hooks/useReveal'; +import ScrollStack, { ScrollStackItem } from '@/components/ScrollStack'; function RevealBlock({ children, delay = 0 }: { children: React.ReactNode; delay?: number }) { const { ref, visible } = useReveal(); @@ -17,6 +18,53 @@ function RevealBlock({ children, delay = 0 }: { children: React.ReactNode; delay ); } +const PRODUCTS = [ + { + n: '01', title: 'Behavision', subtitle: 'AI Powered Footfall Analytics', color: '#f4be28', + desc: 'AI powered footfall analytics and behavioral intelligence for physical retail. Behavision turns every camera and sensor you already have into a live map of how customers move, where they linger, and what makes them walk away, so store layout and staffing decisions are based on evidence, not guesswork.', + capabilities: [ + 'Real-time footfall counting, accurate to 98% across every entry point', + 'Dwell-time and zone heatmaps that show exactly where attention concentrates', + 'Entry-to-purchase funnel tracking that flags where shoppers drop off', + ], + metric: { val: '3.2×', lbl: 'conversion uplift' }, + tags: ['Footfall Tracking', 'Heatmaps', 'Behavior Analysis'], + }, + { + n: '02', title: 'SpendSense', subtitle: 'Customer Spending Intelligence', color: '#10B981', + desc: 'Deep, continuously updating intelligence on how your customers spend. SpendSense studies basket size, purchase frequency, and category preferences across every visit, surfacing the high-value segments and the early warning signs of churn well before a customer stops coming back.', + capabilities: [ + 'Automatic customer segmentation by spend, frequency, and loyalty tier', + 'Predictive churn scoring that flags at-risk high-value shoppers early', + 'Revenue opportunity alerts triggered by shifting basket-size trends', + ], + metric: { val: '68%', lbl: 'retention improvement' }, + tags: ['Spending Trends', 'Revenue Analytics', 'Customer Value'], + }, + { + n: '03', title: 'Dyscount', subtitle: 'Smart Discount Engine', color: '#EF4444', + desc: 'A discount engine that treats margin as a budget, not an afterthought. Dyscount decides who gets an offer, how deep it goes, and when it fires, targeting the customers most likely to convert while capping every discount so promotions never quietly eat into profit.', + capabilities: [ + 'Dynamic offer targeting based on real-time purchase intent signals', + 'Margin-safe discount caps enforced per product, category, or campaign', + 'A/B tested offer formats that keep conversion up without overspending', + ], + metric: { val: '41%', lbl: 'discount waste reduced' }, + tags: ['Smart Discounts', 'Offer Automation', 'Margin Protection'], + }, + { + n: '04', title: 'Identiq', subtitle: 'Consumer Loyalty Platform', color: '#f4be28', + desc: 'The consumer-facing app that closes the loop. Identiq turns daily logins, walking challenges, and quick mini-games into LYT, Loyaly’s reward token, redeemable instantly at any partner store, so every idle moment on a phone becomes a reason to walk into a real one.', + capabilities: [ + 'Daily login and step-challenge rewards that build a lasting habit loop', + 'Game based offers like Spin Wheel, Scratch Card, and Maze Challenge', + 'Instant offline redemption at any partner store, no card required', + ], + metric: { val: '50K+', lbl: 'active shoppers' }, + tags: ['Daily Rewards', 'Game Based Offers', 'Offline Redemption'], + }, +]; + const CAPS = [ { n: '01', title: 'Merchant Campaign Platform', color: '#f4be28', @@ -139,6 +187,83 @@ export default function SolutionsPage() {
+ {/* ── OUR PRODUCTS ── */} +
+
+ +
Our Products
+

+ Four products.
One ecosystem. +

+

+ Each product solves a distinct part of the retail journey, and together they form one connected platform, from footfall to spend to loyalty. +

+
+ +
+ + {PRODUCTS.map((p, i) => ( + +
+
+ + {/* Left — identity + description */} +
+
+ {p.n} + + {p.subtitle} +
+

{p.title}

+

{p.desc}

+
+ {p.tags.map((tag) => ( + {tag} + ))} +
+
+ + {/* Right — metric + capabilities */} +
+
+
{p.metric.val}
+
{p.metric.lbl}
+
+ +
+ {p.capabilities.map((cap) => ( +
+ + + + {cap} +
+ ))} +
+
+
+
+
+ ))} +
+
+
+ {/* ── WHY IT MATTERS ── */}
@@ -192,6 +317,7 @@ export default function SolutionsPage() { .sp-cap-row { grid-template-columns: 1fr !important; gap: 14px !important; padding: 28px 0 !important; } .sp-cap-row > div:last-child { justify-content: flex-start !important; } .sp-cta-grid { grid-template-columns: 1fr !important; gap: 40px !important; } + .sp-product-grid { grid-template-columns: 1fr !important; } } @media (max-width: 480px) { .sp-cta-grid > div > div { grid-template-columns: 1fr !important; } diff --git a/src/app/use_cases/page.tsx b/src/app/use_cases/page.tsx index 0da64bf..36f6926 100644 --- a/src/app/use_cases/page.tsx +++ b/src/app/use_cases/page.tsx @@ -5,30 +5,7 @@ import Link from 'next/link'; import Image from 'next/image'; import GSAPAnimator from '@/components/GSAPAnimator'; import { useReveal } from '@/hooks/useReveal'; - -const PRODUCTS = [ - { - name: 'Behavision', - tag: 'Footfall AI', - desc: 'Track footfall, measure conversions, and understand in-store behavior with precision heatmaps and dwell-time analytics.', - metric: '3.2×', metricLabel: 'conversion uplift', - color: '#f4be28', - }, - { - name: 'SpendSense', - tag: 'Spend Intelligence', - desc: 'Analyze spending patterns and identify high-value customers before your competition does.', - metric: '68%', metricLabel: 'retention improvement', - color: '#a78bfa', - }, - { - name: 'Dyscount', - tag: 'Smart Discounts', - desc: 'Deliver targeted discounts that actually convert, without killing your margins.', - metric: '41%', metricLabel: 'discount waste reduced', - color: '#34d399', - }, -]; +import ProductsScrollSection from '@/components/use-cases/ProductsScrollSection'; const OUTCOMES = [ { stat: '3.2×', label: 'Conversion rate increase' }, @@ -70,7 +47,6 @@ function RevealBlock({ children, delay = 0, className = '' }: { children: React. } export default function ForBusinessPage() { - const [activeProduct, setActiveProduct] = useState(0); const [countVisible, setCountVisible] = useState(false); const statsRef = useRef(null); @@ -184,60 +160,7 @@ export default function ForBusinessPage() {
{/* ── PRODUCTS ── */} -
- -
Our Products
-

- AI Powered Retail Intelligence. -

-
- - {/* Tab selector */} -
- {PRODUCTS.map((p, i) => ( - - ))} -
- - {/* Product detail */} - {PRODUCTS.map((p, i) => ( -
-
-
{p.tag}
-

{p.name}

-

{p.desc}

- Learn More → -
-
-
{p.metric}
-
{p.metricLabel}
-
-
- ))} -
+ {/* ── OUTCOMES ── */}
@@ -314,7 +237,6 @@ export default function ForBusinessPage() { @keyframes floatY { 0%,100%{transform:translateY(0)} 50%{transform:translateY(-12px)} } @media (max-width: 860px) { .uc-challenge-grid { grid-template-columns: 1fr !important; gap: 40px !important; } - .uc-product-grid { grid-template-columns: 1fr !important; gap: 40px !important; } .uc-outcomes-grid { grid-template-columns: repeat(2,1fr) !important; } .uc-outcomes-grid > div > div { border-radius: 16px !important; } .uc-who-grid { grid-template-columns: repeat(2,1fr) !important; } diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx index 3496ca6..f22618b 100644 --- a/src/components/Footer.tsx +++ b/src/components/Footer.tsx @@ -10,7 +10,7 @@ const NAV = [ links: [ { label: 'For People', href: '/for_people' }, { label: 'For Business', href: '/use_cases' }, - { label: 'Solutions', href: '/solutions_page' }, + { label: 'Meet the Crew', href: '/solutions_page' }, ], }, { @@ -24,7 +24,7 @@ const NAV = [ { title: 'Company', links: [ - { label: 'Meet the Crew', href: '/about_us' }, + { label: 'About us', href: '/about_us' }, { label: 'Market Insights', href: '/blog_main' }, { label: 'Contact Us', href: '/contacts' }, ], diff --git a/src/components/GetInTouch.tsx b/src/components/GetInTouch.tsx index e886bf2..7463b5d 100644 --- a/src/components/GetInTouch.tsx +++ b/src/components/GetInTouch.tsx @@ -7,7 +7,7 @@ type FormState = 'idle' | 'sending' | 'success' | 'error'; const DETAILS = [ { label: 'Visit us', - value: <>No.424, Red Rose Towers,
DB Road, RS Puram,
Coimbatore – 641002, + value: <>5th Floor, Vision Ultima,
Street No.3, Jayabheri Enclave,
Gachibowli, Hyderabad, Telangana 500032, icon: ( diff --git a/src/components/Header.tsx b/src/components/Header.tsx index a8a4e4c..871ed58 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -9,9 +9,9 @@ const NAV_LINKS = [ { label: 'Home', href: '/' }, { label: 'For People', href: '/for_people' }, { label: 'For Business', href: '/use_cases' }, - { label: 'Solutions', href: '/solutions_page' }, + { label: 'Meet the Crew', href: '/solutions_page' }, { label: 'Blogs', href: '/blog_main' }, - { label: 'Meet the Crew', href: '/about_us' }, + { label: 'About us', href: '/about_us' }, ]; const MOBILE_QUERY = '(max-width: 1023px)'; @@ -155,7 +155,7 @@ export default function Header() {

Contacts

-

No.424, Red Rose Towers,
DB Road, RS Puram
Coimbatore – 641002

+

5th Floor, Vision Ultima,
Street No.3, Jayabheri Enclave,
Gachibowli, Hyderabad, Telangana 500032

loyalydigital@gmail.com diff --git a/src/components/MagicBento.css b/src/components/MagicBento.css new file mode 100644 index 0000000..7b46f80 --- /dev/null +++ b/src/components/MagicBento.css @@ -0,0 +1,174 @@ +.card-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + grid-auto-rows: minmax(120px, auto); + gap: clamp(10px, 1.4vw, 16px); + width: 100%; + max-width: 100%; + font-family: 'Sora', sans-serif; +} + +/* Asymmetric bento layout for exactly 5 cards, desktop only */ +@media (min-width: 900px) { + .card-grid > *:nth-child(1) { grid-column: 1; grid-row: 1; } + .card-grid > *:nth-child(2) { grid-column: 2; grid-row: 1; } + .card-grid > *:nth-child(3) { grid-column: 3 / span 2; grid-row: 1 / span 2; } + .card-grid > *:nth-child(4) { grid-column: 1 / span 2; grid-row: 2 / span 2; } + .card-grid > *:nth-child(5) { grid-column: 3 / span 2; grid-row: 3; } +} + +.magic-bento-card { + display: flex; + flex-direction: column; + justify-content: space-between; + position: relative; + min-height: 170px; + width: 100%; + max-width: 100%; + padding: clamp(22px, 2.4vw, 32px); + border-radius: 20px; + border: 1px solid rgba(255, 255, 255, 0.14); + background: rgba(255, 255, 255, 0.015); + font-weight: 300; + overflow: hidden; + transition: box-shadow 0.3s ease, border-color 0.3s ease; + + --glow-x: 50%; + --glow-y: 50%; + --glow-intensity: 0; + --glow-radius: 200px; +} + +.magic-bento-card:hover { + border-color: rgba(255, 255, 255, 0.24); +} + +.magic-bento-card__header, +.magic-bento-card__content { + display: flex; + position: relative; + z-index: 2; +} + +.magic-bento-card__header { + justify-content: flex-start; +} + +.magic-bento-card__content { + flex-direction: column; +} + +.magic-bento-card__icon { + width: 22px; + height: 22px; + display: flex; + align-items: center; + justify-content: center; + color: rgb(var(--glow-color)); +} + +.magic-bento-card__title { + font-weight: 700; + font-size: 1.15rem; + letter-spacing: -0.01em; + color: #ffffff; + margin: 0 0 6px; +} + +.magic-bento-card__description { + font-size: 0.85rem; + line-height: 1.5; + color: rgba(255, 255, 255, 0.5); + margin: 0; +} + +.magic-bento-card--text-autohide .magic-bento-card__title, +.magic-bento-card--text-autohide .magic-bento-card__description { + display: -webkit-box; + -webkit-box-orient: vertical; + overflow: hidden; + text-overflow: ellipsis; +} + +.magic-bento-card--text-autohide .magic-bento-card__title { + -webkit-line-clamp: 1; + line-clamp: 1; +} + +.magic-bento-card--text-autohide .magic-bento-card__description { + -webkit-line-clamp: 3; + line-clamp: 3; +} + +@media (max-width: 899px) { + .card-grid { + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + grid-auto-rows: auto; + } +} + +@media (max-width: 480px) { + .card-grid { + grid-template-columns: 1fr; + } +} + +/* Border glow effect, follows the cursor */ +.magic-bento-card--border-glow::after { + content: ''; + position: absolute; + inset: 0; + padding: 1.5px; + background: radial-gradient( + var(--glow-radius) circle at var(--glow-x) var(--glow-y), + rgba(var(--glow-color), calc(var(--glow-intensity) * 0.9)) 0%, + rgba(var(--glow-color), calc(var(--glow-intensity) * 0.45)) 30%, + transparent 60% + ); + border-radius: inherit; + -webkit-mask: + linear-gradient(#fff 0 0) content-box, + linear-gradient(#fff 0 0); + -webkit-mask-composite: xor; + mask: + linear-gradient(#fff 0 0) content-box, + linear-gradient(#fff 0 0); + mask-composite: exclude; + pointer-events: none; + opacity: 1; + transition: opacity 0.3s ease; + z-index: 1; +} + +.magic-bento-card--border-glow:hover { + box-shadow: 0 24px 48px -20px rgba(0, 0, 0, 0.6), 0 0 30px rgba(var(--glow-color), 0.25); +} + +.particle-container { + position: relative; + overflow: hidden; +} + +.particle::before { + content: ''; + position: absolute; + top: -2px; + left: -2px; + right: -2px; + bottom: -2px; + background: rgba(var(--glow-color), 0.2); + border-radius: 50%; + z-index: -1; +} + +.global-spotlight { + mix-blend-mode: screen; + will-change: transform, opacity; + z-index: 200 !important; + pointer-events: none; +} + +.bento-section { + position: relative; + user-select: none; +} diff --git a/src/components/MagicBento.tsx b/src/components/MagicBento.tsx new file mode 100644 index 0000000..e1d0aa0 --- /dev/null +++ b/src/components/MagicBento.tsx @@ -0,0 +1,529 @@ +'use client'; + +import React, { useRef, useEffect, useCallback, useState } from 'react'; +import { gsap } from 'gsap'; +import './MagicBento.css'; + +const DEFAULT_PARTICLE_COUNT = 12; +const DEFAULT_SPOTLIGHT_RADIUS = 300; +const DEFAULT_GLOW_COLOR = '132, 0, 255'; +const MOBILE_BREAKPOINT = 768; + +export interface BentoCardItem { + icon: React.ReactNode; + title: string; + description: string; + label?: string; +} + +const createParticleElement = (x: number, y: number, color = DEFAULT_GLOW_COLOR) => { + const el = document.createElement('div'); + el.className = 'particle'; + el.style.cssText = ` + position: absolute; + width: 4px; + height: 4px; + border-radius: 50%; + background: rgba(${color}, 1); + box-shadow: 0 0 6px rgba(${color}, 0.6); + pointer-events: none; + z-index: 100; + left: ${x}px; + top: ${y}px; + `; + return el; +}; + +const calculateSpotlightValues = (radius: number) => ({ + proximity: radius * 0.5, + fadeDistance: radius * 0.75, +}); + +const updateCardGlowProperties = (card: HTMLElement, mouseX: number, mouseY: number, glow: number, radius: number) => { + const rect = card.getBoundingClientRect(); + const relativeX = ((mouseX - rect.left) / rect.width) * 100; + const relativeY = ((mouseY - rect.top) / rect.height) * 100; + + card.style.setProperty('--glow-x', `${relativeX}%`); + card.style.setProperty('--glow-y', `${relativeY}%`); + card.style.setProperty('--glow-intensity', glow.toString()); + card.style.setProperty('--glow-radius', `${radius}px`); +}; + +interface ParticleCardProps { + children: React.ReactNode; + className?: string; + disableAnimations?: boolean; + style?: React.CSSProperties; + particleCount?: number; + glowColor?: string; + enableTilt?: boolean; + clickEffect?: boolean; + enableMagnetism?: boolean; +} + +export const ParticleCard = ({ + children, + className = '', + disableAnimations = false, + style, + particleCount = DEFAULT_PARTICLE_COUNT, + glowColor = DEFAULT_GLOW_COLOR, + enableTilt = true, + clickEffect = false, + enableMagnetism = false, +}: ParticleCardProps) => { + const cardRef = useRef(null); + const particlesRef = useRef([]); + const timeoutsRef = useRef[]>([]); + const isHoveredRef = useRef(false); + const memoizedParticles = useRef([]); + const particlesInitialized = useRef(false); + const magnetismAnimationRef = useRef(null); + + const initializeParticles = useCallback(() => { + if (particlesInitialized.current || !cardRef.current) return; + + const { width, height } = cardRef.current.getBoundingClientRect(); + memoizedParticles.current = Array.from({ length: particleCount }, () => + createParticleElement(Math.random() * width, Math.random() * height, glowColor) + ); + particlesInitialized.current = true; + }, [particleCount, glowColor]); + + const clearAllParticles = useCallback(() => { + timeoutsRef.current.forEach(clearTimeout); + timeoutsRef.current = []; + magnetismAnimationRef.current?.kill(); + + particlesRef.current.forEach(particle => { + gsap.to(particle, { + scale: 0, + opacity: 0, + duration: 0.3, + ease: 'back.in(1.7)', + onComplete: () => { + particle.parentNode?.removeChild(particle); + }, + }); + }); + particlesRef.current = []; + }, []); + + const animateParticles = useCallback(() => { + if (!cardRef.current || !isHoveredRef.current) return; + + if (!particlesInitialized.current) { + initializeParticles(); + } + + memoizedParticles.current.forEach((particle, index) => { + const timeoutId = setTimeout(() => { + if (!isHoveredRef.current || !cardRef.current) return; + + const clone = particle.cloneNode(true) as HTMLDivElement; + cardRef.current.appendChild(clone); + particlesRef.current.push(clone); + + gsap.fromTo(clone, { scale: 0, opacity: 0 }, { scale: 1, opacity: 1, duration: 0.3, ease: 'back.out(1.7)' }); + + gsap.to(clone, { + x: (Math.random() - 0.5) * 100, + y: (Math.random() - 0.5) * 100, + rotation: Math.random() * 360, + duration: 2 + Math.random() * 2, + ease: 'none', + repeat: -1, + yoyo: true, + }); + + gsap.to(clone, { + opacity: 0.3, + duration: 1.5, + ease: 'power2.inOut', + repeat: -1, + yoyo: true, + }); + }, index * 100); + + timeoutsRef.current.push(timeoutId); + }); + }, [initializeParticles]); + + useEffect(() => { + if (disableAnimations || !cardRef.current) return; + + const element = cardRef.current; + + const handleMouseEnter = () => { + isHoveredRef.current = true; + animateParticles(); + + if (enableTilt) { + gsap.to(element, { + rotateX: 5, + rotateY: 5, + duration: 0.3, + ease: 'power2.out', + transformPerspective: 1000, + }); + } + }; + + const handleMouseLeave = () => { + isHoveredRef.current = false; + clearAllParticles(); + + if (enableTilt) { + gsap.to(element, { + rotateX: 0, + rotateY: 0, + duration: 0.3, + ease: 'power2.out', + }); + } + + if (enableMagnetism) { + gsap.to(element, { + x: 0, + y: 0, + duration: 0.3, + ease: 'power2.out', + }); + } + }; + + const handleMouseMove = (e: MouseEvent) => { + if (!enableTilt && !enableMagnetism) return; + + const rect = element.getBoundingClientRect(); + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + const centerX = rect.width / 2; + const centerY = rect.height / 2; + + if (enableTilt) { + const rotateX = ((y - centerY) / centerY) * -10; + const rotateY = ((x - centerX) / centerX) * 10; + + gsap.to(element, { + rotateX, + rotateY, + duration: 0.1, + ease: 'power2.out', + transformPerspective: 1000, + }); + } + + if (enableMagnetism) { + const magnetX = (x - centerX) * 0.05; + const magnetY = (y - centerY) * 0.05; + + magnetismAnimationRef.current = gsap.to(element, { + x: magnetX, + y: magnetY, + duration: 0.3, + ease: 'power2.out', + }); + } + }; + + const handleClick = (e: MouseEvent) => { + if (!clickEffect) return; + + const rect = element.getBoundingClientRect(); + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + + const maxDistance = Math.max( + Math.hypot(x, y), + Math.hypot(x - rect.width, y), + Math.hypot(x, y - rect.height), + Math.hypot(x - rect.width, y - rect.height) + ); + + const ripple = document.createElement('div'); + ripple.style.cssText = ` + position: absolute; + width: ${maxDistance * 2}px; + height: ${maxDistance * 2}px; + border-radius: 50%; + background: radial-gradient(circle, rgba(${glowColor}, 0.4) 0%, rgba(${glowColor}, 0.2) 30%, transparent 70%); + left: ${x - maxDistance}px; + top: ${y - maxDistance}px; + pointer-events: none; + z-index: 1000; + `; + + element.appendChild(ripple); + + gsap.fromTo( + ripple, + { scale: 0, opacity: 1 }, + { + scale: 1, + opacity: 0, + duration: 0.8, + ease: 'power2.out', + onComplete: () => ripple.remove(), + } + ); + }; + + element.addEventListener('mouseenter', handleMouseEnter); + element.addEventListener('mouseleave', handleMouseLeave); + element.addEventListener('mousemove', handleMouseMove); + element.addEventListener('click', handleClick); + + return () => { + isHoveredRef.current = false; + element.removeEventListener('mouseenter', handleMouseEnter); + element.removeEventListener('mouseleave', handleMouseLeave); + element.removeEventListener('mousemove', handleMouseMove); + element.removeEventListener('click', handleClick); + clearAllParticles(); + }; + }, [animateParticles, clearAllParticles, disableAnimations, enableTilt, enableMagnetism, clickEffect, glowColor]); + + return ( +
+ {children} +
+ ); +}; + +interface GlobalSpotlightProps { + gridRef: React.RefObject; + disableAnimations?: boolean; + enabled?: boolean; + spotlightRadius?: number; + glowColor?: string; +} + +const GlobalSpotlight = ({ + gridRef, + disableAnimations = false, + enabled = true, + spotlightRadius = DEFAULT_SPOTLIGHT_RADIUS, + glowColor = DEFAULT_GLOW_COLOR, +}: GlobalSpotlightProps) => { + const spotlightRef = useRef(null); + + useEffect(() => { + if (disableAnimations || !gridRef?.current || !enabled) return; + + const spotlight = document.createElement('div'); + spotlight.className = 'global-spotlight'; + spotlight.style.cssText = ` + position: fixed; + width: 800px; + height: 800px; + border-radius: 50%; + pointer-events: none; + background: radial-gradient(circle, + rgba(${glowColor}, 0.15) 0%, + rgba(${glowColor}, 0.08) 15%, + rgba(${glowColor}, 0.04) 25%, + rgba(${glowColor}, 0.02) 40%, + rgba(${glowColor}, 0.01) 65%, + transparent 70% + ); + z-index: 200; + opacity: 0; + transform: translate(-50%, -50%); + mix-blend-mode: screen; + `; + document.body.appendChild(spotlight); + spotlightRef.current = spotlight; + + const handleMouseMove = (e: MouseEvent) => { + if (!spotlightRef.current || !gridRef.current) return; + + const section = gridRef.current.closest('.bento-section'); + const rect = section?.getBoundingClientRect(); + const mouseInside = + rect && e.clientX >= rect.left && e.clientX <= rect.right && e.clientY >= rect.top && e.clientY <= rect.bottom; + + const cards = gridRef.current.querySelectorAll('.magic-bento-card'); + + if (!mouseInside) { + gsap.to(spotlightRef.current, { opacity: 0, duration: 0.3, ease: 'power2.out' }); + cards.forEach(card => card.style.setProperty('--glow-intensity', '0')); + return; + } + + const { proximity, fadeDistance } = calculateSpotlightValues(spotlightRadius); + let minDistance = Infinity; + + cards.forEach(card => { + const cardRect = card.getBoundingClientRect(); + const centerX = cardRect.left + cardRect.width / 2; + const centerY = cardRect.top + cardRect.height / 2; + const distance = + Math.hypot(e.clientX - centerX, e.clientY - centerY) - Math.max(cardRect.width, cardRect.height) / 2; + const effectiveDistance = Math.max(0, distance); + + minDistance = Math.min(minDistance, effectiveDistance); + + let glowIntensity = 0; + if (effectiveDistance <= proximity) { + glowIntensity = 1; + } else if (effectiveDistance <= fadeDistance) { + glowIntensity = (fadeDistance - effectiveDistance) / (fadeDistance - proximity); + } + + updateCardGlowProperties(card, e.clientX, e.clientY, glowIntensity, spotlightRadius); + }); + + gsap.to(spotlightRef.current, { left: e.clientX, top: e.clientY, duration: 0.1, ease: 'power2.out' }); + + const targetOpacity = + minDistance <= proximity + ? 0.8 + : minDistance <= fadeDistance + ? ((fadeDistance - minDistance) / (fadeDistance - proximity)) * 0.8 + : 0; + + gsap.to(spotlightRef.current, { + opacity: targetOpacity, + duration: targetOpacity > 0 ? 0.2 : 0.5, + ease: 'power2.out', + }); + }; + + const handleMouseLeave = () => { + gridRef.current?.querySelectorAll('.magic-bento-card').forEach(card => { + card.style.setProperty('--glow-intensity', '0'); + }); + if (spotlightRef.current) { + gsap.to(spotlightRef.current, { opacity: 0, duration: 0.3, ease: 'power2.out' }); + } + }; + + document.addEventListener('mousemove', handleMouseMove); + document.addEventListener('mouseleave', handleMouseLeave); + + return () => { + document.removeEventListener('mousemove', handleMouseMove); + document.removeEventListener('mouseleave', handleMouseLeave); + spotlightRef.current?.parentNode?.removeChild(spotlightRef.current); + }; + }, [gridRef, disableAnimations, enabled, spotlightRadius, glowColor]); + + return null; +}; + +const useMobileDetection = () => { + const [isMobile, setIsMobile] = useState(false); + + useEffect(() => { + const checkMobile = () => setIsMobile(window.innerWidth <= MOBILE_BREAKPOINT); + checkMobile(); + window.addEventListener('resize', checkMobile); + return () => window.removeEventListener('resize', checkMobile); + }, []); + + return isMobile; +}; + +export interface MagicBentoProps { + items: BentoCardItem[]; + textAutoHide?: boolean; + enableStars?: boolean; + enableSpotlight?: boolean; + enableBorderGlow?: boolean; + disableAnimations?: boolean; + spotlightRadius?: number; + particleCount?: number; + enableTilt?: boolean; + glowColor?: string; + clickEffect?: boolean; + enableMagnetism?: boolean; + cardBackground?: string; + className?: string; +} + +export default function MagicBento({ + items, + textAutoHide = true, + enableStars = true, + enableSpotlight = true, + enableBorderGlow = true, + disableAnimations = false, + spotlightRadius = DEFAULT_SPOTLIGHT_RADIUS, + particleCount = DEFAULT_PARTICLE_COUNT, + enableTilt = false, + glowColor = DEFAULT_GLOW_COLOR, + clickEffect = true, + enableMagnetism = true, + cardBackground = '#120F17', + className = '', +}: MagicBentoProps) { + const gridRef = useRef(null); + const isMobile = useMobileDetection(); + const shouldDisableAnimations = disableAnimations || isMobile; + + return ( + <> + {enableSpotlight && ( + + )} + +
+ {items.map((card, index) => { + const baseClassName = `magic-bento-card ${textAutoHide ? 'magic-bento-card--text-autohide' : ''} ${enableBorderGlow ? 'magic-bento-card--border-glow' : ''}`; + const cardStyle: React.CSSProperties = { + backgroundColor: cardBackground, + ['--glow-color' as string]: glowColor, + }; + + const cardBody = ( + <> +
+
{card.icon}
+
+
+

{card.title}

+

{card.description}

+
+ + ); + + if (enableStars) { + return ( + + {cardBody} + + ); + } + + return ( +
+ {cardBody} +
+ ); + })} +
+ + ); +} diff --git a/src/components/MapSection.tsx b/src/components/MapSection.tsx index 07e9138..5d28249 100644 --- a/src/components/MapSection.tsx +++ b/src/components/MapSection.tsx @@ -1,8 +1,8 @@ 'use client'; -const LAT = 11.004985; -const LNG = 76.950933; -const ADDRESS = 'No.424, Red Rose Towers, DB Road, RS Puram, Coimbatore – 641002'; +const LAT = 17.4406; +const LNG = 78.3489; +const ADDRESS = '5th Floor, Vision Ultima, Street No.3, Jayabheri Enclave, Gachibowli, Hyderabad, Telangana 500032'; export default function MapSection() { const embedUrl = `https://maps.google.com/maps?q=${LAT},${LNG}&z=16&output=embed&iwloc=near`; @@ -45,7 +45,7 @@ export default function MapSection() { In Coimbatore + }}>In Hyderabad

, }, { - label: 'City', value: 'Coimbatore, Tamil Nadu, India', + label: 'City', value: 'Hyderabad, Telangana, India', svg: <>, }, ].map((item, i) => ( diff --git a/src/components/PhoneWidget.tsx b/src/components/PhoneWidget.tsx index a102a5f..2915eda 100644 --- a/src/components/PhoneWidget.tsx +++ b/src/components/PhoneWidget.tsx @@ -101,7 +101,7 @@ export default function PhoneWidget() {

Loyaly ✦
- {/* Points card */} + {/* LYT balance card */}
-
Total Points
+
Total LYT
284
≈ ₹284 redeemable
@@ -151,7 +151,7 @@ export default function PhoneWidget() { }}>
-
+120 pts
+
+120 LYT
just now
diff --git a/src/components/ScrollStack.css b/src/components/ScrollStack.css new file mode 100644 index 0000000..797ff8e --- /dev/null +++ b/src/components/ScrollStack.css @@ -0,0 +1,39 @@ +.scroll-stack-scroller { + position: relative; + width: 100%; + height: 100%; + overflow-y: auto; + overflow-x: visible; + overscroll-behavior: contain; + -webkit-overflow-scrolling: touch; + scroll-behavior: smooth; + -webkit-transform: translateZ(0); + transform: translateZ(0); + will-change: scroll-position; +} + +.scroll-stack-inner { + padding: 20vh 0 20rem; + min-height: 100vh; +} + +.scroll-stack-card { + transform-origin: top center; + will-change: transform, filter; + backface-visibility: hidden; + transform-style: preserve-3d; + box-shadow: 0 30px 60px -20px rgba(0, 0, 0, 0.5); + width: 100%; + margin: 30px 0; + border-radius: 28px; + overflow: hidden; + box-sizing: border-box; + -webkit-transform: translateZ(0); + transform: translateZ(0); + position: relative; +} + +.scroll-stack-end { + width: 100%; + height: 1px; +} diff --git a/src/components/ScrollStack.tsx b/src/components/ScrollStack.tsx new file mode 100644 index 0000000..d618d18 --- /dev/null +++ b/src/components/ScrollStack.tsx @@ -0,0 +1,338 @@ +'use client'; + +import React, { useLayoutEffect, useRef, useCallback, ReactNode } from 'react'; +import Lenis from 'lenis'; +import './ScrollStack.css'; + +export const ScrollStackItem = ({ children, itemClassName = '' }: { children: ReactNode; itemClassName?: string }) => ( +
{children}
+); + +interface CardTransform { + translateY: number; + scale: number; + rotation: number; + blur: number; +} + +interface ScrollStackProps { + children: ReactNode; + className?: string; + itemDistance?: number; + itemScale?: number; + itemStackDistance?: number; + stackPosition?: string; + scaleEndPosition?: string; + baseScale?: number; + scaleDuration?: number; + rotationAmount?: number; + blurAmount?: number; + useWindowScroll?: boolean; + onStackComplete?: () => void; +} + +const ScrollStack = ({ + children, + className = '', + itemDistance = 100, + itemScale = 0.03, + itemStackDistance = 30, + stackPosition = '20%', + scaleEndPosition = '10%', + baseScale = 0.85, + rotationAmount = 0, + blurAmount = 0, + useWindowScroll = false, + onStackComplete, +}: ScrollStackProps) => { + const scrollerRef = useRef(null); + const stackCompletedRef = useRef(false); + const animationFrameRef = useRef(null); + const lenisRef = useRef(null); + const cardsRef = useRef([]); + const lastTransformsRef = useRef>(new Map()); + const isUpdatingRef = useRef(false); + // Cached, transform-independent layout positions — re-reading getBoundingClientRect() + // every frame would pick up each card's own translateY from the previous frame and + // feed back into itself, causing jitter. These are measured once (and on resize). + const cardTopsRef = useRef([]); + const endTopRef = useRef(0); + + const calculateProgress = useCallback((scrollTop: number, start: number, end: number) => { + if (scrollTop < start) return 0; + if (scrollTop > end) return 1; + return (scrollTop - start) / (end - start); + }, []); + + const parsePercentage = useCallback((value: string | number, containerHeight: number) => { + if (typeof value === 'string' && value.includes('%')) { + return (parseFloat(value) / 100) * containerHeight; + } + return parseFloat(value as string); + }, []); + + const getScrollData = useCallback(() => { + if (useWindowScroll) { + return { + scrollTop: window.scrollY, + containerHeight: window.innerHeight, + }; + } else { + const scroller = scrollerRef.current!; + return { + scrollTop: scroller.scrollTop, + containerHeight: scroller.clientHeight, + }; + } + }, [useWindowScroll]); + + // Measures each card's static document position with any transform temporarily + // cleared, so the cached value is never contaminated by our own animation. + const measurePositions = useCallback(() => { + const cards = cardsRef.current; + const prevTransforms = cards.map(c => c.style.transform); + cards.forEach(c => { c.style.transform = 'none'; }); + + const endElement = useWindowScroll + ? document.querySelector('.scroll-stack-end') + : scrollerRef.current?.querySelector('.scroll-stack-end'); + const prevEndTransform = endElement?.style.transform; + if (endElement) endElement.style.transform = 'none'; + + if (useWindowScroll) { + cardTopsRef.current = cards.map(c => c.getBoundingClientRect().top + window.scrollY); + endTopRef.current = endElement ? endElement.getBoundingClientRect().top + window.scrollY : 0; + } else { + cardTopsRef.current = cards.map(c => c.offsetTop); + endTopRef.current = endElement ? endElement.offsetTop : 0; + } + + cards.forEach((c, i) => { c.style.transform = prevTransforms[i]; }); + if (endElement && prevEndTransform !== undefined) endElement.style.transform = prevEndTransform; + }, [useWindowScroll]); + + const updateCardTransforms = useCallback(() => { + if (!cardsRef.current.length || isUpdatingRef.current) return; + + isUpdatingRef.current = true; + + const { scrollTop, containerHeight } = getScrollData(); + const stackPositionPx = parsePercentage(stackPosition, containerHeight); + const scaleEndPositionPx = parsePercentage(scaleEndPosition, containerHeight); + + const endElementTop = endTopRef.current; + + cardsRef.current.forEach((card, i) => { + if (!card) return; + + const cardTop = cardTopsRef.current[i] ?? 0; + const triggerStart = cardTop - stackPositionPx - itemStackDistance * i; + const triggerEnd = cardTop - scaleEndPositionPx; + const pinStart = cardTop - stackPositionPx - itemStackDistance * i; + const pinEnd = endElementTop - containerHeight / 2; + + const scaleProgress = calculateProgress(scrollTop, triggerStart, triggerEnd); + const targetScale = baseScale + i * itemScale; + const scale = 1 - scaleProgress * (1 - targetScale); + const rotation = rotationAmount ? i * rotationAmount * scaleProgress : 0; + + let blur = 0; + if (blurAmount) { + let topCardIndex = 0; + for (let j = 0; j < cardsRef.current.length; j++) { + const jCardTop = cardTopsRef.current[j] ?? 0; + const jTriggerStart = jCardTop - stackPositionPx - itemStackDistance * j; + if (scrollTop >= jTriggerStart) { + topCardIndex = j; + } + } + + if (i < topCardIndex) { + const depthInStack = topCardIndex - i; + blur = Math.max(0, depthInStack * blurAmount); + } + } + + let translateY = 0; + const isPinned = scrollTop >= pinStart && scrollTop <= pinEnd; + + if (isPinned) { + translateY = scrollTop - cardTop + stackPositionPx + itemStackDistance * i; + } else if (scrollTop > pinEnd) { + translateY = pinEnd - cardTop + stackPositionPx + itemStackDistance * i; + } + + const newTransform: CardTransform = { + translateY: Math.round(translateY * 100) / 100, + scale: Math.round(scale * 1000) / 1000, + rotation: Math.round(rotation * 100) / 100, + blur: Math.round(blur * 100) / 100, + }; + + const lastTransform = lastTransformsRef.current.get(i); + const hasChanged = + !lastTransform || + Math.abs(lastTransform.translateY - newTransform.translateY) > 0.1 || + Math.abs(lastTransform.scale - newTransform.scale) > 0.001 || + Math.abs(lastTransform.rotation - newTransform.rotation) > 0.1 || + Math.abs(lastTransform.blur - newTransform.blur) > 0.1; + + if (hasChanged) { + const transform = `translate3d(0, ${newTransform.translateY}px, 0) scale(${newTransform.scale}) rotate(${newTransform.rotation}deg)`; + const filter = newTransform.blur > 0 ? `blur(${newTransform.blur}px)` : ''; + + card.style.transform = transform; + card.style.filter = filter; + + lastTransformsRef.current.set(i, newTransform); + } + + if (i === cardsRef.current.length - 1) { + const isInView = scrollTop >= pinStart && scrollTop <= pinEnd; + if (isInView && !stackCompletedRef.current) { + stackCompletedRef.current = true; + onStackComplete?.(); + } else if (!isInView && stackCompletedRef.current) { + stackCompletedRef.current = false; + } + } + }); + + isUpdatingRef.current = false; + }, [ + itemScale, + itemStackDistance, + stackPosition, + scaleEndPosition, + baseScale, + rotationAmount, + blurAmount, + useWindowScroll, + onStackComplete, + calculateProgress, + parsePercentage, + getScrollData, + ]); + + const handleScroll = useCallback(() => { + updateCardTransforms(); + }, [updateCardTransforms]); + + const setupLenis = useCallback(() => { + if (useWindowScroll) { + const lenis = new Lenis({ + duration: 1.2, + easing: (t: number) => Math.min(1, 1.001 - Math.pow(2, -10 * t)), + smoothWheel: true, + touchMultiplier: 2, + wheelMultiplier: 1, + syncTouch: true, + syncTouchLerp: 0.075, + }); + + lenis.on('scroll', handleScroll); + + const raf = (time: number) => { + lenis.raf(time); + animationFrameRef.current = requestAnimationFrame(raf); + }; + animationFrameRef.current = requestAnimationFrame(raf); + + lenisRef.current = lenis; + return lenis; + } else { + const scroller = scrollerRef.current; + if (!scroller) return; + + const lenis = new Lenis({ + wrapper: scroller, + content: scroller.querySelector('.scroll-stack-inner') as HTMLElement, + duration: 1.2, + easing: (t: number) => Math.min(1, 1.001 - Math.pow(2, -10 * t)), + smoothWheel: true, + touchMultiplier: 2, + gestureOrientation: 'vertical', + wheelMultiplier: 1, + syncTouch: true, + syncTouchLerp: 0.075, + }); + + lenis.on('scroll', handleScroll); + + const raf = (time: number) => { + lenis.raf(time); + animationFrameRef.current = requestAnimationFrame(raf); + }; + animationFrameRef.current = requestAnimationFrame(raf); + + lenisRef.current = lenis; + return lenis; + } + }, [handleScroll, useWindowScroll]); + + useLayoutEffect(() => { + const scroller = scrollerRef.current; + if (!scroller) return; + + const cards = Array.from( + useWindowScroll + ? document.querySelectorAll('.scroll-stack-card') + : scroller.querySelectorAll('.scroll-stack-card') + ); + + cardsRef.current = cards; + const transformsCache = lastTransformsRef.current; + + cards.forEach((card, i) => { + if (i < cards.length - 1) { + card.style.marginBottom = `${itemDistance}px`; + } + card.style.willChange = 'transform, filter'; + card.style.transformOrigin = 'top center'; + card.style.backfaceVisibility = 'hidden'; + card.style.transform = 'translateZ(0)'; + }); + + measurePositions(); + setupLenis(); + + updateCardTransforms(); + + const handleResize = () => { + measurePositions(); + updateCardTransforms(); + }; + window.addEventListener('resize', handleResize); + + return () => { + window.removeEventListener('resize', handleResize); + if (animationFrameRef.current) { + cancelAnimationFrame(animationFrameRef.current); + } + if (lenisRef.current) { + lenisRef.current.destroy(); + } + stackCompletedRef.current = false; + cardsRef.current = []; + transformsCache.clear(); + isUpdatingRef.current = false; + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [itemDistance, itemScale, itemStackDistance, stackPosition, scaleEndPosition, baseScale, rotationAmount, blurAmount, useWindowScroll, onStackComplete, setupLenis, updateCardTransforms, measurePositions]); + + return ( +
+
+ {children} + {/* Spacer so the last pin can release cleanly */} +
+
+
+ ); +}; + +export default ScrollStack; diff --git a/src/components/Solutions.tsx b/src/components/Solutions.tsx index 3ab7019..e7c7257 100644 --- a/src/components/Solutions.tsx +++ b/src/components/Solutions.tsx @@ -44,7 +44,7 @@ const SOLUTIONS = [ title: 'Loyalty Management', subtitle: 'Build Lasting Relationships', excerpt: - 'Reward customer loyalty with smart programmes that adapt in real time — from points and tiers to personalised perks that feel genuinely earned.', + 'Reward customer loyalty with smart programmes that adapt in real time — from LYT and tiers to personalised perks that feel genuinely earned.', image: '/loyaly_php/assets/images/cognivision-transformative-ai-for-intelligent-fi-640x640.jpg', imageAlt: 'Loyalty management module showing customer tier performance', href: '/solutions/loyalty-management', diff --git a/src/components/for-people/AIRecommendationsSection.tsx b/src/components/for-people/AIRecommendationsSection.tsx index 6f3b6d9..4ba420b 100644 --- a/src/components/for-people/AIRecommendationsSection.tsx +++ b/src/components/for-people/AIRecommendationsSection.tsx @@ -1,28 +1,72 @@ 'use client'; -import React, { useEffect, useRef, useState } from 'react'; +import React, { useLayoutEffect, useEffect, useRef, useState } from 'react'; +import { gsap } from 'gsap'; +import { ScrollTrigger } from 'gsap/ScrollTrigger'; + +if (typeof window !== 'undefined') gsap.registerPlugin(ScrollTrigger); + +const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect; const recs = [ - { rank: '01', label: 'Double points at Starbucks', match: '98%', tag: 'Near you · 0.3km' }, - { rank: '02', label: 'Nike Air sale ends tonight', match: '94%', tag: 'Trending · Fashion' }, - { rank: '03', label: 'Decathlon weekend special', match: '91%', tag: 'Sports · 1.4km' }, - { rank: '04', label: 'Zara new arrivals, 15% off', match: '87%', tag: 'Your style' }, + { rank: '01', label: 'Earn 2× LYT at Starbucks', match: 98, tag: 'Near you · 0.3km' }, + { rank: '02', label: 'Nike Air sale ends tonight', match: 94, tag: 'Trending · Fashion' }, + { rank: '03', label: 'Decathlon weekend special', match: 91, tag: 'Sports · 1.4km' }, + { rank: '04', label: 'Zara new arrivals, 15% off', match: 87, tag: 'Your style' }, ]; export default function AIRecommendationsSection() { - const [active, setActive] = useState(false); const [hovered, setHovered] = useState(null); const panelRef = useRef(null); + const rowRefs = useRef<(HTMLDivElement | null)[]>([]); + const matchRefs = useRef<(HTMLDivElement | null)[]>([]); + const statRefs = useRef<(HTMLDivElement | null)[]>([]); + const barFillRef = useRef(null); - useEffect(() => { - const el = panelRef.current; - if (!el) return; - if (el.hasAttribute('data-active')) setActive(true); - const obs = new MutationObserver(() => { - if (el.hasAttribute('data-active')) setActive(true); - }); - obs.observe(el, { attributes: true, attributeFilter: ['data-active'] }); - return () => obs.disconnect(); + useIsomorphicLayoutEffect(() => { + if (typeof window === 'undefined') return; + const section = panelRef.current; + if (!section) return; + + const ctx = gsap.context(() => { + const tl = gsap.timeline({ + scrollTrigger: { trigger: section, start: 'top 78%', toggleActions: 'play none none reset' }, + }); + + // Stat chips: scale + count up + tl.fromTo('.ai-stat-chip', { opacity: 0, y: 20, scale: 0.88 }, { opacity: 1, y: 0, scale: 1, duration: 0.55, stagger: 0.1, ease: 'back.out(1.8)' }, 0); + statRefs.current.forEach((el, i) => { + if (!el) return; + const target = i === 0 ? 94 : 3; + const suffix = i === 0 ? '%' : 'ms'; + const counter = { val: 0 }; + tl.to(counter, { + val: target, duration: 0.9, ease: 'power2.out', + onUpdate: () => { el.textContent = `${Math.round(counter.val)}${suffix}`; }, + }, 0.1 + i * 0.1); + }); + + // Recommendation rows: slide + fade + scale in + tl.fromTo('.ai-rec-row', { opacity: 0, x: 64, scale: 0.96 }, { opacity: 1, x: 0, scale: 1, duration: 0.65, stagger: 0.12, ease: 'power3.out' }, 0.15); + + // Match percentages count up, following each row + recs.forEach((rec, i) => { + const el = matchRefs.current[i]; + if (!el) return; + const counter = { val: 0 }; + tl.to(counter, { + val: rec.match, duration: 0.6, ease: 'power2.out', + onUpdate: () => { el.textContent = `${Math.round(counter.val)}%`; }, + }, 0.35 + i * 0.12); + }); + + // Confidence bar fill, synced after rows finish + tl.fromTo(barFillRef.current, { width: '0%' }, { width: '92%', duration: 1, ease: 'power2.out' }, 0.9); + + ScrollTrigger.refresh(); + }, section); + + return () => ctx.revert(); }, []); return ( @@ -32,7 +76,7 @@ export default function AIRecommendationsSection() { {/* Left — editorial */}
-
AI Intelligence · 05
+
AI Intelligence05

- {/* Stat chips — brand gold palette */} -
+ {/* Stat chips — brand gold palette, count up on scroll */} +
{[ - { val: '94%', lbl: 'match accuracy' }, - { val: '3ms', lbl: 'response time' }, + { lbl: 'match accuracy' }, + { lbl: 'response time' }, ].map((s, i) => ( -
-
{s.val}
+
+
{ statRefs.current[i] = el; }} style={{ fontSize: 28, fontWeight: 900, color: '#bc7122', fontFamily: 'Sora, sans-serif', letterSpacing: '-0.03em' }}>0{i === 0 ? '%' : 'ms'}
{s.lbl}
))} @@ -78,16 +122,17 @@ export default function AIRecommendationsSection() { {recs.map((rec, i) => (
{ rowRefs.current[i] = el; }} onMouseEnter={() => setHovered(i)} onMouseLeave={() => setHovered(null)} + className="ai-rec-row" style={{ display: 'flex', alignItems: 'center', gap: 14, padding: '16px 18px', borderRadius: 16, background: hovered === i ? '#fff7e0' : '#fafafa', border: `2px solid ${hovered === i ? '#f4be28' : 'rgba(23,21,18,0.07)'}`, cursor: 'default', - transition: 'all 0.4s cubic-bezier(0.16,1,0.3,1)', - transform: `translateX(${active ? 0 : 48}px)`, opacity: active ? 1 : 0, - transitionDelay: `${0.12 + i * 0.1}s`, + transition: 'background 0.35s ease, border-color 0.35s ease', + opacity: 0, }} > {/* Rank */} @@ -102,13 +147,13 @@ export default function AIRecommendationsSection() {
{rec.tag}
- {/* Match badge — brand gold */} + {/* Match badge — brand gold, counts up on scroll */}
-
{rec.match}
+
{ matchRefs.current[i] = el; }} style={{ fontSize: 14, fontWeight: 900, color: '#bc7122', fontFamily: 'Sora, sans-serif' }}>0%
match
@@ -124,10 +169,9 @@ export default function AIRecommendationsSection() { }}>High
-
diff --git a/src/components/for-people/DiscoverDealsSection.tsx b/src/components/for-people/DiscoverDealsSection.tsx index 54411b7..7e66588 100644 --- a/src/components/for-people/DiscoverDealsSection.tsx +++ b/src/components/for-people/DiscoverDealsSection.tsx @@ -4,7 +4,7 @@ import React, { useState } from 'react'; import Image from 'next/image'; const deals = [ - { store: 'Starbucks', category: 'Coffee & Drinks', offer: '2× Points', savings: '₹120 saved', color: '#00704A', distance: '0.3 km', rating: 4.8 }, + { store: 'Starbucks', category: 'Coffee & Drinks', offer: '2× LYT', savings: '₹120 saved', color: '#00704A', distance: '0.3 km', rating: 4.8 }, { store: 'Nike', category: 'Footwear', offer: '30% OFF', savings: '₹890 saved', color: '#ffffff', distance: '0.7 km', rating: 4.9 }, { store: 'H&M', category: 'Fashion', offer: '₹200 Back', savings: '₹200 saved', color: '#E50010', distance: '1.1 km', rating: 4.6 }, { store: 'Decathlon', category: 'Sports', offer: 'Free Gift', savings: '₹350 saved', color: '#0082C8', distance: '1.4 km', rating: 4.7 }, @@ -32,7 +32,7 @@ export default function DiscoverDealsSection() { {/* Headline column */}
-
Discover · 02
+
Discover02

- Find participating stores nearby and see their active game based discount campaigns, live coupons, and coin earning opportunities, all surfaced in real time so you never miss a deal. + Find participating stores nearby and see their active game based discount campaigns, live coupons, and LYT earning opportunities, all surfaced in real time so you never miss a deal.

setHovered(null)} className="fp-rise" style={{ + position: 'relative', overflow: 'hidden', display: 'grid', gridTemplateColumns: '46px 1fr auto', alignItems: 'center', gap: 16, padding: '16px 20px', borderRadius: 16, background: hovered === i ? 'rgba(244,190,40,0.06)' : 'rgba(255,255,255,0.03)', @@ -80,6 +81,17 @@ export default function DiscoverDealsSection() { transform: `${hovered === i ? 'translateX(8px)' : 'translateX(0)'}`, }} > + {/* Bottom border sweep, left to right on hover */} +
(null); const rafRef = useRef(0); + const rowRefs = useRef<(HTMLDivElement | null)[]>([]); + const activeRowRef = useRef(0); + + // Move the highlighted feed card as the section scrolls through the viewport + useEffect(() => { + if (typeof window === 'undefined') return; + const el = panelRef.current; + if (!el) return; + + const applyRow = (idx: number) => { + if (idx === activeRowRef.current) return; + activeRowRef.current = idx; + rowRefs.current.forEach((row, i) => { + if (!row) return; + const active = i === idx; + row.style.background = active ? '#16a34a' : 'rgba(23,21,18,0.55)'; + row.style.borderColor = active ? '#16a34a' : 'rgba(255,255,255,0.14)'; + row.style.transform = active ? 'translateX(10px) scale(1.06)' : 'translateX(0) scale(1)'; + row.style.boxShadow = active ? '0 16px 40px -12px rgba(22,163,74,0.5)' : 'none'; + row.style.zIndex = active ? '2' : '1'; + + // Flip inner text/icon colors for contrast per background + const title = row.querySelector('.feed-title'); + if (title) title.style.color = '#ffffff'; + const time = row.querySelector('.feed-time'); + if (time) time.style.color = active ? 'rgba(255,255,255,0.75)' : 'rgba(255,255,255,0.55)'; + const icon = row.querySelector('.feed-icon'); + if (icon) { + icon.style.background = active ? 'rgba(255,255,255,0.15)' : 'rgba(74,222,128,0.1)'; + icon.style.borderColor = active ? 'rgba(255,255,255,0.5)' : 'rgba(74,222,128,0.45)'; + icon.style.color = active ? '#ffffff' : '#4ade80'; + } + const points = row.querySelector('.feed-pts'); + if (points) { + points.style.color = active ? '#ffffff' : '#4ade80'; + points.style.background = active ? 'rgba(255,255,255,0.15)' : 'rgba(74,222,128,0.12)'; + } + }); + }; + + const st = ScrollTrigger.create({ + trigger: el, + start: 'top 65%', + end: 'bottom 60%', + onUpdate: (self) => { + applyRow(Math.min(feed.length - 1, Math.floor(self.progress * feed.length))); + }, + }); + + return () => st.kill(); + }, []); useEffect(() => { const el = panelRef.current; @@ -48,7 +103,7 @@ export default function EarnRewardsSection() { background: '#f0fdf4', border: '1.5px solid #16a34a', borderRadius: 100, padding: '7px 16px', }}> - Live points balance + Live LYT balance
{/* Giant gold number */} @@ -70,7 +125,7 @@ export default function EarnRewardsSection() { redeemable value
-
Earn · 03
+
Earn03

- Earn Loyaly Coins through daily app login, completing 5,000-step walking challenges, playing Maze, Spin Wheel, Scratch Card, and Match Master games, or visiting Selfie Booths at partner malls. Every activity puts more coins in your wallet. + Earn LYT (Loyaly Tokens) through daily app login, completing 5,000-step walking challenges, playing Maze, Spin Wheel, Scratch Card, and Match Master games, or visiting Selfie Booths at partner malls. Every activity adds more LYT to your balance.

@@ -107,31 +162,39 @@ export default function EarnRewardsSection() { {/* Feed rows */}
{feed.map((tx, i) => ( -
{ rowRefs.current[i] = el; }} style={{ + position: 'relative', zIndex: i === 0 ? 2 : 1, display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '12px 14px', borderRadius: 12, - background: i === 0 ? '#f0fdf4' : '#fafafa', - border: `1.5px solid ${i === 0 ? '#16a34a' : 'rgba(23,21,18,0.07)'}`, + background: i === 0 ? '#16a34a' : 'rgba(23,21,18,0.55)', + border: `1.5px solid ${i === 0 ? '#16a34a' : 'rgba(255,255,255,0.14)'}`, + backdropFilter: 'blur(16px)', + WebkitBackdropFilter: 'blur(16px)', + transform: i === 0 ? 'translateX(10px) scale(1.06)' : 'translateX(0) scale(1)', + boxShadow: i === 0 ? '0 16px 40px -12px rgba(22,163,74,0.5)' : 'none', + transition: 'background 0.35s ease, border-color 0.35s ease, transform 0.35s cubic-bezier(0.34,1.56,0.64,1), box-shadow 0.35s ease', }}>
-
{tx.type === 'earn' ? '↑' : '↓'}
-
{tx.store}
-
{tx.time}
+
{tx.store}
+
{tx.time}
- {tx.pts > 0 ? '+' : ''}{tx.pts} @@ -144,7 +207,7 @@ export default function EarnRewardsSection() { marginTop: 16, display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10, }}> {[ - { label: 'Earned this month', val: '660 pts', color: '#16a34a', bg: '#f0fdf4', border: '#16a34a' }, + { label: 'Earned this month', val: '660 LYT', color: '#16a34a', bg: '#f0fdf4', border: '#16a34a' }, { label: 'Redeemable value', val: '₹660', color: '#f4be28', bg: '#fff7e0', border: '#f4be28' }, ].map((s, i) => (
diff --git a/src/components/for-people/ExperienceSection.tsx b/src/components/for-people/ExperienceSection.tsx index 054aea7..4ca62d6 100644 --- a/src/components/for-people/ExperienceSection.tsx +++ b/src/components/for-people/ExperienceSection.tsx @@ -11,7 +11,6 @@ const words = [ const steps = [ { n: '01', label: 'Walk into any partner store' }, { n: '02', label: 'Pay the way you always do' }, - { n: '03', label: 'Points land in your wallet' }, ]; export default function ExperienceSection() { @@ -20,7 +19,7 @@ export default function ExperienceSection() {
- The Experience · 06 + The Experience06
diff --git a/src/components/for-people/FinalCTASection.tsx b/src/components/for-people/FinalCTASection.tsx index 5180b7c..79e867c 100644 --- a/src/components/for-people/FinalCTASection.tsx +++ b/src/components/for-people/FinalCTASection.tsx @@ -1,7 +1,13 @@ 'use client'; -import React, { useRef, useEffect } from 'react'; +import React, { useRef, useEffect, useLayoutEffect } from 'react'; import Link from 'next/link'; +import { gsap } from 'gsap'; +import { ScrollTrigger } from 'gsap/ScrollTrigger'; + +if (typeof window !== 'undefined') gsap.registerPlugin(ScrollTrigger); + +const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect; function useMagnetic() { const ref = useRef(null); @@ -10,9 +16,9 @@ function useMagnetic() { if (!el) return; const onMove = (e: MouseEvent) => { const r = el.getBoundingClientRect(); - el.style.transform = `translate(${((e.clientX - r.left) / r.width - 0.5) * 18}px,${((e.clientY - r.top) / r.height - 0.5) * 18}px) scale(1.04)`; + el.style.transform = `translate(${((e.clientX - r.left) / r.width - 0.5) * 16}px,${((e.clientY - r.top) / r.height - 0.5) * 16}px) scale(1.04)`; }; - const onLeave = () => { el.style.transform = 'translate(0,0) scale(1)'; }; + const onLeave = () => { el.style.transform = ''; }; el.addEventListener('mousemove', onMove); el.addEventListener('mouseleave', onLeave); return () => { el.removeEventListener('mousemove', onMove); el.removeEventListener('mouseleave', onLeave); }; @@ -20,97 +26,191 @@ function useMagnetic() { return ref; } -const proof = [ - { val: '50K+', label: 'Active users', color: '#f4be28' }, - { val: '10K+', label: 'Partner stores', color: '#16a34a' }, - { val: '₹28L+', label: 'Rewards given', color: '#2563eb' }, - { val: '4.9★', label: 'App rating', color: '#9333ea' }, +const PROOF = [ + { val: '50K+', label: 'Active users', color: '#f4be28', bg: '#fffbeb', border: 'rgba(244,190,40,0.3)' }, + { val: '10K+', label: 'Partner stores', color: '#16a34a', bg: '#f0fdf4', border: 'rgba(22,163,74,0.25)' }, + { val: '₹28L+', label: 'Rewards given', color: '#2563eb', bg: '#eff6ff', border: 'rgba(37,99,235,0.2)' }, + { val: '4.9★', label: 'App rating', color: '#9333ea', bg: '#faf5ff', border: 'rgba(147,51,234,0.2)' }, ]; export default function FinalCTASection() { + const sectionRef = useRef(null); const mag1 = useMagnetic(); const mag2 = useMagnetic(); - return ( -
+ useIsomorphicLayoutEffect(() => { + if (typeof window === 'undefined') return; + const ctx = gsap.context(() => { + const tl = gsap.timeline({ + scrollTrigger: { trigger: sectionRef.current, start: 'top 80%', toggleActions: 'play none none reset' }, + }); + tl.fromTo('.fcta-badge', + { opacity: 0, y: 20, scale: 0.9 }, + { opacity: 1, y: 0, scale: 1, duration: 0.6, ease: 'back.out(2)' } + ) + .fromTo('.fcta-line-1', + { opacity: 0, y: 50, filter: 'blur(8px)' }, + { opacity: 1, y: 0, filter: 'blur(0px)', duration: 0.8, ease: 'expo.out' }, '-=0.2' + ) + .fromTo('.fcta-line-2', + { opacity: 0, y: 50, filter: 'blur(8px)' }, + { opacity: 1, y: 0, filter: 'blur(0px)', duration: 0.8, ease: 'expo.out' }, '-=0.55' + ) + .fromTo('.fcta-sub', + { opacity: 0, y: 24 }, + { opacity: 1, y: 0, duration: 0.65, ease: 'power3.out' }, '-=0.4' + ) + .fromTo('.fcta-btns', + { opacity: 0, y: 20 }, + { opacity: 1, y: 0, duration: 0.55, ease: 'power3.out' }, '-=0.3' + ) + .fromTo('.fcta-stat', + { opacity: 0, y: 28, scale: 0.88 }, + { opacity: 1, y: 0, scale: 1, duration: 0.55, stagger: 0.1, ease: 'back.out(1.8)' }, '-=0.25' + ); + }, sectionRef); + return () => ctx.revert(); + }, []); - {/* Vivid background shape */} -
-
+ return ( +
+ {/* Subtle dot-grid texture */} +
+ + {/* Amber glow orb top-center */} +
+ + {/* Ghost wordmark */} +
LOYALY
-
+ {/* Content */} +
{/* Live badge */} -
- - + + Now live in Coimbatore
{/* Headline */} -

- Your rewards - are waiting. +

+ + Your rewards + + + are waiting. +

-

Download Loyaly and start earning on your very next purchase. Free. Instant. Yours.

{/* CTAs */} -
- - +
+ + Download for iOS - - + + Get on Android
- {/* Proof stats — colored chips */} -
- {proof.map((p, i) => ( -
-
{p.val}
-
{p.label}
+ {/* Proof stats */} +
+ {PROOF.map((p, i) => ( +
+
{p.val}
+
{p.label}
))}
+ +
); } diff --git a/src/components/for-people/HeroSection.tsx b/src/components/for-people/HeroSection.tsx index 70f4d6e..2c0cb62 100644 --- a/src/components/for-people/HeroSection.tsx +++ b/src/components/for-people/HeroSection.tsx @@ -73,7 +73,7 @@ export default function HeroSection() { fontSize: 'clamp(16px, 1.4vw, 19px)', lineHeight: 1.7, fontWeight: 400, color: 'rgba(255,255,255,0.58)', maxWidth: 560, margin: '0 auto 40px', fontFamily: 'Sora, sans-serif', }}> - Earn Loyaly Coins through daily activities, interactive games, and walking challenges, then redeem them at participating stores near you. Earn Online. Spend Offline. + Earn LYT through daily activities, interactive games, and walking challenges, then redeem them at participating stores near you. Earn Online. Spend Offline.

diff --git a/src/components/for-people/SpendAnywhereSection.tsx b/src/components/for-people/SpendAnywhereSection.tsx index a8f8ca2..64ed15b 100644 --- a/src/components/for-people/SpendAnywhereSection.tsx +++ b/src/components/for-people/SpendAnywhereSection.tsx @@ -1,7 +1,13 @@ 'use client'; -import React, { useState } from 'react'; +import React, { useEffect, useLayoutEffect, useRef } from 'react'; import Image from 'next/image'; +import { gsap } from 'gsap'; +import { ScrollTrigger } from 'gsap/ScrollTrigger'; + +if (typeof window !== 'undefined') gsap.registerPlugin(ScrollTrigger); + +const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect; const GroceryIcon = () => ; const FashionIcon = () => ; @@ -11,16 +17,57 @@ const ElectronicsIcon = () => ; const brands = [ - { name: 'Groceries', Icon: GroceryIcon, stores: ['BigBasket', 'DMart', 'Reliance Fresh'], color: '#22c55e' }, + { name: 'Groceries', Icon: GroceryIcon, stores: ['DMart', 'Reliance Fresh', "Spencer's"], color: '#22c55e' }, { name: 'Fashion', Icon: FashionIcon, stores: ['Zara', 'H&M', 'Lifestyle'], color: '#a855f7' }, { name: 'Food & Cafe', Icon: CafeIcon, stores: ['Starbucks', 'KFC', 'McD'], color: '#f97316' }, { name: 'Sports', Icon: SportsIcon, stores: ['Decathlon', 'Nike', 'Adidas'], color: '#3b82f6' }, { name: 'Electronics', Icon: ElectronicsIcon, stores: ['Croma', 'Reliance Digital'], color: '#06b6d4' }, - { name: 'Beauty', Icon: BeautyIcon, stores: ['Nykaa', 'MAC', 'Forest Essentials'], color: '#ec4899' }, + { name: 'Beauty', Icon: BeautyIcon, stores: ['Health & Glow', 'MAC', 'Forest Essentials'], color: '#ec4899' }, ]; export default function SpendAnywhereSection() { - const [active, setActive] = useState(null); + const gridRef = useRef(null); + const cardRefs = useRef<(HTMLDivElement | null)[]>([]); + + // Scroll-triggered blur/scale entrance for the cards + useIsomorphicLayoutEffect(() => { + if (typeof window === 'undefined') return; + const grid = gridRef.current; + if (!grid) return; + + const ctx = gsap.context(() => { + gsap.fromTo('.spend-card', + { opacity: 0, y: 44, scale: 0.92, filter: 'blur(8px)' }, + { + opacity: 1, y: 0, scale: 1, filter: 'blur(0px)', + duration: 0.9, stagger: 0.09, ease: 'expo.out', + scrollTrigger: { trigger: grid, start: 'top 85%', toggleActions: 'play none none reset' }, + } + ); + }, grid); + + return () => ctx.revert(); + }, []); + + // Cursor-tracked spotlight + tilt per card + const handleMove = (i: number) => (e: React.MouseEvent) => { + const el = cardRefs.current[i]; + if (!el) return; + const rect = el.getBoundingClientRect(); + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + el.style.setProperty('--mx', `${(x / rect.width) * 100}%`); + el.style.setProperty('--my', `${(y / rect.height) * 100}%`); + const rotateX = ((y - rect.height / 2) / rect.height) * -6; + const rotateY = ((x - rect.width / 2) / rect.width) * 6; + gsap.to(el, { rotateX, rotateY, duration: 0.4, ease: 'power2.out', transformPerspective: 700 }); + }; + + const handleLeave = (i: number) => () => { + const el = cardRefs.current[i]; + if (!el) return; + gsap.to(el, { rotateX: 0, rotateY: 0, duration: 0.5, ease: 'power3.out' }); + }; return (
@@ -33,7 +80,7 @@ export default function SpendAnywhereSection() { {/* Header row */}
-
Spend · 04
+
Spend04

- One wallet. Every store. Your Loyaly points are universal: no restrictions, no expiry, no fine print. + One balance. Every store. Your LYT is universal: no restrictions, no expiry, no fine print.

{/* Category grid */} -
+
{brands.map((brand, i) => { const BrandIcon = brand.Icon; return (
setActive(i)} - onMouseLeave={() => setActive(null)} - className="fp-rise" + ref={el => { cardRefs.current[i] = el; }} + onMouseMove={handleMove(i)} + onMouseLeave={handleLeave(i)} + className="spend-card" style={{ position: 'relative', - padding: '24px 28px', + padding: '26px 28px', borderRadius: 22, - background: active === i ? 'rgba(12, 12, 22, 0.65)' : 'rgba(10, 10, 18, 0.45)', - border: `1px solid ${active === i ? `${brand.color}45` : 'rgba(255,255,255,0.08)'}`, - backdropFilter: 'blur(20px) saturate(140%)', - WebkitBackdropFilter: 'blur(20px) saturate(140%)', + background: '#0d0d16', + border: '1px solid rgba(255,255,255,0.08)', cursor: 'default', - transitionDelay: `${0.08 + i * 0.06}s`, - transform: active === i ? 'translateY(-6px)' : 'translateY(0)', - boxShadow: active === i - ? `0 24px 50px -12px rgba(0,0,0,0.65), 0 0 25px -4px ${brand.color}25, inset 0 1px 0 rgba(255,255,255,0.1)` - : '0 8px 30px -10px rgba(0, 0, 0, 0.4), inset 0 1px 0 rgba(255,255,255,0.05)', - transition: 'all 0.4s cubic-bezier(0.25, 1, 0.22, 1)', overflow: 'hidden', + transformStyle: 'preserve-3d', + ['--card-color' as string]: brand.color, }} > - {/* Glowing Left Accent Bar */} - + {/* Border glow ring */} +
-
- {/* Icon Container with subtle background shape */} -
- +
+
+
+ +
+ {brand.name}
- {brand.name} + + {/* Explore arrow, slides in on hover */} + + +
-
+
+ +
{brand.stores.map((s, j) => ( {s} ))}
@@ -143,8 +184,8 @@ export default function SpendAnywhereSection() { }}> {[ { val: '10,000+', lbl: 'Partner stores' }, - { val: 'Zero', lbl: 'Expiry on points' }, - { val: 'Universal', lbl: 'One wallet, all brands' }, + { val: 'Zero', lbl: 'Expiry on LYT' }, + { val: 'Universal', lbl: 'One balance, all brands' }, { val: 'Instant', lbl: 'Redeem at checkout' }, ].map((s, i) => (
@@ -154,6 +195,17 @@ export default function SpendAnywhereSection() { ))}
+ +
); } diff --git a/src/components/for-people/VerticalScrollWrapper.tsx b/src/components/for-people/VerticalScrollWrapper.tsx index 1db6259..a08998f 100644 --- a/src/components/for-people/VerticalScrollWrapper.tsx +++ b/src/components/for-people/VerticalScrollWrapper.tsx @@ -189,12 +189,22 @@ export default function VerticalScrollWrapper({ children, sections = [] }: Props font-size: 12px; letter-spacing: 0.3em; color: rgba(255,255,255,0.25); } .fp-kicker { - display: inline-flex; align-items: center; gap: 12px; - font-family: 'Sora', sans-serif; font-weight: 700; - font-size: 11px; letter-spacing: 0.22em; text-transform: uppercase; color: var(--gold); + display: inline-flex; align-items: center; gap: 14px; + font-family: 'Sora', sans-serif; font-weight: 800; + font-size: clamp(14px, 1.3vw, 17px); letter-spacing: 0.24em; + text-transform: uppercase; color: var(--gold); } .fp-kicker::before { - content: ''; width: 30px; height: 1px; background: var(--gold); display: block; + content: ''; width: 44px; height: 2px; border-radius: 2px; + background: linear-gradient(90deg, transparent, var(--gold)); display: block; + } + .fp-kicker-num { + display: inline-flex; align-items: center; justify-content: center; + min-width: 34px; height: 34px; padding: 0 8px; + border-radius: 50%; border: 1.5px solid var(--gold); + font-size: 13px; font-weight: 800; letter-spacing: 0.05em; + color: var(--gold); background: rgba(244,190,40,0.08); + box-shadow: 0 0 18px rgba(244,190,40,0.18); } /* ── Shared buttons ── */ diff --git a/src/components/for-people/WhyLoyalySection.tsx b/src/components/for-people/WhyLoyalySection.tsx index db953e6..79d39da 100644 --- a/src/components/for-people/WhyLoyalySection.tsx +++ b/src/components/for-people/WhyLoyalySection.tsx @@ -30,8 +30,8 @@ export default function WhyLoyalySection() { }, []); const bars = [ - { label: 'Online retail loyalty', pct: 78, color: '#171512' }, - { label: 'Offline retail loyalty', pct: 2, color: '#bc7122' }, + { label: 'Online retail rewards', pct: 78, color: '#171512' }, + { label: 'Offline retail rewards', pct: 2, color: '#bc7122' }, { label: 'Loyaly target', pct: 60, color: '#f4be28' }, ]; @@ -47,7 +47,7 @@ export default function WhyLoyalySection() {
- The Gap · 01 + The Gap01
@@ -69,7 +69,7 @@ export default function WhyLoyalySection() { display: 'inline-block', padding: '8px 16px', borderRadius: 10, background: '#bc7122', color: '#fff', fontSize: 12, fontWeight: 700, fontFamily: 'Sora, sans-serif', letterSpacing: '0.08em', textTransform: 'uppercase', - }}>Loyalty penetration in offline retail
+ }}>Rewards penetration in offline retail
{/* Three metric chips */} @@ -116,7 +116,7 @@ export default function WhyLoyalySection() { fontSize: 16, lineHeight: 1.8, color: '#4f463a', margin: '0 0 36px', fontFamily: 'Sora, sans-serif', maxWidth: 440, }}> - India's offline retail market is worth ₹2.1 lakh crore. Yet loyalty penetration sits at effectively zero. Loyaly exists to close that gap, permanently. + India's offline retail market is worth ₹2.1 lakh crore. Yet rewards penetration sits at effectively zero. Loyaly exists to close that gap, permanently.

{/* Data bars */} diff --git a/src/components/home/ProblemSection.tsx b/src/components/home/ProblemSection.tsx index 71f9e52..96d0ee3 100644 --- a/src/components/home/ProblemSection.tsx +++ b/src/components/home/ProblemSection.tsx @@ -1,8 +1,10 @@ 'use client'; import React, { useEffect, useLayoutEffect, useRef } from 'react'; +import Image from 'next/image'; import { gsap } from 'gsap'; import { ScrollTrigger } from 'gsap/ScrollTrigger'; +import { ParticleCard } from '@/components/MagicBento'; if (typeof window !== 'undefined') { gsap.registerPlugin(ScrollTrigger); @@ -14,52 +16,60 @@ interface ProblemSectionProps { isLoaded?: boolean; } -interface Item { - text: string; - icon: React.ReactNode; -} - -const PROBLEM_ITEMS: Item[] = [ +const DARK_CARDS = [ { - text: 'No compelling reason for customers to visit your store', + title: 'No Compelling Reason', + description: 'No compelling reason for customers to visit your store', + image: '/problems/4.png', icon: ( - + + + ), }, { - text: 'Traditional discounts erode profit margins', + title: 'Margin Erosion', + description: 'Traditional discounts erode profit margins', + image: '/problems/5.png', icon: ( - + + + ), }, { - text: 'Digital ads rarely convert into store footfall', + title: 'Wasted Ad Spend', + description: 'Digital ads rarely convert into store footfall', + image: '/problems/6.png', icon: ( - + + + ), }, { - text: 'No real-time connection between online activity and offline shopping', + title: 'No Real-Time Signal', + description: 'No real-time connection between online activity and offline shopping', + image: '/problems/7.png', + wide: true, icon: ( - <> - - - + + + ), }, { - text: 'Customers forget about stores between visits', + title: 'Customer Amnesia', + description: 'Customers forget about stores between visits', + image: '/problems/8.png', icon: ( - + + + ), }, ]; -const STATS = [ - { value: '68%', label: 'walk in once, never return' }, - { value: '0', label: 'visibility after they leave' }, -]; - export default function ProblemSection({ isLoaded = true }: ProblemSectionProps) { const sectionRef = useRef(null); const accent = '#ef4444'; @@ -77,22 +87,14 @@ export default function ProblemSection({ isLoaded = true }: ProblemSectionProps) .fromTo('.problem-title', { opacity: 0, y: 40, clipPath: 'polygon(0% 100%, 100% 100%, 100% 100%, 0% 100%)' }, { opacity: 1, y: 0, clipPath: 'polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)', duration: 1.1, ease: 'power4.out' }, '-=0.3') .fromTo('.problem-subtitle', { opacity: 0, y: 24 }, { opacity: 1, y: 0, duration: 0.9, ease: 'power3.out' }, '-=0.8'); - const rowTl = gsap.timeline({ - scrollTrigger: { trigger: '.problem-list', start: 'top 82%', toggleActions: 'play none none reset' }, + const statTl = gsap.timeline({ + scrollTrigger: { trigger: '.problem-stats-row', start: 'top 85%', toggleActions: 'play none none reset' }, }); - rowTl.fromTo('.problem-row', { opacity: 0, x: -30 }, { opacity: 1, x: 0, duration: 0.6, stagger: 0.09, ease: 'power3.out' }); + statTl.fromTo('.problem-stat-chip', { opacity: 0, y: 24, scale: 0.92 }, { opacity: 1, y: 0, scale: 1, duration: 0.6, stagger: 0.1, ease: 'back.out(1.7)' }); - const visualTl = gsap.timeline({ - scrollTrigger: { trigger: '.problem-visual', start: 'top 78%', toggleActions: 'play none none reset' }, - }); - visualTl - .fromTo('.problem-visual-stage', { opacity: 0, scale: 0.9, y: 40 }, { opacity: 1, scale: 1, y: 0, duration: 1, ease: 'power3.out' }) - .fromTo('.problem-chip', { opacity: 0, y: 18, scale: 0.85 }, { opacity: 1, y: 0, scale: 1, duration: 0.6, stagger: 0.15, ease: 'back.out(1.6)' }, '-=0.5'); - - gsap.to('.problem-visual-img', { - y: -22, - ease: 'none', - scrollTrigger: { trigger: '.problem-visual', start: 'top bottom', end: 'bottom top', scrub: 1 }, + gsap.fromTo('.problem-card-grid', { opacity: 0, y: 32, scale: 0.96 }, { + opacity: 1, y: 0, scale: 1, duration: 0.8, ease: 'power3.out', + scrollTrigger: { trigger: '.problem-card-grid', start: 'top 88%', toggleActions: 'play none none reset' }, }); ScrollTrigger.refresh(); @@ -145,79 +147,288 @@ export default function ProblemSection({ isLoaded = true }: ProblemSectionProps)
-

+

Retailers struggle to attract new customers and retain existing ones without sacrificing profit margins on generic discounts.

-
- 05 - Recurring pain points, every quarter -
- {/* Split layout */} -
- {/* Visual */} -
-
+ {/* Row 1: Red Stat Cards */} +
+ {/* Card 1 */} + -
- Customers walking past a store without engaging -
+
+
+ 68%+ +
+
+ Walk in once,
never return +
+
+
+ 68%+ +
+
- {/* Floating stat chips */} -
- {STATS[0].value} - {STATS[0].label} -
-
- {STATS[1].value} - {STATS[1].label} -
+ {/* Card 2 */} + +
+
+ 0+ +
+
+ Visibility
after they leave +
+
+
+ 0+ +
+
+ + {/* Card 3 */} + +
+
+ 5+ +
+
+ Recurring pain points,
every quarter +
+
+
+ 5+ +
+
- {/* List */} -
- {PROBLEM_ITEMS.map((item, idx) => ( -
+ {DARK_CARDS.map((card, i) => ( + - - {item.icon} - - {item.text} -
+
+ {card.icon} +

+ {card.title} +

+

+ {card.description} +

+
+ {card.wide ? ( +
+ {card.title} +
+ ) : ( +
+ {card.title} +
+ )} + ))}
diff --git a/src/components/home/ProductsSection.tsx b/src/components/home/ProductsSection.tsx index 8515667..df0e71d 100644 --- a/src/components/home/ProductsSection.tsx +++ b/src/components/home/ProductsSection.tsx @@ -63,7 +63,7 @@ const PRODUCTS: Product[] = [ title: 'Identiq', subtitle: 'Consumer Loyalty Platform', description: - 'Earn Loyaly Coins through daily logins and games, then redeem rewards at real offline stores.', + 'Earn LYT through daily logins and games, then redeem rewards at real offline stores.', accent: '#f4be28', accentRgb: '244, 190, 40', tags: ['Daily Rewards', 'Game Based Offers', 'Offline Redemption'], @@ -283,21 +283,21 @@ function ProductVisual({ product }: { product: Product }) {
- {/* Points card with progress to next tier */} + {/* LYT balance card with progress to next tier */}
-
Reward Points
+
Reward LYT
284
- 🪙 +
-
116 pts to Platinum
+
116 LYT to Platinum
@@ -389,7 +389,7 @@ function ProductCard({ product, index }: { product: Product; index: number }) {
- ), + value: '3.4x', + label: 'MORE REPEAT VISITS', + image: '/shift/1.png', }, { - text: 'Game-based campaigns drive footfall with excitement', - icon: ( - - ), + value: '20%', + label: 'LIVE IN-APP OFFER', + image: '/shift/2.png', }, { - text: 'Smart surprise coupons create a daily reason to engage', - icon: ( - <> - - - - - - ), - }, - { - text: 'Real-time discovery of nearby stores and live offers', - icon: ( - <> - - - - ), - }, - { - text: 'Every store visit earns more rewards and deepens loyalty', - icon: ( - - ), + value: '5', + label: "EVERYDAY MOMENTS,\nTURNED INTO LOYALTY", + image: '/shift/3.png', }, ]; -const BOTTOM_FEATURES = [ - { label: 'Earn Daily', icon: <> }, - { label: 'Play & Win', icon: }, - { label: 'Smart Coupons', icon: }, - { label: 'Shop Offline', icon: }, -]; - -const STATS = [ - { value: '3.4x', label: 'more repeat visits' }, - { value: '20%', label: 'live in-app offer' }, +const BLACK_CARDS = [ + { + title: 'Earn Daily', + description: 'Customers earn LYT through daily app activities', + icon: ( + + + + + + + ), + image: '/shift/4.png', + }, + { + title: 'Game Campaigns', + description: 'Game-based campaigns drive footfall with excitement', + icon: ( + + + + + + + + ), + image: '/shift/5.png', + }, + { + title: 'Smart Coupons', + description: 'Smart surprise coupons create a daily reason to engage', + icon: ( + + + + + ), + image: '/shift/6.png', + }, + { + title: 'Live Discovery', + description: 'Real-time discovery of nearby stores and live offers', + icon: ( + + + + + ), + image: '/shift/7.png', + }, + { + title: 'Deepens Loyalty', + description: 'Every store visit earns more rewards and deepens loyalty', + icon: ( + + + + ), + image: '/shift/8.png', + }, ]; export default function ShiftSection({ isLoaded = true }: ShiftSectionProps) { const sectionRef = useRef(null); const accent = '#f4be28'; + const iconAccent = '#22c55e'; useIsomorphicLayoutEffect(() => { if (typeof window === 'undefined') return; @@ -89,28 +114,10 @@ export default function ShiftSection({ isLoaded = true }: ShiftSectionProps) { .fromTo('.shift-title', { opacity: 0, y: 40, clipPath: 'polygon(0% 100%, 100% 100%, 100% 100%, 0% 100%)' }, { opacity: 1, y: 0, clipPath: 'polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)', duration: 1.1, ease: 'power4.out' }, '-=0.3') .fromTo('.shift-subtitle', { opacity: 0, y: 24 }, { opacity: 1, y: 0, duration: 0.9, ease: 'power3.out' }, '-=0.8'); - const rowTl = gsap.timeline({ - scrollTrigger: { trigger: '.shift-list', start: 'top 82%', toggleActions: 'play none none reset' }, + gsap.fromTo('.shift-bento-card', { opacity: 0, y: 32, scale: 0.96 }, { + opacity: 1, y: 0, scale: 1, duration: 0.7, ease: 'power3.out', stagger: 0.08, + scrollTrigger: { trigger: '.shift-bento-grid', start: 'top 88%', toggleActions: 'play none none reset' }, }); - rowTl.fromTo('.shift-row', { opacity: 0, x: 30 }, { opacity: 1, x: 0, duration: 0.6, stagger: 0.09, ease: 'power3.out' }); - - const visualTl = gsap.timeline({ - scrollTrigger: { trigger: '.shift-visual', start: 'top 78%', toggleActions: 'play none none reset' }, - }); - visualTl - .fromTo('.shift-visual-stage', { opacity: 0, scale: 0.9, y: 40 }, { opacity: 1, scale: 1, y: 0, duration: 1, ease: 'power3.out' }) - .fromTo('.shift-chip', { opacity: 0, y: 18, scale: 0.85 }, { opacity: 1, y: 0, scale: 1, duration: 0.6, stagger: 0.15, ease: 'back.out(1.6)' }, '-=0.5'); - - gsap.to('.shift-visual-img', { - y: -22, - ease: 'none', - scrollTrigger: { trigger: '.shift-visual', start: 'top bottom', end: 'bottom top', scrub: 1 }, - }); - - const bottomTl = gsap.timeline({ - scrollTrigger: { trigger: '.shift-bottom-bar', start: 'top 92%', toggleActions: 'play none none reset' }, - }); - bottomTl.fromTo('.shift-feature-item', { opacity: 0, y: 36, scale: 0.88 }, { opacity: 1, y: 0, scale: 1, stagger: 0.1, duration: 0.65, ease: 'back.out(1.7)' }); ScrollTrigger.refresh(); }, sectionRef); @@ -155,24 +162,6 @@ export default function ShiftSection({ isLoaded = true }: ShiftSectionProps) {
- {/* Twinkling sparkle accents */} - {[ - { top: '14%', left: '58%', delay: '0s', size: 6 }, - { top: '68%', left: '48%', delay: '1.1s', size: 4 }, - { top: '40%', left: '8%', delay: '2s', size: 5 }, - { top: '78%', left: '82%', delay: '0.6s', size: 4 }, - ].map((s, i) => ( - - ))} - {/* Inner edge vignette for a framed, premium finish */}
@@ -187,112 +176,175 @@ export default function ShiftSection({ isLoaded = true }: ShiftSectionProps) {

- Loyaly makes it
simple. + Loyaly makes it
simple.

-

+

Loyaly bridges digital engagement and offline shopping, with daily rewards, interactive games, and surprise coupons redeemable in person.

-
- 05 - Everyday moments, turned into loyalty -
- {/* Split layout */} -
- {/* List */} -
- {SHIFT_ITEMS.map((item, idx) => ( -
- - {item.icon} - - {item.text} + {/* 8-Card Bento Grid */} +
+ + {/* Render Green Stat Cards (Cards 1-3) */} + {GREEN_CARDS.map((card, i) => ( + +
+
+ + {card.value}+ + +
+
+ {card.label} +
- ))} -
- - {/* Visual */} -
-
-
- Customers engaging with the Loyaly app inside a store with live offers -
- - {/* Floating stat chips */} -
- {STATS[0].value} - {STATS[0].label} -
-
- {STATS[1].value} - {STATS[1].label} -
-
-
- - {/* Bottom features bar */} -
- {BOTTOM_FEATURES.map((f) => ( -
- - {f.icon} - - {f.label} -
+ {card.label} +
+ ))} + + {/* Render Black Feature Cards (Cards 4-8) */} + {BLACK_CARDS.map((card, i) => { + const cardNumber = i + 4; // Card index is 4 to 8 + return ( + +
+
+ {card.icon} +
+

+ {card.title} +

+

+ {card.description} +

+
+ + {/* Papercraft Image */} + {(cardNumber === 7 || cardNumber === 8) ? ( +
+ {card.title} +
+ ) : ( +
+ {card.title} +
+ )} +
+ ); + })} +
+ + + {/* Impact metrics — outside section to avoid overflow:hidden clipping */} +
+
+
Measured Impact @@ -287,59 +480,31 @@ export default function TheLoop() { {IMPACTS.map((m, i) => { const isEven = i % 2 === 1; return ( -
-
- {m.prefix} - {counts[i].toFixed(m.decimals)} - {m.suffix} +
+ {m.prefix} + {counts[i].toFixed(m.decimals)} + {m.suffix} +
+
+ {m.label} +
-
- {m.label} -
-
); })}
- - - +
+ ); } diff --git a/src/components/home/WhatWeDo.tsx b/src/components/home/WhatWeDo.tsx index a3cb2e2..3edf39c 100644 --- a/src/components/home/WhatWeDo.tsx +++ b/src/components/home/WhatWeDo.tsx @@ -145,48 +145,49 @@ export default function WhatWeDo({ isLoaded = false }: WhatWeDoProps) { if (typeof window === 'undefined') return; const ctx = gsap.context(() => { + // Header: blur fade-up with stagger gsap.fromTo( '.wwd-header-el', - { opacity: 0, y: 40 }, + { opacity: 0, y: 36, filter: 'blur(6px)' }, { - opacity: 1, y: 0, duration: DEFAULT_DURATION, stagger: 0.15, ease: DEFAULT_EASE, + opacity: 1, y: 0, filter: 'blur(0px)', + duration: 1, stagger: 0.18, ease: 'expo.out', scrollTrigger: { trigger: '.wwd-header', start: 'top 85%', toggleActions: 'play none none reset' }, } ); - // Per-card reveal: each card gets its own scrubbed-in timeline - // (clip-path wipe + rise), staggered across the row, with the - // icon and copy cascading in right after. - gsap.utils.toArray('.wwd-card').forEach((card, i) => { - const tl = gsap.timeline({ - scrollTrigger: { trigger: card, start: 'top 85%', toggleActions: 'play none none reset' }, - delay: i * 0.1, - }); + // Cards: single staggered timeline triggered once the row enters view + const cards = gsap.utils.toArray('.wwd-card'); + const grid = document.querySelector('.wwd-grid-container'); - tl.fromTo(card, - { opacity: 0, y: 70, scale: 0.94, clipPath: 'inset(0% 0% 100% 0% round 22px)' }, - { opacity: 1, y: 0, scale: 1, clipPath: 'inset(0% 0% 0% 0% round 22px)', duration: 0.75, ease: 'power3.out' }, - 0 - ) - .fromTo(card.querySelector('.wwd-icon'), - { opacity: 0, scale: 0.4, rotate: -20 }, - { opacity: 1, scale: 1, rotate: 0, duration: 0.55, ease: 'back.out(2.2)' }, - 0.3 + gsap.fromTo(cards, + { opacity: 0, y: 56, scale: 0.93, filter: 'blur(4px)' }, + { + opacity: 1, y: 0, scale: 1, filter: 'blur(0px)', + duration: 0.8, stagger: 0.12, ease: 'expo.out', + scrollTrigger: { trigger: grid, start: 'top 82%', toggleActions: 'play none none reset' }, + } + ); + + // Cascade inner elements after cards land + cards.forEach((card, i) => { + const tl = gsap.timeline({ delay: i * 0.12 + 0.25 }); + + tl.fromTo(card.querySelector('.wwd-icon'), + { opacity: 0, scale: 0.5, rotate: -15, filter: 'blur(3px)' }, + { opacity: 1, scale: 1, rotate: 0, filter: 'blur(0px)', duration: 0.55, ease: 'back.out(2)' } ) .fromTo(card.querySelector('.wwd-title'), - { opacity: 0, y: 16 }, - { opacity: 1, y: 0, duration: 0.45, ease: 'power2.out' }, - 0.5 + { opacity: 0, y: 14 }, + { opacity: 1, y: 0, duration: 0.4, ease: 'power3.out' }, '-=0.2' ) .fromTo(card.querySelector('.wwd-tagline'), - { opacity: 0, y: 12 }, - { opacity: 1, y: 0, duration: 0.45, ease: 'power2.out' }, - 0.58 + { opacity: 0, y: 10 }, + { opacity: 1, y: 0, duration: 0.38, ease: 'power3.out' }, '-=0.2' ) .fromTo(card.querySelector('.wwd-detail'), - { opacity: 0, y: 10 }, - { opacity: 1, y: 0, duration: 0.4, ease: 'power2.out' }, - 0.66 + { opacity: 0, y: 8 }, + { opacity: 1, y: 0, duration: 0.35, ease: 'power3.out' }, '-=0.18' ); }); }, containerRef); diff --git a/src/components/use-cases/ProductsScrollSection.tsx b/src/components/use-cases/ProductsScrollSection.tsx new file mode 100644 index 0000000..4a52264 --- /dev/null +++ b/src/components/use-cases/ProductsScrollSection.tsx @@ -0,0 +1,325 @@ +'use client'; + +import React, { useEffect, useLayoutEffect, useRef } from 'react'; +import Link from 'next/link'; +import { gsap } from 'gsap'; +import { ScrollTrigger } from 'gsap/ScrollTrigger'; + +if (typeof window !== 'undefined') gsap.registerPlugin(ScrollTrigger); + +const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect; + +const PRODUCTS = [ + { + name: 'Behavision', + tag: 'Footfall AI', + desc: 'Track footfall, measure conversions, and understand in-store behavior with precision heatmaps and dwell-time analytics.', + metric: '3.2×', metricLabel: 'conversion uplift', + color: '#f4be28', rgb: '244,190,40', deep: '#c98f00', + }, + { + name: 'SpendSense', + tag: 'Spend Intelligence', + desc: 'Analyze spending patterns and identify high-value customers before your competition does.', + metric: '68%', metricLabel: 'retention improvement', + color: '#a78bfa', rgb: '167,139,250', deep: '#7c5ce0', + }, + { + name: 'Dyscount', + tag: 'Smart Discounts', + desc: 'Deliver targeted discounts that actually convert, without killing your margins.', + metric: '41%', metricLabel: 'discount waste reduced', + color: '#34d399', rgb: '52,211,153', deep: '#0f9e6e', + }, +]; + +const TOTAL = PRODUCTS.length; +// Scroll distance per product step, in vh +const STEP_VH = 160; + +export default function ProductsScrollSection() { + const sectionRef = useRef(null); + const pinRef = useRef(null); + const fillRef = useRef(null); + const dotRefs = useRef<(HTMLDivElement | null)[]>([]); + const dotTextRefs = useRef<(HTMLSpanElement | null)[]>([]); + const labelRefs = useRef<(HTMLSpanElement | null)[]>([]); + const textRefs = useRef<(HTMLDivElement | null)[]>([]); + const visualRefs = useRef<(HTMLDivElement | null)[]>([]); + const activeRef = useRef(0); + + const applyActive = (idx: number, progress: number) => { + if (fillRef.current) fillRef.current.style.transform = `scaleX(${progress})`; + + PRODUCTS.forEach((p, i) => { + const reached = progress >= i / (TOTAL - 1) - 0.001; + + const dot = dotRefs.current[i]; + if (dot) { + dot.style.background = reached ? p.color : '#0f0f18'; + dot.style.borderColor = reached ? p.color : 'rgba(255,255,255,0.15)'; + dot.style.transform = i === idx ? 'translate(-50%,-50%) scale(1.2)' : 'translate(-50%,-50%) scale(1)'; + dot.style.boxShadow = reached ? `0 0 22px rgba(${p.rgb},0.6)` : 'none'; + } + const dotText = dotTextRefs.current[i]; + if (dotText) dotText.style.color = reached ? '#111' : 'rgba(255,255,255,0.3)'; + + const label = labelRefs.current[i]; + if (label) { + label.style.color = i === idx ? p.color : reached ? 'rgba(255,255,255,0.55)' : 'rgba(255,255,255,0.25)'; + } + + const isActive = i === idx; + const text = textRefs.current[i]; + if (text) { + text.style.opacity = isActive ? '1' : '0'; + text.style.transform = isActive ? 'translateY(0)' : 'translateY(28px)'; + text.style.pointerEvents = isActive ? 'auto' : 'none'; + } + const vis = visualRefs.current[i]; + if (vis) { + vis.style.opacity = isActive ? '1' : '0'; + vis.style.transform = isActive ? 'rotate(0deg) scale(1)' : 'rotate(-2.5deg) scale(0.94)'; + } + }); + activeRef.current = idx; + }; + + useIsomorphicLayoutEffect(() => { + if (typeof window === 'undefined') return; + const section = sectionRef.current; + const pin = pinRef.current; + if (!section || !pin) return; + + const st = ScrollTrigger.create({ + trigger: section, + start: 'top top', + end: () => `+=${window.innerHeight * (TOTAL - 1) * (STEP_VH / 100)}`, + pin, + pinSpacing: true, + anticipatePin: 1, + invalidateOnRefresh: true, + onUpdate: (self) => { + const idx = Math.min(TOTAL - 1, Math.round(self.progress * (TOTAL - 1))); + applyActive(idx, self.progress); + }, + }); + + applyActive(0, 0); + ScrollTrigger.refresh(); + + return () => st.kill(); + }, []); + + return ( +
+
+ {/* Faint grid backdrop */} +
+ + {/* Header */} +
+
Our Products
+

+ AI Powered Retail Intelligence. +

+
+ + {/* Horizontal progress track with 3 points */} +
+
+ {/* Growing fill line */} +
{ fillRef.current = el; }} + style={{ + position: 'absolute', inset: 0, borderRadius: 2, + background: 'linear-gradient(90deg,#f4be28,#a78bfa,#34d399)', + transformOrigin: 'left center', transform: 'scaleX(0)', + boxShadow: '0 0 14px rgba(244,190,40,0.4)', + }} + /> + {/* Point markers */} + {PRODUCTS.map((p, i) => ( +
+
{ dotRefs.current[i] = el; }} + style={{ + position: 'absolute', left: 0, top: 0, + width: 32, height: 32, borderRadius: '50%', + background: i === 0 ? p.color : '#0f0f18', + border: `1.5px solid ${i === 0 ? p.color : 'rgba(255,255,255,0.15)'}`, + transform: i === 0 ? 'translate(-50%,-50%) scale(1.2)' : 'translate(-50%,-50%) scale(1)', + boxShadow: i === 0 ? `0 0 22px rgba(${p.rgb},0.6)` : 'none', + display: 'flex', alignItems: 'center', justifyContent: 'center', + transition: 'background 0.35s ease, border-color 0.35s ease, transform 0.35s ease, box-shadow 0.35s ease', + zIndex: 2, + }} + > + { dotTextRefs.current[i] = el; }} + style={{ fontSize: 11, fontWeight: 800, color: i === 0 ? '#111' : 'rgba(255,255,255,0.3)', fontFamily: 'Sora, sans-serif', transition: 'color 0.35s ease' }} + > + {String(i + 1).padStart(2, '0')} + +
+ {/* Product name under each point */} + { labelRefs.current[i] = el; }} + className="uc-pp-label" + style={{ + position: 'absolute', top: 26, left: 0, transform: 'translateX(-50%)', + whiteSpace: 'nowrap', fontSize: 12, fontWeight: 700, + letterSpacing: '0.1em', textTransform: 'uppercase', + color: i === 0 ? p.color : 'rgba(255,255,255,0.25)', + fontFamily: 'Sora, sans-serif', transition: 'color 0.35s ease', + }} + > + {p.name} + +
+ ))} +
+
+ + {/* Content: color panel left, editorial text right */} +
+ {/* Left — bold gradient color panel (stacked, cross-fading) */} +
+ {PRODUCTS.map((p, i) => ( +
{ visualRefs.current[i] = el; }} + style={{ + position: 'absolute', inset: 0, + opacity: i === 0 ? 1 : 0, + transform: i === 0 ? 'rotate(0deg) scale(1)' : 'rotate(-2.5deg) scale(0.94)', + transition: 'opacity 0.55s ease, transform 0.55s cubic-bezier(0.22,1,0.36,1)', + borderRadius: 28, + background: `linear-gradient(150deg, ${p.color} 0%, ${p.deep} 100%)`, + boxShadow: `0 30px 80px -30px rgba(${p.rgb},0.5)`, + overflow: 'hidden', + display: 'flex', flexDirection: 'column', justifyContent: 'space-between', + padding: 'clamp(24px,2.6vw,40px)', + }} + > + {/* Decorative concentric rings */} +
+
+
+ + {/* Ghost number */} + 0{i + 1} + + {/* Top row: product wordmark */} +
+ {p.name} + + + +
+ + {/* Hero metric */} +
+
{p.metric}
+
{p.metricLabel}
+
+
+ ))} +
+ + {/* Right — editorial text (stacked, cross-fading) */} +
+ {PRODUCTS.map((p, i) => ( +
{ textRefs.current[i] = el; }} + style={{ + position: 'absolute', inset: 0, + opacity: i === 0 ? 1 : 0, + transform: i === 0 ? 'translateY(0)' : 'translateY(28px)', + pointerEvents: i === 0 ? 'auto' : 'none', + transition: 'opacity 0.5s ease, transform 0.5s cubic-bezier(0.22,1,0.36,1)', + display: 'flex', flexDirection: 'column', justifyContent: 'center', + }} + > +
{p.tag}
+ +

{p.name}

+ +

{p.desc}

+ + Learn More → +
+ ))} +
+
+
+ + +
+ ); +}