fix(portal): corporate landing SEO, favicon, and public home UX
Some checks failed
API CI / API Lint (push) Successful in 53s
API CI / API Type Check (push) Failing after 51s
API CI / API Test (push) Successful in 1m32s
API CI / API Build (push) Failing after 1m0s
API CI / Build Docker Image (push) Has been skipped
CD Pipeline / Deploy to Staging (push) Failing after 32s
CI Pipeline / Lint and Type Check (push) Failing after 33s
CI Pipeline / Build (push) Has been skipped
CI Pipeline / Test Backend (push) Failing after 2m1s
CI Pipeline / Test Frontend (push) Failing after 35s
CI Pipeline / Security Scan (push) Failing after 1m12s
Deploy to Staging / Deploy to Staging (push) Failing after 28s
Portal CI / Portal Lint (push) Failing after 19s
Portal CI / Portal Type Check (push) Failing after 18s
Portal CI / Portal Test (push) Failing after 19s
Portal CI / Portal Build (push) Failing after 18s
Test Suite / frontend-tests (push) Failing after 30s
Test Suite / api-tests (push) Failing after 44s
Test Suite / blockchain-tests (push) Failing after 27s
Type Check / type-check (map[directory:. name:root]) (push) Failing after 18s
Type Check / type-check (map[directory:api name:api]) (push) Failing after 19s
Type Check / type-check (map[directory:portal name:portal]) (push) Failing after 18s
CD Pipeline / Deploy to Production (push) Has been skipped

Add sankofa.nexus marketing site with institutional grade scorecards,
server-side metadata/title, dynamic icons, favicon rewrite, and instant
landing render without session-loading flash; split authenticated AppShell
from unauthenticated corporate chrome.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
defiQUG
2026-06-11 01:27:05 -07:00
parent 73a7b9fc15
commit 456cc613b7
39 changed files with 2641 additions and 305 deletions

View File

@@ -0,0 +1,61 @@
'use client';
import { signIn } from 'next-auth/react';
import { useCallback, useState, type ReactNode } from 'react';
import { CloudflareTurnstile } from '@/components/security/CloudflareTurnstile';
const TURNSTILE_SITE_KEY = process.env.NEXT_PUBLIC_CLOUDFLARE_TURNSTILE_SITE_KEY;
export interface PortalSignInCardProps {
badge?: string;
title: string;
subtitle: string;
callbackUrl?: string;
/** Extra hint under the sign-in button (e.g. dev IdP note) */
footer?: ReactNode;
}
export function PortalSignInCard({
badge = 'Sankofa Phoenix',
title,
subtitle,
callbackUrl = '/',
footer,
}: PortalSignInCardProps) {
const [turnstileToken, setTurnstileToken] = useState<string | null>(() =>
TURNSTILE_SITE_KEY ? null : ''
);
const onTurnstileToken = useCallback((token: string | null) => {
setTurnstileToken(token);
}, []);
const canSignIn = !TURNSTILE_SITE_KEY || Boolean(turnstileToken);
return (
<div className="w-full max-w-md rounded-2xl border border-gray-800 bg-gray-900/80 p-8 shadow-xl shadow-black/40 backdrop-blur-sm">
<p className="mb-1 text-center text-sm font-medium uppercase tracking-wide text-orange-400">{badge}</p>
<h1 className="mb-2 text-center text-2xl font-bold text-white">{title}</h1>
<p className="mb-8 text-center text-gray-400">{subtitle}</p>
{TURNSTILE_SITE_KEY ? (
<div className="mb-6">
<p className="mb-2 text-center text-sm text-gray-500">Verification</p>
<CloudflareTurnstile siteKey={TURNSTILE_SITE_KEY} onToken={onTurnstileToken} />
</div>
) : null}
<button
type="button"
disabled={!canSignIn}
onClick={() => signIn(undefined, { callbackUrl })}
className="w-full rounded-lg bg-gradient-to-r from-orange-500 to-amber-500 px-6 py-3 font-semibold text-gray-950 shadow-lg transition hover:from-orange-400 hover:to-amber-400 focus:outline-none focus:ring-2 focus:ring-orange-400 focus:ring-offset-2 focus:ring-offset-gray-900 disabled:cursor-not-allowed disabled:opacity-50"
>
Sign In
</button>
{footer}
</div>
);
}

View File

