Added ShowcaseUI app
@@ -75,6 +75,7 @@ PostFundManagement/
|
||||
├── PostFundManagement.Tests/ # Unit & Integration test suite (xUnit, Coverlet)
|
||||
├── 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.ShowcaseUI/ # Public Facing Showcase App (React 19 + Vite 6 + Tailwind 4)
|
||||
└── docker-compose.yml # Container orchestration for API and Frontends
|
||||
```
|
||||
|
||||
@@ -115,6 +116,13 @@ PostFundManagement/
|
||||
* **Exports & Utilities**: `jspdf`, `xlsx`, `hashids`
|
||||
* **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
|
||||
* **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.
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
node_modules
|
||||
dist
|
||||
.git
|
||||
.gitignore
|
||||
*.log
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
node_modules
|
||||
dist
|
||||
.env
|
||||
.env.local
|
||||
*.log
|
||||
|
||||
@@ -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;"]
|
||||
|
||||
@@ -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 "$@"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 2.3 MiB |
|
After Width: | Height: | Size: 2.8 MiB |
|
After Width: | Height: | Size: 2.7 MiB |
|
After Width: | Height: | Size: 2.3 MiB |
|
After Width: | Height: | Size: 2.3 MiB |
|
After Width: | Height: | Size: 2.7 MiB |
|
After Width: | Height: | Size: 281 KiB |
|
After Width: | Height: | Size: 129 KiB |
|
After Width: | Height: | Size: 302 KiB |
|
After Width: | Height: | Size: 260 KiB |
|
After Width: | Height: | Size: 269 KiB |
|
After Width: | Height: | Size: 116 KiB |
|
After Width: | Height: | Size: 156 KiB |
|
After Width: | Height: | Size: 140 KiB |
|
After Width: | Height: | Size: 150 KiB |
|
After Width: | Height: | Size: 218 KiB |
|
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 |
@@ -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',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -162,3 +162,13 @@ services:
|
||||
# Backend API Configuration
|
||||
- VITE_API_BASE_URL=${API_BASE_URL:-https://api.ifri.khongisa.co.za/}
|
||||
restart: unless-stopped
|
||||
pfm-showcase:
|
||||
image: pfm-showcase
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./PostFundManagement.ShowcaseUI/Dockerfile
|
||||
container_name: pfm-showcase
|
||||
ports:
|
||||
- "8078:3000"
|
||||
restart: unless-stopped
|
||||
|
||||
|
||||