Convert DBIS, ICCC, OMNL, XOM to git submodules

- Portals now link to Gov_Web_Portals/{DBIS,ICCC,OMNL,XOM}.git
- Added .gitignore to each portal for standalone use
- Updated README: clone with --recurse-submodules, monorepo URL

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
defiQUG
2026-02-13 10:34:57 -08:00
parent c9c8d559a2
commit 1f278c952b
229 changed files with 29 additions and 3679 deletions

12
.gitmodules vendored Normal file
View File

@@ -0,0 +1,12 @@
[submodule "DBIS"]
path = DBIS
url = https://gitea.d-bis.org/Gov_Web_Portals/DBIS.git
[submodule "ICCC"]
path = ICCC
url = https://gitea.d-bis.org/Gov_Web_Portals/ICCC.git
[submodule "OMNL"]
path = OMNL
url = https://gitea.d-bis.org/Gov_Web_Portals/OMNL.git
[submodule "XOM"]
path = XOM
url = https://gitea.d-bis.org/Gov_Web_Portals/XOM.git

1
DBIS Submodule

Submodule DBIS added at 7cc2042101

View File

@@ -1,8 +0,0 @@
# Next.js
NEXTAUTH_URL=http://localhost:3000
# OIDC / OAuth 2.0 (placeholder until identity provider is configured)
# OIDC_ISSUER=
# OIDC_CLIENT_ID=
# OIDC_CLIENT_SECRET=
# OIDC_REDIRECT_URI=

View File

@@ -1,5 +0,0 @@
/** @type { import("eslint").Linter.Config } */
module.exports = {
root: true,
extends: ["../.eslintrc.cjs", "next/core-web-vitals"],
};

7
DBIS/.gitignore vendored
View File

@@ -1,7 +0,0 @@
node_modules/
.next/
out/
dist/
.env
.env.local
.env.*.local

View File

