convert-next.js
This commit is contained in:
154
src/components/About.tsx
Normal file
154
src/components/About.tsx
Normal file
@@ -0,0 +1,154 @@
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
|
||||
const FEATURES = [
|
||||
{
|
||||
icon: '👁',
|
||||
title: 'Turn Footfall into Insight',
|
||||
description:
|
||||
'Real-time visitor tracking and movement analytics reveal exactly how customers move through your store, enabling smarter layout, staffing, and marketing decisions.',
|
||||
},
|
||||
{
|
||||
icon: '🎯',
|
||||
title: 'Personalisation, Perfected',
|
||||
description:
|
||||
'AI-driven profiles let you deliver the right offer to the right customer at precisely the right moment — online, in-store, or both.',
|
||||
},
|
||||
{
|
||||
icon: '⚙️',
|
||||
title: 'One Engine. Total Intelligence.',
|
||||
description:
|
||||
'A single unified platform connects footfall, loyalty, promotions, and revenue data so every team works from the same source of truth.',
|
||||
},
|
||||
];
|
||||
|
||||
export default function About() {
|
||||
return (
|
||||
<section id="about" className="about-section" aria-label="About Loyaly.AI" style={{ margin: '0 20px 20px', borderRadius: '24px', overflow: 'hidden' }}>
|
||||
{/* Two-column intro */}
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))',
|
||||
gap: '48px',
|
||||
alignItems: 'center',
|
||||
maxWidth: '1200px',
|
||||
margin: '0 auto',
|
||||
padding: '80px 24px 60px',
|
||||
}}
|
||||
>
|
||||
{/* Image */}
|
||||
<div style={{ position: 'relative', minHeight: '340px', borderRadius: '16px', overflow: 'hidden' }}>
|
||||
<Image
|
||||
src="/loyaly_php/assets/images/about.png"
|
||||
alt="Loyaly.AI platform dashboard showing customer intelligence data"
|
||||
fill
|
||||
style={{ objectFit: 'cover' }}
|
||||
sizes="(max-width: 768px) 100vw, 50vw"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Text */}
|
||||
<div>
|
||||
<p
|
||||
style={{
|
||||
margin: '0 0 12px',
|
||||
fontSize: '13px',
|
||||
fontWeight: 700,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.1em',
|
||||
color: '#f4be28',
|
||||
}}
|
||||
>
|
||||
About Us
|
||||
</p>
|
||||
<h2
|
||||
style={{
|
||||
margin: '0 0 20px',
|
||||
fontSize: 'clamp(1.6rem, 3vw, 2.4rem)',
|
||||
fontWeight: 800,
|
||||
lineHeight: 1.2,
|
||||
color: '#171512',
|
||||
}}
|
||||
>
|
||||
AI-Powered Customer Intelligence for Retail
|
||||
</h2>
|
||||
<p style={{ margin: '0 0 16px', fontSize: '16px', lineHeight: 1.75, color: '#4f463a' }}>
|
||||
Loyaly.ai is an AI-powered customer intelligence platform built for modern retail. We help
|
||||
businesses understand who their customers are, what drives their behaviour, and how to
|
||||
build lasting loyalty at every touchpoint.
|
||||
</p>
|
||||
<p style={{ margin: '0 0 28px', fontSize: '16px', lineHeight: 1.75, color: '#4f463a' }}>
|
||||
From footfall tracking and personalised engagement to smart loyalty programmes and revenue
|
||||
optimisation, Loyaly.AI gives retailers one unified platform to know their customers deeply
|
||||
and act on that knowledge instantly.
|
||||
</p>
|
||||
<Link
|
||||
href="/solutions_page"
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
padding: '12px 28px',
|
||||
background: '#f4be28',
|
||||
color: '#171512',
|
||||
borderRadius: '4px',
|
||||
fontWeight: 700,
|
||||
textDecoration: 'none',
|
||||
fontSize: '15px',
|
||||
}}
|
||||
>
|
||||
Explore Our Solutions <span aria-hidden="true">→</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Three-feature grid */}
|
||||
<div
|
||||
style={{
|
||||
background: '#fff2c8',
|
||||
padding: '60px 24px',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(240px, 1fr))',
|
||||
gap: '32px',
|
||||
maxWidth: '1200px',
|
||||
margin: '0 auto',
|
||||
}}
|
||||
>
|
||||
{FEATURES.map(({ icon, title, description }) => (
|
||||
<div
|
||||
key={title}
|
||||
style={{
|
||||
background: '#fff8e8',
|
||||
borderRadius: '12px',
|
||||
padding: '32px 28px',
|
||||
boxShadow: '0 2px 12px rgba(23,21,18,0.06)',
|
||||
}}
|
||||
>
|
||||
<div style={{ fontSize: '32px', marginBottom: '16px' }} aria-hidden="true">
|
||||
{icon}
|
||||
</div>
|
||||
<h3
|
||||
style={{
|
||||
margin: '0 0 12px',
|
||||
fontSize: '18px',
|
||||
fontWeight: 700,
|
||||
color: '#171512',
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</h3>
|
||||
<p style={{ margin: 0, fontSize: '15px', lineHeight: 1.7, color: '#4f463a' }}>
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
193
src/components/BlogSection.tsx
Normal file
193
src/components/BlogSection.tsx
Normal file
@@ -0,0 +1,193 @@
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
|
||||
const CASE_STUDIES = [
|
||||
{
|
||||
slug: 'ai-maximises-discount-strategy',
|
||||
title: 'Stop Wasting Discounts: How AI Maximises Every Offer You Give',
|
||||
excerpt:
|
||||
'Most retail discounts go to customers who would have bought anyway. Discover how Loyaly.AI\'s promotion intelligence engine targets the right buyers at the right margin.',
|
||||
tags: ['AI Solutions', 'Retail Intelligence'],
|
||||
image: '/loyaly_php/assets/images/case-study-2-img-960x718.jpeg',
|
||||
imageAlt: 'Data visualisation showing AI-optimised discount targeting across customer segments',
|
||||
readTime: '5 min read',
|
||||
},
|
||||
{
|
||||
slug: 'ai-redefining-retail-loyalty-2026',
|
||||
title: 'Beyond Points: How AI is Redefining Retail Loyalty in 2026',
|
||||
excerpt:
|
||||
'Traditional points schemes are losing ground. Learn how AI-driven loyalty programmes build genuine relationships through personalisation, timing, and context-aware rewards.',
|
||||
tags: ['AI Retail', 'Loyalty Intelligence'],
|
||||
image: '/loyaly_php/assets/images/case-study-1-img-960x718.jpeg',
|
||||
imageAlt: 'Retail customer using a loyalty app powered by Loyaly.AI',
|
||||
readTime: '7 min read',
|
||||
},
|
||||
];
|
||||
|
||||
export default function BlogSection() {
|
||||
return (
|
||||
<section
|
||||
id="case-studies"
|
||||
aria-label="Case studies"
|
||||
style={{ padding: '80px 24px', background: '#fff8e8', margin: '0 20px 20px', borderRadius: '24px', overflow: 'hidden' }}
|
||||
>
|
||||
<div style={{ maxWidth: '1200px', margin: '0 auto' }}>
|
||||
{/* Header */}
|
||||
<div style={{ marginBottom: '48px', display: 'flex', alignItems: 'flex-end', justifyContent: 'space-between', flexWrap: 'wrap', gap: '16px' }}>
|
||||
<div>
|
||||
<p
|
||||
style={{
|
||||
margin: '0 0 10px',
|
||||
fontSize: '13px',
|
||||
fontWeight: 700,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.1em',
|
||||
color: '#f4be28',
|
||||
}}
|
||||
>
|
||||
Case Studies
|
||||
</p>
|
||||
<h2
|
||||
style={{
|
||||
margin: 0,
|
||||
fontSize: 'clamp(1.6rem, 3vw, 2.4rem)',
|
||||
fontWeight: 800,
|
||||
color: '#171512',
|
||||
lineHeight: 1.2,
|
||||
}}
|
||||
>
|
||||
Insights from the Loyaly.AI field
|
||||
</h2>
|
||||
</div>
|
||||
<Link
|
||||
href="/blog_main"
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '6px',
|
||||
padding: '10px 22px',
|
||||
border: '2px solid #f4be28',
|
||||
borderRadius: '4px',
|
||||
color: '#171512',
|
||||
fontWeight: 700,
|
||||
textDecoration: 'none',
|
||||
fontSize: '14px',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
All Articles <span aria-hidden="true">→</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Card grid */}
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))',
|
||||
gap: '32px',
|
||||
}}
|
||||
>
|
||||
{CASE_STUDIES.map(({ slug, title, excerpt, tags, image, imageAlt, readTime }) => (
|
||||
<article
|
||||
key={slug}
|
||||
style={{
|
||||
background: '#fff',
|
||||
borderRadius: '16px',
|
||||
overflow: 'hidden',
|
||||
boxShadow: '0 2px 16px rgba(23,21,18,0.07)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
transition: 'transform 0.25s ease, box-shadow 0.25s ease',
|
||||
}}
|
||||
onMouseEnter={e => {
|
||||
const el = e.currentTarget as HTMLElement;
|
||||
el.style.transform = 'translateY(-4px)';
|
||||
el.style.boxShadow = '0 8px 32px rgba(23,21,18,0.12)';
|
||||
}}
|
||||
onMouseLeave={e => {
|
||||
const el = e.currentTarget as HTMLElement;
|
||||
el.style.transform = 'translateY(0)';
|
||||
el.style.boxShadow = '0 2px 16px rgba(23,21,18,0.07)';
|
||||
}}
|
||||
>
|
||||
{/* Image */}
|
||||
<Link href={`/use_cases/${slug}`} tabIndex={-1} aria-hidden="true">
|
||||
<div style={{ position: 'relative', height: '220px' }}>
|
||||
<Image
|
||||
src={image}
|
||||
alt={imageAlt}
|
||||
fill
|
||||
style={{ objectFit: 'cover' }}
|
||||
sizes="(max-width: 768px) 100vw, 50vw"
|
||||
/>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
{/* Content */}
|
||||
<div style={{ padding: '28px', flex: 1, display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
||||
{/* Tags */}
|
||||
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
|
||||
{tags.map(tag => (
|
||||
<span
|
||||
key={tag}
|
||||
style={{
|
||||
padding: '4px 10px',
|
||||
background: '#fff2c8',
|
||||
color: '#bc7122',
|
||||
borderRadius: '20px',
|
||||
fontSize: '12px',
|
||||
fontWeight: 700,
|
||||
letterSpacing: '0.04em',
|
||||
}}
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<h3
|
||||
style={{
|
||||
margin: 0,
|
||||
fontSize: '18px',
|
||||
fontWeight: 700,
|
||||
lineHeight: 1.35,
|
||||
color: '#171512',
|
||||
}}
|
||||
>
|
||||
<Link
|
||||
href={`/use_cases/${slug}`}
|
||||
style={{ textDecoration: 'none', color: 'inherit' }}
|
||||
>
|
||||
{title}
|
||||
</Link>
|
||||
</h3>
|
||||
|
||||
<p style={{ margin: 0, fontSize: '14px', lineHeight: 1.7, color: '#4f463a', flex: 1 }}>
|
||||
{excerpt}
|
||||
</p>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginTop: '8px' }}>
|
||||
<span style={{ fontSize: '13px', color: '#bc7122', fontWeight: 600 }}>{readTime}</span>
|
||||
<Link
|
||||
href={`/use_cases/${slug}`}
|
||||
style={{
|
||||
fontSize: '14px',
|
||||
fontWeight: 700,
|
||||
color: '#171512',
|
||||
textDecoration: 'none',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
}}
|
||||
>
|
||||
Read more <span aria-hidden="true">→</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
33
src/components/BodyClassManager.tsx
Normal file
33
src/components/BodyClassManager.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
if (typeof window !== 'undefined' && process.env.NODE_ENV === 'development') {
|
||||
const orig = console.error;
|
||||
console.error = (...args: any[]) => {
|
||||
if (typeof args[0] === 'string' && args[0].includes('Encountered a script tag')) return;
|
||||
orig.apply(console, args);
|
||||
};
|
||||
}
|
||||
|
||||
export default function BodyClassManager() {
|
||||
const pathname = usePathname();
|
||||
|
||||
useEffect(() => {
|
||||
// Core stylesheet classes
|
||||
const baseClasses = 'wp-singular page-template-default page wp-theme-aiero theme-aiero woocommerce-no-js mega-menu-main elementor-default elementor-kit-6 elementor-page';
|
||||
|
||||
// Dynamic page-specific Elementor styles class
|
||||
let dynamicClass = '';
|
||||
if (pathname === '/') {
|
||||
dynamicClass = 'page-id-2983 elementor-page-2983';
|
||||
} else {
|
||||
dynamicClass = 'page-id-1996 elementor-page-1996';
|
||||
}
|
||||
|
||||
document.body.className = `${baseClasses} ${dynamicClass}`;
|
||||
}, [pathname]);
|
||||
|
||||
return null;
|
||||
}
|
||||
238
src/components/Footer.tsx
Normal file
238
src/components/Footer.tsx
Normal file
@@ -0,0 +1,238 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import Link from 'next/link';
|
||||
import Image from 'next/image';
|
||||
|
||||
const NAV = [
|
||||
{
|
||||
title: 'Product',
|
||||
links: [
|
||||
{ label: 'For People', href: '/for_people' },
|
||||
{ label: 'For Business', href: '/use_cases' },
|
||||
{ label: 'Solutions', href: '/solutions_page' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Platform',
|
||||
links: [
|
||||
{ label: 'Behavision AI', href: '/solutions_page' },
|
||||
{ label: 'SpendSense AI', href: '/solutions_page' },
|
||||
{ label: 'Dyscount AI', href: '/solutions_page' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Company',
|
||||
links: [
|
||||
{ label: 'Meet the Crew', href: '/about_us' },
|
||||
{ label: 'Market Insights', href: '/blog_main' },
|
||||
{ label: 'Contact Us', href: '/contacts' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const SOCIALS = [
|
||||
{
|
||||
label: 'LinkedIn', href: 'https://www.linkedin.com/company/loyaly-ai',
|
||||
icon: <svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><path d="M19.7 3H4.3C3.58 3 3 3.58 3 4.3v15.4C3 20.42 3.58 21 4.3 21h15.4c.72 0 1.3-.58 1.3-1.3V4.3C21 3.58 20.42 3 19.7 3zM8.34 18.34H5.67V9.75h2.67v8.59zM7 8.57a1.55 1.55 0 1 1 0-3.1 1.55 1.55 0 0 1 0 3.1zm11.34 9.77h-2.67v-4.18c0-1-.02-2.28-1.39-2.28-1.39 0-1.6 1.09-1.6 2.21v4.25h-2.67V9.75h2.56v1.17h.04c.36-.68 1.23-1.39 2.53-1.39 2.7 0 3.2 1.78 3.2 4.09v4.72z"/></svg>,
|
||||
},
|
||||
{
|
||||
label: 'X / Twitter', href: 'https://twitter.com/loyalyai',
|
||||
icon: <svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><path d="M13.98 10.62 20.54 3h-1.55l-5.7 6.62L8.74 3H3.5l6.88 10.01L3.5 21h1.55l6.01-6.99L15.87 21h5.24l-7.13-10.38zm-2.13 2.47-.7-1-5.54-7.93H8l4.47 6.4.7 1 5.82 8.32h-2.39l-4.74-6.79z"/></svg>,
|
||||
},
|
||||
{
|
||||
label: 'Instagram', href: 'https://instagram.com/loyalyai',
|
||||
icon: <svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zm0-2.163c-3.259 0-3.667.014-4.947.072-4.358.2-6.78 2.618-6.98 6.98-.059 1.281-.073 1.689-.073 4.948 0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98 1.281.058 1.689.072 4.948.072 3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98-1.281-.059-1.69-.073-4.949-.073zm0 5.838a6.162 6.162 0 1 0 0 12.324 6.162 6.162 0 0 0 0-12.324zM12 16a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm6.406-11.845a1.44 1.44 0 1 0 0 2.881 1.44 1.44 0 0 0 0-2.881z"/></svg>,
|
||||
},
|
||||
{
|
||||
label: 'YouTube', href: 'https://www.youtube.com/@loyalyai',
|
||||
icon: <svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><path d="M21.8 8s-.2-1.38-.8-1.99c-.76-.8-1.61-.8-2-.85C16.2 5 12 5 12 5s-4.2 0-7 .17c-.39.05-1.24.05-2 .85C2.4 6.62 2.2 8 2.2 8S2 9.62 2 11.24v1.52c0 1.62.2 3.24.2 3.24s.2 1.38.8 1.99c.76.8 1.76.77 2.2.85C6.8 19 12 19.03 12 19.03s4.2-.01 7-.22c.39-.05 1.24-.05 2-.85.6-.61.8-1.99.8-1.99S22 14.38 22 12.76v-1.52C22 9.62 21.8 8 21.8 8zm-11.87 6.59V8.79l5.4 2.82-5.4 2.98z"/></svg>,
|
||||
},
|
||||
];
|
||||
|
||||
export default function Footer() {
|
||||
const [showScroll, setShowScroll] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const onScroll = () => setShowScroll(window.scrollY > 400);
|
||||
window.addEventListener('scroll', onScroll, { passive: true });
|
||||
return () => window.removeEventListener('scroll', onScroll);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<footer style={{
|
||||
background: 'linear-gradient(to top, #3a2800 0%, #1e1600 18%, #0e0b04 36%, #07070d 58%)',
|
||||
color: '#ffffff',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
margin: '0 20px 20px',
|
||||
borderRadius: '0 0 24px 24px',
|
||||
}}>
|
||||
|
||||
{/* ── TOP CTA STRIP ── */}
|
||||
<div style={{
|
||||
padding: '72px 64px 64px',
|
||||
borderBottom: '1px solid rgba(255,255,255,0.06)',
|
||||
display: 'flex', alignItems: 'flex-end', justifyContent: 'space-between',
|
||||
flexWrap: 'wrap', gap: 32,
|
||||
position: 'relative',
|
||||
}}>
|
||||
|
||||
<div style={{ maxWidth: 600 }}>
|
||||
<div style={{ fontSize: 11, fontWeight: 700, letterSpacing: '0.2em', textTransform: 'uppercase', color: '#f4be28', fontFamily: 'Manrope, sans-serif', marginBottom: 20, display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||
<span style={{ width: 24, height: 1, background: '#f4be28', display: 'block' }} />Let's build together
|
||||
</div>
|
||||
<h2 style={{ fontSize: 'clamp(36px, 5vw, 72px)', fontWeight: 900, letterSpacing: '-0.04em', lineHeight: 0.95, color: '#fff', margin: 0, fontFamily: 'Sora, sans-serif' }}>
|
||||
Turn every visit<br />
|
||||
<span style={{ color: '#f4be28' }}>into value.</span>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 12, alignItems: 'flex-end' }}>
|
||||
<Link href="/contacts" style={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 10,
|
||||
padding: '18px 40px', background: '#f4be28', color: '#111',
|
||||
fontSize: 16, fontWeight: 800, borderRadius: 100, textDecoration: 'none',
|
||||
fontFamily: 'Manrope, sans-serif', boxShadow: '0 16px 48px rgba(244,190,40,0.25)',
|
||||
whiteSpace: 'nowrap',
|
||||
}}>
|
||||
Book a Demo
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
|
||||
</Link>
|
||||
<Link href="/contacts" style={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 8,
|
||||
fontSize: 14, fontWeight: 600, color: 'rgba(255,255,255,0.45)',
|
||||
textDecoration: 'none', fontFamily: 'Manrope, sans-serif',
|
||||
borderBottom: '1px solid rgba(255,255,255,0.15)', paddingBottom: 2,
|
||||
}}>
|
||||
or talk to sales →
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── MAIN COLUMNS ── */}
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1.6fr 1fr 1fr 1fr',
|
||||
gap: '48px', padding: '64px 64px 48px',
|
||||
alignItems: 'start',
|
||||
}}>
|
||||
{/* Brand */}
|
||||
<div>
|
||||
<Link href="/" aria-label="Loyaly.AI home">
|
||||
<Image src="/images/logo-dark.png" alt="Loyaly.AI" width={560} height={160} style={{ height: 140, width: 'auto', marginBottom: 24, display: 'block' }} />
|
||||
</Link>
|
||||
<p style={{ fontSize: 14, lineHeight: 1.75, color: 'rgba(255,255,255,0.35)', fontFamily: 'Manrope, sans-serif', margin: '0 0 28px', maxWidth: 280 }}>
|
||||
AI-powered retail intelligence that turns every walk-in into a loyal customer.
|
||||
</p>
|
||||
<div style={{ display: 'flex', gap: 10 }}>
|
||||
{SOCIALS.map(({ label, href, icon }) => (
|
||||
<a key={label} href={href} target="_blank" rel="noopener noreferrer" aria-label={label}
|
||||
style={{
|
||||
width: 38, height: 38, borderRadius: 10,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
background: 'rgba(255,255,255,0.05)', border: '1px solid rgba(255,255,255,0.08)',
|
||||
color: 'rgba(255,255,255,0.5)',
|
||||
transition: 'all 0.2s ease',
|
||||
}}
|
||||
onMouseEnter={e => { const el = e.currentTarget as HTMLElement; el.style.background = 'rgba(244,190,40,0.12)'; el.style.borderColor = 'rgba(244,190,40,0.3)'; el.style.color = '#f4be28'; }}
|
||||
onMouseLeave={e => { const el = e.currentTarget as HTMLElement; el.style.background = 'rgba(255,255,255,0.05)'; el.style.borderColor = 'rgba(255,255,255,0.08)'; el.style.color = 'rgba(255,255,255,0.5)'; }}
|
||||
>{icon}</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Nav columns */}
|
||||
{NAV.map(col => (
|
||||
<nav key={col.title} aria-label={col.title}>
|
||||
<h5 style={{ margin: '0 0 20px', fontSize: 12, fontWeight: 700, color: 'rgba(255,255,255,0.3)', letterSpacing: '0.16em', textTransform: 'uppercase', fontFamily: 'Manrope, sans-serif' }}>
|
||||
{col.title}
|
||||
</h5>
|
||||
<ul style={{ listStyle: 'none', margin: 0, padding: 0, display: 'flex', flexDirection: 'column', gap: 10 }}>
|
||||
{col.links.map(({ label, href }) => (
|
||||
<li key={label}>
|
||||
<Link href={href} style={{ textDecoration: 'none', fontSize: 15, color: 'rgba(255,255,255,0.5)', fontFamily: 'Manrope, sans-serif', transition: 'color 0.2s', fontWeight: 500 }}
|
||||
onMouseEnter={e => ((e.currentTarget as HTMLElement).style.color = '#ffffff')}
|
||||
onMouseLeave={e => ((e.currentTarget as HTMLElement).style.color = 'rgba(255,255,255,0.5)')}
|
||||
>{label}</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* ── BOTTOM BAR ── */}
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
||||
flexWrap: 'wrap', gap: 12,
|
||||
padding: '20px 64px 36px',
|
||||
borderTop: '1px solid rgba(255,255,255,0.05)',
|
||||
fontSize: 13, color: 'rgba(255,255,255,0.25)', fontFamily: 'Manrope, sans-serif',
|
||||
}}>
|
||||
<span>© {new Date().getFullYear()} Loyaly.AI — All rights reserved.</span>
|
||||
<div style={{ display: 'flex', gap: 24 }}>
|
||||
{[['Terms', '/terms'], ['Privacy', '/privacy'], ['Cookies', '/cookies']].map(([l, h]) => (
|
||||
<Link key={l} href={h} style={{ textDecoration: 'none', color: 'inherit', transition: 'color 0.2s' }}
|
||||
onMouseEnter={e => ((e.currentTarget as HTMLElement).style.color = 'rgba(255,255,255,0.6)')}
|
||||
onMouseLeave={e => ((e.currentTarget as HTMLElement).style.color = 'rgba(255,255,255,0.25)')}
|
||||
>{l}</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
{/* Scroll-to-top */}
|
||||
<style>{`
|
||||
@keyframes sttPulseRing {
|
||||
0% { transform: scale(1); opacity: 0.6; }
|
||||
70% { transform: scale(1.55); opacity: 0; }
|
||||
100% { transform: scale(1.55); opacity: 0; }
|
||||
}
|
||||
@keyframes sttBounceArrow {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
40% { transform: translateY(-5px); }
|
||||
60% { transform: translateY(-2px); }
|
||||
}
|
||||
.stt-btn:hover { transform: scale(1.1) !important; box-shadow: 0 8px 32px rgba(244,190,40,0.65) !important; }
|
||||
.stt-arrow { animation: sttBounceArrow 1.6s ease-in-out infinite; }
|
||||
`}</style>
|
||||
|
||||
<div style={{
|
||||
position: 'fixed', bottom: 32, right: 32, zIndex: 9999,
|
||||
opacity: showScroll ? 1 : 0,
|
||||
transform: showScroll ? 'translateY(0)' : 'translateY(16px)',
|
||||
transition: 'opacity 0.3s ease, transform 0.3s ease',
|
||||
pointerEvents: showScroll ? 'auto' : 'none',
|
||||
}}>
|
||||
{/* Pulse ring */}
|
||||
<span style={{
|
||||
position: 'absolute', inset: 0, borderRadius: '50%',
|
||||
background: 'rgba(244,190,40,0.45)',
|
||||
animation: 'sttPulseRing 2s cubic-bezier(0.215,0.61,0.355,1) infinite',
|
||||
pointerEvents: 'none',
|
||||
}} />
|
||||
<button
|
||||
onClick={() => window.scrollTo({ top: 0, behavior: 'smooth' })}
|
||||
aria-label="Scroll to top"
|
||||
className="stt-btn"
|
||||
style={{
|
||||
position: 'relative',
|
||||
width: 64, height: 64, borderRadius: '50%',
|
||||
background: '#f4be28', color: '#111', border: 'none',
|
||||
cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
boxShadow: '0 4px 20px rgba(244,190,40,0.4)',
|
||||
transition: 'transform 0.25s ease, box-shadow 0.25s ease',
|
||||
}}
|
||||
>
|
||||
<span className="stt-arrow" style={{ display: 'flex' }}>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M12 19V5M5 12l7-7 7 7"/>
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
148
src/components/GSAPAnimator.tsx
Normal file
148
src/components/GSAPAnimator.tsx
Normal file
@@ -0,0 +1,148 @@
|
||||
'use client';
|
||||
|
||||
import { useLayoutEffect } from 'react';
|
||||
import { gsap } from 'gsap';
|
||||
import { ScrollTrigger } from 'gsap/ScrollTrigger';
|
||||
|
||||
if (typeof window !== 'undefined') gsap.registerPlugin(ScrollTrigger);
|
||||
|
||||
/**
|
||||
* Drop this component into any page to activate scroll-triggered animations.
|
||||
* Targets elements by data attributes:
|
||||
* data-gsap="heading" — clip-path wipe reveal
|
||||
* data-gsap="text" — fade-up for paragraphs
|
||||
* data-gsap="card" — stagger card reveal
|
||||
* data-gsap="left" — slide from left
|
||||
* data-gsap="right" — slide from right
|
||||
* data-gsap="scale" — scale + fade
|
||||
* data-gsap="line" — horizontal line drawing
|
||||
* data-gsap="badge" — pop scale for pills/tags
|
||||
* data-gsap="counter" — animate data-count attribute value
|
||||
* data-gsap="stagger" — stagger direct children
|
||||
*/
|
||||
export default function GSAPAnimator() {
|
||||
useLayoutEffect(() => {
|
||||
if (typeof window === 'undefined') return;
|
||||
gsap.registerPlugin(ScrollTrigger);
|
||||
|
||||
const ctx = gsap.context(() => {
|
||||
|
||||
// ── HEADINGS — clip-path wipe ────────────────────────────────────
|
||||
gsap.utils.toArray<HTMLElement>('[data-gsap="heading"]').forEach((el, i) => {
|
||||
gsap.fromTo(el,
|
||||
{ clipPath: 'polygon(0% 100%,100% 100%,100% 100%,0% 100%)', y: 28, opacity: 0 },
|
||||
{ clipPath: 'polygon(0% 0%,100% 0%,100% 100%,0% 100%)', y: 0, opacity: 1,
|
||||
duration: 1.1, ease: 'power4.out', delay: i * 0.04,
|
||||
scrollTrigger: { trigger: el, start: 'top 88%', toggleActions: 'play none none reset' },
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// ── PARAGRAPHS — fade up ─────────────────────────────────────────
|
||||
gsap.utils.toArray<HTMLElement>('[data-gsap="text"]').forEach((el) => {
|
||||
gsap.fromTo(el,
|
||||
{ y: 36, opacity: 0 },
|
||||
{ y: 0, opacity: 1, duration: 0.9, ease: 'power3.out',
|
||||
scrollTrigger: { trigger: el, start: 'top 90%', toggleActions: 'play none none reset' },
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// ── CARDS — stagger batch ────────────────────────────────────────
|
||||
const cardGroups = new Map<Element | null, HTMLElement[]>();
|
||||
gsap.utils.toArray<HTMLElement>('[data-gsap="card"]').forEach((el) => {
|
||||
const parent = el.parentElement;
|
||||
if (!cardGroups.has(parent)) cardGroups.set(parent, []);
|
||||
cardGroups.get(parent)!.push(el);
|
||||
});
|
||||
cardGroups.forEach((cards) => {
|
||||
gsap.fromTo(cards,
|
||||
{ y: 70, opacity: 0, scale: 0.95 },
|
||||
{ y: 0, opacity: 1, scale: 1, duration: 0.95, ease: 'power3.out', stagger: 0.12,
|
||||
scrollTrigger: { trigger: cards[0], start: 'top 85%', toggleActions: 'play none none reset' },
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// ── FROM LEFT ────────────────────────────────────────────────────
|
||||
gsap.utils.toArray<HTMLElement>('[data-gsap="left"]').forEach((el) => {
|
||||
gsap.fromTo(el,
|
||||
{ x: -80, opacity: 0 },
|
||||
{ x: 0, opacity: 1, duration: 1.0, ease: 'power3.out',
|
||||
scrollTrigger: { trigger: el, start: 'top 88%', toggleActions: 'play none none reset' },
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// ── FROM RIGHT ───────────────────────────────────────────────────
|
||||
gsap.utils.toArray<HTMLElement>('[data-gsap="right"]').forEach((el) => {
|
||||
gsap.fromTo(el,
|
||||
{ x: 80, opacity: 0 },
|
||||
{ x: 0, opacity: 1, duration: 1.0, ease: 'power3.out',
|
||||
scrollTrigger: { trigger: el, start: 'top 88%', toggleActions: 'play none none reset' },
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// ── SCALE UP ─────────────────────────────────────────────────────
|
||||
gsap.utils.toArray<HTMLElement>('[data-gsap="scale"]').forEach((el) => {
|
||||
gsap.fromTo(el,
|
||||
{ scale: 0.82, opacity: 0, y: 24 },
|
||||
{ scale: 1, opacity: 1, y: 0, duration: 0.95, ease: 'back.out(1.4)',
|
||||
scrollTrigger: { trigger: el, start: 'top 88%', toggleActions: 'play none none reset' },
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// ── HORIZONTAL LINES ─────────────────────────────────────────────
|
||||
gsap.utils.toArray<HTMLElement>('[data-gsap="line"]').forEach((el) => {
|
||||
gsap.fromTo(el,
|
||||
{ scaleX: 0, transformOrigin: 'left center' },
|
||||
{ scaleX: 1, duration: 1.2, ease: 'power4.out',
|
||||
scrollTrigger: { trigger: el, start: 'top 90%', toggleActions: 'play none none reset' },
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// ── BADGE / PILL — pop scale ──────────────────────────────────────
|
||||
gsap.utils.toArray<HTMLElement>('[data-gsap="badge"]').forEach((el) => {
|
||||
gsap.fromTo(el,
|
||||
{ scale: 0.6, opacity: 0 },
|
||||
{ scale: 1, opacity: 1, duration: 0.7, ease: 'back.out(2)',
|
||||
scrollTrigger: { trigger: el, start: 'top 92%', toggleActions: 'play none none reset' },
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// ── COUNTERS ─────────────────────────────────────────────────────
|
||||
gsap.utils.toArray<HTMLElement>('[data-gsap="counter"]').forEach((el) => {
|
||||
const target = parseFloat(el.getAttribute('data-count') || el.innerText);
|
||||
const decimals = (el.getAttribute('data-count') || '').includes('.') ? 1 : 0;
|
||||
const suffix = el.getAttribute('data-suffix') || '';
|
||||
const prefix = el.getAttribute('data-prefix') || '';
|
||||
const obj = { val: 0 };
|
||||
gsap.to(obj, {
|
||||
val: target, duration: 2, ease: 'power2.out',
|
||||
onUpdate: () => { el.innerText = prefix + obj.val.toFixed(decimals) + suffix; },
|
||||
scrollTrigger: { trigger: el, start: 'top 85%', once: true },
|
||||
});
|
||||
});
|
||||
|
||||
// ── STAGGER CHILDREN ─────────────────────────────────────────────
|
||||
gsap.utils.toArray<HTMLElement>('[data-gsap="stagger"]').forEach((parent) => {
|
||||
const children = Array.from(parent.children) as HTMLElement[];
|
||||
gsap.fromTo(children,
|
||||
{ y: 50, opacity: 0 },
|
||||
{ y: 0, opacity: 1, duration: 0.85, ease: 'power3.out', stagger: 0.1,
|
||||
scrollTrigger: { trigger: parent, start: 'top 85%', toggleActions: 'play none none reset' },
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
return () => ctx.revert();
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
}
|
||||
28
src/components/GSAPReveal.tsx
Normal file
28
src/components/GSAPReveal.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { useGSAPReveal, RevealAnimation } from '@/hooks/useGSAPReveal';
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
delay?: number;
|
||||
animation?: RevealAnimation;
|
||||
duration?: number;
|
||||
ease?: string;
|
||||
start?: string;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
as?: keyof React.JSX.IntrinsicElements;
|
||||
}
|
||||
|
||||
export default function GSAPReveal({
|
||||
children, delay = 0, animation = 'fade-up',
|
||||
duration = 1.0, ease, start, className, style,
|
||||
}: Props) {
|
||||
const ref = useGSAPReveal({ delay, animation, duration, ease, start });
|
||||
return (
|
||||
<div ref={ref} className={className} style={{ willChange: 'transform, opacity', ...style }}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
268
src/components/GetInTouch.tsx
Normal file
268
src/components/GetInTouch.tsx
Normal file
@@ -0,0 +1,268 @@
|
||||
'use client';
|
||||
|
||||
import { useState, FormEvent } from 'react';
|
||||
|
||||
type FormState = 'idle' | 'sending' | 'success' | 'error';
|
||||
|
||||
const DETAILS = [
|
||||
{
|
||||
label: 'Visit us',
|
||||
value: <>No.424, Red Rose Towers,<br />DB Road, RS Puram,<br />Coimbatore – 641002</>,
|
||||
icon: (
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0118 0z" /><circle cx="12" cy="10" r="3" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: 'Email us',
|
||||
value: <a href="mailto:loyalydigital@gmail.com" style={{ color: '#fff', textDecoration: 'none', fontWeight: 600 }}>loyalydigital@gmail.com</a>,
|
||||
icon: (
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
|
||||
<rect x="2" y="4" width="20" height="16" rx="2" /><path d="m22 7-10 5L2 7" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const SOCIALS = [
|
||||
{ label: 'LinkedIn', href: 'https://www.linkedin.com/company/loyaly-ai', icon: <svg width="17" height="17" viewBox="0 0 24 24" fill="currentColor"><path d="M19.7 3H4.3C3.58 3 3 3.58 3 4.3v15.4C3 20.42 3.58 21 4.3 21h15.4c.72 0 1.3-.58 1.3-1.3V4.3C21 3.58 20.42 3 19.7 3zM8.34 18.34H5.67V9.75h2.67v8.59zM7 8.57a1.55 1.55 0 1 1 0-3.1 1.55 1.55 0 0 1 0 3.1zm11.34 9.77h-2.67v-4.18c0-1-.02-2.28-1.39-2.28-1.39 0-1.6 1.09-1.6 2.21v4.25h-2.67V9.75h2.56v1.17h.04c.36-.68 1.23-1.39 2.53-1.39 2.7 0 3.2 1.78 3.2 4.09v4.72z" /></svg> },
|
||||
{ label: 'X / Twitter', href: 'https://twitter.com/loyalyai', icon: <svg width="17" height="17" viewBox="0 0 24 24" fill="currentColor"><path d="M13.98 10.62 20.54 3h-1.55l-5.7 6.62L8.74 3H3.5l6.88 10.01L3.5 21h1.55l6.01-6.99L15.87 21h5.24l-7.13-10.38z" /></svg> },
|
||||
{ label: 'Instagram', href: 'https://instagram.com/loyalyai', icon: <svg width="17" height="17" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2.16c3.2 0 3.58.01 4.85.07 3.25.15 4.77 1.69 4.92 4.92.06 1.27.07 1.65.07 4.85 0 3.2-.01 3.58-.07 4.85-.15 3.23-1.66 4.77-4.92 4.92-1.27.06-1.64.07-4.85.07-3.2 0-3.58-.01-4.85-.07-3.26-.15-4.77-1.7-4.92-4.92-.06-1.27-.07-1.64-.07-4.85 0-3.2.01-3.58.07-4.85.15-3.23 1.66-4.77 4.92-4.92C8.42 2.17 8.8 2.16 12 2.16zm0 1.62c-3.14 0-3.51.01-4.75.07-3.17.14-4.56 1.55-4.7 4.7-.06 1.24-.07 1.6-.07 4.75s.01 3.51.07 4.75c.14 3.15 1.53 4.56 4.7 4.7 1.24.06 1.61.07 4.75.07s3.51-.01 4.75-.07c3.17-.14 4.56-1.55 4.7-4.7.06-1.24.07-1.6.07-4.75s-.01-3.51-.07-4.75c-.14-3.15-1.53-4.56-4.7-4.7-1.24-.06-1.61-.07-4.75-.07zm0 4.32a5.74 5.74 0 100 11.48 5.74 5.74 0 000-11.48zm0 9.46a3.72 3.72 0 110-7.44 3.72 3.72 0 010 7.44zm5.96-9.65a1.34 1.34 0 11-2.68 0 1.34 1.34 0 012.68 0z" /></svg> },
|
||||
];
|
||||
|
||||
export default function GetInTouch() {
|
||||
const [formState, setFormState] = useState<FormState>('idle');
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
|
||||
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setFormState('sending');
|
||||
setErrorMessage('');
|
||||
|
||||
const form = e.currentTarget;
|
||||
const data = {
|
||||
name: (form.elements.namedItem('name') as HTMLInputElement).value,
|
||||
email: (form.elements.namedItem('email') as HTMLInputElement).value,
|
||||
subject: (form.elements.namedItem('subject') as HTMLInputElement).value,
|
||||
message: (form.elements.namedItem('message') as HTMLTextAreaElement).value,
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/contact', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const body = await res.json().catch(() => ({}));
|
||||
throw new Error(body.message || `Request failed: ${res.status}`);
|
||||
}
|
||||
setFormState('success');
|
||||
form.reset();
|
||||
} catch (err) {
|
||||
setFormState('error');
|
||||
setErrorMessage(err instanceof Error ? err.message : 'Something went wrong. Please try again.');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<section
|
||||
id="contact"
|
||||
aria-label="Contact us"
|
||||
style={{
|
||||
position: 'relative',
|
||||
margin: '0 20px 0',
|
||||
borderRadius: '24px 24px 0 0',
|
||||
overflow: 'hidden',
|
||||
background: '#07070d',
|
||||
}}
|
||||
>
|
||||
|
||||
<div style={{
|
||||
position: 'relative', zIndex: 2,
|
||||
maxWidth: 1200, margin: '0 auto',
|
||||
padding: 'clamp(56px, 8vh, 100px) clamp(28px, 5vw, 72px)',
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(320px, 1fr))',
|
||||
gap: 'clamp(40px, 5vw, 80px)',
|
||||
alignItems: 'start',
|
||||
}}>
|
||||
{/* ── Left — info ── */}
|
||||
<div>
|
||||
<div style={{
|
||||
fontSize: 11, fontWeight: 700, letterSpacing: '0.2em', textTransform: 'uppercase',
|
||||
color: '#f4be28', fontFamily: 'Manrope, sans-serif', marginBottom: 24,
|
||||
display: 'flex', alignItems: 'center', gap: 12,
|
||||
}}>
|
||||
<span style={{ width: 28, height: 1, background: '#f4be28', display: 'block' }} />Get in Touch
|
||||
</div>
|
||||
|
||||
<h2 style={{
|
||||
margin: '0 0 24px',
|
||||
fontSize: 'clamp(2rem, 4vw, 3.4rem)', fontWeight: 900,
|
||||
lineHeight: 0.98, letterSpacing: '-0.04em',
|
||||
color: '#fff', fontFamily: 'Sora, sans-serif',
|
||||
}}>
|
||||
Let's talk<br /><span style={{ color: '#f4be28' }}>strategy.</span>
|
||||
</h2>
|
||||
|
||||
<p style={{
|
||||
margin: '0 0 44px', fontSize: 16, lineHeight: 1.8,
|
||||
color: 'rgba(255,255,255,0.42)', fontFamily: 'Manrope, sans-serif', maxWidth: 380,
|
||||
}}>
|
||||
Whether you're exploring AI possibilities or ready to deploy, our team will help you find the right solution for your business.
|
||||
</p>
|
||||
|
||||
{/* Detail cards */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 14, marginBottom: 36 }}>
|
||||
{DETAILS.map((d) => (
|
||||
<div key={d.label} style={{
|
||||
display: 'flex', gap: 16, alignItems: 'flex-start',
|
||||
padding: '18px 20px', borderRadius: 16,
|
||||
background: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.07)',
|
||||
}}>
|
||||
<div style={{
|
||||
width: 40, height: 40, borderRadius: 11, flexShrink: 0,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
background: 'rgba(244,190,40,0.1)', border: '1px solid rgba(244,190,40,0.22)',
|
||||
color: '#f4be28',
|
||||
}}>{d.icon}</div>
|
||||
<div>
|
||||
<div style={{ fontSize: 11, fontWeight: 700, letterSpacing: '0.12em', textTransform: 'uppercase', color: 'rgba(255,255,255,0.4)', fontFamily: 'Manrope, sans-serif', marginBottom: 6 }}>{d.label}</div>
|
||||
<div style={{ fontSize: 15, lineHeight: 1.6, color: 'rgba(255,255,255,0.7)', fontFamily: 'Manrope, sans-serif' }}>{d.value}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Socials */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||
<span style={{ fontSize: 11, fontWeight: 700, letterSpacing: '0.12em', textTransform: 'uppercase', color: 'rgba(255,255,255,0.35)', fontFamily: 'Manrope, sans-serif' }}>Follow</span>
|
||||
{SOCIALS.map((s) => (
|
||||
<a key={s.label} href={s.href} target="_blank" rel="noopener noreferrer" aria-label={s.label} className="git-social">
|
||||
{s.icon}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Right — form ── */}
|
||||
<div style={{
|
||||
background: 'rgba(255,255,255,0.03)',
|
||||
border: '1px solid rgba(255,255,255,0.08)',
|
||||
borderRadius: 24,
|
||||
padding: 'clamp(28px, 3vw, 44px)',
|
||||
backdropFilter: 'blur(20px)',
|
||||
WebkitBackdropFilter: 'blur(20px)',
|
||||
boxShadow: 'inset 0 1px 0 rgba(255,255,255,0.05)',
|
||||
}}>
|
||||
{formState === 'success' ? (
|
||||
<div role="status" aria-live="polite" style={{ textAlign: 'center', padding: '40px 0' }}>
|
||||
<div style={{
|
||||
width: 64, height: 64, borderRadius: '50%', margin: '0 auto 24px',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
background: 'rgba(74,222,128,0.12)', border: '1px solid rgba(74,222,128,0.3)',
|
||||
}}>
|
||||
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="#4ade80" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><path d="M20 6 9 17l-5-5" /></svg>
|
||||
</div>
|
||||
<h3 style={{ margin: '0 0 10px', fontSize: 22, fontWeight: 800, color: '#fff', fontFamily: 'Sora, sans-serif', letterSpacing: '-0.02em' }}>Message sent!</h3>
|
||||
<p style={{ margin: 0, color: 'rgba(255,255,255,0.45)', fontFamily: 'Manrope, sans-serif' }}>We'll get back to you within one business day.</p>
|
||||
<button onClick={() => setFormState('idle')} style={{
|
||||
marginTop: 28, padding: '12px 28px', borderRadius: 100,
|
||||
background: 'transparent', border: '1px solid rgba(255,255,255,0.2)',
|
||||
color: '#fff', fontWeight: 600, fontSize: 14, cursor: 'pointer',
|
||||
fontFamily: 'Manrope, sans-serif',
|
||||
}}>Send another message</button>
|
||||
</div>
|
||||
) : (
|
||||
<form onSubmit={handleSubmit} noValidate>
|
||||
<h3 style={{ margin: '0 0 28px', fontSize: 22, fontWeight: 800, color: '#fff', fontFamily: 'Sora, sans-serif', letterSpacing: '-0.02em' }}>
|
||||
Send us a message
|
||||
</h3>
|
||||
|
||||
{formState === 'error' && (
|
||||
<div role="alert" style={{
|
||||
marginBottom: 20, padding: '12px 16px',
|
||||
background: 'rgba(248,113,113,0.1)', border: '1px solid rgba(248,113,113,0.3)',
|
||||
color: '#fca5a5', borderRadius: 12, fontSize: 14, fontFamily: 'Manrope, sans-serif',
|
||||
}}>{errorMessage}</div>
|
||||
)}
|
||||
|
||||
<div style={{ display: 'grid', gap: 18 }}>
|
||||
<label style={labelStyle}>
|
||||
Full Name
|
||||
<input type="text" name="name" required placeholder="Jane Smith" className="git-input" style={inputStyle} />
|
||||
</label>
|
||||
<label style={labelStyle}>
|
||||
Email Address <span aria-hidden="true" style={{ color: '#f4be28' }}>*</span>
|
||||
<input type="email" name="email" required placeholder="jane@company.com" className="git-input" style={inputStyle} />
|
||||
</label>
|
||||
<label style={labelStyle}>
|
||||
Subject
|
||||
<input type="text" name="subject" placeholder="How can we help?" className="git-input" style={inputStyle} />
|
||||
</label>
|
||||
<label style={labelStyle}>
|
||||
Message
|
||||
<textarea name="message" rows={5} placeholder="Tell us about your business and what you're trying to achieve…" className="git-input" style={{ ...inputStyle, resize: 'vertical', minHeight: 120 }} />
|
||||
</label>
|
||||
|
||||
<button type="submit" disabled={formState === 'sending'} style={{
|
||||
marginTop: 4, padding: '16px 28px', borderRadius: 100, border: 'none',
|
||||
background: formState === 'sending' ? 'rgba(244,190,40,0.6)' : '#f4be28',
|
||||
color: '#111', fontWeight: 800, fontSize: 15, fontFamily: 'Manrope, sans-serif',
|
||||
cursor: formState === 'sending' ? 'wait' : 'pointer',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8,
|
||||
boxShadow: '0 12px 36px rgba(244,190,40,0.25)',
|
||||
transition: 'transform 0.25s ease',
|
||||
}}>
|
||||
{formState === 'sending' ? (
|
||||
<>
|
||||
<span style={{ display: 'inline-block', width: 16, height: 16, border: '2px solid #111', borderTopColor: 'transparent', borderRadius: '50%', animation: 'gitspin 0.7s linear infinite' }} aria-hidden="true" />
|
||||
Sending…
|
||||
</>
|
||||
) : (
|
||||
<>Send Message <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><path d="M5 12h14M13 6l6 6-6 6" /></svg></>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>{`
|
||||
@keyframes gitspin { to { transform: rotate(360deg); } }
|
||||
.git-input::placeholder { color: rgba(255,255,255,0.3); }
|
||||
.git-input:focus { border-color: rgba(244,190,40,0.6) !important; box-shadow: 0 0 0 3px rgba(244,190,40,0.12) !important; }
|
||||
.git-social {
|
||||
width: 38px; height: 38px; border-radius: 10px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.08);
|
||||
color: rgba(255,255,255,0.5); transition: all 0.2s ease;
|
||||
}
|
||||
.git-social:hover { background: rgba(244,190,40,0.12); border-color: rgba(244,190,40,0.3); color: #f4be28; }
|
||||
`}</style>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
const labelStyle: React.CSSProperties = {
|
||||
display: 'flex', flexDirection: 'column', gap: 8,
|
||||
fontSize: 13, fontWeight: 600, color: 'rgba(255,255,255,0.55)',
|
||||
fontFamily: 'Manrope, sans-serif',
|
||||
};
|
||||
|
||||
const inputStyle: React.CSSProperties = {
|
||||
padding: '13px 16px',
|
||||
border: '1px solid rgba(255,255,255,0.1)',
|
||||
borderRadius: 12,
|
||||
fontSize: 15,
|
||||
background: 'rgba(255,255,255,0.04)',
|
||||
color: '#fff',
|
||||
outline: 'none',
|
||||
width: '100%',
|
||||
boxSizing: 'border-box',
|
||||
fontFamily: 'Manrope, sans-serif',
|
||||
transition: 'border-color 0.2s ease, box-shadow 0.2s ease',
|
||||
};
|
||||
406
src/components/Header.tsx
Normal file
406
src/components/Header.tsx
Normal file
@@ -0,0 +1,406 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import Image from 'next/image';
|
||||
|
||||
const NAV_LINKS = [
|
||||
{ label: 'Home', href: '/' },
|
||||
{ label: 'For People', href: '/for_people' },
|
||||
{ label: 'For Business', href: '/use_cases' },
|
||||
{ label: 'Solutions', href: '/solutions_page' },
|
||||
{ label: 'Blogs', href: '/blog_main' },
|
||||
{ label: 'Meet the Crew', href: '/about_us' },
|
||||
];
|
||||
|
||||
export default function Header() {
|
||||
const pathname = usePathname();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [mobileOpen, setMobileOpen] = useState(false);
|
||||
const [scrolled, setScrolled] = useState(false);
|
||||
const mobileMenuRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Hide loader after brief mount delay
|
||||
useEffect(() => {
|
||||
const t = setTimeout(() => setLoading(false), 400);
|
||||
return () => clearTimeout(t);
|
||||
}, []);
|
||||
|
||||
// Detect scroll to show/hide shadow on header
|
||||
useEffect(() => {
|
||||
const onScroll = () => setScrolled(window.scrollY > 300);
|
||||
window.addEventListener('scroll', onScroll, { passive: true });
|
||||
return () => window.removeEventListener('scroll', onScroll);
|
||||
}, []);
|
||||
|
||||
// Close mobile menu on route change
|
||||
useEffect(() => {
|
||||
setMobileOpen(false);
|
||||
}, [pathname]);
|
||||
|
||||
// Close on outside click
|
||||
useEffect(() => {
|
||||
if (!mobileOpen) return;
|
||||
const handler = (e: MouseEvent) => {
|
||||
if (mobileMenuRef.current && !mobileMenuRef.current.contains(e.target as Node)) {
|
||||
setMobileOpen(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener('mousedown', handler);
|
||||
return () => document.removeEventListener('mousedown', handler);
|
||||
}, [mobileOpen]);
|
||||
|
||||
// Prevent body scroll when mobile menu is open
|
||||
useEffect(() => {
|
||||
document.body.style.overflow = mobileOpen ? 'hidden' : '';
|
||||
return () => { document.body.style.overflow = ''; };
|
||||
}, [mobileOpen]);
|
||||
|
||||
const isActive = (href: string) =>
|
||||
href === '/' ? pathname === '/' : pathname.startsWith(href);
|
||||
|
||||
return (
|
||||
<div suppressHydrationWarning>
|
||||
{/* Page pre-loader */}
|
||||
{loading && (
|
||||
<div
|
||||
className="page-loader-container"
|
||||
aria-hidden="true"
|
||||
style={{ position: 'fixed', inset: 0, zIndex: 1000000000, display: 'flex', alignItems: 'center', justifyContent: 'center', background: '#fff8e8' }}
|
||||
>
|
||||
<div className="page-loader">
|
||||
<Image
|
||||
src="/loyaly_php/assets/images/loyalyai-logo.png"
|
||||
alt="Loyaly.AI"
|
||||
width={100}
|
||||
height={100}
|
||||
className="page-loader-logo"
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Mobile menu overlay */}
|
||||
{mobileOpen && (
|
||||
<div
|
||||
className="body-overlay"
|
||||
onClick={() => setMobileOpen(false)}
|
||||
aria-hidden="true"
|
||||
style={{ position: 'fixed', inset: 0, zIndex: 99999998, background: 'rgba(0,0,0,0.5)' }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Mobile slide-in panel */}
|
||||
<div
|
||||
ref={mobileMenuRef}
|
||||
role="dialog"
|
||||
aria-label="Navigation menu"
|
||||
aria-modal="true"
|
||||
className="mobile-header-menu-container"
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
height: '100%',
|
||||
width: 'min(320px, 85vw)',
|
||||
zIndex: 999999999,
|
||||
background: '#171512',
|
||||
color: '#fff',
|
||||
transform: mobileOpen ? 'translateX(0)' : 'translateX(-100%)',
|
||||
transition: 'transform 0.3s ease',
|
||||
overflowY: 'auto',
|
||||
padding: '24px 28px 40px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '32px',
|
||||
}}
|
||||
>
|
||||
{/* Panel header */}
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Image
|
||||
src="/loyaly_php/assets/images/loyalyai.png"
|
||||
alt="Loyaly.AI"
|
||||
width={140}
|
||||
height={40}
|
||||
style={{ objectFit: 'contain' }}
|
||||
/>
|
||||
<button
|
||||
onClick={() => setMobileOpen(false)}
|
||||
aria-label="Close menu"
|
||||
style={{ background: 'none', border: 'none', color: '#fff', fontSize: '24px', cursor: 'pointer', lineHeight: 1 }}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Nav links */}
|
||||
<nav>
|
||||
<ul style={{ listStyle: 'none', margin: 0, padding: 0, display: 'flex', flexDirection: 'column', gap: '4px' }}>
|
||||
{NAV_LINKS.map(({ label, href }) => (
|
||||
<li key={href}>
|
||||
<Link
|
||||
href={href}
|
||||
style={{
|
||||
display: 'block',
|
||||
padding: '12px 0',
|
||||
color: isActive(href) ? '#f4be28' : '#fff',
|
||||
textDecoration: 'none',
|
||||
fontSize: '16px',
|
||||
fontWeight: isActive(href) ? 700 : 500,
|
||||
borderBottom: '1px solid rgba(255,255,255,0.08)',
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
{/* Contact info */}
|
||||
<div style={{ fontSize: '14px', color: 'rgba(255,255,255,0.7)', lineHeight: 1.7 }}>
|
||||
<p style={{ margin: '0 0 4px', fontWeight: 700, textTransform: 'uppercase', fontSize: '11px', letterSpacing: '0.08em', color: '#f4be28' }}>
|
||||
Contacts
|
||||
</p>
|
||||
<p style={{ margin: 0 }}>No.424, Red Rose Towers,<br />DB Road, RS Puram<br />Coimbatore – 641002</p>
|
||||
<a href="mailto:loyalydigital@gmail.com" style={{ color: 'rgba(255,255,255,0.8)', textDecoration: 'none', display: 'block', marginTop: '8px' }}>
|
||||
loyalydigital@gmail.com
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* Socials */}
|
||||
<div>
|
||||
<p style={{ margin: '0 0 12px', fontWeight: 700, textTransform: 'uppercase', fontSize: '11px', letterSpacing: '0.08em', color: '#f4be28' }}>
|
||||
Follow us
|
||||
</p>
|
||||
<div style={{ display: 'flex', gap: '16px' }}>
|
||||
{[
|
||||
{ href: 'https://www.linkedin.com/company/loyaly-ai', label: 'LinkedIn', icon: <LinkedInIcon /> },
|
||||
{ href: 'https://twitter.com/loyalyai', label: 'X', icon: <XIcon /> },
|
||||
{ href: 'https://www.facebook.com/loyalyai', label: 'Facebook', icon: <FacebookIcon /> },
|
||||
{ href: 'https://www.youtube.com/@loyalyai', label: 'YouTube', icon: <YouTubeIcon /> },
|
||||
].map(({ href, label, icon }) => (
|
||||
<a
|
||||
key={label}
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label={label}
|
||||
style={{ color: 'rgba(255,255,255,0.7)', display: 'flex', alignItems: 'center' }}
|
||||
>
|
||||
{icon}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Link
|
||||
href="/contacts"
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
marginTop: 'auto',
|
||||
padding: '12px 24px',
|
||||
background: '#f4be28',
|
||||
color: '#171512',
|
||||
borderRadius: '4px',
|
||||
fontWeight: 700,
|
||||
textDecoration: 'none',
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
Download Now →
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Spacer only needed when header is fixed */}
|
||||
{scrolled && <div style={{ height: '88px' }} aria-hidden="true" />}
|
||||
|
||||
{/* ——— Header: in-flow until 100px scroll, then fixed ——— */}
|
||||
<header
|
||||
style={{
|
||||
position: scrolled ? 'fixed' : 'relative',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
width: 'auto',
|
||||
zIndex: 99999999,
|
||||
background: '#ffffff',
|
||||
borderBottomLeftRadius: '24px',
|
||||
borderBottomRightRadius: '24px',
|
||||
boxShadow: scrolled ? '0 4px 24px rgba(0,0,0,0.12)' : '0 2px 12px rgba(0,0,0,0.07)',
|
||||
transition: 'box-shadow 0.3s ease',
|
||||
animation: scrolled ? 'slideDown 0.3s ease' : 'none',
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
padding: '0 48px',
|
||||
height: '88px',
|
||||
width: '100%',
|
||||
boxSizing: 'border-box',
|
||||
}}>
|
||||
{/* Logo */}
|
||||
<div style={{ flexShrink: 0, flex: 1, minWidth: 120 }}>
|
||||
<Link href="/" aria-label="Loyaly.AI home" style={{ display: 'inline-flex', alignItems: 'center', textDecoration: 'none' }}>
|
||||
<Image
|
||||
src="/loyaly_php/assets/images/loyalyai.png"
|
||||
alt="Loyaly.AI"
|
||||
width={160}
|
||||
height={48}
|
||||
className="header-logo-img"
|
||||
style={{ objectFit: 'contain', height: '42px', width: 'auto' }}
|
||||
priority
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Desktop nav — dark pill */}
|
||||
<nav aria-label="Main navigation" className="desktop-nav">
|
||||
<ul className="header-nav-pill" style={{
|
||||
display: 'flex',
|
||||
listStyle: 'none',
|
||||
margin: 0,
|
||||
padding: '7px 10px',
|
||||
alignItems: 'center',
|
||||
background: '#1a1a1a',
|
||||
borderRadius: '50px',
|
||||
gap: 0,
|
||||
}}>
|
||||
{NAV_LINKS.map(({ label, href }) => {
|
||||
const active = isActive(href);
|
||||
return (
|
||||
<li key={href}>
|
||||
<Link
|
||||
href={href}
|
||||
className={`header-nav-link${active ? ' header-nav-link-active' : ''}`}
|
||||
style={{
|
||||
display: 'block',
|
||||
padding: '11px 22px',
|
||||
color: active ? '#f4be28' : '#ffffff',
|
||||
textDecoration: 'none',
|
||||
fontWeight: 600,
|
||||
fontSize: '14px',
|
||||
letterSpacing: '0.04em',
|
||||
textTransform: 'uppercase',
|
||||
borderRadius: '40px',
|
||||
background: active ? 'rgba(244,190,40,0.12)' : 'transparent',
|
||||
transition: 'color 0.2s, background 0.2s',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
{/* Right actions */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '16px', flex: 1, justifyContent: 'flex-end' }}>
|
||||
{/* Get in Touch — outlined pill, desktop */}
|
||||
<Link
|
||||
href="/contacts"
|
||||
className="header-cta desktop-nav"
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
padding: '12px 28px',
|
||||
background: 'transparent',
|
||||
color: '#1a1a1a',
|
||||
border: '1.5px solid #c9960f',
|
||||
borderRadius: '50px',
|
||||
fontWeight: 600,
|
||||
textDecoration: 'none',
|
||||
fontSize: '14px',
|
||||
whiteSpace: 'nowrap',
|
||||
transition: 'background 0.2s, color 0.2s',
|
||||
}}
|
||||
>
|
||||
Download Now
|
||||
</Link>
|
||||
|
||||
{/* Hamburger — mobile only */}
|
||||
<button
|
||||
className="menu-trigger"
|
||||
onClick={() => setMobileOpen(true)}
|
||||
aria-label="Open navigation menu"
|
||||
aria-expanded={mobileOpen}
|
||||
style={{ background: 'none', border: 'none', cursor: 'pointer', padding: '8px', display: 'none', color: '#1a1a1a' }}
|
||||
>
|
||||
<span style={{ display: 'flex', flexDirection: 'column', gap: '5px', width: '24px' }}>
|
||||
<span style={{ display: 'block', height: '2px', background: 'currentColor', borderRadius: '1px' }} />
|
||||
<span style={{ display: 'block', height: '2px', background: 'currentColor', borderRadius: '1px' }} />
|
||||
<span style={{ display: 'block', height: '2px', background: 'currentColor', borderRadius: '1px' }} />
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<style>{`
|
||||
@keyframes slideDown {
|
||||
from { transform: translateY(-100%); opacity: 0; }
|
||||
to { transform: translateY(0); opacity: 1; }
|
||||
}
|
||||
@media (max-width: 1023px) {
|
||||
.desktop-nav { display: none !important; }
|
||||
.menu-trigger { display: flex !important; }
|
||||
}
|
||||
@media (min-width: 1024px) and (max-width: 1280px) {
|
||||
.header-nav-pill { padding: 5px 6px !important; }
|
||||
.header-nav-link { padding: 8px 12px !important; font-size: 11px !important; letter-spacing: 0.02em !important; }
|
||||
.header-cta { padding: 9px 16px !important; font-size: 12px !important; }
|
||||
.header-logo-img { height: 32px !important; }
|
||||
}
|
||||
@media (min-width: 1281px) and (max-width: 1440px) {
|
||||
.header-nav-pill { padding: 6px 8px !important; }
|
||||
.header-nav-link { padding: 9px 16px !important; font-size: 12px !important; }
|
||||
.header-cta { padding: 10px 20px !important; font-size: 13px !important; }
|
||||
.header-logo-img { height: 36px !important; }
|
||||
}
|
||||
.header-cta:hover {
|
||||
background: #f4be28 !important;
|
||||
border-color: #f4be28 !important;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ——— Inline SVG social icons ——— */
|
||||
function LinkedInIcon() {
|
||||
return (
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
||||
<path d="M19.7 3H4.3C3.582 3 3 3.582 3 4.3v15.4C3 20.418 3.582 21 4.3 21h15.4c.718 0 1.3-.582 1.3-1.3V4.3C21 3.582 20.418 3 19.7 3zM8.339 18.338H5.667v-8.59h2.672v8.59zM7.004 8.574a1.548 1.548 0 1 1 0-3.096 1.548 1.548 0 0 1 0 3.096zm11.335 9.764h-2.669v-4.177c0-.996-.017-2.278-1.387-2.278-1.389 0-1.601 1.086-1.601 2.206v4.249h-2.667v-8.59h2.559v1.174h.037c.356-.675 1.227-1.387 2.526-1.387 2.703 0 3.203 1.779 3.203 4.092v4.711z"/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function XIcon() {
|
||||
return (
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
||||
<path d="M13.982 10.622 20.54 3h-1.554l-5.693 6.618L8.745 3H3.5l6.876 10.007L3.5 21h1.554l6.012-6.989L15.868 21h5.245l-7.131-10.378Zm-2.128 2.474-.697-.997-5.543-7.93H8l4.474 6.4.697.996 5.815 8.318h-2.387l-4.745-6.787Z"/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function FacebookIcon() {
|
||||
return (
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
||||
<path d="M12 2C6.5 2 2 6.5 2 12c0 5 3.7 9.1 8.4 9.9v-7H7.9V12h2.5V9.8c0-2.5 1.5-3.9 3.8-3.9 1.1 0 2.2.2 2.2.2v2.5h-1.3c-1.2 0-1.6.8-1.6 1.6V12h2.8l-.4 2.9h-2.3v7C18.3 21.1 22 17 22 12c0-5.5-4.5-10-10-10z"/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function YouTubeIcon() {
|
||||
return (
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
||||
<path d="M21.8 8s-.195-1.378-.795-1.985c-.76-.797-1.613-.801-2.004-.847C16.202 5 12.004 5 12.004 5h-.009S7.797 5 4.999 5.168c-.391.047-1.243.051-2.004.847C2.395 6.622 2.2 8 2.2 8S2 9.62 2 11.238v1.517c0 1.618.2 3.237.2 3.237s.195 1.378.795 1.985c.761.797 1.76.771 2.205.855C6.8 18.985 12 19.034 12 19.034s4.203-.006 7.001-.21c.391-.046 1.243-.05 2.004-.847.6-.607.795-1.985.795-1.985s.2-1.618.2-3.237v-1.517C22 9.62 21.8 8 21.8 8zm-11.865 6.594-.001-5.62 5.404 2.82-5.403 2.8z"/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
161
src/components/MapSection.tsx
Normal file
161
src/components/MapSection.tsx
Normal file
@@ -0,0 +1,161 @@
|
||||
'use client';
|
||||
|
||||
const LAT = 11.004985;
|
||||
const LNG = 76.950933;
|
||||
const ADDRESS = 'No.424, Red Rose Towers, DB Road, RS Puram, Coimbatore – 641002';
|
||||
|
||||
export default function MapSection() {
|
||||
const embedUrl = `https://maps.google.com/maps?q=${LAT},${LNG}&z=16&output=embed&iwloc=near`;
|
||||
|
||||
return (
|
||||
<section style={{
|
||||
margin: '0 20px 20px',
|
||||
borderRadius: 24,
|
||||
overflow: 'hidden',
|
||||
background: '#fff',
|
||||
}}>
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr 1fr',
|
||||
alignItems: 'stretch',
|
||||
minHeight: 520,
|
||||
}}>
|
||||
|
||||
{/* ── Left: text content ── */}
|
||||
<div style={{
|
||||
padding: 'clamp(48px, 7vw, 88px) clamp(32px, 6vw, 72px)',
|
||||
display: 'flex', flexDirection: 'column', justifyContent: 'center',
|
||||
}}>
|
||||
<div style={{
|
||||
fontSize: 11, fontWeight: 700, letterSpacing: '0.2em',
|
||||
textTransform: 'uppercase', color: '#f4be28',
|
||||
fontFamily: 'Manrope, sans-serif', marginBottom: 14,
|
||||
display: 'flex', alignItems: 'center', gap: 10,
|
||||
}}>
|
||||
<span style={{ width: 28, height: 2, background: '#f4be28', borderRadius: 2, display: 'block' }} />
|
||||
Our Location
|
||||
</div>
|
||||
|
||||
<h2 style={{
|
||||
fontSize: 'clamp(32px, 4vw, 56px)', fontWeight: 900,
|
||||
letterSpacing: '-0.04em', lineHeight: 1.0,
|
||||
color: '#0a0a0f', margin: '0 0 14px', fontFamily: 'Sora, sans-serif',
|
||||
}}>
|
||||
Find Us<br />
|
||||
<span style={{
|
||||
background: 'linear-gradient(105deg,#f4be28 0%,#ff9500 100%)',
|
||||
WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent',
|
||||
}}>In Coimbatore</span>
|
||||
</h2>
|
||||
|
||||
<p style={{
|
||||
fontSize: 15, color: 'rgba(0,0,0,0.45)', fontFamily: 'Manrope, sans-serif',
|
||||
lineHeight: 1.7, margin: '0 0 36px', maxWidth: 380,
|
||||
}}>
|
||||
Come visit us or drop a message — we'll get back to you within one business day.
|
||||
</p>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 16, marginBottom: 40 }}>
|
||||
{[
|
||||
{
|
||||
label: 'Address', value: ADDRESS,
|
||||
svg: <><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0118 0z"/><circle cx="12" cy="10" r="3"/></>,
|
||||
},
|
||||
{
|
||||
label: 'Email', value: 'loyalydigital@gmail.com',
|
||||
svg: <><rect x="2" y="4" width="20" height="16" rx="2"/><path d="m22 7-10 5L2 7"/></>,
|
||||
},
|
||||
{
|
||||
label: 'City', value: 'Coimbatore, Tamil Nadu, India',
|
||||
svg: <><circle cx="12" cy="12" r="10"/><path d="M12 8v4l3 3"/></>,
|
||||
},
|
||||
].map((item, i) => (
|
||||
<div key={i} style={{ display: 'flex', gap: 14, alignItems: 'flex-start' }}>
|
||||
<div style={{
|
||||
width: 40, height: 40, flexShrink: 0, borderRadius: 12,
|
||||
background: 'rgba(244,190,40,0.1)', border: '1px solid rgba(244,190,40,0.25)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
}}>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#f4be28" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
{item.svg}
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontSize: 10, fontWeight: 700, color: '#f4be28', fontFamily: 'Manrope, sans-serif', letterSpacing: '0.12em', textTransform: 'uppercase', marginBottom: 3 }}>{item.label}</div>
|
||||
<div style={{ fontSize: 13, color: '#374151', fontFamily: 'Manrope, sans-serif', lineHeight: 1.6, fontWeight: 500 }}>{item.value}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<a
|
||||
href={`https://maps.google.com/?q=${LAT},${LNG}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 10,
|
||||
padding: '14px 32px', background: '#f4be28', color: '#171512',
|
||||
fontSize: 14, fontWeight: 800, borderRadius: 100,
|
||||
textDecoration: 'none', fontFamily: 'Manrope, sans-serif',
|
||||
boxShadow: '0 8px 28px rgba(244,190,40,0.35)',
|
||||
width: 'fit-content',
|
||||
}}
|
||||
>
|
||||
Get Directions
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M5 12h14M13 6l6 6-6 6"/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* ── Right: Google Maps iframe (circular) ── */}
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
padding: 'clamp(32px, 5vw, 64px)',
|
||||
}}>
|
||||
{/* Pulsing outer rings */}
|
||||
<div style={{ position: 'relative' }}>
|
||||
<div style={{
|
||||
position: 'absolute', inset: -16, borderRadius: '50%',
|
||||
border: '2px solid rgba(244,190,40,0.25)',
|
||||
animation: 'mapPulse 3s ease-in-out infinite',
|
||||
}} />
|
||||
<div style={{
|
||||
position: 'absolute', inset: -32, borderRadius: '50%',
|
||||
border: '1px solid rgba(244,190,40,0.12)',
|
||||
animation: 'mapPulse 3s ease-in-out infinite',
|
||||
animationDelay: '0.6s',
|
||||
}} />
|
||||
|
||||
{/* Circular iframe wrapper */}
|
||||
<div style={{
|
||||
width: 'clamp(280px, 34vw, 460px)',
|
||||
height: 'clamp(280px, 34vw, 460px)',
|
||||
borderRadius: '50%',
|
||||
overflow: 'hidden',
|
||||
boxShadow: '0 24px 80px rgba(0,0,0,0.16), 0 0 0 4px rgba(244,190,40,0.3)',
|
||||
position: 'relative',
|
||||
}}>
|
||||
<iframe
|
||||
src={embedUrl}
|
||||
width="100%"
|
||||
height="100%"
|
||||
style={{ border: 'none', display: 'block' }}
|
||||
allowFullScreen
|
||||
loading="lazy"
|
||||
referrerPolicy="no-referrer-when-downgrade"
|
||||
title="Loyaly.ai Office Location"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>{`
|
||||
@media (max-width: 768px) {
|
||||
.map-section-grid { grid-template-columns: 1fr !important; }
|
||||
}
|
||||
`}</style>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
153
src/components/Partners.tsx
Normal file
153
src/components/Partners.tsx
Normal file
@@ -0,0 +1,153 @@
|
||||
import Image from 'next/image';
|
||||
|
||||
const PARTNERS = [
|
||||
{ name: 'Partner 1', src: '/loyaly_php/assets/images/partners-1.png' },
|
||||
{ name: 'Partner 2', src: '/loyaly_php/assets/images/partners-2.png' },
|
||||
{ name: 'Partner 3', src: '/loyaly_php/assets/images/partners-3.png' },
|
||||
{ name: 'Partner 4', src: '/loyaly_php/assets/images/partners-4.png' },
|
||||
{ name: 'Partner 5', src: '/loyaly_php/assets/images/partners-5.png' },
|
||||
{ name: 'Partner 6', src: '/loyaly_php/assets/images/partners-6.png' },
|
||||
];
|
||||
|
||||
// Duplicate list so the CSS marquee loops seamlessly
|
||||
const TRACK = [...PARTNERS, ...PARTNERS];
|
||||
|
||||
export default function Partners() {
|
||||
return (
|
||||
<section
|
||||
id="partners"
|
||||
aria-label="Our partners"
|
||||
style={{ padding: '60px 0', overflow: 'hidden', background: '#fff8e8', margin: '0 20px 20px', borderRadius: '24px' }}
|
||||
>
|
||||
<div style={{ textAlign: 'center', padding: '0 24px 40px' }}>
|
||||
<p
|
||||
style={{
|
||||
margin: '0 0 10px',
|
||||
fontSize: '13px',
|
||||
fontWeight: 700,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.1em',
|
||||
color: '#f4be28',
|
||||
}}
|
||||
>
|
||||
Our Partners
|
||||
</p>
|
||||
<h2
|
||||
style={{
|
||||
margin: 0,
|
||||
fontSize: 'clamp(1.2rem, 2.5vw, 1.8rem)',
|
||||
fontWeight: 700,
|
||||
color: '#171512',
|
||||
}}
|
||||
>
|
||||
Our trusted collaborators in progress and success
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{/* Marquee track */}
|
||||
<div
|
||||
style={{ position: 'relative', overflow: 'hidden' }}
|
||||
aria-hidden="true" // purely decorative – screen readers can skip
|
||||
>
|
||||
{/* Fade edges */}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '120px',
|
||||
height: '100%',
|
||||
background: 'linear-gradient(to right, #fff8e8, transparent)',
|
||||
zIndex: 1,
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
right: 0,
|
||||
width: '120px',
|
||||
height: '100%',
|
||||
background: 'linear-gradient(to left, #fff8e8, transparent)',
|
||||
zIndex: 1,
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
className="partners-track"
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '56px',
|
||||
width: 'max-content',
|
||||
animation: 'marquee 28s linear infinite',
|
||||
padding: '8px 0',
|
||||
}}
|
||||
>
|
||||
{TRACK.map(({ name, src }, i) => (
|
||||
<div
|
||||
key={`${name}-${i}`}
|
||||
style={{
|
||||
flex: '0 0 auto',
|
||||
width: '140px',
|
||||
height: '64px',
|
||||
position: 'relative',
|
||||
filter: 'grayscale(1)',
|
||||
opacity: 0.6,
|
||||
transition: 'filter 0.3s, opacity 0.3s',
|
||||
}}
|
||||
onMouseEnter={e => {
|
||||
(e.currentTarget as HTMLDivElement).style.filter = 'grayscale(0)';
|
||||
(e.currentTarget as HTMLDivElement).style.opacity = '1';
|
||||
}}
|
||||
onMouseLeave={e => {
|
||||
(e.currentTarget as HTMLDivElement).style.filter = 'grayscale(1)';
|
||||
(e.currentTarget as HTMLDivElement).style.opacity = '0.6';
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src={src}
|
||||
alt={name}
|
||||
fill
|
||||
style={{ objectFit: 'contain' }}
|
||||
sizes="140px"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>{`
|
||||
@keyframes marquee {
|
||||
from { transform: translateX(0); }
|
||||
to { transform: translateX(-50%); }
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.partners-track { animation: none; }
|
||||
}
|
||||
`}</style>
|
||||
|
||||
{/* Accessible static list for screen readers */}
|
||||
<ul
|
||||
aria-label="Partner logos"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
width: '1px',
|
||||
height: '1px',
|
||||
overflow: 'hidden',
|
||||
clip: 'rect(0,0,0,0)',
|
||||
whiteSpace: 'nowrap',
|
||||
listStyle: 'none',
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
}}
|
||||
>
|
||||
{PARTNERS.map(({ name }) => (
|
||||
<li key={name}>{name}</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
175
src/components/PhoneWidget.tsx
Normal file
175
src/components/PhoneWidget.tsx
Normal file
@@ -0,0 +1,175 @@
|
||||
'use client';
|
||||
|
||||
import React, { useRef, useEffect, useCallback } from 'react';
|
||||
|
||||
export default function PhoneWidget() {
|
||||
const stageRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const onMove = useCallback((e: MouseEvent) => {
|
||||
const el = stageRef.current;
|
||||
if (!el) return;
|
||||
const r = el.getBoundingClientRect();
|
||||
const px = (e.clientX - r.left) / r.width - 0.5;
|
||||
const py = (e.clientY - r.top) / r.height - 0.5;
|
||||
el.style.setProperty('--rx', `${(-py * 8).toFixed(2)}deg`);
|
||||
el.style.setProperty('--ry', `${(px * 10).toFixed(2)}deg`);
|
||||
el.style.setProperty('--tx', `${(px * 18).toFixed(1)}px`);
|
||||
el.style.setProperty('--ty', `${(py * 18).toFixed(1)}px`);
|
||||
}, []);
|
||||
|
||||
const onLeave = useCallback(() => {
|
||||
const el = stageRef.current;
|
||||
if (!el) return;
|
||||
el.style.setProperty('--rx', '0deg');
|
||||
el.style.setProperty('--ry', '0deg');
|
||||
el.style.setProperty('--tx', '0px');
|
||||
el.style.setProperty('--ty', '0px');
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const el = stageRef.current;
|
||||
if (!el) return;
|
||||
el.addEventListener('mousemove', onMove);
|
||||
el.addEventListener('mouseleave', onLeave);
|
||||
return () => {
|
||||
el.removeEventListener('mousemove', onMove);
|
||||
el.removeEventListener('mouseleave', onLeave);
|
||||
};
|
||||
}, [onMove, onLeave]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<style>{`
|
||||
@keyframes pwOrbFloat { 0%,100%{transform:translateY(0) scale(1)} 50%{transform:translateY(-22px) scale(1.04)} }
|
||||
@keyframes pwOrb2Float { 0%,100%{transform:translateY(0) scale(1)} 50%{transform:translateY(18px) scale(0.97)} }
|
||||
@keyframes pwGlow { 0%,100%{box-shadow:0 0 30px rgba(244,190,40,0.2)} 50%{box-shadow:0 0 60px rgba(244,190,40,0.5),0 0 120px rgba(244,190,40,0.2)} }
|
||||
@keyframes pwScreenShimmer { 0%{left:-40%} 60%{left:120%} 100%{left:120%} }
|
||||
.pw-hero-grid { display: grid; grid-template-columns: 1fr auto; align-items: center; gap: 6vw; width: 100%; }
|
||||
@media (max-width: 900px) { .pw-hero-grid { grid-template-columns: 1fr; } .pw-widget-col { display: none; } }
|
||||
`}</style>
|
||||
|
||||
<div
|
||||
ref={stageRef}
|
||||
style={{
|
||||
position: 'relative', display: 'flex',
|
||||
justifyContent: 'center', alignItems: 'center',
|
||||
perspective: 1200, cursor: 'default',
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
transformStyle: 'preserve-3d',
|
||||
transform: 'rotateX(var(--rx,0deg)) rotateY(var(--ry,0deg)) translate(var(--tx,0px), var(--ty,0px))',
|
||||
transition: 'transform 0.5s cubic-bezier(0.22,1,0.36,1)',
|
||||
position: 'relative',
|
||||
}}>
|
||||
{/* Glow ring */}
|
||||
<div style={{
|
||||
position: 'absolute', inset: -36, borderRadius: '56px',
|
||||
background: 'radial-gradient(circle at 50% 50%, rgba(244,190,40,0.2) 0%, transparent 70%)',
|
||||
animation: 'pwGlow 3s ease-in-out infinite',
|
||||
}} />
|
||||
|
||||
{/* Phone shell */}
|
||||
<div style={{
|
||||
width: 268, height: 548, borderRadius: '48px',
|
||||
background: 'linear-gradient(160deg, #22223a 0%, #10101c 100%)',
|
||||
border: '1.5px solid rgba(255,255,255,0.1)',
|
||||
boxShadow: '0 70px 130px rgba(0,0,0,0.8), 0 0 0 1px rgba(255,255,255,0.06), inset 0 1px 0 rgba(255,255,255,0.1)',
|
||||
display: 'flex', flexDirection: 'column', overflow: 'hidden',
|
||||
animation: 'pwOrbFloat 6s ease-in-out infinite', position: 'relative',
|
||||
}}>
|
||||
{/* Shimmer sweep */}
|
||||
<div style={{ position: 'absolute', inset: 0, pointerEvents: 'none', zIndex: 20, overflow: 'hidden', borderRadius: '48px' }}>
|
||||
<div style={{
|
||||
position: 'absolute', top: '-20%', left: 0, width: '40%', height: '140%',
|
||||
background: 'linear-gradient(105deg, transparent 0%, rgba(255,255,255,0.07) 50%, transparent 100%)',
|
||||
animation: 'pwScreenShimmer 4.5s ease-in-out infinite', animationDelay: '1s',
|
||||
}} />
|
||||
</div>
|
||||
|
||||
{/* Dynamic island */}
|
||||
<div style={{ display: 'flex', justifyContent: 'center', paddingTop: 16, paddingBottom: 4 }}>
|
||||
<div style={{ width: 96, height: 28, borderRadius: 20, background: '#080810', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6 }}>
|
||||
<div style={{ width: 10, height: 10, borderRadius: '50%', background: '#1a1a28', border: '1px solid rgba(255,255,255,0.08)' }} />
|
||||
<div style={{ width: 6, height: 6, borderRadius: '50%', background: '#0d0d18' }} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Header */}
|
||||
<div style={{ padding: '8px 22px 12px' }}>
|
||||
<div style={{ fontSize: 10, color: '#f4be28', fontWeight: 700, fontFamily: 'Manrope, sans-serif', letterSpacing: '0.12em', textTransform: 'uppercase' }}>Good Morning</div>
|
||||
<div style={{ fontSize: 16, color: '#fff', fontWeight: 800, fontFamily: 'Manrope, sans-serif', marginTop: 2 }}>Loyaly ✦</div>
|
||||
</div>
|
||||
|
||||
{/* Points card */}
|
||||
<div style={{
|
||||
margin: '0 14px', borderRadius: 22,
|
||||
background: 'linear-gradient(135deg, #f4be28 0%, #e09820 100%)',
|
||||
padding: '18px 20px', boxShadow: '0 8px 24px rgba(244,190,40,0.35)',
|
||||
position: 'relative', overflow: 'hidden',
|
||||
}}>
|
||||
<div style={{ position: 'absolute', top: '-30%', right: '-10%', width: 110, height: 110, borderRadius: '50%', background: 'rgba(255,255,255,0.12)' }} />
|
||||
<div style={{ fontSize: 10, color: 'rgba(23,21,18,0.65)', fontWeight: 700, fontFamily: 'Manrope, sans-serif', letterSpacing: '0.1em', textTransform: 'uppercase' }}>Total Points</div>
|
||||
<div style={{ fontSize: 34, color: '#171512', fontWeight: 900, fontFamily: 'Manrope, sans-serif', letterSpacing: '-0.03em', marginTop: 2 }}>284</div>
|
||||
<div style={{ fontSize: 10, color: 'rgba(23,21,18,0.6)', fontFamily: 'Manrope, sans-serif', marginTop: 4 }}>≈ ₹284 redeemable</div>
|
||||
</div>
|
||||
|
||||
{/* Activity feed */}
|
||||
<div style={{ padding: '14px 12px', display: 'flex', flexDirection: 'column', gap: 7, flex: 1 }}>
|
||||
{[
|
||||
{ label: 'Daily Login', pts: '+50', color: '#4ade80' },
|
||||
{ label: 'Walking 5k Steps', pts: '+80', color: '#4ade80' },
|
||||
{ label: 'Spin Wheel', pts: '-50', color: '#f87171' },
|
||||
].map((item, i) => (
|
||||
<div key={i} style={{
|
||||
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
|
||||
background: 'rgba(255,255,255,0.04)', borderRadius: 12, padding: '10px 13px',
|
||||
border: '1px solid rgba(255,255,255,0.06)',
|
||||
}}>
|
||||
<span style={{ fontSize: 12, color: 'rgba(255,255,255,0.78)', fontFamily: 'Manrope, sans-serif' }}>{item.label}</span>
|
||||
<span style={{ fontSize: 12, color: item.color, fontWeight: 700, fontFamily: 'Manrope, sans-serif' }}>{item.pts}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Nav dots */}
|
||||
<div style={{ display: 'flex', justifyContent: 'center', gap: 6, padding: '10px 0 20px' }}>
|
||||
{[1, 0, 0, 0].map((a, i) => (
|
||||
<div key={i} style={{ width: a ? 22 : 6, height: 4, borderRadius: 2, background: a ? '#f4be28' : 'rgba(255,255,255,0.2)' }} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Floating chip — top left */}
|
||||
<div style={{
|
||||
position: 'absolute', top: '12%', left: '-22%', borderRadius: 16,
|
||||
padding: '12px 16px', display: 'flex', alignItems: 'center', gap: 10,
|
||||
background: 'rgba(255,255,255,0.08)', backdropFilter: 'blur(20px)',
|
||||
border: '1px solid rgba(255,255,255,0.12)',
|
||||
boxShadow: '0 8px 32px rgba(0,0,0,0.4)',
|
||||
animation: 'pwOrb2Float 7s ease-in-out infinite',
|
||||
}}>
|
||||
<div style={{ width: 30, height: 30, borderRadius: 9, background: 'rgba(74,222,128,0.16)', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#4ade80', fontSize: 14, fontWeight: 800 }}>↑</div>
|
||||
<div>
|
||||
<div style={{ fontSize: 11, fontWeight: 700, color: '#fff', fontFamily: 'Manrope, sans-serif' }}>+120 pts</div>
|
||||
<div style={{ fontSize: 9, color: 'rgba(255,255,255,0.4)', fontFamily: 'Manrope, sans-serif' }}>just now</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Floating chip — bottom right */}
|
||||
<div style={{
|
||||
position: 'absolute', bottom: '14%', right: '-22%', borderRadius: 16,
|
||||
padding: '12px 16px',
|
||||
background: 'rgba(255,255,255,0.08)', backdropFilter: 'blur(20px)',
|
||||
border: '1px solid rgba(255,255,255,0.12)',
|
||||
boxShadow: '0 8px 32px rgba(0,0,0,0.4)',
|
||||
animation: 'pwOrbFloat 8s ease-in-out infinite',
|
||||
}}>
|
||||
<div style={{ fontSize: 9, color: 'rgba(255,255,255,0.45)', fontFamily: 'Manrope, sans-serif', letterSpacing: '0.1em', textTransform: 'uppercase', marginBottom: 3 }}>Redeemable</div>
|
||||
<div style={{ fontSize: 17, fontWeight: 900, color: '#f4be28', fontFamily: 'Sora, sans-serif', letterSpacing: '-0.02em' }}>₹284.00</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
196
src/components/ProblemParticles.tsx
Normal file
196
src/components/ProblemParticles.tsx
Normal file
@@ -0,0 +1,196 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
interface Particle {
|
||||
x: number;
|
||||
y: number;
|
||||
vx: number;
|
||||
vy: number;
|
||||
radius: number;
|
||||
alpha: number;
|
||||
type: 'lost' | 'converted' | 'regular';
|
||||
pulseSpeed: number;
|
||||
pulseTime: number;
|
||||
}
|
||||
|
||||
export default function ProblemParticles() {
|
||||
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
|
||||
let animationFrameId = 0;
|
||||
let cssWidth = canvas.offsetWidth;
|
||||
let cssHeight = canvas.offsetHeight;
|
||||
|
||||
const particles: Particle[] = [];
|
||||
const storeNode = { x: cssWidth / 2, y: cssHeight / 2, radius: 18 };
|
||||
|
||||
const setCanvasSize = () => {
|
||||
const dpr = Math.max(1, window.devicePixelRatio || 1);
|
||||
cssWidth = canvas.offsetWidth;
|
||||
cssHeight = canvas.offsetHeight;
|
||||
canvas.width = Math.round(cssWidth * dpr);
|
||||
canvas.height = Math.round(cssHeight * dpr);
|
||||
// Map drawing coordinates to CSS pixels
|
||||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||
storeNode.x = cssWidth / 2;
|
||||
storeNode.y = cssHeight / 2;
|
||||
};
|
||||
|
||||
const createParticle = (isInitial = false): Particle => {
|
||||
const typeRand = Math.random();
|
||||
const type = typeRand < 0.2 ? 'lost' : typeRand < 0.45 ? 'converted' : 'regular';
|
||||
|
||||
let x: number, y: number;
|
||||
if (isInitial) {
|
||||
x = Math.random() * cssWidth;
|
||||
y = Math.random() * cssHeight;
|
||||
} else {
|
||||
// Spawn from edges
|
||||
if (Math.random() < 0.5) {
|
||||
x = Math.random() < 0.5 ? 0 : cssWidth;
|
||||
y = Math.random() * cssHeight;
|
||||
} else {
|
||||
x = Math.random() * cssWidth;
|
||||
y = Math.random() < 0.5 ? 0 : cssHeight;
|
||||
}
|
||||
}
|
||||
|
||||
// Vector pointing towards the store node (center)
|
||||
const angle = Math.atan2(storeNode.y - y, storeNode.x - x);
|
||||
const speed = 0.4 + Math.random() * 0.8;
|
||||
|
||||
return {
|
||||
x,
|
||||
y,
|
||||
vx: Math.cos(angle) * speed + (Math.random() - 0.5) * 0.2,
|
||||
vy: Math.sin(angle) * speed + (Math.random() - 0.5) * 0.2,
|
||||
radius: 2 + Math.random() * 4,
|
||||
alpha: 0.1 + Math.random() * 0.7,
|
||||
type,
|
||||
pulseSpeed: 0.05 + Math.random() * 0.05,
|
||||
pulseTime: Math.random() * Math.PI,
|
||||
};
|
||||
};
|
||||
|
||||
// Initialize
|
||||
setCanvasSize();
|
||||
for (let i = 0; i < 45; i++) particles.push(createParticle(true));
|
||||
|
||||
const handleResize = () => {
|
||||
if (!canvas) return;
|
||||
setCanvasSize();
|
||||
};
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
|
||||
const draw = () => {
|
||||
ctx.clearRect(0, 0, cssWidth, cssHeight);
|
||||
|
||||
// Background glow
|
||||
const bgGlow = ctx.createRadialGradient(
|
||||
storeNode.x,
|
||||
storeNode.y,
|
||||
10,
|
||||
storeNode.x,
|
||||
storeNode.y,
|
||||
cssWidth * 0.4
|
||||
);
|
||||
bgGlow.addColorStop(0, 'rgba(0, 102, 255, 0.08)');
|
||||
bgGlow.addColorStop(1, 'rgba(0, 0, 0, 0)');
|
||||
ctx.fillStyle = bgGlow;
|
||||
ctx.fillRect(0, 0, cssWidth, cssHeight);
|
||||
|
||||
// Store hub
|
||||
ctx.shadowBlur = 30;
|
||||
ctx.shadowColor = '#0066ff';
|
||||
ctx.fillStyle = 'rgba(0, 102, 255, 0.9)';
|
||||
ctx.beginPath();
|
||||
ctx.arc(storeNode.x, storeNode.y, storeNode.radius, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
ctx.strokeStyle = 'rgba(255, 255, 255, 0.6)';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
ctx.arc(storeNode.x, storeNode.y, storeNode.radius - 6, 0, Math.PI * 2);
|
||||
ctx.stroke();
|
||||
ctx.shadowBlur = 0;
|
||||
|
||||
// Particles
|
||||
particles.forEach((p, idx) => {
|
||||
p.x += p.vx;
|
||||
p.y += p.vy;
|
||||
p.pulseTime += p.pulseSpeed;
|
||||
|
||||
let alpha = p.alpha;
|
||||
let radius = p.radius;
|
||||
|
||||
if (p.type === 'converted') {
|
||||
alpha = Math.max(0.4, p.alpha + Math.sin(p.pulseTime) * 0.3);
|
||||
radius = p.radius + Math.sin(p.pulseTime) * 1.5;
|
||||
} else if (p.type === 'lost') {
|
||||
const dist = Math.hypot(storeNode.x - p.x, storeNode.y - p.y);
|
||||
if (dist < 150) p.alpha -= 0.008;
|
||||
}
|
||||
|
||||
// Connections between particles
|
||||
for (let j = idx + 1; j < particles.length; j++) {
|
||||
const p2 = particles[j];
|
||||
const dist = Math.hypot(p2.x - p.x, p2.y - p.y);
|
||||
if (dist < 80 && p.alpha > 0 && p2.alpha > 0) {
|
||||
const lineAlpha = (1 - dist / 80) * 0.18 * Math.min(alpha, p2.alpha);
|
||||
ctx.strokeStyle = `rgba(0, 102, 255, ${lineAlpha})`;
|
||||
ctx.lineWidth = 1;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(p.x, p.y);
|
||||
ctx.lineTo(p2.x, p2.y);
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
|
||||
// Connection to store hub
|
||||
const distToStore = Math.hypot(storeNode.x - p.x, storeNode.y - p.y);
|
||||
if (distToStore < 120 && p.alpha > 0) {
|
||||
const lineAlpha = (1 - distToStore / 120) * 0.25 * p.alpha;
|
||||
ctx.strokeStyle = p.type === 'converted' ? `rgba(0, 220, 255, ${lineAlpha})` : `rgba(0, 102, 255, ${lineAlpha})`;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(p.x, p.y);
|
||||
ctx.lineTo(storeNode.x, storeNode.y);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
// Respawn check
|
||||
if (distToStore < storeNode.radius || p.x < 0 || p.x > cssWidth || p.y < 0 || p.y > cssHeight || p.alpha <= 0) {
|
||||
particles[idx] = createParticle();
|
||||
} else {
|
||||
ctx.fillStyle = p.type === 'converted' ? `rgba(0, 220, 255, ${alpha})` : p.type === 'lost' ? `rgba(255, 68, 68, ${alpha})` : `rgba(255, 255, 255, ${alpha})`;
|
||||
if (p.type === 'converted') {
|
||||
ctx.shadowBlur = 10;
|
||||
ctx.shadowColor = '#00dcff';
|
||||
}
|
||||
ctx.beginPath();
|
||||
ctx.arc(p.x, p.y, radius, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.shadowBlur = 0;
|
||||
}
|
||||
});
|
||||
|
||||
animationFrameId = requestAnimationFrame(draw);
|
||||
};
|
||||
|
||||
draw();
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
cancelAnimationFrame(animationFrameId);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return <canvas ref={canvasRef} className="w-full h-full absolute inset-0" />;
|
||||
}
|
||||
237
src/components/Solutions.tsx
Normal file
237
src/components/Solutions.tsx
Normal file
@@ -0,0 +1,237 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
|
||||
const SOLUTIONS = [
|
||||
{
|
||||
id: 'footfall',
|
||||
category: 'AI Solution',
|
||||
title: 'Footfall Intelligence',
|
||||
subtitle: 'Understand Customer Movement in Real Time',
|
||||
excerpt:
|
||||
'Track visitor count, movement patterns, and peak hours across your retail spaces. Turn anonymous foot traffic into actionable intelligence that drives layout, staffing, and promotions.',
|
||||
image: '/loyaly_php/assets/images/deepvision-fi-640x640.jpg',
|
||||
imageAlt: 'Footfall Intelligence dashboard showing real-time customer movement heatmaps',
|
||||
href: '/solutions/footfall-intelligence',
|
||||
},
|
||||
{
|
||||
id: 'journey',
|
||||
category: 'AI Solution',
|
||||
title: 'Customer Journey',
|
||||
subtitle: 'Map Every Interaction Seamlessly',
|
||||
excerpt:
|
||||
'Track and understand each step of your customer\'s journey — online and in-store — to identify friction points, improve conversion, and deliver experiences that build loyalty.',
|
||||
image: '/loyaly_php/assets/images/nlpgenius-natural-language-processing-fi-640x640.jpg',
|
||||
imageAlt: 'Customer journey map visualisation inside Loyaly.AI platform',
|
||||
href: '/solutions/customer-journey',
|
||||
},
|
||||
{
|
||||
id: 'engagement',
|
||||
category: 'AI Solution',
|
||||
title: 'Customer Engagement',
|
||||
subtitle: 'Drive Meaningful Interactions',
|
||||
excerpt:
|
||||
'Analyse behaviour and preferences to deliver personalised experiences, timely offers, and targeted messaging that keep customers coming back.',
|
||||
image: '/loyaly_php/assets/images/smarttrader-predictive-stock-market-analysis-fi-640x640.jpg',
|
||||
imageAlt: 'Customer engagement analytics showing personalisation performance metrics',
|
||||
href: '/solutions/customer-engagement',
|
||||
},
|
||||
{
|
||||
id: 'loyalty',
|
||||
category: 'AI Solution',
|
||||
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.',
|
||||
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',
|
||||
},
|
||||
{
|
||||
id: 'revenue',
|
||||
category: 'AI Solution',
|
||||
title: 'Revenue Optimisation',
|
||||
subtitle: 'Maximise Business Performance',
|
||||
excerpt:
|
||||
'Leverage data insights to optimise pricing, promotions, and strategies for higher revenue growth — without sacrificing margin.',
|
||||
image: '/loyaly_php/assets/images/neuraforge-crafting-future-ready-ai-solutions-fi-640x640.jpg',
|
||||
imageAlt: 'Revenue optimisation charts showing margin improvements',
|
||||
href: '/solutions/revenue-optimisation',
|
||||
},
|
||||
];
|
||||
|
||||
export default function Solutions() {
|
||||
const [hoveredId, setHoveredId] = useState<string | null>(null);
|
||||
|
||||
return (
|
||||
<section
|
||||
id="solutions"
|
||||
aria-label="Our solutions"
|
||||
style={{ padding: '80px 24px', background: '#fff', margin: '0 20px 20px', borderRadius: '24px', overflow: 'hidden' }}
|
||||
>
|
||||
<div style={{ maxWidth: '1200px', margin: '0 auto' }}>
|
||||
{/* Header */}
|
||||
<div style={{ marginBottom: '56px', maxWidth: '640px' }}>
|
||||
<p
|
||||
style={{
|
||||
margin: '0 0 12px',
|
||||
fontSize: '13px',
|
||||
fontWeight: 700,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.1em',
|
||||
color: '#f4be28',
|
||||
}}
|
||||
>
|
||||
Our Solutions
|
||||
</p>
|
||||
<h2
|
||||
style={{
|
||||
margin: '0 0 16px',
|
||||
fontSize: 'clamp(1.6rem, 3vw, 2.4rem)',
|
||||
fontWeight: 800,
|
||||
lineHeight: 1.2,
|
||||
color: '#171512',
|
||||
}}
|
||||
>
|
||||
Exploring groundbreaking AI solutions transforming retail
|
||||
</h2>
|
||||
<p style={{ margin: 0, fontSize: '16px', lineHeight: 1.7, color: '#4f463a' }}>
|
||||
From understanding footfall to optimising revenue, our five AI modules work independently
|
||||
or together as one unified intelligence engine.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Grid */}
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))',
|
||||
gap: '28px',
|
||||
marginBottom: '48px',
|
||||
}}
|
||||
>
|
||||
{SOLUTIONS.map(({ id, category, title, subtitle, excerpt, image, imageAlt, href }) => {
|
||||
const isHovered = hoveredId === id;
|
||||
return (
|
||||
<article
|
||||
key={id}
|
||||
onMouseEnter={() => setHoveredId(id)}
|
||||
onMouseLeave={() => setHoveredId(null)}
|
||||
style={{
|
||||
background: '#fff8e8',
|
||||
borderRadius: '16px',
|
||||
overflow: 'hidden',
|
||||
boxShadow: isHovered
|
||||
? '0 12px 40px rgba(23,21,18,0.14)'
|
||||
: '0 2px 12px rgba(23,21,18,0.06)',
|
||||
transform: isHovered ? 'translateY(-6px)' : 'translateY(0)',
|
||||
transition: 'transform 0.3s ease, box-shadow 0.3s ease',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
{/* Card image */}
|
||||
<Link href={href} tabIndex={-1} aria-hidden="true" style={{ display: 'block' }}>
|
||||
<div style={{ position: 'relative', height: '220px', overflow: 'hidden' }}>
|
||||
<Image
|
||||
src={image}
|
||||
alt={imageAlt}
|
||||
fill
|
||||
style={{
|
||||
objectFit: 'cover',
|
||||
transform: isHovered ? 'scale(1.05)' : 'scale(1)',
|
||||
transition: 'transform 0.4s ease',
|
||||
}}
|
||||
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
|
||||
/>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
{/* Card content */}
|
||||
<div
|
||||
style={{
|
||||
padding: '24px 24px 28px',
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '10px',
|
||||
}}
|
||||
>
|
||||
{/* Category badge */}
|
||||
<span
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
padding: '4px 10px',
|
||||
background: '#fff2c8',
|
||||
color: '#bc7122',
|
||||
borderRadius: '20px',
|
||||
fontSize: '12px',
|
||||
fontWeight: 700,
|
||||
letterSpacing: '0.04em',
|
||||
alignSelf: 'flex-start',
|
||||
}}
|
||||
>
|
||||
{category}
|
||||
</span>
|
||||
|
||||
<h3 style={{ margin: 0, fontSize: '19px', fontWeight: 800, color: '#171512', lineHeight: 1.2 }}>
|
||||
<Link href={href} style={{ textDecoration: 'none', color: 'inherit' }}>
|
||||
{title}
|
||||
</Link>
|
||||
</h3>
|
||||
|
||||
<p style={{ margin: 0, fontSize: '14px', fontWeight: 600, color: '#bc7122' }}>
|
||||
{subtitle}
|
||||
</p>
|
||||
|
||||
<p style={{ margin: 0, fontSize: '14px', lineHeight: 1.7, color: '#4f463a', flex: 1 }}>
|
||||
{excerpt}
|
||||
</p>
|
||||
|
||||
<Link
|
||||
href={href}
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '6px',
|
||||
marginTop: '8px',
|
||||
fontWeight: 700,
|
||||
fontSize: '14px',
|
||||
color: '#171512',
|
||||
textDecoration: 'none',
|
||||
}}
|
||||
>
|
||||
Explore more <span aria-hidden="true">→</span>
|
||||
</Link>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* CTA */}
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<Link
|
||||
href="/solutions_page"
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
padding: '14px 36px',
|
||||
background: '#f4be28',
|
||||
color: '#171512',
|
||||
borderRadius: '4px',
|
||||
fontWeight: 700,
|
||||
textDecoration: 'none',
|
||||
fontSize: '16px',
|
||||
}}
|
||||
>
|
||||
View all solutions <span aria-hidden="true">→</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
211
src/components/Testimonial.tsx
Normal file
211
src/components/Testimonial.tsx
Normal file
@@ -0,0 +1,211 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||
|
||||
const TESTIMONIALS = [
|
||||
{
|
||||
quote:
|
||||
'Loyaly.AI transformed how we understand our customers. Foot traffic insights alone helped us rearrange our store layout and boost average basket size by 18% in the first quarter.',
|
||||
author: 'Priya Nair',
|
||||
role: 'Head of Retail Operations',
|
||||
company: 'Trendsetter Fashion Stores',
|
||||
avatar: '/loyaly_php/assets/images/partners-1.png',
|
||||
},
|
||||
{
|
||||
quote:
|
||||
'The AI-powered loyalty engine learns fast. Within weeks we were delivering personalised offers that felt genuinely relevant — our repeat-visit rate climbed 23%.',
|
||||
author: 'Arjun Mehta',
|
||||
role: 'VP of Customer Experience',
|
||||
company: 'FreshMart Supermarkets',
|
||||
avatar: '/loyaly_php/assets/images/partners-2.png',
|
||||
},
|
||||
{
|
||||
quote:
|
||||
"We tried three loyalty platforms before Loyaly.AI. This is the first one that actually connects footfall data, promotions, and CRM in a single dashboard. It's changed how we run campaigns.",
|
||||
author: 'Sarah Connelly',
|
||||
role: 'Chief Marketing Officer',
|
||||
company: 'Urban Living Group',
|
||||
avatar: '/loyaly_php/assets/images/partners-3.png',
|
||||
},
|
||||
];
|
||||
|
||||
export default function Testimonial() {
|
||||
const [active, setActive] = useState(0);
|
||||
const [paused, setPaused] = useState(false);
|
||||
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const total = TESTIMONIALS.length;
|
||||
|
||||
const go = useCallback(
|
||||
(index: number) => {
|
||||
setActive((index + total) % total);
|
||||
},
|
||||
[total]
|
||||
);
|
||||
|
||||
// Auto-advance every 6 s
|
||||
useEffect(() => {
|
||||
if (paused) return;
|
||||
timerRef.current = setTimeout(() => go(active + 1), 6000);
|
||||
return () => {
|
||||
if (timerRef.current) clearTimeout(timerRef.current);
|
||||
};
|
||||
}, [active, paused, go]);
|
||||
|
||||
const { quote, author, role, company } = TESTIMONIALS[active];
|
||||
|
||||
return (
|
||||
<section
|
||||
id="testimonials"
|
||||
aria-label="Customer testimonials"
|
||||
style={{ background: '#171512', color: '#fff', padding: '80px 24px', margin: '0 20px 20px', borderRadius: '24px', overflow: 'hidden' }}
|
||||
onMouseEnter={() => setPaused(true)}
|
||||
onMouseLeave={() => setPaused(false)}
|
||||
>
|
||||
<div style={{ maxWidth: '860px', margin: '0 auto', textAlign: 'center' }}>
|
||||
{/* Section label */}
|
||||
<p
|
||||
style={{
|
||||
margin: '0 0 12px',
|
||||
fontSize: '13px',
|
||||
fontWeight: 700,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.1em',
|
||||
color: '#f4be28',
|
||||
}}
|
||||
>
|
||||
Customer Stories
|
||||
</p>
|
||||
<h2
|
||||
style={{
|
||||
margin: '0 0 52px',
|
||||
fontSize: 'clamp(1.4rem, 3vw, 2rem)',
|
||||
fontWeight: 700,
|
||||
lineHeight: 1.3,
|
||||
color: '#fff',
|
||||
}}
|
||||
>
|
||||
See how retailers are transforming customer loyalty with Loyaly.AI
|
||||
</h2>
|
||||
|
||||
{/* Quote card */}
|
||||
<div
|
||||
key={active}
|
||||
style={{
|
||||
background: 'rgba(255,255,255,0.05)',
|
||||
borderRadius: '16px',
|
||||
padding: '48px 40px',
|
||||
position: 'relative',
|
||||
animation: 'fadeIn 0.5s ease',
|
||||
}}
|
||||
>
|
||||
{/* Decorative quote mark */}
|
||||
<span
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '24px',
|
||||
left: '32px',
|
||||
fontSize: '80px',
|
||||
lineHeight: 1,
|
||||
color: '#f4be28',
|
||||
opacity: 0.25,
|
||||
fontFamily: 'Georgia, serif',
|
||||
}}
|
||||
>
|
||||
"
|
||||
</span>
|
||||
|
||||
<blockquote
|
||||
style={{
|
||||
margin: '0 0 32px',
|
||||
fontSize: 'clamp(1rem, 2.2vw, 1.2rem)',
|
||||
lineHeight: 1.8,
|
||||
color: 'rgba(255,255,255,0.9)',
|
||||
fontStyle: 'italic',
|
||||
position: 'relative',
|
||||
zIndex: 1,
|
||||
}}
|
||||
>
|
||||
{quote}
|
||||
</blockquote>
|
||||
|
||||
<footer style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '4px' }}>
|
||||
<cite style={{ fontStyle: 'normal', fontWeight: 700, fontSize: '16px', color: '#f4be28' }}>
|
||||
{author}
|
||||
</cite>
|
||||
<span style={{ fontSize: '14px', color: 'rgba(255,255,255,0.6)' }}>
|
||||
{role}, {company}
|
||||
</span>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
{/* Navigation dots */}
|
||||
<div
|
||||
role="tablist"
|
||||
aria-label="Testimonial navigation"
|
||||
style={{ display: 'flex', justifyContent: 'center', gap: '10px', marginTop: '32px' }}
|
||||
>
|
||||
{TESTIMONIALS.map((_, i) => (
|
||||
<button
|
||||
key={i}
|
||||
role="tab"
|
||||
aria-selected={i === active}
|
||||
aria-label={`Show testimonial ${i + 1}`}
|
||||
onClick={() => { go(i); setPaused(true); }}
|
||||
style={{
|
||||
width: i === active ? '28px' : '10px',
|
||||
height: '10px',
|
||||
borderRadius: '5px',
|
||||
background: i === active ? '#f4be28' : 'rgba(255,255,255,0.25)',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
padding: 0,
|
||||
transition: 'width 0.3s ease, background 0.3s ease',
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Prev / Next */}
|
||||
<div style={{ display: 'flex', justifyContent: 'center', gap: '12px', marginTop: '20px' }}>
|
||||
<button
|
||||
onClick={() => { go(active - 1); setPaused(true); }}
|
||||
aria-label="Previous testimonial"
|
||||
style={arrowBtnStyle}
|
||||
>
|
||||
←
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { go(active + 1); setPaused(true); }}
|
||||
aria-label="Next testimonial"
|
||||
style={arrowBtnStyle}
|
||||
>
|
||||
→
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>{`
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(12px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
`}</style>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
const arrowBtnStyle: React.CSSProperties = {
|
||||
width: '40px',
|
||||
height: '40px',
|
||||
borderRadius: '50%',
|
||||
background: 'rgba(255,255,255,0.08)',
|
||||
color: '#fff',
|
||||
border: '1px solid rgba(255,255,255,0.15)',
|
||||
cursor: 'pointer',
|
||||
fontSize: '16px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
transition: 'background 0.2s',
|
||||
};
|
||||
139
src/components/for-people/AIRecommendationsSection.tsx
Normal file
139
src/components/for-people/AIRecommendationsSection.tsx
Normal file
@@ -0,0 +1,139 @@
|
||||
'use client';
|
||||
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
|
||||
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' },
|
||||
];
|
||||
|
||||
export default function AIRecommendationsSection() {
|
||||
const [active, setActive] = useState(false);
|
||||
const [hovered, setHovered] = useState<number | null>(null);
|
||||
const panelRef = useRef<HTMLDivElement>(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();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<section ref={panelRef} className="fp-panel fp-light" style={{ background: '#ffffff' }}>
|
||||
<div className="fp-shell">
|
||||
<div className="fp-split" style={{ gridTemplateColumns: '0.85fr 1.15fr' }}>
|
||||
|
||||
{/* Left — editorial */}
|
||||
<div>
|
||||
<div style={{ marginBottom: 28 }}><span className="fp-kicker fp-rise">AI Intelligence · 05</span></div>
|
||||
|
||||
<h2 style={{
|
||||
fontSize: 'clamp(40px, 5vw, 72px)', fontWeight: 900, letterSpacing: '-0.05em', lineHeight: 0.94,
|
||||
color: '#171512', margin: '0 0 28px', fontFamily: 'Sora, sans-serif',
|
||||
}}>
|
||||
<span className="fp-mask"><span>Built for</span></span>
|
||||
<span className="fp-mask"><span style={{ color: '#f4be28' }}>you,</span></span>
|
||||
<span className="fp-mask"><span>by data.</span></span>
|
||||
</h2>
|
||||
|
||||
<p className="fp-rise" style={{
|
||||
fontSize: 15, lineHeight: 1.8, color: '#4f463a', fontFamily: 'Manrope, sans-serif', margin: '0 0 36px', maxWidth: 340,
|
||||
}}>
|
||||
Our recommendation engine learns your taste from every transaction and surfaces offers uniquely matched to you — getting sharper with every visit.
|
||||
</p>
|
||||
|
||||
{/* Stat chips — brand gold palette */}
|
||||
<div className="fp-rise" style={{ display: 'flex', gap: 14, transitionDelay: '0.2s' }}>
|
||||
{[
|
||||
{ val: '94%', lbl: 'match accuracy' },
|
||||
{ val: '3ms', lbl: 'response time' },
|
||||
].map((s, i) => (
|
||||
<div key={i} style={{ padding: '16px 20px', borderRadius: 16, background: '#fff7e0', border: '2px solid #f4be28', minWidth: 110 }}>
|
||||
<div style={{ fontSize: 28, fontWeight: 900, color: '#bc7122', fontFamily: 'Sora, sans-serif', letterSpacing: '-0.03em' }}>{s.val}</div>
|
||||
<div style={{ fontSize: 11, color: 'rgba(23,21,18,0.55)', fontFamily: 'Manrope, sans-serif', marginTop: 4 }}>{s.lbl}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right — recommendation cards */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 4, padding: '0 2px' }}>
|
||||
<span style={{ fontSize: 12, fontWeight: 700, letterSpacing: '0.1em', textTransform: 'uppercase', color: '#171512', fontFamily: 'Manrope, sans-serif' }}>Recommended for you</span>
|
||||
<span style={{ fontSize: 11, color: '#bc7122', fontFamily: 'Manrope, sans-serif', fontWeight: 700,
|
||||
display: 'inline-flex', alignItems: 'center', gap: 5,
|
||||
background: '#fff7e0', border: '1px solid #f4be28', borderRadius: 100, padding: '4px 10px',
|
||||
}}>
|
||||
<span style={{ display: 'inline-block', animation: 'fpSpinSlow 3s linear infinite' }}>↻</span> Updating
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{recs.map((rec, i) => (
|
||||
<div
|
||||
key={i}
|
||||
onMouseEnter={() => setHovered(i)}
|
||||
onMouseLeave={() => setHovered(null)}
|
||||
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`,
|
||||
}}
|
||||
>
|
||||
{/* Rank */}
|
||||
<span style={{ fontSize: 11, fontWeight: 800, color: 'rgba(23,21,18,0.25)', fontFamily: 'Sora, sans-serif', flexShrink: 0 }}>{rec.rank}</span>
|
||||
|
||||
{/* Gold accent bar */}
|
||||
<div style={{ width: 4, height: 42, borderRadius: 100, background: '#f4be28', flexShrink: 0 }} />
|
||||
|
||||
{/* Info */}
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ fontSize: 14, fontWeight: 700, color: '#171512', fontFamily: 'Manrope, sans-serif', marginBottom: 3 }}>{rec.label}</div>
|
||||
<div style={{ fontSize: 11, color: 'rgba(23,21,18,0.45)', fontFamily: 'Manrope, sans-serif' }}>{rec.tag}</div>
|
||||
</div>
|
||||
|
||||
{/* Match badge — brand gold */}
|
||||
<div style={{
|
||||
padding: '6px 14px', borderRadius: 100, flexShrink: 0,
|
||||
background: '#fff7e0', border: '1.5px solid #f4be28',
|
||||
textAlign: 'center',
|
||||
}}>
|
||||
<div style={{ fontSize: 14, fontWeight: 900, color: '#bc7122', fontFamily: 'Sora, sans-serif' }}>{rec.match}</div>
|
||||
<div style={{ fontSize: 9, color: 'rgba(23,21,18,0.4)', fontFamily: 'Manrope, sans-serif' }}>match</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Confidence bar */}
|
||||
<div style={{ padding: '16px 2px 0' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}>
|
||||
<span style={{ fontSize: 12, fontWeight: 600, color: '#171512', fontFamily: 'Manrope, sans-serif' }}>Model confidence</span>
|
||||
<span style={{
|
||||
fontSize: 11, fontWeight: 700, color: '#bc7122', fontFamily: 'Manrope, sans-serif',
|
||||
background: '#fff7e0', border: '1px solid #f4be28', borderRadius: 100, padding: '3px 10px',
|
||||
}}>High</span>
|
||||
</div>
|
||||
<div style={{ height: 10, background: 'rgba(23,21,18,0.07)', borderRadius: 100, overflow: 'hidden' }}>
|
||||
<div style={{
|
||||
height: '100%', width: active ? '92%' : '0%', borderRadius: 100,
|
||||
background: 'linear-gradient(90deg, #f4be28, #bc7122)',
|
||||
transition: 'width 1.4s cubic-bezier(0.22,1,0.36,1) 0.8s',
|
||||
}} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
109
src/components/for-people/DiscoverDealsSection.tsx
Normal file
109
src/components/for-people/DiscoverDealsSection.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
|
||||
const deals = [
|
||||
{ store: 'Starbucks', category: 'Coffee & Drinks', offer: '2× Points', 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 },
|
||||
{ store: 'Zara', category: 'Fashion', offer: '15% OFF', savings: '₹450 saved', color: '#9ca3af', distance: '0.9 km', rating: 4.5 },
|
||||
];
|
||||
|
||||
export default function DiscoverDealsSection() {
|
||||
const [hovered, setHovered] = useState<number | null>(null);
|
||||
|
||||
return (
|
||||
<section className="fp-panel" style={{ background: '#08080f' }}>
|
||||
|
||||
{/* Soft radial */}
|
||||
<div className="fp-deco" style={{
|
||||
position: 'absolute', top: '20%', left: '20%', width: 500, height: 500, borderRadius: '50%',
|
||||
background: 'radial-gradient(circle, rgba(244,190,40,0.06) 0%, transparent 65%)', zIndex: 0,
|
||||
}} />
|
||||
|
||||
<div className="fp-shell">
|
||||
<div className="fp-split" style={{ gridTemplateColumns: '0.85fr 1.15fr', alignItems: 'center' }}>
|
||||
|
||||
{/* Headline column */}
|
||||
<div>
|
||||
<div style={{ marginBottom: 28 }}><span className="fp-kicker fp-rise">Discover · 02</span></div>
|
||||
|
||||
<h2 style={{
|
||||
fontSize: 'clamp(44px, 5.6vw, 88px)', fontWeight: 900, lineHeight: 0.94,
|
||||
letterSpacing: '-0.05em', color: '#fff', margin: '0 0 28px', fontFamily: 'Sora, sans-serif',
|
||||
}}>
|
||||
<span className="fp-mask"><span>Every</span></span>
|
||||
<span className="fp-mask"><span>deal in</span></span>
|
||||
<span className="fp-mask"><span className="fp-shimmer">your city.</span></span>
|
||||
</h2>
|
||||
|
||||
<p className="fp-rise" style={{
|
||||
fontSize: 16, lineHeight: 1.8, color: 'rgba(255,255,255,0.45)', fontFamily: 'Manrope, sans-serif', margin: '0 0 36px', maxWidth: 380,
|
||||
}}>
|
||||
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.
|
||||
</p>
|
||||
|
||||
<div className="fp-rise" style={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 9,
|
||||
background: 'rgba(255,255,255,0.04)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: 100, padding: '11px 18px', backdropFilter: 'blur(12px)',
|
||||
}}>
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none">
|
||||
<circle cx="12" cy="10" r="3" stroke="#f4be28" strokeWidth="2" />
|
||||
<path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7z" stroke="#f4be28" strokeWidth="2" fill="none" />
|
||||
</svg>
|
||||
<span style={{ fontSize: 13, fontWeight: 600, color: 'rgba(255,255,255,0.65)', fontFamily: 'Manrope, sans-serif' }}>Coimbatore, TN</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Deal list */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
|
||||
{deals.map((deal, i) => (
|
||||
<div
|
||||
key={i}
|
||||
onMouseEnter={() => setHovered(i)}
|
||||
onMouseLeave={() => setHovered(null)}
|
||||
className="fp-rise"
|
||||
style={{
|
||||
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)',
|
||||
border: `1px solid ${hovered === i ? 'rgba(244,190,40,0.28)' : 'rgba(255,255,255,0.07)'}`,
|
||||
backdropFilter: 'blur(20px)', cursor: 'pointer',
|
||||
transitionDelay: `${0.1 + i * 0.07}s`,
|
||||
boxShadow: hovered === i ? '0 14px 40px rgba(0,0,0,0.35), inset 0 1px 0 rgba(255,255,255,0.06)' : 'none',
|
||||
transform: `${hovered === i ? 'translateX(8px)' : 'translateX(0)'}`,
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
width: 46, height: 46, borderRadius: 13, background: `${deal.color}22`, border: `1px solid ${deal.color}44`,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
}}>
|
||||
<div style={{ width: 15, height: 15, borderRadius: '50%', background: deal.color }} />
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 4 }}>
|
||||
<span style={{ fontSize: 16, fontWeight: 700, color: '#fff', fontFamily: 'Manrope, sans-serif' }}>{deal.store}</span>
|
||||
<span style={{ fontSize: 11, color: 'rgba(255,255,255,0.32)', fontFamily: 'Manrope, sans-serif' }}>{deal.distance}</span>
|
||||
<span style={{ fontSize: 11, color: '#f4be28', fontFamily: 'Manrope, sans-serif' }}>★ {deal.rating}</span>
|
||||
</div>
|
||||
<span style={{ fontSize: 12, color: 'rgba(255,255,255,0.38)', fontFamily: 'Manrope, sans-serif' }}>{deal.category}</span>
|
||||
</div>
|
||||
<div style={{ textAlign: 'right' }}>
|
||||
<div style={{ fontSize: 15, fontWeight: 800, color: '#f4be28', fontFamily: 'Sora, sans-serif', letterSpacing: '-0.02em' }}>{deal.offer}</div>
|
||||
<div style={{ fontSize: 11, color: 'rgba(255,255,255,0.32)', fontFamily: 'Manrope, sans-serif', marginTop: 2 }}>{deal.savings}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div className="fp-rise" style={{
|
||||
textAlign: 'right', paddingRight: 4, marginTop: 4, transitionDelay: '0.5s',
|
||||
fontSize: 12, color: 'rgba(255,255,255,0.25)', fontFamily: 'Manrope, sans-serif', letterSpacing: '0.06em',
|
||||
}}>
|
||||
+ 2,400 more deals near you
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
161
src/components/for-people/EarnRewardsSection.tsx
Normal file
161
src/components/for-people/EarnRewardsSection.tsx
Normal file
@@ -0,0 +1,161 @@
|
||||
'use client';
|
||||
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
|
||||
const feed = [
|
||||
{ store: 'Daily Login', pts: +50, time: '2m ago', type: 'earn' },
|
||||
{ store: 'Walking 5,000 steps', pts: +80, time: '1h ago', type: 'earn' },
|
||||
{ store: 'Spin Wheel Game', pts: +120, time: '3h ago', type: 'earn' },
|
||||
{ store: 'Selfie Booth', pts: +200, time: 'Yesterday', type: 'earn' },
|
||||
{ store: 'Scratch Card', pts: +40, time: 'Yesterday', type: 'earn' },
|
||||
{ store: 'Match Master', pts: +75, time: '2 days ago', type: 'earn' },
|
||||
{ store: 'Maze Challenge', pts: +95, time: '3 days ago', type: 'earn' },
|
||||
];
|
||||
|
||||
export default function EarnRewardsSection() {
|
||||
const [pts, setPts] = useState(0);
|
||||
const panelRef = useRef<HTMLDivElement>(null);
|
||||
const rafRef = useRef<number>(0);
|
||||
|
||||
useEffect(() => {
|
||||
const el = panelRef.current;
|
||||
if (!el) return;
|
||||
const obs = new MutationObserver(() => {
|
||||
if (el.hasAttribute('data-active')) {
|
||||
const start = performance.now();
|
||||
const tick = (now: number) => {
|
||||
const t = Math.min((now - start) / 1800, 1);
|
||||
setPts(Math.round((1 - Math.pow(1 - t, 3)) * 284));
|
||||
if (t < 1) rafRef.current = requestAnimationFrame(tick);
|
||||
};
|
||||
rafRef.current = requestAnimationFrame(tick);
|
||||
}
|
||||
});
|
||||
obs.observe(el, { attributes: true, attributeFilter: ['data-active'] });
|
||||
return () => { obs.disconnect(); cancelAnimationFrame(rafRef.current); };
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<section ref={panelRef} className="fp-panel fp-light" style={{ background: '#ffffff' }}>
|
||||
<div className="fp-shell">
|
||||
<div className="fp-split" style={{ gridTemplateColumns: '1.05fr 0.95fr' }}>
|
||||
|
||||
{/* Left — points + headline */}
|
||||
<div>
|
||||
{/* Live label */}
|
||||
<div className="fp-rise" style={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 8, marginBottom: 20,
|
||||
background: '#f0fdf4', border: '1.5px solid #16a34a', borderRadius: 100, padding: '7px 16px',
|
||||
}}>
|
||||
<span style={{ width: 7, height: 7, borderRadius: '50%', background: '#16a34a', flexShrink: 0, animation: 'fpPulse 1.5s infinite' }} />
|
||||
<span style={{ fontSize: 11, fontWeight: 700, color: '#16a34a', fontFamily: 'Manrope, sans-serif', letterSpacing: '0.1em', textTransform: 'uppercase' }}>Live points balance</span>
|
||||
</div>
|
||||
|
||||
{/* Giant gold number */}
|
||||
<div style={{
|
||||
fontSize: 'clamp(80px, 12vw, 168px)', fontWeight: 900, fontFamily: 'Sora, sans-serif',
|
||||
color: '#f4be28', letterSpacing: '-0.05em', lineHeight: 0.92,
|
||||
textShadow: '0 4px 40px rgba(244,190,40,0.25)',
|
||||
}}>
|
||||
{pts.toLocaleString()}
|
||||
</div>
|
||||
|
||||
{/* Redeemable line */}
|
||||
<div className="fp-rise" style={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 10, marginTop: 14, marginBottom: 44,
|
||||
padding: '10px 18px', borderRadius: 12,
|
||||
background: '#fff7e0', border: '1px solid #f4be28',
|
||||
}}>
|
||||
<span style={{ fontSize: 18, fontWeight: 900, color: '#bc7122', fontFamily: 'Sora, sans-serif' }}>₹{pts.toLocaleString()}</span>
|
||||
<span style={{ fontSize: 13, color: '#4f463a', fontFamily: 'Manrope, sans-serif' }}>redeemable value</span>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: 16 }}><span className="fp-kicker fp-rise">Earn · 03</span></div>
|
||||
|
||||
<h2 style={{
|
||||
fontSize: 'clamp(28px, 3.4vw, 48px)', fontWeight: 900, lineHeight: 1.08,
|
||||
letterSpacing: '-0.04em', color: '#171512', margin: '0 0 20px', fontFamily: 'Sora, sans-serif',
|
||||
}}>
|
||||
<span className="fp-mask"><span>Every step.</span></span>
|
||||
<span className="fp-mask"><span style={{ color: '#f4be28' }}>Every game.</span></span>
|
||||
<span className="fp-mask"><span>Every day.</span></span>
|
||||
</h2>
|
||||
|
||||
<p className="fp-rise" style={{
|
||||
fontSize: 15, lineHeight: 1.8, color: '#4f463a', fontFamily: 'Manrope, sans-serif', maxWidth: 380, margin: 0,
|
||||
}}>
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Right — live feed */}
|
||||
<div className="fp-rise" style={{ transitionDelay: '0.15s' }}>
|
||||
{/* Feed header */}
|
||||
<div style={{
|
||||
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
|
||||
marginBottom: 14, paddingBottom: 14, borderBottom: '2px solid #f4be28',
|
||||
}}>
|
||||
<span style={{ fontSize: 12, fontWeight: 700, letterSpacing: '0.14em', textTransform: 'uppercase', color: '#171512', fontFamily: 'Manrope, sans-serif' }}>Activity feed</span>
|
||||
<span style={{ fontSize: 11, color: '#16a34a', fontWeight: 700, fontFamily: 'Manrope, sans-serif', display: 'flex', alignItems: 'center', gap: 5,
|
||||
background: '#f0fdf4', border: '1px solid #16a34a', borderRadius: 100, padding: '4px 10px',
|
||||
}}>
|
||||
<span style={{ width: 5, height: 5, borderRadius: '50%', background: '#16a34a', display: 'block', animation: 'fpPulse 1.5s infinite' }} />
|
||||
LIVE
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Feed rows */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||
{feed.map((tx, i) => (
|
||||
<div key={i} style={{
|
||||
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)'}`,
|
||||
}}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||
<div style={{
|
||||
width: 36, height: 36, borderRadius: 10, flexShrink: 0,
|
||||
background: tx.type === 'earn' ? '#f0fdf4' : '#fef2f2',
|
||||
border: `1.5px solid ${tx.type === 'earn' ? '#16a34a' : '#ef4444'}`,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
fontSize: 15, fontWeight: 900,
|
||||
color: tx.type === 'earn' ? '#16a34a' : '#ef4444',
|
||||
}}>{tx.type === 'earn' ? '↑' : '↓'}</div>
|
||||
<div>
|
||||
<div style={{ fontSize: 13, fontWeight: 700, color: '#171512', fontFamily: 'Manrope, sans-serif' }}>{tx.store}</div>
|
||||
<div style={{ fontSize: 11, color: 'rgba(23,21,18,0.45)', fontFamily: 'Manrope, sans-serif', marginTop: 1 }}>{tx.time}</div>
|
||||
</div>
|
||||
</div>
|
||||
<span style={{
|
||||
fontSize: 15, fontWeight: 900, fontFamily: 'Sora, sans-serif',
|
||||
color: tx.type === 'earn' ? '#16a34a' : '#ef4444', letterSpacing: '-0.02em',
|
||||
background: tx.type === 'earn' ? '#f0fdf4' : '#fef2f2',
|
||||
padding: '4px 10px', borderRadius: 8,
|
||||
}}>
|
||||
{tx.pts > 0 ? '+' : ''}{tx.pts}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Summary strip */}
|
||||
<div style={{
|
||||
marginTop: 16, display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10,
|
||||
}}>
|
||||
{[
|
||||
{ label: 'Earned this month', val: '660 pts', color: '#16a34a', bg: '#f0fdf4', border: '#16a34a' },
|
||||
{ label: 'Redeemable value', val: '₹660', color: '#f4be28', bg: '#fff7e0', border: '#f4be28' },
|
||||
].map((s, i) => (
|
||||
<div key={i} style={{ padding: '14px 16px', borderRadius: 14, background: s.bg, border: `1.5px solid ${s.border}` }}>
|
||||
<div style={{ fontSize: 11, color: 'rgba(23,21,18,0.5)', fontFamily: 'Manrope, sans-serif', marginBottom: 4 }}>{s.label}</div>
|
||||
<div style={{ fontSize: 18, fontWeight: 900, color: s.color, fontFamily: 'Sora, sans-serif' }}>{s.val}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
76
src/components/for-people/ExperienceSection.tsx
Normal file
76
src/components/for-people/ExperienceSection.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
const words = [
|
||||
{ text: 'Effortless.', size: 'clamp(58px, 9vw, 134px)', color: '#ffffff' },
|
||||
{ text: 'Invisible.', size: 'clamp(50px, 7.5vw, 112px)', color: 'rgba(255,255,255,0.35)' },
|
||||
{ text: 'Yours.', size: 'clamp(58px, 9vw, 134px)', color: '#f4be28' },
|
||||
];
|
||||
|
||||
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() {
|
||||
return (
|
||||
<section className="fp-panel" style={{ background: '#07070d' }}>
|
||||
<div className="fp-shell">
|
||||
|
||||
<div style={{ marginBottom: 'clamp(36px, 5vh, 60px)' }}>
|
||||
<span className="fp-kicker fp-rise">The Experience · 06</span>
|
||||
</div>
|
||||
|
||||
<div className="fp-split" style={{ gridTemplateColumns: '1.1fr 0.9fr', alignItems: 'center' }}>
|
||||
|
||||
{/* Giant words */}
|
||||
<div>
|
||||
{words.map((w, i) => (
|
||||
<span key={i} className="fp-mask" style={{ display: 'block' }}>
|
||||
<span style={{
|
||||
display: 'block', fontSize: w.size, fontWeight: 900, fontFamily: 'Sora, sans-serif',
|
||||
color: w.color, letterSpacing: '-0.05em', lineHeight: 0.96,
|
||||
}}>{w.text}</span>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Steps */}
|
||||
<div>
|
||||
<p className="fp-rise" style={{
|
||||
fontSize: 16, lineHeight: 1.85, color: 'rgba(255,255,255,0.45)', fontFamily: 'Manrope, sans-serif', margin: '0 0 44px', maxWidth: 360,
|
||||
}}>
|
||||
No plastic cards. No coupon hunting. No app open required at checkout. Loyaly works quietly in the background while you shop the way you always have.
|
||||
</p>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
{steps.map((s, i) => (
|
||||
<div key={i} className="fp-rise" style={{
|
||||
display: 'flex', gap: 20, alignItems: 'center', padding: '20px 0',
|
||||
borderBottom: i < steps.length - 1 ? '1px solid rgba(255,255,255,0.07)' : 'none',
|
||||
transitionDelay: `${0.2 + i * 0.1}s`,
|
||||
}}>
|
||||
<span style={{ fontSize: 12, fontWeight: 700, color: '#f4be28', fontFamily: 'Sora, sans-serif', letterSpacing: '0.06em', flexShrink: 0, width: 24 }}>{s.n}</span>
|
||||
<span style={{ fontSize: 16, fontWeight: 600, color: 'rgba(255,255,255,0.75)', fontFamily: 'Manrope, sans-serif', lineHeight: 1.4 }}>{s.label}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="fp-rise" style={{ marginTop: 40, display: 'flex', alignItems: 'center', gap: 12, transitionDelay: '0.5s' }}>
|
||||
<div style={{ display: 'flex' }}>
|
||||
{['#f4be28', '#4ade80', '#a855f7', '#3b82f6'].map((c, i) => (
|
||||
<div key={i} style={{ width: 30, height: 30, borderRadius: '50%', background: c, border: '2px solid #07070d', marginLeft: i > 0 ? -9 : 0 }} />
|
||||
))}
|
||||
</div>
|
||||
<span style={{ fontSize: 13, color: 'rgba(255,255,255,0.4)', fontFamily: 'Manrope, sans-serif' }}>
|
||||
Joined by <span style={{ color: '#fff', fontWeight: 700 }}>50,000+</span> shoppers
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
116
src/components/for-people/FinalCTASection.tsx
Normal file
116
src/components/for-people/FinalCTASection.tsx
Normal file
@@ -0,0 +1,116 @@
|
||||
'use client';
|
||||
|
||||
import React, { useRef, useEffect } from 'react';
|
||||
import Link from 'next/link';
|
||||
|
||||
function useMagnetic() {
|
||||
const ref = useRef<HTMLAnchorElement | null>(null);
|
||||
useEffect(() => {
|
||||
const el = ref.current;
|
||||
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)`;
|
||||
};
|
||||
const onLeave = () => { el.style.transform = 'translate(0,0) scale(1)'; };
|
||||
el.addEventListener('mousemove', onMove);
|
||||
el.addEventListener('mouseleave', onLeave);
|
||||
return () => { el.removeEventListener('mousemove', onMove); el.removeEventListener('mouseleave', onLeave); };
|
||||
}, []);
|
||||
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' },
|
||||
];
|
||||
|
||||
export default function FinalCTASection() {
|
||||
const mag1 = useMagnetic();
|
||||
const mag2 = useMagnetic();
|
||||
|
||||
return (
|
||||
<section className="fp-panel fp-light" style={{ background: '#ffffff', textAlign: 'center' }}>
|
||||
|
||||
{/* Vivid background shape */}
|
||||
<div className="fp-deco" style={{
|
||||
position: 'absolute', top: '-10%', left: '50%', transform: 'translateX(-50%)',
|
||||
width: '70%', height: '60%', borderRadius: '50%',
|
||||
background: 'radial-gradient(ellipse, rgba(244,190,40,0.12) 0%, transparent 70%)',
|
||||
zIndex: 0, filter: 'blur(50px)',
|
||||
}} />
|
||||
<div className="fp-deco" style={{
|
||||
position: 'absolute', bottom: 0, left: 0, right: 0, textAlign: 'center', zIndex: 0, overflow: 'hidden',
|
||||
}}>
|
||||
<span style={{
|
||||
fontSize: 'clamp(130px, 24vw, 340px)', fontWeight: 900, fontFamily: 'Sora, sans-serif',
|
||||
color: 'transparent', WebkitTextStroke: '1.5px rgba(244,190,40,0.15)', letterSpacing: '-0.06em', lineHeight: 1, display: 'block',
|
||||
}}>LOYALY</span>
|
||||
</div>
|
||||
|
||||
<div className="fp-shell" style={{ maxWidth: 760 }}>
|
||||
|
||||
{/* Live badge */}
|
||||
<div className="fp-rise" style={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 8, marginBottom: 32,
|
||||
background: '#f0fdf4', border: '1.5px solid #16a34a', borderRadius: 100, padding: '10px 22px',
|
||||
}}>
|
||||
<span style={{ width: 8, height: 8, borderRadius: '50%', background: '#16a34a', display: 'block', animation: 'fpPulse 1.5s infinite', boxShadow: '0 0 0 3px rgba(22,163,74,0.2)' }} />
|
||||
<span style={{ fontSize: 13, fontWeight: 700, color: '#16a34a', fontFamily: 'Manrope, sans-serif', letterSpacing: '0.06em' }}>
|
||||
Now live in Coimbatore
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Headline */}
|
||||
<h2 style={{
|
||||
fontSize: 'clamp(48px, 7.5vw, 112px)', fontWeight: 900, letterSpacing: '-0.06em', lineHeight: 0.92,
|
||||
color: '#171512', margin: '0 0 28px', fontFamily: 'Sora, sans-serif',
|
||||
}}>
|
||||
<span className="fp-mask"><span>Your rewards</span></span>
|
||||
<span className="fp-mask"><span className="fp-shimmer">are waiting.</span></span>
|
||||
</h2>
|
||||
|
||||
<p className="fp-rise" style={{
|
||||
fontSize: 18, lineHeight: 1.7, color: '#4f463a', fontFamily: 'Manrope, sans-serif',
|
||||
maxWidth: 460, margin: '0 auto 48px', transitionDelay: '0.1s',
|
||||
}}>
|
||||
Download Loyaly and start earning on your very next purchase. Free. Instant. Yours.
|
||||
</p>
|
||||
|
||||
{/* CTAs */}
|
||||
<div className="fp-rise" style={{ display: 'flex', gap: 14, justifyContent: 'center', flexWrap: 'wrap', marginBottom: 56, transitionDelay: '0.2s' }}>
|
||||
<Link href="/contacts" ref={mag1} className="fp-btn fp-btn-primary">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.8-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z"/></svg>
|
||||
Download for iOS
|
||||
</Link>
|
||||
<Link href="/contacts" ref={mag2} className="fp-btn" style={{
|
||||
padding: '17px 34px', background: '#171512', color: '#fff',
|
||||
border: 'none', borderRadius: 100,
|
||||
transition: 'all 0.35s cubic-bezier(0.34,1.56,0.64,1)',
|
||||
}}>
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6"><rect x="2" y="8" width="20" height="10" rx="2"/><path d="M7.83 3l1.3 2.35M16.17 3l-1.3 2.35"/></svg>
|
||||
Get on Android
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Proof stats — colored chips */}
|
||||
<div className="fp-rise" style={{
|
||||
display: 'flex', justifyContent: 'center', gap: 12, flexWrap: 'wrap', transitionDelay: '0.3s',
|
||||
}}>
|
||||
{proof.map((p, i) => (
|
||||
<div key={i} style={{
|
||||
padding: '16px 24px', borderRadius: 18, textAlign: 'center',
|
||||
background: `${p.color}0f`, border: `2px solid ${p.color}`,
|
||||
minWidth: 110,
|
||||
}}>
|
||||
<div style={{ fontSize: 'clamp(20px, 2.5vw, 28px)', fontWeight: 900, color: p.color, fontFamily: 'Sora, sans-serif', letterSpacing: '-0.03em' }}>{p.val}</div>
|
||||
<div style={{ fontSize: 11, color: 'rgba(23,21,18,0.55)', fontFamily: 'Manrope, sans-serif', marginTop: 5, letterSpacing: '0.04em' }}>{p.label}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
248
src/components/for-people/HeroSection.tsx
Normal file
248
src/components/for-people/HeroSection.tsx
Normal file
@@ -0,0 +1,248 @@
|
||||
'use client';
|
||||
|
||||
import React, { useRef, useEffect, useCallback } from 'react';
|
||||
import Link from 'next/link';
|
||||
|
||||
function useMagnetic<T extends HTMLElement>(strength = 12) {
|
||||
const ref = useRef<T | null>(null);
|
||||
useEffect(() => {
|
||||
const el = ref.current;
|
||||
if (!el) return;
|
||||
const onMove = (e: MouseEvent) => {
|
||||
const rect = el.getBoundingClientRect();
|
||||
const x = ((e.clientX - rect.left) / rect.width - 0.5) * strength * 2;
|
||||
const y = ((e.clientY - rect.top) / rect.height - 0.5) * strength * 2;
|
||||
el.style.transform = `translate(${x}px, ${y}px)`;
|
||||
};
|
||||
const onLeave = () => { el.style.transform = 'translate(0,0)'; };
|
||||
el.addEventListener('mousemove', onMove);
|
||||
el.addEventListener('mouseleave', onLeave);
|
||||
return () => {
|
||||
el.removeEventListener('mousemove', onMove);
|
||||
el.removeEventListener('mouseleave', onLeave);
|
||||
};
|
||||
}, [strength]);
|
||||
return ref;
|
||||
}
|
||||
|
||||
export default function HeroSection() {
|
||||
const magPrimary = useMagnetic<HTMLAnchorElement>(10);
|
||||
const magSecondary = useMagnetic<HTMLButtonElement>(8);
|
||||
const stageRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Pointer parallax for the phone stage
|
||||
const onStageMove = useCallback((e: React.MouseEvent<HTMLDivElement>) => {
|
||||
const el = stageRef.current; if (!el) return;
|
||||
const r = el.getBoundingClientRect();
|
||||
const px = (e.clientX - r.left) / r.width - 0.5;
|
||||
const py = (e.clientY - r.top) / r.height - 0.5;
|
||||
el.style.setProperty('--rx', `${(-py * 8).toFixed(2)}deg`);
|
||||
el.style.setProperty('--ry', `${(px * 10).toFixed(2)}deg`);
|
||||
el.style.setProperty('--tx', `${(px * 18).toFixed(1)}px`);
|
||||
el.style.setProperty('--ty', `${(py * 18).toFixed(1)}px`);
|
||||
}, []);
|
||||
const onStageLeave = useCallback(() => {
|
||||
const el = stageRef.current; if (!el) return;
|
||||
el.style.setProperty('--rx', '0deg');
|
||||
el.style.setProperty('--ry', '0deg');
|
||||
el.style.setProperty('--tx', '0px');
|
||||
el.style.setProperty('--ty', '0px');
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<section className="fp-panel" style={{ background: 'radial-gradient(ellipse 90% 80% at 78% 42%, #1a1040 0%, #0a0a12 55%, #06060b 100%)' }}>
|
||||
|
||||
{/* Ambient orbs + grid */}
|
||||
<div className="fp-deco" style={{ position: 'absolute', inset: 0, zIndex: 0 }}>
|
||||
<div style={{
|
||||
position: 'absolute', top: '14%', left: '58%', width: 460, height: 460, borderRadius: '50%',
|
||||
background: 'radial-gradient(circle, rgba(244,190,40,0.13) 0%, transparent 70%)', animation: 'fpOrbFloat 7s ease-in-out infinite',
|
||||
}} />
|
||||
<div style={{
|
||||
position: 'absolute', bottom: '8%', right: '6%', width: 280, height: 280, borderRadius: '50%',
|
||||
background: 'radial-gradient(circle, rgba(130,88,200,0.18) 0%, transparent 70%)', animation: 'fpOrb2Float 9s ease-in-out infinite',
|
||||
}} />
|
||||
<div style={{
|
||||
position: 'absolute', inset: 0,
|
||||
backgroundImage: 'linear-gradient(rgba(255,255,255,0.018) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,0.018) 1px, transparent 1px)',
|
||||
backgroundSize: '80px 80px', maskImage: 'radial-gradient(ellipse 70% 70% at 50% 50%, #000 0%, transparent 80%)',
|
||||
WebkitMaskImage: 'radial-gradient(ellipse 70% 70% at 50% 50%, #000 0%, transparent 80%)',
|
||||
}} />
|
||||
</div>
|
||||
|
||||
<div className="fp-shell">
|
||||
<div className="fp-split">
|
||||
|
||||
{/* Text column */}
|
||||
<div>
|
||||
<div className="fp-content fp-eyebrow" style={{ marginBottom: 26 }}>
|
||||
<span className="fp-kicker">For People · By Loyaly</span>
|
||||
</div>
|
||||
|
||||
<h1 className="fp-headline" style={{
|
||||
fontSize: 'clamp(40px, 5.4vw, 82px)', fontWeight: 800, lineHeight: 1.02,
|
||||
letterSpacing: '-0.045em', color: '#fff', margin: '0 0 28px', fontFamily: 'Sora, sans-serif',
|
||||
}}>
|
||||
<span className="fp-mask"><span>Shop Smart.</span></span>
|
||||
<span className="fp-mask"><span className="fp-shimmer">Live Rewarded.</span></span>
|
||||
</h1>
|
||||
|
||||
<p className="fp-content fp-subtext" style={{
|
||||
fontSize: 'clamp(16px, 1.4vw, 19px)', lineHeight: 1.7, fontWeight: 400,
|
||||
color: 'rgba(255,255,255,0.58)', maxWidth: 440, margin: '0 0 40px', fontFamily: 'Manrope, 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.
|
||||
</p>
|
||||
|
||||
<div className="fp-content fp-cta" style={{ display: 'flex', gap: 16, flexWrap: 'wrap', alignItems: 'center' }}>
|
||||
<Link
|
||||
href="/contacts"
|
||||
ref={magPrimary}
|
||||
className="fp-btn fp-btn-primary fp-magnetic"
|
||||
>
|
||||
Download Free ↗
|
||||
</Link>
|
||||
<button
|
||||
ref={magSecondary}
|
||||
className="fp-btn fp-btn-ghost fp-magnetic"
|
||||
>
|
||||
See How It Works
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Trust row */}
|
||||
<div className="fp-content fp-cta" style={{ display: 'flex', alignItems: 'center', gap: 14, marginTop: 40 }}>
|
||||
<div style={{ display: 'flex' }}>
|
||||
{['#f4be28', '#4ade80', '#a855f7', '#3b82f6'].map((c, i) => (
|
||||
<div key={i} style={{ width: 30, height: 30, borderRadius: '50%', background: c, border: '2px solid #0a0a12', marginLeft: i > 0 ? -9 : 0, opacity: 0.9 }} />
|
||||
))}
|
||||
</div>
|
||||
<span style={{ fontSize: 13, color: 'rgba(255,255,255,0.42)', fontFamily: 'Manrope, sans-serif' }}>
|
||||
<strong style={{ color: 'rgba(255,255,255,0.75)', fontWeight: 700 }}>50,000+</strong> active shoppers earning daily
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Visual column — phone */}
|
||||
<div
|
||||
ref={stageRef}
|
||||
onMouseMove={onStageMove}
|
||||
onMouseLeave={onStageLeave}
|
||||
className="fp-deco"
|
||||
style={{
|
||||
position: 'relative', display: 'flex', justifyContent: 'center', alignItems: 'center',
|
||||
perspective: 1200,
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
transformStyle: 'preserve-3d',
|
||||
transform: 'rotateX(var(--rx,0deg)) rotateY(var(--ry,0deg)) translate(var(--tx,0px), var(--ty,0px))',
|
||||
transition: 'transform 0.5s cubic-bezier(0.22,1,0.36,1)',
|
||||
position: 'relative',
|
||||
}}>
|
||||
{/* Glow ring */}
|
||||
<div style={{
|
||||
position: 'absolute', inset: -36, borderRadius: '56px',
|
||||
background: 'radial-gradient(circle at 50% 50%, rgba(244,190,40,0.2) 0%, transparent 70%)',
|
||||
animation: 'fpGlow 3s ease-in-out infinite',
|
||||
}} />
|
||||
{/* Phone shell */}
|
||||
<div style={{
|
||||
width: 268, height: 548, borderRadius: '48px',
|
||||
background: 'linear-gradient(160deg, #22223a 0%, #10101c 100%)',
|
||||
border: '1.5px solid rgba(255,255,255,0.1)',
|
||||
boxShadow: '0 70px 130px rgba(0,0,0,0.8), 0 0 0 1px rgba(255,255,255,0.06), inset 0 1px 0 rgba(255,255,255,0.1)',
|
||||
display: 'flex', flexDirection: 'column', overflow: 'hidden',
|
||||
animation: 'fpOrbFloat 6s ease-in-out infinite', position: 'relative',
|
||||
}}>
|
||||
{/* Shimmer sweep */}
|
||||
<div style={{ position: 'absolute', inset: 0, pointerEvents: 'none', zIndex: 20, overflow: 'hidden', borderRadius: '48px' }}>
|
||||
<div style={{
|
||||
position: 'absolute', top: '-20%', left: 0, width: '40%', height: '140%',
|
||||
background: 'linear-gradient(105deg, transparent 0%, rgba(255,255,255,0.07) 50%, transparent 100%)',
|
||||
animation: 'fpScreenShimmer 4.5s ease-in-out infinite', animationDelay: '1s',
|
||||
}} />
|
||||
</div>
|
||||
{/* Dynamic island */}
|
||||
<div style={{ display: 'flex', justifyContent: 'center', paddingTop: 16, paddingBottom: 4 }}>
|
||||
<div style={{ width: 96, height: 28, borderRadius: 20, background: '#080810', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6 }}>
|
||||
<div style={{ width: 10, height: 10, borderRadius: '50%', background: '#1a1a28', border: '1px solid rgba(255,255,255,0.08)' }} />
|
||||
<div style={{ width: 6, height: 6, borderRadius: '50%', background: '#0d0d18' }} />
|
||||
</div>
|
||||
</div>
|
||||
{/* Header */}
|
||||
<div style={{ padding: '8px 22px 12px' }}>
|
||||
<div style={{ fontSize: 10, color: '#f4be28', fontWeight: 700, fontFamily: 'Manrope, sans-serif', letterSpacing: '0.12em', textTransform: 'uppercase' }}>Good Morning</div>
|
||||
<div style={{ fontSize: 16, color: '#fff', fontWeight: 800, fontFamily: 'Manrope, sans-serif', marginTop: 2 }}>Loyaly ✦</div>
|
||||
</div>
|
||||
{/* Points card */}
|
||||
<div style={{
|
||||
margin: '0 14px', borderRadius: 22, background: 'linear-gradient(135deg, #f4be28 0%, #e09820 100%)',
|
||||
padding: '18px 20px', boxShadow: '0 8px 24px rgba(244,190,40,0.35)', position: 'relative', overflow: 'hidden',
|
||||
}}>
|
||||
<div style={{ position: 'absolute', top: '-30%', right: '-10%', width: 110, height: 110, borderRadius: '50%', background: 'rgba(255,255,255,0.12)' }} />
|
||||
<div style={{ fontSize: 10, color: 'rgba(23,21,18,0.65)', fontWeight: 700, fontFamily: 'Manrope, sans-serif', letterSpacing: '0.1em', textTransform: 'uppercase' }}>Total Points</div>
|
||||
<div style={{ fontSize: 34, color: '#171512', fontWeight: 900, fontFamily: 'Manrope, sans-serif', letterSpacing: '-0.03em', marginTop: 2 }}>284</div>
|
||||
<div style={{ fontSize: 10, color: 'rgba(23,21,18,0.6)', fontFamily: 'Manrope, sans-serif', marginTop: 4 }}>≈ ₹284 redeemable</div>
|
||||
</div>
|
||||
{/* Activity */}
|
||||
<div style={{ padding: '14px 12px', display: 'flex', flexDirection: 'column', gap: 7, flex: 1 }}>
|
||||
{[
|
||||
{ store: 'Starbucks', pts: '+120', color: '#4ade80' },
|
||||
{ store: 'Nike Store', pts: '+85', color: '#4ade80' },
|
||||
{ store: 'H&M', pts: '−50', color: '#f87171' },
|
||||
].map((item, i) => (
|
||||
<div key={i} style={{
|
||||
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
|
||||
background: 'rgba(255,255,255,0.04)', borderRadius: 12, padding: '10px 13px', border: '1px solid rgba(255,255,255,0.06)',
|
||||
}}>
|
||||
<span style={{ fontSize: 12, color: 'rgba(255,255,255,0.78)', fontFamily: 'Manrope, sans-serif' }}>{item.store}</span>
|
||||
<span style={{ fontSize: 12, color: item.color, fontWeight: 700, fontFamily: 'Manrope, sans-serif' }}>{item.pts}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{/* Nav dots */}
|
||||
<div style={{ display: 'flex', justifyContent: 'center', gap: 6, padding: '10px 0 20px' }}>
|
||||
{[1, 0, 0, 0].map((a, i) => (
|
||||
<div key={i} style={{ width: a ? 22 : 6, height: 4, borderRadius: 2, background: a ? '#f4be28' : 'rgba(255,255,255,0.2)' }} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Floating mini chips */}
|
||||
<div className="fp-glass" style={{
|
||||
position: 'absolute', top: '12%', left: '-12%', borderRadius: 16, padding: '12px 16px',
|
||||
display: 'flex', alignItems: 'center', gap: 10, animation: 'fpOrb2Float 7s ease-in-out infinite',
|
||||
}}>
|
||||
<div style={{ width: 30, height: 30, borderRadius: 9, background: 'rgba(74,222,128,0.16)', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#4ade80', fontSize: 14, fontWeight: 800 }}>↑</div>
|
||||
<div>
|
||||
<div style={{ fontSize: 11, fontWeight: 700, color: '#fff', fontFamily: 'Manrope, sans-serif' }}>+120 pts</div>
|
||||
<div style={{ fontSize: 9, color: 'rgba(255,255,255,0.4)', fontFamily: 'Manrope, sans-serif' }}>just now</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="fp-glass" style={{
|
||||
position: 'absolute', bottom: '14%', right: '-14%', borderRadius: 16, padding: '12px 16px',
|
||||
animation: 'fpOrbFloat 8s ease-in-out infinite',
|
||||
}}>
|
||||
<div style={{ fontSize: 9, color: 'rgba(255,255,255,0.45)', fontFamily: 'Manrope, sans-serif', letterSpacing: '0.1em', textTransform: 'uppercase', marginBottom: 3 }}>Redeemable</div>
|
||||
<div style={{ fontSize: 17, fontWeight: 900, color: '#f4be28', fontFamily: 'Sora, sans-serif', letterSpacing: '-0.02em' }}>₹284.00</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Scroll hint */}
|
||||
<div style={{
|
||||
position: 'absolute', bottom: 38, left: '50%', transform: 'translateX(-50%)',
|
||||
display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 8, zIndex: 10,
|
||||
color: 'rgba(255,255,255,0.3)', fontSize: 10, fontFamily: 'Manrope, sans-serif', letterSpacing: '0.16em', textTransform: 'uppercase',
|
||||
}}>
|
||||
<span>Scroll</span>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" style={{ animation: 'fpScrollHint 1.6s ease-in-out infinite' }}>
|
||||
<polyline points="6 9 12 15 18 9" />
|
||||
</svg>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
104
src/components/for-people/SpendAnywhereSection.tsx
Normal file
104
src/components/for-people/SpendAnywhereSection.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
|
||||
const GroceryIcon = () => <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M6 2L3 6v14a2 2 0 002 2h14a2 2 0 002-2V6l-3-4z"/><line x1="3" y1="6" x2="21" y2="6"/><path d="M16 10a4 4 0 01-8 0"/></svg>;
|
||||
const FashionIcon = () => <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M20.38 3.46L16 2a4 4 0 01-8 0L3.62 3.46a2 2 0 00-1.34 2.23l.58 3.57a1 1 0 00.99.84H6v10c0 1.1.9 2 2 2h8a2 2 0 002-2V10h2.15a1 1 0 00.99-.84l.58-3.57a2 2 0 00-1.34-2.23z"/></svg>;
|
||||
const CafeIcon = () => <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M18 8h1a4 4 0 010 8h-1"/><path d="M2 8h16v9a4 4 0 01-4 4H6a4 4 0 01-4-4V8z"/><line x1="6" y1="1" x2="6" y2="4"/><line x1="10" y1="1" x2="10" y2="4"/><line x1="14" y1="1" x2="14" y2="4"/></svg>;
|
||||
const SportsIcon = () => <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 8l4 4-4 4-4-4 4-4z"/></svg>;
|
||||
const ElectronicsIcon = () => <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/></svg>;
|
||||
const BeautyIcon = () => <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>;
|
||||
|
||||
const brands = [
|
||||
{ name: 'Groceries', Icon: GroceryIcon, stores: ['BigBasket', 'DMart', 'Reliance Fresh'], 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' },
|
||||
];
|
||||
|
||||
export default function SpendAnywhereSection() {
|
||||
const [active, setActive] = useState<number | null>(null);
|
||||
|
||||
return (
|
||||
<section className="fp-panel" style={{ background: '#080810' }}>
|
||||
<div className="fp-shell">
|
||||
|
||||
{/* Header row */}
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-end', flexWrap: 'wrap', gap: 24, marginBottom: 'clamp(40px, 6vh, 64px)' }}>
|
||||
<div>
|
||||
<div style={{ marginBottom: 22 }}><span className="fp-kicker fp-rise">Spend · 04</span></div>
|
||||
<h2 style={{
|
||||
fontSize: 'clamp(46px, 7vw, 110px)', fontWeight: 900, letterSpacing: '-0.06em', lineHeight: 0.9,
|
||||
color: '#fff', margin: 0, fontFamily: 'Sora, sans-serif',
|
||||
}}>
|
||||
<span className="fp-mask"><span>Spend it</span></span>
|
||||
<span className="fp-mask"><span className="fp-shimmer">anywhere.</span></span>
|
||||
</h2>
|
||||
</div>
|
||||
<p className="fp-rise" style={{
|
||||
fontSize: 15, lineHeight: 1.75, color: 'rgba(255,255,255,0.45)', fontFamily: 'Manrope, sans-serif', margin: 0, maxWidth: 320,
|
||||
}}>
|
||||
One wallet. Every store. Your Loyaly points are universal — no restrictions, no expiry, no fine print.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Category grid */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(260px, 1fr))', gap: 14 }}>
|
||||
{brands.map((brand, i) => (
|
||||
<div
|
||||
key={i}
|
||||
onMouseEnter={() => setActive(i)}
|
||||
onMouseLeave={() => setActive(null)}
|
||||
className="fp-rise"
|
||||
style={{
|
||||
padding: '22px 24px', borderRadius: 18,
|
||||
background: active === i ? `${brand.color}10` : 'rgba(255,255,255,0.03)',
|
||||
border: `1px solid ${active === i ? `${brand.color}40` : 'rgba(255,255,255,0.07)'}`,
|
||||
backdropFilter: 'blur(12px)', cursor: 'default',
|
||||
transitionDelay: `${0.08 + i * 0.06}s`,
|
||||
transform: active === i ? 'translateY(-5px)' : 'translateY(0)',
|
||||
boxShadow: active === i ? `0 22px 60px ${brand.color}1f, inset 0 1px 0 rgba(255,255,255,0.08)` : 'none',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 11, marginBottom: 14 }}>
|
||||
{(() => { const BrandIcon = brand.Icon; return <span style={{ color: active === i ? brand.color : 'rgba(255,255,255,0.45)', transition: 'color 0.3s' }}><BrandIcon /></span>; })()}
|
||||
<span style={{ fontSize: 14, fontWeight: 700, color: active === i ? '#fff' : 'rgba(255,255,255,0.75)', fontFamily: 'Manrope, sans-serif', transition: 'color 0.2s' }}>{brand.name}</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
|
||||
{brand.stores.map((s, j) => (
|
||||
<span key={j} style={{
|
||||
fontSize: 11, fontWeight: 600, fontFamily: 'Manrope, sans-serif',
|
||||
color: active === i ? brand.color : 'rgba(255,255,255,0.3)',
|
||||
background: active === i ? `${brand.color}18` : 'rgba(255,255,255,0.04)',
|
||||
border: `1px solid ${active === i ? `${brand.color}30` : 'rgba(255,255,255,0.06)'}`,
|
||||
borderRadius: 100, padding: '4px 10px', transition: 'all 0.25s ease',
|
||||
}}>{s}</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Stat strip */}
|
||||
<div className="fp-rise" style={{
|
||||
display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 24,
|
||||
marginTop: 'clamp(40px, 6vh, 64px)', paddingTop: 32, borderTop: '1px solid rgba(255,255,255,0.07)', transitionDelay: '0.45s',
|
||||
}}>
|
||||
{[
|
||||
{ val: '10,000+', lbl: 'Partner stores' },
|
||||
{ val: 'Zero', lbl: 'Expiry on points' },
|
||||
{ val: 'Universal', lbl: 'One wallet, all brands' },
|
||||
{ val: 'Instant', lbl: 'Redeem at checkout' },
|
||||
].map((s, i) => (
|
||||
<div key={i}>
|
||||
<div style={{ fontSize: 'clamp(26px, 3vw, 42px)', fontWeight: 900, fontFamily: 'Sora, sans-serif', color: '#f4be28', letterSpacing: '-0.04em', lineHeight: 1 }}>{s.val}</div>
|
||||
<div style={{ fontSize: 12, color: 'rgba(255,255,255,0.35)', fontFamily: 'Manrope, sans-serif', marginTop: 8, letterSpacing: '0.06em' }}>{s.lbl}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
363
src/components/for-people/VerticalScrollWrapper.tsx
Normal file
363
src/components/for-people/VerticalScrollWrapper.tsx
Normal file
@@ -0,0 +1,363 @@
|
||||
'use client';
|
||||
|
||||
import React, { useEffect, useRef, useState, useCallback } from 'react';
|
||||
import { gsap } from 'gsap';
|
||||
import { ScrollTrigger } from 'gsap/ScrollTrigger';
|
||||
import Lenis from 'lenis';
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
gsap.registerPlugin(ScrollTrigger);
|
||||
}
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
sections?: string[];
|
||||
}
|
||||
|
||||
export default function VerticalScrollWrapper({ children, sections = [] }: Props) {
|
||||
const rootRef = useRef<HTMLDivElement>(null);
|
||||
const cursorRef = useRef<HTMLDivElement>(null);
|
||||
const lenisRef = useRef<Lenis | null>(null);
|
||||
const [active, setActive] = useState(0);
|
||||
const [progress, setProgress] = useState(0);
|
||||
|
||||
/* ── Lenis smooth scroll wired into GSAP's ticker ── */
|
||||
useEffect(() => {
|
||||
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
if (reduce) return;
|
||||
|
||||
const lenis = new Lenis({
|
||||
duration: 1.15,
|
||||
easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t)),
|
||||
smoothWheel: true,
|
||||
});
|
||||
lenisRef.current = lenis;
|
||||
lenis.on('scroll', ScrollTrigger.update);
|
||||
const ticker = (time: number) => lenis.raf(time * 1000);
|
||||
gsap.ticker.add(ticker);
|
||||
gsap.ticker.lagSmoothing(0);
|
||||
|
||||
return () => {
|
||||
lenis.destroy();
|
||||
lenisRef.current = null;
|
||||
gsap.ticker.remove(ticker);
|
||||
};
|
||||
}, []);
|
||||
|
||||
/* ── Cursor glow follows the pointer (desktop only) ── */
|
||||
useEffect(() => {
|
||||
const cursor = cursorRef.current;
|
||||
if (!cursor) return;
|
||||
if (window.matchMedia('(pointer: coarse)').matches) return;
|
||||
let raf = 0;
|
||||
const onMove = (e: MouseEvent) => {
|
||||
cancelAnimationFrame(raf);
|
||||
raf = requestAnimationFrame(() => {
|
||||
cursor.style.transform = `translate3d(${e.clientX - 250}px, ${e.clientY - 250}px, 0)`;
|
||||
});
|
||||
};
|
||||
window.addEventListener('mousemove', onMove, { passive: true });
|
||||
return () => { window.removeEventListener('mousemove', onMove); cancelAnimationFrame(raf); };
|
||||
}, []);
|
||||
|
||||
/* ── One-way reveal + active-section + progress tracking ── */
|
||||
useEffect(() => {
|
||||
if (!rootRef.current) return;
|
||||
const panels = Array.from(rootRef.current.querySelectorAll<HTMLElement>('.fp-panel'));
|
||||
|
||||
const revealObs = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((e) => {
|
||||
if (e.isIntersecting && !(e.target as HTMLElement).hasAttribute('data-active')) {
|
||||
(e.target as HTMLElement).setAttribute('data-active', 'true');
|
||||
}
|
||||
});
|
||||
},
|
||||
{ threshold: 0.22 }
|
||||
);
|
||||
panels.forEach((p) => revealObs.observe(p));
|
||||
|
||||
let raf = 0;
|
||||
const onScroll = () => {
|
||||
cancelAnimationFrame(raf);
|
||||
raf = requestAnimationFrame(() => {
|
||||
const mid = window.innerHeight / 2;
|
||||
let best = 0;
|
||||
let bestDist = Infinity;
|
||||
panels.forEach((p, i) => {
|
||||
const r = p.getBoundingClientRect();
|
||||
const center = r.top + r.height / 2;
|
||||
const d = Math.abs(center - mid);
|
||||
if (d < bestDist) { bestDist = d; best = i; }
|
||||
});
|
||||
setActive(best);
|
||||
const doc = document.documentElement;
|
||||
const max = doc.scrollHeight - doc.clientHeight || 1;
|
||||
setProgress(Math.min(1, Math.max(0, doc.scrollTop / max)));
|
||||
});
|
||||
};
|
||||
onScroll();
|
||||
window.addEventListener('scroll', onScroll, { passive: true });
|
||||
window.addEventListener('resize', onScroll);
|
||||
return () => {
|
||||
revealObs.disconnect();
|
||||
window.removeEventListener('scroll', onScroll);
|
||||
window.removeEventListener('resize', onScroll);
|
||||
cancelAnimationFrame(raf);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const goTo = useCallback((i: number) => {
|
||||
const panels = rootRef.current?.querySelectorAll<HTMLElement>('.fp-panel');
|
||||
const target = panels?.[i];
|
||||
if (!target) return;
|
||||
if (lenisRef.current) {
|
||||
lenisRef.current.scrollTo(target, { offset: 0, duration: 1.2 });
|
||||
} else {
|
||||
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div ref={rootRef} className="fp-root">
|
||||
{/* Film grain — fixed over the whole experience */}
|
||||
<div className="fp-grain" aria-hidden />
|
||||
|
||||
{/* Cursor glow */}
|
||||
<div ref={cursorRef} className="fp-cursor" aria-hidden />
|
||||
|
||||
{/* Stacked sections */}
|
||||
{children}
|
||||
|
||||
{/* Side section rail (desktop) */}
|
||||
{sections.length > 0 && (
|
||||
<nav className="fp-rail" aria-label="Sections">
|
||||
{sections.map((label, i) => (
|
||||
<button
|
||||
key={label}
|
||||
className={`fp-rail-item${i === active ? ' is-active' : ''}`}
|
||||
onClick={() => goTo(i)}
|
||||
>
|
||||
<span className="fp-rail-label">
|
||||
<span className="fp-rail-idx">{String(i + 1).padStart(2, '0')}</span>
|
||||
{label}
|
||||
</span>
|
||||
<span className="fp-rail-dot" />
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
)}
|
||||
|
||||
{/* Scroll progress (bottom) */}
|
||||
<div className="fp-progress" aria-hidden>
|
||||
<div className="fp-progress-bar" style={{ transform: `scaleX(${progress})` }} />
|
||||
</div>
|
||||
|
||||
<style>{`
|
||||
.fp-root {
|
||||
--gold: #f4be28;
|
||||
--gold-soft: #ffd95a;
|
||||
--ink: #06060b;
|
||||
--cream: #fff8e8;
|
||||
--carbon: #171512;
|
||||
--warm: #4f463a;
|
||||
--violet: #8258c8;
|
||||
position: relative;
|
||||
background: #ffffff;
|
||||
color: #fff;
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
/* ── Film grain ── */
|
||||
.fp-grain {
|
||||
position: fixed; inset: 0; z-index: 9; pointer-events: none; opacity: 0.04;
|
||||
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
|
||||
mix-blend-mode: overlay;
|
||||
}
|
||||
|
||||
/* ── Cursor glow ── */
|
||||
.fp-cursor {
|
||||
position: fixed; top: 0; left: 0; width: 500px; height: 500px; z-index: 8;
|
||||
border-radius: 50%; pointer-events: none; will-change: transform;
|
||||
background: radial-gradient(circle, rgba(244,190,40,0.06) 0%, transparent 60%);
|
||||
}
|
||||
|
||||
/* ── Panels (framed as rounded cards, like the rest of the site) ── */
|
||||
.fp-panel {
|
||||
position: relative; width: calc(100% - 40px); min-height: calc(100vh - 40px);
|
||||
display: flex; align-items: center; overflow: hidden;
|
||||
padding: clamp(80px, 10vh, 130px) 0;
|
||||
margin: 0 20px 20px;
|
||||
border-radius: 28px;
|
||||
box-sizing: border-box;
|
||||
box-shadow: none;
|
||||
}
|
||||
.fp-panel.fp-light {
|
||||
box-shadow: none;
|
||||
}
|
||||
.fp-shell {
|
||||
position: relative; z-index: 10; width: 100%;
|
||||
max-width: 1320px; margin: 0 auto;
|
||||
padding: 0 clamp(24px, 6vw, 110px);
|
||||
}
|
||||
.fp-deco { pointer-events: none; user-select: none; }
|
||||
|
||||
/* ── Editorial number / kicker ── */
|
||||
.fp-index {
|
||||
font-family: 'Sora', sans-serif; font-weight: 700;
|
||||
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: 'Manrope', sans-serif; font-weight: 700;
|
||||
font-size: 11px; letter-spacing: 0.22em; text-transform: uppercase; color: var(--gold);
|
||||
}
|
||||
.fp-kicker::before {
|
||||
content: ''; width: 30px; height: 1px; background: var(--gold); display: block;
|
||||
}
|
||||
|
||||
/* ── Shared buttons ── */
|
||||
.fp-btn {
|
||||
display: inline-flex; align-items: center; gap: 10px;
|
||||
font-family: 'Manrope', sans-serif; font-weight: 700; font-size: 15px;
|
||||
border-radius: 100px; text-decoration: none; cursor: pointer;
|
||||
letter-spacing: -0.01em; white-space: nowrap;
|
||||
transition: transform 0.35s cubic-bezier(0.34,1.56,0.64,1), background 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
.fp-btn-primary {
|
||||
padding: 17px 34px; background: var(--gold); color: #171512; border: none;
|
||||
box-shadow: 0 14px 44px rgba(244,190,40,0.32);
|
||||
}
|
||||
.fp-btn-primary:hover { background: var(--gold-soft); box-shadow: 0 18px 60px rgba(244,190,40,0.5); }
|
||||
.fp-btn-ghost {
|
||||
padding: 17px 34px; background: rgba(255,255,255,0.05); color: #fff;
|
||||
border: 1px solid rgba(255,255,255,0.14); backdrop-filter: blur(12px);
|
||||
}
|
||||
.fp-btn-ghost:hover { background: rgba(255,255,255,0.1); border-color: rgba(255,255,255,0.28); }
|
||||
.fp-magnetic { transition: transform 0.3s cubic-bezier(0.34,1.56,0.64,1), box-shadow 0.3s ease !important; }
|
||||
|
||||
/* ── Headline line-mask reveal (signature) ── */
|
||||
.fp-mask { display: block; overflow: hidden; padding-bottom: 0.04em; }
|
||||
.fp-mask > span {
|
||||
display: block; transform: translateY(108%);
|
||||
transition: transform 0.95s cubic-bezier(0.16,1,0.3,1);
|
||||
}
|
||||
.fp-panel[data-active='true'] .fp-mask > span { transform: translateY(0); }
|
||||
.fp-panel[data-active='true'] .fp-mask:nth-child(1) > span { transition-delay: 0.05s; }
|
||||
.fp-panel[data-active='true'] .fp-mask:nth-child(2) > span { transition-delay: 0.15s; }
|
||||
.fp-panel[data-active='true'] .fp-mask:nth-child(3) > span { transition-delay: 0.25s; }
|
||||
.fp-panel[data-active='true'] .fp-mask:nth-child(4) > span { transition-delay: 0.35s; }
|
||||
|
||||
/* ── Soft block reveal for supporting content ── */
|
||||
.fp-content {
|
||||
opacity: 0; transform: translateY(34px);
|
||||
transition: opacity 0.8s cubic-bezier(0.22,1,0.36,1), transform 0.8s cubic-bezier(0.22,1,0.36,1);
|
||||
}
|
||||
.fp-content .fp-eyebrow { transition-delay: 0s; }
|
||||
.fp-content .fp-headline { transition-delay: 0.08s; }
|
||||
.fp-content .fp-subtext { transition-delay: 0.16s; }
|
||||
.fp-content .fp-cta { transition-delay: 0.24s; }
|
||||
.fp-panel[data-active='true'] .fp-content { opacity: 1; transform: translateY(0); }
|
||||
|
||||
/* Generic per-element stagger helper */
|
||||
.fp-rise { opacity: 0; transform: translateY(28px); transition: opacity 0.8s cubic-bezier(0.22,1,0.36,1), transform 0.8s cubic-bezier(0.22,1,0.36,1); }
|
||||
.fp-panel[data-active='true'] .fp-rise { opacity: 1; transform: translateY(0); }
|
||||
|
||||
/* ── Gradient shimmer text ── */
|
||||
.fp-shimmer {
|
||||
background: linear-gradient(105deg, #f4be28 0%, #f4be28 38%, #fff8e0 50%, #f4be28 62%, #ff9500 100%);
|
||||
background-size: 200% auto;
|
||||
-webkit-background-clip: text; background-clip: text;
|
||||
-webkit-text-fill-color: transparent; color: transparent;
|
||||
animation: fpShimmer 4s linear infinite;
|
||||
}
|
||||
@keyframes fpShimmer { 0% { background-position: 200% center; } 100% { background-position: -200% center; } }
|
||||
|
||||
/* ── Glass card ── */
|
||||
.fp-glass {
|
||||
background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.1);
|
||||
backdrop-filter: blur(20px) saturate(160%); -webkit-backdrop-filter: blur(20px) saturate(160%);
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,0.4), inset 0 1px 0 rgba(255,255,255,0.06);
|
||||
}
|
||||
|
||||
/* ── Side rail (dot sits in the gutter; label is a floating pill) ── */
|
||||
.fp-rail {
|
||||
position: fixed; right: 13px; top: 50%; transform: translateY(-50%);
|
||||
z-index: 40; display: flex; flex-direction: column; gap: 6px;
|
||||
}
|
||||
.fp-rail-item {
|
||||
display: flex; align-items: center; justify-content: flex-end; gap: 10px;
|
||||
background: none; border: none; cursor: pointer; padding: 6px 0; color: inherit;
|
||||
}
|
||||
.fp-rail-dot {
|
||||
width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0;
|
||||
background: rgba(150,150,160,0.55); transition: all 0.35s cubic-bezier(0.34,1.56,0.64,1);
|
||||
}
|
||||
.fp-rail-item.is-active .fp-rail-dot { background: var(--gold); box-shadow: 0 0 12px rgba(244,190,40,0.8); transform: scale(1.3); }
|
||||
.fp-rail-label {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
font-family: 'Manrope', sans-serif; font-size: 11px; font-weight: 600;
|
||||
letter-spacing: 0.04em; color: rgba(255,255,255,0.8); white-space: nowrap;
|
||||
padding: 5px 12px; border-radius: 100px;
|
||||
background: rgba(12,12,18,0.72); border: 1px solid rgba(255,255,255,0.08);
|
||||
backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
|
||||
opacity: 0; transform: translateX(8px); pointer-events: none;
|
||||
transition: opacity 0.3s ease, transform 0.3s ease;
|
||||
}
|
||||
.fp-rail-idx { font-family: 'Sora', sans-serif; font-size: 9px; color: var(--gold); }
|
||||
.fp-rail-item:hover .fp-rail-label,
|
||||
.fp-rail-item.is-active .fp-rail-label { opacity: 1; transform: translateX(0); }
|
||||
|
||||
/* ── Scroll progress ── */
|
||||
.fp-progress {
|
||||
position: fixed; left: 0; right: 0; bottom: 0; height: 2px; z-index: 40;
|
||||
background: rgba(255,255,255,0.06);
|
||||
}
|
||||
.fp-progress-bar {
|
||||
height: 100%; width: 100%; transform-origin: left center; transform: scaleX(0);
|
||||
background: linear-gradient(90deg, var(--gold), #ff9500);
|
||||
}
|
||||
|
||||
/* ── Shared decorative keyframes (used by sections) ── */
|
||||
@keyframes fpOrbFloat { 0%,100% { transform: translateY(0) scale(1); } 50% { transform: translateY(-22px) scale(1.04); } }
|
||||
@keyframes fpOrb2Float { 0%,100% { transform: translateY(0) scale(1); } 50% { transform: translateY(18px) scale(0.97); } }
|
||||
@keyframes fpPulse { 0%,100% { opacity: 0.4; transform: scale(1); } 50% { opacity: 0.85; transform: scale(1.15); } }
|
||||
@keyframes fpGlow { 0%,100% { box-shadow: 0 0 30px rgba(244,190,40,0.2); } 50% { box-shadow: 0 0 60px rgba(244,190,40,0.5), 0 0 120px rgba(244,190,40,0.2); } }
|
||||
@keyframes fpScreenShimmer {
|
||||
0% { transform: translateX(-150%) rotate(20deg); opacity: 0; }
|
||||
30% { opacity: 0.6; } 70% { opacity: 0.6; }
|
||||
100% { transform: translateX(350%) rotate(20deg); opacity: 0; }
|
||||
}
|
||||
@keyframes fpScrollHint { 0%,100% { transform: translateY(0); opacity: 0.4; } 50% { transform: translateY(7px); opacity: 1; } }
|
||||
@keyframes fpSpinSlow { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
|
||||
|
||||
/* ── Shared layout helpers ── */
|
||||
.fp-split {
|
||||
display: grid; grid-template-columns: 1.05fr 0.95fr;
|
||||
gap: clamp(40px, 6vw, 96px); align-items: center;
|
||||
}
|
||||
|
||||
/* ── Mobile ── */
|
||||
@media (max-width: 900px) {
|
||||
.fp-rail { display: none; }
|
||||
.fp-cursor { display: none; }
|
||||
.fp-panel {
|
||||
padding: 96px 0 80px; min-height: auto;
|
||||
width: calc(100% - 24px); margin: 0 12px 12px;
|
||||
border-radius: 22px;
|
||||
}
|
||||
/* Force single column even when a section sets grid-template-columns inline */
|
||||
.fp-split { grid-template-columns: 1fr !important; gap: 48px !important; }
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
.fp-shell { padding: 0 22px; }
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.fp-mask > span, .fp-content, .fp-rise { transition: none !important; transform: none !important; opacity: 1 !important; }
|
||||
.fp-shimmer { animation: none !important; }
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
143
src/components/for-people/WhyLoyalySection.tsx
Normal file
143
src/components/for-people/WhyLoyalySection.tsx
Normal file
@@ -0,0 +1,143 @@
|
||||
'use client';
|
||||
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
|
||||
export default function WhyLoyalySection() {
|
||||
const [active, setActive] = useState(false);
|
||||
const [count, setCount] = useState(0);
|
||||
const panelRef = useRef<HTMLDivElement>(null);
|
||||
const rafRef = useRef<number>(0);
|
||||
|
||||
useEffect(() => {
|
||||
const el = panelRef.current;
|
||||
if (!el) return;
|
||||
const obs = new MutationObserver(() => {
|
||||
const isActive = el.hasAttribute('data-active');
|
||||
setActive(isActive);
|
||||
if (isActive) {
|
||||
const start = performance.now();
|
||||
const dur = 2000;
|
||||
const tick = (now: number) => {
|
||||
const t = Math.min((now - start) / dur, 1);
|
||||
setCount(Math.round((1 - Math.pow(1 - t, 4)) * 21000000));
|
||||
if (t < 1) rafRef.current = requestAnimationFrame(tick);
|
||||
};
|
||||
rafRef.current = requestAnimationFrame(tick);
|
||||
}
|
||||
});
|
||||
obs.observe(el, { attributes: true, attributeFilter: ['data-active'] });
|
||||
return () => { obs.disconnect(); cancelAnimationFrame(rafRef.current); };
|
||||
}, []);
|
||||
|
||||
const bars = [
|
||||
{ label: 'Online retail loyalty', pct: 78, color: '#171512' },
|
||||
{ label: 'Offline retail loyalty', pct: 2, color: '#ef4444' },
|
||||
{ label: 'Loyaly target', pct: 60, color: '#f4be28' },
|
||||
];
|
||||
|
||||
return (
|
||||
<section ref={panelRef} className="fp-panel fp-light" style={{ background: '#ffffff' }}>
|
||||
|
||||
{/* Faint grid */}
|
||||
<div className="fp-deco" style={{
|
||||
position: 'absolute', inset: 0, zIndex: 0,
|
||||
backgroundImage: 'linear-gradient(rgba(23,21,18,0.03) 1px, transparent 1px), linear-gradient(90deg, rgba(23,21,18,0.03) 1px, transparent 1px)',
|
||||
backgroundSize: '72px 72px',
|
||||
}} />
|
||||
|
||||
<div className="fp-shell">
|
||||
<div style={{ marginBottom: 'clamp(40px, 6vh, 64px)' }}>
|
||||
<span className="fp-kicker fp-rise">The Gap · 01</span>
|
||||
</div>
|
||||
|
||||
<div className="fp-split" style={{ gridTemplateColumns: '1fr 1fr' }}>
|
||||
|
||||
{/* Giant stat */}
|
||||
<div>
|
||||
{/* "0%" as two-color giant */}
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'flex-start', gap: 0,
|
||||
opacity: active ? 1 : 0, transform: active ? 'translateY(0)' : 'translateY(50px)',
|
||||
transition: 'opacity 0.95s cubic-bezier(0.16,1,0.3,1), transform 0.95s cubic-bezier(0.16,1,0.3,1)',
|
||||
}}>
|
||||
<span style={{ fontSize: 'clamp(120px, 18vw, 260px)', fontWeight: 900, fontFamily: 'Sora, sans-serif', color: '#f4be28', letterSpacing: '-0.06em', lineHeight: 0.85 }}>0</span>
|
||||
<span style={{ fontSize: 'clamp(60px, 9vw, 130px)', fontWeight: 900, fontFamily: 'Sora, sans-serif', color: '#171512', letterSpacing: '-0.04em', lineHeight: 0.85, paddingTop: '0.12em' }}>%</span>
|
||||
</div>
|
||||
|
||||
<div className="fp-rise" style={{ marginTop: 20, marginBottom: 32 }}>
|
||||
<div style={{
|
||||
display: 'inline-block', padding: '8px 16px', borderRadius: 10,
|
||||
background: '#ef4444', color: '#fff',
|
||||
fontSize: 12, fontWeight: 700, fontFamily: 'Manrope, sans-serif', letterSpacing: '0.08em', textTransform: 'uppercase',
|
||||
}}>Loyalty penetration in offline retail</div>
|
||||
</div>
|
||||
|
||||
{/* Three metric chips */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||
{[
|
||||
{ val: '₹2.1L Cr', label: 'Offline retail market size', accent: '#171512' },
|
||||
{ val: '<2%', label: 'Currently getting rewards', accent: '#ef4444' },
|
||||
{ val: '10Cr+', label: 'Shoppers Loyaly can reach', accent: '#16a34a' },
|
||||
].map((m, i) => (
|
||||
<div key={i} className="fp-rise" style={{
|
||||
display: 'flex', alignItems: 'center', gap: 16, padding: '14px 18px',
|
||||
borderRadius: 14, background: '#f5f5f5', border: `1.5px solid ${m.accent}22`,
|
||||
transitionDelay: `${0.1 + i * 0.08}s`,
|
||||
}}>
|
||||
<div style={{ fontSize: 20, fontWeight: 900, fontFamily: 'Sora, sans-serif', color: m.accent, letterSpacing: '-0.03em', flexShrink: 0, minWidth: 72 }}>{m.val}</div>
|
||||
<div style={{ fontSize: 12, color: 'rgba(23,21,18,0.6)', fontFamily: 'Manrope, sans-serif', lineHeight: 1.4 }}>{m.label}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div>
|
||||
{/* Live counter badge */}
|
||||
<div className="fp-rise" style={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 10, marginBottom: 28,
|
||||
background: '#fff7e0', border: '1.5px solid #f4be28', borderRadius: 100, padding: '10px 20px',
|
||||
}}>
|
||||
<span style={{ width: 8, height: 8, borderRadius: '50%', background: '#16a34a', display: 'block', flexShrink: 0, boxShadow: '0 0 0 3px rgba(22,163,74,0.2)', animation: 'fpPulse 1.5s ease-in-out infinite' }} />
|
||||
<span style={{ fontSize: 13, fontWeight: 700, color: '#171512', fontFamily: 'Manrope, sans-serif' }}>
|
||||
<span style={{ color: '#f4be28' }}>{count.toLocaleString()}</span> txns today · unrewarded
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<h2 style={{
|
||||
fontSize: 'clamp(30px, 3.6vw, 50px)', fontWeight: 900, lineHeight: 1.08,
|
||||
letterSpacing: '-0.04em', color: '#171512', margin: '0 0 20px', fontFamily: 'Sora, sans-serif',
|
||||
}}>
|
||||
<span className="fp-mask"><span>Millions shop offline.</span></span>
|
||||
<span className="fp-mask"><span style={{ color: '#ef4444' }}>Nobody rewards them.</span></span>
|
||||
</h2>
|
||||
|
||||
<p className="fp-rise" style={{
|
||||
fontSize: 16, lineHeight: 1.8, color: '#4f463a', margin: '0 0 36px',
|
||||
fontFamily: 'Manrope, 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.
|
||||
</p>
|
||||
|
||||
{/* Data bars */}
|
||||
{bars.map((b, i) => (
|
||||
<div key={i} style={{ marginBottom: 16 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 7 }}>
|
||||
<span style={{ fontSize: 13, fontWeight: 600, color: '#171512', fontFamily: 'Manrope, sans-serif' }}>{b.label}</span>
|
||||
<span style={{ fontSize: 14, fontWeight: 800, color: b.color, fontFamily: 'Sora, sans-serif' }}>{b.pct}%</span>
|
||||
</div>
|
||||
<div style={{ height: 8, background: 'rgba(23,21,18,0.07)', borderRadius: 100, overflow: 'hidden' }}>
|
||||
<div style={{
|
||||
height: '100%', borderRadius: 100, background: b.color,
|
||||
width: active ? `${b.pct}%` : '0%',
|
||||
transition: `width ${0.9 + i * 0.2}s cubic-bezier(0.22,1,0.36,1) ${0.4 + i * 0.15}s`,
|
||||
}} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
96
src/components/home/AnimatedDarkBg.tsx
Normal file
96
src/components/home/AnimatedDarkBg.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
/**
|
||||
* Premium animated dark background — gold gradient mesh orbs, drifting grid,
|
||||
* floating particles, and a fine noise overlay. Pure CSS, GPU-friendly.
|
||||
* Sits at z-index 0 behind section content.
|
||||
*/
|
||||
export default function AnimatedDarkBg({
|
||||
variant = 'amber',
|
||||
}: { variant?: 'amber' | 'cool' | 'mono' }) {
|
||||
const orbA = variant === 'cool' ? 'rgba(80,140,220,0.10)' : 'rgba(244,190,40,0.12)';
|
||||
const orbB = variant === 'cool' ? 'rgba(140,90,210,0.10)' : 'rgba(255,149,0,0.08)';
|
||||
|
||||
return (
|
||||
<div aria-hidden className="adb-root" style={{ position: 'absolute', inset: 0, zIndex: 0, overflow: 'hidden', pointerEvents: 'none' }}>
|
||||
{/* Base gradient */}
|
||||
<div style={{
|
||||
position: 'absolute', inset: 0,
|
||||
background: 'radial-gradient(ellipse 120% 80% at 50% -10%, #131017 0%, #07070d 55%, #050509 100%)',
|
||||
}} />
|
||||
|
||||
{/* Drifting mesh orbs */}
|
||||
<div className="adb-orb adb-orb-1" style={{ background: `radial-gradient(circle, ${orbA} 0%, transparent 65%)` }} />
|
||||
<div className="adb-orb adb-orb-2" style={{ background: `radial-gradient(circle, ${orbB} 0%, transparent 65%)` }} />
|
||||
<div className="adb-orb adb-orb-3" style={{ background: `radial-gradient(circle, ${orbA} 0%, transparent 70%)` }} />
|
||||
|
||||
{/* Moving grid */}
|
||||
<div className="adb-grid" />
|
||||
|
||||
{/* Floating particles */}
|
||||
{Array.from({ length: 14 }).map((_, i) => (
|
||||
<span key={i} className="adb-dot" style={{
|
||||
left: `${(i * 67) % 100}%`,
|
||||
top: `${(i * 41) % 100}%`,
|
||||
width: `${2 + (i % 3)}px`,
|
||||
height: `${2 + (i % 3)}px`,
|
||||
animationDuration: `${7 + (i % 5) * 1.6}s`,
|
||||
animationDelay: `${-i * 0.9}s`,
|
||||
opacity: 0.1 + (i % 4) * 0.06,
|
||||
}} />
|
||||
))}
|
||||
|
||||
{/* Noise overlay */}
|
||||
<div className="adb-noise" />
|
||||
|
||||
{/* Vignette */}
|
||||
<div style={{
|
||||
position: 'absolute', inset: 0,
|
||||
background: 'radial-gradient(ellipse 90% 90% at 50% 50%, transparent 55%, rgba(0,0,0,0.45) 100%)',
|
||||
}} />
|
||||
|
||||
<style>{`
|
||||
.adb-orb {
|
||||
position: absolute; border-radius: 50%;
|
||||
filter: blur(40px); will-change: transform;
|
||||
}
|
||||
.adb-orb-1 { width: 50vw; height: 50vw; top: -12%; left: 8%; animation: adbFloat1 18s ease-in-out infinite; }
|
||||
.adb-orb-2 { width: 42vw; height: 42vw; bottom: -14%; right: 6%; animation: adbFloat2 22s ease-in-out infinite; }
|
||||
.adb-orb-3 { width: 34vw; height: 34vw; top: 30%; left: 45%; animation: adbFloat3 26s ease-in-out infinite; }
|
||||
@keyframes adbFloat1 { 0%,100%{transform:translate(0,0) scale(1)} 50%{transform:translate(6%,8%) scale(1.12)} }
|
||||
@keyframes adbFloat2 { 0%,100%{transform:translate(0,0) scale(1)} 50%{transform:translate(-8%,-6%) scale(1.08)} }
|
||||
@keyframes adbFloat3 { 0%,100%{transform:translate(0,0) scale(1)} 50%{transform:translate(-5%,7%) scale(0.92)} }
|
||||
|
||||
.adb-grid {
|
||||
position: absolute; inset: -2px;
|
||||
background-image:
|
||||
linear-gradient(rgba(244,190,40,0.035) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(244,190,40,0.035) 1px, transparent 1px);
|
||||
background-size: 64px 64px;
|
||||
mask-image: radial-gradient(ellipse 80% 70% at 50% 45%, #000 30%, transparent 80%);
|
||||
-webkit-mask-image: radial-gradient(ellipse 80% 70% at 50% 45%, #000 30%, transparent 80%);
|
||||
animation: adbGrid 40s linear infinite;
|
||||
}
|
||||
@keyframes adbGrid { from{background-position:0 0} to{background-position:64px 64px} }
|
||||
|
||||
.adb-dot {
|
||||
position: absolute; border-radius: 50%; background: #f4be28;
|
||||
box-shadow: 0 0 6px rgba(244,190,40,0.6);
|
||||
animation-name: adbDot; animation-timing-function: ease-in-out; animation-iteration-count: infinite;
|
||||
}
|
||||
@keyframes adbDot { 0%,100%{transform:translateY(0)} 50%{transform:translateY(-26px)} }
|
||||
|
||||
.adb-noise {
|
||||
position: absolute; inset: 0; opacity: 0.04; mix-blend-mode: overlay;
|
||||
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.adb-orb, .adb-grid, .adb-dot { animation: none !important; }
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
332
src/components/home/ProblemsShift.tsx
Normal file
332
src/components/home/ProblemsShift.tsx
Normal file
@@ -0,0 +1,332 @@
|
||||
'use client';
|
||||
|
||||
import React, { useEffect, useLayoutEffect, useRef } 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;
|
||||
|
||||
interface ProblemsShiftProps {
|
||||
isLoaded?: boolean;
|
||||
}
|
||||
|
||||
/* ── Data ──────────────────────────────────────────────────────────── */
|
||||
|
||||
interface Item {
|
||||
text: string;
|
||||
icon: React.ReactNode;
|
||||
}
|
||||
|
||||
const PROBLEM_ITEMS: Item[] = [
|
||||
{
|
||||
text: 'No compelling reason for customers to visit your store',
|
||||
icon: (
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M3.98 8.223A10.477 10.477 0 001.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.45 10.45 0 0112 4.5c4.756 0 8.773 3.162 10.065 7.498a10.523 10.523 0 01-4.293 5.774M6.228 6.228L3 3m3.228 3.228l3.65 3.65m7.894 7.894L21 21m-3.228-3.228l-3.65-3.65m0 0a3 3 0 10-4.243-4.243m4.242 4.242L9.88 9.88" />
|
||||
),
|
||||
},
|
||||
{
|
||||
text: 'Traditional discounts erode profit margins',
|
||||
icon: (
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z" />
|
||||
),
|
||||
},
|
||||
{
|
||||
text: 'Digital ads rarely convert into store footfall',
|
||||
icon: (
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M2.25 6l8.25 8.25 4.5-4.5 6.75 6.75M21.75 16.5V11.25m0 5.25H16.5" />
|
||||
),
|
||||
},
|
||||
{
|
||||
text: 'No real-time connection between online activity and offline shopping',
|
||||
icon: (
|
||||
<>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9.568 3H5.25A2.25 2.25 0 003 5.25v4.318c0 .597.237 1.17.659 1.591l9.581 9.581a1.44 1.44 0 002.036 0l4.319-4.319a1.44 1.44 0 000-2.036L11.159 3.659A2.25 2.25 0 009.568 3z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 7.5h.008v.008H6V7.5z" />
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
text: 'Customers forget about stores between visits',
|
||||
icon: (
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 6v6l4 2M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const SHIFT_ITEMS: Item[] = [
|
||||
{
|
||||
text: 'Customers earn Loyaly Coins through daily app activities',
|
||||
icon: (
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15 19.128a9.38 9.38 0 002.625.372 9.337 9.337 0 004.121-.952 4.125 4.125 0 00-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 018.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0111.964-3.07M12 6.375a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zm8.25 2.25a2.625 2.625 0 11-5.25 0 2.625 2.625 0 015.25 0z" />
|
||||
),
|
||||
},
|
||||
{
|
||||
text: 'Game-based campaigns drive footfall with excitement',
|
||||
icon: (
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M2.25 18l8.97-8.97a.75.75 0 011.06 0l2.235 2.234a.75.75 0 001.06 0L21.75 5.5M21.75 5.5h-5.25m5.25 0v5.25" />
|
||||
),
|
||||
},
|
||||
{
|
||||
text: 'Smart surprise coupons create a daily reason to engage',
|
||||
icon: (
|
||||
<>
|
||||
<circle cx="12" cy="12" r="9" />
|
||||
<circle cx="12" cy="12" r="5" />
|
||||
<circle cx="12" cy="12" r="1.5" fill="currentColor" stroke="none" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 2v3M12 19v3M2 12h3M19 12h3" />
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
text: 'Real-time discovery of nearby stores and live offers',
|
||||
icon: (
|
||||
<>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9.568 3H5.25A2.25 2.25 0 003 5.25v4.318c0 .597.237 1.17.659 1.591l9.581 9.581a1.44 1.44 0 002.036 0l4.319-4.319a1.44 1.44 0 000-2.036L11.159 3.659A2.25 2.25 0 009.568 3z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 7.5h.008v.008H6V7.5z" />
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
text: 'Every store visit earns more rewards and deepens loyalty',
|
||||
icon: (
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 13.5l10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75z" />
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const BOTTOM_FEATURES = [
|
||||
{ label: 'Earn Daily', icon: <><circle cx="12" cy="12" r="9" /><circle cx="12" cy="12" r="5" /><path strokeLinecap="round" strokeLinejoin="round" d="M12 2v3M12 19v3M2 12h3M19 12h3" /></> },
|
||||
{ label: 'Play & Win', icon: <path strokeLinecap="round" strokeLinejoin="round" d="M15 19.128a9.38 9.38 0 002.625.372 9.337 9.337 0 004.121-.952 4.125 4.125 0 00-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 018.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0111.964-3.07M12 6.375a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zm8.25 2.25a2.625 2.625 0 11-5.25 0 2.625 2.625 0 015.25 0z" /> },
|
||||
{ label: 'Smart Coupons', icon: <path strokeLinecap="round" strokeLinejoin="round" d="M2.25 18l8.97-8.97a.75.75 0 011.06 0l2.235 2.234a.75.75 0 001.06 0L21.75 5.5M21.75 5.5h-5.25m5.25 0v5.25" /> },
|
||||
{ label: 'Shop Offline', icon: <path strokeLinecap="round" strokeLinejoin="round" d="M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12z" /> },
|
||||
];
|
||||
|
||||
/* ── Card ──────────────────────────────────────────────────────────── */
|
||||
|
||||
function CompareCard({
|
||||
variant,
|
||||
badge,
|
||||
desc,
|
||||
items,
|
||||
image,
|
||||
imageAlt,
|
||||
accent,
|
||||
}: {
|
||||
variant: 'problem' | 'shift';
|
||||
badge: string;
|
||||
desc: string;
|
||||
items: Item[];
|
||||
image: string;
|
||||
imageAlt: string;
|
||||
accent: string;
|
||||
}) {
|
||||
const isProblem = variant === 'problem';
|
||||
return (
|
||||
<div
|
||||
className={`ps-card ps-card-${variant}`}
|
||||
style={{
|
||||
position: 'relative',
|
||||
flex: 1,
|
||||
background: '#ffffff',
|
||||
borderRadius: 28,
|
||||
border: '1px solid rgba(0,0,0,0.05)',
|
||||
padding: 'clamp(28px, 2.6vw, 44px)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
overflow: 'hidden',
|
||||
boxShadow: '0 1px 2px rgba(0,0,0,0.04), 0 24px 60px -28px rgba(23,21,18,0.18)',
|
||||
willChange: 'transform, opacity',
|
||||
}}
|
||||
>
|
||||
{/* accent top edge */}
|
||||
<span style={{ position: 'absolute', top: 0, left: 0, right: 0, height: 4, background: `linear-gradient(90deg, transparent, ${accent}, transparent)` }} />
|
||||
{/* soft accent glow */}
|
||||
<div style={{ position: 'absolute', top: -80, [isProblem ? 'left' : 'right']: -80, width: 260, height: 260, borderRadius: '50%', background: `radial-gradient(circle, ${accent}1f 0%, transparent 70%)`, pointerEvents: 'none' }} />
|
||||
|
||||
{/* Badge */}
|
||||
<div className="ps-reveal" style={{ display: 'inline-flex', alignItems: 'center', gap: 9, marginBottom: 18 }}>
|
||||
<span style={{ width: 30, height: 30, borderRadius: 9, display: 'flex', alignItems: 'center', justifyContent: 'center', background: `${accent}1a`, border: `1px solid ${accent}33` }}>
|
||||
{isProblem ? (
|
||||
<svg width="16" height="16" viewBox="0 0 20 20" fill={accent}><path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" /></svg>
|
||||
) : (
|
||||
<svg width="16" height="16" viewBox="0 0 20 20" fill={accent}><path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" /></svg>
|
||||
)}
|
||||
</span>
|
||||
<span style={{ color: accent, fontSize: 12, fontWeight: 800, letterSpacing: '0.14em', textTransform: 'uppercase', fontFamily: 'Manrope, sans-serif' }}>{badge}</span>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<h3 className="ps-reveal" style={{ margin: '0 0 20px', color: '#171512', fontSize: 'clamp(1.15rem, 1.5vw, 1.45rem)', fontWeight: 800, lineHeight: 1.3, letterSpacing: '-0.01em', fontFamily: 'Sora, sans-serif' }}>
|
||||
{desc}
|
||||
</h3>
|
||||
|
||||
{/* 3D visual */}
|
||||
<div className="ps-reveal ps-visual-wrap" style={{ position: 'relative', height: 'clamp(150px, 17vw, 210px)', margin: '4px 0 22px', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<div style={{ position: 'absolute', inset: 0, background: `radial-gradient(ellipse 60% 60% at 50% 60%, ${accent}22 0%, transparent 70%)`, pointerEvents: 'none' }} />
|
||||
<img src={image} alt={imageAlt} className="ps-visual-img" style={{ maxHeight: '100%', maxWidth: '80%', objectFit: 'contain', pointerEvents: 'none', userSelect: 'none', filter: 'drop-shadow(0 22px 30px rgba(23,21,18,0.16))', willChange: 'transform' }} />
|
||||
</div>
|
||||
|
||||
{/* Items */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 12, marginTop: 'auto' }}>
|
||||
{items.map((item, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className={`ps-item ps-item-${variant}`}
|
||||
style={{ display: 'flex', alignItems: 'center', gap: 13 }}
|
||||
>
|
||||
<span style={{ width: 38, height: 38, flexShrink: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', borderRadius: 11, background: `${accent}12`, border: `1px solid ${accent}26` }}>
|
||||
<svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke={accent} strokeWidth="2" style={{ color: accent }}>{item.icon}</svg>
|
||||
</span>
|
||||
<span style={{ color: '#3f3a32', fontSize: '0.95rem', fontWeight: 600, lineHeight: 1.35, fontFamily: 'Manrope, sans-serif' }}>{item.text}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Section ───────────────────────────────────────────────────────── */
|
||||
|
||||
export default function ProblemsShift({ isLoaded = true }: ProblemsShiftProps) {
|
||||
const sectionRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useIsomorphicLayoutEffect(() => {
|
||||
if (typeof window === 'undefined') return;
|
||||
if (!isLoaded) return;
|
||||
|
||||
const ctx = gsap.context(() => {
|
||||
// Header reveal — clip-path line wipe
|
||||
const headerTl = gsap.timeline({
|
||||
scrollTrigger: { trigger: '.ps-header', start: 'top 85%', toggleActions: 'play none none reset' },
|
||||
});
|
||||
headerTl
|
||||
.fromTo('.ps-eyebrow', { opacity: 0, y: 16 }, { opacity: 1, y: 0, duration: 0.6, ease: 'power3.out' })
|
||||
.fromTo('.ps-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('.ps-subtitle', { opacity: 0, y: 24 }, { opacity: 1, y: 0, duration: 0.9, ease: 'power3.out' }, '-=0.8');
|
||||
|
||||
// Cards reveal + their inner reveal elements + connector
|
||||
const cardsTl = gsap.timeline({
|
||||
scrollTrigger: { trigger: '.ps-cards-row', start: 'top 80%', toggleActions: 'play none none reset' },
|
||||
});
|
||||
cardsTl
|
||||
.fromTo('.ps-card', { opacity: 0, y: 70, scale: 0.97 }, { opacity: 1, y: 0, scale: 1, duration: 1.0, stagger: 0.14, ease: 'power3.out' })
|
||||
.fromTo('.ps-connector', { opacity: 0, scale: 0.4 }, { opacity: 1, scale: 1, duration: 0.7, ease: 'back.out(1.8)' }, '-=0.7')
|
||||
.fromTo('.ps-card-problem .ps-reveal', { opacity: 0, y: 22 }, { opacity: 1, y: 0, duration: 0.6, stagger: 0.08, ease: 'power3.out' }, '-=0.8')
|
||||
.fromTo('.ps-card-shift .ps-reveal', { opacity: 0, y: 22 }, { opacity: 1, y: 0, duration: 0.6, stagger: 0.08, ease: 'power3.out' }, '<')
|
||||
.fromTo('.ps-item-problem', { opacity: 0, x: -24 }, { opacity: 1, x: 0, duration: 0.5, stagger: 0.07, ease: 'power3.out' }, '-=0.5')
|
||||
.fromTo('.ps-item-shift', { opacity: 0, x: 24 }, { opacity: 1, x: 0, duration: 0.5, stagger: 0.07, ease: 'power3.out' }, '<');
|
||||
|
||||
// Parallax drift on the 3D visuals while scrolling through the section
|
||||
gsap.utils.toArray<HTMLElement>('.ps-visual-img').forEach((img) => {
|
||||
gsap.fromTo(img, { y: 26 }, {
|
||||
y: -26,
|
||||
ease: 'none',
|
||||
scrollTrigger: { trigger: img, start: 'top bottom', end: 'bottom top', scrub: 1 },
|
||||
});
|
||||
});
|
||||
|
||||
// Bottom bar
|
||||
const bottomTl = gsap.timeline({
|
||||
scrollTrigger: { trigger: '.ps-bottom-bar', start: 'top 92%', toggleActions: 'play none none reset' },
|
||||
});
|
||||
bottomTl
|
||||
.fromTo('.ps-bottom-bar', { opacity: 0, y: 36, scale: 0.97 }, { opacity: 1, y: 0, scale: 1, duration: 0.8, ease: 'power3.out' })
|
||||
.fromTo('.ps-feature-item', { opacity: 0, y: 16 }, { opacity: 1, y: 0, stagger: 0.09, duration: 0.5, ease: 'power3.out' }, '-=0.5');
|
||||
|
||||
ScrollTrigger.refresh();
|
||||
}, sectionRef);
|
||||
|
||||
return () => ctx.revert();
|
||||
}, [isLoaded]);
|
||||
|
||||
return (
|
||||
<section
|
||||
ref={sectionRef}
|
||||
id="problems-shift"
|
||||
className="relative overflow-hidden"
|
||||
style={{
|
||||
background: 'linear-gradient(180deg, #faf9f5 0%, #f4f2ea 100%)',
|
||||
width: 'calc(100% - 40px)',
|
||||
margin: '0 20px 20px',
|
||||
borderRadius: '32px',
|
||||
padding: 'clamp(72px, 9vw, 130px) clamp(20px, 4vw, 56px) clamp(56px, 7vw, 90px)',
|
||||
zIndex: 1,
|
||||
}}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="ps-header relative z-10 text-center max-w-3xl mx-auto" style={{ marginBottom: 'clamp(40px, 5vw, 72px)' }}>
|
||||
<div className="ps-eyebrow" style={{ display: 'inline-flex', alignItems: 'center', gap: 9, padding: '7px 16px', borderRadius: 999, background: 'rgba(244,190,40,0.1)', border: '1px solid rgba(244,190,40,0.28)', marginBottom: 24 }}>
|
||||
<span style={{ width: 6, height: 6, borderRadius: '50%', background: '#f4be28', display: 'block' }} />
|
||||
<span style={{ fontSize: 11, fontWeight: 800, letterSpacing: '0.18em', textTransform: 'uppercase', color: '#C49600', fontFamily: 'Manrope, sans-serif' }}>The Opportunity</span>
|
||||
</div>
|
||||
<h2 className="ps-title" style={{ color: '#171512', fontWeight: 900, fontSize: 'clamp(2.4rem, 6vw, 4.8rem)', letterSpacing: '-0.05em', lineHeight: 0.94, margin: '0 0 20px', fontFamily: 'Sora, sans-serif' }}>
|
||||
Retail is busy.<br /><span style={{ color: '#f4be28' }}>But blind.</span>
|
||||
</h2>
|
||||
<p className="ps-subtitle" style={{ color: '#4f463a', fontSize: 'clamp(1rem, 1.3vw, 1.2rem)', fontWeight: 500, maxWidth: 620, margin: '0 auto', lineHeight: 1.7, fontFamily: 'Manrope, sans-serif' }}>
|
||||
Customers have no compelling reason to choose your store over the next one. Loyaly creates that reason — with daily rewards, interactive games, and surprise coupons redeemable in person.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Comparison row */}
|
||||
<div className="ps-cards-row relative z-10 mx-auto" style={{ maxWidth: 1240, display: 'flex', alignItems: 'stretch', gap: 'clamp(16px, 2vw, 28px)' }}>
|
||||
<CompareCard
|
||||
variant="problem"
|
||||
badge="The Problem"
|
||||
desc="Retailers struggle to attract new customers and retain existing ones without sacrificing profit margins on generic discounts."
|
||||
items={PROBLEM_ITEMS}
|
||||
image="/Problems+shift/Problems.png"
|
||||
imageAlt="In-store behavior problems"
|
||||
accent="#ef4444"
|
||||
/>
|
||||
|
||||
{/* Connector */}
|
||||
<div className="ps-connector" style={{ flexShrink: 0, alignSelf: 'center', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<div className="animate-float" style={{ width: 56, height: 56, borderRadius: '50%', background: '#fff', border: '3px solid rgba(244,190,40,0.8)', boxShadow: '0 10px 30px rgba(0,0,0,0.1)', display: 'flex', alignItems: 'center', justifyContent: 'center', position: 'relative' }}>
|
||||
<div className="absolute inset-[-3px] rounded-full animate-pulse-ring-1" style={{ background: 'rgba(244,190,40,0.25)', zIndex: -1 }} />
|
||||
<svg className="ps-connector-arrow" width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="#f4be28" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M5 12h14M13 6l6 6-6 6" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CompareCard
|
||||
variant="shift"
|
||||
badge="The Shift"
|
||||
desc="Loyaly bridges digital engagement and offline shopping — customers earn coins and coupons through fun activities, then walk into nearby stores to redeem them in person."
|
||||
items={SHIFT_ITEMS}
|
||||
image="/Problems+shift/Shift.png"
|
||||
imageAlt="In-store intelligence shift"
|
||||
accent="#22c55e"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Bottom features bar */}
|
||||
<div className="ps-bottom-bar relative z-10 mx-auto" style={{ maxWidth: 980, marginTop: 'clamp(40px, 5vw, 72px)', background: '#fff', borderRadius: 20, border: '1px solid rgba(0,0,0,0.05)', boxShadow: '0 20px 50px -30px rgba(23,21,18,0.2)', padding: 'clamp(16px, 1.6vw, 22px) clamp(20px, 3vw, 44px)', display: 'flex', flexWrap: 'wrap', justifyContent: 'space-between', alignItems: 'center', gap: 16 }}>
|
||||
{BOTTOM_FEATURES.map((f, i) => (
|
||||
<React.Fragment key={f.label}>
|
||||
<div className="ps-feature-item" style={{ flex: '1 1 180px', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 11, cursor: 'default' }}>
|
||||
<span style={{ width: 36, height: 36, display: 'flex', alignItems: 'center', justifyContent: 'center', borderRadius: 10, background: '#faf9f5', border: '1px solid rgba(0,0,0,0.05)' }}>
|
||||
<svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="#171512" strokeWidth="2">{f.icon}</svg>
|
||||
</span>
|
||||
<span style={{ color: '#171512', fontWeight: 700, fontSize: '0.95rem', fontFamily: 'Manrope, sans-serif' }}>{f.label}</span>
|
||||
</div>
|
||||
{i < BOTTOM_FEATURES.length - 1 && <span className="ps-sep" style={{ width: 1, height: 28, background: 'rgba(0,0,0,0.07)' }} />}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Responsive: stack cards on narrow screens, rotate connector */}
|
||||
<style>{`
|
||||
@media (max-width: 900px) {
|
||||
#problems-shift .ps-cards-row { flex-direction: column; }
|
||||
#problems-shift .ps-connector-arrow { transform: rotate(90deg); }
|
||||
#problems-shift .ps-sep { display: none; }
|
||||
}
|
||||
`}</style>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
523
src/components/home/ProductsSection.tsx
Normal file
523
src/components/home/ProductsSection.tsx
Normal file
@@ -0,0 +1,523 @@
|
||||
'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;
|
||||
|
||||
interface Product {
|
||||
id: number;
|
||||
href: string;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
description: string;
|
||||
accent: string;
|
||||
accentRgb: string;
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
const PRODUCTS: Product[] = [
|
||||
{
|
||||
id: 1,
|
||||
href: '/solutions/behavision',
|
||||
title: 'Behavision',
|
||||
subtitle: 'AI-Powered Footfall Analytics',
|
||||
description:
|
||||
'AI-powered footfall analytics and behavioral intelligence. Understand how customers move, dwell, and engage — turning raw foot traffic into rich, actionable insights.',
|
||||
accent: '#f4be28',
|
||||
accentRgb: '244, 190, 40',
|
||||
tags: ['Footfall Tracking', 'Heatmaps', 'Behavior Analysis'],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
href: '/solutions/spendsense',
|
||||
title: 'SpendSense',
|
||||
subtitle: 'Customer Spending Intelligence',
|
||||
description:
|
||||
'Deep insights into customer spending patterns. Identify high-value segments, predict buying behaviour, and unlock incremental revenue hiding in plain sight.',
|
||||
accent: '#10B981',
|
||||
accentRgb: '16, 185, 129',
|
||||
tags: ['Spending Trends', 'Revenue Analytics', 'Customer Value'],
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
href: '/solutions/dyscount',
|
||||
title: 'Dyscount',
|
||||
subtitle: 'Smart Discount Engine',
|
||||
description:
|
||||
'Smart discount engine that maximises conversions without killing margins. Deliver the right offer to the right customer at exactly the right moment.',
|
||||
accent: '#EF4444',
|
||||
accentRgb: '239, 68, 68',
|
||||
tags: ['Smart Discounts', 'Offer Automation', 'Margin Protection'],
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
href: '/solutions/loyaly-app',
|
||||
title: 'Loyaly App',
|
||||
subtitle: 'Consumer Loyalty Platform',
|
||||
description:
|
||||
'Earn Loyaly Coins through daily login, walking challenges, and interactive games. Unlock surprise coupons, discover nearby store campaigns, and redeem rewards at real offline stores.',
|
||||
accent: '#f4be28',
|
||||
accentRgb: '244, 190, 40',
|
||||
tags: ['Daily Rewards', 'Game-Based Offers', 'Offline Redemption'],
|
||||
},
|
||||
];
|
||||
|
||||
function ProductIcon({ id, accent }: { id: number; accent: string }) {
|
||||
if (id === 1) {
|
||||
return (
|
||||
<svg viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg" style={{ width: '100%', height: '100%' }}>
|
||||
<circle cx="24" cy="24" r="20" fill={accent} opacity="0.15" />
|
||||
<path d="M14 34 C14 28 18 24 24 24 C30 24 34 28 34 34" stroke={accent} strokeWidth="2.5" strokeLinecap="round" fill="none" />
|
||||
<circle cx="24" cy="18" r="5" stroke={accent} strokeWidth="2.5" fill="none" />
|
||||
<circle cx="14" cy="20" r="3.5" stroke={accent} strokeWidth="2" fill="none" />
|
||||
<circle cx="34" cy="20" r="3.5" stroke={accent} strokeWidth="2" fill="none" />
|
||||
<path d="M8 34 C8 30 10 27 14 27" stroke={accent} strokeWidth="2" strokeLinecap="round" opacity="0.6" fill="none" />
|
||||
<path d="M40 34 C40 30 38 27 34 27" stroke={accent} strokeWidth="2" strokeLinecap="round" opacity="0.6" fill="none" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
if (id === 2) {
|
||||
return (
|
||||
<svg viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg" style={{ width: '100%', height: '100%' }}>
|
||||
<circle cx="24" cy="24" r="20" fill={accent} opacity="0.15" />
|
||||
<rect x="10" y="28" width="6" height="10" rx="2" fill={accent} opacity="0.5" />
|
||||
<rect x="19" y="22" width="6" height="16" rx="2" fill={accent} opacity="0.7" />
|
||||
<rect x="28" y="16" width="6" height="22" rx="2" fill={accent} />
|
||||
<path d="M12 26 L21 18 L30 12 L36 10" stroke={accent} strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" opacity="0.8" fill="none" />
|
||||
<circle cx="36" cy="10" r="2.5" fill={accent} />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
if (id === 3) {
|
||||
return (
|
||||
<svg viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg" style={{ width: '100%', height: '100%' }}>
|
||||
<circle cx="24" cy="24" r="20" fill={accent} opacity="0.15" />
|
||||
<path d="M14 14 L34 34" stroke={accent} strokeWidth="2.2" strokeLinecap="round" opacity="0.4" fill="none" />
|
||||
<circle cx="16" cy="16" r="5" stroke={accent} strokeWidth="2.5" fill="none" />
|
||||
<circle cx="32" cy="32" r="5" stroke={accent} strokeWidth="2.5" fill="none" />
|
||||
<path d="M30 14 L34 14 L34 18" stroke={accent} strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" fill="none" />
|
||||
<path d="M18 34 L14 34 L14 30" stroke={accent} strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" fill="none" />
|
||||
<circle cx="16" cy="16" r="2" fill={accent} />
|
||||
<circle cx="32" cy="32" r="2" fill={accent} />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<svg viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg" style={{ width: '100%', height: '100%' }}>
|
||||
<circle cx="24" cy="24" r="20" fill={accent} opacity="0.15" />
|
||||
<path d="M24 36 C24 36 10 28 10 19 C10 14.6 13.6 11 18 11 C20.6 11 22.9 12.3 24 14.3 C25.1 12.3 27.4 11 30 11 C34.4 11 38 14.6 38 19 C38 28 24 36 24 36Z" stroke={accent} strokeWidth="2.5" fill={accent} fillOpacity="0.12" strokeLinejoin="round" />
|
||||
<circle cx="36" cy="34" r="6" fill={accent} />
|
||||
<path d="M33 34 L35 36 L39 32" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" fill="none" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
/* Real product-preview visual per product — a believable mini-UI, not generic decoration */
|
||||
function ProductVisual({ product }: { product: Product }) {
|
||||
const { id, accent, accentRgb } = product;
|
||||
|
||||
// 1 — Behavision: footfall heatmap + live counter
|
||||
if (id === 1) {
|
||||
const cells = Array.from({ length: 48 });
|
||||
return (
|
||||
<div style={{ width: '86%', display: 'flex', flexDirection: 'column', gap: 14 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<span style={{ fontSize: 11, letterSpacing: '0.12em', textTransform: 'uppercase', color: 'rgba(255,255,255,0.4)', fontFamily: 'Manrope, sans-serif', fontWeight: 600 }}>Live footfall</span>
|
||||
<span style={{ fontSize: 11, color: accent, fontFamily: 'Manrope, sans-serif', fontWeight: 700, display: 'flex', alignItems: 'center', gap: 5 }}>
|
||||
<span style={{ width: 6, height: 6, borderRadius: '50%', background: '#4ade80', display: 'block' }} />2,847
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(8,1fr)', gap: 5 }}>
|
||||
{cells.map((_, i) => {
|
||||
const intensity = Math.abs(Math.sin(i * 1.7) * Math.cos(i * 0.6));
|
||||
const alpha = (0.08 + intensity * 0.7).toFixed(3);
|
||||
return <div key={i} style={{ aspectRatio: '1 / 1', borderRadius: 5, background: `rgba(${accentRgb}, ${alpha})` }} />;
|
||||
})}
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 10 }}>
|
||||
{[['Peak', '6–8 PM'], ['Dwell', '14 min'], ['Zones', '12']].map(([k, v]) => (
|
||||
<div key={k} style={{ flex: 1, padding: '10px 12px', borderRadius: 12, background: 'rgba(255,255,255,0.04)', border: '1px solid rgba(255,255,255,0.08)' }}>
|
||||
<div style={{ fontSize: 10, color: 'rgba(255,255,255,0.4)', fontFamily: 'Manrope, sans-serif' }}>{k}</div>
|
||||
<div style={{ fontSize: 15, fontWeight: 800, color: '#fff', fontFamily: 'Sora, sans-serif', marginTop: 2 }}>{v}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 2 — SpendSense: spend trend chart
|
||||
if (id === 2) {
|
||||
const pts = [22, 30, 26, 42, 38, 54, 48, 66, 72];
|
||||
const max = 80;
|
||||
const w = 280, h = 120;
|
||||
const path = pts.map((p, i) => `${(i / (pts.length - 1)) * w},${h - (p / max) * h}`).join(' ');
|
||||
return (
|
||||
<div style={{ width: '86%', display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||||
<div>
|
||||
<div style={{ fontSize: 11, letterSpacing: '0.12em', textTransform: 'uppercase', color: 'rgba(255,255,255,0.4)', fontFamily: 'Manrope, sans-serif', fontWeight: 600 }}>Avg basket value</div>
|
||||
<div style={{ display: 'flex', alignItems: 'baseline', gap: 10, marginTop: 4 }}>
|
||||
<span style={{ fontSize: 34, fontWeight: 900, color: '#fff', fontFamily: 'Sora, sans-serif', letterSpacing: '-0.03em' }}>₹1,240</span>
|
||||
<span style={{ fontSize: 13, fontWeight: 700, color: accent, fontFamily: 'Manrope, sans-serif' }}>+18%</span>
|
||||
</div>
|
||||
</div>
|
||||
<svg viewBox={`0 0 ${w} ${h}`} style={{ width: '100%', height: 'auto', overflow: 'visible' }}>
|
||||
<defs>
|
||||
<linearGradient id={`sg${id}`} x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stopColor={accent} stopOpacity="0.35" />
|
||||
<stop offset="100%" stopColor={accent} stopOpacity="0" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<polygon points={`0,${h} ${path} ${w},${h}`} fill={`url(#sg${id})`} />
|
||||
<polyline points={path} fill="none" stroke={accent} strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
{pts.map((p, i) => <circle key={i} cx={(i / (pts.length - 1)) * w} cy={h - (p / max) * h} r={i === pts.length - 1 ? 4 : 0} fill={accent} />)}
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 3 — Dyscount: smart offer + conversion uplift
|
||||
if (id === 3) {
|
||||
return (
|
||||
<div style={{ width: '82%', display: 'flex', flexDirection: 'column', gap: 14 }}>
|
||||
<div style={{ borderRadius: 16, padding: '18px 20px', background: `linear-gradient(135deg, ${accent} 0%, #c0392b 100%)`, position: 'relative', overflow: 'hidden' }}>
|
||||
<div style={{ fontSize: 11, fontWeight: 700, letterSpacing: '0.1em', textTransform: 'uppercase', color: 'rgba(0,0,0,0.6)', fontFamily: 'Manrope, sans-serif' }}>Smart Offer</div>
|
||||
<div style={{ fontSize: 30, fontWeight: 900, color: '#fff', fontFamily: 'Sora, sans-serif', letterSpacing: '-0.02em', marginTop: 2 }}>15% OFF</div>
|
||||
<div style={{ fontSize: 12, color: 'rgba(255,255,255,0.85)', fontFamily: 'Manrope, sans-serif', marginTop: 2 }}>Targeted · high-intent shoppers</div>
|
||||
</div>
|
||||
<div style={{ padding: '14px 16px', borderRadius: 14, background: 'rgba(255,255,255,0.04)', border: '1px solid rgba(255,255,255,0.08)' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 8 }}>
|
||||
<span style={{ fontSize: 12, color: 'rgba(255,255,255,0.5)', fontFamily: 'Manrope, sans-serif' }}>Conversion uplift</span>
|
||||
<span style={{ fontSize: 13, fontWeight: 800, color: accent, fontFamily: 'Sora, sans-serif' }}>+41%</span>
|
||||
</div>
|
||||
<div style={{ height: 6, borderRadius: 3, background: 'rgba(255,255,255,0.08)', overflow: 'hidden' }}>
|
||||
<div style={{ height: '100%', width: '72%', borderRadius: 3, background: accent }} />
|
||||
</div>
|
||||
<div style={{ fontSize: 11, color: 'rgba(255,255,255,0.35)', fontFamily: 'Manrope, sans-serif', marginTop: 10 }}>Margin protected · 0 waste</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 4 — Loyaly App: rewards card
|
||||
return (
|
||||
<div style={{ width: '80%', borderRadius: 22, padding: 22, background: 'rgba(255,255,255,0.04)', border: '1px solid rgba(255,255,255,0.1)', display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<span style={{ fontSize: 12, fontWeight: 800, color: '#fff', fontFamily: 'Sora, sans-serif' }}>Loyaly ✦</span>
|
||||
<span style={{ fontSize: 10, color: 'rgba(255,255,255,0.4)', fontFamily: 'Manrope, sans-serif' }}>GOLD MEMBER</span>
|
||||
</div>
|
||||
<div style={{ borderRadius: 16, padding: '18px 20px', background: `linear-gradient(135deg, ${accent} 0%, #e09820 100%)` }}>
|
||||
<div style={{ fontSize: 10, fontWeight: 700, letterSpacing: '0.1em', textTransform: 'uppercase', color: 'rgba(0,0,0,0.6)', fontFamily: 'Manrope, sans-serif' }}>Reward Points</div>
|
||||
<div style={{ fontSize: 32, fontWeight: 900, color: '#111', fontFamily: 'Sora, sans-serif', letterSpacing: '-0.03em', marginTop: 2 }}>284</div>
|
||||
</div>
|
||||
{[['Starbucks', '+120'], ['Nike Store', '+85']].map(([s, p]) => (
|
||||
<div key={s} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '8px 12px', borderRadius: 10, background: 'rgba(255,255,255,0.04)' }}>
|
||||
<span style={{ fontSize: 12, color: 'rgba(255,255,255,0.7)', fontFamily: 'Manrope, sans-serif' }}>{s}</span>
|
||||
<span style={{ fontSize: 12, fontWeight: 700, color: '#4ade80', fontFamily: 'Manrope, sans-serif' }}>{p}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Gallery card (horizontal panel) ──────────────────────────────── */
|
||||
function ProductCard({ product, index }: { product: Product; index: number }) {
|
||||
return (
|
||||
<article
|
||||
className="products-card"
|
||||
data-index={index}
|
||||
style={{
|
||||
borderRadius: '28px',
|
||||
background: 'linear-gradient(145deg, #0e0e13 0%, #16161d 100%)',
|
||||
boxShadow: `inset 0 1px 0 rgba(255,255,255,0.05)`,
|
||||
border: `1px solid rgba(${product.accentRgb}, 0.16)`,
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
overflow: 'hidden',
|
||||
scrollSnapAlign: 'center',
|
||||
}}
|
||||
>
|
||||
{/* ── Left content ── */}
|
||||
<div className="products-card-content" style={{
|
||||
flex: '0 0 54%',
|
||||
padding: 'clamp(22px, 2.6vw, 46px)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'space-between',
|
||||
gap: 14,
|
||||
minWidth: 0,
|
||||
}}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 'clamp(11px, 1.5vh, 18px)' }}>
|
||||
<div style={{
|
||||
width: 48, height: 48, borderRadius: 14, padding: 10,
|
||||
background: `rgba(${product.accentRgb}, 0.1)`,
|
||||
border: `1px solid rgba(${product.accentRgb}, 0.22)`,
|
||||
boxShadow: 'inset 0 1px 0 rgba(255,255,255,0.06)',
|
||||
flexShrink: 0,
|
||||
}}>
|
||||
<ProductIcon id={product.id} accent={product.accent} />
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 14 }}>
|
||||
<span style={{ fontSize: 13, fontWeight: 800, color: product.accent, fontFamily: 'Sora, sans-serif', letterSpacing: '0.05em' }}>0{product.id}</span>
|
||||
<span style={{ width: 24, height: 1, background: `rgba(${product.accentRgb},0.4)`, display: 'block' }} />
|
||||
<span style={{ fontSize: '0.68rem', fontWeight: 700, letterSpacing: '0.12em', textTransform: 'uppercase', color: product.accent, fontFamily: 'Manrope, sans-serif' }}>{product.subtitle}</span>
|
||||
</div>
|
||||
|
||||
<h3 style={{
|
||||
margin: 0,
|
||||
fontSize: 'clamp(1.7rem, 2.8vw, 3rem)',
|
||||
fontWeight: 900, lineHeight: 0.98, letterSpacing: '-0.04em',
|
||||
color: '#ffffff', fontFamily: 'Sora, sans-serif',
|
||||
}}>{product.title}</h3>
|
||||
|
||||
<p style={{
|
||||
margin: 0,
|
||||
fontSize: 'clamp(0.82rem, 0.95vw, 0.98rem)',
|
||||
lineHeight: 1.6, color: 'rgba(255,255,255,0.5)',
|
||||
maxWidth: '420px', fontFamily: 'Manrope, sans-serif',
|
||||
}}>{product.description}</p>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 'clamp(11px, 1.5vh, 16px)' }}>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
|
||||
{product.tags.map((tag) => (
|
||||
<span key={tag} style={{
|
||||
padding: '5px 13px', borderRadius: '99px', fontSize: '0.7rem', fontWeight: 600,
|
||||
color: 'rgba(255,255,255,0.7)', background: 'rgba(255,255,255,0.05)',
|
||||
border: '1px solid rgba(255,255,255,0.1)', fontFamily: 'Manrope, sans-serif',
|
||||
}}>{tag}</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Link
|
||||
href={product.href}
|
||||
style={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: '8px', width: 'max-content',
|
||||
padding: '11px 22px', borderRadius: '99px', fontSize: '0.85rem', fontWeight: 800,
|
||||
textDecoration: 'none', color: '#111111', background: product.accent,
|
||||
fontFamily: 'Manrope, sans-serif', boxShadow: `0 12px 36px rgba(${product.accentRgb},0.25)`,
|
||||
transition: 'transform 0.3s cubic-bezier(0.34,1.56,0.64,1)',
|
||||
}}
|
||||
onMouseEnter={(e) => { (e.currentTarget as HTMLAnchorElement).style.transform = 'scale(1.04)'; }}
|
||||
onMouseLeave={(e) => { (e.currentTarget as HTMLAnchorElement).style.transform = 'scale(1)'; }}
|
||||
>
|
||||
Explore {product.title}
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style={{ width: '15px', height: '15px' }}>
|
||||
<path d="M5 12h14M13 6l6 6-6 6" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Right visual panel ── */}
|
||||
<div className="products-card-visual" style={{
|
||||
flex: '0 0 46%',
|
||||
position: 'relative', overflow: 'hidden',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
borderLeft: `1px solid rgba(${product.accentRgb}, 0.1)`,
|
||||
background: `radial-gradient(ellipse 90% 80% at 60% 40%, rgba(${product.accentRgb},0.08) 0%, transparent 70%)`,
|
||||
}}>
|
||||
<div style={{
|
||||
position: 'absolute', inset: 0,
|
||||
backgroundImage: `linear-gradient(rgba(${product.accentRgb},0.05) 1px,transparent 1px),linear-gradient(90deg,rgba(${product.accentRgb},0.05) 1px,transparent 1px)`,
|
||||
backgroundSize: '40px 40px',
|
||||
maskImage: 'radial-gradient(ellipse 70% 70% at 50% 50%, #000 40%, transparent 85%)',
|
||||
WebkitMaskImage: 'radial-gradient(ellipse 70% 70% at 50% 50%, #000 40%, transparent 85%)',
|
||||
}} />
|
||||
<ProductVisual product={product} />
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
interface ProductsSectionProps {
|
||||
isLoaded?: boolean;
|
||||
}
|
||||
|
||||
export default function ProductsSection({ isLoaded = false }: ProductsSectionProps) {
|
||||
const sectionRef = useRef<HTMLDivElement>(null);
|
||||
const trackRef = useRef<HTMLDivElement>(null);
|
||||
const progressRef = useRef<HTMLDivElement>(null);
|
||||
const counterRef = useRef<HTMLSpanElement>(null);
|
||||
|
||||
useIsomorphicLayoutEffect(() => {
|
||||
if (!isLoaded) return;
|
||||
if (typeof window === 'undefined') return;
|
||||
const section = sectionRef.current;
|
||||
const track = trackRef.current;
|
||||
if (!section || !track) return;
|
||||
|
||||
const mm = gsap.matchMedia();
|
||||
|
||||
// Desktop / tablet: pinned horizontal scroll
|
||||
mm.add('(min-width: 901px)', () => {
|
||||
const distance = () => Math.max(0, track.scrollWidth - section.clientWidth);
|
||||
|
||||
const tween = gsap.to(track, {
|
||||
x: () => -distance(),
|
||||
ease: 'none',
|
||||
scrollTrigger: {
|
||||
trigger: section,
|
||||
start: 'top top',
|
||||
end: () => `+=${distance()}`,
|
||||
pin: true,
|
||||
pinType: 'transform',
|
||||
pinSpacing: true,
|
||||
anticipatePin: 1,
|
||||
scrub: 1,
|
||||
invalidateOnRefresh: true,
|
||||
refreshPriority: 5,
|
||||
onUpdate: (self) => {
|
||||
if (progressRef.current) progressRef.current.style.transform = `scaleX(${self.progress})`;
|
||||
if (counterRef.current) {
|
||||
const idx = Math.min(PRODUCTS.length, Math.max(1, Math.round(self.progress * (PRODUCTS.length - 1)) + 1));
|
||||
counterRef.current.textContent = `0${idx}`;
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
ScrollTrigger.refresh();
|
||||
return () => { tween.scrollTrigger?.kill(); tween.kill(); gsap.set(track, { clearProps: 'transform' }); };
|
||||
});
|
||||
|
||||
return () => mm.revert();
|
||||
}, [isLoaded]);
|
||||
|
||||
return (
|
||||
<section
|
||||
ref={sectionRef}
|
||||
id="products-section"
|
||||
className="products-section-root"
|
||||
style={{ position: 'relative', zIndex: 3 }}
|
||||
>
|
||||
{/* ambient glow */}
|
||||
<div style={{ position: 'absolute', inset: 0, background: 'radial-gradient(ellipse 80% 60% at 50% 0%, rgba(244, 190, 40, 0.07) 0%, transparent 70%)', pointerEvents: 'none' }} />
|
||||
|
||||
{/* Heading (fixed within the pinned viewport) */}
|
||||
<div className="products-heading">
|
||||
<div style={{ maxWidth: 480 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 14 }}>
|
||||
<span style={{ width: 32, height: 2, background: '#f4be28', display: 'block', borderRadius: 2 }} />
|
||||
<span style={{ fontSize: 11, fontWeight: 700, letterSpacing: '0.2em', textTransform: 'uppercase', color: '#C49600', fontFamily: 'Manrope, sans-serif' }}>Platform</span>
|
||||
</div>
|
||||
<h2 style={{ margin: 0, fontSize: 'clamp(1.9rem, 3.6vw, 3.2rem)', fontWeight: 900, lineHeight: 0.94, letterSpacing: '-0.04em', color: '#111111', fontFamily: 'Sora, sans-serif' }}>
|
||||
Four products.<br />
|
||||
<span style={{ background: 'linear-gradient(105deg, #f4be28 0%, #FF9800 100%)', backgroundSize: '200% auto', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent', animation: 'prodShimmer 3s linear infinite' }}>One ecosystem.</span>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="products-heading-hint" style={{ maxWidth: 280, textAlign: 'right' }}>
|
||||
<p style={{ margin: '0 0 14px', fontSize: 'clamp(0.85rem, 1vw, 1rem)', lineHeight: 1.6, color: 'rgba(0,0,0,0.45)', fontFamily: 'Manrope, sans-serif' }}>
|
||||
Powering every stage of the retail journey with intelligent AI solutions.
|
||||
</p>
|
||||
<div style={{ display: 'inline-flex', alignItems: 'center', gap: 8, fontSize: 11, fontWeight: 700, letterSpacing: '0.1em', textTransform: 'uppercase', color: 'rgba(0,0,0,0.32)', fontFamily: 'Manrope, sans-serif' }}>
|
||||
Scroll to explore
|
||||
<svg viewBox="0 0 24 16" fill="none" width="22" height="14">
|
||||
<path d="M2 8h18M14 3l5 5-5 5" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Horizontal track */}
|
||||
<div ref={trackRef} className="products-track">
|
||||
{PRODUCTS.map((product, index) => (
|
||||
<ProductCard key={product.id} product={product} index={index} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Progress bar + counter */}
|
||||
<div className="products-progress-wrap">
|
||||
<span ref={counterRef} style={{ fontSize: 12, fontWeight: 800, color: '#C49600', fontFamily: 'Sora, sans-serif', letterSpacing: '0.08em', minWidth: 22 }}>01</span>
|
||||
<div style={{ position: 'relative', flex: 1, height: 3, borderRadius: 99, background: 'rgba(0,0,0,0.08)', overflow: 'hidden' }}>
|
||||
<div ref={progressRef} style={{ position: 'absolute', inset: 0, borderRadius: 99, background: 'linear-gradient(90deg,#f4be28,#FF9800)', transformOrigin: 'left center', transform: 'scaleX(0)' }} />
|
||||
</div>
|
||||
<span style={{ fontSize: 12, fontWeight: 800, color: 'rgba(0,0,0,0.3)', fontFamily: 'Sora, sans-serif', letterSpacing: '0.08em' }}>0{PRODUCTS.length}</span>
|
||||
</div>
|
||||
|
||||
<style>{`
|
||||
@keyframes prodShimmer { 0%{background-position:200% center} 100%{background-position:-200% center} }
|
||||
|
||||
.products-section-root {
|
||||
--pt: clamp(230px, 28vh, 310px);
|
||||
--pb: clamp(64px, 8vh, 96px);
|
||||
width: calc(100% - 40px);
|
||||
margin: 20px 20px;
|
||||
border-radius: 32px;
|
||||
background: #FAFAFA;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
.products-card {
|
||||
flex: 0 0 auto;
|
||||
width: clamp(320px, 74vw, 880px);
|
||||
/* Fill the band between heading and progress bar, capped on huge screens */
|
||||
height: min(740px, calc(100vh - var(--pt) - var(--pb)));
|
||||
}
|
||||
.products-heading {
|
||||
position: absolute; top: 0; left: 0; right: 0; z-index: 20;
|
||||
display: flex; align-items: flex-end; justify-content: space-between;
|
||||
padding: clamp(32px, 5vh, 56px) clamp(24px, 5vw, 68px) 0;
|
||||
box-sizing: border-box; pointer-events: none;
|
||||
}
|
||||
.products-heading a { pointer-events: auto; }
|
||||
.products-track {
|
||||
position: absolute; top: 0; left: 0; height: 100%;
|
||||
display: flex; align-items: center;
|
||||
gap: clamp(20px, 2.4vw, 40px);
|
||||
padding-left: clamp(24px, 5vw, 68px);
|
||||
padding-right: clamp(24px, 5vw, 68px);
|
||||
padding-top: var(--pt);
|
||||
padding-bottom: var(--pb);
|
||||
box-sizing: border-box;
|
||||
will-change: transform;
|
||||
}
|
||||
.products-progress-wrap {
|
||||
position: absolute; bottom: clamp(24px, 4vh, 44px);
|
||||
left: 50%; transform: translateX(-50%); z-index: 20;
|
||||
width: min(420px, 70vw);
|
||||
display: flex; align-items: center; gap: 14px;
|
||||
}
|
||||
|
||||
/* Mobile / small tablet: native horizontal swipe, no pin */
|
||||
@media (max-width: 900px) {
|
||||
.products-section-root {
|
||||
height: auto;
|
||||
overflow: visible;
|
||||
padding-bottom: 28px;
|
||||
}
|
||||
.products-heading {
|
||||
position: relative;
|
||||
flex-direction: column; align-items: flex-start; gap: 16px;
|
||||
padding: clamp(40px, 8vw, 64px) 24px 24px;
|
||||
}
|
||||
.products-heading-hint { text-align: left !important; }
|
||||
.products-track {
|
||||
position: relative; height: auto;
|
||||
padding-top: 8px; padding-bottom: 8px;
|
||||
overflow-x: auto; overflow-y: hidden;
|
||||
scroll-snap-type: x mandatory;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
.products-track::-webkit-scrollbar { display: none; }
|
||||
.products-track { scrollbar-width: none; }
|
||||
.products-card { height: 560px; width: min(86vw, 460px); }
|
||||
.products-card { flex-direction: column !important; }
|
||||
.products-card-content { flex: 1 1 auto !important; }
|
||||
.products-card-visual { flex: 0 0 200px !important; border-left: none !important; border-top: 1px solid rgba(255,255,255,0.08); }
|
||||
.products-progress-wrap { position: relative; bottom: auto; transform: none; left: auto; margin: 20px auto 0; }
|
||||
}
|
||||
`}</style>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
344
src/components/home/TheLoop.tsx
Normal file
344
src/components/home/TheLoop.tsx
Normal file
@@ -0,0 +1,344 @@
|
||||
'use client';
|
||||
|
||||
import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
||||
import { gsap } from 'gsap';
|
||||
import { ScrollTrigger } from 'gsap/ScrollTrigger';
|
||||
import AnimatedDarkBg from './AnimatedDarkBg';
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
gsap.registerPlugin(ScrollTrigger);
|
||||
}
|
||||
|
||||
const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;
|
||||
|
||||
interface Step {
|
||||
step: string;
|
||||
id: 'earn' | 'engage' | 'spend' | 'repeat';
|
||||
title: string;
|
||||
desc: string;
|
||||
}
|
||||
|
||||
const STEPS: Step[] = [
|
||||
{ step: '01', id: 'earn', title: 'Earn Online', desc: 'Daily login, 5,000-step walking challenges, and interactive games like Maze and Spin Wheel earn Loyaly Coins and unlock surprise coupons.' },
|
||||
{ step: '02', id: 'engage', title: 'Unlock Deals', desc: 'Merchants create game-based discount campaigns — customers play once to reveal their exclusive reward on a specific product.' },
|
||||
{ step: '03', id: 'spend', title: 'Discover & Visit', desc: 'Browse participating stores nearby, view active game campaigns and coin-earning offers, then walk in and shop in person.' },
|
||||
{ step: '04', id: 'repeat', title: 'Redeem & Repeat', desc: 'Use Loyaly Coins and coupons at checkout to save money — then come back tomorrow to earn more and keep the loop going.' },
|
||||
];
|
||||
|
||||
const IMPACTS = [
|
||||
{ label: 'Conversion Rates', value: 42, suffix: '%', prefix: '+', decimals: 0 },
|
||||
{ label: 'Repeat Customers', value: 3.8, suffix: 'x', prefix: '+', decimals: 1 },
|
||||
{ label: 'Discount Optimization', value: 28, suffix: '%', prefix: '+', decimals: 0 },
|
||||
{ label: 'Real-Time Decisions', value: 500, suffix: 'ms', prefix: '<', decimals: 0 },
|
||||
];
|
||||
|
||||
function StepIcon({ id }: { id: Step['id'] }) {
|
||||
const p = { fill: 'none', stroke: 'currentColor', strokeWidth: 1.7, strokeLinecap: 'round' as const, strokeLinejoin: 'round' as const };
|
||||
if (id === 'earn') return (
|
||||
<svg viewBox="0 0 24 24" {...p} style={{ width: '100%', height: '100%' }}>
|
||||
<circle cx="12" cy="12" r="9" />
|
||||
<path d="M14.5 9a2.5 2.5 0 00-5 0c0 2.5 5 2.5 5 5a2.5 2.5 0 01-5 0" />
|
||||
<path d="M12 6.5V5M12 19v-1.5" />
|
||||
</svg>
|
||||
);
|
||||
if (id === 'engage') return (
|
||||
<svg viewBox="0 0 24 24" {...p} style={{ width: '100%', height: '100%' }}>
|
||||
<path d="M3 9l9-7 9 7v11a2 2 0 01-2 2H5a2 2 0 01-2-2z" />
|
||||
<polyline points="9 22 9 12 15 12 15 22" />
|
||||
</svg>
|
||||
);
|
||||
if (id === 'spend') return (
|
||||
<svg viewBox="0 0 24 24" {...p} style={{ width: '100%', height: '100%' }}>
|
||||
<rect x="1.5" y="4" width="21" height="16" rx="2" />
|
||||
<line x1="1.5" y1="9.5" x2="22.5" y2="9.5" />
|
||||
</svg>
|
||||
);
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" {...p} style={{ width: '100%', height: '100%' }}>
|
||||
<polyline points="17 1 21 5 17 9" />
|
||||
<path d="M3 11V9a4 4 0 014-4h14" />
|
||||
<polyline points="7 23 3 19 7 15" />
|
||||
<path d="M21 13v2a4 4 0 01-4 4H3" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default function TheLoop() {
|
||||
const [active, setActive] = useState(0);
|
||||
const [counts, setCounts] = useState<number[]>(IMPACTS.map(() => 0));
|
||||
const [revealed, setRevealed] = useState(false);
|
||||
const sectionRef = useRef<HTMLDivElement>(null);
|
||||
const metricsRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Sequential highlight cycle (client-only → no hydration mismatch)
|
||||
useEffect(() => {
|
||||
const t = setInterval(() => setActive(a => (a + 1) % STEPS.length), 1900);
|
||||
return () => clearInterval(t);
|
||||
}, []);
|
||||
|
||||
// Scroll-triggered reveals + count-up, all via GSAP ScrollTrigger
|
||||
useIsomorphicLayoutEffect(() => {
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
const ctx = gsap.context(() => {
|
||||
// Reveal the flow diagram cards on scroll-in
|
||||
ScrollTrigger.create({
|
||||
trigger: '.loop-diagram',
|
||||
start: 'top 78%',
|
||||
once: true,
|
||||
onEnter: () => setRevealed(true),
|
||||
});
|
||||
|
||||
// Subtle parallax drift on the diagram + metrics as the section scrolls
|
||||
gsap.fromTo('.loop-diagram', { y: 40 }, {
|
||||
y: -40, ease: 'none',
|
||||
scrollTrigger: { trigger: '.loop-diagram', start: 'top bottom', end: 'bottom top', scrub: 1 },
|
||||
});
|
||||
|
||||
// Count-up metrics when they scroll into view
|
||||
ScrollTrigger.create({
|
||||
trigger: metricsRef.current,
|
||||
start: 'top 82%',
|
||||
once: true,
|
||||
onEnter: () => {
|
||||
const start = performance.now();
|
||||
const dur = 1800;
|
||||
const tick = (now: number) => {
|
||||
const t = Math.min((now - start) / dur, 1);
|
||||
const ease = 1 - Math.pow(1 - t, 3);
|
||||
setCounts(IMPACTS.map(m => m.value * ease));
|
||||
if (t < 1) requestAnimationFrame(tick);
|
||||
};
|
||||
requestAnimationFrame(tick);
|
||||
},
|
||||
});
|
||||
|
||||
// Metric cells stagger in
|
||||
gsap.fromTo('.loop-metric', { opacity: 0, y: 26 }, {
|
||||
opacity: 1, y: 0, duration: 0.7, stagger: 0.1, ease: 'power3.out',
|
||||
scrollTrigger: { trigger: metricsRef.current, start: 'top 82%', toggleActions: 'play none none reset' },
|
||||
});
|
||||
|
||||
ScrollTrigger.refresh();
|
||||
}, sectionRef);
|
||||
|
||||
return () => ctx.revert();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<section
|
||||
ref={sectionRef}
|
||||
id="the-loop"
|
||||
style={{
|
||||
position: 'relative',
|
||||
width: 'calc(100% - 40px)',
|
||||
margin: '0 20px 20px',
|
||||
borderRadius: '32px',
|
||||
overflow: 'hidden',
|
||||
zIndex: 2,
|
||||
padding: 'clamp(80px, 10vh, 140px) clamp(28px, 5vw, 80px) clamp(72px, 8vh, 120px)',
|
||||
}}
|
||||
>
|
||||
<AnimatedDarkBg variant="amber" />
|
||||
|
||||
{/* ── Header ── */}
|
||||
<div style={{ position: 'relative', zIndex: 10, textAlign: 'center', maxWidth: 760, margin: '0 auto' }}>
|
||||
<div style={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 8, marginBottom: 24,
|
||||
padding: '7px 18px', borderRadius: 999,
|
||||
background: 'rgba(244,190,40,0.08)', border: '1px solid rgba(244,190,40,0.22)',
|
||||
}}>
|
||||
<span style={{ width: 6, height: 6, borderRadius: '50%', background: '#f4be28', display: 'block' }} />
|
||||
<span style={{ fontSize: 11, fontWeight: 700, letterSpacing: '0.18em', textTransform: 'uppercase', color: '#f4be28', fontFamily: 'Manrope, sans-serif' }}>The Loop</span>
|
||||
</div>
|
||||
|
||||
<h2 style={{
|
||||
margin: '0 0 18px',
|
||||
fontSize: 'clamp(2.6rem, 6vw, 5rem)',
|
||||
fontWeight: 900, color: '#fff',
|
||||
lineHeight: 0.92, letterSpacing: '-0.05em',
|
||||
fontFamily: 'Sora, sans-serif',
|
||||
}}>
|
||||
A smarter<br />
|
||||
<span style={{
|
||||
background: 'linear-gradient(105deg,#f4be28 0%,#fff8e0 48%,#f4be28 62%,#ff9500 100%)',
|
||||
backgroundSize: '200% auto', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent',
|
||||
animation: 'loopShimmer 3s linear infinite',
|
||||
}}>retail cycle.</span>
|
||||
</h2>
|
||||
|
||||
<p style={{
|
||||
margin: '0 auto', maxWidth: 480,
|
||||
fontSize: 'clamp(0.95rem, 1.1vw, 1.05rem)',
|
||||
color: 'rgba(255,255,255,0.42)', lineHeight: 1.8,
|
||||
fontFamily: 'Manrope, sans-serif',
|
||||
}}>
|
||||
Earn coins online, unlock game-based deals, discover nearby stores, and redeem offline — a self-reinforcing loop that keeps customers engaged and drives repeat footfall.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* ── Flow diagram ── */}
|
||||
<div className="loop-diagram" style={{
|
||||
position: 'relative', zIndex: 10,
|
||||
maxWidth: 1180, margin: 'clamp(140px, 16vh, 200px) auto 0',
|
||||
}}>
|
||||
{/* Loop-back arc (desktop) */}
|
||||
<svg className="loop-arc" viewBox="0 0 1000 120" preserveAspectRatio="none" aria-hidden style={{
|
||||
position: 'absolute', top: -64, left: '11%', width: '78%', height: 70, overflow: 'visible',
|
||||
}}>
|
||||
<defs>
|
||||
<linearGradient id="loopArcGrad" x1="0" y1="0" x2="1" y2="0">
|
||||
<stop offset="0%" stopColor="rgba(244,190,40,0)" />
|
||||
<stop offset="50%" stopColor="rgba(244,190,40,0.55)" />
|
||||
<stop offset="100%" stopColor="rgba(244,190,40,0)" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path d="M 985 118 C 985 20, 800 8, 500 8 C 200 8, 15 20, 15 118"
|
||||
fill="none" stroke="url(#loopArcGrad)" strokeWidth="2"
|
||||
strokeDasharray="7 9" strokeLinecap="round"
|
||||
style={{ animation: revealed ? 'loopDash 1.4s linear infinite' : 'none' }} />
|
||||
<path d="M 8 104 L 15 120 L 22 104" fill="none" stroke="rgba(244,190,40,0.6)" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
|
||||
{/* Center "continuous" label on the arc */}
|
||||
<div className="loop-arc-label" style={{
|
||||
position: 'absolute', top: -78, left: '50%', transform: 'translateX(-50%)',
|
||||
fontSize: 10, fontWeight: 700, letterSpacing: '0.2em', textTransform: 'uppercase',
|
||||
color: 'rgba(244,190,40,0.7)', fontFamily: 'Manrope, sans-serif',
|
||||
background: 'rgba(244,190,40,0.06)', border: '1px solid rgba(244,190,40,0.18)',
|
||||
padding: '5px 14px', borderRadius: 999, whiteSpace: 'nowrap',
|
||||
}}>
|
||||
↻ Continuous loop
|
||||
</div>
|
||||
|
||||
{/* Steps row */}
|
||||
<div className="loop-row" style={{ display: 'flex', alignItems: 'stretch', gap: 0 }}>
|
||||
{STEPS.map((s, i) => {
|
||||
const isActive = i === active;
|
||||
return (
|
||||
<React.Fragment key={s.id}>
|
||||
<div
|
||||
className="loop-card"
|
||||
style={{
|
||||
flex: 1,
|
||||
position: 'relative',
|
||||
padding: 'clamp(26px, 2.2vw, 38px) clamp(20px, 1.8vw, 28px)',
|
||||
borderRadius: 26,
|
||||
background: isActive ? 'rgba(244,190,40,0.07)' : 'rgba(255,255,255,0.025)',
|
||||
border: `1px solid ${isActive ? 'rgba(244,190,40,0.45)' : 'rgba(255,255,255,0.07)'}`,
|
||||
backdropFilter: 'blur(20px)',
|
||||
WebkitBackdropFilter: 'blur(20px)',
|
||||
boxShadow: isActive
|
||||
? '0 28px 80px rgba(244,190,40,0.2), inset 0 1px 0 rgba(255,255,255,0.08)'
|
||||
: 'inset 0 1px 0 rgba(255,255,255,0.04)',
|
||||
transform: revealed ? (isActive ? 'translateY(-10px) scale(1.02)' : 'translateY(0) scale(1)') : 'translateY(48px) scale(0.98)',
|
||||
opacity: revealed ? 1 : 0,
|
||||
transition: `transform 0.7s cubic-bezier(0.22,1,0.36,1) ${i * 0.1}s, opacity 0.7s ease ${i * 0.1}s, background 0.5s ease, border-color 0.5s ease, box-shadow 0.5s ease`,
|
||||
}}
|
||||
>
|
||||
{/* big ghost number */}
|
||||
<span style={{
|
||||
position: 'absolute', top: 8, right: 16,
|
||||
fontSize: 'clamp(3rem, 5vw, 5rem)', fontWeight: 900, lineHeight: 1,
|
||||
color: 'transparent', WebkitTextStroke: `1px rgba(244,190,40,${isActive ? 0.2 : 0.06})`,
|
||||
fontFamily: 'Sora, sans-serif', letterSpacing: '-0.05em',
|
||||
pointerEvents: 'none', userSelect: 'none', transition: 'all 0.5s ease',
|
||||
}}>{s.step}</span>
|
||||
|
||||
{/* icon */}
|
||||
<div style={{
|
||||
width: 56, height: 56, borderRadius: 16, padding: 13, marginBottom: 22,
|
||||
background: isActive ? 'rgba(244,190,40,0.16)' : 'rgba(244,190,40,0.08)',
|
||||
border: `1px solid rgba(244,190,40,${isActive ? 0.4 : 0.2})`,
|
||||
color: '#f4be28',
|
||||
boxShadow: isActive ? '0 0 30px rgba(244,190,40,0.35)' : 'none',
|
||||
transition: 'all 0.5s ease',
|
||||
position: 'relative', zIndex: 2,
|
||||
}}>
|
||||
<StepIcon id={s.id} />
|
||||
</div>
|
||||
|
||||
<div style={{ fontSize: 10, fontWeight: 700, letterSpacing: '0.16em', textTransform: 'uppercase', color: 'rgba(244,190,40,0.7)', fontFamily: 'Manrope, sans-serif', marginBottom: 8 }}>
|
||||
Step {s.step}
|
||||
</div>
|
||||
<h3 style={{ margin: '0 0 10px', fontSize: 'clamp(1.15rem, 1.5vw, 1.5rem)', fontWeight: 800, color: '#fff', letterSpacing: '-0.02em', lineHeight: 1.1, fontFamily: 'Sora, sans-serif' }}>
|
||||
{s.title}
|
||||
</h3>
|
||||
<p style={{ margin: 0, fontSize: '0.86rem', lineHeight: 1.65, color: 'rgba(255,255,255,0.42)', fontFamily: 'Manrope, sans-serif' }}>
|
||||
{s.desc}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* connector arrow between cards */}
|
||||
{i < STEPS.length - 1 && (
|
||||
<div className="loop-connector" style={{
|
||||
flexShrink: 0, alignSelf: 'center',
|
||||
width: 'clamp(28px, 3vw, 56px)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
position: 'relative',
|
||||
}}>
|
||||
<div style={{
|
||||
position: 'absolute', left: 0, right: 0, height: 2, borderRadius: 2,
|
||||
background: 'linear-gradient(90deg, rgba(244,190,40,0.1), rgba(244,190,40,0.45), rgba(244,190,40,0.1))',
|
||||
backgroundSize: '200% 100%',
|
||||
animation: revealed ? 'loopFlow 1.8s linear infinite' : 'none',
|
||||
}} />
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#f4be28" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" style={{ position: 'relative', zIndex: 2, opacity: 0.8 }}>
|
||||
<path d="M5 12h14M13 6l6 6-6 6" />
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Impact metrics ── */}
|
||||
<div ref={metricsRef} style={{ position: 'relative', zIndex: 10, maxWidth: 1000, margin: 'clamp(64px, 8vh, 110px) auto 0' }}>
|
||||
<div style={{ textAlign: 'center', marginBottom: 36 }}>
|
||||
<span style={{ fontSize: 11, fontWeight: 700, letterSpacing: '0.2em', textTransform: 'uppercase', color: 'rgba(255,255,255,0.35)', fontFamily: 'Manrope, sans-serif' }}>
|
||||
Measured Impact
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(190px, 1fr))', gap: 2, borderRadius: 24, overflow: 'hidden' }}>
|
||||
{IMPACTS.map((m, i) => (
|
||||
<div key={m.label} className="loop-metric" style={{
|
||||
padding: 'clamp(28px, 3vw, 44px) 28px',
|
||||
background: 'rgba(255,255,255,0.025)',
|
||||
borderRight: i < IMPACTS.length - 1 ? '1px solid rgba(255,255,255,0.06)' : 'none',
|
||||
textAlign: 'center',
|
||||
}}>
|
||||
<div style={{
|
||||
fontSize: 'clamp(2.4rem, 4vw, 3.4rem)', fontWeight: 900,
|
||||
color: '#f4be28', lineHeight: 1, letterSpacing: '-0.04em',
|
||||
fontVariantNumeric: 'tabular-nums', fontFamily: 'Sora, sans-serif',
|
||||
}}>
|
||||
<span style={{ fontSize: '55%', verticalAlign: 'middle', marginRight: 1 }}>{m.prefix}</span>
|
||||
{counts[i].toFixed(m.decimals)}
|
||||
<span style={{ fontSize: '50%', verticalAlign: 'middle', marginLeft: 1 }}>{m.suffix}</span>
|
||||
</div>
|
||||
<div style={{ fontSize: 13, fontWeight: 600, color: 'rgba(255,255,255,0.5)', fontFamily: 'Manrope, sans-serif', marginTop: 12, lineHeight: 1.4 }}>
|
||||
{m.label}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>{`
|
||||
@keyframes loopShimmer { 0%{background-position:200% center} 100%{background-position:-200% center} }
|
||||
@keyframes loopDash { to { stroke-dashoffset: -32; } }
|
||||
@keyframes loopFlow { 0%{background-position:200% 0} 100%{background-position:-200% 0} }
|
||||
@media (max-width: 880px) {
|
||||
.loop-row { flex-direction: column; }
|
||||
.loop-connector { width: 100% !important; height: 40px; transform: rotate(90deg); }
|
||||
.loop-arc, .loop-arc-label { display: none !important; }
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
#the-loop [style*="animation"] { animation: none !important; }
|
||||
}
|
||||
`}</style>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
376
src/components/home/WhatWeDo.tsx
Normal file
376
src/components/home/WhatWeDo.tsx
Normal file
@@ -0,0 +1,376 @@
|
||||
'use client';
|
||||
|
||||
import React, { useEffect, useLayoutEffect, useRef, useState, useCallback } from 'react';
|
||||
import { gsap } from 'gsap';
|
||||
import { ScrollTrigger } from 'gsap/ScrollTrigger';
|
||||
import { DEFAULT_EASE, DEFAULT_DURATION } from '@/utils/animation';
|
||||
import AnimatedDarkBg from './AnimatedDarkBg';
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
gsap.registerPlugin(ScrollTrigger);
|
||||
}
|
||||
|
||||
const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;
|
||||
|
||||
interface CardProps {
|
||||
num: string;
|
||||
title: string;
|
||||
description: string;
|
||||
icon: React.ReactNode;
|
||||
cardRef: React.RefObject<HTMLDivElement | null>;
|
||||
orderClass: string;
|
||||
}
|
||||
|
||||
function StageCard({ num, title, description, icon, cardRef, orderClass }: CardProps) {
|
||||
return (
|
||||
<div
|
||||
ref={cardRef}
|
||||
className={`wwd-card relative group ${orderClass}`}
|
||||
style={{
|
||||
background: 'rgba(255,255,255,0.025)',
|
||||
backdropFilter: 'blur(24px) saturate(160%)',
|
||||
WebkitBackdropFilter: 'blur(24px) saturate(160%)',
|
||||
border: '1px solid rgba(255,255,255,0.08)',
|
||||
borderRadius: '28px',
|
||||
padding: 'clamp(30px, 3vw, 48px)',
|
||||
transition: 'transform 0.5s cubic-bezier(0.22,1,0.36,1), box-shadow 0.5s ease, border-color 0.5s ease',
|
||||
cursor: 'default',
|
||||
overflow: 'hidden',
|
||||
boxShadow: 'inset 0 1px 0 rgba(255,255,255,0.05)',
|
||||
}}
|
||||
onMouseEnter={e => {
|
||||
(e.currentTarget as HTMLElement).style.transform = 'translateY(-8px)';
|
||||
(e.currentTarget as HTMLElement).style.boxShadow = '0 32px 80px rgba(244, 190, 40,0.12), 0 0 0 1px rgba(244, 190, 40,0.25), inset 0 1px 0 rgba(255,255,255,0.08)';
|
||||
(e.currentTarget as HTMLElement).style.borderColor = 'rgba(244, 190, 40,0.3)';
|
||||
}}
|
||||
onMouseLeave={e => {
|
||||
(e.currentTarget as HTMLElement).style.transform = 'translateY(0)';
|
||||
(e.currentTarget as HTMLElement).style.boxShadow = 'inset 0 1px 0 rgba(255,255,255,0.05)';
|
||||
(e.currentTarget as HTMLElement).style.borderColor = 'rgba(255,255,255,0.08)';
|
||||
}}
|
||||
>
|
||||
{/* Giant editorial number watermark */}
|
||||
<div style={{
|
||||
position: 'absolute', top: '-24px', right: '8px',
|
||||
fontSize: 'clamp(8rem, 11vw, 13rem)', fontWeight: 900, lineHeight: 1,
|
||||
color: 'transparent',
|
||||
WebkitTextStroke: '1px rgba(244, 190, 40,0.06)',
|
||||
fontFamily: 'Sora, sans-serif',
|
||||
letterSpacing: '-0.06em',
|
||||
userSelect: 'none', pointerEvents: 'none',
|
||||
}}>
|
||||
{num}
|
||||
</div>
|
||||
|
||||
{/* Icon */}
|
||||
<div style={{
|
||||
width: '56px', height: '56px',
|
||||
borderRadius: '16px',
|
||||
background: 'rgba(244, 190, 40,0.1)',
|
||||
border: '1px solid rgba(244, 190, 40,0.22)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
marginBottom: '32px',
|
||||
position: 'relative', zIndex: 2,
|
||||
boxShadow: 'inset 0 1px 0 rgba(255,255,255,0.08)',
|
||||
}}>
|
||||
{icon}
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
<h3 style={{
|
||||
margin: '0 0 16px',
|
||||
fontSize: 'clamp(1.5rem, 2.2vw, 2rem)',
|
||||
fontWeight: 800, color: '#ffffff',
|
||||
lineHeight: 1.1, letterSpacing: '-0.03em',
|
||||
fontFamily: 'Sora, sans-serif',
|
||||
position: 'relative', zIndex: 2,
|
||||
}}>
|
||||
{title}
|
||||
</h3>
|
||||
|
||||
{/* Description */}
|
||||
<p style={{
|
||||
margin: 0,
|
||||
fontSize: 'clamp(0.9rem, 1vw, 1rem)',
|
||||
color: 'rgba(255,255,255,0.45)',
|
||||
lineHeight: 1.75, maxWidth: '360px',
|
||||
fontFamily: 'Manrope, sans-serif',
|
||||
position: 'relative', zIndex: 2,
|
||||
}}>
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface WhatWeDoProps {
|
||||
isLoaded?: boolean;
|
||||
}
|
||||
|
||||
export default function WhatWeDo({ isLoaded = false }: WhatWeDoProps) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const gridContainerRef = useRef<HTMLDivElement>(null);
|
||||
const card1Ref = useRef<HTMLDivElement>(null);
|
||||
const card2Ref = useRef<HTMLDivElement>(null);
|
||||
const card3Ref = useRef<HTMLDivElement>(null);
|
||||
const card4Ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [points, setPoints] = useState({
|
||||
p1: { x: 0, y: 0 },
|
||||
p2: { x: 0, y: 0 },
|
||||
p3: { x: 0, y: 0 },
|
||||
p4: { x: 0, y: 0 }
|
||||
});
|
||||
|
||||
const [hasMeasured, setHasMeasured] = useState(false);
|
||||
|
||||
const updatePoints = useCallback(() => {
|
||||
if (!gridContainerRef.current || !card1Ref.current || !card2Ref.current || !card3Ref.current || !card4Ref.current) return;
|
||||
|
||||
const containerRect = gridContainerRef.current.getBoundingClientRect();
|
||||
const r1 = card1Ref.current.getBoundingClientRect();
|
||||
const r2 = card2Ref.current.getBoundingClientRect();
|
||||
const r3 = card3Ref.current.getBoundingClientRect();
|
||||
const r4 = card4Ref.current.getBoundingClientRect();
|
||||
|
||||
// Check that width is non-zero to verify the elements have rendered and have dimensions
|
||||
if (r1.width === 0 || r2.width === 0 || r3.width === 0 || r4.width === 0) return;
|
||||
|
||||
setPoints({
|
||||
p1: {
|
||||
x: r1.left - containerRect.left + r1.width / 2,
|
||||
y: r1.top - containerRect.top + r1.height / 2
|
||||
},
|
||||
p2: {
|
||||
x: r2.left - containerRect.left + r2.width / 2,
|
||||
y: r2.top - containerRect.top + r2.height / 2
|
||||
},
|
||||
p3: {
|
||||
x: r3.left - containerRect.left + r3.width / 2,
|
||||
y: r3.top - containerRect.top + r3.height / 2
|
||||
},
|
||||
p4: {
|
||||
x: r4.left - containerRect.left + r4.width / 2,
|
||||
y: r4.top - containerRect.top + r4.height / 2
|
||||
}
|
||||
});
|
||||
setHasMeasured(true);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
// Use ResizeObserver for highly responsive layout tracking
|
||||
const observer = new ResizeObserver(() => {
|
||||
updatePoints();
|
||||
});
|
||||
|
||||
if (gridContainerRef.current) observer.observe(gridContainerRef.current);
|
||||
if (card1Ref.current) observer.observe(card1Ref.current);
|
||||
if (card2Ref.current) observer.observe(card2Ref.current);
|
||||
if (card3Ref.current) observer.observe(card3Ref.current);
|
||||
if (card4Ref.current) observer.observe(card4Ref.current);
|
||||
|
||||
// Initial calculation
|
||||
updatePoints();
|
||||
|
||||
// Secondary delay to ensure clean alignment post-font/css loading
|
||||
const timer = setTimeout(updatePoints, 200);
|
||||
|
||||
window.addEventListener('resize', updatePoints);
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
window.removeEventListener('resize', updatePoints);
|
||||
clearTimeout(timer);
|
||||
};
|
||||
}, [updatePoints]);
|
||||
|
||||
// GSAP animations for stagger entrances on entering viewport
|
||||
useIsomorphicLayoutEffect(() => {
|
||||
if (!isLoaded) return;
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
const ctx = gsap.context(() => {
|
||||
// Header Animation
|
||||
gsap.fromTo(
|
||||
'.wwd-header-el',
|
||||
{ opacity: 0, y: 40 },
|
||||
{
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
duration: DEFAULT_DURATION,
|
||||
stagger: 0.15,
|
||||
ease: DEFAULT_EASE,
|
||||
scrollTrigger: {
|
||||
trigger: '.wwd-header',
|
||||
start: 'top 85%',
|
||||
toggleActions: 'play none none reset'
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Card Stagger Animation
|
||||
gsap.fromTo(
|
||||
'.wwd-card',
|
||||
{ opacity: 0, y: 60 },
|
||||
{
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
duration: DEFAULT_DURATION,
|
||||
stagger: 0.15,
|
||||
ease: DEFAULT_EASE,
|
||||
onComplete: updatePoints,
|
||||
scrollTrigger: {
|
||||
trigger: '.wwd-grid-container',
|
||||
start: 'top 80%',
|
||||
toggleActions: 'play none none reset'
|
||||
}
|
||||
}
|
||||
);
|
||||
}, containerRef);
|
||||
|
||||
return () => ctx.revert();
|
||||
}, [isLoaded, updatePoints]);
|
||||
|
||||
return (
|
||||
<section
|
||||
ref={containerRef}
|
||||
id="what-we-do"
|
||||
className="relative bg-[#0D0D0D] !pt-32 sm:!pt-40 md:!pt-44 pb-24 md:pb-32 px-6 md:px-12 xl:px-16 overflow-hidden select-none"
|
||||
style={{
|
||||
width: 'calc(100% - 40px)',
|
||||
margin: '0 20px 20px',
|
||||
borderRadius: '32px',
|
||||
zIndex: 2,
|
||||
}}
|
||||
>
|
||||
{/* Animated dark background (replaces video) */}
|
||||
<AnimatedDarkBg variant="amber" />
|
||||
|
||||
{/* Header Container */}
|
||||
<div className="wwd-header relative z-10 text-center max-w-4xl mx-auto mb-24 sm:mb-28 md:mb-32 lg:mb-36 flex flex-col items-center">
|
||||
{/* Pill Badge */}
|
||||
<div className="wwd-header-el inline-flex items-center gap-2 px-4 py-1.5 rounded-full border border-white/10 bg-white/5 text-[#f4be28] text-xs font-bold tracking-wider uppercase mb-5">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-[#f4be28] animate-pulse" />
|
||||
What We Do
|
||||
</div>
|
||||
|
||||
{/* Main Heading */}
|
||||
<h2 className="wwd-header-el text-white font-black mb-6" style={{ fontSize: 'clamp(2.8rem, 6vw, 5.5rem)', letterSpacing: '-0.05em', lineHeight: 0.9, fontFamily: 'Sora, sans-serif' }}>
|
||||
Understand.<br />
|
||||
<span className="text-[#f4be28]">Engage. Convert.</span><br />
|
||||
Retain.
|
||||
</h2>
|
||||
|
||||
{/* Subtitle */}
|
||||
<p className="wwd-header-el text-gray-400 text-base sm:text-lg max-w-xl font-medium" style={{ fontFamily: 'Manrope, sans-serif', lineHeight: 1.75 }}>
|
||||
Four stages. One platform. A smarter retail cycle that turns every walk-in into a loyal customer.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Grid and Paths Container */}
|
||||
<div ref={gridContainerRef} className="relative z-10 wwd-grid-container max-w-6xl mx-auto">
|
||||
|
||||
|
||||
|
||||
{/* 2 x 2 Responsive Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 md:gap-16 lg:gap-24 relative z-20 px-2 md:px-0">
|
||||
|
||||
{/* Card 1 — Understand */}
|
||||
<StageCard
|
||||
num="01"
|
||||
title="Understand"
|
||||
description="Track store footfall, customer behavior, and real-time engagement with AI — giving merchants the intelligence to run smarter campaigns and targeted offers that actually convert."
|
||||
cardRef={card1Ref}
|
||||
orderClass="order-1 md:order-1"
|
||||
icon={
|
||||
<svg className="w-6 h-6 text-[#f4be28]" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2.2">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Card 2 — Engage */}
|
||||
<StageCard
|
||||
num="02"
|
||||
title="Engage"
|
||||
description="Customers earn Loyaly Coins through daily login, 5,000-step walking challenges, and interactive games — Maze, Spin Wheel, Scratch Card, and Match Master — keeping them engaged every day."
|
||||
cardRef={card2Ref}
|
||||
orderClass="order-2 md:order-2"
|
||||
icon={
|
||||
<svg className="w-6 h-6 text-[#f4be28]" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2.2">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Card 4 — Retain (positioned bottom-left on desktop via order-3) */}
|
||||
<StageCard
|
||||
num="04"
|
||||
title="Retain"
|
||||
description="Daily rewards, surprise coupons, and game-based offers keep customers returning to earn, play, and redeem — creating a self-reinforcing loyalty loop that compounds value with every visit."
|
||||
cardRef={card4Ref}
|
||||
orderClass="order-4 md:order-3"
|
||||
icon={
|
||||
<svg className="w-6 h-6 text-[#f4be28]" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2.2">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Card 3 — Convert (positioned bottom-right on desktop via order-4) */}
|
||||
<StageCard
|
||||
num="03"
|
||||
title="Convert"
|
||||
description="Merchants create game-based discount campaigns where customers play once to reveal their reward — turning product discovery into an exciting, high-conversion in-store experience."
|
||||
cardRef={card3Ref}
|
||||
orderClass="order-3 md:order-4"
|
||||
icon={
|
||||
<svg className="w-6 h-6 text-[#f4be28]" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2.2">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
|
||||
</div>
|
||||
|
||||
{/* Center Area Ornament */}
|
||||
{hasMeasured && (
|
||||
<div
|
||||
className="absolute z-30 hidden md:flex items-center justify-center"
|
||||
style={{
|
||||
pointerEvents: 'none',
|
||||
left: `${(points.p1.x + points.p2.x + points.p3.x + points.p4.x) / 4}px`,
|
||||
top: `${(points.p1.y + points.p2.y + points.p3.y + points.p4.y) / 4}px`,
|
||||
transform: 'translate(-50%, -50%)'
|
||||
}}
|
||||
>
|
||||
{/* Glowing container */}
|
||||
<div className="relative w-16 h-16 rounded-full bg-[#0D0D0D] border border-[#f4be28]/35 flex items-center justify-center shadow-[0_0_35px_rgba(244, 190, 40,0.22)]">
|
||||
{/* Pulsing outer aura */}
|
||||
<div className="absolute inset-0 rounded-full border border-[#f4be28]/20 animate-ping opacity-60 pointer-events-none" />
|
||||
|
||||
{/* Rotating ↻ Icon */}
|
||||
<svg
|
||||
className="w-7 h-7 text-[#f4be28] animate-[spin_12s_linear_infinite]"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2.5"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user