animation fixed in section
This commit is contained in:
@@ -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}</>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user