Initial monorepo: shared package and DBIS, ICCC, OMNL, XOM portals

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
defiQUG
2026-02-13 10:27:01 -08:00
parent 0cd7701e93
commit f5e217efcd
240 changed files with 8161 additions and 0 deletions

View File

@@ -0,0 +1,48 @@
import Link from "next/link";
import { navItems } from "@/lib/nav-config";
const footerLinks = [
{ label: "About", href: "/about" },
{ label: "Documents", href: "/documents" },
{ label: "Transparency", href: "/transparency" },
{ label: "Contact", href: "/contact" },
{ label: "Regional Offices", href: "/regions" },
];
function flatNavItems(items: typeof navItems): { label: string; href: string }[] {
const out: { label: string; href: string }[] = [];
for (const item of items) {
if (item.children) {
for (const c of item.children) out.push({ label: c.label, href: c.href });
} else {
out.push({ label: item.label, href: item.href });
}
}
return out;
}
export function Footer() {
const flat = flatNavItems(navItems);
const keyLinks = footerLinks.length ? footerLinks : flat.slice(0, 6);
return (
<footer className="border-t border-neutral-200 bg-white">
<div className="container mx-auto px-4 py-8">
<div className="flex flex-wrap gap-6">
{keyLinks.map((link) => (
<Link
key={link.href}
href={link.href}
className="text-sm text-neutral-600 hover:text-neutral-900 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 rounded"
>
{link.label}
</Link>
))}
</div>
<p className="mt-6 text-sm text-neutral-500">
Digital Bank of International Settlements. All portals follow the same tech stack and policies.
</p>
</div>
</footer>
);
}

View File

@@ -0,0 +1,58 @@
"use client";
import Link from "next/link";
import { useState } from "react";
import { navItems } from "@/lib/nav-config";
import { NavDropdown } from "./NavDropdown";
import { MobileNav } from "./MobileNav";
export function Header() {
const [mobileOpen, setMobileOpen] = useState(false);
return (
<header className="sticky top-0 z-50 border-b border-neutral-200 bg-white">
<div className="container mx-auto flex h-16 items-center justify-between px-4">
<Link href="/" className="text-xl font-semibold text-neutral-900 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 rounded">
DBIS
</Link>
<nav className="hidden md:flex md:items-center md:gap-1" aria-label="Main navigation">
{navItems.map((item) =>
item.children ? (
<NavDropdown key={item.href} item={item} />
) : (
<Link
key={item.href}
href={item.href}
className="rounded px-3 py-2 text-sm font-medium text-neutral-700 hover:bg-neutral-100 hover:text-neutral-900 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-inset"
>
{item.label}
</Link>
)
)}
</nav>
<div className="flex items-center gap-2">
<Link href="/login" className="hidden md:inline-flex rounded px-3 py-2 text-sm font-medium text-neutral-700 hover:bg-neutral-100 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-inset">
Sign in
</Link>
<button
type="button"
className="md:hidden rounded p-2 text-neutral-600 hover:bg-neutral-100 focus:outline-none focus:ring-2 focus:ring-primary-500"
aria-expanded={mobileOpen}
aria-controls="mobile-nav"
onClick={() => setMobileOpen(!mobileOpen)}
>
<span className="sr-only">{mobileOpen ? "Close menu" : "Open menu"}</span>
<svg className="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden>
{mobileOpen ? (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
) : (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
)}
</svg>
</button>
</div>
</div>
<MobileNav items={navItems} open={mobileOpen} onClose={() => setMobileOpen(false)} />
</header>
);
}

View File

@@ -0,0 +1,57 @@
"use client";
import Link from "next/link";
import type { NavItem } from "@public-web-portals/shared";
export function MobileNav({
items,
open,
onClose,
}: {
items: NavItem[];
open: boolean;
onClose: () => void;
}) {
if (!open) return null;
return (
<nav
id="mobile-nav"
className="md:hidden border-t border-neutral-200 bg-white px-4 py-4"
aria-label="Mobile navigation"
>
<ul className="space-y-1">
{items.map((item) => (
<li key={item.href}>
{item.children ? (
<>
<span className="block px-3 py-2 text-sm font-semibold text-neutral-900">{item.label}</span>
<ul className="ml-4 space-y-1 border-l border-neutral-200 pl-4">
{item.children.map((child) => (
<li key={child.href}>
<Link
href={child.href}
className="block px-3 py-2 text-sm text-neutral-600 hover:text-neutral-900 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-inset rounded"
onClick={onClose}
>
{child.label}
</Link>
</li>
))}
</ul>
</>
) : (
<Link
href={item.href}
className="block rounded px-3 py-2 text-sm font-medium text-neutral-700 hover:bg-neutral-100 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-inset"
onClick={onClose}
>
{item.label}
</Link>
)}
</li>
))}
</ul>
</nav>
);
}

View File

@@ -0,0 +1,57 @@
"use client";
import Link from "next/link";
import { useState, useRef, useEffect } from "react";
import type { NavItem } from "@public-web-portals/shared";
export function NavDropdown({ item }: { item: NavItem }) {
const [open, setOpen] = useState(false);
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (ref.current && !ref.current.contains(event.target as Node)) setOpen(false);
}
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);
return (
<div className="relative" ref={ref}>
<button
type="button"
className="flex items-center gap-1 rounded px-3 py-2 text-sm font-medium text-neutral-700 hover:bg-neutral-100 hover:text-neutral-900 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-inset"
aria-expanded={open}
aria-haspopup="true"
aria-controls={`nav-menu-${item.label.replace(/\s/g, "-")}`}
id={`nav-button-${item.label.replace(/\s/g, "-")}`}
onClick={() => setOpen(!open)}
>
{item.label}
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
<ul
id={`nav-menu-${item.label.replace(/\s/g, "-")}`}
role="menu"
aria-orientation="vertical"
aria-labelledby={`nav-button-${item.label.replace(/\s/g, "-")}`}
className={`absolute left-0 top-full z-10 mt-1 min-w-[12rem] rounded-md border border-neutral-200 bg-white py-1 shadow-lg ${open ? "block" : "hidden"}`}
>
{item.children?.map((child) => (
<li key={child.href} role="none">
<Link
href={child.href}
role="menuitem"
className="block px-4 py-2 text-sm text-neutral-700 hover:bg-neutral-100 focus:bg-neutral-100 focus:outline-none"
onClick={() => setOpen(false)}
>
{child.label}
</Link>
</li>
))}
</ul>
</div>
);
}