@@ -0,0 +1,94 @@
import Link from 'next/link';
import { ECOSYSTEM_SIGN_IN_PATH } from '@/lib/corporate-site-data';
function isExternalHref(href: string) {
return href.startsWith('http://') || href.startsWith('https://');
}
const footerColumns = [
{
title: 'Products',
links: [
{ label: 'Phoenix Cloud', href: 'https://phoenix.sankofa.nexus' },
{ label: 'Complete Credential', href: 'https://cc.sankofa.nexus' },
{ label: 'Sankofa Studio', href: 'https://studio.sankofa.nexus/studio/' },
{ label: 'Client Portal', href: 'https://portal.sankofa.nexus' },
],
},
{
title: 'Platform',
links: [
{ label: 'Identity (Keycloak)', href: 'https://keycloak.sankofa.nexus' },
{ label: 'Blockchain Explorer', href: 'https://explorer.d-bis.org' },
{ label: 'Documentation', href: 'https://docs.d-bis.org' },
{ label: 'Marketplace', href: 'https://portal.sankofa.nexus' },
],
},
{
title: 'Company',
links: [
{ label: 'About Sankofa', href: '#resources' },
{ label: 'Institutional registry', href: 'https://d-bis.org/cb/dbis' },
{ label: 'Partner program', href: '/partner' },
{ label: 'Contact', href: 'https://portal.sankofa.nexus' },
],
},
{
title: 'Access',
links: [
{ label: 'Sign in to ecosystem', href: ECOSYSTEM_SIGN_IN_PATH },
{ label: 'Phoenix console', href: 'https://phoenix.sankofa.nexus' },
{ label: 'Operator dashboard', href: 'https://dash.sankofa.nexus' },
{ label: 'Admin console', href: 'https://admin.sankofa.nexus' },
],
},
];
export function CorporateFooter() {
return (
<footer className="border-t border-gray-800 bg-gray-950">
<div className="mx-auto max-w-7xl px-4 py-12 sm:px-6 lg:px-8">
<div className="grid grid-cols-2 gap-8 md:grid-cols-4 lg:gap-12">
{footerColumns.map((column) => (
<div key={column.title}>
<h3 className="mb-4 text-sm font-semibold uppercase tracking-wider text-gray-400">
{column.title}
</h3>
<ul className="space-y-2.5">
{column.links.map((link) => (
<li key={link.label}>
<Link
href={link.href}
className="text-sm text-gray-400 no-underline transition-colors hover:text-orange-300"
{...(isExternalHref(link.href)
? { target: '_blank', rel: 'noopener noreferrer' }
: {})}
>
{link.label}
</Link>
</li>
))}
</ul>
</div>
))}
</div>
<div className="mt-12 flex flex-col items-start justify-between gap-4 border-t border-gray-800 pt-8 sm:flex-row sm:items-center">
<div className="flex items-center gap-3">
<span className="flex h-7 w-7 items-center justify-center rounded-md bg-gradient-to-br from-orange-500 to-amber-400 text-xs font-bold text-gray-950">
S
</span>
<div>
<p className="text-sm font-medium text-white">Sankofa Sovereign Technologies</p>
<p className="text-xs text-gray-500">Remember · Retrieve · Restore · Rise</p>
</div>
</div>
<p className="text-xs text-gray-500">
© {new Date().getFullYear()} Sankofa Ltd. All rights reserved.
</p>
</div>
</div>
</footer>
);
}

View File

@@ -0,0 +1,122 @@
'use client';
import { ChevronDown, LogIn, Menu, X } from 'lucide-react';
import Link from 'next/link';
import { useState } from 'react';
import { corporateNav, ECOSYSTEM_SIGN_IN_PATH } from '@/lib/corporate-site-data';
import { cn } from '@/lib/utils';
export function CorporateHeader() {
const [mobileOpen, setMobileOpen] = useState(false);
return (
<header className="sticky top-0 z-50 border-b border-gray-800/80 bg-gray-950/95 backdrop-blur-md">
<div className="mx-auto flex h-16 max-w-7xl items-center gap-4 px-4 sm:px-6 lg:px-8">
{/* Top-left: brand + ecosystem sign-in (AWS / Microsoft pattern) */}
<div className="flex shrink-0 items-center gap-3 sm:gap-4">
<Link href="/" className="group flex items-center gap-2 no-underline">
<span
className="flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-orange-500 to-amber-400 text-sm font-bold text-gray-950 shadow-lg shadow-orange-500/20"
aria-hidden
>
S
</span>
<span className="hidden bg-gradient-to-r from-orange-300 to-amber-200 bg-clip-text text-lg font-semibold tracking-tight text-transparent sm:inline">
Sankofa
</span>
</Link>
<span className="hidden h-5 w-px bg-gray-700 sm:block" aria-hidden />
<Link
href={ECOSYSTEM_SIGN_IN_PATH}
className="inline-flex items-center gap-1.5 rounded-md px-2 py-1.5 text-sm font-medium text-orange-300 no-underline transition-colors hover:bg-orange-500/10 hover:text-orange-200"
>
<LogIn className="h-4 w-4" aria-hidden />
<span>Sign in</span>
</Link>
</div>
{/* Desktop nav */}
<nav className="hidden flex-1 items-center justify-center gap-1 lg:flex" aria-label="Primary">
{corporateNav.map((item) => (
<Link
key={item.href}
href={item.href}
className="rounded-md px-3 py-2 text-sm font-medium text-gray-300 no-underline transition-colors hover:bg-gray-800/60 hover:text-white"
>
{item.label}
</Link>
))}
</nav>
{/* Desktop CTAs */}
<div className="ml-auto hidden items-center gap-2 lg:flex">
<Link
href="https://portal.sankofa.nexus"
className="rounded-md px-3 py-2 text-sm font-medium text-gray-300 no-underline transition-colors hover:text-white"
>
Contact sales
</Link>
<Link
href="https://phoenix.sankofa.nexus"
className="rounded-lg bg-gradient-to-r from-orange-500 to-amber-500 px-4 py-2 text-sm font-semibold text-gray-950 no-underline shadow-md shadow-orange-500/25 transition hover:from-orange-400 hover:to-amber-400"
>
Get started
</Link>
</div>
{/* Mobile menu toggle */}
<button
type="button"
className="ml-auto inline-flex items-center justify-center rounded-md p-2 text-gray-400 hover:bg-gray-800 hover:text-white lg:hidden"
aria-expanded={mobileOpen}
aria-label={mobileOpen ? 'Close menu' : 'Open menu'}
onClick={() => setMobileOpen((open) => !open)}
>
{mobileOpen ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />}
</button>
</div>
{/* Mobile drawer */}
<div
className={cn(
'overflow-hidden border-t border-gray-800/80 bg-gray-950 lg:hidden',
mobileOpen ? 'max-h-[28rem] opacity-100' : 'max-h-0 opacity-0'
)}
>
<nav className="flex flex-col gap-1 px-4 py-3" aria-label="Mobile primary">
{corporateNav.map((item) => (
<Link
key={item.href}
href={item.href}
className="flex items-center justify-between rounded-md px-3 py-2.5 text-sm font-medium text-gray-200 no-underline hover:bg-gray-800"
onClick={() => setMobileOpen(false)}
>
{item.label}
<ChevronDown className="-rotate-90 h-4 w-4 text-gray-500" aria-hidden />
</Link>
))}
<div className="mt-2 flex flex-col gap-2 border-t border-gray-800 pt-3">
<Link
href={ECOSYSTEM_SIGN_IN_PATH}
className="inline-flex items-center justify-center gap-2 rounded-lg border border-gray-700 px-4 py-2.5 text-sm font-medium text-white no-underline hover:bg-gray-800"
onClick={() => setMobileOpen(false)}
>
<LogIn className="h-4 w-4" />
Sign in to ecosystem
</Link>
<Link
href="https://phoenix.sankofa.nexus"
className="inline-flex items-center justify-center rounded-lg bg-gradient-to-r from-orange-500 to-amber-500 px-4 py-2.5 text-sm font-semibold text-gray-950 no-underline"
onClick={() => setMobileOpen(false)}
>
Get started with Phoenix
</Link>
</div>
</nav>
</div>
</header>
);
}

