Added ShowcaseUI app

This commit is contained in:
2026-10-29 17:40:48 +02:00
parent 0fe6f40b2a
commit 2bd0b12335
50 changed files with 4182 additions and 0 deletions
+8
View File
@@ -75,6 +75,7 @@ PostFundManagement/
├── PostFundManagement.Tests/ # Unit & Integration test suite (xUnit, Coverlet) ├── PostFundManagement.Tests/ # Unit & Integration test suite (xUnit, Coverlet)
├── PostFundManagement.ConsoleUI/ # Internal Staff & Admin Web Portal (React 19 + Vite 6 + Tailwind 4) ├── PostFundManagement.ConsoleUI/ # Internal Staff & Admin Web Portal (React 19 + Vite 6 + Tailwind 4)
├── PostFundManagement.PartnerUI/ # External Grantee / Partner Portal (React 19 + Vite 8 + Tailwind 4) ├── PostFundManagement.PartnerUI/ # External Grantee / Partner Portal (React 19 + Vite 8 + Tailwind 4)
├── PostFundManagement.ShowcaseUI/ # Public Facing Showcase App (React 19 + Vite 6 + Tailwind 4)
└── docker-compose.yml # Container orchestration for API and Frontends └── docker-compose.yml # Container orchestration for API and Frontends
``` ```
@@ -115,6 +116,13 @@ PostFundManagement/
* **Exports & Utilities**: `jspdf`, `xlsx`, `hashids` * **Exports & Utilities**: `jspdf`, `xlsx`, `hashids`
* **Target Users**: Grantee / Partner Project Directors, Financial Controllers, Field Officers submitting evidence and disbursement drawdown requests. * **Target Users**: Grantee / Partner Project Directors, Financial Controllers, Field Officers submitting evidence and disbursement drawdown requests.
#### **`PostFundManagement.ShowcaseUI`** (Public-Facing Product & Experience Showcase Portal)
* **Package Manager**: **Yarn** (`yarn.lock` is authoritative; `npm` must not be used)
* **Runtime & Bundler**: React 19.0.1, TypeScript 5.8.2, Vite 6.2.3
* **Styling**: Tailwind CSS v4.1.14 (`@tailwindcss/vite`)
* **Animation & Icons**: `motion` (Framer Motion) 12.23.24, `lucide-react` 0.546.0, `canvas-confetti`
* **Purpose**: Public-facing showcase and product experience portal for prospective funders, development institutions, partner organizations, and stakeholders. Integrates high-resolution screenshots and marketing posters to demonstrate both **ConsoleUI** (Funder Backoffice) and **PartnerUI** (Grantee Delivery Hub), along with an interactive 5-phase fiduciary lifecycle simulator and ROI calculator.
### 2.3 Infrastructure, Storage & Security Topology ### 2.3 Infrastructure, Storage & Security Topology
* **Database**: PostgreSQL 14+ (Separate connection strings for primary transactional store `pfm` and scheduler store `pfm-scheduler`). * **Database**: PostgreSQL 14+ (Separate connection strings for primary transactional store `pfm` and scheduler store `pfm-scheduler`).
* **Object / Evidence Store**: S3-compatible storage (e.g., Garage S3 / AWS S3) with pre-signed upload URLs and CDN acceleration. * **Object / Evidence Store**: S3-compatible storage (e.g., Garage S3 / AWS S3) with pre-signed upload URLs and CDN acceleration.
@@ -0,0 +1,6 @@
node_modules
dist
.git
.gitignore
*.log
+6
View File
@@ -0,0 +1,6 @@
node_modules
dist
.env
.env.local
*.log
+42
View File
@@ -0,0 +1,42 @@
# Stage 1: Build stage
FROM node:20-alpine AS builder
WORKDIR /app
# Enable corepack to use Yarn
RUN corepack enable
# Copy dependency definitions from the Showcase subdirectory
COPY PostFundManagement.ShowcaseUI/package*.json PostFundManagement.ShowcaseUI/yarn.lock* ./
# Install dependencies via Yarn
RUN yarn install --frozen-lockfile || yarn install
# Copy application source from the Showcase subdirectory
COPY PostFundManagement.ShowcaseUI/ ./
# Build Vite application for production
RUN yarn build
# Stage 2: Production Nginx Server
FROM nginx:alpine
WORKDIR /usr/share/nginx/html
# Copy nginx configuration
COPY --from=builder /app/nginx.conf /etc/nginx/conf.d/default.conf
# Copy compiled static assets from build stage
COPY --from=builder /app/dist ./
# Copy entrypoint script
COPY --from=builder /app/docker-entrypoint.sh /docker-entrypoint.sh
RUN chmod +x /docker-entrypoint.sh
# Expose port 3000
EXPOSE 3000
# Set entrypoint and default command
ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["nginx", "-g", "daemon off;"]
+27
View File
@@ -0,0 +1,27 @@
# PostFundManagement.ShowcaseUI
The **PFM Platform Showcase** is a public-facing, interactive web application designed to showcase **PFM Console** (Funder & Executive Management Suite) and **PFM Partner** (Implementing Partner & Grantee Suite).
## Core Capabilities
- **Dual Portal Experience**: Interactive previews of both PFM Console and PFM Partner with live hotspot explanations.
- **Milestone & Evidence Lifecycle Simulator**: Interactive 5-phase simulation walking visitors through award intake, SSO onboarding, evidence upload to Garage S3, statutory gate checks, and covenant-tied tranche release.
- **Marketing Campaign Deck (6 Posters)**: Full visual gallery and lightbox viewer for the official 6-page marketing posters.
- **Enterprise Architecture Assurance**: Deep dive into the Clean DDD architecture, .NET 10, React 19, Quartz.NET background worker, Garage S3 vault, and Duende IdentityServer SSO.
- **Fiduciary ROI & Governance Calculator**: Real-time estimations of audit hours saved and unverified capital exposure mitigated.
## Running Locally
```bash
cd PostFundManagement.ShowcaseUI
yarn install
yarn dev
```
App runs on `http://localhost:3002`.
## Building for Production
```bash
yarn build
```
@@ -0,0 +1,16 @@
#!/bin/sh
set -e
TARGET_DIR="/usr/share/nginx/html"
if [ ! -d "$TARGET_DIR" ]; then
if [ -d "./dist" ]; then
TARGET_DIR="./dist"
elif [ -d "./public" ]; then
TARGET_DIR="./public"
fi
fi
echo "[docker-entrypoint] PFM Showcase UI ready in $TARGET_DIR."
exec "$@"
+30
View File
@@ -0,0 +1,30 @@
<!doctype html>
<html lang="en" class="scroll-smooth dark">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Post Fund Management (PFM) • Executive Console & Partner Suite</title>
<meta
name="description"
content="Unified Post-Fund Management, Performance & Impact Platform. Showcasing PFM Console for Funders and PFM Partner for Grantees."
/>
<meta property="og:title" content="PFM Platform • Showcase & Experience" />
<meta
property="og:description"
content="Turn sanctioned grant awards into verified field delivery with zero uncontrolled disbursements."
/>
<meta property="og:type" content="website" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap"
rel="stylesheet"
/>
</head>
<body class="bg-slate-950 font-sans text-slate-100 antialiased selection:bg-cyan-500/25 selection:text-cyan-200 min-h-screen overflow-x-hidden">
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+38
View File
@@ -0,0 +1,38 @@
server {
listen 3000;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
# Gzip Compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_proxied expired no-cache no-store private auth;
gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml application/json image/svg+xml;
# Never cache dynamic runtime configuration files or index.html
location ~* ^/(?:config\.json|env-config\.js|index\.html)$ {
expires -1;
add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0";
}
# Cache control for static assets (CSS, JS, fonts, images)
location ~* \.(?:css|js|jpg|jpeg|gif|png|ico|cur|gz|svg|svgz|mp4|ogg|ogv|webm|htc|woff|woff2|ttf|eot)$ {
expires 1y;
access_log off;
add_header Cache-Control "public, immutable";
}
# HTML5 SPA History Mode Support
location / {
try_files $uri $uri/ /index.html;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
@@ -0,0 +1,32 @@
{
"name": "post-fund-management-showcase",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite --port=3002 --host=0.0.0.0",
"build": "vite build",
"preview": "vite preview --port=3002",
"clean": "rm -rf dist",
"lint": "tsc --noEmit"
},
"dependencies": {
"@tailwindcss/vite": "^4.1.14",
"canvas-confetti": "^1.9.4",
"lucide-react": "^0.546.0",
"motion": "^12.23.24",
"react": "^19.0.1",
"react-dom": "^19.0.1",
"tailwindcss": "^4.1.14"
},
"devDependencies": {
"@types/canvas-confetti": "^1.9.0",
"@types/node": "^22.14.0",
"@types/react": "^19.3.0",
"@types/react-dom": "^19.3.0",
"@vitejs/plugin-react": "^5.0.4",
"typescript": "~5.8.2",
"vite": "^6.2.3"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 281 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 302 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 260 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 637 KiB

@@ -0,0 +1,14 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
<defs>
<linearGradient id="pfmGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#06b6d4" />
<stop offset="50%" stop-color="#3b82f6" />
<stop offset="100%" stop-color="#8b5cf6" />
</linearGradient>
</defs>
<rect width="48" height="48" rx="12" fill="#020617" />
<rect x="1" y="1" width="46" height="46" rx="11" stroke="url(#pfmGrad)" stroke-width="2" stroke-opacity="0.6" />
<path d="M14 16H26C29.3137 16 32 18.6863 32 22C32 25.3137 29.3137 28 26 28H20V34H14V16Z" fill="url(#pfmGrad)" />
<circle cx="33" cy="33" r="4" fill="#10b981" />
</svg>

After

Width:  |  Height:  |  Size: 676 B

+73
View File
@@ -0,0 +1,73 @@
import React, { useState } from 'react';
import { HeroSection } from './components/hero/HeroSection';
import { ConsoleShowcase } from './components/console/ConsoleShowcase';
import { PartnerShowcase } from './components/partner/PartnerShowcase';
import { LifecycleSimulator } from './components/simulator/LifecycleSimulator';
import { CampaignDeckGallery } from './components/gallery/CampaignDeckGallery';
import { DualExperienceComparison } from './components/comparison/DualExperienceComparison';
import { ArchitectureAssurance } from './components/architecture/ArchitectureAssurance';
import { ImpactRoiCalculator } from './components/calculator/ImpactRoiCalculator';
import { Footer } from './components/layout/Footer';
import { ScreenshotViewerModal } from './components/modal/ScreenshotViewerModal';
import { PosterModal } from './components/modal/PosterModal';
import { ScreenshotItem, PosterItem } from './data/showcaseData';
export function App() {
const [selectedScreenshot, setSelectedScreenshot] = useState<ScreenshotItem | null>(null);
const [selectedPoster, setSelectedPoster] = useState<PosterItem | null>(null);
return (
<div className="min-h-screen bg-slate-950 text-slate-100 flex flex-col selection:bg-cyan-500/20 selection:text-cyan-300">
{/* Main Content Area */}
<main className="flex-1">
{/* 1. Hero Section & Interactive Dual-Device Showcase with spacious Brand Header */}
<HeroSection
onOpenScreenshot={(item) => setSelectedScreenshot(item)}
/>
{/* 2. PFM Console Showcase (Executive Funder Backoffice) */}
<ConsoleShowcase
onOpenScreenshot={(item) => setSelectedScreenshot(item)}
/>
{/* 3. PFM Partner Showcase (Grantee Field Delivery & Evidence) */}
<PartnerShowcase
onOpenScreenshot={(item) => setSelectedScreenshot(item)}
/>
{/* 4. Interactive 5-Phase Fiduciary Lifecycle Simulator */}
<LifecycleSimulator />
{/* 5. Strategic 6-Poster Campaign Deck Gallery */}
<CampaignDeckGallery
onOpenPosterModal={(poster) => setSelectedPoster(poster)}
/>
{/* 6. Dual Experience Comparison Matrix */}
<DualExperienceComparison />
{/* 7. Enterprise Architecture & 10 Cardinal Business Rules */}
<ArchitectureAssurance />
{/* 8. Interactive Governance & ROI Calculator */}
<ImpactRoiCalculator />
</main>
{/* Footer */}
<Footer />
{/* Interactive Lightboxes & Modals */}
<ScreenshotViewerModal
item={selectedScreenshot}
onClose={() => setSelectedScreenshot(null)}
/>
<PosterModal
poster={selectedPoster}
onClose={() => setSelectedPoster(null)}
/>
</div>
);
}
export default App;
@@ -0,0 +1,105 @@
import React from 'react';
import {
Layers,
Cpu,
HardDrive,
ShieldCheck,
Database,
Atom,
Sparkles,
CheckCircle2,
Terminal,
Server
} from 'lucide-react';
import { ARCHITECTURE_HIGHLIGHTS } from '../../data/showcaseData';
export const ArchitectureAssurance: React.FC = () => {
return (
<section id="architecture" className="py-24 bg-slate-950 relative border-t border-slate-900 overflow-hidden">
{/* Background ambient lighting */}
<div className="absolute top-1/3 right-1/4 w-96 h-96 bg-cyan-600/10 blur-[150px] rounded-full pointer-events-none -z-10" />
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Section Header */}
<div className="text-center max-w-3xl mx-auto mb-16">
<div className="inline-flex items-center gap-2 px-3.5 py-1 rounded-full bg-slate-900 border border-slate-800 text-cyan-400 text-xs font-semibold mb-4">
<Cpu className="w-3.5 h-3.5" />
<span>Enterprise Architecture Assurance</span>
</div>
<h2 className="text-3xl sm:text-4xl font-extrabold text-white tracking-tight leading-tight">
High-Performance &{' '}
<span className="text-transparent bg-clip-text bg-gradient-to-r from-cyan-400 via-blue-400 to-indigo-400">
Tamper-Evident Engineering
</span>
</h2>
<p className="mt-3 text-base text-slate-300 leading-relaxed">
Derived directly from PFM System Requirements Specification v2.0. Built on modern Clean Architecture, .NET 10, React 19, Quartz.NET, and Garage S3 object storage.
</p>
</div>
{/* 6 Tech Highlight Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-16">
{ARCHITECTURE_HIGHLIGHTS.map((item, idx) => (
<div
key={idx}
className={`p-6 rounded-2xl bg-gradient-to-br ${item.color} bg-slate-900/60 border backdrop-blur-xl shadow-xl transition-all duration-300 hover:-translate-y-1`}
>
<div className="flex items-center justify-between mb-4">
<span className="text-[11px] font-mono uppercase tracking-wider text-slate-400">
{item.subtitle}
</span>
<span className="w-2 h-2 rounded-full bg-cyan-400 animate-pulse"></span>
</div>
<h3 className="text-lg font-bold text-white mb-2">
{item.title}
</h3>
<p className="text-xs text-slate-300 leading-relaxed">
{item.description}
</p>
</div>
))}
</div>
{/* Master Business Rules Banner */}
<div className="p-8 rounded-3xl bg-slate-900/60 border border-slate-800/80 shadow-2xl backdrop-blur-xl">
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-6 pb-6 mb-6 border-b border-slate-800">
<div>
<div className="text-xs font-mono font-semibold text-cyan-400 uppercase tracking-wider mb-1">
Fiduciary Guarantee
</div>
<h4 className="text-xl font-bold text-white">
The 10 Cardinal Business Rules (BR-001 – BR-010)
</h4>
</div>
<div className="flex items-center gap-2 text-xs font-mono text-slate-400 bg-slate-950 px-3 py-1.5 rounded-lg border border-slate-800">
<Terminal className="w-3.5 h-3.5 text-emerald-400" />
<span>Strict Domain Invariant Enforcement</span>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 text-xs">
<div className="p-3 rounded-xl bg-slate-950/70 border border-slate-800/80">
<span className="font-mono text-cyan-400 font-bold">BR-001:</span> Approval is Entry Point. No contract or project exists without external award reference.
</div>
<div className="p-3 rounded-xl bg-slate-950/70 border border-slate-800/80">
<span className="font-mono text-cyan-400 font-bold">BR-002:</span> Automatic Contract Initiation. Award captures immediately spawn draft agreements.
</div>
<div className="p-3 rounded-xl bg-slate-950/70 border border-slate-800/80">
<span className="font-mono text-cyan-400 font-bold">BR-003:</span> No Uncontrolled Disbursement. Zero capital release without verified evidence proofs.
</div>
<div className="p-3 rounded-xl bg-slate-950/70 border border-slate-800/80">
<span className="font-mono text-cyan-400 font-bold">BR-004:</span> Evidence-Based Performance. Uploaded files must substantiate milestone deliverables.
</div>
<div className="p-3 rounded-xl bg-slate-950/70 border border-slate-800/80">
<span className="font-mono text-cyan-400 font-bold">BR-005:</span> Single Source of Truth. Recipient KYC and bank details captured once, referenced immutably.
</div>
<div className="p-3 rounded-xl bg-slate-950/70 border border-slate-800/80">
<span className="font-mono text-cyan-400 font-bold">BR-010:</span> Automated Portfolio Roll-Up. Activity metrics roll up to Programme, Portfolio, and Donor tiers.
</div>
</div>
</div>
</div>
</section>
);
};
@@ -0,0 +1,196 @@
import React, { useState } from 'react';
import {
Calculator,
TrendingUp,
Clock,
ShieldCheck,
Sparkles,
DollarSign
} from 'lucide-react';
export const ImpactRoiCalculator: React.FC = () => {
const [portfolioCapital, setPortfolioCapital] = useState<number>(50); // in Millions (ZAR)
const [projectCount, setProjectCount] = useState<number>(25);
const [milestonesPerProject, setMilestonesPerProject] = useState<number>(6);
// Calculations
const totalMilestones = projectCount * milestonesPerProject;
const hoursSavedPerYear = Math.round(totalMilestones * 4.5); // 4.5 hours saved per manual audit/evidence verification cycle
const riskMitigatedZar = (portfolioCapital * 0.18).toFixed(1); // Avg 18% unverified leakage prevented
const turnaroundSpeedMultiplier = '3.8x';
return (
<section id="roi-calculator" className="py-24 bg-slate-950 relative border-t border-slate-900 overflow-hidden">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Section Header */}
<div className="text-center max-w-3xl mx-auto mb-16">
<div className="inline-flex items-center gap-2 px-3.5 py-1 rounded-full bg-slate-900 border border-slate-800 text-emerald-400 text-xs font-semibold mb-4">
<Calculator className="w-3.5 h-3.5" />
<span>Interactive Governance & ROI Calculator</span>
</div>
<h2 className="text-3xl sm:text-4xl font-extrabold text-white tracking-tight leading-tight">
Calculate Your{' '}
<span className="text-transparent bg-clip-text bg-gradient-to-r from-emerald-400 via-teal-400 to-cyan-400">
Fiduciary Savings & Efficiency
</span>
</h2>
<p className="mt-3 text-base text-slate-300 leading-relaxed">
Estimate how automated statutory gate checks, direct milestone proof uploads, and Quartz.NET SLA monitors reduce administrative overhead and mitigate capital exposure.
</p>
</div>
{/* Calculator Main Grid */}
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 items-center bg-slate-900/50 rounded-3xl p-6 sm:p-8 border border-slate-800/80 shadow-2xl backdrop-blur-xl">
{/* Controls Column */}
<div className="lg:col-span-6 space-y-6">
{/* Slider 1: Capital */}
<div>
<div className="flex items-center justify-between text-xs font-semibold text-slate-300 mb-2">
<span>Total Portfolio Capital Under Management</span>
<span className="text-emerald-400 font-mono text-sm font-bold">
R {portfolioCapital} Million
</span>
</div>
<input
type="range"
min="5"
max="250"
step="5"
value={portfolioCapital}
onChange={(e) => setPortfolioCapital(Number(e.target.value))}
className="w-full h-2 bg-slate-800 rounded-lg appearance-none cursor-pointer accent-emerald-500"
/>
<div className="flex justify-between text-[10px] text-slate-500 mt-1 font-mono">
<span>R 5M</span>
<span>R 100M</span>
<span>R 250M+</span>
</div>
</div>
{/* Slider 2: Projects */}
<div>
<div className="flex items-center justify-between text-xs font-semibold text-slate-300 mb-2">
<span>Number of Funded Projects / Grantees</span>
<span className="text-cyan-400 font-mono text-sm font-bold">
{projectCount} Projects
</span>
</div>
<input
type="range"
min="3"
max="100"
step="1"
value={projectCount}
onChange={(e) => setProjectCount(Number(e.target.value))}
className="w-full h-2 bg-slate-800 rounded-lg appearance-none cursor-pointer accent-cyan-500"
/>
<div className="flex justify-between text-[10px] text-slate-500 mt-1 font-mono">
<span>3</span>
<span>50</span>
<span>100+</span>
</div>
</div>
{/* Slider 3: Milestones */}
<div>
<div className="flex items-center justify-between text-xs font-semibold text-slate-300 mb-2">
<span>Contractual Milestones per Grant Award</span>
<span className="text-indigo-400 font-mono text-sm font-bold">
{milestonesPerProject} Milestones
</span>
</div>
<input
type="range"
min="2"
max="15"
step="1"
value={milestonesPerProject}
onChange={(e) => setMilestonesPerProject(Number(e.target.value))}
className="w-full h-2 bg-slate-800 rounded-lg appearance-none cursor-pointer accent-indigo-500"
/>
<div className="flex justify-between text-[10px] text-slate-500 mt-1 font-mono">
<span>2</span>
<span>8</span>
<span>15</span>
</div>
</div>
<div className="p-4 rounded-xl bg-slate-950/70 border border-slate-800 text-xs text-slate-400">
<span className="text-slate-200 font-semibold">Underlying Model:</span> Estimates based on independent development finance audits comparing manual spreadsheet evidence gathering against PFM automated gate clearances.
</div>
</div>
{/* Results Output Column */}
<div className="lg:col-span-6 space-y-4">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{/* Metric 1 */}
<div className="p-5 rounded-2xl bg-slate-950 border border-slate-800/80 shadow-lg">
<div className="flex items-center justify-between mb-2">
<span className="text-xs text-slate-400">Risk Exposure Avoided</span>
<ShieldCheck className="w-4 h-4 text-emerald-400" />
</div>
<div className="text-2xl font-extrabold text-emerald-400 font-mono mb-1">
R {riskMitigatedZar}M
</div>
<p className="text-[11px] text-slate-400">
Uncontrolled tranches shielded by BR-003 condition precedent gates.
</p>
</div>
{/* Metric 2 */}
<div className="p-5 rounded-2xl bg-slate-950 border border-slate-800/80 shadow-lg">
<div className="flex items-center justify-between mb-2">
<span className="text-xs text-slate-400">Annual Audit Hours Saved</span>
<Clock className="w-4 h-4 text-cyan-400" />
</div>
<div className="text-2xl font-extrabold text-cyan-400 font-mono mb-1">
{hoursSavedPerYear.toLocaleString()} hrs
</div>
<p className="text-[11px] text-slate-400">
Manual document chasing and reconciliation automated.
</p>
</div>
{/* Metric 3 */}
<div className="p-5 rounded-2xl bg-slate-950 border border-slate-800/80 shadow-lg">
<div className="flex items-center justify-between mb-2">
<span className="text-xs text-slate-400">Tranche Velocity</span>
<TrendingUp className="w-4 h-4 text-indigo-400" />
</div>
<div className="text-2xl font-extrabold text-indigo-400 font-mono mb-1">
{turnaroundSpeedMultiplier} Faster
</div>
<p className="text-[11px] text-slate-400">
From partner evidence submission to verified tranche release.
</p>
</div>
{/* Metric 4 */}
<div className="p-5 rounded-2xl bg-slate-950 border border-slate-800/80 shadow-lg">
<div className="flex items-center justify-between mb-2">
<span className="text-xs text-slate-400">Auditable Proof Artifacts</span>
<Sparkles className="w-4 h-4 text-purple-400" />
</div>
<div className="text-2xl font-extrabold text-purple-400 font-mono mb-1">
{totalMilestones} Proofs
</div>
<p className="text-[11px] text-slate-400">
Indexed with immutable SHA signatures in Garage S3 vault.
</p>
</div>
</div>
{/* Telemetry pill */}
<div className="pt-2 p-4 rounded-2xl bg-emerald-950/20 border border-emerald-500/30 flex items-center justify-between text-xs">
<div className="flex items-center gap-2 text-emerald-300 font-semibold">
<ShieldCheck className="w-4 h-4 text-emerald-400" />
<span>Automated Statutory Governance Protection</span>
</div>
<span className="font-mono text-slate-400 text-[11px]">BR-001 — BR-010 Enforced</span>
</div>
</div>
</div>
</div>
</section>
);
};
@@ -0,0 +1,82 @@
import React from 'react';
import {
ShieldCheck,
FileCheck2,
Check,
ArrowRight,
ExternalLink,
Sparkles
} from 'lucide-react';
import { COMPARISON_FEATURES } from '../../data/showcaseData';
export const DualExperienceComparison: React.FC = () => {
return (
<section className="py-24 bg-slate-950 relative border-t border-slate-900 overflow-hidden">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Section Header */}
<div className="text-center max-w-3xl mx-auto mb-16">
<div className="inline-flex items-center gap-2 px-3.5 py-1 rounded-full bg-slate-900 border border-slate-800 text-indigo-400 text-xs font-semibold mb-4">
<Sparkles className="w-3.5 h-3.5" />
<span>Dual Experience Matrix</span>
</div>
<h2 className="text-3xl sm:text-4xl font-extrabold text-white tracking-tight leading-tight">
PFM Console vs.{' '}
<span className="text-transparent bg-clip-text bg-gradient-to-r from-blue-400 via-indigo-400 to-emerald-400">
PFM Partner
</span>
</h2>
<p className="mt-3 text-base text-slate-300 leading-relaxed">
Engineered with clear separation of duties. Backoffice fiduciary gatekeepers govern portfolios, while field implementation teams deliver verified outcomes.
</p>
</div>
{/* Comparison Table */}
<div className="overflow-x-auto rounded-3xl border border-slate-800 bg-slate-900/40 backdrop-blur-xl shadow-2xl">
<table className="w-full text-left border-collapse">
<thead>
<tr className="border-b border-slate-800 bg-slate-950/80">
<th className="py-5 px-6 text-xs font-mono uppercase tracking-wider text-slate-400 w-1/4">
Functional Dimension
</th>
<th className="py-5 px-6 text-xs font-bold uppercase tracking-wider text-blue-400 w-3/8 bg-blue-950/20 border-x border-slate-800">
<div className="flex items-center gap-2">
<ShieldCheck className="w-4 h-4" />
<span>PFM Console (Funders & DFIs)</span>
</div>
</th>
<th className="py-5 px-6 text-xs font-bold uppercase tracking-wider text-emerald-400 w-3/8 bg-emerald-950/20">
<div className="flex items-center gap-2">
<FileCheck2 className="w-4 h-4" />
<span>PFM Partner (Grantees & Contractors)</span>
</div>
</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-800/60 text-xs sm:text-sm">
{COMPARISON_FEATURES.map((row, idx) => (
<tr key={idx} className="hover:bg-slate-800/30 transition-colors">
<td className="py-4 px-6 font-semibold text-slate-300">
{row.feature}
</td>
<td className="py-4 px-6 text-slate-300 bg-blue-950/10 border-x border-slate-800/80">
<div className="flex items-start gap-2">
<span className="w-1.5 h-1.5 rounded-full bg-blue-400 mt-2 flex-shrink-0"></span>
<span>{row.console}</span>
</div>
</td>
<td className="py-4 px-6 text-slate-300 bg-emerald-950/10">
<div className="flex items-start gap-2">
<span className="w-1.5 h-1.5 rounded-full bg-emerald-400 mt-2 flex-shrink-0"></span>
<span>{row.partner}</span>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</section>
);
};
@@ -0,0 +1,214 @@
import React, { useState } from 'react';
import {
ShieldCheck,
BarChart3,
Layers,
Calendar,
FileCheck,
Lock,
AlertTriangle,
TrendingUp,
Users,
ExternalLink,
ChevronRight,
Maximize2
} from 'lucide-react';
import { SCREENSHOTS, ScreenshotItem } from '../../data/showcaseData';
interface ConsoleShowcaseProps {
onOpenScreenshot: (item: ScreenshotItem) => void;
}
export const ConsoleShowcase: React.FC<ConsoleShowcaseProps> = ({ onOpenScreenshot }) => {
const consoleScreenshots = SCREENSHOTS.filter((s) => s.platform === 'PFM Console');
const [selectedCategory, setSelectedCategory] = useState<string>('All');
const categories = ['All', 'Dashboard', 'WBS & Gantt', 'Governance & Gate'];
const filteredScreens = selectedCategory === 'All'
? consoleScreenshots
: consoleScreenshots.filter((s) => s.category === selectedCategory);
return (
<section id="console-section" className="py-24 bg-slate-950 relative border-t border-slate-900 overflow-hidden">
{/* Background ambient glow */}
<div className="absolute top-1/3 left-0 w-96 h-96 bg-blue-600/10 blur-[130px] rounded-full pointer-events-none -z-10" />
<div className="absolute bottom-10 right-0 w-80 h-80 bg-indigo-600/10 blur-[120px] rounded-full pointer-events-none -z-10" />
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Section Header */}
<div className="flex flex-col md:flex-row md:items-end justify-between gap-6 mb-16">
<div className="max-w-3xl">
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-blue-500/10 border border-blue-500/30 text-blue-400 text-xs font-semibold mb-4">
<ShieldCheck className="w-3.5 h-3.5" />
<span>Institutional Funder Backoffice Suite</span>
</div>
<h2 className="text-3xl sm:text-4xl font-extrabold text-white tracking-tight leading-tight">
PFM Console •{' '}
<span className="text-transparent bg-clip-text bg-gradient-to-r from-blue-400 via-indigo-400 to-purple-400">
Executive Portfolio Command
</span>
</h2>
<p className="mt-3 text-base text-slate-300 leading-relaxed">
Equip investment committees, CFOs, and programme directors with real-time capital deployment telemetry, automated statutory gate checks, and direct beneficiary impact verification.
</p>
</div>
<div className="flex items-center gap-3">
<a
href="https://console.ifri.khongisa.co.za"
target="_blank"
rel="noreferrer"
className="px-4 py-2 rounded-xl bg-blue-600/20 hover:bg-blue-600/30 border border-blue-500/40 text-blue-300 hover:text-white text-xs font-semibold transition-all flex items-center gap-2 shadow-sm"
>
<span>Launch Live Console</span>
<ExternalLink className="w-3.5 h-3.5" />
</a>
</div>
</div>
{/* 3 Core Pillar Cards */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-16">
{/* Pillar 1 */}
<div className="p-6 rounded-2xl bg-slate-900/60 border border-slate-800/80 hover:border-blue-500/40 transition-all shadow-xl group">
<div className="w-12 h-12 rounded-xl bg-blue-500/10 border border-blue-500/20 flex items-center justify-center text-blue-400 mb-5 group-hover:scale-110 transition-transform">
<BarChart3 className="w-6 h-6" />
</div>
<div className="flex items-center gap-2 mb-2">
<span className="text-xs font-mono font-semibold text-blue-400">PILLAR 01</span>
<span className="text-slate-600">•</span>
<span className="text-xs text-slate-400">Capital Scale</span>
</div>
<h3 className="text-lg font-bold text-white mb-2">
Multi-Portfolio Capital Deployment
</h3>
<p className="text-xs text-slate-300 leading-relaxed mb-4">
Filter commitments, released tranches (R 150k deployed), and active awards dynamically by programme, sector, and financial period with automated rollups.
</p>
<div className="pt-4 border-t border-slate-800/80 flex items-center justify-between text-[11px] text-slate-400">
<span className="font-mono text-blue-400 font-semibold">R 21.0m Governed</span>
<span>BR-010 Compliant</span>
</div>
</div>
{/* Pillar 2 */}
<div className="p-6 rounded-2xl bg-slate-900/60 border border-slate-800/80 hover:border-indigo-500/40 transition-all shadow-xl group">
<div className="w-12 h-12 rounded-xl bg-indigo-500/10 border border-indigo-500/20 flex items-center justify-center text-indigo-400 mb-5 group-hover:scale-110 transition-transform">
<Lock className="w-6 h-6" />
</div>
<div className="flex items-center gap-2 mb-2">
<span className="text-xs font-mono font-semibold text-indigo-400">PILLAR 02</span>
<span className="text-slate-600">•</span>
<span className="text-xs text-slate-400">Fiduciary Gating</span>
</div>
<h3 className="text-lg font-bold text-white mb-2">
100% Statutory Gate Enforcement
</h3>
<p className="text-xs text-slate-300 leading-relaxed mb-4">
Enforce Business Rule BR-003: no payment or disbursement is approved without verified banking KYC, signed legal agreements, and timestamped proofs.
</p>
<div className="pt-4 border-t border-slate-800/80 flex items-center justify-between text-[11px] text-slate-400">
<span className="font-mono text-emerald-400 font-semibold">0% Exposure</span>
<span>BR-003 Hard Gate</span>
</div>
</div>
{/* Pillar 3 */}
<div className="p-6 rounded-2xl bg-slate-900/60 border border-slate-800/80 hover:border-purple-500/40 transition-all shadow-xl group">
<div className="w-12 h-12 rounded-xl bg-purple-500/10 border border-purple-500/20 flex items-center justify-center text-purple-400 mb-5 group-hover:scale-110 transition-transform">
<Calendar className="w-6 h-6" />
</div>
<div className="flex items-center gap-2 mb-2">
<span className="text-xs font-mono font-semibold text-purple-400">PILLAR 03</span>
<span className="text-slate-600">•</span>
<span className="text-xs text-slate-400">WBS & Gantt</span>
</div>
<h3 className="text-lg font-bold text-white mb-2">
Chronological Gantt & Variance Audits
</h3>
<p className="text-xs text-slate-300 leading-relaxed mb-4">
55-day span milestone chronology with automated variance detection comparing work breakdown task actuals against allocated budgets.
</p>
<div className="pt-4 border-t border-slate-800/80 flex items-center justify-between text-[11px] text-slate-400">
<span className="font-mono text-purple-400 font-semibold">Realized Actuals vs Budget</span>
<span>SLA Alerts</span>
</div>
</div>
</div>
{/* Screenshot Category Filter & Grid */}
<div className="mb-6 flex flex-wrap items-center justify-between gap-4">
<div className="flex items-center gap-2 p-1 rounded-xl bg-slate-900 border border-slate-800">
{categories.map((cat) => (
<button
key={cat}
onClick={() => setSelectedCategory(cat)}
className={`px-3 py-1.5 rounded-lg text-xs font-semibold transition-all ${
selectedCategory === cat
? 'bg-blue-600 text-white shadow-sm'
: 'text-slate-400 hover:text-slate-200'
}`}
>
{cat}
</button>
))}
</div>
<span className="text-xs text-slate-400 font-mono">
Showing {filteredScreens.length} Console Views
</span>
</div>
{/* Screenshot Cards Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{filteredScreens.map((screen) => (
<div
key={screen.id}
onClick={() => onOpenScreenshot(screen)}
className="group cursor-pointer rounded-2xl bg-slate-900/60 border border-slate-800 hover:border-blue-500/50 overflow-hidden shadow-xl transition-all duration-300 hover:-translate-y-1"
>
<div className="relative aspect-[16/10] overflow-hidden bg-slate-950">
<img
src={screen.image}
alt={screen.title}
className="w-full h-full object-cover object-top group-hover:scale-105 transition-transform duration-500"
loading="lazy"
/>
<div className="absolute inset-0 bg-gradient-to-t from-slate-950 via-slate-950/20 to-transparent opacity-80 group-hover:opacity-40 transition-opacity" />
<div className="absolute top-3 right-3 p-2 rounded-lg bg-slate-950/80 backdrop-blur-md border border-slate-700/80 text-slate-300 opacity-0 group-hover:opacity-100 transition-opacity">
<Maximize2 className="w-4 h-4" />
</div>
<div className="absolute bottom-3 left-3">
<span className="text-[10px] uppercase font-bold tracking-wider px-2.5 py-1 rounded-full bg-blue-500/20 text-blue-300 border border-blue-500/30 backdrop-blur-md">
{screen.category}
</span>
</div>
</div>
<div className="p-5">
<h4 className="text-base font-bold text-white mb-1 group-hover:text-blue-300 transition-colors">
{screen.title}
</h4>
<p className="text-xs text-slate-300 leading-relaxed mb-4 line-clamp-2">
{screen.description}
</p>
<div className="flex flex-wrap gap-1.5">
{screen.tags.map((tag) => (
<span
key={tag}
className="text-[10px] font-mono px-2 py-0.5 rounded bg-slate-800/80 text-slate-400"
>
#{tag}
</span>
))}
</div>
</div>
</div>
))}
</div>
</div>
</section>
);
};
@@ -0,0 +1,187 @@
import React, { useState } from 'react';
import {
FileText,
Maximize2,
ChevronRight,
Download,
CheckCircle2,
Users,
Sparkles,
ExternalLink
} from 'lucide-react';
import { CAMPAIGN_POSTERS, PosterItem } from '../../data/showcaseData';
interface CampaignDeckGalleryProps {
onOpenPosterModal: (poster: PosterItem) => void;
}
export const CampaignDeckGallery: React.FC<CampaignDeckGalleryProps> = ({ onOpenPosterModal }) => {
const [selectedPosterId, setSelectedPosterId] = useState<number>(1);
const currentPoster = CAMPAIGN_POSTERS.find((p) => p.id === selectedPosterId) || CAMPAIGN_POSTERS[0];
return (
<section id="campaign-posters" className="py-24 bg-slate-950 relative border-t border-slate-900 overflow-hidden">
{/* Ambient background glow */}
<div className="absolute top-1/4 left-1/3 w-96 h-96 bg-purple-600/10 blur-[140px] rounded-full pointer-events-none -z-10" />
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Section Header */}
<div className="flex flex-col md:flex-row md:items-end justify-between gap-6 mb-16">
<div className="max-w-3xl">
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-purple-500/10 border border-purple-500/30 text-purple-400 text-xs font-semibold mb-4">
<FileText className="w-3.5 h-3.5" />
<span>Official 6-Page Campaign Deck</span>
</div>
<h2 className="text-3xl sm:text-4xl font-extrabold text-white tracking-tight leading-tight">
PFM Platform Marketing Posters •{' '}
<span className="text-transparent bg-clip-text bg-gradient-to-r from-purple-400 via-pink-400 to-indigo-400">
Strategic Campaign
</span>
</h2>
<p className="mt-3 text-base text-slate-300 leading-relaxed">
Explore the official marketing campaign series showcasing field-level milestone proof, fiduciary portfolio command, institutional SSO security, and interactive WBS Gantt oversight.
</p>
</div>
<div className="flex items-center gap-3">
<a
href="/assets/marketing/PFM Platform Marketing Posters (6-Page Campaign Deck).pdf"
target="_blank"
rel="noreferrer"
className="px-4 py-2 rounded-xl bg-purple-600/20 hover:bg-purple-600/30 border border-purple-500/40 text-purple-300 hover:text-white text-xs font-semibold transition-all flex items-center gap-2 shadow-sm"
>
<Download className="w-3.5 h-3.5" />
<span>Download PDF Campaign Deck</span>
</a>
</div>
</div>
{/* Poster Tabs Selector */}
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-6 gap-2 mb-10">
{CAMPAIGN_POSTERS.map((poster) => {
const isSelected = poster.id === selectedPosterId;
return (
<button
key={poster.id}
onClick={() => setSelectedPosterId(poster.id)}
className={`p-3 rounded-xl border text-left transition-all ${
isSelected
? 'bg-purple-950/40 border-purple-500/60 shadow-lg shadow-purple-500/10'
: 'bg-slate-900/60 border-slate-800/80 hover:bg-slate-800/60'
}`}
>
<div className="flex items-center justify-between text-[10px] font-mono mb-1">
<span className={isSelected ? 'text-purple-400 font-bold' : 'text-slate-500'}>
POSTER {poster.number.split('/')[0]}
</span>
<span className={`px-1.5 py-0.2 rounded text-[9px] ${
poster.platform === 'PFM Console'
? 'bg-blue-500/20 text-blue-300'
: 'bg-emerald-500/20 text-emerald-300'
}`}>
{poster.platform === 'PFM Console' ? 'Console' : 'Partner'}
</span>
</div>
<div className="text-xs font-bold text-white truncate">
{poster.badge}
</div>
</button>
);
})}
</div>
{/* Selected Poster Showcase Card */}
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 items-center bg-slate-900/50 rounded-3xl p-6 sm:p-8 border border-slate-800/80 shadow-2xl backdrop-blur-xl">
{/* Poster Image Preview */}
<div className="lg:col-span-6 relative group cursor-pointer" onClick={() => onOpenPosterModal(currentPoster)}>
<div className="relative aspect-[3/4] sm:aspect-[4/5] rounded-2xl overflow-hidden border border-slate-700/80 shadow-2xl bg-slate-950">
<img
src={currentPoster.image}
alt={currentPoster.title}
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
/>
<div className="absolute inset-0 bg-gradient-to-t from-slate-950/80 via-transparent to-transparent opacity-60 group-hover:opacity-20 transition-opacity" />
{/* Zoom pill */}
<div className="absolute bottom-4 right-4 flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-slate-950/80 backdrop-blur-md border border-slate-700 text-xs text-white shadow-lg">
<Maximize2 className="w-3.5 h-3.5 text-purple-400" />
<span>Zoom Poster</span>
</div>
</div>
</div>
{/* Poster Narrative & Highlights */}
<div className="lg:col-span-6 space-y-6">
<div>
<div className="flex items-center gap-2 mb-2">
<span className="text-xs font-mono font-bold text-purple-400">
{currentPoster.number}
</span>
<span className="text-slate-600">•</span>
<span className={`text-[11px] font-semibold uppercase px-2.5 py-0.5 rounded-full border ${
currentPoster.platform === 'PFM Console'
? 'bg-blue-500/10 text-blue-400 border-blue-500/30'
: 'bg-emerald-500/10 text-emerald-400 border-emerald-500/30'
}`}>
{currentPoster.platform}
</span>
</div>
<h3 className="text-2xl font-extrabold text-white tracking-tight mb-2">
{currentPoster.title}
</h3>
<p className="text-xs sm:text-sm text-slate-300 leading-relaxed">
{currentPoster.subtitle}
</p>
</div>
{/* Poster Metrics Grid */}
<div className="grid grid-cols-2 gap-3 p-4 rounded-2xl bg-slate-950/80 border border-slate-800">
{currentPoster.metrics.map((metric, idx) => (
<div key={idx} className="p-2">
<div className="text-xs text-slate-400 mb-0.5">{metric.label}</div>
<div className="text-sm font-bold text-white font-mono">{metric.value}</div>
<div className="text-[10px] text-slate-400">{metric.detail}</div>
</div>
))}
</div>
{/* 3 Numbered Key Takeaways */}
<div className="space-y-3">
{currentPoster.keyHighlights.map((hl) => (
<div key={hl.number} className="flex items-start gap-3">
<span className="flex items-center justify-center w-6 h-6 rounded-md bg-purple-500/20 text-purple-300 text-xs font-mono font-bold border border-purple-500/30 flex-shrink-0 mt-0.5">
{hl.number}
</span>
<div>
<h5 className="text-xs font-bold text-white">{hl.title}</h5>
<p className="text-xs text-slate-300 leading-relaxed">{hl.description}</p>
</div>
</div>
))}
</div>
{/* Target Market Tags */}
<div className="pt-4 border-t border-slate-800/80">
<div className="text-xs text-slate-400 font-semibold mb-2 flex items-center gap-1.5">
<Users className="w-3.5 h-3.5 text-purple-400" />
<span>Target Market Audience</span>
</div>
<div className="flex flex-wrap gap-1.5">
{currentPoster.targetMarket.map((item, idx) => (
<span
key={idx}
className="text-[11px] px-2.5 py-1 rounded-lg bg-slate-900 border border-slate-800 text-slate-300"
>
{item}
</span>
))}
</div>
</div>
</div>
</div>
</div>
</section>
);
};
@@ -0,0 +1,125 @@
import React, { useState } from 'react';
import {
ShieldCheck,
Sparkles,
ArrowRight,
CheckCircle2,
Layers,
ExternalLink,
Laptop,
Users2,
FileCheck2,
Compass
} from 'lucide-react';
import { BrandLogo } from '../ui/BrandLogo';
import { InteractiveDeviceFrame } from './InteractiveDeviceFrame';
import { SHOWCASE_STATS, ScreenshotItem } from '../../data/showcaseData';
interface HeroSectionProps {
onOpenScreenshot: (item: ScreenshotItem) => void;
}
export const HeroSection: React.FC<HeroSectionProps> = ({ onOpenScreenshot }) => {
const [activeMode, setActiveMode] = useState<'console' | 'partner' | 'unified'>('console');
return (
<section id="overview" className="relative pt-10 sm:pt-14 pb-20 overflow-hidden bg-slate-950">
{/* Background ambient lighting */}
<div className="absolute top-1/4 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[700px] h-[500px] bg-gradient-to-tr from-cyan-600/15 via-blue-600/10 to-indigo-600/15 blur-[120px] rounded-full pointer-events-none -z-10" />
<div className="absolute top-1/3 left-10 w-72 h-72 bg-emerald-500/10 blur-[90px] rounded-full pointer-events-none -z-10" />
<div className="absolute top-1/2 right-10 w-80 h-80 bg-purple-600/10 blur-[100px] rounded-full pointer-events-none -z-10" />
{/* Grid overlay */}
<div className="absolute inset-0 bg-grid-pattern opacity-40 -z-10" />
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Spacious, uncrowded brand header */}
<div className="flex flex-col sm:flex-row items-center justify-between gap-6 pb-12 mb-10 border-b border-slate-900/80">
<BrandLogo size="lg" variant="full" />
{/* Clean minimal CTA to jump straight into the experience */}
<div className="flex items-center gap-3">
<a
href="#interactive-tour"
className="px-5 py-2.5 rounded-xl bg-gradient-to-r from-cyan-500 to-blue-600 hover:from-cyan-400 hover:to-blue-500 text-white font-semibold text-xs sm:text-sm shadow-lg shadow-cyan-500/20 hover:shadow-cyan-500/35 transition-all flex items-center gap-2"
>
<Laptop className="w-4 h-4" />
<span>Explore Interactive Portals</span>
</a>
</div>
</div>
{/* Main Hero Header */}
<div className="text-center max-w-4xl mx-auto mb-12 sm:mb-16">
{/* Pill Badge */}
<div className="inline-flex items-center gap-2 px-4 py-1.5 rounded-full bg-slate-900/90 border border-slate-700/80 shadow-xl mb-6 backdrop-blur-md">
<span className="flex h-2 w-2 rounded-full bg-cyan-400 animate-pulse" />
<span className="text-xs font-semibold text-slate-200">
Post-Fund Management, Performance & Impact Platform
</span>
<span className="text-slate-500 text-xs">•</span>
<span className="text-xs font-mono text-cyan-400">SRS v2.0 Production Suite</span>
</div>
{/* Main Title */}
<h1 className="text-4xl sm:text-5xl md:text-6xl font-extrabold tracking-tight text-white mb-6 leading-[1.12]">
Two Portals.{' '}
<span className="text-transparent bg-clip-text bg-gradient-to-r from-cyan-400 via-blue-400 to-indigo-400">
One Unified Fiduciary Lifecycle.
</span>
</h1>
{/* Subtitle */}
<p className="text-base sm:text-lg md:text-xl text-slate-300 font-normal leading-relaxed mb-8 max-w-3xl mx-auto">
From sanctioned award intake to audited field delivery. PFM bridges the gap between{' '}
<span className="text-indigo-300 font-semibold">Institutional Funders (PFM Console)</span> and{' '}
<span className="text-emerald-300 font-semibold">Implementing Grantees (PFM Partner)</span> with zero uncontrolled disbursements.
</p>
{/* CTAs */}
<div className="flex flex-wrap items-center justify-center gap-4">
<a
href="#interactive-tour"
className="px-6 py-3 rounded-xl bg-gradient-to-r from-cyan-500 via-blue-600 to-indigo-600 hover:from-cyan-400 hover:to-indigo-500 text-white font-semibold text-sm shadow-xl shadow-cyan-500/20 hover:shadow-cyan-500/35 transition-all flex items-center gap-2 group"
>
<span>Explore Interactive Lifecycle Tour</span>
<ArrowRight className="w-4 h-4 group-hover:translate-x-1 transition-transform" />
</a>
<a
href="#campaign-posters"
className="px-5 py-3 rounded-xl bg-slate-900/80 hover:bg-slate-900 border border-slate-800 hover:border-slate-700 text-slate-300 hover:text-white font-medium text-sm transition-all flex items-center gap-2"
>
<span>View Marketing Campaign Deck (6 Posters)</span>
</a>
</div>
</div>
{/* Live Device Showcase */}
<div className="mb-16">
<InteractiveDeviceFrame
onOpenScreenshot={onOpenScreenshot}
activeMode={activeMode}
setActiveMode={setActiveMode}
/>
</div>
{/* Stats Ticker */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 p-6 rounded-2xl bg-slate-900/40 border border-slate-800/80 backdrop-blur-xl shadow-xl">
{SHOWCASE_STATS.map((stat, idx) => (
<div key={idx} className="p-4 text-center border-r last:border-r-0 border-slate-800/60">
<div className={`text-2xl sm:text-3xl font-extrabold tracking-tight ${stat.highlight} mb-1 font-mono`}>
{stat.value}
</div>
<div className="text-xs sm:text-sm font-semibold text-slate-200 mb-0.5">
{stat.label}
</div>
<div className="text-[11px] text-slate-400">
{stat.detail}
</div>
</div>
))}
</div>
</div>
</section>
);
};
@@ -0,0 +1,221 @@
import React, { useState } from 'react';
import {
Maximize2,
ShieldCheck,
FileCheck2,
Layers,
ExternalLink,
CheckCircle2,
ChevronRight,
Sparkles,
Info
} from 'lucide-react';
import { SCREENSHOTS, ScreenshotItem } from '../../data/showcaseData';
interface InteractiveDeviceFrameProps {
onOpenScreenshot: (item: ScreenshotItem) => void;
activeMode: 'console' | 'partner' | 'unified';
setActiveMode: (mode: 'console' | 'partner' | 'unified') => void;
}
export const InteractiveDeviceFrame: React.FC<InteractiveDeviceFrameProps> = ({
onOpenScreenshot,
activeMode,
setActiveMode,
}) => {
const [activeHotspot, setActiveHotspot] = useState<number | null>(null);
const consoleItem = SCREENSHOTS.find((s) => s.id === 'console-primary') || SCREENSHOTS[0];
const partnerItem = SCREENSHOTS.find((s) => s.id === 'partner-primary') || SCREENSHOTS[1];
const currentItem = activeMode === 'partner' ? partnerItem : consoleItem;
return (
<div className="relative w-full max-w-6xl mx-auto">
{/* Platform Switcher Controls */}
<div className="flex flex-wrap items-center justify-between gap-4 mb-4 px-2">
<div className="flex items-center p-1 rounded-xl bg-slate-900/90 border border-slate-800 shadow-xl backdrop-blur-md">
<button
onClick={() => setActiveMode('console')}
className={`flex items-center gap-2 px-4 py-2 rounded-lg text-xs sm:text-sm font-semibold transition-all ${
activeMode === 'console'
? 'bg-gradient-to-r from-blue-600 to-indigo-600 text-white shadow-md shadow-blue-500/20'
: 'text-slate-400 hover:text-slate-200'
}`}
>
<ShieldCheck className="w-4 h-4 text-blue-300" />
<span>PFM Console (Funder Suite)</span>
</button>
<button
onClick={() => setActiveMode('partner')}
className={`flex items-center gap-2 px-4 py-2 rounded-lg text-xs sm:text-sm font-semibold transition-all ${
activeMode === 'partner'
? 'bg-gradient-to-r from-emerald-600 to-teal-600 text-white shadow-md shadow-emerald-500/20'
: 'text-slate-400 hover:text-slate-200'
}`}
>
<FileCheck2 className="w-4 h-4 text-emerald-300" />
<span>PFM Partner (Grantee Suite)</span>
</button>
</div>
<div className="flex items-center gap-2 text-xs text-slate-400">
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-slate-900 border border-slate-800">
<span className="w-2 h-2 rounded-full bg-emerald-400 animate-pulse"></span>
<span className="font-mono text-[11px] text-slate-300">Live Application Telemetry</span>
</span>
<button
onClick={() => onOpenScreenshot(currentItem)}
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-slate-900/80 border border-slate-800 hover:bg-slate-800 text-slate-300 hover:text-white transition-colors"
>
<Maximize2 className="w-3.5 h-3.5" />
<span>Full Lightbox</span>
</button>
</div>
</div>
{/* Modern Device Shell */}
<div className="relative rounded-2xl p-2 sm:p-3 bg-gradient-to-b from-slate-800/80 via-slate-900/60 to-slate-950 border border-slate-700/60 shadow-2xl shadow-cyan-950/20 group">
{/* Glow ambient background behind the frame */}
<div
className={`absolute -inset-1 rounded-3xl blur-2xl opacity-40 transition-all duration-700 pointer-events-none ${
activeMode === 'partner'
? 'bg-gradient-to-r from-emerald-600/30 via-teal-500/20 to-cyan-500/30'
: 'bg-gradient-to-r from-blue-600/30 via-indigo-500/20 to-purple-500/30'
}`}
/>
{/* Browser Top Bar */}
<div className="relative z-10 flex items-center justify-between px-4 py-2.5 bg-slate-950/90 rounded-t-xl border-b border-slate-800/80">
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded-full bg-rose-500/80 border border-rose-600/40"></div>
<div className="w-3 h-3 rounded-full bg-amber-500/80 border border-amber-600/40"></div>
<div className="w-3 h-3 rounded-full bg-emerald-500/80 border border-emerald-600/40"></div>
</div>
{/* Browser Address Bar */}
<div className="flex-1 max-w-md mx-4">
<div className="flex items-center justify-center gap-2 px-3 py-1 bg-slate-900/80 border border-slate-800 rounded-md text-[11px] text-slate-400 font-mono">
<span className="text-emerald-400">https://</span>
<span className="text-slate-200">
{activeMode === 'partner' ? 'partner.ifri.khongisa.co.za' : 'console.ifri.khongisa.co.za'}
</span>
<span className="text-slate-500">/workspace</span>
</div>
</div>
<div className="text-slate-500 text-xs font-mono hidden sm:block">
{activeMode === 'partner' ? 'PORT 8077' : 'PORT 8076'}
</div>
</div>
{/* Browser Viewport with Image & Interactive Hotspots */}
<div className="relative z-10 overflow-hidden rounded-b-xl bg-slate-950 aspect-[16/10] sm:aspect-[16/9]">
<img
src={currentItem.image}
alt={currentItem.title}
className="w-full h-full object-cover object-top transition-transform duration-500 group-hover:scale-[1.01]"
loading="eager"
/>
{/* Interactive Hotspots Overlay */}
{currentItem.hotspots?.map((hotspot, idx) => (
<div
key={idx}
className="absolute z-20"
style={{ left: `${hotspot.x}%`, top: `${hotspot.y}%` }}
>
<button
onClick={() => setActiveHotspot(activeHotspot === idx ? null : idx)}
onMouseEnter={() => setActiveHotspot(idx)}
onMouseLeave={() => setActiveHotspot(null)}
className="relative flex items-center justify-center w-7 h-7 rounded-full bg-cyan-500/90 text-slate-950 font-bold text-xs shadow-lg shadow-cyan-500/50 hover:scale-125 transition-transform"
aria-label={`Hotspot ${hotspot.title}`}
>
<span className="absolute -inset-1 rounded-full bg-cyan-400 animate-ping opacity-60"></span>
<span>{idx + 1}</span>
</button>
{/* Tooltip Card */}
{activeHotspot === idx && (
<div
className="absolute left-1/2 -translate-x-1/2 mt-2 w-64 p-3 rounded-xl bg-slate-900/95 border border-cyan-500/40 shadow-2xl backdrop-blur-xl text-left z-30 pointer-events-none animate-in fade-in zoom-in-95 duration-150"
style={{ top: '100%' }}
>
<div className="flex items-center gap-1.5 text-cyan-400 font-semibold text-xs mb-1">
<Sparkles className="w-3.5 h-3.5" />
<span>{hotspot.title}</span>
</div>
<p className="text-[11px] text-slate-300 leading-relaxed">
{hotspot.description}
</p>
</div>
)}
</div>
))}
{/* Bottom Bar Info Overlay */}
<div className="absolute bottom-0 inset-x-0 p-4 bg-gradient-to-t from-slate-950 via-slate-950/80 to-transparent flex flex-wrap items-end justify-between gap-3">
<div>
<div className="flex items-center gap-2 mb-1">
<span className={`text-[10px] font-bold uppercase tracking-wider px-2 py-0.5 rounded-full border ${
activeMode === 'partner'
? 'bg-emerald-500/10 text-emerald-400 border-emerald-500/30'
: 'bg-blue-500/10 text-blue-400 border-blue-500/30'
}`}>
{currentItem.platform}
</span>
<span className="text-xs text-slate-400 font-medium">
{currentItem.category}
</span>
</div>
<h3 className="text-sm sm:text-base font-bold text-white tracking-tight">
{currentItem.title}
</h3>
<p className="text-xs text-slate-300 max-w-xl line-clamp-1 sm:line-clamp-none">
{currentItem.subtitle}
</p>
</div>
<button
onClick={() => onOpenScreenshot(currentItem)}
className="px-3.5 py-1.5 rounded-lg bg-white/10 hover:bg-white/20 backdrop-blur-md border border-white/20 text-xs font-semibold text-white transition-all flex items-center gap-1.5 shadow-lg"
>
<span>Explore High-Res</span>
<ChevronRight className="w-3.5 h-3.5" />
</button>
</div>
</div>
</div>
{/* Hotspots Quick Guide */}
<div className="mt-4 grid grid-cols-1 sm:grid-cols-3 gap-3">
{currentItem.hotspots?.map((hotspot, idx) => (
<div
key={idx}
onMouseEnter={() => setActiveHotspot(idx)}
onMouseLeave={() => setActiveHotspot(null)}
className={`p-3 rounded-xl border transition-all cursor-pointer ${
activeHotspot === idx
? 'bg-slate-900 border-cyan-500/60 shadow-lg shadow-cyan-500/10'
: 'bg-slate-950/50 border-slate-800/80 hover:bg-slate-900/60'
}`}
>
<div className="flex items-center gap-2 mb-1">
<span className="flex items-center justify-center w-5 h-5 rounded-full bg-cyan-500/20 text-cyan-400 text-[11px] font-bold border border-cyan-500/30">
{idx + 1}
</span>
<span className="text-xs font-semibold text-white truncate">
{hotspot.title}
</span>
</div>
<p className="text-[11px] text-slate-400 line-clamp-2">
{hotspot.description}
</p>
</div>
))}
</div>
</div>
);
};
@@ -0,0 +1,130 @@
import React from 'react';
import {
ShieldCheck,
Layers,
ExternalLink,
FileText,
Code2,
Cpu,
HardDrive,
Sparkles
} from 'lucide-react';
import { BrandLogo } from '../ui/BrandLogo';
export const Footer: React.FC = () => {
return (
<footer className="bg-slate-950 border-t border-slate-900 pt-16 pb-12 relative overflow-hidden">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-8 mb-12">
{/* Brand Col */}
<div className="lg:col-span-2 space-y-4">
<BrandLogo size="md" variant="full" />
<p className="text-xs text-slate-400 leading-relaxed max-w-sm">
The enterprise post-fund management, performance, and impact governance suite. Bridging institutional funders and field implementing partners with zero uncontrolled disbursements.
</p>
<div className="flex items-center gap-2 pt-2">
<span className="text-[10px] font-mono px-2.5 py-1 rounded-md bg-slate-900 border border-slate-800 text-cyan-400">
SRS v2.0 Specification
</span>
<span className="text-[10px] font-mono px-2.5 py-1 rounded-md bg-slate-900 border border-slate-800 text-emerald-400">
Production Architecture
</span>
</div>
</div>
{/* Col 1: Applications */}
<div className="space-y-3">
<h5 className="text-xs font-bold font-mono uppercase tracking-wider text-white">
Application Portals
</h5>
<ul className="space-y-2 text-xs text-slate-400">
<li>
<a
href="https://console.ifri.khongisa.co.za"
target="_blank"
rel="noreferrer"
className="hover:text-blue-400 transition-colors flex items-center gap-1.5"
>
<span>PFM Console (Port 8076)</span>
<ExternalLink className="w-3 h-3 text-slate-500" />
</a>
</li>
<li>
<a
href="https://partner.ifri.khongisa.co.za"
target="_blank"
rel="noreferrer"
className="hover:text-emerald-400 transition-colors flex items-center gap-1.5"
>
<span>PFM Partner (Port 8077)</span>
<ExternalLink className="w-3 h-3 text-slate-500" />
</a>
</li>
<li>
<a
href="https://api.ifri.khongisa.co.za/scalar/v1"
target="_blank"
rel="noreferrer"
className="hover:text-cyan-400 transition-colors flex items-center gap-1.5"
>
<span>API Scalar Documentation</span>
<ExternalLink className="w-3 h-3 text-slate-500" />
</a>
</li>
<li>
<a href="#interactive-tour" className="hover:text-white transition-colors">
Lifecycle Simulator
</a>
</li>
</ul>
</div>
{/* Col 2: Modules & Capabilities */}
<div className="space-y-3">
<h5 className="text-xs font-bold font-mono uppercase tracking-wider text-white">
Core Modules
</h5>
<ul className="space-y-2 text-xs text-slate-400">
<li>M01 Award Intake & Registration</li>
<li>M02 Contract & Agreement Versioning</li>
<li>M03 Recipient Onboarding & KYC</li>
<li>M04 Project WBS & Gantt Scheduling</li>
<li>M06 Disbursement & Gate Control</li>
<li>M07 Milestone Evidence Vault</li>
</ul>
</div>
{/* Col 3: Architecture */}
<div className="space-y-3">
<h5 className="text-xs font-bold font-mono uppercase tracking-wider text-white">
Technology Core
</h5>
<ul className="space-y-2 text-xs text-slate-400">
<li>.NET 10 (C# 13) Clean DDD</li>
<li>React 19 + TypeScript 5.8</li>
<li>Quartz.NET Dedicated Daemon Host</li>
<li>Garage S3 Pre-Signed Storage</li>
<li>Duende IdentityServer SSO</li>
<li>PostgreSQL 14+ & HybridCache</li>
</ul>
</div>
</div>
{/* Bottom Bar */}
<div className="pt-8 border-t border-slate-900 flex flex-col sm:flex-row items-center justify-between gap-4 text-xs text-slate-500">
<div>
© 2026 Post Fund Management Platform (PFM). All rights reserved.
</div>
<div className="flex items-center gap-4 text-[11px] font-mono">
<span>Security by Duende STS</span>
<span>•</span>
<span>BR-001 to BR-010 Compliant</span>
<span>•</span>
<span>POPIA Safeguards</span>
</div>
</div>
</div>
</footer>
);
};
@@ -0,0 +1,82 @@
import React, { useEffect } from 'react';
import { X, Download, Maximize2, ExternalLink } from 'lucide-react';
import { PosterItem } from '../../data/showcaseData';
interface PosterModalProps {
poster: PosterItem | null;
onClose: () => void;
}
export const PosterModal: React.FC<PosterModalProps> = ({ poster, onClose }) => {
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose();
};
if (poster) {
document.body.style.overflow = 'hidden';
window.addEventListener('keydown', handleKeyDown);
}
return () => {
document.body.style.overflow = 'auto';
window.removeEventListener('keydown', handleKeyDown);
};
}, [poster, onClose]);
if (!poster) return null;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-3 sm:p-6 bg-slate-950/90 backdrop-blur-2xl animate-in fade-in duration-200">
<div
className="relative w-full max-w-4xl max-h-[94vh] flex flex-col rounded-3xl bg-slate-900 border border-slate-700/80 shadow-2xl overflow-hidden"
onClick={(e) => e.stopPropagation()}
>
{/* Top Bar */}
<div className="flex items-center justify-between px-6 py-4 bg-slate-950/90 border-b border-slate-800">
<div className="flex items-center gap-3">
<span className="text-xs font-mono font-bold text-purple-400">
{poster.number}
</span>
<span className="text-slate-600">•</span>
<span className="text-sm font-bold text-white tracking-tight truncate">
{poster.title}
</span>
</div>
<div className="flex items-center gap-2">
<a
href={poster.image}
download={`PFM_Poster_${poster.id}.jpg`}
className="p-2 rounded-xl bg-slate-800 hover:bg-slate-700 text-slate-300 hover:text-white transition-colors"
title="Download High-Res Poster Image"
>
<Download className="w-4 h-4" />
</a>
<button
onClick={onClose}
className="p-2 rounded-xl bg-slate-800 hover:bg-slate-700 text-slate-400 hover:text-white transition-colors"
aria-label="Close modal"
>
<X className="w-5 h-5" />
</button>
</div>
</div>
{/* Poster Image */}
<div className="flex-1 overflow-y-auto bg-slate-950 p-4 sm:p-6 flex items-center justify-center">
<img
src={poster.image}
alt={poster.title}
className="max-h-[75vh] w-auto object-contain rounded-xl shadow-2xl border border-slate-800"
/>
</div>
{/* Bottom Banner */}
<div className="px-6 py-3 bg-slate-950/90 border-t border-slate-800 flex items-center justify-between text-xs text-slate-400">
<span>{poster.coreProposition}</span>
<span className="font-mono text-purple-400">{poster.platform}</span>
</div>
</div>
</div>
);
};
@@ -0,0 +1,92 @@
import React, { useEffect } from 'react';
import { X, ExternalLink, ShieldCheck, FileCheck2, Tag } from 'lucide-react';
import { ScreenshotItem } from '../../data/showcaseData';
interface ScreenshotViewerModalProps {
item: ScreenshotItem | null;
onClose: () => void;
}
export const ScreenshotViewerModal: React.FC<ScreenshotViewerModalProps> = ({ item, onClose }) => {
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose();
};
if (item) {
document.body.style.overflow = 'hidden';
window.addEventListener('keydown', handleKeyDown);
}
return () => {
document.body.style.overflow = 'auto';
window.removeEventListener('keydown', handleKeyDown);
};
}, [item, onClose]);
if (!item) return null;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-3 sm:p-6 bg-slate-950/85 backdrop-blur-2xl animate-in fade-in duration-200">
<div
className="relative w-full max-w-5xl max-h-[92vh] flex flex-col rounded-3xl bg-slate-900 border border-slate-700/80 shadow-2xl overflow-hidden"
onClick={(e) => e.stopPropagation()}
>
{/* Top Header */}
<div className="flex items-center justify-between px-6 py-4 bg-slate-950/90 border-b border-slate-800">
<div className="flex items-center gap-3">
<span className={`text-[10px] font-bold uppercase tracking-wider px-2.5 py-1 rounded-full border ${
item.platform === 'PFM Console'
? 'bg-blue-500/20 text-blue-300 border-blue-500/30'
: 'bg-emerald-500/20 text-emerald-300 border-emerald-500/30'
}`}>
{item.platform}
</span>
<span className="text-sm font-bold text-white tracking-tight">
{item.title}
</span>
</div>
<button
onClick={onClose}
className="p-2 rounded-xl bg-slate-800 hover:bg-slate-700 text-slate-400 hover:text-white transition-colors"
aria-label="Close modal"
>
<X className="w-5 h-5" />
</button>
</div>
{/* Image Content */}
<div className="flex-1 overflow-y-auto bg-slate-950 p-2 sm:p-6 flex flex-col items-center justify-center">
<img
src={item.image}
alt={item.title}
className="w-full max-h-[65vh] object-contain rounded-xl border border-slate-800/80 shadow-2xl"
/>
</div>
{/* Footer Details */}
<div className="px-6 py-4 bg-slate-950/90 border-t border-slate-800 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3 text-xs">
<div className="space-y-1 max-w-2xl">
<div className="text-slate-400 font-medium">
{item.subtitle}
</div>
<p className="text-slate-300 text-xs">
{item.description}
</p>
</div>
<div className="flex flex-wrap gap-1.5 flex-shrink-0">
{item.tags.map((tag) => (
<span
key={tag}
className="text-[10px] font-mono px-2 py-0.5 rounded bg-slate-800 text-slate-300"
>
#{tag}
</span>
))}
</div>
</div>
</div>
</div>
);
};
@@ -0,0 +1,212 @@
import React, { useState } from 'react';
import {
FileCheck2,
UploadCloud,
MessageSquare,
KeyRound,
CheckCircle,
Clock,
ShieldAlert,
ExternalLink,
Maximize2,
FileSignature
} from 'lucide-react';
import { SCREENSHOTS, ScreenshotItem } from '../../data/showcaseData';
interface PartnerShowcaseProps {
onOpenScreenshot: (item: ScreenshotItem) => void;
}
export const PartnerShowcase: React.FC<PartnerShowcaseProps> = ({ onOpenScreenshot }) => {
const partnerScreenshots = SCREENSHOTS.filter((s) => s.platform === 'PFM Partner');
const [selectedCategory, setSelectedCategory] = useState<string>('All');
const categories = ['All', 'Dashboard', 'Evidence & Proof', 'Communications', 'Onboarding'];
const filteredScreens = selectedCategory === 'All'
? partnerScreenshots
: partnerScreenshots.filter((s) => s.category === selectedCategory);
return (
<section id="partner-section" className="py-24 bg-slate-950 relative border-t border-slate-900 overflow-hidden">
{/* Background ambient glow */}
<div className="absolute top-1/4 right-0 w-96 h-96 bg-emerald-600/10 blur-[140px] rounded-full pointer-events-none -z-10" />
<div className="absolute bottom-10 left-0 w-80 h-80 bg-teal-600/10 blur-[130px] rounded-full pointer-events-none -z-10" />
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Section Header */}
<div className="flex flex-col md:flex-row md:items-end justify-between gap-6 mb-16">
<div className="max-w-3xl">
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-emerald-500/10 border border-emerald-500/30 text-emerald-400 text-xs font-semibold mb-4">
<FileCheck2 className="w-3.5 h-3.5" />
<span>Grantee & Implementing Partner Suite</span>
</div>
<h2 className="text-3xl sm:text-4xl font-extrabold text-white tracking-tight leading-tight">
PFM Partner •{' '}
<span className="text-transparent bg-clip-text bg-gradient-to-r from-emerald-400 via-teal-400 to-cyan-400">
Field Delivery & Evidence Hub
</span>
</h2>
<p className="mt-3 text-base text-slate-300 leading-relaxed">
Empower implementing teams to manage sanctioned grant agreements, execute work-breakdown activities, and upload tamper-evident proofs directly against funding covenants.
</p>
</div>
<div className="flex items-center gap-3">
<a
href="https://partner.ifri.khongisa.co.za"
target="_blank"
rel="noreferrer"
className="px-4 py-2 rounded-xl bg-emerald-600/20 hover:bg-emerald-600/30 border border-emerald-500/40 text-emerald-300 hover:text-white text-xs font-semibold transition-all flex items-center gap-2 shadow-sm"
>
<span>Launch Live Partner Portal</span>
<ExternalLink className="w-3.5 h-3.5" />
</a>
</div>
</div>
{/* 3 Core Partner Pillars */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-16">
{/* Pillar 1 */}
<div className="p-6 rounded-2xl bg-slate-900/60 border border-slate-800/80 hover:border-emerald-500/40 transition-all shadow-xl group">
<div className="w-12 h-12 rounded-xl bg-emerald-500/10 border border-emerald-500/20 flex items-center justify-center text-emerald-400 mb-5 group-hover:scale-110 transition-transform">
<UploadCloud className="w-6 h-6" />
</div>
<div className="flex items-center gap-2 mb-2">
<span className="text-xs font-mono font-semibold text-emerald-400">PILLAR 01</span>
<span className="text-slate-600">•</span>
<span className="text-xs text-slate-400">Evidence Vault</span>
</div>
<h3 className="text-lg font-bold text-white mb-2">
Granular Activity Proof Uploads
</h3>
<p className="text-xs text-slate-300 leading-relaxed mb-4">
Attach statutory documents, invoices, and geotagged photographs directly to milestones with pre-signed S3 storage and immediate SHA verification.
</p>
<div className="pt-4 border-t border-slate-800/80 flex items-center justify-between text-[11px] text-slate-400">
<span className="font-mono text-emerald-400 font-semibold">100% Traceable</span>
<span>Garage S3 Vault</span>
</div>
</div>
{/* Pillar 2 */}
<div className="p-6 rounded-2xl bg-slate-900/60 border border-slate-800/80 hover:border-teal-500/40 transition-all shadow-xl group">
<div className="w-12 h-12 rounded-xl bg-teal-500/10 border border-teal-500/20 flex items-center justify-center text-teal-400 mb-5 group-hover:scale-110 transition-transform">
<MessageSquare className="w-6 h-6" />
</div>
<div className="flex items-center gap-2 mb-2">
<span className="text-xs font-mono font-semibold text-teal-400">PILLAR 02</span>
<span className="text-slate-600">•</span>
<span className="text-xs text-slate-400">Direct Comms</span>
</div>
<h3 className="text-lg font-bold text-white mb-2">
Direct Programme Officer Comms
</h3>
<p className="text-xs text-slate-300 leading-relaxed mb-4">
Eliminate lost email chains. Built-in contextual messaging links every query, revision request, and milestone note directly into an auditable project thread.
</p>
<div className="pt-4 border-t border-slate-800/80 flex items-center justify-between text-[11px] text-slate-400">
<span className="font-mono text-teal-400 font-semibold">Direct Inbox</span>
<span>Audit Logged</span>
</div>
</div>
{/* Pillar 3 */}
<div className="p-6 rounded-2xl bg-slate-900/60 border border-slate-800/80 hover:border-cyan-500/40 transition-all shadow-xl group">
<div className="w-12 h-12 rounded-xl bg-cyan-500/10 border border-cyan-500/20 flex items-center justify-center text-cyan-400 mb-5 group-hover:scale-110 transition-transform">
<KeyRound className="w-6 h-6" />
</div>
<div className="flex items-center gap-2 mb-2">
<span className="text-xs font-mono font-semibold text-cyan-400">PILLAR 03</span>
<span className="text-slate-600">•</span>
<span className="text-xs text-slate-400">Rapid Mobilization</span>
</div>
<h3 className="text-lg font-bold text-white mb-2">
Statutory SSO Onboarding & Tranches
</h3>
<p className="text-xs text-slate-300 leading-relaxed mb-4">
Single-token statutory onboarding invitations allow instant portal mobilization, KYC compliance verification, and clear visibility into covenant-tied tranches.
</p>
<div className="pt-4 border-t border-slate-800/80 flex items-center justify-between text-[11px] text-slate-400">
<span className="font-mono text-cyan-400 font-semibold">Zero Friction</span>
<span>Duende STS Auth</span>
</div>
</div>
</div>
{/* Screenshot Category Filter & Grid */}
<div className="mb-6 flex flex-wrap items-center justify-between gap-4">
<div className="flex items-center gap-2 p-1 rounded-xl bg-slate-900 border border-slate-800">
{categories.map((cat) => (
<button
key={cat}
onClick={() => setSelectedCategory(cat)}
className={`px-3 py-1.5 rounded-lg text-xs font-semibold transition-all ${
selectedCategory === cat
? 'bg-emerald-600 text-white shadow-sm'
: 'text-slate-400 hover:text-slate-200'
}`}
>
{cat}
</button>
))}
</div>
<span className="text-xs text-slate-400 font-mono">
Showing {filteredScreens.length} Partner Views
</span>
</div>
{/* Screenshot Cards Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{filteredScreens.map((screen) => (
<div
key={screen.id}
onClick={() => onOpenScreenshot(screen)}
className="group cursor-pointer rounded-2xl bg-slate-900/60 border border-slate-800 hover:border-emerald-500/50 overflow-hidden shadow-xl transition-all duration-300 hover:-translate-y-1"
>
<div className="relative aspect-[16/10] overflow-hidden bg-slate-950">
<img
src={screen.image}
alt={screen.title}
className="w-full h-full object-cover object-top group-hover:scale-105 transition-transform duration-500"
loading="lazy"
/>
<div className="absolute inset-0 bg-gradient-to-t from-slate-950 via-slate-950/20 to-transparent opacity-80 group-hover:opacity-40 transition-opacity" />
<div className="absolute top-3 right-3 p-2 rounded-lg bg-slate-950/80 backdrop-blur-md border border-slate-700/80 text-slate-300 opacity-0 group-hover:opacity-100 transition-opacity">
<Maximize2 className="w-4 h-4" />
</div>
<div className="absolute bottom-3 left-3">
<span className="text-[10px] uppercase font-bold tracking-wider px-2.5 py-1 rounded-full bg-emerald-500/20 text-emerald-300 border border-emerald-500/30 backdrop-blur-md">
{screen.category}
</span>
</div>
</div>
<div className="p-5">
<h4 className="text-base font-bold text-white mb-1 group-hover:text-emerald-300 transition-colors">
{screen.title}
</h4>
<p className="text-xs text-slate-300 leading-relaxed mb-4 line-clamp-2">
{screen.description}
</p>
<div className="flex flex-wrap gap-1.5">
{screen.tags.map((tag) => (
<span
key={tag}
className="text-[10px] font-mono px-2 py-0.5 rounded bg-slate-800/80 text-slate-400"
>
#{tag}
</span>
))}
</div>
</div>
</div>
))}
</div>
</div>
</section>
);
};
@@ -0,0 +1,273 @@
import React, { useState } from 'react';
import {
Play,
RotateCcw,
CheckCircle2,
ShieldCheck,
FileCheck2,
Lock,
DollarSign,
ArrowRight,
Sparkles,
CheckCircle,
FileText,
Clock,
Layers
} from 'lucide-react';
import confetti from 'canvas-confetti';
import { SIMULATION_STEPS } from '../../data/showcaseData';
export const LifecycleSimulator: React.FC = () => {
const [currentStepIndex, setCurrentStepIndex] = useState<number>(0);
const [completedSteps, setCompletedSteps] = useState<number[]>([0]);
const activeStep = SIMULATION_STEPS[currentStepIndex];
const handleNextStep = () => {
if (currentStepIndex < SIMULATION_STEPS.length - 1) {
const nextIndex = currentStepIndex + 1;
setCurrentStepIndex(nextIndex);
if (!completedSteps.includes(nextIndex)) {
setCompletedSteps([...completedSteps, nextIndex]);
}
// If reaching the final step (Disbursement Release), launch celebratory confetti!
if (nextIndex === SIMULATION_STEPS.length - 1) {
confetti({
particleCount: 100,
spread: 70,
origin: { y: 0.6 },
colors: ['#06b6d4', '#10b981', '#3b82f6', '#8b5cf6'],
});
}
}
};
const handleReset = () => {
setCurrentStepIndex(0);
setCompletedSteps([0]);
};
return (
<section id="interactive-tour" className="py-24 bg-slate-950 relative border-t border-slate-900 overflow-hidden">
{/* Background ambient lighting */}
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[800px] h-[500px] bg-gradient-to-r from-cyan-600/10 via-indigo-600/10 to-emerald-600/10 blur-[150px] rounded-full pointer-events-none -z-10" />
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Header */}
<div className="text-center max-w-3xl mx-auto mb-16">
<div className="inline-flex items-center gap-2 px-3.5 py-1 rounded-full bg-slate-900 border border-slate-800 text-cyan-400 text-xs font-semibold mb-4">
<Sparkles className="w-3.5 h-3.5" />
<span>Interactive Lifecycle Simulator</span>
</div>
<h2 className="text-3xl sm:text-4xl font-extrabold text-white tracking-tight leading-tight">
Experience the Complete{' '}
<span className="text-transparent bg-clip-text bg-gradient-to-r from-cyan-400 via-blue-400 to-emerald-400">
Fiduciary Lifecycle
</span>
</h2>
<p className="mt-3 text-base text-slate-300 leading-relaxed">
Follow a R 1,000,000 sanctioned award across both PFM Console and PFM Partner. See how business rules BR-001 through BR-010 guarantee zero uncontrolled capital release.
</p>
</div>
{/* Stepper Progress Bar */}
<div className="relative mb-12">
<div className="hidden md:block absolute top-1/2 left-0 right-0 h-1 bg-slate-800 -translate-y-1/2 z-0">
<div
className="h-full bg-gradient-to-r from-cyan-500 via-blue-500 to-emerald-500 transition-all duration-500"
style={{ width: `${(currentStepIndex / (SIMULATION_STEPS.length - 1)) * 100}%` }}
/>
</div>
<div className="relative z-10 flex flex-col md:flex-row items-stretch md:items-center justify-between gap-4">
{SIMULATION_STEPS.map((step, idx) => {
const isCurrent = currentStepIndex === idx;
const isCompleted = completedSteps.includes(idx);
return (
<button
key={step.id}
onClick={() => {
setCurrentStepIndex(idx);
if (!completedSteps.includes(idx)) {
setCompletedSteps([...completedSteps, idx]);
}
}}
className={`flex md:flex-col items-center gap-3 p-3 md:p-2 rounded-xl transition-all text-left md:text-center ${
isCurrent
? 'bg-slate-900 md:bg-transparent scale-105'
: 'opacity-70 hover:opacity-100'
}`}
>
<div
className={`flex items-center justify-center w-10 h-10 rounded-full font-bold text-xs transition-all shadow-lg ${
isCurrent
? 'bg-gradient-to-r from-cyan-500 to-blue-600 text-white ring-4 ring-cyan-500/30'
: isCompleted
? 'bg-emerald-600 text-white'
: 'bg-slate-800 text-slate-400 border border-slate-700'
}`}
>
{isCompleted && !isCurrent ? (
<CheckCircle2 className="w-5 h-5" />
) : (
idx + 1
)}
</div>
<div>
<div className="text-[11px] font-mono font-semibold text-slate-400 uppercase">
{step.step}
</div>
<div className="text-xs sm:text-sm font-bold text-white whitespace-nowrap">
{step.title}
</div>
</div>
</button>
);
})}
</div>
</div>
{/* Step Card Details & Interactive Console View */}
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 items-center bg-slate-900/60 rounded-3xl p-6 sm:p-8 border border-slate-800/80 shadow-2xl backdrop-blur-xl">
{/* Left Column: Context & Narrative */}
<div className="lg:col-span-5 space-y-6">
<div className="flex items-center gap-2">
<span className={`text-xs font-semibold px-3 py-1 rounded-full border ${activeStep.badgeColor}`}>
{activeStep.actor}
</span>
<span className="text-xs font-mono text-slate-400">
Phase {currentStepIndex + 1} of 5
</span>
</div>
<div>
<h3 className="text-2xl font-bold text-white mb-2">
{activeStep.title}
</h3>
<p className="text-sm text-slate-300 leading-relaxed">
{activeStep.summary}
</p>
</div>
<div className="p-4 rounded-xl bg-slate-950/80 border border-slate-800 space-y-2">
<div className="text-xs font-semibold text-cyan-400 flex items-center gap-1.5">
<ShieldCheck className="w-4 h-4" />
<span>Statutory Rule Enforcement</span>
</div>
<p className="text-xs text-slate-300">
{activeStep.details}
</p>
</div>
<div className="p-4 rounded-xl bg-blue-950/20 border border-blue-900/40 text-xs text-blue-200 font-mono">
<span className="text-slate-400">System Trace:</span> {activeStep.systemAction}
</div>
{/* Stepper Buttons */}
<div className="flex items-center gap-3 pt-4">
{currentStepIndex < SIMULATION_STEPS.length - 1 ? (
<button
onClick={handleNextStep}
className="px-5 py-2.5 rounded-xl bg-gradient-to-r from-cyan-500 to-blue-600 hover:from-cyan-400 hover:to-blue-500 text-white font-semibold text-xs transition-all flex items-center gap-2 shadow-lg shadow-cyan-500/25"
>
<span>Proceed to Next Step</span>
<ArrowRight className="w-4 h-4" />
</button>
) : (
<button
onClick={handleReset}
className="px-5 py-2.5 rounded-xl bg-emerald-600 hover:bg-emerald-500 text-white font-semibold text-xs transition-all flex items-center gap-2 shadow-lg shadow-emerald-500/25"
>
<RotateCcw className="w-4 h-4" />
<span>Restart Simulation</span>
</button>
)}
<button
onClick={handleReset}
className="px-3.5 py-2.5 rounded-xl bg-slate-900 hover:bg-slate-800 text-slate-400 hover:text-slate-200 border border-slate-800 text-xs font-semibold transition-all flex items-center gap-1.5"
>
<RotateCcw className="w-3.5 h-3.5" />
<span>Reset</span>
</button>
</div>
</div>
{/* Right Column: Visual Telemetry Display */}
<div className="lg:col-span-7">
<div className="rounded-2xl bg-slate-950 border border-slate-800 p-5 font-mono text-xs shadow-inner">
{/* Terminal-like Header */}
<div className="flex items-center justify-between pb-3 mb-4 border-b border-slate-800">
<div className="flex items-center gap-2">
<div className="w-2.5 h-2.5 rounded-full bg-rose-500"></div>
<div className="w-2.5 h-2.5 rounded-full bg-amber-500"></div>
<div className="w-2.5 h-2.5 rounded-full bg-emerald-500"></div>
<span className="ml-2 text-slate-400 text-[11px]">pfm-runtime-telemetry.log</span>
</div>
<div className="text-emerald-400 text-[11px] flex items-center gap-1">
<span className="w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse"></span>
<span>SYNCED</span>
</div>
</div>
{/* Dynamic State Telemetry Block */}
<div className="space-y-3">
<div className="flex items-center justify-between text-slate-400">
<span>AWARD_ID:</span>
<span className="text-cyan-400">PFM-2026-AWD-0091</span>
</div>
<div className="flex items-center justify-between text-slate-400">
<span>SANCTIONED_AMOUNT:</span>
<span className="text-white font-bold">ZAR 1,000,000.00</span>
</div>
<div className="flex items-center justify-between text-slate-400">
<span>DISBURSEMENT_GATE:</span>
<span className={currentStepIndex >= 4 ? 'text-emerald-400 font-bold' : 'text-amber-400 font-bold'}>
{currentStepIndex >= 4 ? 'CLEARED (BR-003 ENFORCED)' : 'LOCKED (PENDING EVIDENCE)'}
</span>
</div>
<div className="flex items-center justify-between text-slate-400">
<span>GARAGE_S3_EVIDENCE_PROOFS:</span>
<span className={currentStepIndex >= 2 ? 'text-emerald-400' : 'text-slate-600'}>
{currentStepIndex >= 2 ? '2 VERIFIED SHA-256 ARTIFACTS' : '0 UPLOADED'}
</span>
</div>
<div className="flex items-center justify-between text-slate-400">
<span>RECIPIENT_KYC_STATUS:</span>
<span className={currentStepIndex >= 1 ? 'text-emerald-400' : 'text-slate-600'}>
{currentStepIndex >= 1 ? 'VERIFIED (SSO AUTHENTICATED)' : 'AWAITING INVITATION ACCEPTANCE'}
</span>
</div>
<div className="flex items-center justify-between text-slate-400">
<span>DEPLOYED_TRANCHE:</span>
<span className={currentStepIndex >= 4 ? 'text-emerald-400 font-bold' : 'text-slate-500'}>
{currentStepIndex >= 4 ? 'ZAR 350,000.00 (TRANCHE 1 RELEASED)' : 'ZAR 0.00 (EXPOSURE MITIGATED)'}
</span>
</div>
</div>
{/* Interactive Visual Status Bar */}
<div className="mt-6 pt-4 border-t border-slate-800">
<div className="flex items-center justify-between text-[11px] text-slate-400 mb-2">
<span>FIDUCIARY INTEGRITY INDEX:</span>
<span className="text-cyan-400 font-bold">
{Math.min(100, (currentStepIndex + 1) * 20)}%
</span>
</div>
<div className="w-full h-2 rounded-full bg-slate-800 overflow-hidden">
<div
className="h-full bg-gradient-to-r from-cyan-500 via-blue-500 to-emerald-500 transition-all duration-500"
style={{ width: `${Math.min(100, (currentStepIndex + 1) * 20)}%` }}
/>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
);
};
@@ -0,0 +1,161 @@
import React, { useId } from 'react';
export interface BrandLogoProps {
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | number;
variant?: 'icon-only' | 'full';
showGlow?: boolean;
className?: string;
animateOnHover?: boolean;
}
const sizeMap = {
xs: 24,
sm: 32,
md: 44,
lg: 56,
xl: 72,
};
export const BrandLogo: React.FC<BrandLogoProps> = ({
size = 'lg',
variant = 'full',
showGlow = true,
className = '',
animateOnHover = true,
}) => {
const uniqueId = useId().replace(/:/g, '');
const pixelSize = typeof size === 'number' ? size : sizeMap[size] || 56;
const glowId = `pfm-ambient-${uniqueId}`;
const topGradId = `pfm-top-${uniqueId}`;
const leftGradId = `pfm-left-${uniqueId}`;
const rightGradId = `pfm-right-${uniqueId}`;
const coreGradId = `pfm-core-${uniqueId}`;
const filterId = `pfm-filter-${uniqueId}`;
return (
<div
className={`inline-flex items-center gap-3.5 select-none ${
animateOnHover ? 'group cursor-pointer' : ''
} ${className}`}
>
<div
style={{ width: pixelSize, height: pixelSize }}
className={`relative shrink-0 flex items-center justify-center transition-transform duration-300 ${
animateOnHover ? 'group-hover:scale-105' : ''
}`}
>
{showGlow && (
<div
className="absolute inset-0 rounded-2xl bg-blue-500/25 blur-lg pointer-events-none transition-opacity duration-300 group-hover:opacity-100 opacity-60"
/>
)}
<svg
viewBox="0 0 64 64"
width={pixelSize}
height={pixelSize}
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="relative z-10 w-full h-full drop-shadow-md"
>
<defs>
<radialGradient id={glowId} cx="50%" cy="50%" r="50%">
<stop offset="0%" stopColor="#38bdf8" stopOpacity="0.4" />
<stop offset="70%" stopColor="#2563eb" stopOpacity="0.12" />
<stop offset="100%" stopColor="#1e1b4b" stopOpacity="0" />
</radialGradient>
<linearGradient id={topGradId} x1="12" y1="8" x2="52" y2="26" gradientUnits="userSpaceOnUse">
<stop offset="0%" stopColor="#a5b4fc" />
<stop offset="35%" stopColor="#6366f1" />
<stop offset="100%" stopColor="#4338ca" />
</linearGradient>
<linearGradient id={leftGradId} x1="10" y1="22" x2="30" y2="56" gradientUnits="userSpaceOnUse">
<stop offset="0%" stopColor="#38bdf8" />
<stop offset="45%" stopColor="#2563eb" />
<stop offset="100%" stopColor="#1e40af" />
</linearGradient>
<linearGradient id={rightGradId} x1="34" y1="28" x2="54" y2="56" gradientUnits="userSpaceOnUse">
<stop offset="0%" stopColor="#22d3ee" />
<stop offset="50%" stopColor="#06b6d4" />
<stop offset="100%" stopColor="#10b981" />
</linearGradient>
<linearGradient id={coreGradId} x1="27" y1="27" x2="37" y2="37" gradientUnits="userSpaceOnUse">
<stop offset="0%" stopColor="#ffffff" />
<stop offset="40%" stopColor="#e0f2fe" />
<stop offset="100%" stopColor="#38bdf8" />
</linearGradient>
<filter id={filterId} x="-20%" y="-20%" width="140%" height="140%">
<feGaussianBlur stdDeviation="1.5" result="blur" />
<feComposite in="SourceGraphic" in2="blur" operator="over" />
</filter>
</defs>
{/* Deep Contrast Foundation Disk */}
<circle cx="32" cy="32" r="31" fill="#090d16" />
<circle cx="32" cy="32" r="30" stroke="#1e293b" strokeWidth="1" />
<circle cx="32" cy="32" r="28" fill={`url(#${glowId})`} />
{/* Top Facet */}
<path
d="M32 9L51 20L32 31L13 20L32 9Z"
fill={`url(#${topGradId})`}
fillOpacity="0.95"
stroke="#c7d2fe"
strokeWidth="0.75"
strokeLinejoin="round"
/>
{/* Left Facet */}
<path
d="M13 20L32 31V53L13 42V20Z"
fill={`url(#${leftGradId})`}
fillOpacity="0.95"
stroke="#bae6fd"
strokeWidth="0.75"
strokeLinejoin="round"
/>
{/* Right Facet */}
<path
d="M32 31L51 20V42L32 53V31Z"
fill={`url(#${rightGradId})`}
fillOpacity="0.95"
stroke="#99f6e4"
strokeWidth="0.75"
strokeLinejoin="round"
/>
{/* Inner Central Star */}
<path
d="M32 23L38 31L32 39L26 31L32 23Z"
fill={`url(#${coreGradId})`}
filter={`url(#${filterId})`}
opacity="0.85"
/>
</svg>
</div>
{variant === 'full' && (
<div className="flex flex-col text-left">
<div className="flex items-center gap-2">
<span className="font-extrabold text-xl sm:text-2xl tracking-tight text-white font-sans">
Post Fund Management
</span>
<span className="text-[10px] uppercase font-bold tracking-wider px-2 py-0.5 rounded-full bg-cyan-500/15 text-cyan-400 border border-cyan-500/30">
PFM Suite
</span>
</div>
<span className="text-xs text-slate-400 font-medium tracking-wide">
Performance, Governance & Impact Platform
</span>
</div>
)}
</div>
);
};
@@ -0,0 +1,531 @@
export interface PosterItem {
id: number;
number: string;
badge: string;
title: string;
subtitle: string;
platform: 'PFM Console' | 'PFM Partner';
image: string;
coreProposition: string;
metrics: { label: string; value: string; detail: string }[];
keyHighlights: { number: string; title: string; description: string }[];
targetMarket: string[];
}
export interface ScreenshotItem {
id: string;
title: string;
subtitle: string;
platform: 'PFM Console' | 'PFM Partner';
category: 'Dashboard' | 'WBS & Gantt' | 'Governance & Gate' | 'Evidence & Proof' | 'Communications' | 'Onboarding';
image: string;
description: string;
hotspots?: { x: number; y: number; title: string; description: string }[];
tags: string[];
}
export interface FeatureHighlight {
id: string;
title: string;
subtitle: string;
description: string;
iconName: string;
platform: 'console' | 'partner' | 'both';
metric: string;
metricLabel: string;
bulletPoints: string[];
}
export const SHOWCASE_STATS = [
{
value: 'R 21.0M+',
label: 'Statutory Capital Governed',
detail: 'Under active fiduciary management',
highlight: 'text-indigo-400',
},
{
value: '0% Exposure',
label: 'Unverified Disbursements',
detail: 'Strict statutory gate enforcement',
highlight: 'text-emerald-400',
},
{
value: '100% Traceable',
label: 'Milestone Evidence Trails',
detail: 'Tamper-evident S3 object vault',
highlight: 'text-cyan-400',
},
{
value: '1.2k+ Lives',
label: 'M&E Verified Beneficiaries',
detail: 'POPIA-compliant direct telemetry',
highlight: 'text-purple-400',
},
];
export const CAMPAIGN_POSTERS: PosterItem[] = [
{
id: 1,
number: '01 / 06',
badge: 'PARTNER COLLABORATION',
title: 'Implementing Partner Suite',
subtitle: 'Real-Time Milestone Oversight, Direct Programme Officer Comms & Live Capital Analytics',
platform: 'PFM Partner',
image: '/assets/marketing/1.jpeg',
coreProposition: 'Connect field-level contractors and recipient NGOs directly with funding administrators for seamless project execution.',
metrics: [
{ label: 'Access Control', value: 'SSO Ready', detail: 'Statutory Invitation Verified' },
{ label: 'Comms Channel', value: 'Direct Inbox', detail: 'Programme Officer Messaging' },
{ label: 'Audited Proofs', value: '100% Traceable', detail: 'Substantiating Indicator Outcomes' },
{ label: 'Tranche Release', value: 'Covenant-Tied', detail: 'Automated Precedent Checks' },
],
keyHighlights: [
{
number: '01',
title: 'Contract Clarity',
description: 'View approved grant capital, governing portfolios, linked projects, and countersigned PDF agreements in one unified workspace.',
},
{
number: '02',
title: 'Proof Workflows',
description: 'Attach statutory documents, financial invoices, and field receipts directly to contractual milestones with real-time review badges.',
},
{
number: '03',
title: 'Risk Visibility',
description: 'Automated overdue milestone indicators and sector compliance tags keep field coordinators ahead of audit covenants.',
},
],
targetMarket: ['Fieldwork Coordinators', 'Grant Recipients', 'Engineering & Infrastructure Contractors', 'Community Development NGOs'],
},
{
id: 2,
number: '02 / 06',
badge: 'EVIDENCE & GOVERNANCE',
title: 'Field-Level Activity Oversight & Evidence Verification',
subtitle: 'From Sanctioned Grant Award to Verified Field Delivery: Turn Contractual Milestones into Auditable Proof',
platform: 'PFM Partner',
image: '/assets/marketing/2.jpeg',
coreProposition: 'Empower implementing teams to manage sanctioned grant agreements, execute work-breakdown activities, and upload tamper-evident proofs.',
metrics: [
{ label: 'Sanctioned Capital', value: 'R 1 000 000', detail: 'Binding Grant Agreement v1' },
{ label: 'Fulfillment Pace', value: '33% Verified', detail: '1 / 1 Statutory Gate Signed' },
{ label: 'Evidence Trail', value: '2 Proofs', detail: 'Timestamped & Status-Tracked' },
{ label: 'Contract Execution', value: 'Countersigned', detail: 'Instant PDF Agreement Access' },
],
keyHighlights: [
{
number: '01',
title: 'Sanctioned Award Alignment',
description: 'Access binding legal agreements, project scopes, and tranche disbursements mapped directly to approved award letters.',
},
{
number: '02',
title: 'Granular Activity Evidence',
description: 'Upload verifiable proofs against work breakdown items with immediate SHA hashing and tamper-evident storage.',
},
{
number: '03',
title: 'Proactive Compliance Alerts',
description: 'Keep projects on schedule with automated milestone reminders, overdue notifications, and SLA warnings.',
},
],
targetMarket: ['Field Coordinators', 'Financial Controllers', 'Compliance Officers', 'Infrastructure Engineers'],
},
{
id: 3,
number: '03 / 06',
badge: 'ONBOARDING & VELOCITY',
title: 'Statutory Onboarding, SSO Access & Live Portfolio Analytics',
subtitle: 'Zero-Friction Partner Mobilization & Transparency',
platform: 'PFM Partner',
image: '/assets/marketing/3.jpeg',
coreProposition: 'Accelerate post-award mobilization from statutory invitation approval to authenticated portal access with live tranche visibility.',
metrics: [
{ label: 'Access Control', value: 'SSO Ready', detail: 'Institutional Identity Gate' },
{ label: 'Comms Channel', value: 'Direct Inbox', detail: 'Direct Funder-Partner Chat' },
{ label: 'Audited Proofs', value: '100% Traceable', detail: 'Zero Unsubstantiated Claims' },
{ label: 'Tranche Release', value: 'Covenant-Tied', detail: 'Milestone-Gated Releases' },
],
keyHighlights: [
{
number: '01',
title: 'Rapid Mobilization',
description: 'Formal onboarding invitations grant immediate portal access to complete KYC compliance, review contracts, and start delivery.',
},
{
number: '02',
title: 'Capital Transparency',
description: 'Eliminate payment guesswork with transparent tranche release indicators tied directly to verified milestone deliverables.',
},
{
number: '03',
title: 'Strict Multi-Tenant Isolation',
description: 'Every partner organization operates in an encrypted tenant boundary with immutable audit logs protecting sensitive data.',
},
],
targetMarket: ['Social Impact Enterprises', 'Research Consortia', 'Municipal Agencies', 'Non-Profit Finance Leads'],
},
{
id: 4,
number: '04 / 06',
badge: 'ENTERPRISE SECURITY & SSO',
title: 'Enterprise Security & Governance Architecture',
subtitle: 'Enterprise SSO Identity Management, Encrypted Multi-Tenant Isolation & Tamper-Evident Audit Trails',
platform: 'PFM Console',
image: '/assets/marketing/4.jpg',
coreProposition: 'The foundational security infrastructure ensuring institutional funders and government development portfolios remain unbreachable.',
metrics: [
{ label: 'Architecture', value: 'Clean DDD', detail: '.NET 10 + React 19' },
{ label: 'Storage Vault', value: 'Garage S3', detail: 'Pre-Signed Object Security' },
{ label: 'Identity Gate', value: 'Duende STS', detail: 'OIDC / OAuth 2.0 PKCE' },
{ label: 'Scheduler', value: 'Quartz.NET', detail: 'Dedicated Daemon Host' },
],
keyHighlights: [
{
number: '01',
title: 'Institutional-Grade Identity',
description: 'Integrated with Skoruba Duende IdentityServer for multi-factor SSO, role-based access control, and granular tenant claims.',
},
{
number: '02',
title: 'Encrypted Multi-Tenancy',
description: 'Strict database tenant isolation with automatic global query filters, ensuring complete data sovereignty across portfolios.',
},
{
number: '03',
title: 'Immutable Decision Logs',
description: 'Every status transition, budget adjustment, evidence review, and disbursement approval writes an auditable record.',
},
],
targetMarket: ['System Architects', 'CISO & InfoSec Teams', 'Compliance Auditors', 'Government Portfolio IT Leads'],
},
{
id: 5,
number: '05 / 06',
badge: 'EXECUTIVE COMMAND',
title: 'Executive Portfolio Command & Fiduciary Governance',
subtitle: 'Real-Time Fiduciary Command Across Every Programme & Sector: Total Portfolio Visibility, Zero Unverified Disbursements',
platform: 'PFM Console',
image: '/assets/marketing/5.jpg',
coreProposition: 'Equip executive funders, investment committees, and development agencies with live capital deployment telemetry and statutory gate checks.',
metrics: [
{ label: 'Portfolio Value', value: 'R 21.0m', detail: 'Statutory Total Commitments' },
{ label: 'Budget At Risk', value: 'R 0 Exposure', detail: '100% Statutory Gate Requirement' },
{ label: 'Delivery Pace', value: '67% On Track', detail: 'Milestones Verified Index' },
{ label: 'Verified Impact', value: '1.2k Lives', detail: 'M&E Verified Direct Beneficiaries' },
],
keyHighlights: [
{
number: '01',
title: 'Multi-Portfolio Capital Deployment',
description: 'Filter commitments, released tranches, and active awards dynamically by programme, sector, and financial period.',
},
{
number: '02',
title: '100% Statutory Gate Enforcement',
description: 'Enforce mandatory governance gates before capital release—automatically flagging at-risk budgets and unverified deliverables.',
},
{
number: '03',
title: 'M&E Impact Roll-Up',
description: 'Aggregate project-level results directly into executive impact metrics, connecting every rand disbursed to verified human outcomes.',
},
],
targetMarket: ['Institutional Funders', 'Science Councils', 'DFIs', 'Chief Financial Officers', 'Portfolio Investment Committees'],
},
{
id: 6,
number: '06 / 06',
badge: 'WBS & SCHEDULE CONTROL',
title: 'Interactive WBS Console, Gantt Timeline & Variance Governance',
subtitle: 'Granular Operational Oversight From Award Intake to Final Close-Out',
platform: 'PFM Console',
image: '/assets/marketing/6.jpeg',
coreProposition: 'Give programme managers and M&E specialists an interactive Work Breakdown Structure console, chronological Gantt, and variance detection.',
metrics: [
{ label: 'WBS Precision', value: 'Live Gantt', detail: '55-Day Span Chronology' },
{ label: 'Cost Oversight', value: 'Variance Alerts', detail: 'Realized Actuals vs Budget' },
{ label: 'Scope Control', value: 'Locked Gates', detail: 'Statutory Clearance Enforced' },
{ label: 'M&E Tracking', value: 'KPI Matrices', detail: 'Site Visits & Change Control' },
],
keyHighlights: [
{
number: '01',
title: 'Chronological Gantt & Milestone Mapping',
description: 'Visualize verified milestones, pending reviews, contractual deliverables, and work activities along an interactive timeline.',
},
{
number: '02',
title: 'Realized Actuals & Cost Audits',
description: 'Automatically compare work breakdown task costs against allocated project budgets to expose cost overruns and schedule slippage.',
},
{
number: '03',
title: 'Integrated Change & Site Visit Control',
description: 'Manage scope locks, evidence queues, beneficiary registries, and formal change requests from a single governance console.',
},
],
targetMarket: ['Programme Directors', 'M&E Specialists', 'Grant Operations Managers', 'Risk & Compliance Auditors'],
},
];
export const SCREENSHOTS: ScreenshotItem[] = [
{
id: 'console-primary',
title: 'PFM Console — Executive Dashboard',
subtitle: 'Fiduciary Portfolio Command & Statutory Gate Overview',
platform: 'PFM Console',
category: 'Dashboard',
image: '/assets/screenshots/console.png',
description: 'Executive birds-eye view of portfolio commitments (R21.0M), active awards, disbursement progress, sector allocations, and statutory compliance status.',
hotspots: [
{ x: 22, y: 18, title: 'Portfolio Capital Ticker', description: 'Real-time roll-up of total commitments and released tranches across all programmes.' },
{ x: 50, y: 35, title: 'Compliance Radar', description: 'Visual breakdown of projects satisfying statutory conditions precedent vs at-risk.' },
{ x: 80, y: 22, title: 'Quick Action Center', description: 'One-click award registration, disbursement approvals, and compliance export.' },
],
tags: ['Executive', 'Fiduciary', 'Capital Allocation', 'Real-time'],
},
{
id: 'partner-primary',
title: 'PFM Partner — Grantee Workspace',
subtitle: 'Active Grants, Milestone Fulfilment & Evidence Hub',
platform: 'PFM Partner',
category: 'Dashboard',
image: '/assets/screenshots/partner.png',
description: 'The recipient portal where implementing partners review contract scopes, upload required milestone proofs, and track disbursement releases.',
hotspots: [
{ x: 30, y: 25, title: 'Binding Contract Cards', description: 'Overview of sanctioned awards, active project phases, and contractual covenants.' },
{ x: 70, y: 40, title: 'Milestone Fulfilment Bar', description: 'Visual progress bar tracking verified deliverables against statutory disbursement gates.' },
{ x: 88, y: 15, title: 'Direct Comms Inbox', description: 'Direct messaging channel with the assigned Programme Officer.' },
],
tags: ['Grantee', 'Partner', 'Evidence Upload', 'Milestones'],
},
{
id: 'screen-1',
title: 'Console: Portfolio Telemetry & Sector Breakdown',
subtitle: 'Capital distribution by Agriculture, Mining, ICT, and Energy',
platform: 'PFM Console',
category: 'Dashboard',
image: '/assets/screenshots/1.png',
description: 'Dynamic portfolio analytics slicing sanctioned capital across industrial sectors and monitoring regional development impacts.',
tags: ['Analytics', 'Sectors', 'Reporting'],
},
{
id: 'screen-2',
title: 'Console: WBS & Project Directory',
subtitle: 'Multi-level project hierarchy with variance detection',
platform: 'PFM Console',
category: 'WBS & Gantt',
image: '/assets/screenshots/2.png',
description: 'Interactive directory of all funded projects, work packages, task budgets, and real-time variance calculations against contractual baselines.',
tags: ['WBS', 'Projects', 'Variance Detection'],
},
{
id: 'screen-3',
title: 'Console: Chronological Gantt Timeline',
subtitle: '55-Day span milestone chronology and critical paths',
platform: 'PFM Console',
category: 'WBS & Gantt',
image: '/assets/screenshots/3.png',
description: 'Interactive timeline modal mapping activity dependencies, milestone due dates, and automated overdue warning flags.',
tags: ['Gantt', 'Timelines', 'SLA Monitoring'],
},
{
id: 'screen-4',
title: 'Console: Fiduciary Governance & Gate Checks',
subtitle: 'Automated conditions precedent verification engine',
platform: 'PFM Console',
category: 'Governance & Gate',
image: '/assets/screenshots/4.png',
description: 'Enforces Business Rule BR-003: prevents any payment or disbursement release unless mandatory KYC, tax clearances, and milestone proofs are locked.',
tags: ['Governance', 'BR-003', 'Disbursement Gate'],
},
{
id: 'screen-6',
title: 'Partner: Project Delivery & Milestones',
subtitle: 'Transparent deliverable tracking for field teams',
platform: 'PFM Partner',
category: 'Evidence & Proof',
image: '/assets/screenshots/6.png',
description: 'Fieldwork teams manage deliverables, view assigned officer comments, and track the exact verification status of submitted documents.',
tags: ['Fieldwork', 'Verification', 'Milestones'],
},
{
id: 'screen-7',
title: 'Partner: Milestone Evidence Upload Queue',
subtitle: 'Granular file attachment directly to contractual items',
platform: 'PFM Partner',
category: 'Evidence & Proof',
image: '/assets/screenshots/7.png',
description: 'Implementing teams attach invoices, geotagged photos, and technical reports with instant preview and upload validation.',
tags: ['Evidence Queue', 'S3 Upload', 'Validation'],
},
{
id: 'screen-8',
title: 'Partner: Evidence Submission Modal',
subtitle: 'Pre-signed S3 storage upload with metadata tagging',
platform: 'PFM Partner',
category: 'Evidence & Proof',
image: '/assets/screenshots/8.png',
description: 'Upload modal with progress indicators, file size checks, and automated checksum generation for tamper-evident storage.',
tags: ['Upload Modal', 'Security', 'File Integrity'],
},
{
id: 'screen-9',
title: 'Partner: In-App Direct Communications',
subtitle: 'Instant messaging between Partner and Programme Officer',
platform: 'PFM Partner',
category: 'Communications',
image: '/assets/screenshots/9.png',
description: 'Eliminates lost emails and unrecorded WhatsApp conversations. All milestone clarifications and feedback are logged in an immutable thread.',
tags: ['Direct Comms', 'Audit Trail', 'Messaging'],
},
{
id: 'screen-10',
title: 'Partner: Statutory Onboarding & SSO Login',
subtitle: 'Instant mobilization via single sign-on invitation',
platform: 'PFM Partner',
category: 'Onboarding',
image: '/assets/screenshots/10.png',
description: 'Secure, seamless invitation acceptance. Grantees sign in with institutional SSO, accept terms, and configure their bank accounts.',
tags: ['SSO', 'KYC', 'Rapid Mobilization'],
},
];
export const SIMULATION_STEPS = [
{
id: 1,
step: 'Step 01',
actor: 'PFM Console (Funder)',
title: 'Sanctioned Award Intake',
summary: 'Funding award letter captured. Contract v1 initialized automatically (BR-001 & BR-002).',
details: 'Award of R1,000,000 registered with external committee reference. Initial conditions precedent generated.',
systemAction: 'Contract generated in DRAFT -> Counter-signed -> Tranche 1 pre-conditions locked.',
badgeColor: 'bg-indigo-500/20 text-indigo-300 border-indigo-500/30',
},
{
id: 2,
step: 'Step 02',
actor: 'PFM Partner (Grantee)',
title: 'SSO Onboarding & Banking KYC',
summary: 'Partner receives statutory invitation, authenticates via Duende SSO, and validates bank details.',
details: 'Bank confirmation letter and tax compliance certificate submitted. BR-005 single source of truth established.',
systemAction: 'Hashed recipient credentials locked into encrypted tenant boundary.',
badgeColor: 'bg-emerald-500/20 text-emerald-300 border-emerald-500/30',
},
{
id: 3,
step: 'Step 03',
actor: 'PFM Partner (Grantee)',
title: 'Milestone Execution & Proof Upload',
summary: 'Field team completes Activity 1.1 and uploads geotagged site photos & contractor invoices.',
details: 'Files uploaded directly to Garage S3 via pre-signed HTTPS URLs. Metadata & SHA-256 signatures generated.',
systemAction: 'Evidence record linked to Milestone #M01 in UNDER_REVIEW status.',
badgeColor: 'bg-cyan-500/20 text-cyan-300 border-cyan-500/30',
},
{
id: 4,
step: 'Step 04',
actor: 'PFM Console (Funder)',
title: 'Evidence Review & Gate Clearance',
summary: 'Programme Officer inspects proofs, verifies M&E indicators, and certifies condition precedent.',
details: 'Milestone marked VERIFIED (100%). System checks all gate conditions for Tranche 1 release.',
systemAction: 'Quartz.NET job triggers automated statutory audit log & notification dispatch.',
badgeColor: 'bg-purple-500/20 text-purple-300 border-purple-500/30',
},
{
id: 5,
step: 'Step 05',
actor: 'Unified System',
title: 'Fiduciary Tranche Disbursement Release',
summary: 'BR-003 Gate cleared. R350,000 initial drawdown released with zero uncontrolled exposure.',
details: 'Capital utilization telemetry updated across both Console and Partner dashboards in real time.',
systemAction: 'Disbursement instruction approved; partner notified in-app; M&E beneficiary count updated.',
badgeColor: 'bg-amber-500/20 text-amber-300 border-amber-500/30',
},
];
export const COMPARISON_FEATURES = [
{
feature: 'Primary Target User',
console: 'Institutional Funders, CFOs, Programme Managers, M&E Specialists',
partner: 'Field Coordinators, Grantee CEOs, Non-Profit Finance Leads, Contractors',
},
{
feature: 'Core Fiduciary Goal',
console: 'Capital control, risk mitigation, gate enforcement, portfolio roll-up',
partner: 'Activity execution, milestone evidence uploads, tranche visibility',
},
{
feature: 'Project Tracking View',
console: 'Interactive WBS, multi-project Gantt chronology, variance audits',
partner: 'Focused milestone deliverables list, task statuses, due dates',
},
{
feature: 'Financial Controls',
console: 'Budget allocation, covenant conditions precedent, tranche approvals',
partner: 'Drawdown requests, payment transparency, expenditure reconciliation',
},
{
feature: 'Evidence Management',
console: 'Multi-stakeholder verification, review notes, statutory audits',
partner: 'Drag-and-drop file upload to Garage S3, geotagging, progress notes',
},
{
feature: 'Communications Channel',
console: 'Official directives, formal compliance notices, query resolutions',
partner: 'Direct in-app messaging with assigned Programme Officer',
},
{
feature: 'Identity & Access',
console: 'Duende STS backoffice roles, multi-tenant portfolio administration',
partner: 'Statutory onboarding invitation token, one-click SSO login',
},
];
export const ARCHITECTURE_HIGHLIGHTS = [
{
title: '.NET 10 & Clean Architecture',
subtitle: 'Domain-Driven Design (DDD)',
description: 'Clear separation of Domain, Infrastructure, Application (CQRS via Mediator), and Presentation slices. Zero leaky abstractions.',
icon: 'Layers',
color: 'from-blue-500/20 to-indigo-500/20 border-blue-500/30',
},
{
title: 'React 19 & Vite 6/8 Frontend',
subtitle: 'Modern TypeScript Web Apps',
description: 'ConsoleUI and PartnerUI built on React 19, Tailwind CSS v4, Motion animations, and TanStack React Query for zero-stale reads.',
icon: 'Atom',
color: 'from-cyan-500/20 to-teal-500/20 border-cyan-500/30',
},
{
title: 'Distributed Background Host',
subtitle: 'Quartz.NET & PostFundManagement.Jobs',
description: 'Dedicated background daemon executing milestone sweeps, overdue alerts, risk score recalculations, and SMTP email notifications.',
icon: 'Cpu',
color: 'from-amber-500/20 to-orange-500/20 border-amber-500/30',
},
{
title: 'Garage S3 Object Storage',
subtitle: 'Tamper-Evident Evidence Vault',
description: 'Pre-signed secure upload URLs, SHA checksum verification, and accelerated CDN delivery for millions of statutory proof documents.',
icon: 'HardDrive',
color: 'from-emerald-500/20 to-teal-500/20 border-emerald-500/30',
},
{
title: 'Duende IdentityServer SSO',
subtitle: 'Institutional Security & RBAC',
description: 'OAuth 2.0 / OIDC PKCE flow with Skoruba administration, encrypted multi-tenant claims, and hashed ID persistence.',
icon: 'ShieldCheck',
color: 'from-purple-500/20 to-pink-500/20 border-purple-500/30',
},
{
title: 'PostgreSQL 14+ & HybridCache',
subtitle: 'Segregated Relational Data',
description: 'Segregated transactional (pfm) and scheduler (pfm-scheduler) databases paired with Redis output caching for sub-millisecond reads.',
icon: 'Database',
color: 'from-sky-500/20 to-blue-500/20 border-sky-500/30',
},
];
+116
View File
@@ -0,0 +1,116 @@
@import "tailwindcss";
@theme {
--font-sans: 'Plus Jakarta Sans', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
--font-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
}
:root {
color-scheme: dark;
}
@layer base {
html {
scroll-behavior: smooth;
background-color: #020617;
color: #f8fafc;
font-family: var(--font-sans);
}
body {
background-color: #020617;
color: #f8fafc;
overflow-x: hidden;
}
code, pre, .font-mono {
font-family: var(--font-mono);
}
}
/* Custom modern scrollbar */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #020617;
}
::-webkit-scrollbar-thumb {
background: #1e293b;
border-radius: 9999px;
border: 2px solid #020617;
}
::-webkit-scrollbar-thumb:hover {
background: #334155;
}
/* Glassmorphism utilities */
.glass-panel {
background: rgba(15, 23, 42, 0.7);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border: 1px solid rgba(255, 255, 255, 0.08);
}
.glass-panel-elevated {
background: rgba(30, 41, 59, 0.65);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.12);
box-shadow: 0 20px 50px -10px rgba(0, 0, 0, 0.5);
}
.glass-card-hover {
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}
.glass-card-hover:hover {
transform: translateY(-3px);
border-color: rgba(56, 189, 248, 0.35);
box-shadow: 0 12px 30px -10px rgba(14, 165, 233, 0.25);
}
/* Ambient glow effects */
.radial-glow-cyan {
background: radial-gradient(circle, rgba(6, 182, 212, 0.15) 0%, rgba(2, 6, 23, 0) 70%);
}
.radial-glow-indigo {
background: radial-gradient(circle, rgba(99, 102, 241, 0.15) 0%, rgba(2, 6, 23, 0) 70%);
}
.radial-glow-emerald {
background: radial-gradient(circle, rgba(16, 185, 129, 0.15) 0%, rgba(2, 6, 23, 0) 70%);
}
/* Animated gradient text */
.text-gradient-cyan {
background: linear-gradient(135deg, #38bdf8 0%, #06b6d4 50%, #10b981 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.text-gradient-funder {
background: linear-gradient(135deg, #60a5fa 0%, #818cf8 50%, #c084fc 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.text-gradient-partner {
background: linear-gradient(135deg, #34d399 0%, #2dd4bf 50%, #38bdf8 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
/* Subtle dot grid pattern */
.bg-grid-pattern {
background-size: 32px 32px;
background-image:
linear-gradient(to right, rgba(255, 255, 255, 0.04) 1px, transparent 1px),
linear-gradient(to bottom, rgba(255, 255, 255, 0.04) 1px, transparent 1px);
}
@@ -0,0 +1,11 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import './index.css';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
+2
View File
@@ -0,0 +1,2 @@
/// <reference types="vite/client" />
@@ -0,0 +1,22 @@
{
"compilerOptions": {
"target": "ES2022",
"experimentalDecorators": true,
"useDefineForClassFields": false,
"module": "ESNext",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"moduleResolution": "bundler",
"isolatedModules": true,
"moduleDetection": "force",
"allowJs": true,
"jsx": "react-jsx",
"paths": {
"@/*": ["./src/*"]
},
"allowImportingTsExtensions": true,
"noEmit": true
},
"include": ["src"]
}
@@ -0,0 +1,20 @@
import tailwindcss from '@tailwindcss/vite';
import react from '@vitejs/plugin-react';
import path from 'path';
import { defineConfig } from 'vite';
export default defineConfig(() => {
return {
plugins: [react(), tailwindcss()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
server: {
port: 3002,
host: '0.0.0.0',
},
};
});
File diff suppressed because it is too large Load Diff
+10
View File
@@ -162,3 +162,13 @@ services:
# Backend API Configuration # Backend API Configuration
- VITE_API_BASE_URL=${API_BASE_URL:-https://api.ifri.khongisa.co.za/} - VITE_API_BASE_URL=${API_BASE_URL:-https://api.ifri.khongisa.co.za/}
restart: unless-stopped restart: unless-stopped
pfm-showcase:
image: pfm-showcase
build:
context: .
dockerfile: ./PostFundManagement.ShowcaseUI/Dockerfile
container_name: pfm-showcase
ports:
- "8078:3000"
restart: unless-stopped