Initial monorepo: shared package and DBIS, ICCC, OMNL, XOM portals
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
48
DBIS/components/layout/Footer.tsx
Normal file
48
DBIS/components/layout/Footer.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
58
DBIS/components/layout/Header.tsx
Normal file
58
DBIS/components/layout/Header.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
57
DBIS/components/layout/MobileNav.tsx
Normal file
57
DBIS/components/layout/MobileNav.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
57
DBIS/components/layout/NavDropdown.tsx
Normal file
57
DBIS/components/layout/NavDropdown.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user