View File

@@ -0,0 +1,270 @@
'use client';
import { ArrowRight, ExternalLink } from 'lucide-react';
import Link from 'next/link';
import { CorporateFooter } from '@/components/corporate/CorporateFooter';
import { CorporateHeader } from '@/components/corporate/CorporateHeader';
import { InstitutionalGradeSection } from '@/components/corporate/InstitutionalGradeSection';
import {
ECOSYSTEM_SIGN_IN_PATH,
philosophySteps,
platformServices,
productDivisions,
solutionVerticals,
} from '@/lib/corporate-site-data';
export function CorporateLandingPage() {
return (
<div className="flex min-h-screen flex-col bg-gray-950 text-gray-100">
<CorporateHeader />
<main className="flex-1">
{/* Hero */}
<section className="relative overflow-hidden border-b border-gray-800/60">
<div
className="pointer-events-none absolute inset-0 bg-[radial-gradient(ellipse_80%_60%_at_50%_-20%,rgba(251,146,60,0.18),transparent)]"
aria-hidden
/>
<div className="relative mx-auto max-w-7xl px-4 py-20 sm:px-6 sm:py-28 lg:px-8">
<p className="mb-4 text-sm font-semibold uppercase tracking-widest text-orange-400">
Sovereign Technologies
</p>
<h1 className="max-w-4xl text-4xl font-bold tracking-tight text-white sm:text-5xl lg:text-6xl">
Build on infrastructure you{' '}
<span className="bg-gradient-to-r from-orange-300 to-amber-200 bg-clip-text text-transparent">
own and control
</span>
</h1>
<p className="mt-6 max-w-2xl text-lg leading-relaxed text-gray-400">
Sankofa delivers sovereign cloud, identity, financial rails, and credential infrastructure
the complete ecosystem for institutions that cannot depend on hyperscaler public cloud.
</p>
<div className="mt-10 flex flex-wrap items-center gap-4">
<Link
href="https://phoenix.sankofa.nexus"
className="inline-flex items-center gap-2 rounded-lg bg-gradient-to-r from-orange-500 to-amber-500 px-6 py-3 text-sm font-semibold text-gray-950 no-underline shadow-lg shadow-orange-500/25 transition hover:from-orange-400 hover:to-amber-400"
>
Explore Phoenix Cloud
<ArrowRight className="h-4 w-4" aria-hidden />
</Link>
<Link
href={ECOSYSTEM_SIGN_IN_PATH}
className="inline-flex items-center gap-2 rounded-lg border border-gray-700 bg-gray-900/50 px-6 py-3 text-sm font-semibold text-white no-underline transition hover:border-gray-600 hover:bg-gray-800"
>
Sign in to ecosystem
</Link>
</div>
</div>
</section>
{/* Philosophy */}
<section className="border-b border-gray-800/60 bg-gray-900/30 py-14">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
{philosophySteps.map((step, index) => (
<div key={step.verb} className="relative rounded-xl border border-gray-800/80 bg-gray-900/40 p-5">
<span className="text-xs font-medium uppercase tracking-wider text-orange-500/80">
{String(index + 1).padStart(2, '0')}
</span>
<h2 className="mt-2 text-xl font-semibold text-white">{step.verb}</h2>
<p className="mt-2 text-sm leading-relaxed text-gray-400">{step.detail}</p>
</div>
))}
</div>
</div>
</section>
{/* Product divisions */}
<section id="products" className="scroll-mt-20 py-20">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="max-w-2xl">
<h2 className="text-3xl font-bold tracking-tight text-white sm:text-4xl">Products</h2>
<p className="mt-4 text-lg text-gray-400">
From sovereign cloud to verifiable credentials integrated products under one ecosystem,
modeled for institutional scale.
</p>
</div>
<div className="mt-12 grid gap-6 sm:grid-cols-2 xl:grid-cols-3">
{productDivisions.map((product) => {
const Icon = product.icon;
return (
<article
key={product.id}
className="group flex flex-col rounded-2xl border border-gray-800 bg-gray-900/40 p-6 transition hover:border-orange-500/40 hover:bg-gray-900/70"
>
<div className="mb-4 flex items-start justify-between gap-3">
<div className="flex h-11 w-11 items-center justify-center rounded-xl bg-orange-500/10 text-orange-400 ring-1 ring-orange-500/20">
<Icon className="h-5 w-5" aria-hidden />
</div>
{product.external ? (
<ExternalLink className="h-4 w-4 shrink-0 text-gray-600 group-hover:text-orange-400" aria-hidden />
) : null}
</div>
<p className="text-xs font-semibold uppercase tracking-wider text-orange-400/90">
{product.tagline}
</p>
<h3 className="mt-1 text-xl font-semibold text-white">{product.name}</h3>
<p className="mt-3 flex-1 text-sm leading-relaxed text-gray-400">{product.description}</p>
<ul className="mt-4 space-y-1.5">
{product.highlights.map((item) => (
<li key={item} className="flex items-center gap-2 text-sm text-gray-300">
<span className="h-1 w-1 rounded-full bg-orange-500" aria-hidden />
{item}
</li>
))}
</ul>
<Link
href={product.href}
className="mt-6 inline-flex items-center gap-1.5 text-sm font-medium text-orange-300 no-underline hover:text-orange-200"
{...(product.external ? { target: '_blank', rel: 'noopener noreferrer' } : {})}
>
Learn more
<ArrowRight className="h-4 w-4 transition group-hover:translate-x-0.5" aria-hidden />
</Link>
</article>
);
})}
</div>
</div>
</section>
{/* Platform services */}
<section id="services" className="scroll-mt-20 border-y border-gray-800/60 bg-gray-900/20 py-20">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="max-w-2xl">
<h2 className="text-3xl font-bold tracking-tight text-white sm:text-4xl">Platform services</h2>
<p className="mt-4 text-lg text-gray-400">
Owned core primitives ledger, identity, wallet, orchestration available through the
Phoenix marketplace without third-party platform lock-in.
</p>
</div>
<div className="mt-12 grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{platformServices.map((service) => {
const Icon = service.icon;
return (
<div
key={service.name}
className="rounded-xl border border-gray-800 bg-gray-950/60 p-5 transition hover:border-gray-700"
>
<div className="flex items-center gap-3">
<div className="flex h-9 w-9 items-center justify-center rounded-lg bg-gray-800 text-orange-400">
<Icon className="h-4 w-4" aria-hidden />
</div>
<div>
<p className="text-xs font-medium uppercase tracking-wide text-gray-500">{service.category}</p>
<h3 className="text-base font-semibold text-white">{service.name}</h3>
</div>
</div>
<p className="mt-3 text-sm leading-relaxed text-gray-400">{service.description}</p>
</div>
);
})}
</div>
<div className="mt-10">
<Link
href="https://portal.sankofa.nexus"
className="inline-flex items-center gap-2 text-sm font-medium text-orange-300 no-underline hover:text-orange-200"
>
Browse full marketplace catalog
<ArrowRight className="h-4 w-4" aria-hidden />
</Link>
</div>
</div>
</section>
{/* Solutions */}
<section id="solutions" className="scroll-mt-20 py-20">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="max-w-2xl">
<h2 className="text-3xl font-bold tracking-tight text-white sm:text-4xl">Industry solutions</h2>
<p className="mt-4 text-lg text-gray-400">
Purpose-built stacks for regulated industries public sector, finance, healthcare, and telecom.
</p>
</div>
<div className="mt-12 grid gap-6 sm:grid-cols-2">
{solutionVerticals.map((vertical) => {
const Icon = vertical.icon;
return (
<div
key={vertical.name}
className="flex gap-4 rounded-2xl border border-gray-800 bg-gray-900/30 p-6"
>
<div className="flex h-12 w-12 shrink-0 items-center justify-center rounded-xl bg-gradient-to-br from-orange-500/20 to-amber-500/10 text-orange-400">
<Icon className="h-6 w-6" aria-hidden />
</div>
<div>
<h3 className="text-lg font-semibold text-white">{vertical.name}</h3>
<p className="mt-2 text-sm leading-relaxed text-gray-400">{vertical.description}</p>
</div>
</div>
);
})}
</div>
</div>
</section>
<InstitutionalGradeSection />
{/* Resources / trust */}
<section id="resources" className="scroll-mt-20 border-t border-gray-800/60 bg-gray-900/30 py-20">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="grid gap-10 lg:grid-cols-2 lg:items-center">
<div>
<h2 className="text-3xl font-bold tracking-tight text-white">Built for institutional trust</h2>
<p className="mt-4 text-lg leading-relaxed text-gray-400">
Sankofa Phoenix operates under sovereign governance with SOC 2, GDPR, and sector-specific
compliance pathways. Identity, ledger, and credential services are designed for operators
who need auditability without outsourcing control.
</p>
<ul className="mt-6 space-y-3 text-sm text-gray-300">
<li className="flex items-center gap-2">
<span className="text-orange-500"></span> Sovereign Keycloak identity no Azure AD dependency
</li>
<li className="flex items-center gap-2">
<span className="text-orange-500"></span> Multi-tenant Proxmox orchestration with GitOps
</li>
<li className="flex items-center gap-2">
<span className="text-orange-500"></span> Chain 138 DeFi Oracle Meta Mainnet & cross-chain mesh
</li>
<li className="flex items-center gap-2">
<span className="text-orange-500"></span> Complete Credential eIDAS-aligned issuance
</li>
</ul>
</div>
<div className="rounded-2xl border border-gray-800 bg-gradient-to-br from-gray-900 to-gray-950 p-8 shadow-xl">
<p className="text-sm font-semibold uppercase tracking-wider text-orange-400">Get started</p>
<h3 className="mt-2 text-2xl font-bold text-white">Join the Sankofa ecosystem</h3>
<p className="mt-3 text-sm leading-relaxed text-gray-400">
Sign in to access Phoenix Cloud, marketplace subscriptions, partner tools, and client
workspaces one identity across the platform.
</p>
<div className="mt-6 flex flex-col gap-3 sm:flex-row">
<Link
href={ECOSYSTEM_SIGN_IN_PATH}
className="inline-flex flex-1 items-center justify-center rounded-lg bg-gradient-to-r from-orange-500 to-amber-500 px-5 py-3 text-sm font-semibold text-gray-950 no-underline hover:from-orange-400 hover:to-amber-400"
>
Sign in to ecosystem
</Link>
<Link
href="https://phoenix.sankofa.nexus"
className="inline-flex flex-1 items-center justify-center rounded-lg border border-gray-700 px-5 py-3 text-sm font-semibold text-white no-underline hover:bg-gray-800"
>
Phoenix Cloud
</Link>
</div>
</div>
</div>
</div>
</section>
</main>
<CorporateFooter />
</div>
);
}

