- Move cohorta-banner.png into navbar left slot (height 42px, w-auto) - Navbar py-2/py-1.5 sized to logo — no left spacer needed - PublicLayout main: pt-20 → pt-[58px] to match new navbar height - Hero: mt/pt offsets updated to 58px; content starts 45px below navbar - Hero: banner block removed (now lives in navbar) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
196 lines
7.5 KiB
TypeScript
196 lines
7.5 KiB
TypeScript
import { useState, useEffect } from 'react';
|
|
import { Link, useNavigate, useLocation } from 'react-router-dom';
|
|
import { useAuth } from '@/contexts/AuthContext';
|
|
import UserDropdown from '@/components/brand/UserDropdown';
|
|
import { Menu, X } from 'lucide-react';
|
|
import { cn } from '@/lib/utils';
|
|
import { motion, useScroll, useSpring, useReducedMotion } from 'framer-motion';
|
|
import { LimelightNav } from '@/components/ui/LimelightNav';
|
|
|
|
const navLinks = [
|
|
{ label: 'Home', to: '/', anchor: null },
|
|
{ label: 'Product', to: '/', anchor: 'product' },
|
|
{ label: 'Pricing', to: '/', anchor: 'pricing' },
|
|
{ label: 'Contact', to: 'mailto:hello@ai-impress.com', anchor: null, external: true },
|
|
];
|
|
|
|
export default function Header() {
|
|
const { isAuthenticated } = useAuth();
|
|
const navigate = useNavigate();
|
|
const location = useLocation();
|
|
const [scrolled, setScrolled] = useState(false);
|
|
const [mobileOpen, setMobileOpen] = useState(false);
|
|
const [activeIndex, setActiveIndex] = useState(0);
|
|
const shouldReduce = useReducedMotion();
|
|
const { scrollYProgress } = useScroll();
|
|
const scaleX = useSpring(scrollYProgress, { stiffness: 200, damping: 30 });
|
|
|
|
useEffect(() => {
|
|
const onScroll = () => setScrolled(window.scrollY > 24);
|
|
window.addEventListener('scroll', onScroll, { passive: true });
|
|
return () => window.removeEventListener('scroll', onScroll);
|
|
}, []);
|
|
|
|
// Track active section based on scroll position (landing page only)
|
|
useEffect(() => {
|
|
if (location.pathname !== '/') { setActiveIndex(-1); return; }
|
|
const anchorSections = ['product', 'pricing']; // maps to index 1, 2
|
|
const update = () => {
|
|
let current = 0;
|
|
for (let i = anchorSections.length - 1; i >= 0; i--) {
|
|
const el = document.getElementById(anchorSections[i]);
|
|
if (el && el.getBoundingClientRect().top <= 120) { current = i + 1; break; }
|
|
}
|
|
setActiveIndex(current);
|
|
};
|
|
window.addEventListener('scroll', update, { passive: true });
|
|
update();
|
|
return () => window.removeEventListener('scroll', update);
|
|
}, [location.pathname]);
|
|
|
|
const handleNavClick = (anchor: string | null, to: string, external?: boolean) => {
|
|
if (external) return;
|
|
if (!anchor) { navigate(to); return; }
|
|
if (location.pathname === '/') {
|
|
document.getElementById(anchor)?.scrollIntoView({ behavior: 'smooth' });
|
|
} else {
|
|
navigate(`/?scroll=${anchor}`);
|
|
}
|
|
};
|
|
|
|
const limelightItems = navLinks.map(({ label, to, anchor, external }, i) => ({
|
|
id: label,
|
|
label,
|
|
onClick: external ? () => { window.location.href = to; } : () => handleNavClick(anchor, to),
|
|
icon: (
|
|
<span className="text-sm font-medium text-foreground whitespace-nowrap">
|
|
{label}
|
|
</span>
|
|
),
|
|
}));
|
|
|
|
return (
|
|
<header
|
|
className={cn(
|
|
'fixed top-0 left-0 right-0 z-50 transition-all duration-300',
|
|
scrolled ? 'py-2' : 'py-2'
|
|
)}
|
|
>
|
|
{!shouldReduce && (
|
|
<motion.div
|
|
className="absolute top-0 left-0 right-0 h-[2px] bg-primary origin-left z-10"
|
|
style={{ scaleX }}
|
|
/>
|
|
)}
|
|
|
|
<div className="max-w-7xl mx-auto px-5">
|
|
<div
|
|
className={cn(
|
|
'flex items-center justify-between px-4 py-1.5 rounded-2xl transition-all duration-300',
|
|
scrolled
|
|
? 'bg-[hsl(220_22%_10%/0.88)] backdrop-blur-xl border border-border/50 shadow-lg shadow-black/30'
|
|
: 'bg-transparent'
|
|
)}
|
|
>
|
|
{/* Left: Logo banner */}
|
|
<Link to="/" className="flex-shrink-0">
|
|
<img
|
|
src={`${import.meta.env.BASE_URL}cohorta-banner.png`}
|
|
alt="Cohorta"
|
|
style={{ height: '42px', objectFit: 'contain', objectPosition: 'left center' }}
|
|
className="w-auto"
|
|
draggable={false}
|
|
/>
|
|
</Link>
|
|
|
|
{/* Center: LimelightNav */}
|
|
<div className="hidden md:flex items-center justify-center flex-1">
|
|
<LimelightNav
|
|
items={limelightItems}
|
|
activeIndex={activeIndex >= 0 ? activeIndex : 0}
|
|
/>
|
|
</div>
|
|
|
|
{/* Right: auth buttons */}
|
|
<div className="hidden md:flex items-center justify-end gap-3 w-[160px]">
|
|
{isAuthenticated ? (
|
|
<UserDropdown />
|
|
) : (
|
|
<>
|
|
<Link
|
|
to="/login"
|
|
className="px-4 py-2 rounded-full text-sm font-medium text-foreground/80 hover:text-foreground transition-colors duration-200 whitespace-nowrap"
|
|
>
|
|
Log in
|
|
</Link>
|
|
<button
|
|
onClick={() => navigate('/register')}
|
|
className="px-4 py-2 rounded-full text-sm font-semibold bg-primary text-primary-foreground hover:bg-primary/90 transition-all duration-200 shadow-sm whitespace-nowrap"
|
|
>
|
|
Get started
|
|
</button>
|
|
</>
|
|
)}
|
|
</div>
|
|
|
|
{/* Mobile toggle */}
|
|
<button
|
|
className="md:hidden p-2 rounded-full text-muted-foreground hover:text-foreground ml-auto"
|
|
onClick={() => setMobileOpen(v => !v)}
|
|
aria-label="Toggle menu"
|
|
>
|
|
{mobileOpen ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />}
|
|
</button>
|
|
</div>
|
|
|
|
{/* Mobile menu */}
|
|
{mobileOpen && (
|
|
<div className="md:hidden mt-2 bg-[hsl(220_22%_13%/0.97)] backdrop-blur-xl border border-border/60 rounded-2xl p-4 shadow-xl animate-slide-down">
|
|
<nav className="flex flex-col gap-1 mb-4">
|
|
{navLinks.map(({ label, to, anchor, external }) => {
|
|
if (external) return (
|
|
<a key={label} href={to}
|
|
className="px-4 py-2.5 rounded-xl text-sm font-medium text-muted-foreground hover:text-foreground hover:bg-secondary/60 transition-all"
|
|
onClick={() => setMobileOpen(false)}
|
|
>{label}</a>
|
|
);
|
|
return (
|
|
<button key={label}
|
|
className="px-4 py-2.5 rounded-xl text-sm font-medium text-muted-foreground hover:text-foreground hover:bg-secondary/60 transition-all text-left"
|
|
onClick={() => { handleNavClick(anchor, to); setMobileOpen(false); }}
|
|
>{label}</button>
|
|
);
|
|
})}
|
|
</nav>
|
|
<div className="flex flex-col gap-2 border-t border-border pt-3">
|
|
{isAuthenticated ? (
|
|
<Link to="/dashboard" onClick={() => setMobileOpen(false)}>
|
|
<button className="w-full px-4 py-2.5 rounded-xl text-sm font-semibold bg-primary text-primary-foreground">
|
|
My account
|
|
</button>
|
|
</Link>
|
|
) : (
|
|
<>
|
|
<Link
|
|
to="/login"
|
|
className="px-4 py-2.5 rounded-xl text-sm font-medium text-center text-foreground border border-border"
|
|
onClick={() => setMobileOpen(false)}
|
|
>
|
|
Log in
|
|
</Link>
|
|
<Link
|
|
to="/register"
|
|
className="px-4 py-2.5 rounded-xl text-sm font-semibold text-center bg-primary text-primary-foreground"
|
|
onClick={() => setMobileOpen(false)}
|
|
>
|
|
Get started free
|
|
</Link>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</header>
|
|
);
|
|
}
|