cohorta/src/components/layout/Header.tsx
Vadym Samoilenko c0536df9e2 feat: logo banner in navbar, 45px hero gap, navbar sized to logo
- 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>
2026-05-23 22:22:27 +01:00

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