Files
freno-dev/src/lib/nav-config.ts
2026-08-28 13:16:28 -04:00

152 lines
5.5 KiB
TypeScript

/**
* Per-site navigation configuration — site-aware layout & navigation.
*
* Pure module — imports NOTHING from solid-js / @solidjs/router / @solidjs/meta —
* so it can be unit-tested in `bun:test` without spinning up the router / Meta
* provider, mirroring the pattern established by `page-head-meta.ts`.
*
* Selection contract:
* - `href` values are the **public browser paths** on the subdomain origin
* (e.g. `/contact`), NOT the internal rewritten prefixes. vercel.json maps
* `nessa.freno.me/contact` → `/nessa/contact` server-side, but the browser
* sees (and links must emit) the clean `/contact`. This matches the
* canonical-URL derivation rule documented in `page-head-meta.ts`.
* - `external: true` means an absolute URL (e.g. GitHub / LinkedIn).
* - `showLoggedIn` / `showLoggedOut` gate auth-scoped items. Subdomain sites
* do NOT use the web (freno.me) JWT cookies — Nessa uses Clerk, Lineage
* uses its mobile JWT — so subdomain nav items never set these, keeping
* the "auth-aware items only on main" acceptance criterion satisfied by
* construction.
* - `icon` is a string key resolved to an SVG by the bar renderer
* (`Bars.tsx`), kept here as a string so this module stays import-free.
*
* The main-site nav is also represented here for parity / unit-testability,
* but `Bars.tsx` preserves the main site's pre-existing bespoke rendering
* (Recent Posts, auth-aware Account/Login/SignOut, admin Analytics, the
* "What's this?" glitch button, + the right-bar widgets). NAV_CONFIG[main] is
* authoritative only for the *link set* the unit tests assert against.
*/
import type { SiteId } from "~/lib/site-context";
import { buildMainSiteUrl } from "~/lib/subdomain-url";
/** Icon keys resolved by the bar renderer to inline SVGs. */
export type NavIcon =
| "home"
| "blog"
| "downloads"
| "resume"
| "contact"
| "privacy"
| "deletion"
| "github"
| "linkedin"
| "back";
export interface NavItem {
label: string;
/** Public browser path (subdomain-relative) or absolute URL when external. */
href: string;
icon?: NavIcon;
/** Absolute external link (opens in a new tab). */
external?: boolean;
/** Only render when the viewer is authenticated (main-site web auth). */
showLoggedIn?: boolean;
/** Only render when the viewer is logged out (main-site web auth). */
showLoggedOut?: boolean;
}
/** Apex/host link used as a "back to freno.me" affordance on subdomains. */
export const BACK_TO_FRENO: NavItem = {
label: "back to freno.me",
href: buildMainSiteUrl("/"),
icon: "back",
external: true
};
/**
* Per-site navigation link sets.
*
* Defined to satisfy the acceptance matrix:
* - main: Home, Blog, Downloads, Resume, Contact, GitHub, LinkedIn
* - nessa: Home, Contact, Privacy
* - lineage: Home, Downloads, Contact, Privacy, Account Deletion
* - gaze: Home, Contact, Privacy, Downloads
* - inputhalo: Home, Contact, Privacy, Downloads
*/
export const NAV_CONFIG: Record<SiteId, NavItem[]> = {
main: [
{ label: "Home", href: "/", icon: "home" },
{ label: "Blog", href: "/blog", icon: "blog" },
{ label: "Downloads", href: "/downloads", icon: "downloads" },
{ label: "Resume", href: "/resume", icon: "resume" },
{ label: "Contact", href: "/contact", icon: "contact" },
{ label: "Privacy", href: "/privacy-policy", icon: "privacy" },
{
label: "GitHub",
href: "https://github.com/MikeFreno/",
icon: "github",
external: true
},
{
label: "LinkedIn",
href: "https://www.linkedin.com/in/michael-freno-176001256/",
icon: "linkedin",
external: true
}
],
nessa: [
{ label: "Home", href: "/", icon: "home" },
{ label: "Contact", href: "/contact", icon: "contact" },
{ label: "Privacy", href: "/privacy", icon: "privacy" }
],
lineage: [
{ label: "Home", href: "/", icon: "home" },
{ label: "Downloads", href: "/downloads", icon: "downloads" },
{ label: "Contact", href: "/contact", icon: "contact" },
{ label: "Privacy", href: "/privacy", icon: "privacy" },
{ label: "Account Deletion", href: "/deletion", icon: "deletion" }
],
gaze: [
{ label: "Home", href: "/", icon: "home" },
{ label: "Contact", href: "/contact", icon: "contact" },
{ label: "Privacy", href: "/privacy", icon: "privacy" },
{ label: "Downloads", href: "/downloads", icon: "downloads" }
],
inputhalo: [
{ label: "Home", href: "/", icon: "home" },
{ label: "Contact", href: "/contact", icon: "contact" },
{ label: "Privacy", href: "/privacy", icon: "privacy" },
{ label: "Downloads", href: "/downloads", icon: "downloads" }
],
nook: [
{ label: "Home", href: "/", icon: "home" },
{ label: "Privacy", href: "/privacy", icon: "privacy" }
]
};
/**
* Filter a site's nav items by the viewer's auth state.
*
* Used by the renderer so auth-gated items (e.g. an admin link) only appear
* for the appropriate audience. Items without `showLoggedIn`/`showLoggedOut`
* are always shown.
*/
export function filterNavByAuth(
items: readonly NavItem[],
isAuthenticated: boolean
): NavItem[] {
return items.filter((item) => {
if (item.showLoggedIn && !isAuthenticated) return false;
if (item.showLoggedOut && isAuthenticated) return false;
return true;
});
}
/**
* Labels for a site's nav — convenience for asserting against in unit tests
* without pulling the full NavItem shape.
*/
export function navLabelsFor(site: SiteId): string[] {
return NAV_CONFIG[site].map((item) => item.label);
}