SPB Git forge

spb/rareindex

Public
54commits 1branches 0releases
7.1 MBsize
maindefault branch
10 days agolast push
TypeScript 61.9% HTML 37.2% SQL 0.7%
3.8 KB · 87 lines tsx
Raw Blame History
1'use client';23import Link from 'next/link';4import { useEffect, useState } from 'react';5import { Bell } from 'lucide-react';67interface SessionInfo {8  user: { id: string; email: string; name: string | null; handle: string | null; avatarUrl: string | null; displayCurrency: string; role: string } | null;9  unread?: number;10}1112/**13 * Header user menu (drop-in for the site header): shows "Sign in" when anonymous, otherwise the14 * avatar, unread badge and a compact menu. Fetches /api/auth/session client-side so the header15 * stays static/cacheable.16 */17export function UserMenu() {18  const [info, setInfo] = useState<SessionInfo | null>(null);19  useEffect(() => {20    let alive = true;21    fetch('/api/auth/session', { credentials: 'same-origin' })22      .then((r) => (r.ok ? r.json() : { user: null }))23      .then((j: SessionInfo) => {24        if (alive) setInfo(j);25      })26      .catch(() => {27        if (alive) setInfo({ user: null });28      });29    return () => {30      alive = false;31    };32  }, []);33  if (!info?.user) {34    return (35      <Link href="/login" className="hidden rounded-md border border-border px-2.5 py-1.5 text-[13px] font-medium text-fg hover:bg-inset sm:inline-flex">36        Sign in37      </Link>38    );39  }40  const u = info.user;41  const initial = (u.name ?? u.email).slice(0, 1).toUpperCase();42  return (43    <div className="flex items-center gap-1">44      <Link href="/notifications" aria-label="Inbox" className="relative inline-flex h-8 w-8 items-center justify-center rounded-md text-muted hover:bg-inset hover:text-fg">45        <Bell className="h-4 w-4" />46        {info.unread ? <span className="absolute -right-0.5 -top-0.5 min-w-[16px] rounded-full bg-alert px-1 text-center text-[10px] font-semibold leading-4 text-white">{info.unread > 99 ? '99+' : info.unread}</span> : null}47      </Link>48      <details className="relative">49        <summary className="flex cursor-pointer list-none items-center gap-2 rounded-md px-1.5 py-1 hover:bg-inset [&::-webkit-details-marker]:hidden">50          {u.avatarUrl ? (51            // eslint-disable-next-line @next/next/no-img-element52            <img src={u.avatarUrl} alt="" className="h-7 w-7 rounded-full border border-border object-cover" />53          ) : (54            <span className="inline-flex h-7 w-7 items-center justify-center rounded-full bg-accent text-[11px] font-semibold text-accent-fg">{initial}</span>55          )}56          <span className="hidden max-w-[120px] truncate text-[13px] font-medium md:inline">{u.name ?? u.email.split('@')[0]}</span>57        </summary>58        <div className="absolute right-0 top-full z-50 mt-1 w-56 rounded-lg border border-border bg-elevated p-1.5 shadow-pop">59          <div className="px-2 py-1.5">60            <p className="truncate text-[13px] font-medium">{u.name ?? 'Collector'}</p>61            <p className="truncate text-[11px] text-subtle">{u.email}</p>62          </div>63          <div className="my-1 border-t border-border" />64          {[65            ['/collections', 'Collections'],66            ['/watchlist', 'Watchlist'],67            ['/alerts', 'Alerts'],68            ['/deals', 'Deal Radar'],69            ['/account/settings', 'Settings'],70            ...(u.handle ? [[`/u/${u.handle}`, 'Public profile']] : []),71          ].map(([href, label]) => (72            <Link key={href} href={href!} className="block rounded-sm px-2 py-1.5 text-[13px] text-muted hover:bg-inset hover:text-fg">73              {label}74            </Link>75          ))}76          <div className="my-1 border-t border-border" />77          <form action="/api/auth/logout" method="post">78            <button type="submit" className="block w-full rounded-sm px-2 py-1.5 text-left text-[13px] text-muted hover:bg-inset hover:text-fg">79              Sign out80            </button>81          </form>82        </div>83      </details>84    </div>85  );86}87