diff --git a/apps/portal-public/next.config.js b/apps/portal-public/next.config.js index 53d8169..1991ec8 100644 --- a/apps/portal-public/next.config.js +++ b/apps/portal-public/next.config.js @@ -5,4 +5,3 @@ const nextConfig = { }; module.exports = nextConfig; - diff --git a/apps/portal-public/package.json b/apps/portal-public/package.json index 313622a..2455b38 100644 --- a/apps/portal-public/package.json +++ b/apps/portal-public/package.json @@ -7,7 +7,9 @@ "build": "next build", "start": "next start", "lint": "next lint", - "type-check": "tsc --noEmit" + "type-check": "tsc --noEmit", + "standards:generate": "tsx src/content/standards/generate-artifacts.ts", + "test": "vitest run" }, "dependencies": { "next": "^14.0.4", @@ -26,20 +28,21 @@ "react-hook-form": "^7.49.2", "@hookform/resolvers": "^3.3.4", "zod": "^3.22.4", - "date-fns": "^3.0.6", - "zustand": "^4.4.7" + "date-fns": "^3.0.6" }, "devDependencies": { "@types/node": "^20.10.6", "@types/react": "^18.2.45", "@types/react-dom": "^18.2.18", "typescript": "^5.3.3", + "tsx": "^4.20.5", "eslint": "^8.57.1", "eslint-config-next": "^14.0.4", "tailwindcss": "^3.4.1", "postcss": "^8.4.33", "autoprefixer": "^10.4.16", - "tailwindcss-animate": "^1.0.7" + "baseline-browser-mapping": "^2.10.19", + "tailwindcss-animate": "^1.0.7", + "vitest": "^1.6.1" } } - diff --git a/apps/portal-public/public/standards/registry.json b/apps/portal-public/public/standards/registry.json new file mode 100644 index 0000000..0d72125 --- /dev/null +++ b/apps/portal-public/public/standards/registry.json @@ -0,0 +1,699 @@ +{ + "standards": [ + { + "slug": "erc-3643", + "name": "ERC-3643", + "shortName": "Compliant Tokenization", + "eyebrow": "Compliant Token Standard", + "icon": "shield", + "summary": "Issue, manage, and distribute regulated digital assets with identity-based compliance enforced directly at the token level.", + "category": "Compliance and tokenization", + "status": "Live", + "audience": "Financial institutions, issuers, tokenization platforms, and regulated RWA teams.", + "standards": [ + "ERC-3643", + "ERC-20" + ], + "publisher": "Tokeny", + "auditor": "Hacken", + "seo": { + "title": "ERC-3643 | The Order", + "description": "Compliant tokenization for real-world assets with identity-based compliance, lifecycle automation, and ERC-20 compatibility.", + "keywords": [ + "ERC-3643", + "compliant tokenization", + "real-world assets", + "regulated digital assets" + ] + }, + "specification": { + "label": "EIP-3643 Specification", + "href": "https://eips.ethereum.org/EIPS/eip-3643", + "kind": "spec" + }, + "docs": [ + { + "label": "Tokeny ERC-3643 Overview", + "href": "https://www.erc3643.org/", + "kind": "docs" + } + ], + "repos": [ + { + "label": "ethereum/EIPs", + "href": "https://github.com/ethereum/EIPs", + "kind": "repo" + }, + { + "label": "TokenySolutions/T-REX", + "href": "https://github.com/TokenySolutions/T-REX", + "kind": "repo" + } + ], + "submissionPoints": [ + { + "label": "Ethereum EIPs Repository", + "href": "https://github.com/ethereum/EIPs", + "kind": "submission" + }, + { + "label": "EIP-3643 Pull Requests", + "href": "https://github.com/ethereum/EIPs/pulls", + "kind": "submission" + } + ], + "compliance": { + "native": true, + "label": "Native compliance", + "summary": "Identity, eligibility, and transfer rules are enforced directly in token behavior rather than being bolted on externally.", + "enforcement": [ + "Identity-aware transfer restrictions", + "Eligibility controls for holders and counterparties", + "Lifecycle rule updates during issuance and administration" + ] + }, + "governanceBody": "Ethereum Improvement Proposal process with ecosystem implementation led by Tokeny and related adopters.", + "maintainers": [ + { + "name": "Ethereum EIPs editors", + "role": "Canonical specification governance", + "href": "https://github.com/ethereum/EIPs" + }, + { + "name": "Tokeny", + "role": "Reference implementation and ecosystem steward", + "href": "https://github.com/TokenySolutions/T-REX" + } + ], + "statusHistory": [ + { + "date": "2021-11-09", + "title": "EIP-3643 created", + "summary": "The compliant tokenization standard was introduced through the Ethereum EIP process." + }, + { + "date": "2022-01-26", + "title": "T-REX reference implementation publicized", + "summary": "Production-oriented implementation guidance and contracts became widely available through Tokeny." + }, + { + "date": "2024-01-01", + "title": "Ecosystem adoption broadened", + "summary": "ERC-3643 continued to mature as a practical framework for regulated asset issuance and lifecycle management." + } + ], + "implementationExamples": [ + { + "label": "Tokeny T-REX reference contracts", + "summary": "Production-oriented smart contract suite implementing identity-aware compliance controls for ERC-3643 assets.", + "href": "https://github.com/TokenySolutions/T-REX" + }, + { + "label": "EIP-3643 canonical specification", + "summary": "The normative source for interface behavior, goals, and compliance-oriented design expectations.", + "href": "https://eips.ethereum.org/EIPS/eip-3643" + } + ], + "submissionChecklist": [ + { + "label": "Canonical EIP specification reference", + "required": true, + "summary": "Point to the accepted EIP text and normative standard behavior." + }, + { + "label": "Reference implementation repository", + "required": true, + "summary": "Include a public implementation repo that demonstrates how the standard is realized in production." + }, + { + "label": "Compliance posture declaration", + "required": true, + "summary": "Document whether compliance is natively enforced or relies on external controls." + }, + { + "label": "Submission points and governance path", + "required": true, + "summary": "Identify the repo, pull request surface, and governance venue for amendments or follow-on proposals." + } + ], + "ecosystemSupport": [ + { + "area": "Wallet compatibility", + "status": "Moderate", + "summary": "Leverages ERC-20 compatibility, though full compliance-aware workflows depend on supporting infrastructure." + }, + { + "area": "Exchange and marketplace support", + "status": "Specialized", + "summary": "Best fit for venues prepared to handle regulated asset onboarding and rule-aware transfer flows." + }, + { + "area": "Issuer administration tooling", + "status": "Strong", + "summary": "Well suited to issuer-facing workflows, lifecycle events, and identity-driven operating models." + }, + { + "area": "Regulated asset operations", + "status": "Strong", + "summary": "Purpose-built for permissioned or regulated issuance where transfer and holder eligibility matter." + } + ], + "comparisonTarget": "erc-20", + "featured": true, + "order": 10, + "href": "/standards/erc-3643", + "landingPage": { + "hero": { + "eyebrow": "Compliant Token Standard", + "headline": "Compliant Tokenization for Real-World Assets", + "subheadline": "ERC-3643 is the token standard for issuing, managing, and distributing regulated digital assets with identity-based compliance built into every transfer.", + "supportingCopy": "Designed for regulated markets, ERC-3643 combines token issuance, transfer control, lifecycle automation, and ecosystem compatibility in a single standard. It is built for teams bringing securities, funds, private debt, real estate, and other real-world assets onchain.", + "primaryCta": { + "label": "Explore ERC-3643", + "href": "/docs" + }, + "secondaryCta": { + "label": "Compare Standards", + "href": "#comparison" + }, + "tertiaryLink": { + "label": "View Documentation", + "href": "/docs" + } + }, + "trustStrip": { + "label": "Standards and assurance", + "items": [ + { + "label": "Standard", + "value": "ERC-3643, ERC-20 compatible" + }, + { + "label": "Publisher", + "value": "Tokeny" + }, + { + "label": "Auditor", + "value": "Hacken" + }, + { + "label": "Category", + "value": "Compliant tokenization" + }, + { + "label": "Fit", + "value": "Regulated digital assets and RWAs" + } + ] + }, + "valueProps": { + "headline": "Built for compliant digital asset issuance", + "intro": "ERC-3643 embeds identity, eligibility, and transfer controls directly into the token so compliance is enforced as part of the asset itself, not added as a separate layer.", + "cards": [ + { + "title": "Built-In Compliance", + "copy": "Ensure only eligible participants can hold and transfer assets through identity-based controls enforced onchain." + }, + { + "title": "Lifecycle Automation", + "copy": "Support issuance, restrictions, corporate actions, and ongoing administration with extensive built-in functionality." + }, + { + "title": "Regulated Asset Ready", + "copy": "Well suited for securities, funds, private debt, commodities, real estate, and other tokenized real-world assets." + }, + { + "title": "ERC-20 Compatibility", + "copy": "Integrates with wallets, marketplaces, exchanges, and dApps while maintaining compliance enforcement." + }, + { + "title": "Configurable Rules", + "copy": "Adapt compliance policies over time to meet changing operational, legal, or market requirements." + }, + { + "title": "Proven Model", + "copy": "Validated across more than 120 use cases for compliant issuance and asset management." + } + ] + }, + "useCases": { + "headline": "Designed for regulated and real-world asset use cases", + "intro": "ERC-3643 can be applied across a broad range of tokenized asset classes and identity-driven applications.", + "items": [ + "Equities", + "Loans", + "Currencies", + "Funds and securities", + "Commodities", + "Private debt", + "Bonds", + "Real estate", + "Real-world assets", + "Identity", + "Loyalty" + ], + "supportingLine": "From institutional issuance to programmable asset distribution, ERC-3643 provides a consistent compliance framework across asset classes." + }, + "proofPoints": { + "headline": "Trusted for compliant tokenization", + "intro": "ERC-3643 is designed for teams that need more than simple token issuance. It provides the operational and compliance foundation required for regulated digital asset programs.", + "items": [ + "More than 120 validated use cases across compliant digital asset issuance and management.", + "Over 100 built-in functions for lifecycle operations, automation, and corporate actions.", + "Identity-based compliance enforced directly at the token level.", + "Broad compatibility with exchanges, marketplaces, wallets, and dApps." + ] + }, + "comparison": { + "headline": "Why ERC-3643 instead of a basic token standard?", + "intro": "ERC-20 is foundational for fungible tokens, but it does not natively provide the compliance controls needed for regulated issuance. ERC-3643 extends ERC-20 compatibility with identity-aware transfer rules and operational controls built into the token.", + "columns": [ + "Category", + "ERC-3643", + "ERC-20" + ], + "rows": [ + { + "category": "Primary purpose", + "left": "Compliant tokenization for regulated assets", + "right": "General-purpose fungible tokens" + }, + { + "category": "Compliance", + "left": "Enforced onchain through identity and rules", + "right": "Typically external or manual" + }, + { + "category": "Transfer restrictions", + "left": "Native", + "right": "Not native" + }, + { + "category": "Eligibility controls", + "left": "Native", + "right": "Not native" + }, + { + "category": "Lifecycle management", + "left": "Extensive built-in functions", + "right": "Minimal core functions" + }, + { + "category": "Best fit", + "left": "Regulated assets and RWAs", + "right": "Simple token issuance" + } + ], + "secondaryNote": "Compared with ERC-1400-style positioning, ERC-3643 is often easier to present as a practical, issuer-friendly standard for compliant tokenization with strong interoperability and lifecycle flexibility.", + "cta": { + "label": "View Full Comparison", + "href": "#comparison" + } + }, + "faq": { + "headline": "Frequently asked questions", + "items": [ + { + "question": "What is ERC-3643?", + "answer": "ERC-3643 is an Ethereum token standard for compliant tokenization. It combines ERC-20 compatibility with identity-based compliance controls that govern who can hold, transfer, and interact with tokenized assets." + }, + { + "question": "How is ERC-3643 different from ERC-20?", + "answer": "ERC-20 defines the baseline behavior for fungible tokens, but it does not natively enforce compliance. ERC-3643 adds identity, eligibility, and transfer restrictions directly into token logic, making it better suited for regulated assets." + }, + { + "question": "What types of assets can use ERC-3643?", + "answer": "ERC-3643 is well suited for equities, funds, securities, private debt, bonds, commodities, real estate, currencies, and other tokenized real-world assets." + }, + { + "question": "Does ERC-3643 still work with the broader ecosystem?", + "answer": "Yes. ERC-3643 is designed to remain compatible with much of the broader Ethereum ecosystem, including wallets, dApps, marketplaces, and exchanges, while preserving compliance enforcement." + }, + { + "question": "Who is ERC-3643 for?", + "answer": "It is particularly relevant for financial institutions, asset issuers, tokenization platforms, fund operators, and enterprises that need compliant digital asset issuance and lifecycle management." + } + ] + }, + "footerCta": { + "headline": "Bring regulated assets onchain with greater control", + "copy": "Use ERC-3643 to issue compliant digital assets with identity-based transfer enforcement, lifecycle automation, and compatibility across the broader digital asset ecosystem.", + "primaryCta": { + "label": "Get Started", + "href": "/contact" + }, + "secondaryCta": { + "label": "Talk to Sales", + "href": "/contact" + } + } + } + }, + { + "slug": "erc-20", + "name": "ERC-20", + "shortName": "Fungible Token Baseline", + "eyebrow": "Foundational Token Standard", + "icon": "coins", + "summary": "The core fungible token standard for broad wallet, exchange, and application compatibility across the Ethereum ecosystem.", + "category": "General-purpose tokenization", + "status": "Live", + "audience": "Protocol teams, issuers, exchanges, treasury teams, and builders focused on interoperability.", + "standards": [ + "ERC-20" + ], + "seo": { + "title": "ERC-20 | The Order", + "description": "The foundational fungible token standard for broad wallet, exchange, and application compatibility across Ethereum.", + "keywords": [ + "ERC-20", + "fungible token", + "Ethereum token standard", + "token interoperability" + ] + }, + "specification": { + "label": "EIP-20 Specification", + "href": "https://eips.ethereum.org/EIPS/eip-20", + "kind": "spec" + }, + "docs": [ + { + "label": "OpenZeppelin ERC-20 Guide", + "href": "https://docs.openzeppelin.com/contracts/4.x/erc20", + "kind": "docs" + } + ], + "repos": [ + { + "label": "ethereum/EIPs", + "href": "https://github.com/ethereum/EIPs", + "kind": "repo" + }, + { + "label": "OpenZeppelin/openzeppelin-contracts", + "href": "https://github.com/OpenZeppelin/openzeppelin-contracts", + "kind": "repo" + } + ], + "submissionPoints": [ + { + "label": "Ethereum EIPs Repository", + "href": "https://github.com/ethereum/EIPs", + "kind": "submission" + }, + { + "label": "EIP-20 Pull Requests", + "href": "https://github.com/ethereum/EIPs/pulls", + "kind": "submission" + } + ], + "compliance": { + "native": false, + "label": "External compliance required", + "summary": "ERC-20 does not natively enforce compliance. Regulated issuance typically depends on wrappers, policy layers, or additional contract systems.", + "enforcement": [ + "External policy engines or offchain controls", + "Custom transfer restrictions when needed", + "Operational overlays for regulated counterparties" + ] + }, + "governanceBody": "Ethereum Improvement Proposal process with widespread ecosystem stewardship across libraries, wallets, and exchanges.", + "maintainers": [ + { + "name": "Ethereum EIPs editors", + "role": "Canonical specification governance", + "href": "https://github.com/ethereum/EIPs" + }, + { + "name": "OpenZeppelin", + "role": "Widely used implementation maintainer", + "href": "https://github.com/OpenZeppelin/openzeppelin-contracts" + } + ], + "statusHistory": [ + { + "date": "2015-11-19", + "title": "EIP-20 proposed", + "summary": "The fungible token interface was introduced as a standard for interoperable Ethereum assets." + }, + { + "date": "2017-09-20", + "title": "EIP-20 finalized", + "summary": "ERC-20 became the canonical baseline for fungible token behavior across the ecosystem." + }, + { + "date": "2024-01-01", + "title": "Baseline ecosystem standard maintained", + "summary": "ERC-20 remains the reference layer for fungible token interoperability and exchange integration." + } + ], + "implementationExamples": [ + { + "label": "OpenZeppelin ERC-20 implementation", + "summary": "Widely used and audited Solidity implementation for fungible token behavior.", + "href": "https://github.com/OpenZeppelin/openzeppelin-contracts" + }, + { + "label": "EIP-20 canonical specification", + "summary": "Normative interface definition for fungible token balances, transfers, and approvals.", + "href": "https://eips.ethereum.org/EIPS/eip-20" + } + ], + "submissionChecklist": [ + { + "label": "Canonical EIP specification reference", + "required": true, + "summary": "Link the accepted EIP text as the normative basis for the registry entry." + }, + { + "label": "Reference implementation repository", + "required": true, + "summary": "Provide at least one widely adopted implementation repo to anchor ecosystem usage." + }, + { + "label": "Compliance dependency disclosure", + "required": true, + "summary": "State clearly that compliance is external if the token standard does not enforce it natively." + }, + { + "label": "Submission and amendment path", + "required": true, + "summary": "Document the repository and PR path for future revisions to the standard." + } + ], + "ecosystemSupport": [ + { + "area": "Wallet compatibility", + "status": "Strong", + "summary": "Near-universal support across Ethereum wallets and custody providers." + }, + { + "area": "Exchange and marketplace support", + "status": "Strong", + "summary": "Deep support across centralized and decentralized trading venues." + }, + { + "area": "Issuer administration tooling", + "status": "Moderate", + "summary": "Strong for basic issuance, though advanced compliance workflows usually require add-on systems." + }, + { + "area": "Regulated asset operations", + "status": "Specialized", + "summary": "Can be used as a base layer, but regulated issuance typically needs additional policy and control infrastructure." + } + ], + "comparisonTarget": "erc-3643", + "featured": false, + "order": 20, + "href": "/standards/erc-20", + "landingPage": { + "hero": { + "eyebrow": "Foundational Token Standard", + "headline": "The Baseline Standard for Fungible Tokens", + "subheadline": "ERC-20 defines the core interface for fungible tokens on Ethereum and remains the foundation for interoperable token issuance across wallets, exchanges, and applications.", + "supportingCopy": "For teams that need broad ecosystem support and straightforward fungible token behavior, ERC-20 provides the common baseline. It is best suited to general-purpose token issuance and often serves as the interoperability layer beneath more specialized token frameworks.", + "primaryCta": { + "label": "Explore ERC-20", + "href": "/docs" + }, + "secondaryCta": { + "label": "Compare Standards", + "href": "#comparison" + }, + "tertiaryLink": { + "label": "View Documentation", + "href": "/docs" + } + }, + "trustStrip": { + "label": "Standards overview", + "items": [ + { + "label": "Standard", + "value": "ERC-20" + }, + { + "label": "Role", + "value": "Baseline fungible token interface" + }, + { + "label": "Category", + "value": "General-purpose tokenization" + }, + { + "label": "Fit", + "value": "Interoperable token issuance and ecosystem compatibility" + }, + { + "label": "Primary strength", + "value": "Simplicity and universal support" + } + ] + }, + "valueProps": { + "headline": "Optimized for interoperability and simplicity", + "intro": "ERC-20 established the common language for fungible tokens across the Ethereum ecosystem. Its strength is broad adoption, predictable behavior, and straightforward integration.", + "cards": [ + { + "title": "Universal Compatibility", + "copy": "Supported across wallets, exchanges, custodians, and dApps as the baseline fungible token format." + }, + { + "title": "Simple Token Logic", + "copy": "Provides a minimal and predictable interface for balances, transfers, approvals, and allowances." + }, + { + "title": "Builder Friendly", + "copy": "Well understood by developers, infrastructure providers, and integrators across the Ethereum ecosystem." + }, + { + "title": "Strong Liquidity Fit", + "copy": "Works naturally with trading venues, pools, bridges, and DeFi protocols designed around fungible tokens." + }, + { + "title": "Composable Foundation", + "copy": "Frequently used as the compatibility layer for more specialized token models and compliance overlays." + }, + { + "title": "Fast to Adopt", + "copy": "A practical choice for teams that need broad support without embedding complex policy logic directly into the token." + } + ] + }, + "useCases": { + "headline": "Best suited to general-purpose token issuance", + "intro": "ERC-20 is often the right choice when the goal is broad market compatibility rather than native compliance enforcement.", + "items": [ + "Utility tokens", + "Stablecoins", + "Governance tokens", + "Rewards tokens", + "Exchange-listed assets", + "Protocol assets", + "Treasury instruments", + "Liquidity and settlement rails", + "Wrapped assets", + "Ecosystem incentives" + ], + "supportingLine": "ERC-20 can still be used in regulated contexts, but compliance controls generally need to be added through external systems, wrappers, or custom extensions." + }, + "proofPoints": { + "headline": "Why teams still start with ERC-20", + "intro": "ERC-20 remains the default standard for fungible token interoperability because it balances simplicity, widespread support, and implementation maturity.", + "items": [ + "Widely supported across the Ethereum ecosystem and token infrastructure stack.", + "Simple enough to implement, audit, and integrate without specialized compliance architecture.", + "Well suited to liquid, broadly distributed token models and exchange-facing assets.", + "Acts as the reference point for comparing more specialized token standards such as ERC-3643." + ] + }, + "comparison": { + "headline": "When is ERC-20 enough, and when is it not?", + "intro": "ERC-20 is ideal when broad compatibility and simple fungible token behavior matter most. It becomes less suitable when regulated issuance requires native controls over identity, eligibility, and transfer permissions.", + "columns": [ + "Category", + "ERC-20", + "ERC-3643" + ], + "rows": [ + { + "category": "Primary purpose", + "left": "General-purpose fungible tokens", + "right": "Compliant tokenization for regulated assets" + }, + { + "category": "Compliance model", + "left": "Usually external, manual, or custom", + "right": "Identity-based and enforced onchain" + }, + { + "category": "Transfer restrictions", + "left": "Not native", + "right": "Native" + }, + { + "category": "Eligibility controls", + "left": "Not native", + "right": "Native" + }, + { + "category": "Best fit", + "left": "Broad ecosystem issuance and liquidity", + "right": "Regulated assets and RWAs" + }, + { + "category": "Operational complexity", + "left": "Lower baseline complexity", + "right": "Higher capability with more built-in controls" + } + ], + "secondaryNote": "ERC-20 is the baseline standard. For regulated programs, it is often the starting point for comparison rather than the final architecture.", + "cta": { + "label": "View Standards Library", + "href": "/standards" + } + }, + "faq": { + "headline": "Frequently asked questions", + "items": [ + { + "question": "What is ERC-20?", + "answer": "ERC-20 is the standard interface for fungible tokens on Ethereum. It defines how balances, transfers, approvals, and allowances should behave so tokens can interoperate across the ecosystem." + }, + { + "question": "Why is ERC-20 so widely used?", + "answer": "Its broad adoption comes from its simplicity and ecosystem support. Most wallets, exchanges, and DeFi applications are built to work with ERC-20 tokens by default." + }, + { + "question": "Does ERC-20 include compliance controls?", + "answer": "Not natively. Teams that need regulated issuance or controlled transfers usually add compliance through external systems, custom extensions, or more specialized standards." + }, + { + "question": "When should a team choose ERC-20?", + "answer": "ERC-20 is a strong choice when broad compatibility, liquidity integration, and straightforward token behavior matter more than embedding policy logic directly into the asset." + }, + { + "question": "How does ERC-20 relate to ERC-3643?", + "answer": "ERC-3643 builds on ERC-20 compatibility while adding identity-based compliance and transfer controls. ERC-20 is the simpler baseline; ERC-3643 is the more compliance-oriented framework." + } + ] + }, + "footerCta": { + "headline": "Start with the right token standard for the job", + "copy": "Use ERC-20 when you need broad compatibility and simple fungible token behavior, or compare it against specialized standards when compliance and lifecycle controls are part of the requirement.", + "primaryCta": { + "label": "Explore Standards", + "href": "/standards" + }, + "secondaryCta": { + "label": "Talk to Sales", + "href": "/contact" + } + } + } + } + ], + "count": 2 +} diff --git a/apps/portal-public/public/standards/schema.json b/apps/portal-public/public/standards/schema.json new file mode 100644 index 0000000..26cc739 --- /dev/null +++ b/apps/portal-public/public/standards/schema.json @@ -0,0 +1,448 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://the-order.local/standards/schema.json", + "title": "The Order Standards Registry", + "description": "Canonical schema for standards registry entries used by the public portal.", + "type": "array", + "items": { + "$ref": "#/$defs/standardRegistryEntry" + }, + "$defs": { + "standardIcon": { + "type": "string", + "enum": [ + "shield", + "coins", + "scale", + "network", + "file-stack" + ] + }, + "standardLink": { + "type": "object", + "additionalProperties": false, + "required": [ + "label", + "href", + "kind" + ], + "properties": { + "label": { + "type": "string", + "minLength": 1 + }, + "href": { + "type": "string", + "format": "uri" + }, + "kind": { + "type": "string", + "enum": [ + "spec", + "repo", + "docs", + "submission" + ] + } + } + }, + "seo": { + "type": "object", + "additionalProperties": false, + "required": [ + "title", + "description" + ], + "properties": { + "title": { + "type": "string", + "minLength": 1 + }, + "description": { + "type": "string", + "minLength": 1 + }, + "keywords": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + } + } + }, + "complianceProfile": { + "type": "object", + "additionalProperties": false, + "required": [ + "native", + "label", + "summary", + "enforcement" + ], + "properties": { + "native": { + "type": "boolean" + }, + "label": { + "type": "string", + "minLength": 1 + }, + "summary": { + "type": "string", + "minLength": 1 + }, + "enforcement": { + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "minLength": 1 + } + } + } + }, + "maintainer": { + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "role" + ], + "properties": { + "name": { + "type": "string", + "minLength": 1 + }, + "role": { + "type": "string", + "minLength": 1 + }, + "href": { + "type": "string", + "format": "uri" + } + } + }, + "statusEvent": { + "type": "object", + "additionalProperties": false, + "required": [ + "date", + "title", + "summary" + ], + "properties": { + "date": { + "type": "string", + "minLength": 1 + }, + "title": { + "type": "string", + "minLength": 1 + }, + "summary": { + "type": "string", + "minLength": 1 + } + } + }, + "implementationExample": { + "type": "object", + "additionalProperties": false, + "required": [ + "label", + "summary", + "href" + ], + "properties": { + "label": { + "type": "string", + "minLength": 1 + }, + "summary": { + "type": "string", + "minLength": 1 + }, + "href": { + "type": "string", + "format": "uri" + } + } + }, + "checklistItem": { + "type": "object", + "additionalProperties": false, + "required": [ + "label", + "required", + "summary" + ], + "properties": { + "label": { + "type": "string", + "minLength": 1 + }, + "required": { + "type": "boolean" + }, + "summary": { + "type": "string", + "minLength": 1 + } + } + }, + "ecosystemSupport": { + "type": "object", + "additionalProperties": false, + "required": [ + "area", + "status", + "summary" + ], + "properties": { + "area": { + "type": "string", + "minLength": 1 + }, + "status": { + "type": "string", + "enum": [ + "Strong", + "Moderate", + "Specialized" + ] + }, + "summary": { + "type": "string", + "minLength": 1 + } + } + }, + "marketingLink": { + "type": "object", + "additionalProperties": false, + "required": [ + "label", + "href" + ], + "properties": { + "label": { + "type": "string", + "minLength": 1 + }, + "href": { + "type": "string", + "minLength": 1 + } + } + }, + "standardRegistryEntry": { + "type": "object", + "additionalProperties": false, + "required": [ + "slug", + "name", + "icon", + "summary", + "category", + "status", + "audience", + "standards", + "seo", + "specification", + "repos", + "submissionPoints", + "compliance", + "governanceBody", + "maintainers", + "statusHistory", + "implementationExamples", + "submissionChecklist", + "ecosystemSupport", + "order", + "href", + "landingPage" + ], + "properties": { + "slug": { + "type": "string", + "minLength": 1 + }, + "name": { + "type": "string", + "minLength": 1 + }, + "shortName": { + "type": "string", + "minLength": 1 + }, + "eyebrow": { + "type": "string", + "minLength": 1 + }, + "icon": { + "$ref": "#/$defs/standardIcon" + }, + "summary": { + "type": "string", + "minLength": 1 + }, + "category": { + "type": "string", + "minLength": 1 + }, + "status": { + "type": "string", + "enum": [ + "Live", + "Preview" + ] + }, + "audience": { + "type": "string", + "minLength": 1 + }, + "standards": { + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "minLength": 1 + } + }, + "publisher": { + "type": "string", + "minLength": 1 + }, + "auditor": { + "type": "string", + "minLength": 1 + }, + "seo": { + "$ref": "#/$defs/seo" + }, + "specification": { + "$ref": "#/$defs/standardLink" + }, + "docs": { + "type": "array", + "items": { + "$ref": "#/$defs/standardLink" + } + }, + "repos": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/standardLink" + } + }, + "submissionPoints": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/standardLink" + } + }, + "compliance": { + "$ref": "#/$defs/complianceProfile" + }, + "governanceBody": { + "type": "string", + "minLength": 1 + }, + "maintainers": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/maintainer" + } + }, + "statusHistory": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/statusEvent" + } + }, + "implementationExamples": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/implementationExample" + } + }, + "submissionChecklist": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/checklistItem" + } + }, + "ecosystemSupport": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/ecosystemSupport" + } + }, + "comparisonTarget": { + "type": "string", + "minLength": 1 + }, + "featured": { + "type": "boolean" + }, + "order": { + "type": "integer", + "minimum": 0 + }, + "href": { + "type": "string", + "minLength": 1 + }, + "landingPage": { + "type": "object", + "additionalProperties": false, + "required": [ + "hero" + ], + "properties": { + "hero": { + "type": "object", + "additionalProperties": false, + "required": [ + "headline", + "subheadline" + ], + "properties": { + "eyebrow": { + "type": "string" + }, + "headline": { + "type": "string" + }, + "subheadline": { + "type": "string" + }, + "supportingCopy": { + "type": "string" + }, + "primaryCta": { + "$ref": "#/$defs/marketingLink" + }, + "secondaryCta": { + "$ref": "#/$defs/marketingLink" + }, + "tertiaryLink": { + "$ref": "#/$defs/marketingLink" + } + } + } + } + } + } + } + } +} diff --git a/apps/portal-public/src/app/about/page.tsx b/apps/portal-public/src/app/about/page.tsx index 36cbc83..83adac6 100644 --- a/apps/portal-public/src/app/about/page.tsx +++ b/apps/portal-public/src/app/about/page.tsx @@ -1,4 +1,4 @@ -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@the-order/ui'; +import { Card, CardContent, CardHeader, CardTitle } from '../../lib/ui-server'; export default function AboutPage(): JSX.Element { return ( @@ -39,7 +39,7 @@ export default function AboutPage(): JSX.Element { eResidency: Digital residency credentials for individuals seeking - to participate in The Order's ecosystem + to participate in The Order's ecosystem
  • @@ -113,4 +113,3 @@ export default function AboutPage(): JSX.Element { ); } - diff --git a/apps/portal-public/src/app/api/standards/[slug]/route.ts b/apps/portal-public/src/app/api/standards/[slug]/route.ts new file mode 100644 index 0000000..7a06376 --- /dev/null +++ b/apps/portal-public/src/app/api/standards/[slug]/route.ts @@ -0,0 +1,15 @@ +import { NextResponse } from 'next/server'; +import { getStandardBySlug } from '../../../../content/standards'; + +export async function GET( + _request: Request, + { params }: { params: { slug: string } } +): Promise { + const standard = getStandardBySlug(params.slug); + + if (!standard) { + return NextResponse.json({ error: 'Standard not found' }, { status: 404 }); + } + + return NextResponse.json(standard); +} diff --git a/apps/portal-public/src/app/api/standards/route.ts b/apps/portal-public/src/app/api/standards/route.ts new file mode 100644 index 0000000..12abcab --- /dev/null +++ b/apps/portal-public/src/app/api/standards/route.ts @@ -0,0 +1,35 @@ +import { NextResponse } from 'next/server'; +import { getSortedStandards } from '../../../content/standards'; +import { filterStandards } from '../../../content/standards/filter'; + +export async function GET(request: Request): Promise { + const { searchParams } = new URL(request.url); + const query = searchParams.get('q')?.trim().toLowerCase() ?? ''; + const category = searchParams.get('category')?.trim().toLowerCase() ?? ''; + const status = searchParams.get('status')?.trim().toLowerCase() ?? ''; + const featured = searchParams.get('featured'); + const compliance = searchParams.get('compliance')?.trim().toLowerCase() ?? ''; + const limit = Number(searchParams.get('limit') ?? ''); + + const standards = filterStandards(getSortedStandards(), { + q: query, + category, + status, + featured, + compliance, + limit: Number.isFinite(limit) && limit > 0 ? limit : null, + }); + + return NextResponse.json({ + standards, + count: standards.length, + filters: { + q: query || null, + category: category || null, + status: status || null, + featured: featured ?? null, + compliance: compliance || null, + limit: Number.isFinite(limit) && limit > 0 ? limit : null, + }, + }); +} diff --git a/apps/portal-public/src/app/api/standards/schema/route.ts b/apps/portal-public/src/app/api/standards/schema/route.ts new file mode 100644 index 0000000..e068bc7 --- /dev/null +++ b/apps/portal-public/src/app/api/standards/schema/route.ts @@ -0,0 +1,6 @@ +import { NextResponse } from 'next/server'; +import { standardsRegistryJsonSchema } from '../../../../content/standards/schema-json'; + +export async function GET(): Promise { + return NextResponse.json(standardsRegistryJsonSchema); +} diff --git a/apps/portal-public/src/app/contact/page.tsx b/apps/portal-public/src/app/contact/page.tsx index 8cbb6a5..5396743 100644 --- a/apps/portal-public/src/app/contact/page.tsx +++ b/apps/portal-public/src/app/contact/page.tsx @@ -22,7 +22,7 @@ export default function ContactPage(): JSX.Element { await new Promise((resolve) => setTimeout(resolve, 1000)); success('Message sent successfully!', 'We will get back to you soon.'); setFormData({ name: '', email: '', subject: '', message: '' }); - } catch (err) { + } catch { showError('Failed to send message', 'Please try again later.'); } finally { setIsSubmitting(false); @@ -35,14 +35,14 @@ export default function ContactPage(): JSX.Element {

    Contact Us

    - Have questions? We'd love to hear from you. + Have questions? We'd love to hear from you.

    Send us a message - Fill out the form below and we'll get back to you as soon as possible. + Fill out the form below and we'll get back to you as soon as possible.
    @@ -118,4 +118,3 @@ export default function ContactPage(): JSX.Element { ); } - diff --git a/apps/portal-public/src/app/docs/page.tsx b/apps/portal-public/src/app/docs/page.tsx index fcbcde2..66fd121 100644 --- a/apps/portal-public/src/app/docs/page.tsx +++ b/apps/portal-public/src/app/docs/page.tsx @@ -1,4 +1,4 @@ -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@the-order/ui'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../../lib/ui-server'; import Link from 'next/link'; export default function DocsPage(): JSX.Element { @@ -8,7 +8,7 @@ export default function DocsPage(): JSX.Element {

    Documentation

    - Learn how to use The Order's services and APIs + Learn how to use The Order's services and APIs

    @@ -26,7 +26,7 @@ export default function DocsPage(): JSX.Element { application form - . You'll need to provide personal information and identity documents. + . You'll need to provide personal information and identity documents.

    @@ -43,7 +43,7 @@ export default function DocsPage(): JSX.Element {

    3. Receive Credentials

    - Once approved, you'll receive verifiable credentials that you can use to access + Once approved, you'll receive verifiable credentials that you can use to access services and prove your identity.

    @@ -108,7 +108,7 @@ export default function DocsPage(): JSX.Element { Support - Need help? We're here for you + Need help? We're here for you

    @@ -125,4 +125,3 @@ export default function DocsPage(): JSX.Element {

    ); } - diff --git a/apps/portal-public/src/app/error.tsx b/apps/portal-public/src/app/error.tsx index 186ccde..8a955b5 100644 --- a/apps/portal-public/src/app/error.tsx +++ b/apps/portal-public/src/app/error.tsx @@ -1,6 +1,5 @@ 'use client'; -import { useEffect } from 'react'; import { Button, Card, CardContent, CardDescription, CardHeader, CardTitle } from '@the-order/ui'; export default function Error({ @@ -10,10 +9,6 @@ export default function Error({ error: Error & { digest?: string }; reset: () => void; }): JSX.Element { - useEffect(() => { - console.error(error); - }, [error]); - return (
    @@ -40,4 +35,3 @@ export default function Error({
    ); } - diff --git a/apps/portal-public/src/app/login/page.tsx b/apps/portal-public/src/app/login/page.tsx index 024f19f..2a9ad53 100644 --- a/apps/portal-public/src/app/login/page.tsx +++ b/apps/portal-public/src/app/login/page.tsx @@ -10,6 +10,7 @@ export default function LoginPage(): JSX.Element { const router = useRouter(); const { login } = useAuth(); const { success, error: showError } = useToast(); + const isDev = process.env.NODE_ENV !== 'production'; const [formData, setFormData] = useState({ email: '', password: '', @@ -17,6 +18,18 @@ export default function LoginPage(): JSX.Element { const [isLoading, setIsLoading] = useState(false); const [loginError, setLoginError] = useState(null); + const completeLogin = (user: { + id: string; + email: string; + name: string; + accessToken: string; + roles: string[]; + }): void => { + login(user); + success('Login successful', 'Welcome back!'); + router.push('/'); + }; + const handleSubmit = async (e: React.FormEvent): Promise => { e.preventDefault(); setIsLoading(true); @@ -33,14 +46,12 @@ export default function LoginPage(): JSX.Element { const mockUser = { id: 'user-123', email: formData.email, - name: formData.email.split('@')[0], + name: formData.email.split('@')[0] || 'User', accessToken: 'mock-access-token-' + Date.now(), roles: ['user'], }; - login(mockUser); - success('Login successful', 'Welcome back!'); - router.push('/'); + completeLogin(mockUser); } else { throw new Error('Please enter email and password'); } @@ -53,6 +64,18 @@ export default function LoginPage(): JSX.Element { } }; + const handleDevSeedLogin = (role: 'admin' | 'editor' | 'user'): void => { + const mockUser = { + id: `dev-${role}`, + email: `${role}@local.dev`, + name: `Dev ${role[0]!.toUpperCase()}${role.slice(1)}`, + accessToken: `dev-token-${role}-${Date.now()}`, + roles: role === 'user' ? ['user'] : ['user', role], + }; + + completeLogin(mockUser); + }; + return (
    @@ -105,7 +128,7 @@ export default function LoginPage(): JSX.Element {
    - Don't have an account?{' '} + Don't have an account?{' '} Sign up @@ -123,9 +146,28 @@ export default function LoginPage(): JSX.Element {
    + + {isDev ? ( +
    +

    Development quick access

    +
    + + + +
    +

    + Use `Seed Admin` or `Seed Editor` to access `/standards/admin` locally. +

    +
    + ) : null} ); } - diff --git a/apps/portal-public/src/app/not-found.tsx b/apps/portal-public/src/app/not-found.tsx index 5f8f227..ad86f97 100644 --- a/apps/portal-public/src/app/not-found.tsx +++ b/apps/portal-public/src/app/not-found.tsx @@ -1,5 +1,5 @@ import Link from 'next/link'; -import { Button, Card, CardContent, CardDescription, CardHeader, CardTitle } from '@the-order/ui'; +import { Button, Card, CardContent, CardDescription, CardHeader, CardTitle } from '../lib/ui-server'; export default function NotFound(): JSX.Element { return ( @@ -11,19 +11,18 @@ export default function NotFound(): JSX.Element {

    - The page you're looking for doesn't exist or has been moved. + The page you're looking for doesn't exist or has been moved.

    - + + +
    ); } - diff --git a/apps/portal-public/src/app/page.tsx b/apps/portal-public/src/app/page.tsx index b1bcdd3..7175362 100644 --- a/apps/portal-public/src/app/page.tsx +++ b/apps/portal-public/src/app/page.tsx @@ -1,21 +1,49 @@ import Link from 'next/link'; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@the-order/ui'; +import { Badge, Card, CardContent, CardDescription, CardHeader, CardTitle } from '../lib/ui-server'; +import { getComparisonTarget, getFeaturedStandards, getSortedStandards } from '../content/standards'; +import { StandardIcon } from '../components/marketing/standard-icon'; export default function Home(): JSX.Element { + const featuredStandard = getFeaturedStandards()[0] ?? getSortedStandards()[0]; + const standardsPreview = getSortedStandards().slice(0, 2); + return ( -
    +
    -
    -

    The Order

    -

    - Order of Military Hospitallers - Digital Identity & Governance Platform -

    -

    - Apply for eResidency, verify credentials, and access our services -

    +
    +
    + + Public Portal + +

    The Order

    +

    + Order of Military Hospitallers digital identity, standards, and governance platform. +

    +

    + Apply for eResidency, verify credentials, review implementation standards, and access + public documentation for regulated digital asset programs. +

    +
    + + Begin Application + + + Explore Standards + +
    +
    -
    +
    Apply for eResidency @@ -88,6 +116,151 @@ export default function Home(): JSX.Element {
    + + {featuredStandard ? ( +
    +
    +
    +

    + Featured standard +

    +

    + Standards built for regulated digital asset programs +

    +
    + + Browse all standards + +
    + + + +
    + + {featuredStandard.status} + + + {featuredStandard.compliance.native ? 'Native compliance' : 'External compliance'} + + + + + + {featuredStandard.category} + +
    +
    + {featuredStandard.name} + + {featuredStandard.summary} + +
    +
    + +
    +
    +
    Audience
    +

    {featuredStandard.audience}

    +
    +
    +
    Standards
    +

    + {featuredStandard.standards.join(', ')} +

    +
    +
    +
    +
    + {featuredStandard.publisher ?

    Publisher: {featuredStandard.publisher}

    : null} + {featuredStandard.auditor ?

    Auditor: {featuredStandard.auditor}

    : null} + {getComparisonTarget(featuredStandard) ? ( +

    Compare with: {getComparisonTarget(featuredStandard)?.name}

    + ) : null} +

    Submission points: {featuredStandard.submissionPoints.length}

    +
    + + View {featuredStandard.name} + +
    +
    +
    +
    + ) : null} + + {standardsPreview.length > 0 ? ( +
    +
    + {standardsPreview.map((standard) => ( + + +
    + + {standard.status} + + {standard.featured ? ( + Featured + ) : null} + + {standard.compliance.native ? 'Native compliance' : 'External compliance'} + +
    +
    + + + + + {standard.name} + + + {standard.summary} + +
    +
    + +
    +

    {standard.audience}

    +

    + Submission points: {standard.submissionPoints.length} +

    + {getComparisonTarget(standard) ? ( + + Compare with {getComparisonTarget(standard)?.name} + + ) : null} +
    + + View standard + +
    +
    + ))} +
    +
    + ) : null}
    ); diff --git a/apps/portal-public/src/app/privacy/page.tsx b/apps/portal-public/src/app/privacy/page.tsx index e4e5b84..88ba6fe 100644 --- a/apps/portal-public/src/app/privacy/page.tsx +++ b/apps/portal-public/src/app/privacy/page.tsx @@ -1,4 +1,4 @@ -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@the-order/ui'; +import { Card, CardContent, CardHeader, CardTitle } from '../../lib/ui-server'; export default function PrivacyPage(): JSX.Element { return ( @@ -91,4 +91,3 @@ export default function PrivacyPage(): JSX.Element {
    ); } - diff --git a/apps/portal-public/src/app/standards/[slug]/page.tsx b/apps/portal-public/src/app/standards/[slug]/page.tsx new file mode 100644 index 0000000..79549b1 --- /dev/null +++ b/apps/portal-public/src/app/standards/[slug]/page.tsx @@ -0,0 +1,57 @@ +import type { Metadata } from 'next'; +import { notFound } from 'next/navigation'; +import { MarketingLandingPage } from '../../../components/marketing/landing-page'; +import { getRelatedStandards, getSortedStandards, getStandardBySlug } from '../../../content/standards'; +import { StandardRegistryDetails } from '../../../components/marketing/standard-registry-details'; +import { RelatedStandards } from '../../../components/marketing/related-standards'; + +export function generateStaticParams(): { slug: string }[] { + return getSortedStandards().map((standard) => ({ slug: standard.slug })); +} + +export function generateMetadata({ + params, +}: { + params: { slug: string }; +}): Metadata { + const standard = getStandardBySlug(params.slug); + + if (!standard) { + return { + title: 'Standard Not Found | The Order', + }; + } + + return { + title: standard.seo.title, + description: standard.seo.description, + keywords: standard.seo.keywords, + openGraph: { + title: standard.seo.title, + description: standard.seo.description, + url: standard.href, + }, + }; +} + +export default function StandardDetailPage({ + params, +}: { + params: { slug: string }; +}): JSX.Element { + const standard = getStandardBySlug(params.slug); + + if (!standard) { + notFound(); + } + + const relatedStandards = getRelatedStandards(standard); + + return ( + <> + + + + + ); +} diff --git a/apps/portal-public/src/app/standards/admin/page.tsx b/apps/portal-public/src/app/standards/admin/page.tsx new file mode 100644 index 0000000..f87bcb5 --- /dev/null +++ b/apps/portal-public/src/app/standards/admin/page.tsx @@ -0,0 +1,11 @@ +import type { Metadata } from 'next'; +import { StandardsAdmin } from '../../../components/marketing/standards-admin'; + +export const metadata: Metadata = { + title: 'Standards Admin | The Order', + description: 'Authoring and validation interface for standards registry entries.', +}; + +export default function StandardsAdminPage(): JSX.Element { + return ; +} diff --git a/apps/portal-public/src/app/standards/page.tsx b/apps/portal-public/src/app/standards/page.tsx new file mode 100644 index 0000000..af8b410 --- /dev/null +++ b/apps/portal-public/src/app/standards/page.tsx @@ -0,0 +1,13 @@ +import type { Metadata } from 'next'; +import { StandardsBrowser } from '../../components/marketing/standards-browser'; +import { getSortedStandards } from '../../content/standards'; + +export const metadata: Metadata = { + title: 'Standards | The Order', + description: + 'Explore standards and frameworks for compliant tokenization, regulated digital asset issuance, and real-world asset programs.', +}; + +export default function StandardsPage(): JSX.Element { + return ; +} diff --git a/apps/portal-public/src/app/status/page.tsx b/apps/portal-public/src/app/status/page.tsx index 34c13fb..79c8835 100644 --- a/apps/portal-public/src/app/status/page.tsx +++ b/apps/portal-public/src/app/status/page.tsx @@ -1,11 +1,32 @@ 'use client'; +import { Suspense } from 'react'; import { useSearchParams } from 'next/navigation'; import { useQuery } from '@tanstack/react-query'; import { Card, CardContent, CardDescription, CardHeader, CardTitle, Label } from '@the-order/ui'; import { getApiClient } from '@the-order/api-client'; -export default function StatusPage(): JSX.Element { +function StatusCardShell({ children }: { children: React.ReactNode }): JSX.Element { + return ( +
    +
    {children}
    +
    + ); +} + +function StatusLoadingState(): JSX.Element { + return ( + + + +

    Loading application status...

    +
    +
    +
    + ); +} + +function StatusPageContent(): JSX.Element { const searchParams = useSearchParams(); const applicationId = searchParams.get('id'); const apiClient = getApiClient(); @@ -21,63 +42,47 @@ export default function StatusPage(): JSX.Element { if (!applicationId) { return ( -
    -
    - - - Application Status - Please provide an application ID to check status - - -
    -
    + + + + Application Status + Please provide an application ID to check status + + + ); } if (isLoading) { - return ( -
    -
    - - -

    Loading application status...

    -
    -
    -
    -
    - ); + return ; } if (error) { return ( -
    -
    - - - Error - Failed to load application status - - -

    {error instanceof Error ? error.message : 'Unknown error'}

    -
    -
    -
    -
    + + + + Error + Failed to load application status + + +

    {error instanceof Error ? error.message : 'Unknown error'}

    +
    +
    +
    ); } if (!application) { return ( -
    -
    - - - Application Not Found - No application found with the provided ID - - -
    -
    + + + + Application Not Found + No application found with the provided ID + + + ); } @@ -97,8 +102,7 @@ export default function StatusPage(): JSX.Element { }; return ( -
    -
    + Application Status @@ -163,9 +167,15 @@ export default function StatusPage(): JSX.Element { )} -
    -
    + ); } +export default function StatusPage(): JSX.Element { + return ( + }> + + + ); +} diff --git a/apps/portal-public/src/app/terms/page.tsx b/apps/portal-public/src/app/terms/page.tsx index 17823c9..e1f80f8 100644 --- a/apps/portal-public/src/app/terms/page.tsx +++ b/apps/portal-public/src/app/terms/page.tsx @@ -1,4 +1,4 @@ -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@the-order/ui'; +import { Card, CardContent, CardHeader, CardTitle } from '../../lib/ui-server'; export default function TermsPage(): JSX.Element { return ( @@ -16,7 +16,7 @@ export default function TermsPage(): JSX.Element {

    - By accessing and using The Order's services, you accept and agree to be bound by + By accessing and using The Order's services, you accept and agree to be bound by these Terms of Service. If you do not agree, you may not use our services.

    @@ -99,4 +99,3 @@ export default function TermsPage(): JSX.Element {
    ); } - diff --git a/apps/portal-public/src/components/AuthGuard.tsx b/apps/portal-public/src/components/AuthGuard.tsx index a674620..0da8413 100644 --- a/apps/portal-public/src/components/AuthGuard.tsx +++ b/apps/portal-public/src/components/AuthGuard.tsx @@ -4,9 +4,19 @@ import { useEffect } from 'react'; import { useRouter } from 'next/navigation'; import { useAuth } from '../lib/auth'; -export function AuthGuard({ children }: { children: React.ReactNode }): JSX.Element | null { - const { isAuthenticated, isLoading } = useAuth(); +export function AuthGuard({ + children, + requiredRoles, +}: { + children: React.ReactNode; + requiredRoles?: string[]; +}): JSX.Element | null { + const { isAuthenticated, isLoading, user } = useAuth(); const router = useRouter(); + const hasRequiredRole = + !requiredRoles || requiredRoles.length === 0 + ? true + : requiredRoles.some((role) => user?.roles?.includes(role)); useEffect(() => { if (!isLoading && !isAuthenticated) { @@ -14,6 +24,12 @@ export function AuthGuard({ children }: { children: React.ReactNode }): JSX.Elem } }, [isAuthenticated, isLoading, router]); + useEffect(() => { + if (!isLoading && isAuthenticated && !hasRequiredRole) { + router.push('/'); + } + }, [hasRequiredRole, isAuthenticated, isLoading, router]); + if (isLoading) { return (
    @@ -26,6 +42,9 @@ export function AuthGuard({ children }: { children: React.ReactNode }): JSX.Elem return null; } + if (!hasRequiredRole) { + return null; + } + return <>{children}; } - diff --git a/apps/portal-public/src/components/Footer.tsx b/apps/portal-public/src/components/Footer.tsx index f81fc51..dac0f48 100644 --- a/apps/portal-public/src/components/Footer.tsx +++ b/apps/portal-public/src/components/Footer.tsx @@ -39,6 +39,11 @@ export function Footer(): JSX.Element { About
  • +
  • + + Standards + +
  • Documentation @@ -74,4 +79,3 @@ export function Footer(): JSX.Element { ); } - diff --git a/apps/portal-public/src/components/Header.tsx b/apps/portal-public/src/components/Header.tsx index 74fbe7b..33ee7d0 100644 --- a/apps/portal-public/src/components/Header.tsx +++ b/apps/portal-public/src/components/Header.tsx @@ -8,6 +8,7 @@ import { useAuth } from '../lib/auth'; export function Header(): JSX.Element { const router = useRouter(); const { isAuthenticated, user, logout } = useAuth(); + const canAccessStandardsAdmin = !!user?.roles?.some((role) => role === 'admin' || role === 'editor'); const handleLogout = (): void => { logout(); @@ -25,6 +26,9 @@ export function Header(): JSX.Element { Apply + + Standards + Status @@ -36,6 +40,11 @@ export function Header(): JSX.Element { {isAuthenticated ? (
    + {canAccessStandardsAdmin ? ( + + Registry Admin + + ) : null} {user?.email || user?.name} + + +
    +