@@ -1,29 +0,0 @@
# DBIS Portal
Portal for the **Digital Bank of International Settlements**.
**Gitea repository:** https://gitea.d-bis.org/Gov_Web_Portals/DBIS.git Next-generation web application following the shared tech stack and policies.
## Run locally
```bash
pnpm install
pnpm dev
```
Open [http://localhost:3000](http://localhost:3000).
## Environment
Copy `.env.example` to `.env.local` and set values. No secrets in repo.
## Standards
- [TECH_STACK.md](../TECH_STACK.md)
- [TECH_POLICIES.md](../TECH_POLICIES.md)
- [PORTAL_NAVIGATION.md](../PORTAL_NAVIGATION.md)
- [ORG_STRUCTURE.md](../ORG_STRUCTURE.md)
## Structure
Same app structure as other portals: public routes, transparency, workflow (submit/apply/report/request), dashboard, auth. Entity-specific routes: Financial Bulletins, Monetary Operations.

View File

@@ -1,55 +0,0 @@
"use client";
import { Suspense } from "react";
import { useSearchParams } from "next/navigation";
import Link from "next/link";
function LoginForm() {
const searchParams = useSearchParams();
const returnUrl = searchParams.get("returnUrl") ?? "/dashboard";
return (
<div className="container mx-auto px-4 py-12 max-w-md">
<h1 className="text-3xl font-bold text-neutral-900">Sign in</h1>
<p className="mt-2 text-neutral-600">
Placeholder login. Replace with OIDC/OAuth 2.0 when identity provider is configured.
</p>
<form action="/api/auth/mock-login" method="POST" className="mt-6 space-y-4">
<input type="hidden" name="returnUrl" value={returnUrl} />
<div>
<label htmlFor="role" className="block text-sm font-medium text-neutral-700">
Role (dev placeholder)
</label>
<select
id="role"
name="role"
className="mt-1 block w-full rounded border border-neutral-300 px-3 py-2 focus:border-primary-500 focus:ring-1 focus:ring-primary-500"
>
<option value="member">Member</option>
<option value="staff">Staff</option>
<option value="admin">Admin</option>
</select>
</div>
<button
type="submit"
className="w-full rounded-md bg-primary-600 px-4 py-2 text-sm font-medium text-white hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2"
>
Sign in
</button>
</form>
<p className="mt-4 text-sm text-neutral-500">
<Link href={returnUrl} className="text-primary-600 hover:underline">
Return to previous page
</Link>
</p>
</div>
);
}
export default function LoginPage() {
return (
<Suspense fallback={<div className="container mx-auto px-4 py-12 max-w-md">Loading...</div>}>
<LoginForm />
</Suspense>
);
}

View File

@@ -1,20 +0,0 @@
"use client";
import { useEffect } from "react";
import Link from "next/link";
export default function LogoutPage() {
useEffect(() => {
document.cookie = "portal-role=; path=/; max-age=0";
}, []);
return (
<div className="container mx-auto px-4 py-12 max-w-md">
<h1 className="text-3xl font-bold text-neutral-900">Signed out</h1>
<p className="mt-4 text-neutral-600">You have been signed out.</p>
<Link href="/" className="mt-6 inline-block text-primary-600 hover:underline">
Return to home
</Link>
</div>
);
}

View File

@@ -1,8 +0,0 @@
export default function AboutPage() {
return (
<div className="container mx-auto px-4 py-12">
<h1 className="text-3xl font-bold text-neutral-900">Mandate</h1>
<p className="mt-4 text-neutral-600">About the Digital Bank of International Settlements.</p>
</div>
);
}

View File

@@ -1,8 +0,0 @@
export default function AccreditationPage() {
return (
<div className="container mx-auto px-4 py-12">
<h1 className="text-3xl font-bold text-neutral-900">Accreditation</h1>
<p className="mt-4 text-neutral-600">Accreditation and credentials.</p>
</div>
);
}

View File

@@ -1,8 +0,0 @@
export default function ContactPage() {
return (
<div className="container mx-auto px-4 py-12">
<h1 className="text-3xl font-bold text-neutral-900">Contact</h1>
<p className="mt-4 text-neutral-600">General contact and enquiries.</p>
</div>
);
}

View File

@@ -1,8 +0,0 @@
export default function DepartmentsPage() {
return (
<div className="container mx-auto px-4 py-12">
<h1 className="text-3xl font-bold text-neutral-900">Departments</h1>
<p className="mt-4 text-neutral-600">Mission departments and programs.</p>
</div>
);
}

View File

@@ -1,8 +0,0 @@
export default function DocumentsPage() {
return (
<div className="container mx-auto px-4 py-12">
<h1 className="text-3xl font-bold text-neutral-900">Documents</h1>
<p className="mt-4 text-neutral-600">Library, standards, and directives.</p>
</div>
);
}

View File

@@ -1,8 +0,0 @@
export default function GovernancePage() {
return (
<div className="container mx-auto px-4 py-12">
<h1 className="text-3xl font-bold text-neutral-900">Governance</h1>
<p className="mt-4 text-neutral-600">Charters, instruments, and governance.</p>
</div>
);
}

View File

@@ -1,8 +0,0 @@
export default function LeadershipPage() {
return (
<div className="container mx-auto px-4 py-12">
<h1 className="text-3xl font-bold text-neutral-900">Leadership</h1>
<p className="mt-4 text-neutral-600">Executive leadership and secretariat.</p>
</div>
);
}

View File

@@ -1,8 +0,0 @@
export default function MembershipPage() {
return (
<div className="container mx-auto px-4 py-12">
<h1 className="text-3xl font-bold text-neutral-900">Membership</h1>
<p className="mt-4 text-neutral-600">Member directory and accreditation.</p>
</div>
);
}

View File

@@ -1,8 +0,0 @@
export default function NewsPage() {
return (
<div className="container mx-auto px-4 py-12">
<h1 className="text-3xl font-bold text-neutral-900">News</h1>
<p className="mt-4 text-neutral-600">News and announcements.</p>
</div>
);
}

View File

@@ -1,8 +0,0 @@
export default function OrgChartPage() {
return (
<div className="container mx-auto px-4 py-12">
<h1 className="text-3xl font-bold text-neutral-900">Org Chart</h1>
<p className="mt-4 text-neutral-600">Organizational structure.</p>
</div>
);
}

View File

@@ -1,8 +0,0 @@
export default function ProgramsPage() {
return (
<div className="container mx-auto px-4 py-12">
<h1 className="text-3xl font-bold text-neutral-900">Programs</h1>
<p className="mt-4 text-neutral-600">Programs and initiatives.</p>
</div>
);
}

View File

@@ -1,8 +0,0 @@
export default function PublicationsPage() {
return (
<div className="container mx-auto px-4 py-12">
<h1 className="text-3xl font-bold text-neutral-900">Publications</h1>
<p className="mt-4 text-neutral-600">Publications and reports.</p>
</div>
);
}

View File

@@ -1,8 +0,0 @@
export default function RegionsPage() {
return (
<div className="container mx-auto px-4 py-12">
<h1 className="text-3xl font-bold text-neutral-900">Regional Offices</h1>
<p className="mt-4 text-neutral-600">Americas, EMEA, APAC delegations and field missions.</p>
</div>
);
}

View File

@@ -1,8 +0,0 @@
export default function StandardsPage() {
return (
<div className="container mx-auto px-4 py-12">
<h1 className="text-3xl font-bold text-neutral-900">Standards</h1>
<p className="mt-4 text-neutral-600">Standards and codes.</p>
</div>
);
}

View File

@@ -1,10 +0,0 @@
import { NextRequest, NextResponse } from "next/server";
export async function POST(request: NextRequest) {
const form = await request.formData();
const returnUrl = (form.get("returnUrl") as string) ?? "/dashboard";
const role = (form.get("role") as string) ?? "member";
const res = NextResponse.redirect(new URL(returnUrl, request.url));
res.cookies.set("portal-role", role, { path: "/", httpOnly: true, maxAge: 60 * 60 * 24 });
return res;
}

View File

@@ -1,8 +0,0 @@
export default function ApplyPage() {
return (
<div className="container mx-auto px-4 py-12">
<h1 className="text-3xl font-bold text-neutral-900">Apply</h1>
<p className="mt-4 text-neutral-600">Apply for membership or accreditation.</p>
</div>
);
}

View File

@@ -1,17 +0,0 @@
import Link from "next/link";
import type { ReactNode } from "react";
export default function DashboardLayout({ children }: { children: ReactNode }) {
return (
<div className="flex min-h-[60vh]">
<aside className="w-56 border-r border-neutral-200 bg-white p-4">
<nav className="space-y-1" aria-label="Dashboard navigation">
<Link href="/dashboard" className="block rounded px-3 py-2 text-sm font-medium text-neutral-700 hover:bg-neutral-100">Home</Link>
<Link href="/dashboard/submissions" className="block rounded px-3 py-2 text-sm font-medium text-neutral-700 hover:bg-neutral-100">My submissions</Link>
<Link href="/dashboard/requests" className="block rounded px-3 py-2 text-sm font-medium text-neutral-700 hover:bg-neutral-100">My requests</Link>
</nav>
</aside>
<div className="flex-1 p-6">{children}</div>
</div>
);
}

View File

@@ -1,8 +0,0 @@
export default function DashboardPage() {
return (
<div>
<h1 className="text-3xl font-bold text-neutral-900">Dashboard</h1>
<p className="mt-4 text-neutral-600">Role-based dashboard.</p>
</div>
);
}

View File

@@ -1,8 +0,0 @@
export default function RequestsPage() {
return (
<div>
<h1 className="text-3xl font-bold text-neutral-900">My requests</h1>
<p className="mt-4 text-neutral-600">Track your requests.</p>
</div>
);
}

View File

@@ -1,8 +0,0 @@
export default function SubmissionsPage() {
return (
<div>
<h1 className="text-3xl font-bold text-neutral-900">My submissions</h1>
<p className="mt-4 text-neutral-600">Track your submissions and status.</p>
</div>
);
}

View File

@@ -1,8 +0,0 @@
export default function MonetaryOperationsPage() {
return (
<div className="container mx-auto px-4 py-12">
<h1 className="text-3xl font-bold text-neutral-900">Monetary Operations</h1>
<p className="mt-4 text-neutral-600">Digital reserves and monetary operations.</p>
</div>
);
}

View File

@@ -1,8 +0,0 @@
export default function BulletinsPage() {
return (
<div className="container mx-auto px-4 py-12">
<h1 className="text-3xl font-bold text-neutral-900">Financial Bulletins</h1>
<p className="mt-4 text-neutral-600">DBIS financial bulletins and reports.</p>
</div>
);
}

View File

@@ -1,3 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@@ -1,34 +0,0 @@
import type { Metadata } from "next";
import type { ReactNode } from "react";
import "./globals.css";
import { Header } from "@/components/layout/Header";
import { Footer } from "@/components/layout/Footer";
export const metadata: Metadata = {
title: "Digital Bank of International Settlements",
description: "Official portal of the Digital Bank of International Settlements",
};
export default function RootLayout({
children,
}: Readonly<{
children: ReactNode;
}>) {
return (
<html lang="en">
<body className="min-h-screen flex flex-col bg-neutral-50 text-neutral-900 antialiased">
<a
href="#main-content"
className="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-[100] focus:px-4 focus:py-2 focus:bg-primary-600 focus:text-white focus:rounded"
>
Skip to main content
</a>
<Header />
<main id="main-content" className="flex-1">
{children}
</main>
<Footer />
</body>
</html>
);
}

View File

@@ -1,8 +0,0 @@
export default function RenewalsPage() {
return (
<div className="container mx-auto px-4 py-12">
<h1 className="text-3xl font-bold text-neutral-900">Renewals and Attestations</h1>
<p className="mt-4 text-neutral-600">Annual renewals for members.</p>
</div>
);
}

View File

@@ -1,24 +0,0 @@
export default function HomePage() {
return (
<div className="container mx-auto px-4 py-12">
<h1 className="text-4xl font-bold text-neutral-900">Digital Bank of International Settlements</h1>
<p className="mt-4 text-lg text-neutral-600">
Welcome to the official portal. Explore mandate, governance, documents, and services.
</p>
<div className="mt-8 flex flex-wrap gap-4">
<a href="/about" className="inline-flex items-center rounded-md bg-primary-600 px-4 py-2 text-sm font-medium text-white hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2">
About
</a>
<a href="/documents" className="inline-flex items-center rounded-md border border-neutral-300 bg-white px-4 py-2 text-sm font-medium text-neutral-700 hover:bg-neutral-50 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2">
Documents
</a>
<a href="/transparency" className="inline-flex items-center rounded-md border border-neutral-300 bg-white px-4 py-2 text-sm font-medium text-neutral-700 hover:bg-neutral-50 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2">
Transparency
</a>
<a href="/dashboard" className="inline-flex items-center rounded-md border border-neutral-300 bg-white px-4 py-2 text-sm font-medium text-neutral-700 hover:bg-neutral-50 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2">
Dashboard
</a>
</div>
</div>
);
}

View File

@@ -1,8 +0,0 @@
export default function ReportPage() {
return (
<div className="container mx-auto px-4 py-12">
<h1 className="text-3xl font-bold text-neutral-900">Report</h1>
<p className="mt-4 text-neutral-600">Report incidents or compliance matters.</p>
</div>
);
}

View File

@@ -1,8 +0,0 @@
export default function RequestPage() {
return (
<div className="container mx-auto px-4 py-12">
<h1 className="text-3xl font-bold text-neutral-900">Request</h1>
<p className="mt-4 text-neutral-600">Request documents or support.</p>
</div>
);
}

View File

@@ -1,8 +0,0 @@
export default function SubmitPage() {
return (
<div className="container mx-auto px-4 py-12">
<h1 className="text-3xl font-bold text-neutral-900">Submit</h1>
<p className="mt-4 text-neutral-600">Submission portal.</p>
</div>
);
}

View File

@@ -1,8 +0,0 @@
export default function AuditPage() {
return (
<div className="container mx-auto px-4 py-12">
<h1 className="text-3xl font-bold text-neutral-900">Audit Reports</h1>
<p className="mt-4 text-neutral-600">Audit reports and findings tracker.</p>
</div>
);
}

View File

@@ -1,8 +0,0 @@
export default function DataProtectionPage() {
return (
<div className="container mx-auto px-4 py-12">
<h1 className="text-3xl font-bold text-neutral-900">Data Protection and Records</h1>
<p className="mt-4 text-neutral-600">Records retention and data protection.</p>
</div>
);
}

View File

@@ -1,8 +0,0 @@
export default function EthicsPage() {
return (
<div className="container mx-auto px-4 py-12">
<h1 className="text-3xl font-bold text-neutral-900">Ethics and Conduct</h1>
<p className="mt-4 text-neutral-600">Ethics and conduct policies.</p>
</div>
);
}

View File

@@ -1,8 +0,0 @@
export default function TransparencyPage() {
return (
<div className="container mx-auto px-4 py-12">
<h1 className="text-3xl font-bold text-neutral-900">Transparency</h1>
<p className="mt-4 text-neutral-600">Audit, ethics, whistleblower, sanctions, and data protection.</p>
</div>
);
}

View File

@@ -1,8 +0,0 @@
export default function SanctionsPage() {
return (
<div className="container mx-auto px-4 py-12">
<h1 className="text-3xl font-bold text-neutral-900">Sanctions and Restricted Parties</h1>
<p className="mt-4 text-neutral-600">Eligibility and restricted parties.</p>
</div>
);
}

View File

@@ -1,8 +0,0 @@
export default function WhistleblowerPage() {
return (
<div className="container mx-auto px-4 py-12">
<h1 className="text-3xl font-bold text-neutral-900">Whistleblower Intake</h1>
<p className="mt-4 text-neutral-600">Secure anonymous reporting.</p>
</div>
);
}

View File

@@ -1,48 +0,0 @@
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

@@ -1,58 +0,0 @@
"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

@@ -1,57 +0,0 @@
"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

@@ -1,57 +0,0 @@
"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>
);
}

