- HeroSection: 3-col layout (copy | concentric circles+dashboard | display headline) - 'use client' + framer-motion entrance animations (slide in from sides, scale centre) - DashboardPreview inline component (compact portal mockup) - Two floating stat mini-cards (Avg Tax Saved, Response Time) - Mobile: stacked layout, right headline column hidden, H1 in left column - ContainerScroll: simplified — removed 72rem scroll container and scroll transforms; now plain layout wrapper with CSS fadeInUp entrance - Header: logo size increased h-10 → h-13 (40px → 52px) - fix: escape apostrophes in ProcessSection, SolutionSection, TestimonialsSection - fix: remove unused customSize param from SpotlightCard - docs: update CONTEXT_HANDOVER.md with session 4 changes Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
276 lines
9.2 KiB
TypeScript
276 lines
9.2 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect, useRef } from 'react';
|
|
import Image from 'next/image';
|
|
import Link from 'next/link';
|
|
import { usePathname } from 'next/navigation';
|
|
import { motion, AnimatePresence } from 'framer-motion';
|
|
import { Button } from '@/components/ui/Button';
|
|
import { MenuIcon, XIcon, ChevronDownIcon } from '@/components/ui/icons';
|
|
import { InteractiveMenu } from '@/components/ui/InteractiveMenu';
|
|
import { Home, Briefcase, BookOpen, Info, Mail } from 'lucide-react';
|
|
import { cn } from '@/lib/utils';
|
|
|
|
const NAV_ITEMS = [
|
|
{ label: 'Home', href: '/' },
|
|
{
|
|
label: 'Services',
|
|
href: '/services',
|
|
dropdown: [
|
|
{
|
|
label: 'Bookkeeping',
|
|
href: '/services/bookkeeping',
|
|
desc: 'Accurate records, zero stress',
|
|
},
|
|
{ label: 'Tax Returns', href: '/services/tax-returns', desc: 'Every allowance claimed' },
|
|
{ label: 'Payroll', href: '/services/payroll', desc: 'On time, every time' },
|
|
{ label: 'VAT Returns', href: '/services/vat-returns', desc: 'MTD-compliant filing' },
|
|
],
|
|
},
|
|
{ label: 'About', href: '/about' },
|
|
{ label: 'Blog', href: '/blog' },
|
|
{ label: 'Contact', href: '/contact' },
|
|
];
|
|
|
|
const DOCK_ITEMS = [
|
|
{ label: 'Home', icon: Home, href: '/' },
|
|
{ label: 'Services', icon: Briefcase, href: '/services' },
|
|
{ label: 'Blog', icon: BookOpen, href: '/blog' },
|
|
{ label: 'About', icon: Info, href: '/about' },
|
|
{ label: 'Contact', icon: Mail, href: '/contact' },
|
|
];
|
|
|
|
function AxilLogo({ className }: { className?: string }) {
|
|
return (
|
|
<Image
|
|
src="/logo-axil.png"
|
|
alt="Axil Accounting"
|
|
width={140}
|
|
height={97}
|
|
className={cn('w-auto', className)}
|
|
priority
|
|
/>
|
|
);
|
|
}
|
|
|
|
/** Nav links shared between static header and floating pill */
|
|
function NavLinks({
|
|
pathname,
|
|
dropdownOpen,
|
|
setDropdownOpen,
|
|
size = 'md',
|
|
}: {
|
|
pathname: string;
|
|
dropdownOpen: boolean;
|
|
setDropdownOpen: (v: boolean) => void;
|
|
size?: 'sm' | 'md';
|
|
}) {
|
|
const textSize = size === 'sm' ? 'text-xs' : 'text-sm';
|
|
const px = size === 'sm' ? 'px-3' : 'px-4';
|
|
|
|
return (
|
|
<>
|
|
{NAV_ITEMS.map((item) =>
|
|
item.dropdown ? (
|
|
<div
|
|
key={item.label}
|
|
className="relative"
|
|
onMouseEnter={() => setDropdownOpen(true)}
|
|
onMouseLeave={() => setDropdownOpen(false)}
|
|
>
|
|
<button
|
|
className={cn(
|
|
'rounded-pill text-charcoal hover:bg-emerald-mist flex items-center gap-1 py-2 font-medium transition-colors',
|
|
textSize,
|
|
px,
|
|
)}
|
|
aria-expanded={dropdownOpen}
|
|
aria-haspopup="true"
|
|
>
|
|
{item.label}
|
|
<ChevronDownIcon
|
|
size={12}
|
|
className={`transition-transform duration-200 ${dropdownOpen ? 'rotate-180' : ''}`}
|
|
/>
|
|
</button>
|
|
{dropdownOpen && (
|
|
<div className="rounded-hero border-charcoal/10 absolute top-full left-0 mt-2 w-60 border bg-white p-2 shadow-xl">
|
|
{item.dropdown.map((child) => (
|
|
<Link
|
|
key={child.label}
|
|
href={child.href}
|
|
className="rounded-card hover:bg-emerald-mist flex flex-col px-4 py-3 transition-colors"
|
|
onClick={() => setDropdownOpen(false)}
|
|
>
|
|
<span className="text-charcoal text-sm font-medium">{child.label}</span>
|
|
<span className="text-muted text-xs">{child.desc}</span>
|
|
</Link>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
) : (
|
|
<Link
|
|
key={item.label}
|
|
href={item.href}
|
|
className={cn(
|
|
'rounded-pill hover:bg-emerald-mist py-2 font-medium transition-colors',
|
|
textSize,
|
|
px,
|
|
pathname === item.href ? 'text-emerald font-semibold' : 'text-charcoal',
|
|
)}
|
|
>
|
|
{item.label}
|
|
</Link>
|
|
),
|
|
)}
|
|
</>
|
|
);
|
|
}
|
|
|
|
export function Header() {
|
|
const lastScrollY = useRef(0);
|
|
const [atTop, setAtTop] = useState(true);
|
|
const [floatVisible, setFloatVisible] = useState(false);
|
|
const [mobileOpen, setMobileOpen] = useState(false);
|
|
const [dropdownOpen, setDropdownOpen] = useState(false);
|
|
const [dropdownOpenF, setDropdownOpenF] = useState(false);
|
|
const pathname = usePathname();
|
|
|
|
useEffect(() => {
|
|
const onScroll = () => {
|
|
const y = window.scrollY;
|
|
const direction = y - lastScrollY.current;
|
|
lastScrollY.current = y;
|
|
|
|
if (y < 60) {
|
|
setAtTop(true);
|
|
setFloatVisible(false);
|
|
} else {
|
|
setAtTop(false);
|
|
setFloatVisible(direction < 0); // scroll up → show pill
|
|
}
|
|
};
|
|
window.addEventListener('scroll', onScroll, { passive: true });
|
|
return () => window.removeEventListener('scroll', onScroll);
|
|
}, []);
|
|
|
|
return (
|
|
<>
|
|
{/* ─── Static header (visible only at top of page) ─── */}
|
|
<AnimatePresence>
|
|
{atTop && (
|
|
<motion.header
|
|
key="static-header"
|
|
initial={{ opacity: 0, y: -16 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
exit={{ opacity: 0, y: -16 }}
|
|
transition={{ duration: 0.2 }}
|
|
className="border-charcoal/10 fixed inset-x-0 top-0 z-50 border-b bg-white/90 shadow-sm backdrop-blur-md"
|
|
>
|
|
<div className="mx-auto flex h-18 max-w-[1440px] items-center justify-between px-4 sm:px-6 lg:px-8 xl:px-16">
|
|
<Link href="/" aria-label="Axil Accountants — Home">
|
|
<AxilLogo className="h-13" />
|
|
</Link>
|
|
|
|
<nav className="hidden items-center gap-1 lg:flex">
|
|
<NavLinks
|
|
pathname={pathname}
|
|
dropdownOpen={dropdownOpen}
|
|
setDropdownOpen={setDropdownOpen}
|
|
/>
|
|
</nav>
|
|
|
|
<div className="hidden items-center gap-3 lg:flex">
|
|
<Button size="sm" trailingArrow>
|
|
Book Free Consultation
|
|
</Button>
|
|
</div>
|
|
|
|
<button
|
|
className="rounded-card text-charcoal hover:bg-emerald-mist flex size-10 items-center justify-center transition-colors lg:hidden"
|
|
onClick={() => setMobileOpen((v) => !v)}
|
|
aria-label={mobileOpen ? 'Close menu' : 'Open menu'}
|
|
>
|
|
{mobileOpen ? <XIcon size={20} /> : <MenuIcon size={20} />}
|
|
</button>
|
|
</div>
|
|
</motion.header>
|
|
)}
|
|
</AnimatePresence>
|
|
|
|
{/* ─── Floating pill nav (appears on scroll-up) ─── */}
|
|
<AnimatePresence mode="wait">
|
|
{floatVisible && (
|
|
<motion.div
|
|
key="floating-nav"
|
|
initial={{ opacity: 0, y: -100 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
exit={{ opacity: 0, y: -100 }}
|
|
transition={{ duration: 0.2 }}
|
|
className="fixed inset-x-0 top-5 z-[5000] flex justify-center px-4"
|
|
>
|
|
<div className="border-charcoal/10 flex items-center gap-3 rounded-full border bg-white/95 py-2 pr-2 pl-3 shadow-lg backdrop-blur-md">
|
|
{/* Logo */}
|
|
<Link href="/" aria-label="Home" className="mr-1 flex-shrink-0">
|
|
<AxilLogo className="h-7" />
|
|
</Link>
|
|
|
|
{/* Divider */}
|
|
<div className="bg-charcoal/10 h-5 w-px" />
|
|
|
|
{/* Nav links */}
|
|
<nav className="hidden items-center lg:flex">
|
|
<NavLinks
|
|
pathname={pathname}
|
|
dropdownOpen={dropdownOpenF}
|
|
setDropdownOpen={setDropdownOpenF}
|
|
size="sm"
|
|
/>
|
|
</nav>
|
|
|
|
{/* CTA */}
|
|
<Button size="sm" trailingArrow className="ml-1 flex-shrink-0">
|
|
Book Free Consultation
|
|
</Button>
|
|
</div>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
|
|
{/* ─── Mobile slide-out menu ─── */}
|
|
{mobileOpen && (
|
|
<div className="fixed inset-0 z-40 flex flex-col bg-white pt-18 lg:hidden">
|
|
<nav className="flex flex-col gap-1 px-4 py-6">
|
|
{NAV_ITEMS.map((item) => (
|
|
<Link
|
|
key={item.label}
|
|
href={item.href}
|
|
className="rounded-card text-charcoal hover:bg-emerald-mist px-4 py-4 text-lg font-medium transition-colors"
|
|
onClick={() => setMobileOpen(false)}
|
|
>
|
|
{item.label}
|
|
</Link>
|
|
))}
|
|
</nav>
|
|
<div className="px-4">
|
|
<Button size="lg" className="w-full justify-center" trailingArrow>
|
|
Book Free Consultation
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* ─── Mobile floating dock ─── */}
|
|
<div className="fixed bottom-5 left-1/2 z-40 -translate-x-1/2 lg:hidden">
|
|
<InteractiveMenu
|
|
items={DOCK_ITEMS}
|
|
accentColor="var(--emerald)"
|
|
onSelect={(_, item) => {
|
|
if (item.href) window.location.href = item.href;
|
|
}}
|
|
/>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|