spb/spboucher.ai Public
spboucher.ai — personal website of Simon-Pierre Boucher.
TypeScript 93.6%
HTML 5.4%
CSS 0.9%
1/*2 site-header.tsx3 spboucher.ai Web4 Author: Simon-Pierre Boucher5 Mail: contact@spboucher.ai6*/78"use client";910import * as React from "react";11import Link from "next/link";12import { usePathname } from "next/navigation";13import { Menu, X } from "lucide-react";1415import { cn } from "@/lib/utils";16import { Button } from "@/components/ui/button";17import { ThemeToggle } from "@/components/theme-toggle";1819const navItems = [20 { href: "/", label: "Home" },21 { href: "/research", label: "Research" },22 { href: "/teaching", label: "Teaching" },23 { href: "/apps", label: "Apps" },24 { href: "/blog", label: "Blog" },25 { href: "/cv", label: "CV" },26];2728export function SiteHeader() {29 const pathname = usePathname();30 const [open, setOpen] = React.useState(false);3132 const isActive = (href: string) =>33 pathname === href || (href !== "/" && pathname.startsWith(`${href}/`));3435 return (36 <header className="sticky top-0 z-50 w-full border-b border-border/80 bg-background/88 backdrop-blur-xl">37 <div className="mx-auto flex h-16 max-w-5xl items-center justify-between px-4 sm:px-6">38 <Link href="/" className="group flex items-baseline gap-2.5">39 <span40 aria-hidden="true"41 className="self-center border border-foreground bg-foreground px-1.5 py-0.5 font-mono text-[10px] font-semibold tracking-[0.08em] text-background transition-colors duration-300 group-hover:bg-transparent group-hover:text-foreground"42 >43 SB44 </span>45 <span className="font-display text-[1.05rem] font-semibold tracking-tight">46 Simon-Pierre Boucher47 </span>48 </Link>4950 <nav className="hidden items-center gap-0.5 md:flex" aria-label="Main">51 {navItems.map((item) => (52 <Link53 key={item.href}54 href={item.href}55 className={cn(56 "relative px-3 py-2 font-mono text-[0.7rem] font-medium uppercase tracking-[0.14em] transition-colors",57 isActive(item.href)58 ? "text-primary after:absolute after:inset-x-3 after:-bottom-[1.35rem] after:h-[2px] after:bg-primary"59 : "text-muted-foreground hover:text-foreground"60 )}61 >62 {item.label}63 </Link>64 ))}65 <span aria-hidden="true" className="mx-2 h-4 w-px bg-border" />66 <ThemeToggle />67 </nav>6869 <div className="flex items-center gap-1 md:hidden">70 <ThemeToggle />71 <Button72 variant="ghost"73 size="icon"74 aria-label={open ? "Close menu" : "Open menu"}75 aria-expanded={open}76 onClick={() => setOpen(!open)}77 >78 {open ? <X /> : <Menu />}79 </Button>80 </div>81 </div>8283 {open && (84 <nav85 className="border-t border-border/80 px-4 pb-4 pt-2 md:hidden"86 aria-label="Mobile"87 >88 {navItems.map((item) => (89 <Link90 key={item.href}91 href={item.href}92 onClick={() => setOpen(false)}93 className={cn(94 "flex items-center justify-between border-b border-border/50 px-1 py-3 font-mono text-xs font-medium uppercase tracking-[0.14em] transition-colors last:border-0",95 isActive(item.href)96 ? "text-primary"97 : "text-muted-foreground hover:text-foreground"98 )}99 >100 {item.label}101 {isActive(item.href) && (102 <span aria-hidden="true" className="h-1.5 w-1.5 bg-primary" />103 )}104 </Link>105 ))}106 </nav>107 )}108 </header>109 );110}111