View File

@@ -1,10 +0,0 @@
import type { SessionOrNull } from "@public-web-portals/shared";
/**
* Placeholder session. Replace with OIDC/OAuth 2.0 when identity provider is configured.
* TECH_POLICIES: use httpOnly cookies / secure token storage.
*/
export async function getSession(): Promise<SessionOrNull> {
// TODO: read session from cookie or header (e.g. NextAuth, OIDC)
return null;
}

View File

@@ -1,4 +0,0 @@
import { getNavForPortal } from "@public-web-portals/shared";
export const portalId = "DBIS" as const;
export const navItems = getNavForPortal(portalId);

View File

@@ -1,66 +0,0 @@
import type { Role } from "@public-web-portals/shared";
/** Route prefixes that require any authenticated role */
const protectedPrefixes = [
"/dashboard",
"/submit",
"/apply",
"/report",
"/request",
"/membership/renewals",
];
/** Route prefixes that require admin role */
const adminOnlyPrefixes = ["/dashboard/admin"];
/** Routes that are always public (no auth) */
const publicPaths = [
"/",
"/about",
"/governance",
"/leadership",
"/org-chart",
"/departments",
"/programs",
"/membership",
"/accreditation",
"/documents",
"/standards",
"/news",
"/publications",
"/contact",
"/regions",
"/transparency",
"/login",
"/logout",
];
function pathIsPublic(pathname: string): boolean {
if (publicPaths.includes(pathname)) return true;
if (pathname.startsWith("/transparency/")) return true;
return false;
}
function pathIsProtected(pathname: string): boolean {
return protectedPrefixes.some((p) => pathname === p || pathname.startsWith(p + "/"));
}
function pathIsAdminOnly(pathname: string): boolean {
return adminOnlyPrefixes.some((p) => pathname === p || pathname.startsWith(p + "/"));
}
export function canAccess(pathname: string, role: Role | null): boolean {
if (pathIsPublic(pathname)) return true;
if (role == null) return false;
if (pathIsAdminOnly(pathname)) return role === "admin";
if (pathIsProtected(pathname)) return true;
return false;
}
export function requiresAuth(pathname: string): boolean {
return pathIsProtected(pathname);
}
export function requiresAdmin(pathname: string): boolean {
return pathIsAdminOnly(pathname);
}

