ux(nav): unify navbar palette, localize, fix locale-aware metadata
Visual audit found several issues in the navbar / header layer; this pass addresses the cluster. - AuthBar: localize sign in/out + dev-disabled message; switch matrix-green (#00ff9c) to gold (#e0c080) to match the magazine palette. - NavLink (new): client component reading usePathname to mark the active section with aria-current="page" and a gold underline+fill, so the user knows where they are inside the entity hub. - Locale toggle: bump font to 12px, widen the pill, lift the active state's contrast. Hard reload on switch (window.location.reload) instead of router.refresh — that is what makes the <title>, <html lang> and og:locale actually update on toggle (router.refresh kept stale layout metadata). - Root layout: convert static `export const metadata` to generateMetadata() so title/description/OG/twitter all read the cookie per request. Drop the duplicate floating LocaleToggle at bottom-left; the navbar carries it now. - BureauNav: drop the cyan-on-home / gold-on-bureau split (palette mismatch with SiteHeader); single gold tone; localize "home"/"case files" + aria labels; mount AuthBar inside the bar so sub-pages get a sign-in surface. - Pages using BureauNav: drop the standalone <AuthBar /> below it (now duplicated). Affected: /bureau, /c/[slug], /h/[hypothesisId], /jobs/[id]. - Lift nav-link rest text from #9aa6b8 to #cbd2dd for WCAG-AA on near-black. Verified live: title/lang/og:locale switch with cookie; active link gold indicator on /sightings; "Entrar"/"Sign in" localizes; no double AuthBar. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
912758e18a
commit
2ea350e283
10 changed files with 216 additions and 128 deletions
|
|
@ -4,7 +4,6 @@
|
|||
* Reader-facing list of every assembled narrative. No detective surfacing,
|
||||
* no artefact dumps. Just stories with hooks.
|
||||
*/
|
||||
import { AuthBar } from "@/components/auth-bar";
|
||||
import { BureauNav } from "@/components/bureau-nav";
|
||||
import { CaseLibrary } from "@/components/case-library";
|
||||
import { getLocale } from "@/components/locale-toggle";
|
||||
|
|
@ -18,7 +17,6 @@ export default async function BureauPage() {
|
|||
return (
|
||||
<div className="min-h-screen bg-[#0a0e1a] text-[#e7ecf3]">
|
||||
<BureauNav locale={locale} crumbs={[{ label: locale === "en" ? "case files" : "casos" }]} />
|
||||
<AuthBar />
|
||||
<div className="mx-auto max-w-5xl px-4 py-6 pt-4">
|
||||
<header className="mb-6 border-b border-[rgba(224,192,128,0.32)] pb-4">
|
||||
<h1 className="font-mono text-3xl text-[#e0c080]">
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import { readFile } from "node:fs/promises";
|
|||
import path from "node:path";
|
||||
import type { Metadata } from "next";
|
||||
import { MarkdownBody } from "@/components/markdown-body";
|
||||
import { AuthBar } from "@/components/auth-bar";
|
||||
import { BureauNav } from "@/components/bureau-nav";
|
||||
import { getLocale } from "@/components/locale-toggle";
|
||||
import { pickLocaleBody } from "@/lib/bilingual";
|
||||
|
|
@ -156,7 +155,6 @@ export default async function CaseReportPage({
|
|||
{ label: locale === "en" ? "case files" : "casos", href: "/bureau" },
|
||||
{ label: title.length > 32 ? title.slice(0, 32) + "…" : title },
|
||||
]} />
|
||||
<AuthBar />
|
||||
|
||||
{/* Full-bleed editorial hero — only when a painterly illustration
|
||||
exists for this case. Other cases get the plain title header. */}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ import Link from "next/link";
|
|||
import { readFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { pgQuery } from "@/lib/retrieval/db";
|
||||
import { AuthBar } from "@/components/auth-bar";
|
||||
import { BureauNav } from "@/components/bureau-nav";
|
||||
import { getLocale } from "@/components/locale-toggle";
|
||||
import { RedTeamRequestButton } from "@/components/red-team-request-button";
|
||||
|
|
@ -204,7 +203,6 @@ export default async function HypothesisPage({
|
|||
{ label: "hypotheses", href: "/bureau#hypotheses" },
|
||||
{ label: hypothesisId },
|
||||
]} />
|
||||
<AuthBar />
|
||||
<div className="mx-auto max-w-5xl px-4 py-6 pt-4">
|
||||
|
||||
{/* Header */}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@
|
|||
import { notFound } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import { pgQuery } from "@/lib/retrieval/db";
|
||||
import { AuthBar } from "@/components/auth-bar";
|
||||
import { BureauNav } from "@/components/bureau-nav";
|
||||
import { getLocale } from "@/components/locale-toggle";
|
||||
import { JobStatusPoller } from "@/components/job-status-poller";
|
||||
|
|
@ -83,7 +82,6 @@ export default async function JobPage({
|
|||
{ label: "jobs", href: "/bureau#jobs" },
|
||||
{ label: job.job_id.slice(0, 8) },
|
||||
]} />
|
||||
<AuthBar />
|
||||
<div className="mx-auto max-w-5xl px-4 py-6 pt-4">
|
||||
|
||||
<div className={`rounded-lg border border-[rgba(127,219,255,0.18)] bg-gradient-to-br ${detectiveBg} to-transparent p-5`}>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import type { Metadata } from "next";
|
|||
import { JetBrains_Mono, Inter, Fraunces } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import { CommandPalette } from "@/components/command-palette";
|
||||
import { LocaleToggle, getLocale } from "@/components/locale-toggle";
|
||||
import { getLocale } from "@/components/locale-toggle";
|
||||
|
||||
const inter = Inter({ subsets: ["latin"], variable: "--font-sans" });
|
||||
const mono = JetBrains_Mono({ subsets: ["latin"], variable: "--font-mono" });
|
||||
|
|
@ -16,67 +16,90 @@ const fraunces = Fraunces({
|
|||
|
||||
const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://disclosure.top";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
metadataBase: new URL(SITE_URL),
|
||||
title: {
|
||||
default: "The Disclosure Bureau — UAP/UFO desclassificado",
|
||||
template: "%s · The Disclosure Bureau",
|
||||
},
|
||||
description:
|
||||
"122 documentos desclassificados do Departamento de Guerra dos EUA sobre UAP/UFO. " +
|
||||
"Pilotos, oficiais e físicos relatam o que viram. Avistamentos, testemunhas, " +
|
||||
"objetos catalogados — arquivos abertos da divulgação.",
|
||||
keywords: [
|
||||
"UAP", "UFO", "ovni", "desclassificado", "war.gov", "Pentagon",
|
||||
"Kenneth Arnold", "Mantell", "green fireballs", "Sandia",
|
||||
"Project Blue Book", "Robertson Panel", "AATIP",
|
||||
"documentos desclassificados", "avistamento", "testemunha",
|
||||
"disclosure", "divulgação UFO",
|
||||
],
|
||||
authors: [{ name: "The Disclosure Bureau" }],
|
||||
openGraph: {
|
||||
type: "website",
|
||||
siteName: "The Disclosure Bureau",
|
||||
title: "The Disclosure Bureau — UAP/UFO desclassificado",
|
||||
description:
|
||||
"122 documentos desclassificados. Pilotos, oficiais, físicos relatam o que viram.",
|
||||
locale: "pt_BR",
|
||||
alternateLocale: ["en_US"],
|
||||
url: SITE_URL,
|
||||
images: [{
|
||||
url: `${SITE_URL}/api/static/processing/case-art/green-fireballs-narrative.png`,
|
||||
width: 2000, height: 1125,
|
||||
alt: "The Disclosure Bureau — UAP/UFO desclassificado",
|
||||
}],
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: "The Disclosure Bureau",
|
||||
description: "Arquivos UAP/UFO desclassificados, narrados a partir do registro público.",
|
||||
images: [`${SITE_URL}/api/static/processing/case-art/green-fireballs-narrative.png`],
|
||||
},
|
||||
alternates: {
|
||||
canonical: "/",
|
||||
languages: { "pt-BR": "/", "en-US": "/" },
|
||||
},
|
||||
robots: {
|
||||
index: true,
|
||||
follow: true,
|
||||
googleBot: {
|
||||
// Site-wide metadata is locale-aware: title, description, OG, twitter, html
|
||||
// lang and openGraph.locale must all switch when the cookie toggles. Static
|
||||
// `export const metadata` is resolved without cookies, so the title would
|
||||
// stay pinned to whichever language built the page. generateMetadata() runs
|
||||
// per request and can read the cookie via getLocale().
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
const locale = await getLocale();
|
||||
const t = locale === "en"
|
||||
? {
|
||||
title: "The Disclosure Bureau — declassified UAP/UFO",
|
||||
template: "%s · The Disclosure Bureau",
|
||||
desc: "122 declassified US Department of War documents on UAP/UFO. " +
|
||||
"Pilots, officers and physicists describe what they saw. Sightings, " +
|
||||
"witnesses, catalogued craft — the open files of disclosure.",
|
||||
og_desc: "122 declassified documents. Pilots, officers, physicists describe what they saw.",
|
||||
tw_desc: "Declassified UAP/UFO files, narrated from the public record.",
|
||||
ogLocale: "en_US",
|
||||
altLocale: "pt_BR",
|
||||
}
|
||||
: {
|
||||
title: "The Disclosure Bureau — UAP/UFO desclassificado",
|
||||
template: "%s · The Disclosure Bureau",
|
||||
desc: "122 documentos desclassificados do Departamento de Guerra dos EUA sobre UAP/UFO. " +
|
||||
"Pilotos, oficiais e físicos relatam o que viram. Avistamentos, testemunhas, " +
|
||||
"objetos catalogados — arquivos abertos da divulgação.",
|
||||
og_desc: "122 documentos desclassificados. Pilotos, oficiais, físicos relatam o que viram.",
|
||||
tw_desc: "Arquivos UAP/UFO desclassificados, narrados a partir do registro público.",
|
||||
ogLocale: "pt_BR",
|
||||
altLocale: "en_US",
|
||||
};
|
||||
|
||||
return {
|
||||
metadataBase: new URL(SITE_URL),
|
||||
title: { default: t.title, template: t.template },
|
||||
description: t.desc,
|
||||
keywords: [
|
||||
"UAP", "UFO", "ovni", "desclassificado", "war.gov", "Pentagon",
|
||||
"Kenneth Arnold", "Mantell", "green fireballs", "Sandia",
|
||||
"Project Blue Book", "Robertson Panel", "AATIP",
|
||||
"documentos desclassificados", "avistamento", "testemunha",
|
||||
"disclosure", "divulgação UFO",
|
||||
],
|
||||
authors: [{ name: "The Disclosure Bureau" }],
|
||||
openGraph: {
|
||||
type: "website",
|
||||
siteName: "The Disclosure Bureau",
|
||||
title: t.title,
|
||||
description: t.og_desc,
|
||||
locale: t.ogLocale,
|
||||
alternateLocale: [t.altLocale],
|
||||
url: SITE_URL,
|
||||
images: [{
|
||||
url: `${SITE_URL}/api/static/processing/case-art/green-fireballs-narrative.png`,
|
||||
width: 2000, height: 1125,
|
||||
alt: t.title,
|
||||
}],
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: "The Disclosure Bureau",
|
||||
description: t.tw_desc,
|
||||
images: [`${SITE_URL}/api/static/processing/case-art/green-fireballs-narrative.png`],
|
||||
},
|
||||
alternates: {
|
||||
canonical: "/",
|
||||
languages: { "pt-BR": "/", "en-US": "/" },
|
||||
},
|
||||
robots: {
|
||||
index: true,
|
||||
follow: true,
|
||||
"max-image-preview": "large",
|
||||
"max-snippet": -1,
|
||||
googleBot: {
|
||||
index: true,
|
||||
follow: true,
|
||||
"max-image-preview": "large",
|
||||
"max-snippet": -1,
|
||||
},
|
||||
},
|
||||
},
|
||||
other: {
|
||||
// GEO (Generative Engine Optimization) — explicit primary-source statement
|
||||
// so retrieval-augmented assistants understand what the site is.
|
||||
"ai:purpose":
|
||||
"Public, citation-linked archive of declassified UAP/UFO documents from the US Department of War. Each case file is grounded in primary-source memos with verbatim quotes and bbox-cropped imagery.",
|
||||
"ai:license": "Documents are US Government works in the public domain; site narrative © Disclosure Bureau, CC-BY 4.0.",
|
||||
},
|
||||
};
|
||||
other: {
|
||||
"ai:purpose":
|
||||
"Public, citation-linked archive of declassified UAP/UFO documents from the US Department of War. Each case file is grounded in primary-source memos with verbatim quotes and bbox-cropped imagery.",
|
||||
"ai:license": "Documents are US Government works in the public domain; site narrative © Disclosure Bureau, CC-BY 4.0.",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default async function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
const locale = await getLocale();
|
||||
|
|
@ -105,9 +128,6 @@ export default async function RootLayout({ children }: { children: React.ReactNo
|
|||
</a>
|
||||
{children}
|
||||
<CommandPalette />
|
||||
<div className="fixed bottom-3 left-3 z-40 opacity-70 hover:opacity-100 transition">
|
||||
<LocaleToggle current={locale} />
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -13,10 +13,26 @@ interface Profile {
|
|||
daily_quota?: number;
|
||||
}
|
||||
|
||||
export function AuthBar() {
|
||||
const COPY = {
|
||||
"pt-br": {
|
||||
sign_in: "Entrar",
|
||||
sign_out: "Sair",
|
||||
auth_disabled: "auth: desativada (dev)",
|
||||
msgs_today: "mensagens hoje",
|
||||
},
|
||||
en: {
|
||||
sign_in: "Sign in",
|
||||
sign_out: "Sign out",
|
||||
auth_disabled: "auth: disabled (dev)",
|
||||
msgs_today: "msgs today",
|
||||
},
|
||||
} as const;
|
||||
|
||||
export function AuthBar({ locale = "pt-br" }: { locale?: "pt-br" | "en" }) {
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [profile, setProfile] = useState<Profile | null>(null);
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
const t = COPY[locale];
|
||||
|
||||
useEffect(() => {
|
||||
if (!isSupabaseConfigured()) {
|
||||
|
|
@ -45,7 +61,7 @@ export function AuthBar() {
|
|||
if (!isSupabaseConfigured()) {
|
||||
return (
|
||||
<div className="font-mono text-[10px] text-[#5a6678] uppercase tracking-widest">
|
||||
auth: disabled (dev)
|
||||
{t.auth_disabled}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -54,10 +70,10 @@ export function AuthBar() {
|
|||
return (
|
||||
<Link
|
||||
href="/auth/signin"
|
||||
className="font-mono text-xs px-3 py-1 border border-[rgba(0,255,156,0.32)]
|
||||
text-[#00ff9c] hover:bg-[rgba(0,255,156,0.08)] rounded"
|
||||
className="font-mono text-[12px] px-3 py-1 border border-[rgba(224,192,128,0.35)]
|
||||
text-[#e0c080] hover:bg-[rgba(224,192,128,0.08)] rounded-full transition-colors"
|
||||
>
|
||||
sign in
|
||||
{t.sign_in}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
|
@ -69,17 +85,17 @@ export function AuthBar() {
|
|||
<div>{profile.display_name ?? user.email}</div>
|
||||
<div>
|
||||
${(profile.total_cost_usd ?? 0).toFixed(2)} / ${(profile.budget_cap_usd ?? 0).toFixed(2)} ·{" "}
|
||||
{profile.daily_used ?? 0}/{profile.daily_quota ?? 0} msgs today
|
||||
{profile.daily_used ?? 0}/{profile.daily_quota ?? 0} {t.msgs_today}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<form action="/auth/signout" method="POST">
|
||||
<button
|
||||
type="submit"
|
||||
className="font-mono text-[10px] px-2 py-1 text-[#8896aa] hover:text-[#ff3344]
|
||||
border border-transparent hover:border-[#ff3344] rounded"
|
||||
className="font-mono text-[11px] px-2.5 py-1 text-[#9aa6b8] hover:text-[#ff3344]
|
||||
border border-transparent hover:border-[#ff3344] rounded-full transition-colors"
|
||||
>
|
||||
sign out
|
||||
{t.sign_out}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,16 +1,18 @@
|
|||
/**
|
||||
* BureauNav — top navigation bar present on every bureau page.
|
||||
* BureauNav — top navigation bar present on every bureau sub-page.
|
||||
*
|
||||
* Solves "I'm stuck on this page, there's no way back" UX. Shows:
|
||||
* - "← Home" (always → /)
|
||||
* - "🔎 Bureau" (always → /bureau)
|
||||
* - "Bureau" (always → /bureau)
|
||||
* - Breadcrumb trail of current page (passed via props)
|
||||
* - Locale toggle + auth pill on the right
|
||||
*
|
||||
* Server component. No client-side history needed because the parent links
|
||||
* are deterministic (every bureau page has a known parent in the hierarchy).
|
||||
* Visually harmonised with SiteHeader (single gold palette, no cyan); labels
|
||||
* localised.
|
||||
*/
|
||||
import Link from "next/link";
|
||||
import { ArrowLeft, Home } from "lucide-react";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
import { AuthBar } from "./auth-bar";
|
||||
import { LocaleToggle, type Locale } from "./locale-toggle";
|
||||
|
||||
export interface Crumb {
|
||||
|
|
@ -20,49 +22,56 @@ export interface Crumb {
|
|||
href?: string;
|
||||
}
|
||||
|
||||
export function BureauNav({ crumbs, locale }: { crumbs: Crumb[]; locale?: Locale }) {
|
||||
const COPY = {
|
||||
"pt-br": { home: "home", bureau: "casos", aria_home: "Voltar para a home", aria_bureau: "Listagem de casos" },
|
||||
en: { home: "home", bureau: "case files", aria_home: "Back to home", aria_bureau: "Case file library" },
|
||||
} as const;
|
||||
|
||||
export function BureauNav({ crumbs, locale = "pt-br" }: { crumbs: Crumb[]; locale?: Locale }) {
|
||||
const t = COPY[locale];
|
||||
return (
|
||||
<nav className="sticky top-0 z-30 bg-[#0a0e1a]/95 backdrop-blur border-b border-[rgba(224,192,128,0.18)]">
|
||||
<div className="mx-auto max-w-6xl px-4 py-2 flex items-center gap-3 flex-wrap text-[12px] font-mono">
|
||||
<nav
|
||||
aria-label={locale === "en" ? "Breadcrumb" : "Trilha"}
|
||||
className="sticky top-0 z-30 bg-[#0a0e1a]/95 backdrop-blur border-b border-[rgba(224,192,128,0.18)]"
|
||||
>
|
||||
<div className="mx-auto max-w-6xl px-4 py-2.5 flex items-center gap-3 flex-wrap text-[12px] font-mono">
|
||||
<Link
|
||||
href="/"
|
||||
className="inline-flex items-center gap-1 px-2 py-1 rounded border border-[rgba(127,219,255,0.30)] text-[#7fdbff] hover:bg-[rgba(127,219,255,0.08)]"
|
||||
aria-label="Voltar para a home"
|
||||
className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full border border-[rgba(224,192,128,0.30)] text-[#cbd2dd] hover:text-[#e0c080] hover:bg-[rgba(224,192,128,0.08)] transition-colors"
|
||||
aria-label={t.aria_home}
|
||||
>
|
||||
<ArrowLeft size={12} />
|
||||
<Home size={12} />
|
||||
<span>home</span>
|
||||
<ArrowLeft size={12} aria-hidden="true" />
|
||||
<span>{t.home}</span>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/bureau"
|
||||
className="inline-flex items-center gap-1 px-2 py-1 rounded border border-[#e0c080] text-[#e0c080] hover:bg-[rgba(224,192,128,0.08)]"
|
||||
aria-label="Hub do bureau"
|
||||
className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full border border-[rgba(224,192,128,0.50)] text-[#e0c080] hover:bg-[rgba(224,192,128,0.08)] transition-colors"
|
||||
aria-label={t.aria_bureau}
|
||||
>
|
||||
🔎 <span>bureau</span>
|
||||
<span>{t.bureau}</span>
|
||||
</Link>
|
||||
|
||||
{crumbs.length > 0 && (
|
||||
<div className="text-[11px] text-[#5a6678] flex items-center gap-1.5 flex-wrap">
|
||||
<span className="mx-1">/</span>
|
||||
<span className="mx-1" aria-hidden="true">/</span>
|
||||
{crumbs.map((c, i) => (
|
||||
<span key={`${c.label}-${i}`} className="inline-flex items-center gap-1.5">
|
||||
{c.href ? (
|
||||
<Link href={c.href} className="hover:text-[#7fdbff]">{c.label}</Link>
|
||||
<Link href={c.href} className="hover:text-[#e0c080] transition-colors">{c.label}</Link>
|
||||
) : (
|
||||
<span className="text-[#e7ecf3]">{c.label}</span>
|
||||
<span className="text-[#e7ecf3]" aria-current="page">{c.label}</span>
|
||||
)}
|
||||
{i < crumbs.length - 1 && <span>/</span>}
|
||||
{i < crumbs.length - 1 && <span aria-hidden="true">/</span>}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{locale && (
|
||||
<span className="ml-auto">
|
||||
<LocaleToggle current={locale} variant="navbar" />
|
||||
</span>
|
||||
)}
|
||||
<span className="ml-auto inline-flex items-center gap-2">
|
||||
<LocaleToggle current={locale} variant="navbar" />
|
||||
<AuthBar locale={locale} />
|
||||
</span>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Globe } from "lucide-react";
|
||||
import type { Locale } from "./locale-toggle";
|
||||
|
||||
|
|
@ -8,30 +7,54 @@ export function LocaleToggleClient({
|
|||
current,
|
||||
variant = "retro",
|
||||
}: { current: Locale; variant?: "retro" | "navbar" }) {
|
||||
const router = useRouter();
|
||||
|
||||
function setLocale(next: Locale) {
|
||||
if (next === current) return;
|
||||
document.cookie = `locale=${next}; path=/; max-age=${60 * 60 * 24 * 365}; SameSite=Lax`;
|
||||
router.refresh();
|
||||
// Hard reload, not router.refresh(): the latter re-fetches the route's
|
||||
// server tree but Next.js can keep a stale <title>/<meta> because metadata
|
||||
// is rendered up in the layout tree which isn't always re-invalidated.
|
||||
// A full navigation re-runs generateMetadata against the new cookie, so
|
||||
// the tab title, OG tags, and html lang attribute all switch together.
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
if (variant === "navbar") {
|
||||
// Gold-amber magazine theme to match SiteHeader / BureauNav.
|
||||
// The active locale fills with gold; the inactive one stays muted text.
|
||||
// Sized to be unmistakably readable in the navbar — the previous 11px pill
|
||||
// disappeared visually next to the other links.
|
||||
return (
|
||||
<div className="inline-flex items-center gap-0.5 font-mono text-[11px] uppercase tracking-wider border border-[rgba(224,192,128,0.30)] rounded-full overflow-hidden">
|
||||
<span className="pl-2.5 pr-1 text-[#5a6678]"><Globe size={11} /></span>
|
||||
<div
|
||||
role="group"
|
||||
aria-label="Language"
|
||||
className="inline-flex items-center font-mono text-[12px] uppercase tracking-wider border border-[rgba(224,192,128,0.35)] rounded-full overflow-hidden"
|
||||
>
|
||||
<span className="pl-2.5 pr-1.5 py-1 text-[#9aa6b8]" aria-hidden="true">
|
||||
<Globe size={12} />
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setLocale("pt-br")}
|
||||
aria-pressed={current === "pt-br"}
|
||||
className={`px-2 py-1 transition-colors ${current === "pt-br" ? "bg-[#e0c080] text-[#0a0e1a] font-semibold" : "text-[#9aa6b8] hover:text-[#e0c080]"}`}
|
||||
aria-label="Português (Brasil)"
|
||||
className={`px-2.5 py-1 transition-colors ${
|
||||
current === "pt-br"
|
||||
? "bg-[#e0c080] text-[#0a0e1a] font-semibold"
|
||||
: "text-[#cbd2dd] hover:text-[#e0c080] hover:bg-[rgba(224,192,128,0.08)]"
|
||||
}`}
|
||||
>
|
||||
PT
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setLocale("en")}
|
||||
aria-pressed={current === "en"}
|
||||
className={`px-2 py-1 transition-colors ${current === "en" ? "bg-[#e0c080] text-[#0a0e1a] font-semibold" : "text-[#9aa6b8] hover:text-[#e0c080]"}`}
|
||||
aria-label="English"
|
||||
className={`px-2.5 py-1 transition-colors ${
|
||||
current === "en"
|
||||
? "bg-[#e0c080] text-[#0a0e1a] font-semibold"
|
||||
: "text-[#cbd2dd] hover:text-[#e0c080] hover:bg-[rgba(224,192,128,0.08)]"
|
||||
}`}
|
||||
>
|
||||
EN
|
||||
</button>
|
||||
|
|
@ -43,12 +66,14 @@ export function LocaleToggleClient({
|
|||
<div className="inline-flex items-center gap-1 font-mono text-[10px] uppercase tracking-widest border border-[rgba(0,255,156,0.32)] rounded">
|
||||
<span className="px-2 py-1 text-[#5a6678]"><Globe size={11} /></span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setLocale("en")}
|
||||
className={`px-2 py-1 ${current === "en" ? "bg-[rgba(0,255,156,0.12)] text-[#00ff9c]" : "text-[#8896aa] hover:text-[#7fdbff]"}`}
|
||||
>
|
||||
EN
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setLocale("pt-br")}
|
||||
className={`px-2 py-1 ${current === "pt-br" ? "bg-[rgba(0,255,156,0.12)] text-[#00ff9c]" : "text-[#8896aa] hover:text-[#7fdbff]"}`}
|
||||
>
|
||||
|
|
|
|||
33
web/components/nav-link.tsx
Normal file
33
web/components/nav-link.tsx
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
"use client";
|
||||
|
||||
/**
|
||||
* NavLink — primary-nav link with an active-page indicator.
|
||||
*
|
||||
* Reads usePathname to decide whether this link points at the active route,
|
||||
* then renders a visible gold underline plus a brighter text colour and
|
||||
* sets `aria-current="page"` for screen readers. A link counts as active for
|
||||
* its exact path AND for descendant paths (e.g. /sightings is active when
|
||||
* on /sightings/EV-1947-…), so deep pages still highlight their section.
|
||||
*/
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
export function NavLink({ href, label }: { href: string; label: string }) {
|
||||
const pathname = usePathname() || "/";
|
||||
const isActive =
|
||||
pathname === href || (href !== "/" && pathname.startsWith(`${href}/`));
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={href}
|
||||
aria-current={isActive ? "page" : undefined}
|
||||
className={
|
||||
isActive
|
||||
? "px-2.5 py-1.5 rounded text-[#e0c080] bg-[rgba(224,192,128,0.10)] border-b-2 border-[#e0c080] -mb-[2px] transition-colors"
|
||||
: "px-2.5 py-1.5 rounded text-[#cbd2dd] hover:text-[#e0c080] hover:bg-[rgba(224,192,128,0.06)] border-b-2 border-transparent -mb-[2px] transition-colors"
|
||||
}
|
||||
>
|
||||
{label}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
|
@ -7,19 +7,23 @@
|
|||
import Link from "next/link";
|
||||
import { AuthBar } from "./auth-bar";
|
||||
import { LocaleToggle } from "./locale-toggle";
|
||||
import { NavLink } from "./nav-link";
|
||||
|
||||
export function SiteHeader({ locale }: { locale: "pt-br" | "en" }) {
|
||||
return (
|
||||
<header className="border-b border-[rgba(224,192,128,0.15)]">
|
||||
<div className="mx-auto max-w-7xl px-4 md:px-8 py-4 flex items-center justify-between gap-4 flex-wrap">
|
||||
<Link href="/" className="flex items-center gap-2 group">
|
||||
<span className="text-[#e0c080] text-lg">▍</span>
|
||||
<Link href="/" className="flex items-center gap-2 group" aria-label="The Disclosure Bureau — home">
|
||||
<span className="text-[#e0c080] text-lg" aria-hidden="true">▍</span>
|
||||
<span className="font-display text-xl text-[#e7ecf3] group-hover:text-[#e0c080] transition-colors">
|
||||
The Disclosure Bureau
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
<nav className="flex items-center gap-1 text-[12px] font-mono">
|
||||
<nav
|
||||
aria-label={locale === "en" ? "Primary" : "Principal"}
|
||||
className="flex items-center gap-1 text-[13px] font-mono"
|
||||
>
|
||||
<NavLink href="/sightings" label={locale === "en" ? "Sightings" : "Avistamentos"} />
|
||||
<NavLink href="/witnesses" label={locale === "en" ? "Witnesses" : "Testemunhas"} />
|
||||
<NavLink href="/objects" label={locale === "en" ? "Craft" : "Objetos"} />
|
||||
|
|
@ -27,21 +31,10 @@ export function SiteHeader({ locale }: { locale: "pt-br" | "en" }) {
|
|||
<NavLink href="/operations" label={locale === "en" ? "Programs" : "Programas"} />
|
||||
<NavLink href="/bureau" label={locale === "en" ? "Case files" : "Casos"} />
|
||||
<NavLink href="/search" label={locale === "en" ? "Search" : "Busca"} />
|
||||
<span className="ml-1"><LocaleToggle current={locale} variant="navbar" /></span>
|
||||
<span className="ml-1"><AuthBar /></span>
|
||||
<span className="ml-2"><LocaleToggle current={locale} variant="navbar" /></span>
|
||||
<span className="ml-2"><AuthBar locale={locale} /></span>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
function NavLink({ href, label }: { href: string; label: string }) {
|
||||
return (
|
||||
<Link
|
||||
href={href}
|
||||
className="px-2.5 py-1.5 rounded text-[#9aa6b8] hover:text-[#e0c080] hover:bg-[rgba(224,192,128,0.06)] transition-colors"
|
||||
>
|
||||
{label}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue