feat: port website header with mega-menu, add showcase bg to hero, fix carousel container size
This commit is contained in:
309
components/marketing/Header.tsx
Normal file
309
components/marketing/Header.tsx
Normal file
@@ -0,0 +1,309 @@
|
||||
'use client';
|
||||
import React from 'react';
|
||||
import Link from 'next/link';
|
||||
import Image from 'next/image';
|
||||
import { Button } from '@/components/ui/button-website';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { MenuToggleIcon } from '@/components/ui/menu-toggle-icon';
|
||||
import { createPortal } from 'react-dom';
|
||||
import {
|
||||
NavigationMenu,
|
||||
NavigationMenuContent,
|
||||
NavigationMenuItem,
|
||||
NavigationMenuLink,
|
||||
NavigationMenuList,
|
||||
NavigationMenuTrigger,
|
||||
} from '@/components/ui/navigation-menu';
|
||||
import { LucideIcon } from 'lucide-react';
|
||||
import {
|
||||
BarChart3,
|
||||
Eye,
|
||||
Funnel,
|
||||
Send,
|
||||
FileText,
|
||||
Puzzle,
|
||||
HelpCircle,
|
||||
} from 'lucide-react';
|
||||
|
||||
type LinkItem = {
|
||||
title: string;
|
||||
href: string;
|
||||
icon?: LucideIcon;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
const featureLinks: LinkItem[] = [
|
||||
{
|
||||
title: 'Dashboard',
|
||||
href: '/features#dashboard',
|
||||
icon: BarChart3,
|
||||
description: 'Real-time traffic overview',
|
||||
},
|
||||
{
|
||||
title: 'Visitor Insights',
|
||||
href: '/features#visitors',
|
||||
icon: Eye,
|
||||
description: 'Browser, device & geo data',
|
||||
},
|
||||
{
|
||||
title: 'Conversion Funnels',
|
||||
href: '/features#funnels',
|
||||
icon: Funnel,
|
||||
description: 'Multi-step drop-off analysis',
|
||||
},
|
||||
{
|
||||
title: 'Email Reports',
|
||||
href: '/features#reports',
|
||||
icon: Send,
|
||||
description: 'Scheduled inbox summaries',
|
||||
},
|
||||
];
|
||||
|
||||
const resourceLinks: LinkItem[] = [
|
||||
{
|
||||
title: 'Installation',
|
||||
href: '/installation',
|
||||
icon: FileText,
|
||||
description: 'Setup guides & code snippets',
|
||||
},
|
||||
{
|
||||
title: 'Integrations',
|
||||
href: '/integrations',
|
||||
icon: Puzzle,
|
||||
description: '75+ framework guides',
|
||||
},
|
||||
{
|
||||
title: 'FAQ',
|
||||
href: '/faq',
|
||||
icon: HelpCircle,
|
||||
description: 'Common questions answered',
|
||||
},
|
||||
];
|
||||
|
||||
export function Header() {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const scrolled = useScroll(10);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (open) {
|
||||
document.body.style.overflow = 'hidden';
|
||||
} else {
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
return () => {
|
||||
document.body.style.overflow = '';
|
||||
};
|
||||
}, [open]);
|
||||
|
||||
return (
|
||||
<header
|
||||
className={cn('sticky top-0 z-50 w-full border-b border-transparent', {
|
||||
'border-white/[0.06]': scrolled,
|
||||
})}
|
||||
>
|
||||
<div className={cn("absolute inset-0 -z-10 transition-opacity duration-300", scrolled ? "opacity-100 backdrop-blur-xl bg-neutral-950/60 supports-[backdrop-filter]:bg-neutral-950/50" : "opacity-0")} />
|
||||
<nav className="mx-auto flex h-16 w-full max-w-6xl items-center justify-between px-6 my-3">
|
||||
<div className="flex items-center gap-5">
|
||||
<Link href="/" className="hover:bg-accent rounded-md p-2 flex items-center gap-2">
|
||||
<Image
|
||||
src="/pulse_icon_no_margins.png"
|
||||
alt="Pulse"
|
||||
width={36}
|
||||
height={36}
|
||||
priority
|
||||
className="object-contain w-8 h-8"
|
||||
unoptimized
|
||||
/>
|
||||
<span className="text-xl font-bold text-foreground tracking-tight">
|
||||
Pulse
|
||||
</span>
|
||||
</Link>
|
||||
<NavigationMenu className="hidden md:flex">
|
||||
<NavigationMenuList>
|
||||
{/* Features dropdown */}
|
||||
<NavigationMenuItem>
|
||||
<NavigationMenuTrigger className="bg-transparent">Features</NavigationMenuTrigger>
|
||||
<NavigationMenuContent className="bg-transparent p-1 pr-1.5">
|
||||
<ul className="grid w-[32rem] grid-cols-2 gap-2 rounded-md border border-white/[0.06] bg-white/[0.04] p-2">
|
||||
{featureLinks.map((item, i) => (
|
||||
<li key={i}>
|
||||
<ListItem title={item.title} href={item.href} icon={item.icon} description={item.description} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</NavigationMenuContent>
|
||||
</NavigationMenuItem>
|
||||
|
||||
{/* Resources dropdown */}
|
||||
<NavigationMenuItem>
|
||||
<NavigationMenuTrigger className="bg-transparent">Resources</NavigationMenuTrigger>
|
||||
<NavigationMenuContent className="bg-transparent p-1 pr-1.5 pb-1.5">
|
||||
<div className="grid w-[32rem] grid-cols-2 gap-2">
|
||||
<ul className="space-y-2 rounded-md border border-white/[0.06] bg-white/[0.04] p-2">
|
||||
{resourceLinks.map((item, i) => (
|
||||
<li key={i}>
|
||||
<ListItem {...item} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className="flex flex-col justify-center gap-3 p-4">
|
||||
<p className="text-sm font-medium text-foreground">Need help?</p>
|
||||
<p className="text-xs text-muted-foreground leading-relaxed">
|
||||
Questions about setup, integrations, or billing — we typically respond within 24-48 hours.
|
||||
</p>
|
||||
<a href="mailto:support@ciphera.net" className="text-sm font-medium text-brand-orange hover:underline">
|
||||
support@ciphera.net →
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</NavigationMenuContent>
|
||||
</NavigationMenuItem>
|
||||
|
||||
{/* Pricing standalone link */}
|
||||
<NavigationMenuItem>
|
||||
<NavigationMenuLink asChild>
|
||||
<Link href="/pricing" className="group inline-flex h-9 w-max items-center justify-center rounded-md bg-transparent px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none">
|
||||
Pricing
|
||||
</Link>
|
||||
</NavigationMenuLink>
|
||||
</NavigationMenuItem>
|
||||
</NavigationMenuList>
|
||||
</NavigationMenu>
|
||||
</div>
|
||||
<div className="hidden items-center gap-2 md:flex">
|
||||
<Button variant="outline" asChild>
|
||||
<a href="https://pulse.ciphera.net">Sign In</a>
|
||||
</Button>
|
||||
<Button asChild>
|
||||
<a href="https://pulse.ciphera.net">Get Started</a>
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 md:hidden">
|
||||
<Button
|
||||
size="icon"
|
||||
variant="outline"
|
||||
onClick={() => setOpen(!open)}
|
||||
aria-expanded={open}
|
||||
aria-controls="mobile-menu"
|
||||
aria-label="Toggle menu"
|
||||
>
|
||||
<MenuToggleIcon open={open} className="size-5" duration={300} />
|
||||
</Button>
|
||||
</div>
|
||||
</nav>
|
||||
<MobileMenu open={open} className="flex flex-col justify-between gap-2 overflow-y-auto">
|
||||
<NavigationMenu className="max-w-full">
|
||||
<div className="flex w-full flex-col gap-y-2">
|
||||
<span className="text-sm">Features</span>
|
||||
{featureLinks.map((link) => (
|
||||
<ListItem key={link.title} title={link.title} href={link.href} icon={link.icon} description={link.description} />
|
||||
))}
|
||||
<span className="text-sm">Resources</span>
|
||||
{resourceLinks.map((link) => (
|
||||
<ListItem key={link.title} {...link} />
|
||||
))}
|
||||
<Link
|
||||
href="/pricing"
|
||||
className="flex flex-row gap-x-2 rounded-sm p-2 transition-colors hover:bg-white/[0.06]"
|
||||
>
|
||||
<div className="flex aspect-square size-12 items-center justify-center rounded-md border border-white/[0.08] bg-white/[0.05] shadow-sm p-2">
|
||||
<BarChart3 className="text-foreground size-5" />
|
||||
</div>
|
||||
<div className="flex flex-col items-start justify-center">
|
||||
<span className="text-sm font-medium">Pricing</span>
|
||||
<span className="text-muted-foreground text-xs">Plans & billing</span>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
</NavigationMenu>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Button variant="outline" className="w-full bg-transparent" asChild>
|
||||
<a href="https://pulse.ciphera.net">
|
||||
Sign In
|
||||
</a>
|
||||
</Button>
|
||||
<Button className="w-full" asChild>
|
||||
<a href="https://pulse.ciphera.net">
|
||||
Get Started
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</MobileMenu>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
type MobileMenuProps = React.ComponentProps<'div'> & {
|
||||
open: boolean;
|
||||
};
|
||||
|
||||
function MobileMenu({ open, children, className, ...props }: MobileMenuProps) {
|
||||
if (!open || typeof window === 'undefined') return null;
|
||||
|
||||
return createPortal(
|
||||
<div
|
||||
id="mobile-menu"
|
||||
className={cn(
|
||||
'bg-background/95 supports-[backdrop-filter]:bg-background/50 backdrop-blur-lg',
|
||||
'fixed top-16 right-0 bottom-0 left-0 z-40 flex flex-col overflow-hidden border-y md:hidden',
|
||||
)}
|
||||
>
|
||||
<div
|
||||
data-slot={open ? 'open' : 'closed'}
|
||||
className={cn(
|
||||
'data-[slot=open]:animate-in data-[slot=open]:zoom-in-95 ease-out',
|
||||
'size-full p-4',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>,
|
||||
document.body,
|
||||
);
|
||||
}
|
||||
|
||||
function ListItem({
|
||||
title,
|
||||
description,
|
||||
icon: Icon,
|
||||
className,
|
||||
href,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NavigationMenuLink> & LinkItem) {
|
||||
return (
|
||||
<NavigationMenuLink className={cn('w-full flex flex-row gap-x-2 data-[active=true]:focus:bg-white/[0.06] data-[active=true]:hover:bg-white/[0.06] data-[active=true]:text-accent-foreground hover:bg-white/[0.06] hover:text-accent-foreground focus:bg-white/[0.06] focus:text-accent-foreground rounded-sm p-2 transition-colors', className)} {...props} asChild>
|
||||
<Link href={href || '#'}>
|
||||
<div className="flex aspect-square size-12 items-center justify-center rounded-md border border-white/[0.08] bg-white/[0.05] shadow-sm p-2">
|
||||
{Icon ? (
|
||||
<Icon className="text-foreground size-5" />
|
||||
) : null}
|
||||
</div>
|
||||
<div className="flex flex-col items-start justify-center">
|
||||
<span className="text-sm font-medium">{title}</span>
|
||||
<span className="text-muted-foreground text-xs">{description}</span>
|
||||
</div>
|
||||
</Link>
|
||||
</NavigationMenuLink>
|
||||
);
|
||||
}
|
||||
|
||||
function useScroll(threshold: number) {
|
||||
const [scrolled, setScrolled] = React.useState(false);
|
||||
|
||||
const onScroll = React.useCallback(() => {
|
||||
setScrolled(window.scrollY > threshold);
|
||||
}, [threshold]);
|
||||
|
||||
React.useEffect(() => {
|
||||
window.addEventListener('scroll', onScroll);
|
||||
return () => window.removeEventListener('scroll', onScroll);
|
||||
}, [onScroll]);
|
||||
|
||||
React.useEffect(() => {
|
||||
onScroll();
|
||||
}, [onScroll]);
|
||||
|
||||
return scrolled;
|
||||
}
|
||||
Reference in New Issue
Block a user