View File

@@ -1,39 +0,0 @@
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { canAccess, requiresAuth, requiresAdmin } from "./lib/rbac";
import type { Role } from "@public-web-portals/shared";
const LOGIN = "/login";
function getRoleFromCookie(request: NextRequest): Role | null {
const role = request.cookies.get("portal-role")?.value;
if (!role) return null;
const valid: Role[] = ["member", "judge", "clerk", "diplomat", "donor", "staff", "admin"];
return valid.includes(role as Role) ? (role as Role) : null;
}
export function middleware(request: NextRequest) {
const pathname = request.nextUrl.pathname;
const role = getRoleFromCookie(request);
if (requiresAdmin(pathname) && role !== "admin") {
return NextResponse.redirect(new URL("/dashboard", request.url));
}
if (requiresAuth(pathname)) {
if (role == null) {
const url = new URL(LOGIN, request.url);
url.searchParams.set("returnUrl", pathname);
return NextResponse.redirect(url);
}
if (!canAccess(pathname, role)) {
return NextResponse.redirect(new URL("/dashboard", request.url));
}
}
return NextResponse.next();
}
export const config = {
matcher: ["/((?!_next/static|_next/image|favicon.ico|api).*)"],
};

5
DBIS/next-env.d.ts vendored
View File

@@ -1,5 +0,0 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.

