Skip to content
File

Blob: src/client/components/header.tsx

typescript82 lines
1import { Layers, LogOut } from "lucide-react";
2import { Link, useLocation } from "@tanstack/react-router";
3import { useQueryClient } from "@tanstack/react-query";
4 
5import { $api } from "@/client/lib/api";
6import { meQueryKey, useMe } from "@/client/auth/use-me";
7import { paths } from "@/client/lib/paths";
8import { toast } from "@/client/lib/toast";
9 
10const navItems = [
11 { to: paths.pats, label: "Tokens" },
12 { to: paths.calendars, label: "Calendars" },
13 { to: paths.addressbooks, label: "Contacts" },
14 { to: paths.files, label: "Files" },
15] as const;
16 
17export function Header() {
18 const location = useLocation();
19 const me = useMe();
20 const queryClient = useQueryClient();
21 const logout = $api.useMutation("post", "/api/v1/auth/logout", {
22 onSuccess: () => {
23 queryClient.setQueryData(meQueryKey, null);
24 toast.info("Signed out.");
25 window.location.assign(paths.login);
26 },
27 });
28 
29 const isAuthed = me.data != null;
30 
31 return (
32 <header className="sticky top-0 z-50 border-b border-zinc-800/60 bg-zinc-900">
33 <div className="mx-auto flex max-w-7xl items-center justify-between gap-4 px-4 py-3 sm:px-6">
34 <Link to={paths.me} className="group flex items-center gap-3 transition-opacity hover:opacity-80">
35 <span className="inline-grid h-9 w-9 place-items-center">
36 <Layers
37 className="h-6 w-6 text-accent-400 transition-transform duration-200 group-hover:-rotate-6"
38 strokeWidth={2}
39 aria-hidden="true"
40 />
41 </span>
42 <span className="hidden sm:block">
43 <strong className="block text-sm font-semibold text-zinc-100">dab</strong>
44 <small className="block text-xs text-zinc-400">your DAV-native data home</small>
45 </span>
46 </Link>
47
48 {isAuthed ? (
49 <nav className="flex items-center gap-1 overflow-x-auto" aria-label="Primary">
50 {navItems.map(({ to, label }) => {
51 const active = location.pathname.startsWith(to);
52 return (
53 <Link
54 key={to}
55 to={to}
56 aria-current={active ? "page" : undefined}
57 className={`rounded-lg px-3 py-1.5 text-sm transition-colors ${
58 active
59 ? "bg-accent-500/10 text-accent-300"
60 : "text-zinc-400 hover:bg-zinc-800/70 hover:text-zinc-100"
61 }`}
62 >
63 {label}
64 </Link>
65 );
66 })}
67 <button
68 type="button"
69 onClick={() => logout.mutate({})}
70 disabled={logout.isPending}
71 aria-label="Sign out"
72 className="ml-2 inline-flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-sm text-zinc-400 transition-colors hover:bg-zinc-800/70 hover:text-zinc-100 disabled:opacity-60"
73 >
74 <LogOut className="h-3.5 w-3.5" strokeWidth={2} aria-hidden="true" />
75 </button>
76 </nav>
77 ) : null}
78 </div>
79 </header>
80 );
81}