View File

@@ -0,0 +1,186 @@
import { ArrowRight, ShieldCheck } from 'lucide-react';
import Link from 'next/link';
import {
gradeBadgeClass,
institutionalGradeRubric,
scoreBarClass,
sovereignInstitutionGrades,
} from '@/lib/corporate-site-data';
const audienceLabels = {
sovereign: 'Sovereign & public sector',
regulated: 'Regulated counterparties',
platform: 'Platform & L1',
} as const;
export function InstitutionalGradeSection() {
const sovereignRows = sovereignInstitutionGrades.filter((e) => e.audience === 'sovereign');
const otherRows = sovereignInstitutionGrades.filter((e) => e.audience !== 'sovereign');
return (
<section id="institutional-grade" className="scroll-mt-20 border-y border-gray-800/60 bg-gray-950 py-20">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
<div className="max-w-2xl">
<div className="mb-3 inline-flex items-center gap-2 rounded-full border border-orange-500/30 bg-orange-500/10 px-3 py-1 text-xs font-semibold uppercase tracking-wider text-orange-300">
<ShieldCheck className="h-3.5 w-3.5" aria-hidden />
Institutional readiness
</div>
<h2 className="text-3xl font-bold tracking-tight text-white sm:text-4xl">
Grade &amp; score for sovereign institutions
</h2>
<p className="mt-4 text-lg text-gray-400">
Transparent engineering scorecards aligned with the DBIS institutional rubric the same
framework used for ecosystem readiness, jurisdiction matrices, and settlement evidence.
</p>
</div>
<p className="max-w-md text-xs leading-relaxed text-gray-500">
Informational only not legal advice, a regulatory determination, or an audit certificate.
Counsel owns statutory interpretation. Scores reflect repo artifacts and live probes as of each
assessment date.
</p>
</div>
{/* Rubric legend */}
<div className="mt-10 overflow-x-auto rounded-xl border border-gray-800 bg-gray-900/40">
<table className="w-full min-w-[640px] text-left text-sm">
<thead>
<tr className="border-b border-gray-800 text-xs uppercase tracking-wider text-gray-500">
<th className="px-4 py-3 font-semibold">Grade</th>
<th className="px-4 py-3 font-semibold">Score</th>
<th className="px-4 py-3 font-semibold">Institutional meaning</th>
</tr>
</thead>
<tbody>
{institutionalGradeRubric.map((row) => (
<tr key={row.grade} className="border-b border-gray-800/60 last:border-0">
<td className="px-4 py-3">
<span
className={`inline-flex min-w-[2rem] items-center justify-center rounded-md px-2 py-0.5 text-sm font-bold ring-1 ${gradeBadgeClass(row.grade)}`}
>
{row.grade}
</span>
</td>
<td className="px-4 py-3 font-mono text-gray-300">{row.range}</td>
<td className="px-4 py-3 text-gray-400">{row.meaning}</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Sovereign institution scorecards */}
<div className="mt-12">
<h3 className="text-lg font-semibold text-white">{audienceLabels.sovereign}</h3>
<div className="mt-6 grid gap-4 lg:grid-cols-2">
{sovereignRows.map((entry) => (
<GradeCard key={entry.id} entry={entry} />
))}
</div>
</div>
<div className="mt-12">
<h3 className="text-lg font-semibold text-white">Related institutional assessments</h3>
<div className="mt-6 grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
{otherRows.map((entry) => (
<GradeCard key={entry.id} entry={entry} compact />
))}
</div>
</div>
<div className="mt-10 flex flex-wrap items-center gap-4 text-sm">
<Link
href="https://d-bis.org/cb/dbis"
className="inline-flex items-center gap-1.5 font-medium text-orange-300 no-underline hover:text-orange-200"
>
DBIS institutional registry
<ArrowRight className="h-4 w-4" aria-hidden />
</Link>
<span className="text-gray-600" aria-hidden>
·
</span>
<Link
href="https://docs.d-bis.org"
className="inline-flex items-center gap-1.5 font-medium text-orange-300 no-underline hover:text-orange-200"
>
Compliance matrices &amp; onboarding charter
<ArrowRight className="h-4 w-4" aria-hidden />
</Link>
</div>
</div>
</section>
);
}
function GradeCard({
entry,
compact = false,
}: {
entry: (typeof sovereignInstitutionGrades)[number];
compact?: boolean;
}) {
const pct = Math.round((entry.score / entry.maxScore) * 100);
const inner = (
<>
<div className="flex items-start justify-between gap-3">
<div>
<p className="text-sm font-semibold text-white">{entry.name}</p>
{!compact ? (
<p className="mt-1 text-xs text-gray-500">Assessed {entry.assessmentDate}</p>
) : null}
</div>
<span
className={`shrink-0 rounded-lg px-2.5 py-1 text-lg font-bold ring-1 ${gradeBadgeClass(entry.letterGrade)}`}
>
{entry.letterGrade}
</span>
</div>
<div className="mt-4 flex items-baseline gap-2">
<span className="text-3xl font-bold tabular-nums text-white">{entry.score}</span>
<span className="text-sm text-gray-500">/ {entry.maxScore}</span>
</div>
<div className="mt-3 h-1.5 overflow-hidden rounded-full bg-gray-800">
<div
className={`h-full rounded-full transition-all ${scoreBarClass(entry.letterGrade)}`}
style={{ width: `${pct}%` }}
role="progressbar"
aria-valuenow={entry.score}
aria-valuemin={0}
aria-valuemax={entry.maxScore}
aria-label={`${entry.name} score ${entry.score} of ${entry.maxScore}`}
/>
</div>
<p className={`text-gray-400 ${compact ? 'mt-3 text-xs leading-relaxed' : 'mt-4 text-sm leading-relaxed'}`}>
{entry.tier}
</p>
{compact ? (
<p className="mt-2 text-xs text-gray-600">{entry.assessmentDate}</p>
) : null}
</>
);
const className = compact
? 'rounded-xl border border-gray-800 bg-gray-900/30 p-4 transition hover:border-gray-700'
: 'rounded-2xl border border-gray-800 bg-gray-900/40 p-6 transition hover:border-orange-500/30';
if (entry.href) {
return (
<Link href={entry.href} className={`group block no-underline ${className}`}>
{inner}
{!compact ? (
<span className="mt-4 inline-flex items-center gap-1 text-xs font-medium text-orange-400 group-hover:text-orange-300">
View assessment
<ArrowRight className="h-3 w-3" aria-hidden />
</span>
) : null}
</Link>
);
}
return <article className={className}>{inner}</article>;
}

View File

@@ -0,0 +1,35 @@
'use client';
import { useSession } from 'next-auth/react';
import { KeyboardShortcutsProvider } from '@/components/KeyboardShortcutsProvider';
import { MobileNavigation } from '@/components/layout/MobileNavigation';
import { PortalBreadcrumbs } from '@/components/layout/PortalBreadcrumbs';
import { PortalHeader } from '@/components/layout/PortalHeader';
import { PortalSidebar } from '@/components/layout/PortalSidebar';
/**
* Full console chrome only after sign-in. Unauthenticated routes render children alone
* so the landing / sign-in view is not squeezed beside sidebar + mega-header.
*/
export function AppShell({ children }: { children: React.ReactNode }) {
const { status } = useSession();
if (status !== 'authenticated') {
return <>{children}</>;
}
return (
<KeyboardShortcutsProvider>
<div className="flex min-h-screen flex-col bg-gray-950 text-gray-100 antialiased">
<PortalHeader />
<PortalBreadcrumbs />
<div className="flex min-h-0 flex-1">
<PortalSidebar />
<main className="min-w-0 flex-1 md:ml-64">{children}</main>
</div>
<MobileNavigation />
</div>
</KeyboardShortcutsProvider>
);
}

View File

@@ -10,26 +10,32 @@ export function PortalHeader() {
return (
<header className="sticky top-0 z-50 w-full border-b border-gray-800 bg-gray-900/95 backdrop-blur supports-[backdrop-filter]:bg-gray-900/60">
<div className="container flex h-16 items-center">
<Link href="/" className="mr-6 flex items-center space-x-2">
<span className="text-xl font-bold bg-gradient-to-r from-orange-500 to-yellow-500 bg-clip-text text-transparent">
<div className="mx-auto flex h-auto min-h-16 w-full max-w-[1920px] flex-wrap items-center gap-3 px-4 py-2 sm:px-6 lg:h-16 lg:flex-nowrap lg:py-0">
<Link
href="/"
className="order-1 flex shrink-0 items-center no-underline hover:opacity-90"
>
<span className="bg-gradient-to-r from-orange-400 to-amber-400 bg-clip-text text-xl font-bold text-transparent">
Nexus Console
</span>
</Link>
{/* Search Bar - Enterprise-class search-first UX */}
<div className="flex flex-1 items-center justify-center max-w-2xl mx-8">
<div className="order-3 flex min-w-0 flex-1 basis-full items-center lg:order-2 lg:mx-6 lg:max-w-2xl lg:basis-auto">
<div className="relative w-full">
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-400" />
<Search
className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-400"
aria-hidden
/>
<input
type="search"
placeholder="Search resources, settings, docs..."
className="w-full rounded-md border border-gray-700 bg-gray-800 py-2 pl-10 pr-4 text-sm text-white placeholder-gray-400 focus:border-orange-500 focus:outline-none focus:ring-1 focus:ring-orange-500"
placeholder="Search resources, settings, docs"
aria-label="Search resources and settings"
className="w-full rounded-md border border-gray-700 bg-gray-800 py-2 pl-10 pr-4 text-sm text-white placeholder-gray-400 focus:border-orange-500 focus:outline-none focus:ring-2 focus:ring-orange-500/40"
/>
</div>
</div>
<nav className="flex items-center space-x-4">
<nav className="order-2 flex shrink-0 items-center gap-1 sm:gap-2 lg:order-3 lg:ml-auto">
<button className="relative p-2 text-gray-400 hover:text-white transition-colors">
<Bell className="h-5 w-5" />
<span className="absolute top-1 right-1 h-2 w-2 rounded-full bg-orange-500" />
@@ -37,7 +43,7 @@ export function PortalHeader() {
<Link
href="/settings"
className="p-2 text-gray-400 hover:text-white transition-colors"
className="p-2 text-gray-400 no-underline transition-colors hover:text-white"
>
<Settings className="h-5 w-5" />
</Link>

View File

@@ -1,54 +1,25 @@
'use client'
import {
LayoutDashboard,
Server,
Network,
Settings,
FileText,
Activity,
Users,
CreditCard,
Shield,
HelpCircle
} from 'lucide-react'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { primaryNavigation, supportNavigation } from '@/lib/portal-navigation'
import { cn } from '@/lib/utils'
const navigation = [
{ name: 'Dashboard', href: '/', icon: LayoutDashboard },
{ name: 'Infrastructure', href: '/infrastructure', icon: Server },
{ name: 'Resources', href: '/resources', icon: Server },
{ name: 'Virtual Machines', href: '/vms', icon: Server },
{ name: 'Networking', href: '/network', icon: Network },
{ name: 'Monitoring', href: '/dashboards', icon: Activity },
{ name: 'Users & Access', href: '/users', icon: Users },
{ name: 'Billing', href: '/billing', icon: CreditCard },
{ name: 'Security', href: '/security', icon: Shield },
{ name: 'Settings', href: '/settings', icon: Settings },
]
const helpLinks = [
{ name: 'Documentation', href: '/help/docs', icon: FileText },
{ name: 'Support', href: '/help/support', icon: HelpCircle },
]
export function PortalSidebar() {
const pathname = usePathname()
return (
<aside className="fixed left-0 top-16 h-[calc(100vh-4rem)] w-64 border-r border-gray-800 bg-gray-900 overflow-y-auto">
<nav className="p-4 space-y-1">
{navigation.map((item) => {
{primaryNavigation.map((item) => {
const isActive = pathname === item.href || pathname?.startsWith(item.href + '/')
return (
<Link
key={item.name}
href={item.href}
className={cn(
'flex items-center space-x-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors',
'flex items-center space-x-3 rounded-lg px-3 py-2 text-sm font-medium no-underline transition-colors',
isActive
? 'bg-orange-500/10 text-orange-500 border border-orange-500/20'
: 'text-gray-400 hover:bg-gray-800 hover:text-white'
@@ -66,11 +37,11 @@ export function PortalSidebar() {
Help & Support
</div>
<nav className="space-y-1">
{helpLinks.map((item) => (
{supportNavigation.map((item) => (
<Link
key={item.name}
href={item.href}
className="flex items-center space-x-3 rounded-lg px-3 py-2 text-sm font-medium text-gray-400 hover:bg-gray-800 hover:text-white transition-colors"
className="flex items-center space-x-3 rounded-lg px-3 py-2 text-sm font-medium text-gray-400 no-underline transition-colors hover:bg-gray-800 hover:text-white"
>
<item.icon className="h-5 w-5" />
<span>{item.name}</span>
@@ -81,4 +52,3 @@ export function PortalSidebar() {
</aside>
)
}

View File

@@ -15,20 +15,31 @@ interface OnboardingStep {
interface OnboardingWizardProps {
steps: OnboardingStep[];
onComplete: () => void;
onComplete: () => Promise<void>;
}
export function OnboardingWizard({ steps, onComplete }: OnboardingWizardProps) {
const [currentStep, setCurrentStep] = useState(0);
const [completedSteps, setCompletedSteps] = useState<Set<string>>(new Set());
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
const router = useRouter();
const handleNext = () => {
const handleNext = async () => {
setError(null);
if (currentStep < steps.length - 1) {
setCurrentStep(currentStep + 1);
} else {
onComplete();
router.push('/dashboard');
try {
setSubmitting(true);
await onComplete();
router.push('/dashboard');
} catch (err) {
setError(err instanceof Error ? err.message : 'Unable to complete onboarding.');
} finally {
setSubmitting(false);
}
}
};
@@ -110,6 +121,11 @@ export function OnboardingWizard({ steps, onComplete }: OnboardingWizardProps) {
<p className="text-gray-400 mb-6">
{steps[currentStep].description}
</p>
{error ? (
<div className="mb-4 rounded-lg border border-red-500/30 bg-red-500/10 px-4 py-3 text-sm text-red-300">
{error}
</div>
) : null}
<CurrentStepComponent
onComplete={() => handleStepComplete(steps[currentStep].id)}
/>
@@ -126,9 +142,10 @@ export function OnboardingWizard({ steps, onComplete }: OnboardingWizardProps) {
</button>
<button
onClick={handleNext}
disabled={submitting}
className="px-6 py-2 bg-orange-500 text-white rounded-lg hover:bg-orange-600 transition-colors flex items-center gap-2"
>
{currentStep === steps.length - 1 ? 'Complete' : 'Next'}
{submitting ? 'Saving...' : currentStep === steps.length - 1 ? 'Complete' : 'Next'}
<ArrowRight className="h-4 w-4" />
</button>
</div>
@@ -137,4 +154,3 @@ export function OnboardingWizard({ steps, onComplete }: OnboardingWizardProps) {
</div>
);
}

View File

@@ -0,0 +1,81 @@
'use client';
import Link from 'next/link';
import { Badge } from '@/components/ui/badge';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/Card';
import { cn } from '@/lib/utils';
interface PreviewAction {
href: string;
label: string;
}
interface FeaturePreviewPageProps {
eyebrow: string;
title: string;
description: string;
status?: 'preview' | 'request-only' | 'active';
bullets: readonly string[];
primaryAction?: PreviewAction;
secondaryAction?: PreviewAction;
}
export function FeaturePreviewPage({
eyebrow,
title,
description,
status = 'preview',
bullets,
primaryAction,
secondaryAction,
}: FeaturePreviewPageProps) {
const primaryActionClassName =
'inline-flex h-10 items-center justify-center rounded-md bg-gray-700 px-4 text-base font-medium text-white transition-colors hover:bg-gray-600';
const secondaryActionClassName =
'inline-flex h-10 items-center justify-center rounded-md border border-gray-600 bg-transparent px-4 text-base font-medium text-white transition-colors hover:bg-gray-800';
return (
<div className="mx-auto max-w-4xl px-4 py-10">
<div className="mb-8">
<p className="mb-2 text-sm font-medium uppercase tracking-wide text-orange-400">{eyebrow}</p>
<div className="mb-3 flex items-center gap-3">
<h1 className="text-3xl font-bold text-white">{title}</h1>
<Badge variant={status === 'active' ? 'default' : 'secondary'}>
{status === 'request-only' ? 'Request Only' : status === 'active' ? 'Active' : 'Preview'}
</Badge>
</div>
<p className="max-w-3xl text-gray-400">{description}</p>
</div>
<Card className="border-gray-800 bg-gray-900/70">
<CardHeader>
<CardTitle className="text-white">Current Scope</CardTitle>
<CardDescription>
This route is now real and intentionally describes the supported boundary for this workspace area.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<ul className="space-y-2 text-sm text-gray-300">
{bullets.map((bullet) => (
<li key={bullet}> {bullet}</li>
))}
</ul>
<div className="flex flex-col gap-3 pt-2 sm:flex-row">
{primaryAction ? (
<Link href={primaryAction.href} className={cn(primaryActionClassName)}>
{primaryAction.label}
</Link>
) : null}
{secondaryAction ? (
<Link href={secondaryAction.href} className={cn(secondaryActionClassName)}>
{secondaryAction.label}
</Link>
) : null}
</div>
</CardContent>
</Card>
</div>
);
}

View File

@@ -0,0 +1,116 @@
'use client';
/**
* Cloudflare Turnstile widget. Site key is public (NEXT_PUBLIC_*).
* Pair with Turnstile secret on any backend route you protect — not the same as Cloudflare DNS API keys.
*/
import { useEffect, useRef } from 'react';
const SCRIPT_SRC = 'https://challenges.cloudflare.com/turnstile/v0/api.js';
declare global {
interface Window {
turnstile?: {
render: (
container: HTMLElement,
options: {
sitekey: string;
callback: (token: string) => void;
'error-callback'?: () => void;
'expired-callback'?: () => void;
}
) => string;
remove?: (widgetId: string) => void;
};
}
}
function loadTurnstileScript(): Promise<void> {
if (typeof window === 'undefined') {
return Promise.reject(new Error('no window'));
}
if (window.turnstile) {
return Promise.resolve();
}
const existing = document.querySelector<HTMLScriptElement>(`script[src="${SCRIPT_SRC}"]`);
if (existing) {
return new Promise((resolve, reject) => {
const t0 = Date.now();
const tick = () => {
if (window.turnstile) {
resolve();
return;
}
if (Date.now() - t0 > 15000) {
reject(new Error('Turnstile script timeout'));
return;
}
requestAnimationFrame(tick);
};
tick();
});
}
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = SCRIPT_SRC;
script.async = true;
script.onload = () => resolve();
script.onerror = () => reject(new Error('Failed to load Turnstile'));
document.head.appendChild(script);
});
}
export interface CloudflareTurnstileProps {
siteKey: string;
onToken: (token: string | null) => void;
}
export function CloudflareTurnstile({ siteKey, onToken }: CloudflareTurnstileProps) {
const containerRef = useRef<HTMLDivElement>(null);
const widgetIdRef = useRef<string | null>(null);
const onTokenRef = useRef(onToken);
onTokenRef.current = onToken;
useEffect(() => {
if (!siteKey || !containerRef.current) {
return;
}
let cancelled = false;
void (async () => {
try {
await loadTurnstileScript();
if (cancelled || !containerRef.current || !window.turnstile) {
return;
}
containerRef.current.innerHTML = '';
const id = window.turnstile.render(containerRef.current, {
sitekey: siteKey,
callback: (token: string) => onTokenRef.current(token),
'error-callback': () => onTokenRef.current(null),
'expired-callback': () => onTokenRef.current(null),
});
widgetIdRef.current = id;
} catch {
onTokenRef.current(null);
}
})();
return () => {
cancelled = true;
const id = widgetIdRef.current;
widgetIdRef.current = null;
if (id && window.turnstile?.remove) {
try {
window.turnstile.remove(id);
} catch {
/* noop */
}
}
};
}, [siteKey]);
return <div ref={containerRef} className="min-h-[65px]" aria-live="polite" />;
}