Files
0pt/components/header.tsx
T
adminandClaude Sonnet 4.6 2ca769efbb feat: dark mode toggle, copy-to-clipboard order IDs, accessibility
- Dark mode toggle (●/○) in desktop + mobile header nav
- ThemeProvider wired in layout.tsx (class-based, light default)
- Header scroll-glass uses CSS color-mix for dark-mode correctness
- PortalShell main gets id="main-content" for skip-link target
- Portal order table: copy order ID to clipboard with toast confirmation
- Admin page: toast success/error on status advance
- Skip-to-content link in layout.tsx (sr-only, visible on keyboard focus)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 17:45:05 +02:00

137 lines
5.5 KiB
TypeScript

"use client";
import Link from "next/link";
import { useEffect, useState } from "react";
import { useTheme } from "next-themes";
import { useT } from "@/components/locale-provider";
import { LocaleSwitcher } from "@/components/locale-switcher";
const NAV: { label: { en: string; sv: string }; href: string }[] = [
{ label: { en: "Services", sv: "Tjänster" }, href: "/tjanster" },
{ label: { en: "Catalog", sv: "Katalog" }, href: "/katalog" },
{ label: { en: "How it works", sv: "Så funkar det" }, href: "/#motorn" },
{ label: { en: "Sources", sv: "Källor" }, href: "/kallor" },
{ label: { en: "Pricing", sv: "Priser" }, href: "/#priser" },
];
function Wordmark() {
return (
<Link href="/" className="group flex items-baseline gap-2">
<span className="text-xl font-semibold tracking-[-0.04em]">0pt</span>
<span
className="inline-block h-[5px] w-[5px] translate-y-[-2px] transition-transform duration-300 group-hover:translate-y-[-4px]"
style={{ backgroundColor: "var(--accent-red)" }}
/>
<span className="font-mono text-[10px] uppercase tracking-[0.22em] text-muted-foreground">your data</span>
</Link>
);
}
function ThemeToggle() {
const { resolvedTheme, setTheme } = useTheme();
const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []);
if (!mounted) return <span className="h-9 w-9" />;
const dark = resolvedTheme === "dark";
return (
<button
type="button"
aria-label={dark ? "Switch to light mode" : "Switch to dark mode"}
onClick={() => setTheme(dark ? "light" : "dark")}
className="flex h-9 w-9 items-center justify-center border border-border font-mono text-[11px] text-muted-foreground transition-colors hover:border-foreground hover:text-foreground"
>
{dark ? "○" : "●"}
</button>
);
}
export function Header() {
const t = useT();
const [scrolled, setScrolled] = useState(false);
const [open, setOpen] = useState(false);
useEffect(() => {
const onScroll = () => setScrolled(window.scrollY > 40);
window.addEventListener("scroll", onScroll, { passive: true });
onScroll();
return () => window.removeEventListener("scroll", onScroll);
}, []);
return (
<header
className="sticky top-0 z-50 border-b border-border transition-colors"
style={{
backgroundColor: scrolled ? "color-mix(in srgb, var(--background) 82%, transparent)" : "var(--background)",
backdropFilter: scrolled ? "blur(8px)" : "none",
}}
>
<div className="mx-auto flex h-16 max-w-[80rem] items-center justify-between px-6">
<Wordmark />
<nav className="hidden items-center md:flex">
{NAV.map((n) => (
<Link
key={n.href}
href={n.href}
className="group relative px-4 py-2 font-mono text-[12px] uppercase tracking-[0.12em] text-muted-foreground transition-colors hover:text-foreground"
>
{t(n.label)}
<span aria-hidden className="pointer-events-none absolute bottom-1 left-4 right-4 h-px origin-left scale-x-0 bg-foreground transition-transform duration-300 group-hover:scale-x-100" />
</Link>
))}
</nav>
<div className="hidden items-center gap-4 md:flex">
<ThemeToggle />
<LocaleSwitcher />
<Link href="/portal" className="px-3 py-2 font-mono text-[12px] uppercase tracking-[0.12em] text-muted-foreground transition-colors hover:text-foreground">
{t({ en: "Sign in", sv: "Logga in" })}
</Link>
<Link
href="/bestall"
className="inline-flex items-center gap-2 bg-foreground px-5 py-2.5 font-mono text-[12px] uppercase tracking-[0.12em] text-background transition-colors hover:bg-[var(--accent-red)]"
>
{t({ en: "Order", sv: "Beställ" })}
<span aria-hidden>→</span>
</Link>
</div>
<button
aria-label="Menu"
onClick={() => setOpen((v) => !v)}
className="flex h-9 w-9 items-center justify-center border border-border transition-colors hover:border-foreground md:hidden"
>
<span className="font-mono text-sm">{open ? "✕" : "≡"}</span>
</button>
</div>
{open ? (
<div className="border-t border-border bg-background md:hidden">
<div className="flex flex-col px-6 py-2">
{NAV.map((n, i) => (
<Link
key={n.href}
href={n.href}
onClick={() => setOpen(false)}
className="flex items-baseline justify-between border-b border-border py-4 font-mono text-[12px] uppercase tracking-[0.12em] text-muted-foreground transition-colors hover:text-foreground"
>
<span>{t(n.label)}</span>
<span className="text-[10px] tabular-nums text-muted-foreground/50">{String(i + 1).padStart(2, "0")}</span>
</Link>
))}
<div className="flex items-center justify-between py-4">
<div className="flex items-center gap-3">
<ThemeToggle />
<LocaleSwitcher />
</div>
<Link href="/bestall" onClick={() => setOpen(false)} className="bg-foreground px-5 py-3 font-mono text-[12px] uppercase tracking-[0.12em] text-background">
{t({ en: "Order", sv: "Beställ" })} →
</Link>
</div>
</div>
</div>
) : null}
</header>
);
}