View File

@@ -1,32 +0,0 @@
/** @type { import('next').NextConfig } */
const nextConfig = {
transpilePackages: ["@public-web-portals/shared"],
async headers() {
return [
{
source: "/:path*",
headers: [
{ key: "X-Frame-Options", value: "DENY" },
{ key: "X-Content-Type-Options", value: "nosniff" },
{ key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
{
key: "Content-Security-Policy",
value: [
"default-src 'self'",
"script-src 'self'",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: https:",
"font-src 'self'",
"connect-src 'self'",
"frame-ancestors 'none'",
"base-uri 'self'",
"form-action 'self'",
].join("; "),
},
],
},
];
},
};
module.exports = nextConfig;

View File

@@ -1,32 +0,0 @@
{
"name": "portal-dbis",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev --port 3000",
"build": "next build",
"start": "next start --port 3000",
"lint": "eslint . --max-warnings 0 --config ../.eslintrc.cjs",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@public-web-portals/shared": "workspace:*",
"next": "^14.2.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/node": "^20.10.0",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"autoprefixer": "^10.4.0",
"eslint": "^8.56.0",
"eslint-config-next": "^14.2.0",
"postcss": "^8.4.0",
"tailwindcss": "^3.4.0",
"typescript": "^5.3.0"
},
"engines": {
"node": ">=20.0.0"
}
}

View File

@@ -1,6 +0,0 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

View File

@@ -1,15 +0,0 @@
import type { Config } from "tailwindcss";
import { designTokens } from "@public-web-portals/shared";
const config: Config = {
content: [
"./app/**/*.{js,ts,jsx,tsx,mdx}",
"./components/**/*.{js,ts,jsx,tsx,mdx}",
],
theme: {
extend: designTokens as Record<string, unknown>,
},
plugins: [],
};
export default config;

View File

@@ -1,30 +0,0 @@
{
"extends": "../tsconfig.base.json",
"compilerOptions": {
"plugins": [
{
"name": "next"
}
],
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "preserve",
"noEmit": true,
"incremental": true,
"paths": {
"@/*": [
"./*"
]
},
"allowJs": true
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts"
],
"exclude": [
"node_modules"
]
}

1
ICCC Submodule

Submodule ICCC added at a488346ea2

View File

@@ -1,8 +0,0 @@
# Next.js
NEXTAUTH_URL=http://localhost:3000
# OIDC / OAuth 2.0 (placeholder until identity provider is configured)
# OIDC_ISSUER=
# OIDC_CLIENT_ID=
# OIDC_CLIENT_SECRET=
# OIDC_REDIRECT_URI=

View File

@@ -1,5 +0,0 @@
/** @type { import("eslint").Linter.Config } */
module.exports = {
root: true,
extends: ["../.eslintrc.cjs", "next/core-web-vitals"],
};

7
ICCC/.gitignore vendored
View File

@@ -1,7 +0,0 @@
node_modules/
.next/
out/
dist/
.env
.env.local
.env.*.local

View File

