animation fixed in section
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { MaterialIcon } from "@/components/ui/MaterialIcon";
|
||||
import { fadeUp, scaleIn, staggerContainer, staggerSlow, viewport } from "@/lib/animations";
|
||||
|
||||
// ─── Feature SVG Illustrations ───────────────────────────────────────────────
|
||||
// Flat vector style · Nearle brand palette · no text inside SVGs
|
||||
@@ -428,46 +430,50 @@ export function BusinessFeaturesGrid() {
|
||||
>
|
||||
|
||||
{/* ── LEFT — clean text feature grid, no illustrations ── */}
|
||||
<div>
|
||||
{/* Badge */}
|
||||
<span className="inline-flex items-center rounded-full bg-[#6330D6]/[0.08] px-3.5 py-1 text-xs font-black uppercase tracking-widest text-[#6330D6]">
|
||||
Our features
|
||||
</span>
|
||||
<motion.div
|
||||
variants={staggerContainer}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={viewport}
|
||||
>
|
||||
{/* Badge + Heading */}
|
||||
<motion.div variants={fadeUp}>
|
||||
<span className="inline-flex items-center rounded-full bg-[#6330D6]/[0.08] px-3.5 py-1 text-xs font-black uppercase tracking-widest text-[#6330D6]">
|
||||
Our features
|
||||
</span>
|
||||
<h2
|
||||
className="font-display font-black text-[#16001D] mt-4"
|
||||
style={{
|
||||
fontSize: "clamp(36px, 3.4vw, 60px)",
|
||||
lineHeight: 1.08,
|
||||
letterSpacing: "-0.04em",
|
||||
maxWidth: 780,
|
||||
marginBottom: 0,
|
||||
}}
|
||||
>
|
||||
Move Your Business Forward
|
||||
</h2>
|
||||
<p
|
||||
className="text-[#5E5866]"
|
||||
style={{ fontSize: 18, lineHeight: 1.65, maxWidth: 760, marginTop: 16, marginBottom: 36 }}
|
||||
>
|
||||
Be a part of a community of like-minded people. Brand your business on Nearle and reach out to your neighborhood with ease.
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
{/* Heading */}
|
||||
<h2
|
||||
className="font-display font-black text-[#16001D] mt-4"
|
||||
style={{
|
||||
fontSize: "clamp(36px, 3.4vw, 60px)",
|
||||
lineHeight: 1.08,
|
||||
letterSpacing: "-0.04em",
|
||||
maxWidth: 780,
|
||||
marginBottom: 0,
|
||||
}}
|
||||
>
|
||||
Move Your Business Forward
|
||||
</h2>
|
||||
|
||||
{/* Description */}
|
||||
<p
|
||||
className="text-[#5E5866]"
|
||||
style={{ fontSize: 18, lineHeight: 1.65, maxWidth: 760, marginTop: 16, marginBottom: 36 }}
|
||||
>
|
||||
Be a part of a community of like-minded people. Brand your business on Nearle and reach out to your neighborhood with ease.
|
||||
</p>
|
||||
|
||||
{/* Feature list – check + text only, no illustration columns */}
|
||||
<div
|
||||
{/* Feature list */}
|
||||
<motion.div
|
||||
className="grid grid-cols-1 md:grid-cols-2"
|
||||
style={{ columnGap: 48, rowGap: 32 }}
|
||||
variants={staggerSlow}
|
||||
>
|
||||
{FEATURES.map((feat) => (
|
||||
<div
|
||||
<motion.div
|
||||
key={feat.title}
|
||||
variants={fadeUp}
|
||||
className="grid items-start"
|
||||
style={{ gridTemplateColumns: "30px 1fr", columnGap: 16 }}
|
||||
>
|
||||
{/* Green check icon */}
|
||||
<div
|
||||
className="rounded-full bg-[#DCFCE7] flex items-center justify-center shrink-0"
|
||||
style={{ width: 30, height: 30, marginTop: 3 }}
|
||||
@@ -476,8 +482,6 @@ export function BusinessFeaturesGrid() {
|
||||
<path d="M3.5 8.5L6.5 11.5L12.5 5" stroke="#16A34A" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
{/* Title + description */}
|
||||
<div className="min-w-0">
|
||||
<h4
|
||||
className="font-display font-black text-[#16001D]"
|
||||
@@ -489,30 +493,33 @@ export function BusinessFeaturesGrid() {
|
||||
{feat.descr}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
|
||||
{/* ── RIGHT — 4 cards + illustration strip below ─────── */}
|
||||
<div className="self-start flex flex-col" style={{ gap: 28 }}>
|
||||
|
||||
{/* 2×2 feature benefit cards */}
|
||||
{/* ── RIGHT — 2×2 feature benefit cards ─────── */}
|
||||
<motion.div
|
||||
className="self-start flex flex-col"
|
||||
style={{ gap: 28 }}
|
||||
variants={staggerSlow}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={viewport}
|
||||
>
|
||||
<div className="grid grid-cols-2" style={{ gap: 28 }}>
|
||||
{CARDS.map((card) => {
|
||||
const { CardIllus } = card;
|
||||
return (
|
||||
<div
|
||||
<motion.div
|
||||
key={card.title}
|
||||
variants={scaleIn}
|
||||
className={`relative flex flex-col overflow-hidden border border-[#6330D6]/10 shadow-[0_4px_28px_rgba(99,48,214,0.09)] ${card.bg}`}
|
||||
style={{ borderRadius: 34, padding: 28, minHeight: 330 }}
|
||||
>
|
||||
{/* Top icon */}
|
||||
<div className={`w-9 h-9 rounded-full flex items-center justify-center shrink-0 ${card.iconBg}`}>
|
||||
<MaterialIcon icon={card.iconName} size={18} />
|
||||
</div>
|
||||
|
||||
{/* Illustration */}
|
||||
<div
|
||||
className="flex-1 flex items-center justify-center overflow-hidden"
|
||||
style={{ margin: "8px 0" }}
|
||||
@@ -521,8 +528,6 @@ export function BusinessFeaturesGrid() {
|
||||
<CardIllus />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom label */}
|
||||
<div className="mt-auto shrink-0">
|
||||
<p
|
||||
className={`font-display font-black leading-none tracking-tight ${card.titleColor}`}
|
||||
@@ -537,13 +542,11 @@ export function BusinessFeaturesGrid() {
|
||||
{card.subtitle}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,27 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { MaterialIcon } from "@/components/ui/MaterialIcon";
|
||||
|
||||
const EASE = [0.22, 1, 0.36, 1] as [number, number, number, number];
|
||||
|
||||
// Stagger form contents after the card slides in from right (card enters at delay 0.25, duration 0.65 → visible ~t=0.7s)
|
||||
const formContentStagger = {
|
||||
hidden: {},
|
||||
visible: { transition: { staggerChildren: 0.1, delayChildren: 0.75 } },
|
||||
};
|
||||
|
||||
const fieldVariant = {
|
||||
hidden: { opacity: 0, y: 14 },
|
||||
visible: { opacity: 1, y: 0, transition: { duration: 0.45, ease: EASE } },
|
||||
};
|
||||
|
||||
const submitVariant = {
|
||||
hidden: { opacity: 0, y: 10, scale: 0.98 },
|
||||
visible: { opacity: 1, y: 0, scale: 1, transition: { duration: 0.4, ease: EASE } },
|
||||
};
|
||||
|
||||
type FormData = {
|
||||
name: string;
|
||||
email: string;
|
||||
@@ -100,16 +119,21 @@ export function ContactForm() {
|
||||
<div className="absolute top-0 right-0 w-32 h-32 bg-primary/5 rounded-full blur-3xl" />
|
||||
<div className="absolute bottom-0 left-0 w-32 h-32 bg-secondary/5 rounded-full blur-2xl" />
|
||||
|
||||
<div className="relative z-10">
|
||||
<span className="text-label-sm font-black text-primary uppercase tracking-widest">
|
||||
<motion.div
|
||||
className="relative z-10"
|
||||
variants={formContentStagger}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
>
|
||||
<motion.span variants={fieldVariant} className="text-label-sm font-black text-primary uppercase tracking-widest block">
|
||||
Write to us
|
||||
</span>
|
||||
<h2 className="font-display text-2xl md:text-3xl font-black text-brand-dark mt-2 mb-8">
|
||||
</motion.span>
|
||||
<motion.h2 variants={fieldVariant} className="font-display text-2xl md:text-3xl font-black text-brand-dark mt-2 mb-8">
|
||||
Send Message
|
||||
</h2>
|
||||
</motion.h2>
|
||||
|
||||
<form onSubmit={handleSubmit} className="flex flex-col gap-5">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
|
||||
<motion.div variants={fieldVariant} className="grid grid-cols-1 md:grid-cols-2 gap-5">
|
||||
{/* Name field */}
|
||||
<div className="flex flex-col">
|
||||
<label htmlFor="field-name" className="text-label-sm text-on-surface-variant font-bold mb-2">
|
||||
@@ -171,10 +195,10 @@ export function ContactForm() {
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Message field */}
|
||||
<div className="flex flex-col">
|
||||
<motion.div variants={fieldVariant} className="flex flex-col">
|
||||
<label htmlFor="field-message" className="text-label-sm text-on-surface-variant font-bold mb-2">
|
||||
Message
|
||||
</label>
|
||||
@@ -202,28 +226,30 @@ export function ContactForm() {
|
||||
{errors.message}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Submit button */}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className="w-full bg-[#6330D6] hover:bg-[#5225b8] disabled:bg-[#6330D6]/60 text-on-primary font-black px-8 py-4 rounded-xl text-label-md hover:shadow-lg transition-all duration-300 hover:-translate-y-0.5 active:translate-y-0 active:scale-[0.98] flex items-center justify-center gap-2 cursor-pointer"
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<>
|
||||
<span className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin" />
|
||||
<span>Sending Message...</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<MaterialIcon icon="send" size={18} />
|
||||
<span>Submit Message</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
{/* Submit button — appears last */}
|
||||
<motion.div variants={submitVariant}>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className="w-full bg-[#6330D6] hover:bg-[#5225b8] disabled:bg-[#6330D6]/60 text-on-primary font-black px-8 py-4 rounded-xl text-label-md hover:shadow-lg transition-all duration-300 hover:-translate-y-0.5 active:translate-y-0 active:scale-[0.98] flex items-center justify-center gap-2 cursor-pointer"
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<>
|
||||
<span className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin" />
|
||||
<span>Sending Message...</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<MaterialIcon icon="send" size={18} />
|
||||
<span>Submit Message</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</motion.div>
|
||||
</form>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* ─── Success Toast Notification ─── */}
|
||||
|
||||
71
src/components/delivery/AnimatedTimeline.tsx
Normal file
71
src/components/delivery/AnimatedTimeline.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
type TimelineItem = {
|
||||
step: string;
|
||||
title: string;
|
||||
desc: string;
|
||||
time: string;
|
||||
};
|
||||
|
||||
const EASE = [0.22, 1, 0.36, 1] as [number, number, number, number];
|
||||
|
||||
const containerVariant = {
|
||||
hidden: {},
|
||||
visible: { transition: { staggerChildren: 0.1, delayChildren: 0.15 } },
|
||||
};
|
||||
|
||||
const itemVariant = {
|
||||
hidden: { opacity: 0, x: -24 },
|
||||
visible: { opacity: 1, x: 0, transition: { duration: 0.55, ease: EASE } },
|
||||
};
|
||||
|
||||
const connectorVariant = {
|
||||
hidden: { scaleY: 0 },
|
||||
visible: { scaleY: 1, transition: { duration: 0.9, ease: EASE } },
|
||||
};
|
||||
|
||||
export function AnimatedTimeline({ items }: { items: TimelineItem[] }) {
|
||||
return (
|
||||
<div className="relative max-w-3xl mx-auto">
|
||||
{/* Vertical connector line — animated scaleY from origin-top */}
|
||||
<motion.div
|
||||
className="absolute left-[22px] md:left-[26px] top-8 bottom-8 w-0.5 bg-gradient-to-b from-[#6330D6]/30 via-[#6330D6]/20 to-transparent hidden sm:block origin-top"
|
||||
variants={connectorVariant}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true, amount: 0.1 }}
|
||||
/>
|
||||
|
||||
<motion.div
|
||||
className="space-y-6"
|
||||
variants={containerVariant}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true, amount: 0.1 }}
|
||||
>
|
||||
{items.map((item, index) => (
|
||||
<motion.div
|
||||
key={item.step}
|
||||
className="relative flex gap-5 sm:gap-7"
|
||||
variants={itemVariant}
|
||||
>
|
||||
<div className="flex-shrink-0 w-11 h-11 md:w-12 md:h-12 rounded-full bg-[#6330D6] text-white flex items-center justify-center text-xs font-black shadow-[0_8px_20px_rgba(99,48,214,0.25)] relative z-10">
|
||||
{String(index + 1).padStart(2, "0")}
|
||||
</div>
|
||||
<div className="flex-1 pb-2 pt-1">
|
||||
<div className="flex items-center gap-3 mb-1.5">
|
||||
<h3 className="text-base font-black text-[#16001D] leading-tight">{item.title}</h3>
|
||||
<span className="px-2.5 py-0.5 rounded-full bg-[#6330D6]/8 text-[#6330D6] text-[10px] font-black uppercase tracking-wide shrink-0">
|
||||
{item.time}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-[#5E5866] leading-relaxed">{item.desc}</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
150
src/components/heroes/AboutHero.tsx
Normal file
150
src/components/heroes/AboutHero.tsx
Normal file
@@ -0,0 +1,150 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { motion } from "framer-motion";
|
||||
import { HeroVisualCollage } from "@/components/layout/HeroVisualCollage";
|
||||
import { FeatureStatsRow } from "@/components/ui/FeatureStatsRow";
|
||||
import type { Variants } from "framer-motion";
|
||||
import {
|
||||
fadeInSlow,
|
||||
fadeUp,
|
||||
staggerSlow,
|
||||
viewport,
|
||||
} from "@/lib/animations";
|
||||
|
||||
const HERO_STATS = [
|
||||
{ title: "2km Coverage", icon: "explore", desc: "Hyperlocal radius" },
|
||||
{ title: "Verified Local Stores", icon: "store", desc: "Growing local commerce" },
|
||||
{ title: "Happy Customers", icon: "mood", desc: "Neighbourhood community" },
|
||||
{ title: "Coimbatore & Beyond", icon: "location_city", desc: "Growing neighbourhood reach" },
|
||||
];
|
||||
|
||||
const COLLAGE_CARDS = [
|
||||
{ text: "Local First", icon: "store" },
|
||||
{ text: "Trusted Network", icon: "verified" },
|
||||
{ text: "Connected Communities", icon: "groups" },
|
||||
];
|
||||
|
||||
// Choreography: deliberate, slow-stagger sequence — thoughtful and purposeful
|
||||
// badge fades in (opacity only) → h1 sweeps from left → p fades up → CTAs fade up
|
||||
// The collage scales up softly, reinforcing the "built carefully" feeling
|
||||
const badgeVariant = fadeInSlow;
|
||||
const titleVariant = {
|
||||
hidden: { opacity: 0, x: -30 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
x: 0,
|
||||
transition: { duration: 0.7, ease: [0.22, 1, 0.36, 1] as [number, number, number, number] },
|
||||
},
|
||||
};
|
||||
const descVariant = {
|
||||
hidden: { opacity: 0, y: 20 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
transition: { duration: 0.6, ease: [0.22, 1, 0.36, 1] as [number, number, number, number] },
|
||||
},
|
||||
};
|
||||
const ctaVariant = fadeUp;
|
||||
|
||||
const collageVariant = {
|
||||
hidden: { opacity: 0, scale: 0.92 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
scale: 1,
|
||||
transition: { duration: 0.95, ease: [0.22, 1, 0.36, 1] as [number, number, number, number], delay: 0.2 },
|
||||
},
|
||||
};
|
||||
|
||||
// Trust-feeling card variant: no spring bounce, deliberate easing
|
||||
const trustCardVariant: Variants = {
|
||||
hidden: { opacity: 0, y: 20 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
transition: { duration: 0.7, ease: [0.22, 1, 0.36, 1] as [number, number, number, number] },
|
||||
},
|
||||
};
|
||||
|
||||
export function AboutHero() {
|
||||
return (
|
||||
<section className="w-full flex flex-col pt-6 md:pt-10 select-none bg-gradient-to-br from-white via-[#FEFCFF] to-[#F4ECFA]">
|
||||
<div className="border-b border-[#6330D6]/10">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-[45%_55%] items-center gap-10 lg:gap-6 px-6 md:px-12 lg:px-16 pb-12 max-w-[1600px] mx-auto">
|
||||
|
||||
{/* Left Column: slow, deliberate stagger — thoughtful pacing */}
|
||||
<motion.div
|
||||
className="flex flex-col text-left max-w-xl"
|
||||
variants={staggerSlow}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
>
|
||||
<motion.span
|
||||
variants={badgeVariant}
|
||||
className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-black tracking-widest bg-[#6330D6]/10 text-[#6330D6] uppercase mb-6 w-fit"
|
||||
>
|
||||
About Nearle
|
||||
</motion.span>
|
||||
|
||||
<motion.h1
|
||||
variants={titleVariant}
|
||||
className="font-display text-4xl sm:text-5xl lg:text-[48px] xl:text-[54px] font-black text-[#16001D] leading-[1.1] tracking-tight"
|
||||
>
|
||||
Built for Every
|
||||
<br />
|
||||
<span className="text-[#6330D6]">Neighbourhood.</span>
|
||||
</motion.h1>
|
||||
|
||||
<motion.p
|
||||
variants={descVariant}
|
||||
className="text-sm md:text-base text-[#5E5866] font-bold mt-6 leading-relaxed"
|
||||
>
|
||||
Nearle is India's first hyper-local managed “Neighbourhood living platform” which brings all the amenities and requirements of residents to their fingertips.
|
||||
</motion.p>
|
||||
|
||||
<motion.div
|
||||
variants={ctaVariant}
|
||||
className="flex flex-col sm:flex-row gap-3.5 mt-8 w-full sm:w-auto"
|
||||
>
|
||||
<a
|
||||
href="#story-section"
|
||||
className="bg-[#6330D6] hover:bg-[#5225b8] text-white font-black px-8 py-3.5 rounded-xl text-sm md:text-base text-center shadow-md hover:shadow-lg transition-all duration-200 active:scale-[0.98]"
|
||||
>
|
||||
Our Story
|
||||
</a>
|
||||
<Link
|
||||
href="/contact"
|
||||
className="border-2 border-[#6330D6]/20 hover:border-[#6330D6]/40 text-[#6330D6] font-black px-8 py-3.5 rounded-xl text-sm md:text-base text-center transition-all duration-200 hover:bg-[#6330D6]/5 active:scale-[0.98]"
|
||||
>
|
||||
Join Our Journey
|
||||
</Link>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
|
||||
{/* Right Column: soft scale-up — gentle and community-first */}
|
||||
<motion.div
|
||||
className="w-full"
|
||||
variants={collageVariant}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
>
|
||||
<HeroVisualCollage
|
||||
centerImage="/nearlenewlogo.png"
|
||||
leftImage="/Nearle Business.png"
|
||||
rightImage="/nearle people.png"
|
||||
glassCards={COLLAGE_CARDS}
|
||||
isPhoneCenter={false}
|
||||
cardVariant={trustCardVariant}
|
||||
/>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-[#FAF7FD] border-b border-[#6330D6]/10">
|
||||
<div className="px-6 md:px-12 lg:px-16 max-w-[1600px] mx-auto">
|
||||
<FeatureStatsRow items={HERO_STATS} />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
123
src/components/heroes/BusinessHero.tsx
Normal file
123
src/components/heroes/BusinessHero.tsx
Normal file
@@ -0,0 +1,123 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { motion } from "framer-motion";
|
||||
import { HeroVisualCollage } from "@/components/layout/HeroVisualCollage";
|
||||
import { FeatureStatsRow } from "@/components/ui/FeatureStatsRow";
|
||||
import {
|
||||
slideInLeftSubtle,
|
||||
fadeUp,
|
||||
fadeIn,
|
||||
scaleIn,
|
||||
staggerContainer,
|
||||
viewport,
|
||||
} from "@/lib/animations";
|
||||
|
||||
const HERO_BENEFITS = [
|
||||
{ title: "More Visibility Across Nearle", icon: "visibility", desc: "Get discovered by locals" },
|
||||
{ title: "Easy Order Management", icon: "assignment", desc: "Track and fulfill cleanly" },
|
||||
{ title: "Grow Repeat Customers", icon: "trending_up", desc: "Build neighborhood loyalty" },
|
||||
{ title: "Local Delivery Support", icon: "local_shipping", desc: "Deliver fast & verified" },
|
||||
];
|
||||
|
||||
const COLLAGE_CARDS = [
|
||||
{ text: "Nearby Customers", icon: "people" },
|
||||
{ text: "Orders & Offers", icon: "local_offer" },
|
||||
{ text: "Local Delivery Support", icon: "local_shipping" },
|
||||
];
|
||||
|
||||
// Choreography: badge slides left → title fades up → description fades in (opacity only) → CTAs scale in
|
||||
const badgeVariant = slideInLeftSubtle;
|
||||
const titleVariant = {
|
||||
hidden: { opacity: 0, y: 28 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
transition: { duration: 0.55, ease: [0.22, 1, 0.36, 1] as [number, number, number, number] },
|
||||
},
|
||||
};
|
||||
const descVariant = fadeIn;
|
||||
const ctaVariant = scaleIn;
|
||||
|
||||
export function BusinessHero() {
|
||||
return (
|
||||
<section className="w-full flex flex-col pt-6 md:pt-10 select-none bg-gradient-to-br from-white via-[#FEFCFF] to-[#F4ECFA]">
|
||||
<div className="border-b border-[#6330D6]/10">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-[45%_55%] items-center gap-10 lg:gap-6 px-6 md:px-12 lg:px-16 pb-12 max-w-[1600px] mx-auto">
|
||||
|
||||
{/* Left Column: animated with unique stagger */}
|
||||
<motion.div
|
||||
className="flex flex-col text-left max-w-xl"
|
||||
variants={staggerContainer}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
>
|
||||
<motion.span
|
||||
variants={badgeVariant}
|
||||
className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-black tracking-widest bg-[#6330D6]/10 text-[#6330D6] uppercase mb-6 w-fit"
|
||||
>
|
||||
Nearle for Business
|
||||
</motion.span>
|
||||
|
||||
<motion.h1
|
||||
variants={titleVariant}
|
||||
className="font-display text-4xl sm:text-5xl lg:text-[48px] xl:text-[54px] font-black text-[#16001D] leading-[1.1] tracking-tight"
|
||||
>
|
||||
Grow Your Local Business,
|
||||
<br />
|
||||
<span className="text-[#6330D6]">Neighbourhood by Neighbourhood.</span>
|
||||
</motion.h1>
|
||||
|
||||
<motion.p
|
||||
variants={descVariant}
|
||||
className="text-sm md:text-base text-[#5E5866] font-bold mt-6 leading-relaxed"
|
||||
>
|
||||
Get discovered by nearby customers, manage orders, and build repeat business from one simple platform.
|
||||
</motion.p>
|
||||
|
||||
<motion.div
|
||||
variants={ctaVariant}
|
||||
className="flex flex-col sm:flex-row gap-3.5 mt-8 w-full sm:w-auto"
|
||||
>
|
||||
<Link
|
||||
href="/contact"
|
||||
className="bg-[#6330D6] hover:bg-[#5225b8] text-white font-black px-8 py-3.5 rounded-xl text-sm md:text-base text-center shadow-md hover:shadow-lg transition-all duration-200 active:scale-[0.98]"
|
||||
>
|
||||
List Your Store
|
||||
</Link>
|
||||
<Link
|
||||
href="/contact"
|
||||
className="border-2 border-[#6330D6]/20 hover:border-[#6330D6]/40 text-[#6330D6] font-black px-8 py-3.5 rounded-xl text-sm md:text-base text-center transition-all duration-200 hover:bg-[#6330D6]/5 active:scale-[0.98]"
|
||||
>
|
||||
Explore Solutions
|
||||
</Link>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
|
||||
{/* Right Column: subtle fade-in — HeroVisualCollage internal animations choreograph each element */}
|
||||
<motion.div
|
||||
className="w-full"
|
||||
variants={fadeIn}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
transition={{ delay: 0.2 }}
|
||||
>
|
||||
<HeroVisualCollage
|
||||
centerImage="/nearlenewlogo.png"
|
||||
leftImage="/images/cat-daily-essentials.png"
|
||||
rightImage="/Nearle Business.png"
|
||||
glassCards={COLLAGE_CARDS}
|
||||
isPhoneCenter={false}
|
||||
/>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-[#FAF7FD] border-b border-[#6330D6]/10">
|
||||
<div className="px-6 md:px-12 lg:px-16 max-w-[1600px] mx-auto">
|
||||
<FeatureStatsRow items={HERO_BENEFITS} />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
139
src/components/heroes/ContactHero.tsx
Normal file
139
src/components/heroes/ContactHero.tsx
Normal file
@@ -0,0 +1,139 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import { MaterialIcon } from "@/components/ui/MaterialIcon";
|
||||
import { ContactForm } from "@/components/contact/ContactForm";
|
||||
import { fadeUp, fadeUpStrong, slideInRight, staggerContainer, viewport } from "@/lib/animations";
|
||||
|
||||
const CONTACT_INFO = [
|
||||
{
|
||||
icon: "phone",
|
||||
label: "Call Us",
|
||||
value: "+91 909 206 8666",
|
||||
},
|
||||
{
|
||||
icon: "mail",
|
||||
label: "Email Us",
|
||||
value: "nearle@care.in",
|
||||
},
|
||||
{
|
||||
icon: "location_on",
|
||||
label: "Address",
|
||||
value: "424, DB Rd, R.S. Puram, Coimbatore, TN 641002",
|
||||
},
|
||||
];
|
||||
|
||||
// Choreography: professional, clear sequence
|
||||
// Left column — each info row staggers in from left; h1 has strong upward entry
|
||||
// Form card slides in from right with slight delay
|
||||
// Info rows use a tighter stagger to feel organised and systematic
|
||||
const infoRowVariant = {
|
||||
hidden: { opacity: 0, x: -18 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
x: 0,
|
||||
transition: { duration: 0.45, ease: [0.22, 1, 0.36, 1] as [number, number, number, number] },
|
||||
},
|
||||
};
|
||||
|
||||
const formVariant = {
|
||||
hidden: { opacity: 0, x: 40 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
x: 0,
|
||||
transition: { duration: 0.65, ease: [0.22, 1, 0.36, 1] as [number, number, number, number], delay: 0.25 },
|
||||
},
|
||||
};
|
||||
|
||||
const infoStagger = {
|
||||
hidden: {},
|
||||
visible: { transition: { staggerChildren: 0.08 } },
|
||||
};
|
||||
|
||||
export function ContactHero() {
|
||||
return (
|
||||
<section className="relative border-b border-[#6330D6]/10 px-4 py-10 md:px-6 md:py-12 lg:px-8 lg:py-14 xl:px-10 xl:py-16 2xl:px-12 2xl:py-20">
|
||||
|
||||
{/* Decorative map grid — not animated so it stays subtle */}
|
||||
<div className="absolute left-[5%] top-[10%] w-[400px] h-[300px] opacity-[0.04] select-none pointer-events-none -z-10">
|
||||
<svg className="w-full h-full text-[#6330D6]" stroke="currentColor" fill="none" strokeWidth="1.5" viewBox="0 0 400 300">
|
||||
<path d="M0,50 L400,50 M0,130 L400,130 M0,210 L400,210 M80,0 L80,300 M200,0 L200,300 M320,0 L320,300" />
|
||||
<path d="M 80,130 C 120,90 160,170 200,130 C 240,90 280,170 320,130" strokeDasharray="3,3" />
|
||||
<circle cx="200" cy="130" r="4" fill="currentColor" />
|
||||
<circle cx="200" cy="130" r="14" strokeWidth="1" />
|
||||
<circle cx="80" cy="50" r="3" fill="currentColor" />
|
||||
<circle cx="320" cy="210" r="3" fill="currentColor" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 mx-auto w-full max-w-[1520px]">
|
||||
<div className="grid grid-cols-1 items-start gap-10 lg:grid-cols-[1.2fr_0.8fr] lg:gap-12 xl:gap-16">
|
||||
|
||||
{/* Left Column — Contact Info: sequential stagger from top */}
|
||||
<motion.div
|
||||
className="w-full max-w-[760px] py-6 md:py-8 lg:py-10 relative z-10 text-[#16001D]"
|
||||
variants={staggerContainer}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
>
|
||||
<motion.span
|
||||
variants={fadeUp}
|
||||
className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-black tracking-widest bg-[#6330D6]/10 text-[#6330D6] uppercase mb-6 w-fit"
|
||||
>
|
||||
Contact Us
|
||||
</motion.span>
|
||||
|
||||
<motion.h1
|
||||
variants={fadeUpStrong}
|
||||
className="font-display text-4xl font-black leading-[1.05] text-[#16001D] md:text-5xl lg:text-[58px] xl:text-[64px] 2xl:text-[70px]"
|
||||
>
|
||||
We're Here to <br />
|
||||
<span className="text-[#6330D6]">Help You.</span>
|
||||
</motion.h1>
|
||||
|
||||
<motion.p
|
||||
variants={fadeUp}
|
||||
className="mt-6 max-w-[620px] text-sm font-bold leading-8 text-[#5E5866] md:text-base xl:text-lg"
|
||||
>
|
||||
Whether you are a customer, local store owner, or delivery partner, our team is here to help you get started with Nearle.
|
||||
</motion.p>
|
||||
|
||||
{/* Info rows: each row staggers in from left */}
|
||||
<motion.div
|
||||
className="mt-8 border-t border-[#6330D6]/10"
|
||||
variants={infoStagger}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
>
|
||||
{CONTACT_INFO.map((item) => (
|
||||
<motion.div
|
||||
key={item.label}
|
||||
variants={infoRowVariant}
|
||||
className="flex gap-4 py-4.5 border-b border-[#6330D6]/10 items-center"
|
||||
>
|
||||
<div className="w-10 h-10 rounded-full bg-[#6330D6]/10 text-[#6330D6] flex items-center justify-center shrink-0">
|
||||
<MaterialIcon icon={item.icon} size={18} />
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<h4 className="text-[#16001D] text-sm font-black">{item.label}</h4>
|
||||
<p className="text-[#5E5866] text-sm font-semibold mt-0.5">{item.value}</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
|
||||
{/* Right Column: form slides in from right */}
|
||||
<motion.div
|
||||
className="w-full max-w-[660px] justify-self-end pt-10 lg:pt-14 xl:pt-26"
|
||||
variants={formVariant}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
>
|
||||
<ContactForm />
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
117
src/components/heroes/PeopleHero.tsx
Normal file
117
src/components/heroes/PeopleHero.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { motion } from "framer-motion";
|
||||
import { HeroVisualCollage } from "@/components/layout/HeroVisualCollage";
|
||||
import { FeatureStatsRow } from "@/components/ui/FeatureStatsRow";
|
||||
import {
|
||||
scaleInSpring,
|
||||
fadeUpStrong,
|
||||
fadeUp,
|
||||
slideInLeftSubtle,
|
||||
fadeIn,
|
||||
staggerFast,
|
||||
} from "@/lib/animations";
|
||||
|
||||
const HERO_STATS = [
|
||||
{ title: "Verified Local Stores", icon: "store", desc: "Growing local commerce" },
|
||||
{ title: "2km Neighbourhood", icon: "location_on", desc: "Hyperlocal service radius" },
|
||||
{ title: "Happy Customers", icon: "mood", desc: "Growing neighbourhood community" },
|
||||
{ title: "Trusted Platform", icon: "star", desc: "Loved by local shoppers" },
|
||||
];
|
||||
|
||||
const COLLAGE_CARDS = [
|
||||
{ text: "Discover Nearby", icon: "explore" },
|
||||
{ text: "Save Favourites", icon: "favorite" },
|
||||
{ text: "Order Anytime", icon: "shopping_bag" },
|
||||
];
|
||||
|
||||
// Choreography: badge bounces in (spring) → h1 strong fade-up → p subtle up → CTAs slide from left
|
||||
// Fast stagger (0.07) gives a snappy, energetic feel — phone/fast/mobile-first vibe
|
||||
const badgeVariant = scaleInSpring;
|
||||
const titleVariant = fadeUpStrong;
|
||||
const descVariant = fadeUp;
|
||||
const ctaVariant = slideInLeftSubtle;
|
||||
|
||||
export function PeopleHero() {
|
||||
return (
|
||||
<section className="w-full flex flex-col pt-6 md:pt-10 select-none bg-gradient-to-br from-white via-[#FEFCFF] to-[#F4ECFA]">
|
||||
<div className="border-b border-[#6330D6]/10">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-[45%_55%] items-center gap-10 lg:gap-6 px-6 md:px-12 lg:px-16 pb-12 max-w-[1600px] mx-auto">
|
||||
|
||||
{/* Left Column: snappy fast-stagger sequence */}
|
||||
<motion.div
|
||||
className="flex flex-col text-left max-w-xl"
|
||||
variants={staggerFast}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
>
|
||||
<motion.span
|
||||
variants={badgeVariant}
|
||||
className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-black tracking-widest bg-[#6330D6]/10 text-[#6330D6] uppercase mb-6 w-fit"
|
||||
>
|
||||
Nearle for People
|
||||
</motion.span>
|
||||
|
||||
<motion.h1
|
||||
variants={titleVariant}
|
||||
className="font-display text-4xl sm:text-5xl lg:text-[48px] xl:text-[54px] font-black text-[#16001D] leading-[1.1] tracking-tight"
|
||||
>
|
||||
Your Neighbourhood,
|
||||
<br />
|
||||
Now in Your <span className="text-[#6330D6]">Pocket.</span>
|
||||
</motion.h1>
|
||||
|
||||
<motion.p
|
||||
variants={descVariant}
|
||||
className="text-sm md:text-base text-[#5E5866] font-bold mt-6 leading-relaxed"
|
||||
>
|
||||
Discover verified local stores, save your favorite merchants, and order everyday items with superfast local fulfillment.
|
||||
</motion.p>
|
||||
|
||||
<motion.div
|
||||
variants={ctaVariant}
|
||||
className="flex flex-col sm:flex-row gap-3.5 mt-8 w-full sm:w-auto"
|
||||
>
|
||||
<a
|
||||
href="#download-section"
|
||||
className="bg-[#6330D6] hover:bg-[#5225b8] text-white font-black px-8 py-3.5 rounded-xl text-sm md:text-base text-center shadow-md hover:shadow-lg transition-all duration-200 active:scale-[0.98]"
|
||||
>
|
||||
Get the App
|
||||
</a>
|
||||
<Link
|
||||
href="/"
|
||||
className="border-2 border-[#6330D6]/20 hover:border-[#6330D6]/40 text-[#6330D6] font-black px-8 py-3.5 rounded-xl text-sm md:text-base text-center transition-all duration-200 hover:bg-[#6330D6]/5 active:scale-[0.98]"
|
||||
>
|
||||
Explore Stores
|
||||
</Link>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
|
||||
{/* Right Column: subtle fade — HeroVisualCollage's internal animations handle phone, images, badges individually */}
|
||||
<motion.div
|
||||
className="w-full"
|
||||
variants={fadeIn}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
transition={{ delay: 0.15 }}
|
||||
>
|
||||
<HeroVisualCollage
|
||||
centerImage="/images/app-mockup.png"
|
||||
leftImage="/images/cat-hot-food.png"
|
||||
rightImage="/images/cat-health-wellness.png"
|
||||
glassCards={COLLAGE_CARDS}
|
||||
isPhoneCenter={true}
|
||||
/>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-[#FAF7FD] border-b border-[#6330D6]/10">
|
||||
<div className="px-6 md:px-12 lg:px-16 max-w-[1600px] mx-auto">
|
||||
<FeatureStatsRow items={HERO_STATS} />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -69,8 +69,16 @@ export function BusinessSection() {
|
||||
<section className="section bg-white w-full overflow-hidden">
|
||||
<div className="mx-auto w-full max-w-[2200px] px-2 md:px-4 lg:px-5 xl:px-6">
|
||||
<div className="relative overflow-hidden rounded-[2rem] bg-[linear-gradient(135deg,#ffffff_0%,#fbf7ff_50%,#ede4f5_100%)] border border-primary/10 shadow-[0_24px_70px_rgba(22,0,29,0.08)] p-5 md:p-8 lg:p-10">
|
||||
<div className="pointer-events-none absolute -left-16 top-24 h-64 w-64 rounded-full bg-primary/5 blur-3xl" />
|
||||
<div className="pointer-events-none absolute -right-20 bottom-10 h-64 w-64 rounded-full bg-secondary/5 blur-3xl" />
|
||||
<motion.div
|
||||
className="pointer-events-none absolute -left-16 top-24 h-64 w-64 rounded-full bg-primary/5 blur-3xl"
|
||||
animate={{ y: [0, -16, 0] }}
|
||||
transition={{ duration: 13, repeat: Infinity, ease: "easeInOut" }}
|
||||
/>
|
||||
<motion.div
|
||||
className="pointer-events-none absolute -right-20 bottom-10 h-64 w-64 rounded-full bg-secondary/5 blur-3xl"
|
||||
animate={{ y: [0, 14, 0] }}
|
||||
transition={{ duration: 11, repeat: Infinity, ease: "easeInOut" }}
|
||||
/>
|
||||
|
||||
<div className="relative grid gap-6 grid-cols-1 md:gap-7
|
||||
xl:grid-cols-[420px_1fr] xl:gap-8 xl:items-start">
|
||||
|
||||
@@ -2,14 +2,11 @@
|
||||
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { motion } from "framer-motion";
|
||||
import { motion, useReducedMotion } from "framer-motion";
|
||||
import { MaterialIcon } from "@/components/ui/MaterialIcon";
|
||||
import {
|
||||
fadeUp,
|
||||
slideInRight,
|
||||
staggerContainer,
|
||||
viewport,
|
||||
} from "@/lib/animations";
|
||||
import { viewport } from "@/lib/animations";
|
||||
|
||||
const EASE = [0.22, 1, 0.36, 1] as [number, number, number, number];
|
||||
|
||||
const PROOF_ITEMS = [
|
||||
{ label: "Verified", text: "Local Stores", icon: "store" },
|
||||
@@ -18,16 +15,86 @@ const PROOF_ITEMS = [
|
||||
{ label: "Trusted", text: "Local Platform", icon: "star" },
|
||||
];
|
||||
|
||||
// Left column: badge → line1 mask-reveal → line2 mask-reveal → desc → primary CTA → secondary CTA
|
||||
const heroStagger = {
|
||||
hidden: {},
|
||||
visible: { transition: { staggerChildren: 0.12, delayChildren: 0.05 } },
|
||||
};
|
||||
|
||||
const badgeVariant = {
|
||||
hidden: { opacity: 0, y: 10, scale: 0.95 },
|
||||
visible: { opacity: 1, y: 0, scale: 1, transition: { duration: 0.38, ease: EASE } },
|
||||
};
|
||||
|
||||
// Lines stagger internally
|
||||
const lineStagger = {
|
||||
hidden: {},
|
||||
visible: { transition: { staggerChildren: 0.09 } },
|
||||
};
|
||||
|
||||
// True mask reveal: text slides up from below a clipping parent
|
||||
const lineReveal = {
|
||||
hidden: { y: "110%" },
|
||||
visible: { y: 0, transition: { duration: 0.72, ease: EASE } },
|
||||
};
|
||||
|
||||
const descVariant = {
|
||||
hidden: { opacity: 0, y: 18 },
|
||||
visible: { opacity: 1, y: 0, transition: { duration: 0.55, ease: EASE } },
|
||||
};
|
||||
|
||||
const primaryCtaVariant = {
|
||||
hidden: { opacity: 0, y: 14, scale: 0.97 },
|
||||
visible: { opacity: 1, y: 0, scale: 1, transition: { duration: 0.45, ease: EASE } },
|
||||
};
|
||||
|
||||
const secondaryCtaVariant = {
|
||||
hidden: { opacity: 0, x: -14 },
|
||||
visible: { opacity: 1, x: 0, transition: { duration: 0.4, ease: EASE } },
|
||||
};
|
||||
|
||||
// Right column: each element enters independently
|
||||
const imgFromLeft = {
|
||||
hidden: { opacity: 0, x: -36 },
|
||||
visible: { opacity: 1, x: 0, transition: { duration: 0.65, ease: EASE } },
|
||||
};
|
||||
|
||||
const centerCardReveal = {
|
||||
hidden: { opacity: 0, scale: 0.86, y: 24 },
|
||||
visible: { opacity: 1, scale: 1, y: 0, transition: { duration: 0.75, ease: EASE } },
|
||||
};
|
||||
|
||||
const imgFromRight = {
|
||||
hidden: { opacity: 0, x: 36 },
|
||||
visible: { opacity: 1, x: 0, transition: { duration: 0.65, ease: EASE } },
|
||||
};
|
||||
|
||||
const statStagger = {
|
||||
hidden: {},
|
||||
visible: { transition: { staggerChildren: 0.08 } },
|
||||
};
|
||||
|
||||
const statItem = {
|
||||
hidden: { opacity: 0, y: 14 },
|
||||
visible: { opacity: 1, y: 0, transition: { duration: 0.45, ease: EASE } },
|
||||
};
|
||||
|
||||
export function Hero() {
|
||||
const reducedMotion = useReducedMotion();
|
||||
|
||||
return (
|
||||
<section className="relative overflow-hidden bg-gradient-to-b from-[#FAF8FC] to-[#FFFFFF] select-none w-full">
|
||||
{/* ─── DECORATIONS (BEHIND CONTENT) ─── */}
|
||||
|
||||
{/* 1. Large pale lavender partial circle (top-left) */}
|
||||
<div className="hidden md:block absolute top-[-100px] left-[-100px] w-[350px] h-[350px] rounded-full border-[1.5px] border-[#6D28D9]/15 bg-[#6D28D9]/5 opacity-[0.55] z-0 pointer-events-none" />
|
||||
<motion.div
|
||||
className="hidden md:block absolute top-[-100px] left-[-100px] w-[350px] h-[350px] rounded-full border-[1.5px] border-[#6D28D9]/15 bg-[#6D28D9]/5 opacity-[0.55] z-0 pointer-events-none"
|
||||
animate={{ y: [0, -14, 0] }}
|
||||
transition={{ duration: 14, repeat: Infinity, ease: "easeInOut" }}
|
||||
/>
|
||||
|
||||
{/* 2. Purple solid circle (center-right around collage) */}
|
||||
<div className="hidden lg:block absolute right-[22%] top-[38%] w-[84px] h-[84px] rounded-full bg-[#6D28D9] z-0 shadow-[0_8px_32px_rgba(109,40,217,0.3)] pointer-events-none animate-pulse" />
|
||||
<div className="hidden lg:block absolute right-[22%] top-[38%] w-[84px] h-[84px] rounded-full bg-[#6D28D9] z-0 shadow-[0_8px_32px_rgba(109,40,217,0.3)] pointer-events-none motion-safe:animate-pulse" />
|
||||
|
||||
{/* 3. Striped purple circle (lower-middle collage area) */}
|
||||
<div
|
||||
@@ -53,7 +120,11 @@ export function Hero() {
|
||||
<div className="absolute bottom-[14%] right-[5%] text-[#6D28D9]/25 font-bold text-xl pointer-events-none select-none z-0">✕</div>
|
||||
|
||||
{/* 6. Large partial purple circle (bottom-right corner) */}
|
||||
<div className="absolute bottom-[-100px] right-[-100px] w-[200px] h-[200px] rounded-full bg-[#7C3AED]/80 z-0 pointer-events-none" />
|
||||
<motion.div
|
||||
className="absolute bottom-[-100px] right-[-100px] w-[200px] h-[200px] rounded-full bg-[#7C3AED]/80 z-0 pointer-events-none"
|
||||
animate={{ y: [0, 12, 0] }}
|
||||
transition={{ duration: 12, repeat: Infinity, ease: "easeInOut" }}
|
||||
/>
|
||||
|
||||
{/* ─── TWO-COLUMN CONTENT GRID ─── */}
|
||||
<div className="page-container relative grid min-h-[650px] grid-cols-1 lg:grid-cols-[0.92fr_1.08fr] items-center py-16 md:py-20 lg:py-24 z-10 gap-12 lg:gap-6">
|
||||
@@ -61,64 +132,84 @@ export function Hero() {
|
||||
{/* LEFT COLUMN: Editorial Typography & Copy */}
|
||||
<motion.div
|
||||
className="flex flex-col text-left max-w-xl z-20 relative"
|
||||
variants={staggerContainer}
|
||||
variants={heroStagger}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
>
|
||||
{/* Badge */}
|
||||
<motion.span
|
||||
variants={fadeUp}
|
||||
variants={badgeVariant}
|
||||
className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-black tracking-widest bg-[#6D28D9]/10 text-[#6D28D9] uppercase mb-6 w-fit"
|
||||
>
|
||||
Welcome to Nearle
|
||||
</motion.span>
|
||||
|
||||
<motion.h1
|
||||
variants={fadeUp}
|
||||
className="font-display text-4xl sm:text-5xl lg:text-[68px] xl:text-[76px] font-black text-[#16001D] leading-[0.95] tracking-[-0.05em]"
|
||||
>
|
||||
Everything Local.
|
||||
<br />
|
||||
Everything <span className="text-[#6D28D9]">Near.</span>
|
||||
</motion.h1>
|
||||
{/* H1: line-by-line mask reveal */}
|
||||
<motion.div variants={lineStagger} className="font-display text-4xl sm:text-5xl lg:text-[68px] xl:text-[76px] font-black text-[#16001D] tracking-[-0.05em]">
|
||||
<div className="overflow-hidden pb-1">
|
||||
<motion.div variants={lineReveal} className="leading-[0.95]">
|
||||
Everything Local.
|
||||
</motion.div>
|
||||
</div>
|
||||
<div className="overflow-hidden pb-1">
|
||||
<motion.div variants={lineReveal} className="leading-[0.95]">
|
||||
Everything <span className="text-[#6D28D9]">Near.</span>
|
||||
</motion.div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Description */}
|
||||
<motion.p
|
||||
variants={fadeUp}
|
||||
variants={descVariant}
|
||||
className="max-w-[480px] text-base md:text-lg text-[#6D6172] leading-relaxed font-bold mt-6"
|
||||
>
|
||||
Discover verified local stores, exclusive offers, fast delivery, and a better way to shop within your neighbourhood.
|
||||
</motion.p>
|
||||
|
||||
<motion.div
|
||||
variants={fadeUp}
|
||||
className="flex flex-col sm:flex-row gap-3.5 mt-8 w-full sm:w-auto"
|
||||
>
|
||||
<Link
|
||||
href="/people"
|
||||
className="bg-[#6D28D9] hover:bg-[#5225b8] text-white font-black px-8 py-3.5 rounded-xl text-sm md:text-base text-center shadow-md hover:shadow-lg transition-all duration-200 active:scale-[0.98]"
|
||||
{/* CTAs: primary scales in, secondary slides from left */}
|
||||
<div className="flex flex-col sm:flex-row gap-3.5 mt-8 w-full sm:w-auto">
|
||||
<motion.div
|
||||
variants={primaryCtaVariant}
|
||||
whileHover={{ scale: 1.03, transition: { duration: 0.2, ease: EASE } }}
|
||||
whileTap={{ scale: 0.97, transition: { duration: 0.1 } }}
|
||||
>
|
||||
Explore Stores
|
||||
</Link>
|
||||
<Link
|
||||
href="/people#download-section"
|
||||
className="border-2 border-[#6D28D9]/20 hover:border-[#6D28D9]/40 text-[#6D28D9] font-black px-8 py-3.5 rounded-xl text-sm md:text-base text-center transition-all duration-200 hover:bg-[#6D28D9]/5 active:scale-[0.98]"
|
||||
<Link
|
||||
href="/people"
|
||||
className="bg-[#6D28D9] hover:bg-[#5225b8] text-white font-black px-8 py-3.5 rounded-xl text-sm md:text-base text-center shadow-md hover:shadow-lg transition-colors duration-200 block"
|
||||
>
|
||||
Explore Stores
|
||||
</Link>
|
||||
</motion.div>
|
||||
<motion.div
|
||||
variants={secondaryCtaVariant}
|
||||
whileHover={{ scale: 1.03, transition: { duration: 0.2, ease: EASE } }}
|
||||
whileTap={{ scale: 0.97, transition: { duration: 0.1 } }}
|
||||
>
|
||||
Get the App
|
||||
</Link>
|
||||
</motion.div>
|
||||
<Link
|
||||
href="/people#download-section"
|
||||
className="border-2 border-[#6D28D9]/20 hover:border-[#6D28D9]/40 text-[#6D28D9] font-black px-8 py-3.5 rounded-xl text-sm md:text-base text-center transition-colors duration-200 hover:bg-[#6D28D9]/5 block"
|
||||
>
|
||||
Get the App
|
||||
</Link>
|
||||
</motion.div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* RIGHT COLUMN: Creative Overlapping Strips Collage */}
|
||||
<motion.div
|
||||
className="w-full z-20"
|
||||
variants={slideInRight}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
>
|
||||
{/* RIGHT COLUMN: Individual image + center card animations */}
|
||||
<div className="w-full z-20">
|
||||
|
||||
{/* DESKTOP LAYOUT (lg and above) */}
|
||||
<div className="hidden lg:block relative h-[500px] lg:h-[560px] w-full">
|
||||
{/* left image: absolute left-[10%] top-[20%] w-[170px] h-[360px] */}
|
||||
<div className="absolute left-[10%] top-[20%] w-[170px] h-[360px] rounded-[1.5rem] overflow-hidden shadow-lg border-2 border-white z-10 transition-transform duration-300 hover:scale-[1.02]">
|
||||
|
||||
{/* Left image: slides in from left */}
|
||||
<motion.div
|
||||
className="absolute left-[10%] top-[20%] w-[170px] h-[360px] rounded-[1.5rem] overflow-hidden shadow-lg border-2 border-white z-10"
|
||||
variants={imgFromLeft}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
transition={{ delay: 0.3 }}
|
||||
whileHover={{ scale: 1.02, transition: { duration: 0.25, ease: EASE } }}
|
||||
>
|
||||
<Image
|
||||
src="/images/cat-daily-essentials.png"
|
||||
alt="Local grocery products"
|
||||
@@ -126,37 +217,63 @@ export function Hero() {
|
||||
className="object-cover"
|
||||
sizes="170px"
|
||||
/>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* middle image: central logo panel absolute left-[38%] top-[8%] */}
|
||||
<div className="absolute left-[38%] top-[8%] z-20 transition-transform duration-300 hover:scale-[1.02]">
|
||||
<div className="relative w-[180px] sm:w-[220px] h-[280px] sm:h-[340px] rounded-[24px] border-[5px] sm:border-[6px] border-white/95 shadow-[0_20px_40px_rgba(99,48,214,0.1)] bg-[#FAF8FC] overflow-hidden flex items-center justify-center p-6">
|
||||
<Image
|
||||
src="/nearlenewlogo.png"
|
||||
alt="Central logo panel"
|
||||
width={160}
|
||||
height={160}
|
||||
className="object-contain max-h-[140px] w-auto animate-[pulse_6s_infinite]"
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* Center card: scales up from below, then gently bobs */}
|
||||
<motion.div
|
||||
className="absolute left-[38%] top-[8%] z-20"
|
||||
variants={centerCardReveal}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
transition={{ delay: 0.15 }}
|
||||
>
|
||||
<motion.div
|
||||
animate={!reducedMotion ? { y: [0, -10, 0] } : {}}
|
||||
transition={{ delay: 0.9, duration: 4, repeat: Infinity, ease: "easeInOut" }}
|
||||
whileHover={{ scale: 1.02, transition: { duration: 0.25, ease: EASE } }}
|
||||
>
|
||||
<div className="relative w-[180px] sm:w-[220px] h-[280px] sm:h-[340px] rounded-[24px] border-[5px] sm:border-[6px] border-white/95 shadow-[0_20px_40px_rgba(99,48,214,0.1)] bg-[#FAF8FC] overflow-hidden flex items-center justify-center p-6">
|
||||
<Image
|
||||
src="/nearlenewlogo.png"
|
||||
alt="Central logo panel"
|
||||
width={160}
|
||||
height={160}
|
||||
className="object-contain max-h-[140px] w-auto motion-safe:animate-[pulse_6s_infinite]"
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
|
||||
{/* right image: absolute right-[2%] top-[18%] w-[180px] h-[390px] */}
|
||||
<div className="absolute right-[2%] top-[18%] w-[180px] h-[390px] rounded-[1.5rem] overflow-hidden shadow-lg border-2 border-white z-10 transition-transform duration-300 hover:scale-[1.02]">
|
||||
{/* Right image: slides in from right */}
|
||||
<motion.div
|
||||
className="absolute right-[2%] top-[18%] w-[180px] h-[390px] rounded-[1.5rem] overflow-hidden shadow-lg border-2 border-white z-10"
|
||||
variants={imgFromRight}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
transition={{ delay: 0.42 }}
|
||||
whileHover={{ scale: 1.02, transition: { duration: 0.25, ease: EASE } }}
|
||||
>
|
||||
<Image
|
||||
src="/nearle people.png"
|
||||
alt="Local customer shopping"
|
||||
fill
|
||||
priority
|
||||
className="object-cover"
|
||||
sizes="180px"
|
||||
/>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* MOBILE/TABLET COLLAGE (below lg) */}
|
||||
<div className="lg:hidden flex items-center justify-center gap-2 mt-2 w-full relative h-[280px]">
|
||||
<div className="relative w-[95px] h-[200px] rounded-2xl overflow-hidden border border-white shadow-md z-10">
|
||||
<motion.div
|
||||
className="relative w-[95px] h-[200px] rounded-2xl overflow-hidden border border-white shadow-md z-10"
|
||||
variants={imgFromLeft}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
transition={{ delay: 0.28 }}
|
||||
>
|
||||
<Image
|
||||
src="/images/cat-daily-essentials.png"
|
||||
alt="Grocery products"
|
||||
@@ -164,18 +281,37 @@ export function Hero() {
|
||||
className="object-cover"
|
||||
sizes="95px"
|
||||
/>
|
||||
</div>
|
||||
<div className="relative w-[115px] h-[240px] rounded-2xl border-4 border-white shadow-lg bg-[#FAF8FC] overflow-hidden flex items-center justify-center p-3 -mx-5 z-20">
|
||||
<Image
|
||||
src="/nearlenewlogo.png"
|
||||
alt="Central logo panel"
|
||||
width={100}
|
||||
height={100}
|
||||
className="object-contain max-h-[90px] w-auto animate-[pulse_6s_infinite]"
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
<div className="relative w-[105px] h-[220px] rounded-2xl overflow-hidden border border-white shadow-md z-10">
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
className="relative w-[115px] h-[240px] rounded-2xl border-4 border-white shadow-lg bg-[#FAF8FC] overflow-hidden flex items-center justify-center p-3 -mx-5 z-20"
|
||||
variants={centerCardReveal}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
transition={{ delay: 0.15 }}
|
||||
>
|
||||
<motion.div
|
||||
animate={!reducedMotion ? { y: [0, -6, 0] } : {}}
|
||||
transition={{ delay: 0.85, duration: 3.5, repeat: Infinity, ease: "easeInOut" }}
|
||||
>
|
||||
<Image
|
||||
src="/nearlenewlogo.png"
|
||||
alt="Central logo panel"
|
||||
width={100}
|
||||
height={100}
|
||||
className="object-contain max-h-[90px] w-auto motion-safe:animate-[pulse_6s_infinite]"
|
||||
priority
|
||||
/>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
className="relative w-[105px] h-[220px] rounded-2xl overflow-hidden border border-white shadow-md z-10"
|
||||
variants={imgFromRight}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
transition={{ delay: 0.36 }}
|
||||
>
|
||||
<Image
|
||||
src="/nearle people.png"
|
||||
alt="Customer"
|
||||
@@ -183,10 +319,10 @@ export function Hero() {
|
||||
className="object-cover"
|
||||
sizes="105px"
|
||||
/>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -194,14 +330,13 @@ export function Hero() {
|
||||
<div className="border-t border-[#EEE7F4]/80 bg-white">
|
||||
<motion.div
|
||||
className="page-container py-8 grid grid-cols-2 md:grid-cols-4 gap-8 md:gap-4 relative z-20"
|
||||
variants={staggerContainer}
|
||||
variants={statStagger}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={viewport}
|
||||
>
|
||||
{PROOF_ITEMS.map((item, idx) => (
|
||||
<motion.div key={item.label} variants={fadeUp} className="flex items-center gap-4 text-left md:justify-center relative">
|
||||
{/* Subtle vertical separator for desktop, except for the first item */}
|
||||
<motion.div key={item.label} variants={statItem} className="flex items-center gap-4 text-left md:justify-center relative">
|
||||
{idx > 0 && (
|
||||
<div className="hidden md:block absolute left-0 top-1/2 -translate-y-1/2 w-[1px] h-10 bg-[#EEE7F4]" />
|
||||
)}
|
||||
|
||||
@@ -256,10 +256,14 @@ export function HowItWorks() {
|
||||
<span className="font-black text-primary">Yours</span>{" "}
|
||||
establish a local online presence and empower neighbourhood customers to discover and interact with you directly.
|
||||
</p>
|
||||
<button className="mt-8 inline-flex w-full items-center justify-center gap-2 rounded-full bg-primary px-8 py-4 text-base font-black text-white shadow-[0_16px_30px_rgba(101,20,154,0.22)] transition-transform duration-200 hover:-translate-y-0.5 sm:w-auto">
|
||||
<motion.button
|
||||
className="mt-8 inline-flex w-full items-center justify-center gap-2 rounded-full bg-primary px-8 py-4 text-base font-black text-white shadow-[0_16px_30px_rgba(101,20,154,0.22)] sm:w-auto"
|
||||
whileHover={{ y: -2, scale: 1.02, transition: { duration: 0.2, ease: [0.22, 1, 0.36, 1] } }}
|
||||
whileTap={{ scale: 0.97, transition: { duration: 0.1 } }}
|
||||
>
|
||||
<MaterialIcon icon="forum" size={20} />
|
||||
Talk to an Expert
|
||||
</button>
|
||||
</motion.button>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
|
||||
@@ -6,7 +6,7 @@ import { motion } from "framer-motion";
|
||||
import { MaterialIcon } from "@/components/ui/MaterialIcon";
|
||||
import { fadeUp, fadeIn, staggerContainer, viewport } from "@/lib/animations";
|
||||
|
||||
const MotionLink = motion(Link);
|
||||
const MotionLink = motion.create(Link);
|
||||
|
||||
const CATEGORIES = [
|
||||
{
|
||||
@@ -106,6 +106,8 @@ export function ServiceCategories() {
|
||||
href="/people"
|
||||
variants={fadeIn}
|
||||
className="bg-white rounded-[2rem] border border-outline-variant/10 shadow-lg overflow-hidden premium-card flex flex-col group h-full cursor-pointer"
|
||||
whileHover={{ y: -4, scale: 1.02, transition: { duration: 0.22, ease: [0.22, 1, 0.36, 1] } }}
|
||||
whileTap={{ scale: 0.98, transition: { duration: 0.1 } }}
|
||||
>
|
||||
{/* Image Container */}
|
||||
<div className="relative aspect-[4/3] w-full overflow-hidden bg-surface-container/30">
|
||||
|
||||
@@ -17,8 +17,16 @@ export function StoreQRSection() {
|
||||
<div className="w-full">
|
||||
<div id="download" className="w-full bg-gradient-to-br from-[#1A0322] to-[#0A000D] rounded-3xl md:rounded-[2rem] overflow-hidden text-white relative flex items-center py-12 md:py-16 px-6 md:px-12 lg:px-16 xl:px-20 border border-white/10 shadow-[0_20px_50px_rgba(20,0,30,0.22)]">
|
||||
{/* Background ambient glowing details */}
|
||||
<div className="absolute right-[-10%] top-[-10%] w-[50%] h-[50%] rounded-full bg-brand-accent/5 blur-[120px] pointer-events-none" />
|
||||
<div className="absolute left-[-10%] bottom-[-10%] w-[50%] h-[50%] rounded-full bg-primary/10 blur-[120px] pointer-events-none" />
|
||||
<motion.div
|
||||
className="absolute right-[-10%] top-[-10%] w-[50%] h-[50%] rounded-full bg-brand-accent/5 blur-[120px] pointer-events-none"
|
||||
animate={{ y: [0, -18, 0] }}
|
||||
transition={{ duration: 14, repeat: Infinity, ease: "easeInOut" }}
|
||||
/>
|
||||
<motion.div
|
||||
className="absolute left-[-10%] bottom-[-10%] w-[50%] h-[50%] rounded-full bg-primary/10 blur-[120px] pointer-events-none"
|
||||
animate={{ y: [0, 16, 0] }}
|
||||
transition={{ duration: 11, repeat: Infinity, ease: "easeInOut" }}
|
||||
/>
|
||||
|
||||
<div className="w-full relative z-10">
|
||||
|
||||
@@ -124,7 +132,7 @@ export function StoreQRSection() {
|
||||
</div>
|
||||
|
||||
<span className="inline-flex items-center gap-1.5 px-3.5 py-1.5 rounded-full text-[10px] font-black tracking-wider uppercase bg-primary/5 text-primary leading-none">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-primary animate-pulse" />
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-primary motion-safe:animate-pulse" />
|
||||
Scan to Shop
|
||||
</span>
|
||||
</motion.div>
|
||||
@@ -189,7 +197,7 @@ export function StoreQRSection() {
|
||||
</div>
|
||||
|
||||
<span className="inline-flex items-center gap-1.5 px-3.5 py-1.5 rounded-full text-[10px] font-black tracking-wider uppercase bg-primary/5 text-primary">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-primary animate-pulse" />
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-primary motion-safe:animate-pulse" />
|
||||
Scan to Shop
|
||||
</span>
|
||||
</motion.div>
|
||||
|
||||
@@ -81,7 +81,7 @@ export function WhyNearle() {
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
{/* Cards Grid — fadeIn preserves .premium-card:hover CSS transform */}
|
||||
{/* Cards Grid */}
|
||||
<motion.div
|
||||
className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-5 lg:gap-7"
|
||||
variants={staggerContainer}
|
||||
@@ -93,7 +93,9 @@ export function WhyNearle() {
|
||||
<motion.div
|
||||
key={card.title}
|
||||
variants={fadeIn}
|
||||
className="premium-card bg-white rounded-3xl p-5 md:p-6 border border-outline-variant/15 shadow-lg flex flex-col h-full hover:scale-[1.02] transition-transform duration-300"
|
||||
className="premium-card bg-white rounded-3xl p-5 md:p-6 border border-outline-variant/15 shadow-lg flex flex-col h-full"
|
||||
whileHover={{ y: -4, scale: 1.02, transition: { duration: 0.22, ease: [0.22, 1, 0.36, 1] } }}
|
||||
whileTap={{ scale: 0.98, transition: { duration: 0.1 } }}
|
||||
>
|
||||
<div className={`w-14 h-14 rounded-2xl flex items-center justify-center border mb-5 shrink-0 ${card.color}`}>
|
||||
<MaterialIcon icon={card.icon} size={27} />
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import { motion } from "framer-motion";
|
||||
import { GlassInfoCard } from "../ui/GlassInfoCard";
|
||||
import { scaleIn, staggerContainer, viewport } from "@/lib/animations";
|
||||
import type { Variants } from "framer-motion";
|
||||
|
||||
type GlassCardItem = {
|
||||
text: string;
|
||||
@@ -12,6 +17,40 @@ type HeroVisualCollageProps = {
|
||||
rightImage?: string;
|
||||
glassCards: GlassCardItem[];
|
||||
isPhoneCenter?: boolean;
|
||||
cardVariant?: Variants;
|
||||
};
|
||||
|
||||
const leftPanelVariant: Variants = {
|
||||
hidden: { opacity: 0, x: -28 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
x: 0,
|
||||
transition: { duration: 0.6, ease: [0.22, 1, 0.36, 1] as [number, number, number, number] },
|
||||
},
|
||||
};
|
||||
|
||||
const rightPanelVariant: Variants = {
|
||||
hidden: { opacity: 0, x: 28 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
x: 0,
|
||||
transition: { duration: 0.6, ease: [0.22, 1, 0.36, 1] as [number, number, number, number] },
|
||||
},
|
||||
};
|
||||
|
||||
const glasscardVariant: Variants = {
|
||||
hidden: { opacity: 0, scale: 0.88, y: 8 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
scale: 1,
|
||||
y: 0,
|
||||
transition: { type: "spring", stiffness: 260, damping: 22 },
|
||||
},
|
||||
};
|
||||
|
||||
const glassCardStagger: Variants = {
|
||||
hidden: {},
|
||||
visible: { transition: { staggerChildren: 0.1, delayChildren: 0.3 } },
|
||||
};
|
||||
|
||||
export function HeroVisualCollage({
|
||||
@@ -20,14 +59,14 @@ export function HeroVisualCollage({
|
||||
rightImage,
|
||||
glassCards,
|
||||
isPhoneCenter = true,
|
||||
cardVariant,
|
||||
}: HeroVisualCollageProps) {
|
||||
const activeCardVariant = cardVariant ?? glasscardVariant;
|
||||
return (
|
||||
<div className="relative w-full h-[380px] sm:h-[450px] lg:h-[500px] max-w-[540px] mx-auto flex items-center justify-center select-none">
|
||||
{/* Decorative backgrounds */}
|
||||
{/* 1. Soft purple circle */}
|
||||
<div className="absolute w-[240px] h-[240px] rounded-full bg-[#7C3AED]/10 blur-[60px] -z-10" />
|
||||
|
||||
{/* 2. SVG Dot grid */}
|
||||
<div className="absolute -top-4 -right-4 w-32 h-32 text-[#6330D6]/10 -z-10 opacity-20">
|
||||
<svg className="w-full h-full" fill="currentColor">
|
||||
<pattern id="collage-dots" x="0" y="0" width="16" height="16" patternUnits="userSpaceOnUse">
|
||||
@@ -37,13 +76,19 @@ export function HeroVisualCollage({
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
{/* 3. Small x marks */}
|
||||
<div className="absolute top-12 left-6 text-[#6330D6]/15 font-display text-xl font-bold select-none pointer-events-none">✕</div>
|
||||
<div className="absolute bottom-16 right-8 text-[#6330D6]/15 font-display text-xl font-bold select-none pointer-events-none">✕</div>
|
||||
|
||||
{/* LEFT IMAGE PANEL (Hidden or compact on small mobile) */}
|
||||
{/* LEFT IMAGE PANEL */}
|
||||
{leftImage && (
|
||||
<div className="absolute left-2 sm:left-4 top-[15%] w-[100px] sm:w-[130px] h-[180px] sm:h-[220px] rounded-[18px] overflow-hidden border-2 border-white shadow-[0_8px_24px_rgba(0,0,0,0.06)] z-10 transition-transform hover:scale-105 duration-300">
|
||||
<motion.div
|
||||
className="absolute left-2 sm:left-4 top-[15%] w-[100px] sm:w-[130px] h-[180px] sm:h-[220px] rounded-[18px] overflow-hidden border-2 border-white shadow-[0_8px_24px_rgba(0,0,0,0.06)] z-10"
|
||||
variants={leftPanelVariant}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={viewport}
|
||||
whileHover={{ scale: 1.04, transition: { duration: 0.25, ease: [0.22, 1, 0.36, 1] } }}
|
||||
>
|
||||
<Image
|
||||
src={leftImage}
|
||||
alt="Collage left asset"
|
||||
@@ -51,12 +96,19 @@ export function HeroVisualCollage({
|
||||
className="object-cover"
|
||||
sizes="(max-width: 640px) 100px, 130px"
|
||||
/>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{/* RIGHT IMAGE PANEL */}
|
||||
{rightImage && (
|
||||
<div className="absolute right-2 sm:right-4 bottom-[15%] w-[100px] sm:w-[130px] h-[180px] sm:h-[220px] rounded-[18px] overflow-hidden border-2 border-white shadow-[0_8px_24px_rgba(0,0,0,0.06)] z-10 transition-transform hover:scale-105 duration-300">
|
||||
<motion.div
|
||||
className="absolute right-2 sm:right-4 bottom-[15%] w-[100px] sm:w-[130px] h-[180px] sm:h-[220px] rounded-[18px] overflow-hidden border-2 border-white shadow-[0_8px_24px_rgba(0,0,0,0.06)] z-10"
|
||||
variants={rightPanelVariant}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={viewport}
|
||||
whileHover={{ scale: 1.04, transition: { duration: 0.25, ease: [0.22, 1, 0.36, 1] } }}
|
||||
>
|
||||
<Image
|
||||
src={rightImage}
|
||||
alt="Collage right asset"
|
||||
@@ -64,13 +116,18 @@ export function HeroVisualCollage({
|
||||
className="object-cover"
|
||||
sizes="(max-width: 640px) 100px, 130px"
|
||||
/>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{/* CENTER COMPONENT */}
|
||||
<div className="relative z-20">
|
||||
<motion.div
|
||||
className="relative z-20"
|
||||
variants={scaleIn}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={viewport}
|
||||
>
|
||||
{isPhoneCenter ? (
|
||||
/* Phone Frame Container */
|
||||
<div className="relative w-[180px] sm:w-[220px] h-[360px] sm:h-[440px] rounded-[36px] border-[5px] sm:border-[6px] border-white/95 shadow-[0_20px_50px_rgba(99,48,214,0.12)] bg-[#FAF8FC] overflow-hidden flex flex-col justify-center items-center">
|
||||
<Image
|
||||
src={centerImage}
|
||||
@@ -82,56 +139,70 @@ export function HeroVisualCollage({
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
/* Central Logo / Visual Panel */
|
||||
<div className="relative w-[180px] sm:w-[220px] h-[280px] sm:h-[340px] rounded-[24px] border-[5px] sm:border-[6px] border-white/95 shadow-[0_20px_40px_rgba(99,48,214,0.1)] bg-[#FAF8FC] overflow-hidden flex items-center justify-center p-6">
|
||||
<Image
|
||||
src={centerImage}
|
||||
alt="Central logo panel"
|
||||
width={160}
|
||||
height={160}
|
||||
className="object-contain max-h-[140px] w-auto animate-[pulse_6s_infinite]"
|
||||
className="object-contain max-h-[140px] w-auto motion-safe:animate-[pulse_6s_infinite]"
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* FLOATING GLASS CARDS (Positioned Absolute, scaled down on mobile) */}
|
||||
{/* FLOATING GLASS CARDS — stagger in after center */}
|
||||
{glassCards.length > 0 && (
|
||||
<div className="absolute inset-0 z-30 pointer-events-none">
|
||||
{/* Card 1: Top Right */}
|
||||
<motion.div
|
||||
className="absolute inset-0 z-30 pointer-events-none"
|
||||
variants={glassCardStagger}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={viewport}
|
||||
>
|
||||
{glassCards[0] && (
|
||||
<div className="absolute right-0 sm:right-[5%] top-[10%] pointer-events-auto hover:translate-y-[-2px] transition-transform duration-200">
|
||||
<motion.div
|
||||
variants={activeCardVariant}
|
||||
className="absolute right-0 sm:right-[5%] top-[10%] pointer-events-auto"
|
||||
whileHover={{ y: -2, transition: { duration: 0.2, ease: [0.22, 1, 0.36, 1] } }}
|
||||
>
|
||||
<GlassInfoCard
|
||||
text={glassCards[0].text}
|
||||
icon={glassCards[0].icon}
|
||||
className="scale-90 sm:scale-100"
|
||||
/>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{/* Card 2: Bottom Left */}
|
||||
{glassCards[1] && (
|
||||
<div className="absolute left-0 sm:left-[5%] bottom-[10%] pointer-events-auto hover:translate-y-[-2px] transition-transform duration-200">
|
||||
<motion.div
|
||||
variants={activeCardVariant}
|
||||
className="absolute left-0 sm:left-[5%] bottom-[10%] pointer-events-auto"
|
||||
whileHover={{ y: -2, transition: { duration: 0.2, ease: [0.22, 1, 0.36, 1] } }}
|
||||
>
|
||||
<GlassInfoCard
|
||||
text={glassCards[1].text}
|
||||
icon={glassCards[1].icon}
|
||||
className="scale-90 sm:scale-100"
|
||||
/>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{/* Card 3: Mid Left/Right */}
|
||||
{glassCards[2] && (
|
||||
<div className="absolute left-[5%] top-[45%] pointer-events-auto hover:translate-y-[-2px] transition-transform duration-200">
|
||||
<motion.div
|
||||
variants={activeCardVariant}
|
||||
className="absolute left-[5%] top-[45%] pointer-events-auto"
|
||||
whileHover={{ y: -2, transition: { duration: 0.2, ease: [0.22, 1, 0.36, 1] } }}
|
||||
>
|
||||
<GlassInfoCard
|
||||
text={glassCards[2].text}
|
||||
icon={glassCards[2].icon}
|
||||
className="scale-90 sm:scale-100"
|
||||
/>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { useState } from "react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { MaterialIcon } from "@/components/ui/MaterialIcon";
|
||||
|
||||
const NAV_LINKS = [
|
||||
{ label: "Home", href: "/" },
|
||||
{ label: "Nearle for Business", href: "/businesses" },
|
||||
{ label: "Nearle for People", href: "/people" },
|
||||
{ label: "About", href: "/about" },
|
||||
{ label: "Contact", href: "/contact" },
|
||||
];
|
||||
|
||||
export function Navbar() {
|
||||
const [mobileOpen, setMobileOpen] = useState(false);
|
||||
const pathname = usePathname();
|
||||
|
||||
return (
|
||||
<div className="fixed top-0 left-0 right-0 z-50 w-full flex justify-center pointer-events-none">
|
||||
<div className="w-full max-w-[1920px] px-4 md:px-6 lg:px-8 xl:px-10 2xl:px-12 pt-[18px] md:pt-[24px] pointer-events-auto relative">
|
||||
<nav className="w-full h-[68px] lg:h-[76px] rounded-full bg-[rgba(61,45,143,0.88)] border border-white/20 shadow-sm backdrop-blur-md px-5 md:px-8 lg:px-10 flex items-center transition-all duration-300">
|
||||
<div className="grid grid-cols-2 md:grid-cols-[1fr_auto_1fr] items-center w-full h-full">
|
||||
{/* Left: Brand */}
|
||||
<div className="flex items-center justify-start">
|
||||
<Link
|
||||
href="/"
|
||||
className="flex items-center hover:scale-[1.02] transition-transform active:scale-95 duration-200"
|
||||
>
|
||||
<Image
|
||||
src="/logo-light.png"
|
||||
alt="Nearle"
|
||||
width={400}
|
||||
height={50}
|
||||
className="h-8 lg:h-9 w-auto object-contain"
|
||||
priority
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Center: Navigation Links */}
|
||||
<div className="hidden md:flex items-center justify-center gap-6 lg:gap-8 xl:gap-10">
|
||||
{NAV_LINKS.map((link) => {
|
||||
const isActive = pathname === link.href;
|
||||
return (
|
||||
<Link
|
||||
key={link.label}
|
||||
href={link.href}
|
||||
className={`relative py-2 text-[15px] font-semibold leading-5 transition-all duration-200 whitespace-nowrap ${
|
||||
isActive ? "text-white" : "text-white/75 hover:text-white"
|
||||
} group`}
|
||||
>
|
||||
{link.label}
|
||||
<span
|
||||
className={`absolute bottom-[-6px] left-1/2 -translate-x-1/2 h-0.5 bg-white rounded-full transition-all duration-300 ${
|
||||
isActive
|
||||
? "w-5 opacity-100"
|
||||
: "w-0 opacity-0 group-hover:w-4 group-hover:opacity-60"
|
||||
}`}
|
||||
/>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Right: Actions */}
|
||||
<div className="flex items-center justify-end gap-3">
|
||||
<Link
|
||||
href="/#download"
|
||||
className="hidden md:inline-flex items-center justify-center bg-white text-[rgba(61,45,143,0.88)] font-bold px-7 h-[46px] rounded-full text-[15px] hover:bg-white/90 hover:shadow-md hover:scale-[1.02] active:scale-95 transition-all duration-300 whitespace-nowrap"
|
||||
>
|
||||
Get App
|
||||
</Link>
|
||||
|
||||
{/* Mobile menu toggle */}
|
||||
<button
|
||||
className="md:hidden flex items-center justify-center w-10 h-10 rounded-full hover:bg-white/10 text-white transition-all active:scale-90 cursor-pointer"
|
||||
onClick={() => setMobileOpen(!mobileOpen)}
|
||||
aria-label="Toggle menu"
|
||||
>
|
||||
<MaterialIcon icon={mobileOpen ? "close" : "menu"} size={22} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{/* Mobile Drawer */}
|
||||
{mobileOpen && (
|
||||
<div className="absolute top-[calc(100%+12px)] left-4 right-4 z-40 bg-[rgba(61,45,143,0.95)] border border-white/20 rounded-2xl shadow-[0_20px_40px_rgba(100,60,120,0.15)] backdrop-blur-md p-4 md:hidden">
|
||||
<div className="flex flex-col gap-2">
|
||||
{NAV_LINKS.map((link) => {
|
||||
const isActive = pathname === link.href;
|
||||
return (
|
||||
<Link
|
||||
key={link.label}
|
||||
href={link.href}
|
||||
className={`px-4 py-3 text-body-md font-bold rounded-xl transition-all duration-200 flex items-center justify-between ${
|
||||
isActive
|
||||
? "bg-white/10 text-white"
|
||||
: "text-white/70 hover:text-white hover:bg-white/5"
|
||||
}`}
|
||||
onClick={() => setMobileOpen(false)}
|
||||
>
|
||||
{link.label}
|
||||
{isActive && (
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-white animate-pulse" />
|
||||
)}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,13 +3,16 @@
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import React from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { FaFacebookF, FaInstagram, FaLinkedinIn } from "react-icons/fa";
|
||||
import { MaterialIcon } from "@/components/ui/MaterialIcon";
|
||||
import { fadeUp, fadeIn, staggerContainer, staggerSlow, viewport } from "@/lib/animations";
|
||||
|
||||
const EASE = [0.22, 1, 0.36, 1] as [number, number, number, number];
|
||||
|
||||
const COMPANY = [
|
||||
{ label: "About Us", href: "/about" },
|
||||
{ label: "Careers", href: "#" },
|
||||
// { label: "Press & News", href: "#" },
|
||||
{ label: "Help Center", href: "#" },
|
||||
];
|
||||
|
||||
@@ -17,26 +20,87 @@ const PARTNERSHIPS = [
|
||||
{ label: "Merchant Partner", href: "/merchants" },
|
||||
{ label: "Delivery Partner", href: "/delivery-partners" },
|
||||
{ label: "Direct to Customer Brands", href: "/brands" },
|
||||
// { label: "Local Real Estate", href: "#" },
|
||||
];
|
||||
|
||||
const LEGAL = [
|
||||
{ label: "Privacy Policy", href: "/privacy" },
|
||||
{ label: "Terms of Service", href: "/terms" },
|
||||
{ label: "Cookie Policy", href: "/cookies" },
|
||||
// { label: "Sitemap", href: "#" },
|
||||
];
|
||||
|
||||
const socialButtonClass =
|
||||
"flex h-10 w-10 items-center justify-center rounded-full border border-white/10 bg-white/5 text-white transition-all duration-300 xl:h-[54px] xl:w-[54px]";
|
||||
"flex h-10 w-10 items-center justify-center rounded-full border border-white/10 bg-white/5 text-white transition-[background-color] duration-300 xl:h-[54px] xl:w-[54px]";
|
||||
|
||||
const linkColumnVariant = {
|
||||
hidden: { opacity: 0, y: 20 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
transition: { duration: 0.5, ease: EASE },
|
||||
},
|
||||
};
|
||||
|
||||
const footerReveal = {
|
||||
hidden: { opacity: 0, y: 20 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
transition: { duration: 0.6, ease: EASE },
|
||||
},
|
||||
};
|
||||
|
||||
// Renders an animated link list used in all three footer link columns
|
||||
function FooterLinkList({ links }: { links: { label: string; href: string }[] }) {
|
||||
return (
|
||||
<ul className="flex flex-col gap-4 xl:gap-5">
|
||||
{links.map((link) => (
|
||||
<li key={link.label}>
|
||||
<motion.span
|
||||
className="relative inline-block"
|
||||
initial="rest"
|
||||
whileHover="hover"
|
||||
animate="rest"
|
||||
>
|
||||
<Link
|
||||
href={link.href}
|
||||
className="text-sm leading-normal text-white/70 transition-colors duration-200 hover:text-white md:text-base xl:text-[17px]"
|
||||
>
|
||||
{link.label}
|
||||
</Link>
|
||||
{/* Underline grows from left on hover */}
|
||||
<motion.span
|
||||
className="absolute -bottom-0.5 left-0 right-0 h-px rounded-full bg-white/50"
|
||||
style={{ originX: 0 }}
|
||||
variants={{ rest: { scaleX: 0 }, hover: { scaleX: 1 } }}
|
||||
transition={{ duration: 0.2, ease: EASE }}
|
||||
/>
|
||||
</motion.span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
export function PremiumFooter() {
|
||||
return (
|
||||
<section className="bg-white px-2 pb-2 md:px-3 md:pb-3 lg:px-4 lg:pb-4 xl:px-5 xl:pb-4 2xl:px-6 2xl:pb-5">
|
||||
<footer className="w-full overflow-hidden rounded-[28px] bg-gradient-to-br from-[#16001D] to-[#25002F] text-white shadow-[0_-12px_40px_rgba(22,0,29,0.12)]">
|
||||
{/* Footer reveal: the whole footer fades up as it enters the viewport */}
|
||||
<motion.footer
|
||||
className="w-full overflow-hidden rounded-[28px] bg-gradient-to-br from-[#16001D] to-[#25002F] text-white shadow-[0_-12px_40px_rgba(22,0,29,0.12)]"
|
||||
variants={footerReveal}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={viewport}
|
||||
>
|
||||
<div className="flex flex-col px-6 pb-8 pt-10 md:px-8 md:pb-8 md:pt-12 xl:px-16 xl:pb-8 xl:pt-12 2xl:px-20 2xl:pt-10 2xl:pb-8">
|
||||
<div className="grid w-full grid-cols-1 items-start gap-10 text-left md:grid-cols-2 md:gap-x-12 md:gap-y-12 lg:grid-cols-[1.6fr_1fr_1fr_1fr] lg:gap-10 xl:gap-14">
|
||||
<div className="flex flex-col gap-5">
|
||||
|
||||
{/* Main grid: columns stagger in via parent footer variant propagation */}
|
||||
<motion.div
|
||||
className="grid w-full grid-cols-1 items-start gap-10 text-left md:grid-cols-2 md:gap-x-12 md:gap-y-12 lg:grid-cols-[1.6fr_1fr_1fr_1fr] lg:gap-10 xl:gap-14"
|
||||
variants={staggerSlow}
|
||||
>
|
||||
{/* Brand column */}
|
||||
<motion.div variants={fadeUp} className="flex flex-col gap-5">
|
||||
<Link href="/" className="inline-block w-fit">
|
||||
<Image
|
||||
src="/logo-light.png"
|
||||
@@ -52,100 +116,81 @@ export function PremiumFooter() {
|
||||
communities and verified local businesses within a 2km radius.
|
||||
</p>
|
||||
|
||||
{/* Social icons with scale spring on hover */}
|
||||
<div className="mt-1 flex gap-3 xl:mt-2 xl:gap-4">
|
||||
<a
|
||||
<motion.a
|
||||
href="https://www.facebook.com/nearledigital"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="Facebook"
|
||||
className={`${socialButtonClass} hover:bg-[#1877F2]`}
|
||||
whileHover={{ scale: 1.12 }}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
transition={{ type: "spring", stiffness: 400, damping: 17 }}
|
||||
>
|
||||
<FaFacebookF size={20} />
|
||||
</a>
|
||||
</motion.a>
|
||||
|
||||
<a
|
||||
<motion.a
|
||||
href="https://www.instagram.com/nearledigital/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="Instagram"
|
||||
className={`${socialButtonClass} hover:bg-[#E4405F]`}
|
||||
whileHover={{ scale: 1.12 }}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
transition={{ type: "spring", stiffness: 400, damping: 17 }}
|
||||
>
|
||||
<FaInstagram size={21} />
|
||||
</a>
|
||||
</motion.a>
|
||||
|
||||
<a
|
||||
<motion.a
|
||||
href="https://www.linkedin.com/company/nearledigital/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="LinkedIn"
|
||||
className={`${socialButtonClass} hover:bg-[#0A66C2]`}
|
||||
whileHover={{ scale: 1.12 }}
|
||||
whileTap={{ scale: 0.9 }}
|
||||
transition={{ type: "spring", stiffness: 400, damping: 17 }}
|
||||
>
|
||||
<FaLinkedinIn size={20} />
|
||||
</a>
|
||||
</motion.a>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<div className="flex flex-col">
|
||||
{/* Company links */}
|
||||
<motion.div variants={linkColumnVariant} className="flex flex-col">
|
||||
<h5 className="mb-5 text-xs font-extrabold uppercase tracking-wider text-white/40 md:text-sm xl:mb-7 xl:text-[17px]">
|
||||
Company
|
||||
</h5>
|
||||
<FooterLinkList links={COMPANY} />
|
||||
</motion.div>
|
||||
|
||||
<ul className="flex flex-col gap-4 xl:gap-5">
|
||||
{COMPANY.map((link) => (
|
||||
<li key={link.label}>
|
||||
<Link
|
||||
href={link.href}
|
||||
className="inline-block text-sm leading-normal text-white/70 transition-all hover:translate-x-0.5 hover:text-white md:text-base xl:text-[17px]"
|
||||
>
|
||||
{link.label}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col">
|
||||
{/* Nearle links */}
|
||||
<motion.div variants={linkColumnVariant} className="flex flex-col">
|
||||
<h5 className="mb-5 text-xs font-extrabold uppercase tracking-wider text-white/40 md:text-sm xl:mb-7 xl:text-[17px]">
|
||||
Nearle
|
||||
</h5>
|
||||
<FooterLinkList links={PARTNERSHIPS} />
|
||||
</motion.div>
|
||||
|
||||
<ul className="flex flex-col gap-4 xl:gap-5">
|
||||
{PARTNERSHIPS.map((link) => (
|
||||
<li key={link.label}>
|
||||
<Link
|
||||
href={link.href}
|
||||
className="inline-block text-sm leading-normal text-white/70 transition-all hover:translate-x-0.5 hover:text-white md:text-base xl:text-[17px]"
|
||||
>
|
||||
{link.label}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col">
|
||||
{/* Support links */}
|
||||
<motion.div variants={linkColumnVariant} className="flex flex-col">
|
||||
<h5 className="mb-5 text-xs font-extrabold uppercase tracking-wider text-white/40 md:text-sm xl:mb-7 xl:text-[17px]">
|
||||
Support
|
||||
</h5>
|
||||
|
||||
<ul className="flex flex-col gap-4 xl:gap-5">
|
||||
{LEGAL.map((link) => (
|
||||
<li key={link.label}>
|
||||
<Link
|
||||
href={link.href}
|
||||
className="inline-block text-sm leading-normal text-white/70 transition-all hover:translate-x-0.5 hover:text-white md:text-base xl:text-[17px]"
|
||||
>
|
||||
{link.label}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<FooterLinkList links={LEGAL} />
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
|
||||
<div className="my-8 w-full border-t border-white/10 xl:my-10" />
|
||||
|
||||
<div className="flex w-full flex-col items-center justify-between gap-8 md:flex-row xl:gap-12">
|
||||
{/* Bottom row */}
|
||||
<motion.div
|
||||
className="flex w-full flex-col items-center justify-between gap-8 md:flex-row xl:gap-12"
|
||||
variants={fadeUp}
|
||||
>
|
||||
<div className="flex min-h-[120px] w-full max-w-[560px] items-start gap-3 rounded-2xl border border-white/10 bg-white/5 p-4 md:w-auto md:p-5 xl:min-h-[130px] xl:gap-4 xl:p-6">
|
||||
<div className="mt-0.5 flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-white/10 text-white xl:h-[58px] xl:w-[58px] xl:rounded-2xl">
|
||||
<MaterialIcon icon="location_on" size={20} />
|
||||
@@ -155,7 +200,6 @@ export function PremiumFooter() {
|
||||
<h6 className="text-sm font-black text-white md:text-base xl:text-lg">
|
||||
Nearle
|
||||
</h6>
|
||||
|
||||
<p className="mt-0.5 text-xs leading-normal text-white/60 md:text-sm xl:mt-1 xl:text-base">
|
||||
424, Diwan Bahadur Rd, Opp. Sri Krishna Sweets, R.S. Puram,
|
||||
Coimbatore, TN 641002
|
||||
@@ -181,11 +225,15 @@ export function PremiumFooter() {
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<div className="mb-4 mt-8 w-full border-t border-white/5 xl:mb-6 xl:mt-10" />
|
||||
|
||||
<div className="flex w-full flex-col items-center justify-between gap-4 text-[13px] text-white/40 md:flex-row md:text-[14px] xl:text-[15px]">
|
||||
{/* Copyright bar */}
|
||||
<motion.div
|
||||
className="flex w-full flex-col items-center justify-between gap-4 text-[13px] text-white/40 md:flex-row md:text-[14px] xl:text-[15px]"
|
||||
variants={fadeIn}
|
||||
>
|
||||
<p className="text-center md:text-left">
|
||||
© {new Date().getFullYear()} Nearle. All rights reserved.
|
||||
</p>
|
||||
@@ -203,9 +251,9 @@ export function PremiumFooter() {
|
||||
Help Center
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</footer>
|
||||
</motion.footer>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,10 +2,18 @@
|
||||
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { useState } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import {
|
||||
motion,
|
||||
AnimatePresence,
|
||||
useScroll,
|
||||
useMotionValueEvent,
|
||||
} from "framer-motion";
|
||||
import { MaterialIcon } from "@/components/ui/MaterialIcon";
|
||||
|
||||
const EASE = [0.22, 1, 0.36, 1] as [number, number, number, number];
|
||||
|
||||
const NAV_LINKS = [
|
||||
{ label: "Home", href: "/" },
|
||||
{ label: "Nearle for Business", href: "/businesses" },
|
||||
@@ -14,9 +22,44 @@ const NAV_LINKS = [
|
||||
{ label: "Contact", href: "/contact" },
|
||||
];
|
||||
|
||||
const mobileMenuVariants = {
|
||||
hidden: { opacity: 0, height: 0 },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
height: "auto",
|
||||
transition: { duration: 0.3, ease: EASE },
|
||||
},
|
||||
exit: {
|
||||
opacity: 0,
|
||||
height: 0,
|
||||
transition: { duration: 0.2, ease: "easeIn" as const },
|
||||
},
|
||||
};
|
||||
|
||||
const mobileStagger = {
|
||||
hidden: {},
|
||||
visible: { transition: { staggerChildren: 0.04, delayChildren: 0.06 } },
|
||||
};
|
||||
|
||||
const mobileItemVariants = {
|
||||
hidden: { opacity: 0, x: -10 },
|
||||
visible: { opacity: 1, x: 0, transition: { duration: 0.22, ease: EASE } },
|
||||
};
|
||||
|
||||
export function PremiumNavbar() {
|
||||
const [mobileOpen, setMobileOpen] = useState(false);
|
||||
const [scrolled, setScrolled] = useState(false);
|
||||
const pathname = usePathname();
|
||||
const { scrollY } = useScroll();
|
||||
|
||||
useMotionValueEvent(scrollY, "change", (y) => {
|
||||
setScrolled(y > 20);
|
||||
});
|
||||
|
||||
// Close mobile menu whenever the route changes (navbar no longer remounts on navigation)
|
||||
useEffect(() => {
|
||||
setMobileOpen(false);
|
||||
}, [pathname]);
|
||||
|
||||
const isActiveLink = (href: string) => {
|
||||
if (href === "/") return pathname === "/";
|
||||
@@ -24,10 +67,18 @@ export function PremiumNavbar() {
|
||||
};
|
||||
|
||||
return (
|
||||
// <header className="sticky top-0 z-50 w-full border-b border-white/10 bg-[#3D2D8F]/90 text-white backdrop-blur-xl">
|
||||
<header className="sticky top-0 z-50 w-full bg-[#83429f] border-b border-white/10 text-white">
|
||||
<motion.header
|
||||
className={`sticky top-0 z-50 w-full text-white transition-[background-color,box-shadow,backdrop-filter] duration-300 ${
|
||||
scrolled
|
||||
? "bg-[#83429f]/82 backdrop-blur-lg shadow-[0_6px_32px_rgba(0,0,0,0.35)] border-b border-white/15"
|
||||
: "bg-[#83429f] border-b border-white/10"
|
||||
}`}
|
||||
initial={{ opacity: 0, y: -8 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.4, ease: EASE }}
|
||||
>
|
||||
<div className="relative mx-auto flex h-16 w-full max-w-[1600px] items-center px-6 md:h-20 md:px-10 xl:px-16">
|
||||
{/* Logo - Left */}
|
||||
{/* Logo */}
|
||||
<Link
|
||||
href="/"
|
||||
className="flex shrink-0 items-center transition-transform duration-200 hover:scale-[1.02] active:scale-95"
|
||||
@@ -43,38 +94,55 @@ export function PremiumNavbar() {
|
||||
/>
|
||||
</Link>
|
||||
|
||||
{/* Desktop Navigation - Center */}
|
||||
{/* Desktop Navigation */}
|
||||
<nav className="absolute left-1/2 hidden -translate-x-1/2 items-center gap-6 lg:flex lg:gap-8 xl:gap-10">
|
||||
{NAV_LINKS.map((link) => {
|
||||
const isActive = isActiveLink(link.href);
|
||||
|
||||
return (
|
||||
<Link
|
||||
<motion.div
|
||||
key={link.label}
|
||||
href={link.href}
|
||||
className={`relative whitespace-nowrap py-2 text-sm font-bold transition-colors duration-200 ${
|
||||
isActive
|
||||
? "text-white"
|
||||
: "text-white/70 hover:text-white"
|
||||
}`}
|
||||
className="relative py-2"
|
||||
initial="rest"
|
||||
whileHover="hover"
|
||||
animate="rest"
|
||||
>
|
||||
{link.label}
|
||||
|
||||
<span
|
||||
className={`absolute bottom-0 left-1/2 h-0.5 -translate-x-1/2 rounded-full bg-white transition-all duration-300 ${
|
||||
isActive ? "w-full" : "w-0"
|
||||
<Link
|
||||
href={link.href}
|
||||
className={`whitespace-nowrap text-sm font-bold transition-colors duration-200 ${
|
||||
isActive ? "text-white" : "text-white/70 hover:text-white"
|
||||
}`}
|
||||
/>
|
||||
</Link>
|
||||
>
|
||||
{link.label}
|
||||
</Link>
|
||||
|
||||
{/* Active underline: layoutId lets it slide between links on navigation */}
|
||||
{isActive && (
|
||||
<motion.span
|
||||
layoutId="activeNavUnderline"
|
||||
className="absolute bottom-0 left-0 right-0 h-0.5 rounded-full bg-white"
|
||||
transition={{ duration: 0.3, ease: EASE }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Hover underline: grows from left on inactive links */}
|
||||
{!isActive && (
|
||||
<motion.span
|
||||
className="absolute bottom-0 left-0 right-0 h-0.5 rounded-full bg-white/40"
|
||||
style={{ originX: 0 }}
|
||||
variants={{ rest: { scaleX: 0 }, hover: { scaleX: 1 } }}
|
||||
transition={{ duration: 0.2, ease: EASE }}
|
||||
/>
|
||||
)}
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
|
||||
{/* Mobile Toggle - Right */}
|
||||
{/* Mobile Toggle */}
|
||||
<button
|
||||
type="button"
|
||||
className="ml-auto flex h-10 w-10 items-center justify-center rounded-full text-white transition-all duration-200 hover:bg-white/10 active:scale-90 lg:hidden"
|
||||
onClick={() => setMobileOpen((value) => !value)}
|
||||
onClick={() => setMobileOpen((v) => !v)}
|
||||
aria-label={mobileOpen ? "Close menu" : "Open menu"}
|
||||
aria-expanded={mobileOpen}
|
||||
>
|
||||
@@ -82,39 +150,48 @@ export function PremiumNavbar() {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Mobile Menu */}
|
||||
<div
|
||||
className={`overflow-hidden border-t border-white/10 bg-[#30226F]/98 backdrop-blur-xl transition-all duration-300 lg:hidden ${
|
||||
mobileOpen
|
||||
? "max-h-[500px] opacity-100"
|
||||
: "pointer-events-none max-h-0 opacity-0"
|
||||
}`}
|
||||
>
|
||||
<nav className="mx-auto flex w-full max-w-[1600px] flex-col gap-1 px-6 py-5 md:px-10">
|
||||
{NAV_LINKS.map((link) => {
|
||||
const isActive = isActiveLink(link.href);
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={link.label}
|
||||
href={link.href}
|
||||
onClick={() => setMobileOpen(false)}
|
||||
className={`flex items-center justify-between rounded-xl px-4 py-3 text-sm font-bold transition-all duration-200 ${
|
||||
isActive
|
||||
? "bg-white/15 text-white"
|
||||
: "text-white/70 hover:bg-white/10 hover:text-white"
|
||||
}`}
|
||||
>
|
||||
{link.label}
|
||||
|
||||
{isActive && (
|
||||
<span className="h-2 w-2 rounded-full bg-white" />
|
||||
)}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
{/* Mobile Menu with enter/exit animation */}
|
||||
<AnimatePresence>
|
||||
{mobileOpen && (
|
||||
<motion.div
|
||||
key="mobile-menu"
|
||||
variants={mobileMenuVariants}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
exit="exit"
|
||||
className="overflow-hidden border-t border-white/10 bg-[#30226F]/98 backdrop-blur-xl lg:hidden"
|
||||
>
|
||||
<motion.nav
|
||||
className="mx-auto flex w-full max-w-[1600px] flex-col gap-1 px-6 py-5 md:px-10"
|
||||
variants={mobileStagger}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
>
|
||||
{NAV_LINKS.map((link) => {
|
||||
const isActive = isActiveLink(link.href);
|
||||
return (
|
||||
<motion.div key={link.label} variants={mobileItemVariants}>
|
||||
<Link
|
||||
href={link.href}
|
||||
onClick={() => setMobileOpen(false)}
|
||||
className={`flex items-center justify-between rounded-xl px-4 py-3 text-sm font-bold transition-all duration-200 ${
|
||||
isActive
|
||||
? "bg-white/15 text-white"
|
||||
: "text-white/70 hover:bg-white/10 hover:text-white"
|
||||
}`}
|
||||
>
|
||||
{link.label}
|
||||
{isActive && (
|
||||
<span className="h-2 w-2 rounded-full bg-white" />
|
||||
)}
|
||||
</Link>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</motion.nav>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</motion.header>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +1,12 @@
|
||||
import React from "react";
|
||||
import { PremiumNavbar } from "./PremiumNavbar";
|
||||
import { PremiumFooter } from "./PremiumFooter";
|
||||
import { MotionProvider } from "./MotionProvider";
|
||||
|
||||
type PremiumPageShellProps = {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function PremiumPageShell({ children, className = "" }: PremiumPageShellProps) {
|
||||
return (
|
||||
<MotionProvider>
|
||||
<div className={`min-h-screen bg-white font-body text-[#16001D] antialiased flex flex-col ${className}`}>
|
||||
{/* Header/Navbar */}
|
||||
<PremiumNavbar />
|
||||
|
||||
{/* Page Content */}
|
||||
<main className="w-full overflow-x-hidden bg-white">
|
||||
{children}
|
||||
</main>
|
||||
|
||||
<PremiumFooter />
|
||||
</div>
|
||||
</MotionProvider>
|
||||
);
|
||||
// Navbar, footer, and MotionProvider are rendered in the root layout (app/layout.tsx)
|
||||
// and persist across all navigations. This component is kept for call-site compatibility.
|
||||
export function PremiumPageShell({ children }: PremiumPageShellProps) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
42
src/components/motion/AnimatedGrid.tsx
Normal file
42
src/components/motion/AnimatedGrid.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import type { ReactNode, CSSProperties } from "react";
|
||||
import { cardReveal, staggerCards, viewport20 } from "@/lib/animations";
|
||||
|
||||
type Props = {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
};
|
||||
|
||||
export function AnimatedGrid({ children, className, style }: Props) {
|
||||
return (
|
||||
<motion.div
|
||||
className={className}
|
||||
style={style}
|
||||
variants={staggerCards}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={viewport20}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
const EASE = [0.22, 1, 0.36, 1] as [number, number, number, number];
|
||||
|
||||
export function AnimatedCard({ children, className, style }: Props) {
|
||||
return (
|
||||
<motion.div
|
||||
className={className}
|
||||
style={style}
|
||||
variants={cardReveal}
|
||||
whileHover={{ y: -4, transition: { duration: 0.22, ease: EASE } }}
|
||||
whileTap={{ scale: 0.97, transition: { duration: 0.1 } }}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
50
src/components/motion/AnimatedSection.tsx
Normal file
50
src/components/motion/AnimatedSection.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import type { ReactNode } from "react";
|
||||
import { fadeUp, staggerContainer, viewport } from "@/lib/animations";
|
||||
import type { Variants } from "framer-motion";
|
||||
|
||||
const VARIANTS: Record<string, Variants> = {
|
||||
fadeUp,
|
||||
stagger: staggerContainer,
|
||||
};
|
||||
|
||||
type AnimatedSectionProps = {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
variant?: "fadeUp" | "stagger";
|
||||
delay?: number;
|
||||
};
|
||||
|
||||
export function AnimatedSection({
|
||||
children,
|
||||
className = "",
|
||||
variant = "fadeUp",
|
||||
delay = 0,
|
||||
}: AnimatedSectionProps) {
|
||||
const chosen = VARIANTS[variant] ?? fadeUp;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const withDelay: Variants = {
|
||||
hidden: chosen.hidden as any,
|
||||
visible: {
|
||||
...(chosen.visible as any),
|
||||
transition: {
|
||||
...((chosen.visible as any)?.transition ?? {}),
|
||||
delay,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
className={className}
|
||||
variants={withDelay}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={viewport}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
102
src/components/ui/CTABanner.tsx
Normal file
102
src/components/ui/CTABanner.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { motion } from "framer-motion";
|
||||
import { MaterialIcon } from "@/components/ui/MaterialIcon";
|
||||
|
||||
const EASE = [0.22, 1, 0.36, 1] as [number, number, number, number];
|
||||
const VP = { once: true, amount: 0.25 } as const;
|
||||
|
||||
const stagger = {
|
||||
hidden: {},
|
||||
visible: { transition: { staggerChildren: 0.13 } },
|
||||
};
|
||||
const textReveal = {
|
||||
hidden: { opacity: 0, y: 18 },
|
||||
visible: { opacity: 1, y: 0, transition: { duration: 0.52, ease: EASE } },
|
||||
};
|
||||
const buttonReveal = {
|
||||
hidden: { opacity: 0, y: 12 },
|
||||
visible: { opacity: 1, y: 0, transition: { duration: 0.42, ease: EASE } },
|
||||
};
|
||||
const decorReveal = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: { opacity: 1, transition: { duration: 1.1, delay: 0.38 } },
|
||||
};
|
||||
|
||||
type CTABannerProps = {
|
||||
badge?: { icon: string; label: string };
|
||||
heading: React.ReactNode;
|
||||
body: string;
|
||||
primary: { href: string; icon: string; label: string };
|
||||
secondary: { href: string; icon: string; label: string };
|
||||
decorations?: string[];
|
||||
};
|
||||
|
||||
export function CTABanner({ badge, heading, body, primary, secondary, decorations }: CTABannerProps) {
|
||||
return (
|
||||
<section className="bg-white px-2 pt-2 pb-2 md:px-3 md:pt-3 md:pb-3 lg:px-4 lg:pt-4 lg:pb-4 xl:px-5 xl:pt-4 xl:pb-4 2xl:px-6 2xl:pb-5">
|
||||
<motion.div
|
||||
className="w-full overflow-hidden rounded-[28px] bg-gradient-to-br from-[#1A0322] via-[#2B0040] to-[#0A000D] text-white"
|
||||
variants={stagger}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={VP}
|
||||
>
|
||||
<div className="px-6 py-12 md:px-10 md:py-14 lg:px-14 xl:py-12 text-center relative">
|
||||
{decorations?.map((grad, i) => (
|
||||
<motion.div
|
||||
key={i}
|
||||
className="absolute inset-0 pointer-events-none"
|
||||
style={{ background: `radial-gradient(${grad})` }}
|
||||
variants={decorReveal}
|
||||
/>
|
||||
))}
|
||||
|
||||
<div className="relative z-10 max-w-2xl mx-auto">
|
||||
{badge && (
|
||||
<motion.span
|
||||
className="inline-flex items-center gap-2 px-3 py-1 rounded-full border border-white/15 text-white/70 text-xs font-black uppercase tracking-widest mb-6"
|
||||
variants={textReveal}
|
||||
>
|
||||
<MaterialIcon icon={badge.icon} size={14} />
|
||||
{badge.label}
|
||||
</motion.span>
|
||||
)}
|
||||
|
||||
<motion.h2
|
||||
className="font-display text-3xl md:text-4xl xl:text-5xl font-black mb-4 tracking-tight leading-tight"
|
||||
variants={textReveal}
|
||||
>
|
||||
{heading}
|
||||
</motion.h2>
|
||||
|
||||
<motion.p
|
||||
className="text-sm md:text-base text-white/70 mb-8 leading-relaxed max-w-lg mx-auto"
|
||||
variants={textReveal}
|
||||
>
|
||||
{body}
|
||||
</motion.p>
|
||||
|
||||
<motion.div className="flex flex-wrap justify-center gap-3.5" variants={buttonReveal}>
|
||||
<Link
|
||||
href={primary.href}
|
||||
className="bg-[#6330D6] hover:bg-[#5225b8] text-white font-black px-8 py-3.5 rounded-xl text-sm hover:shadow-xl transition-all duration-200 active:scale-95 flex items-center gap-2"
|
||||
>
|
||||
<MaterialIcon icon={primary.icon} size={18} />
|
||||
<span>{primary.label}</span>
|
||||
</Link>
|
||||
<Link
|
||||
href={secondary.href}
|
||||
className="bg-white/10 hover:bg-white/15 border border-white/20 text-white font-black px-8 py-3.5 rounded-xl text-sm transition-all duration-200 active:scale-95 flex items-center gap-2"
|
||||
>
|
||||
<MaterialIcon icon={secondary.icon} size={18} />
|
||||
<span>{secondary.label}</span>
|
||||
</Link>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { MaterialIcon } from "./MaterialIcon";
|
||||
|
||||
const EASE = [0.22, 1, 0.36, 1] as [number, number, number, number];
|
||||
|
||||
export type FAQItem = {
|
||||
question: string;
|
||||
answer: string;
|
||||
@@ -31,25 +34,30 @@ export function FAQAccordion({ items }: FAQAccordionProps) {
|
||||
<span className="text-sm md:text-base font-black text-[#16001D] leading-snug">
|
||||
{item.question}
|
||||
</span>
|
||||
<div
|
||||
className={`flex-shrink-0 w-8 h-8 rounded-full bg-[#6330D6]/8 flex items-center justify-center text-[#6330D6] transition-transform duration-300 ${
|
||||
openIndex === index ? "rotate-180" : ""
|
||||
}`}
|
||||
<motion.div
|
||||
className="flex-shrink-0 w-8 h-8 rounded-full bg-[#6330D6]/8 flex items-center justify-center text-[#6330D6]"
|
||||
animate={{ rotate: openIndex === index ? 180 : 0 }}
|
||||
transition={{ duration: 0.3, ease: EASE }}
|
||||
>
|
||||
<MaterialIcon icon="expand_more" size={20} />
|
||||
</div>
|
||||
</motion.div>
|
||||
</button>
|
||||
<div
|
||||
className={`overflow-hidden transition-all duration-300 ease-in-out ${
|
||||
openIndex === index
|
||||
? "max-h-[500px] opacity-100"
|
||||
: "max-h-0 opacity-0"
|
||||
}`}
|
||||
>
|
||||
<p className="px-6 pb-5 text-sm md:text-base text-[#5E5866] leading-relaxed border-t border-[#6330D6]/5 pt-4">
|
||||
{item.answer}
|
||||
</p>
|
||||
</div>
|
||||
<AnimatePresence initial={false}>
|
||||
{openIndex === index && (
|
||||
<motion.div
|
||||
key="content"
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{ height: "auto", opacity: 1 }}
|
||||
exit={{ height: 0, opacity: 0 }}
|
||||
transition={{ duration: 0.3, ease: EASE }}
|
||||
style={{ overflow: "hidden" }}
|
||||
>
|
||||
<p className="px-6 pb-5 text-sm md:text-base text-[#5E5866] leading-relaxed border-t border-[#6330D6]/5 pt-4">
|
||||
{item.answer}
|
||||
</p>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import { MaterialIcon } from "./MaterialIcon";
|
||||
import { fadeUp, staggerContainer, viewport } from "@/lib/animations";
|
||||
|
||||
type FeatureStatsItem = {
|
||||
icon: string;
|
||||
@@ -13,11 +17,18 @@ type FeatureStatsRowProps = {
|
||||
|
||||
export function FeatureStatsRow({ items, className = "" }: FeatureStatsRowProps) {
|
||||
return (
|
||||
<div className={`w-full py-6 md:py-8 ${className}`}>
|
||||
<motion.div
|
||||
className={`w-full py-6 md:py-8 ${className}`}
|
||||
variants={staggerContainer}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={viewport}
|
||||
>
|
||||
<div className="grid grid-cols-2 lg:flex lg:flex-row items-center justify-between gap-y-6 gap-x-4 lg:gap-0">
|
||||
{items.map((item, index) => (
|
||||
<div
|
||||
<motion.div
|
||||
key={item.title}
|
||||
variants={fadeUp}
|
||||
className={`flex items-center gap-3.5 lg:flex-1 px-3 lg:px-8 text-left ${
|
||||
index < items.length - 1
|
||||
? "lg:border-r lg:border-[#6330D6]/10"
|
||||
@@ -37,9 +48,9 @@ export function FeatureStatsRow({ items, className = "" }: FeatureStatsRowProps)
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ export function GlassInfoCard({ text, icon, className = "" }: GlassInfoCardProps
|
||||
<MaterialIcon icon={icon} size={14} />
|
||||
</div>
|
||||
) : (
|
||||
<span className="w-2.5 h-2.5 rounded-full bg-[#6330D6] shrink-0 animate-pulse" />
|
||||
<span className="w-2.5 h-2.5 rounded-full bg-[#6330D6] shrink-0 motion-safe:animate-pulse" />
|
||||
)}
|
||||
<span className="text-sm font-extrabold text-[#16001D] whitespace-nowrap">
|
||||
{text}
|
||||
|
||||
Reference in New Issue
Block a user