SPB Git

spb/spboucher.ai Public

spboucher.ai — personal website of Simon-Pierre Boucher.

TypeScript 93.6% HTML 5.4% CSS 0.9%
3.7 KB · 111 lines tsx
Raw Blame History
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