Axil_website/src/components/layout/Header.tsx
Vadym Samoilenko 83a8878f4a feat: redesign HeroSection to 3-column MinimalistHero layout
- 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>
2026-02-22 21:20:42 +00:00

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>
</>
);
}