@@ -1,29 +0,0 @@
# ICCC Portal
Portal for the **International Criminal Court of Commerce**.
**Gitea repository:** https://gitea.d-bis.org/Gov_Web_Portals/ICCC.git Next-generation web application following the shared tech stack and policies.
## Run locally
```bash
pnpm install
pnpm dev
```
Open [http://localhost:3000](http://localhost:3000).
## Environment
Copy `.env.example` to `.env.local` and set values. No secrets in repo.
## Standards
- [TECH_STACK.md](../TECH_STACK.md)
- [TECH_POLICIES.md](../TECH_POLICIES.md)
- [PORTAL_NAVIGATION.md](../PORTAL_NAVIGATION.md)
- [ORG_STRUCTURE.md](../ORG_STRUCTURE.md)
## Structure
Same app structure as other portals: public routes, transparency, workflow (submit/apply/report/request), dashboard, auth. Entity-specific routes: Case Law, Registry, Chambers, Judgments.

View File

@@ -1,55 +0,0 @@
"use client";
import { Suspense } from "react";
import { useSearchParams } from "next/navigation";
import Link from "next/link";
function LoginForm() {
const searchParams = useSearchParams();
const returnUrl = searchParams.get("returnUrl") ?? "/dashboard";
return (
<div className="container mx-auto px-4 py-12 max-w-md">
<h1 className="text-3xl font-bold text-neutral-900">Sign in</h1>
<p className="mt-2 text-neutral-600">
Placeholder login. Replace with OIDC/OAuth 2.0 when identity provider is configured.
</p>
<form action="/api/auth/mock-login" method="POST" className="mt-6 space-y-4">
<input type="hidden" name="returnUrl" value={returnUrl} />
<div>
<label htmlFor="role" className="block text-sm font-medium text-neutral-700">
Role (dev placeholder)
</label>
<select
id="role"
name="role"
className="mt-1 block w-full rounded border border-neutral-300 px-3 py-2 focus:border-primary-500 focus:ring-1 focus:ring-primary-500"
>
<option value="member">Member</option>
<option value="staff">Staff</option>
<option value="admin">Admin</option>
</select>
</div>
<button
type="submit"
className="w-full rounded-md bg-primary-600 px-4 py-2 text-sm font-medium text-white hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2"
>
Sign in
</button>
</form>
<p className="mt-4 text-sm text-neutral-500">
<Link href={returnUrl} className="text-primary-600 hover:underline">
Return to previous page
</Link>
</p>
</div>
);
}
export default function LoginPage() {
return (
<Suspense fallback={<div className="container mx-auto px-4 py-12 max-w-md">Loading...</div>}>
<LoginForm />
</Suspense>
);
}

View File

@@ -1,20 +0,0 @@
"use client";
import { useEffect } from "react";
import Link from "next/link";
export default function LogoutPage() {
useEffect(() => {
document.cookie = "portal-role=; path=/; max-age=0";
}, []);
return (
<div className="container mx-auto px-4 py-12 max-w-md">
<h1 className="text-3xl font-bold text-neutral-900">Signed out</h1>
<p className="mt-4 text-neutral-600">You have been signed out.</p>
<Link href="/" className="mt-6 inline-block text-primary-600 hover:underline">
Return to home
</Link>
</div>
);
}

View File

@@ -1,8 +0,0 @@
export default function AboutPage() {
return (
<div className="container mx-auto px-4 py-12">
<h1 className="text-3xl font-bold text-neutral-900">Mandate</h1>
<p className="mt-4 text-neutral-600">About the Digital Bank of International Settlements.</p>
</div>
);
}

View File

@@ -1,8 +0,0 @@
export default function AccreditationPage() {
return (
<div className="container mx-auto px-4 py-12">
<h1 className="text-3xl font-bold text-neutral-900">Accreditation</h1>
<p className="mt-4 text-neutral-600">Accreditation and credentials.</p>
</div>
);
}

View File

@@ -1,8 +0,0 @@
export default function ContactPage() {
return (
<div className="container mx-auto px-4 py-12">
<h1 className="text-3xl font-bold text-neutral-900">Contact</h1>
<p className="mt-4 text-neutral-600">General contact and enquiries.</p>
</div>
);
}

View File

@@ -1,8 +0,0 @@
export default function DepartmentsPage() {
return (
<div className="container mx-auto px-4 py-12">
<h1 className="text-3xl font-bold text-neutral-900">Departments</h1>
<p className="mt-4 text-neutral-600">Mission departments and programs.</p>
</div>
);
}

View File

@@ -1,8 +0,0 @@
export default function DocumentsPage() {
return (
<div className="container mx-auto px-4 py-12">
<h1 className="text-3xl font-bold text-neutral-900">Documents</h1>
<p className="mt-4 text-neutral-600">Library, standards, and directives.</p>
</div>
);
}

View File

@@ -1,8 +0,0 @@
export default function GovernancePage() {
return (
<div className="container mx-auto px-4 py-12">
<h1 className="text-3xl font-bold text-neutral-900">Governance</h1>
<p className="mt-4 text-neutral-600">Charters, instruments, and governance.</p>
</div>
);
}

View File

@@ -1,8 +0,0 @@
export default function LeadershipPage() {
return (
<div className="container mx-auto px-4 py-12">
<h1 className="text-3xl font-bold text-neutral-900">Leadership</h1>
<p className="mt-4 text-neutral-600">Executive leadership and secretariat.</p>
</div>
);
}

View File

@@ -1,8 +0,0 @@
export default function MembershipPage() {
return (
<div className="container mx-auto px-4 py-12">
<h1 className="text-3xl font-bold text-neutral-900">Membership</h1>
<p className="mt-4 text-neutral-600">Member directory and accreditation.</p>
</div>
);
}

View File

@@ -1,8 +0,0 @@
export default function NewsPage() {
return (
<div className="container mx-auto px-4 py-12">
<h1 className="text-3xl font-bold text-neutral-900">News</h1>
<p className="mt-4 text-neutral-600">News and announcements.</p>
</div>
);
}

View File

@@ -1,8 +0,0 @@
export default function OrgChartPage() {
return (
<div className="container mx-auto px-4 py-12">
<h1 className="text-3xl font-bold text-neutral-900">Org Chart</h1>
<p className="mt-4 text-neutral-600">Organizational structure.</p>
</div>
);
}

View File

@@ -1,8 +0,0 @@
export default function ProgramsPage() {
return (
<div className="container mx-auto px-4 py-12">
<h1 className="text-3xl font-bold text-neutral-900">Programs</h1>
<p className="mt-4 text-neutral-600">Programs and initiatives.</p>
</div>
);
}

View File

@@ -1,8 +0,0 @@
export default function PublicationsPage() {
return (
<div className="container mx-auto px-4 py-12">
<h1 className="text-3xl font-bold text-neutral-900">Publications</h1>
<p className="mt-4 text-neutral-600">Publications and reports.</p>
</div>
);
}

View File

@@ -1,8 +0,0 @@
export default function RegionsPage() {
return (
<div className="container mx-auto px-4 py-12">
<h1 className="text-3xl font-bold text-neutral-900">Regional Offices</h1>
<p className="mt-4 text-neutral-600">Americas, EMEA, APAC delegations and field missions.</p>
</div>
);
}

View File

@@ -1,8 +0,0 @@
export default function StandardsPage() {
return (
<div className="container mx-auto px-4 py-12">
<h1 className="text-3xl font-bold text-neutral-900">Standards</h1>
<p className="mt-4 text-neutral-600">Standards and codes.</p>
</div>
);
}

View File

@@ -1,10 +0,0 @@
import { NextRequest, NextResponse } from "next/server";
export async function POST(request: NextRequest) {
const form = await request.formData();
const returnUrl = (form.get("returnUrl") as string) ?? "/dashboard";
const role = (form.get("role") as string) ?? "member";
const res = NextResponse.redirect(new URL(returnUrl, request.url));
res.cookies.set("portal-role", role, { path: "/", httpOnly: true, maxAge: 60 * 60 * 24 });
return res;
}

View File

@@ -1,8 +0,0 @@
export default function ApplyPage() {
return (
<div className="container mx-auto px-4 py-12">
<h1 className="text-3xl font-bold text-neutral-900">Apply</h1>
<p className="mt-4 text-neutral-600">Apply for membership or accreditation.</p>
</div>
);
}

View File

@@ -1,8 +0,0 @@
export default function CaseLawPage() {
return (
<div className="container mx-auto px-4 py-12">
<h1 className="text-3xl font-bold text-neutral-900">Case Law</h1>
<p className="mt-4 text-neutral-600">Case law and decisions.</p>
</div>
);
}

View File

@@ -1,8 +0,0 @@
export default function ChambersPage() {
return (
<div className="container mx-auto px-4 py-12">
<h1 className="text-3xl font-bold text-neutral-900">Chambers</h1>
<p className="mt-4 text-neutral-600">Trial and appeals chambers.</p>
</div>
);
}

View File

@@ -1,17 +0,0 @@
import Link from "next/link";
import type { ReactNode } from "react";
export default function DashboardLayout({ children }: { children: ReactNode }) {
return (
<div className="flex min-h-[60vh]">
<aside className="w-56 border-r border-neutral-200 bg-white p-4">
<nav className="space-y-1" aria-label="Dashboard navigation">
<Link href="/dashboard" className="block rounded px-3 py-2 text-sm font-medium text-neutral-700 hover:bg-neutral-100">Home</Link>
<Link href="/dashboard/submissions" className="block rounded px-3 py-2 text-sm font-medium text-neutral-700 hover:bg-neutral-100">My submissions</Link>
<Link href="/dashboard/requests" className="block rounded px-3 py-2 text-sm font-medium text-neutral-700 hover:bg-neutral-100">My requests</Link>
</nav>
</aside>
<div className="flex-1 p-6">{children}</div>
</div>
);
}

View File

@@ -1,8 +0,0 @@
export default function DashboardPage() {
return (
<div>
<h1 className="text-3xl font-bold text-neutral-900">Dashboard</h1>
<p className="mt-4 text-neutral-600">Role-based dashboard.</p>
</div>
);
}

View File

@@ -1,8 +0,0 @@
export default function RequestsPage() {
return (
<div>
<h1 className="text-3xl font-bold text-neutral-900">My requests</h1>
<p className="mt-4 text-neutral-600">Track your requests.</p>
</div>
);
}

View File

@@ -1,8 +0,0 @@
export default function SubmissionsPage() {
return (
<div>
<h1 className="text-3xl font-bold text-neutral-900">My submissions</h1>
<p className="mt-4 text-neutral-600">Track your submissions and status.</p>
</div>
);
}

View File

@@ -1,3 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@@ -1,8 +0,0 @@
export default function JudgmentsPage() {
return (
<div className="container mx-auto px-4 py-12">
<h1 className="text-3xl font-bold text-neutral-900">Judgments</h1>
<p className="mt-4 text-neutral-600">Judgments, orders, and remedies.</p>
</div>
);
}

View File

@@ -1,34 +0,0 @@
import type { Metadata } from "next";
import type { ReactNode } from "react";
import "./globals.css";
import { Header } from "@/components/layout/Header";
import { Footer } from "@/components/layout/Footer";
export const metadata: Metadata = {
title: "International Criminal Court of Commerce",
description: "Official portal of the International Criminal Court of Commerce",
};
export default function RootLayout({
children,
}: Readonly<{
children: ReactNode;
}>) {
return (
<html lang="en">
<body className="min-h-screen flex flex-col bg-neutral-50 text-neutral-900 antialiased">
<a
href="#main-content"
className="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-[100] focus:px-4 focus:py-2 focus:bg-primary-600 focus:text-white focus:rounded"
>
Skip to main content
</a>
<Header />
<main id="main-content" className="flex-1">
{children}
</main>
<Footer />
</body>
</html>
);
}

View File

@@ -1,8 +0,0 @@
export default function RenewalsPage() {
return (
<div className="container mx-auto px-4 py-12">
<h1 className="text-3xl font-bold text-neutral-900">Renewals and Attestations</h1>
<p className="mt-4 text-neutral-600">Annual renewals for members.</p>
</div>
);
}

View File

@@ -1,24 +0,0 @@
export default function HomePage() {
return (
<div className="container mx-auto px-4 py-12">
<h1 className="text-4xl font-bold text-neutral-900">International Criminal Court of Commerce</h1>
<p className="mt-4 text-lg text-neutral-600">
Welcome to the official portal. Explore registry, chambers, case law, and judgments.
</p>
<div className="mt-8 flex flex-wrap gap-4">
<a href="/about" className="inline-flex items-center rounded-md bg-primary-600 px-4 py-2 text-sm font-medium text-white hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2">
About
</a>
<a href="/documents" className="inline-flex items-center rounded-md border border-neutral-300 bg-white px-4 py-2 text-sm font-medium text-neutral-700 hover:bg-neutral-50 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2">
Documents
</a>
<a href="/transparency" className="inline-flex items-center rounded-md border border-neutral-300 bg-white px-4 py-2 text-sm font-medium text-neutral-700 hover:bg-neutral-50 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2">
Transparency
</a>
<a href="/dashboard" className="inline-flex items-center rounded-md border border-neutral-300 bg-white px-4 py-2 text-sm font-medium text-neutral-700 hover:bg-neutral-50 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2">
Dashboard
</a>
</div>
</div>
);
}

View File

@@ -1,8 +0,0 @@
export default function RegistryPage() {
return (
<div className="container mx-auto px-4 py-12">
<h1 className="text-3xl font-bold text-neutral-900">Registry</h1>
<p className="mt-4 text-neutral-600">Clerk of Court and registry.</p>
</div>
);
}

View File

@@ -1,8 +0,0 @@
export default function ReportPage() {
return (
<div className="container mx-auto px-4 py-12">
<h1 className="text-3xl font-bold text-neutral-900">Report</h1>
<p className="mt-4 text-neutral-600">Report incidents or compliance matters.</p>
</div>
);
}

View File

@@ -1,8 +0,0 @@
export default function RequestPage() {
return (
<div className="container mx-auto px-4 py-12">
<h1 className="text-3xl font-bold text-neutral-900">Request</h1>
<p className="mt-4 text-neutral-600">Request documents or support.</p>
</div>
);
}

View File

@@ -1,8 +0,0 @@
export default function SubmitPage() {
return (
<div className="container mx-auto px-4 py-12">
<h1 className="text-3xl font-bold text-neutral-900">Submit</h1>
<p className="mt-4 text-neutral-600">Submission portal.</p>
</div>
);
}

View File

@@ -1,8 +0,0 @@
export default function AuditPage() {
return (
<div className="container mx-auto px-4 py-12">
<h1 className="text-3xl font-bold text-neutral-900">Audit Reports</h1>
<p className="mt-4 text-neutral-600">Audit reports and findings tracker.</p>
</div>
);
}

View File

@@ -1,8 +0,0 @@
export default function DataProtectionPage() {
return (
<div className="container mx-auto px-4 py-12">
<h1 className="text-3xl font-bold text-neutral-900">Data Protection and Records</h1>
<p className="mt-4 text-neutral-600">Records retention and data protection.</p>
</div>
);
}

View File

@@ -1,8 +0,0 @@
export default function EthicsPage() {
return (
<div className="container mx-auto px-4 py-12">
<h1 className="text-3xl font-bold text-neutral-900">Ethics and Conduct</h1>
<p className="mt-4 text-neutral-600">Ethics and conduct policies.</p>
</div>
);
}

View File

@@ -1,8 +0,0 @@
export default function TransparencyPage() {
return (
<div className="container mx-auto px-4 py-12">
<h1 className="text-3xl font-bold text-neutral-900">Transparency</h1>
<p className="mt-4 text-neutral-600">Audit, ethics, whistleblower, sanctions, and data protection.</p>
</div>
);
}

View File

@@ -1,8 +0,0 @@
export default function SanctionsPage() {
return (
<div className="container mx-auto px-4 py-12">
<h1 className="text-3xl font-bold text-neutral-900">Sanctions and Restricted Parties</h1>
<p className="mt-4 text-neutral-600">Eligibility and restricted parties.</p>
</div>
);
}

Some files were not shown because too many files have changed in this diff Show More