feat: initial commit — Falah Coach Portal v0.1
Deploy to Netlify / build-and-deploy (push) Failing after 14m42s

- React 18 + Vite 5 + TailwindCSS 3 SPA
- Mission Control dashboard
- AI Co-Pilot & Simulator view
- Tactical Audio Briefings library
- MS Loop Wiki integration view
- Global audio player in sidebar
- Netlify deployment config with SPA redirect
- GitHub Actions CI/CD workflow
This commit is contained in:
wmj
2026-08-23 12:10:24 +08:00
commit 0c2da241aa
14 changed files with 3313 additions and 0 deletions
+452
View File
@@ -0,0 +1,452 @@
import React, { useState } from 'react';
import {
LayoutDashboard,
MessageSquareQuote,
Headphones,
BookOpen,
PlayCircle,
PauseCircle,
SkipForward,
SkipBack,
TerminalSquare,
Bot,
User,
Zap,
TrendingUp,
FileText,
ChevronRight,
FolderOpen
} from 'lucide-react';
/* ==========================================================================
MOCK DATA
========================================================================== */
const MOCK_AUDIO_TRACKS = [
{ id: 1, title: 'The 100-Day Turnaround', duration: '14:20', tag: 'Strategy', author: 'Senior Partner A' },
{ id: 2, title: 'Negotiating with Hostile Boards', duration: '22:15', tag: 'Leadership', author: 'Principal B' },
{ id: 3, title: 'M&A Synergy Identification', duration: '18:45', tag: 'Finance', author: 'Director C' },
{ id: 4, title: 'Digital Transformation Pitfalls', duration: '31:10', tag: 'Technology', author: 'Principal D' },
{ id: 5, title: 'Pricing Strategies in Inflation', duration: '12:55', tag: 'Economics', author: 'Partner E' },
];
const MOCK_WIKI_DIRECTORY = [
{
folder: 'Core Frameworks',
items: ['Falah MECE Guidelines', 'Value Creation Matrix', 'Go-To-Market Playbook']
},
{
folder: 'Banking Edition',
items: ['Retail Bank Cost Reduction', 'Fintech Threat Analysis', 'Regulatory Compliance 2026']
},
{
folder: 'Telco Edition',
items: ['5G Monetization', 'Churn Reduction Models']
}
];
const MOCK_CHAT_HISTORY = [
{ role: 'system', text: 'Falah Simulator initialized. Persona: Hostile Telco CIO. Objective: Justify £2M Agile Transformation.' },
{ role: 'user', text: 'Good morning. I\'d like to walk you through the projected ROI for the Agile Transformation initiative.' },
{ role: 'assistant', text: 'I don\'t have time for buzzwords today. You want £2M of my budget to "transform" teams that are already delivering. Where is the hard financial justification? I need EBITDA impact, not velocity charts.' },
];
/* ==========================================================================
SUB-VIEWS
========================================================================== */
const MissionControlView = ({ setActiveView, setActiveAudio }) => {
return (
<div className="space-y-8 animate-fade-in pr-6">
<header>
<h1 className="text-4xl font-bold tracking-tight text-white mb-2">Mission Control</h1>
<p className="text-slate-400">Welcome back. Your Falah Coach portal is ready.</p>
</header>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Featured Audio Widget */}
<div className="lg:col-span-2 bg-slate-900 border border-slate-800 rounded-2xl p-6 relative overflow-hidden group">
<div className="absolute top-0 right-0 p-32 bg-emerald-500/10 rounded-full blur-[100px] -mr-16 -mt-16 pointer-events-none"></div>
<div className="relative z-10 flex flex-col h-full justify-between">
<div>
<div className="flex items-center gap-2 text-emerald-400 font-medium mb-4 text-sm tracking-wide uppercase">
<Zap size={16} /> Featured Briefing
</div>
<h2 className="text-3xl font-semibold text-white mb-3 tracking-tight">The 100-Day Turnaround</h2>
<p className="text-slate-400 max-w-lg mb-6 leading-relaxed">
Master the critical first 100 days of a private equity backed turnaround. Tactical insights from our top Senior Partners.
</p>
</div>
<button
onClick={() => setActiveAudio(MOCK_AUDIO_TRACKS[0])}
className="flex items-center gap-2 bg-emerald-500 hover:bg-emerald-400 text-slate-950 font-semibold py-3 px-6 rounded-lg transition-colors w-max"
>
<PlayCircle size={20} />
Play Briefing (14:20)
</button>
</div>
</div>
{/* Quick Starts */}
<div className="flex flex-col gap-4">
<div
onClick={() => setActiveView('simulator')}
className="bg-slate-900 border border-slate-800 hover:border-blue-500/50 rounded-2xl p-6 cursor-pointer transition-all hover:-translate-y-1 group"
>
<div className="bg-blue-500/10 text-blue-400 p-3 rounded-lg w-max mb-4 group-hover:bg-blue-500 group-hover:text-white transition-colors">
<Bot size={24} />
</div>
<h3 className="text-xl font-semibold text-white mb-1">AI Simulator</h3>
<p className="text-slate-400 text-sm">Practice hostile boardroom scenarios.</p>
</div>
<div
onClick={() => setActiveView('wiki')}
className="bg-slate-900 border border-slate-800 hover:border-emerald-500/50 rounded-2xl p-6 cursor-pointer transition-all hover:-translate-y-1 group"
>
<div className="bg-emerald-500/10 text-emerald-400 p-3 rounded-lg w-max mb-4 group-hover:bg-emerald-500 group-hover:text-white transition-colors">
<BookOpen size={24} />
</div>
<h3 className="text-xl font-semibold text-white mb-1">Loop Wiki</h3>
<p className="text-slate-400 text-sm">Review the Falah MECE Guidelines.</p>
</div>
</div>
</div>
</div>
);
};
const AICoPilotView = () => {
const [messages, setMessages] = useState(MOCK_CHAT_HISTORY);
const [input, setInput] = useState('');
const handleSend = (e) => {
e.preventDefault();
if (!input.trim()) return;
setMessages([...messages, { role: 'user', text: input }]);
setInput('');
// Mock response
setTimeout(() => {
setMessages(prev => [...prev, {
role: 'assistant',
text: 'That response lacks financial rigor. Reframe your answer focusing on cost-of-delay and projected OPEX reduction over the next 3 quarters.'
}]);
}, 1000);
};
return (
<div className="h-full flex flex-col pb-4 animate-fade-in pr-6">
<header className="mb-6">
<h1 className="text-3xl font-bold tracking-tight text-white mb-1">AI Co-Pilot & Simulator</h1>
<p className="text-slate-400 text-sm">Engage in high-pressure executive roleplay to refine your delivery.</p>
</header>
<div className="flex-1 bg-slate-900 border border-slate-800 rounded-2xl flex flex-col overflow-hidden">
<div className="flex-1 p-6 overflow-y-auto space-y-6">
{messages.map((msg, i) => (
<div key={i} className={`flex gap-4 ${msg.role === 'user' ? 'justify-end' : 'justify-start'}`}>
{msg.role !== 'user' && (
<div className={`w-8 h-8 rounded-full flex items-center justify-center shrink-0 ${msg.role === 'system' ? 'bg-slate-800 text-slate-400' : 'bg-blue-500/20 text-blue-400'}`}>
{msg.role === 'system' ? <TerminalSquare size={16} /> : <Bot size={16} />}
</div>
)}
<div className={`max-w-[70%] p-4 rounded-2xl ${
msg.role === 'user'
? 'bg-blue-600 text-white rounded-tr-sm'
: msg.role === 'system'
? 'bg-slate-800 text-slate-300 font-mono text-sm border border-slate-700'
: 'bg-slate-800 text-slate-200 border border-slate-700 rounded-tl-sm'
}`}>
{msg.text}
</div>
{msg.role === 'user' && (
<div className="w-8 h-8 rounded-full bg-slate-700 text-slate-300 flex items-center justify-center shrink-0">
<User size={16} />
</div>
)}
</div>
))}
</div>
<div className="p-4 bg-slate-850 border-t border-slate-800">
<div className="flex gap-2 mb-3 overflow-x-auto pb-1 hide-scrollbar">
{['/simulate Telco CIO', '/mentor DevSecOps', '/grade'].map(cmd => (
<button
key={cmd}
onClick={() => setInput(cmd)}
className="whitespace-nowrap px-3 py-1.5 bg-slate-800 hover:bg-slate-700 text-blue-400 text-xs font-mono rounded-md border border-slate-700 transition-colors"
>
{cmd}
</button>
))}
</div>
<form onSubmit={handleSend} className="relative">
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Deploy a response or command..."
className="w-full bg-slate-900 border border-slate-700 rounded-xl px-4 py-4 pr-12 text-slate-200 focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500 transition-all placeholder:text-slate-500"
/>
<button
type="submit"
className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 hover:text-blue-400 p-2"
>
<Zap size={20} />
</button>
</form>
</div>
</div>
</div>
);
};
const AudioLibraryView = ({ setActiveAudio }) => {
return (
<div className="animate-fade-in pr-6">
<header className="mb-8">
<h1 className="text-3xl font-bold tracking-tight text-white mb-2">Tactical Audio Briefings</h1>
<p className="text-slate-400">Exclusive micro-learning tracks from Falah's leadership network.</p>
</header>
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
{MOCK_AUDIO_TRACKS.map(track => (
<div
key={track.id}
onClick={() => setActiveAudio(track)}
className="group bg-slate-900 border border-slate-800 hover:border-emerald-500/50 rounded-2xl p-6 cursor-pointer transition-all hover:shadow-lg hover:shadow-emerald-500/5 relative overflow-hidden"
>
<div className="flex justify-between items-start mb-12">
<span className="px-2.5 py-1 bg-slate-800 text-slate-300 text-xs font-semibold rounded-md uppercase tracking-wider">
{track.tag}
</span>
<div className="w-10 h-10 rounded-full bg-emerald-500/20 text-emerald-400 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity scale-75 group-hover:scale-100">
<PlayCircle size={24} />
</div>
</div>
<div>
<h3 className="text-xl font-semibold text-white mb-2 group-hover:text-emerald-400 transition-colors">{track.title}</h3>
<div className="flex justify-between items-center text-sm text-slate-500">
<span>{track.author}</span>
<span className="flex items-center gap-1 font-mono"><Headphones size={14}/> {track.duration}</span>
</div>
</div>
</div>
))}
</div>
</div>
);
};
const MSLoopWikiView = () => {
const [activePage, setActivePage] = useState('Falah MECE Guidelines');
return (
<div className="h-full flex flex-col pb-4 animate-fade-in pr-6">
<header className="mb-6">
<h1 className="text-3xl font-bold tracking-tight text-white mb-1">MS Loop Integration</h1>
<p className="text-slate-400 text-sm">Falah Playbooks & Living Intellectual Property.</p>
</header>
<div className="flex-1 flex gap-6 h-[calc(100vh-140px)] overflow-hidden">
{/* Left Col: Directory */}
<div className="w-64 shrink-0 bg-slate-900 border border-slate-800 rounded-2xl p-4 overflow-y-auto custom-scrollbar">
<div className="text-xs font-semibold text-slate-500 uppercase tracking-wider mb-4 ml-2">Directory</div>
<div className="space-y-4">
{MOCK_WIKI_DIRECTORY.map((dir, idx) => (
<div key={idx}>
<div className="flex items-center gap-2 text-slate-300 font-medium text-sm mb-2 px-2">
<FolderOpen size={16} className="text-blue-400/70" />
{dir.folder}
</div>
<div className="space-y-1">
{dir.items.map(item => (
<button
key={item}
onClick={() => setActivePage(item)}
className={`w-full flex items-center gap-2 px-2 py-1.5 text-sm rounded-md transition-colors text-left ${
activePage === item
? 'bg-blue-500/10 text-blue-400 font-medium'
: 'text-slate-400 hover:text-slate-200 hover:bg-slate-800'
}`}
>
<FileText size={14} />
<span className="truncate">{item}</span>
</button>
))}
</div>
</div>
))}
</div>
</div>
{/* Right Col: Iframe container */}
<div className="flex-1 bg-white rounded-2xl overflow-hidden flex flex-col shadow-2xl relative">
{/* Iframe Chrome */}
<div className="bg-slate-100 border-b border-slate-200 p-3 flex items-center gap-4">
<div className="flex gap-1.5">
<div className="w-3 h-3 rounded-full bg-slate-300"></div>
<div className="w-3 h-3 rounded-full bg-slate-300"></div>
<div className="w-3 h-3 rounded-full bg-slate-300"></div>
</div>
<div className="bg-white border border-slate-200 text-slate-400 text-xs px-3 py-1.5 rounded flex-1 flex justify-between items-center shadow-sm max-w-md">
<div className="truncate text-slate-500 font-mono text-[11px]"><span className="text-slate-400">loop.microsoft.com/falah/</span>{activePage.toLowerCase().replace(/ /g, '-')}</div>
</div>
</div>
{/* Simulated content area */}
<div className="flex-1 p-12 overflow-y-auto text-slate-800 bg-[#fbfbfb]">
<div className="max-w-2xl mx-auto">
<h1 className="text-4xl font-bold mb-8 text-slate-900 border-b border-slate-200 pb-4">{activePage}</h1>
<div className="prose prose-slate max-w-none">
<p className="text-lg leading-relaxed mb-6 text-slate-600">
This document serves as the canonical reference for exactly how our consultants should approach problem structuring in this domain.
</p>
<div className="grid grid-cols-2 gap-4 mb-8">
<div className="p-4 bg-blue-50 border border-blue-100 rounded-lg">
<h4 className="font-semibold text-blue-900 m-0 mb-2">Key Objective</h4>
<p className="text-sm text-blue-800 m-0">Drive unambiguous clarity to executive stakeholders within 3 minutes of engagement.</p>
</div>
<div className="p-4 bg-emerald-50 border border-emerald-100 rounded-lg">
<h4 className="font-semibold text-emerald-900 m-0 mb-2">Success Metric</h4>
<p className="text-sm text-emerald-800 m-0">Achieve client alignment before the first break in the agenda.</p>
</div>
</div>
<h3 className="text-2xl font-semibold mb-4 mt-8 text-slate-800">1. Executive Summary</h3>
<p className="text-slate-600 leading-relaxed mb-4">
The imperative here is not just structural elegance, but actionable intelligence. When framing the issue, prioritize the financial delta and the organizational capacity required to capture it.
</p>
<ul className="space-y-2 list-disc pl-5 text-slate-600 mb-8">
<li>Identify the core constraint (Capital, Talent, or Regulatory).</li>
<li>Map the adjacent capabilities required.</li>
<li>Draft the 100-day execution roadmap.</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
);
};
/* ==========================================================================
MAIN APP SHELL
========================================================================== */
export default function App() {
const [activeView, setActiveView] = useState('missionControl');
const [activeAudio, setActiveAudio] = useState(null);
const [isPlaying, setIsPlaying] = useState(false);
const togglePlay = () => setIsPlaying(!isPlaying);
const navItems = [
{ id: 'missionControl', label: 'Mission Control', icon: LayoutDashboard },
{ id: 'simulator', label: 'AI Co-Pilot', icon: MessageSquareQuote },
{ id: 'audio', label: 'Audio Library', icon: Headphones },
{ id: 'wiki', label: 'Loop Wiki', icon: BookOpen },
];
return (
<div className="flex h-screen bg-slate-950 text-slate-100 font-sans overflow-hidden selection:bg-blue-500/30">
{/* Sidebar Layout */}
<div className="w-72 border-r border-slate-800 flex flex-col bg-slate-900 shadow-2xl relative z-20">
{/* Brand */}
<div className="p-6">
<div className="flex items-center gap-3 mb-1">
<div className="w-8 h-8 bg-white rounded-lg flex items-center justify-center">
<TrendingUp className="text-slate-950" size={20} strokeWidth={2.5}/>
</div>
<span className="font-bold text-xl tracking-wide text-white uppercase">FALAH</span>
</div>
<p className="text-[10px] uppercase font-bold tracking-[0.2em] text-slate-500 ml-11">Coach Portal</p>
</div>
{/* Navigation */}
<nav className="flex-1 px-4 mt-6 space-y-2 overflow-y-auto">
<div className="text-xs font-semibold text-slate-600 uppercase tracking-wider mb-4 ml-3">Platform</div>
{navItems.map((item) => {
const Icon = item.icon;
const isActive = activeView === item.id;
return (
<button
key={item.id}
onClick={() => setActiveView(item.id)}
className={`w-full flex items-center justify-between px-3 py-2.5 rounded-lg transition-all duration-200 group ${
isActive
? 'bg-blue-600 shadow-md shadow-blue-500/20'
: 'hover:bg-slate-800 text-slate-400 hover:text-slate-100'
}`}
>
<div className="flex items-center gap-3">
<Icon size={18} className={isActive ? 'text-white' : 'text-slate-500 group-hover:text-blue-400 transition-colors'}/>
<span className={`font-medium ${isActive ? 'text-white' : ''}`}>{item.label}</span>
</div>
{isActive && <ChevronRight size={16} className="text-blue-200" />}
</button>
)
})}
</nav>
{/* Global Audio Player */}
<div className="mt-auto border-t border-slate-800 p-4 bg-slate-950/50 backdrop-blur-md">
{activeAudio ? (
<div className="animate-fade-in-up">
<div className="flex justify-between items-center mb-2">
<span className="text-[10px] font-bold text-emerald-400 uppercase tracking-wider">Now Playing</span>
<span className="text-xs text-slate-500 font-mono">{isPlaying ? '02:14' : '00:00'} / {activeAudio.duration}</span>
</div>
<p className="font-semibold text-sm text-white truncate mb-1" title={activeAudio.title}>
{activeAudio.title}
</p>
<p className="text-xs text-slate-400 mb-3 truncate">{activeAudio.author}</p>
<div className="flex items-center justify-between mt-3 px-2">
<button className="text-slate-400 hover:text-white transition-colors"><SkipBack size={18} /></button>
<button
onClick={togglePlay}
className="text-white bg-emerald-500 hover:bg-emerald-400 rounded-full w-10 h-10 flex items-center justify-center transition-all shadow-lg shadow-emerald-500/20"
>
{isPlaying ? <PauseCircle size={24} className="text-slate-950" /> : <PlayCircle size={24} className="text-slate-950" />}
</button>
<button className="text-slate-400 hover:text-white transition-colors"><SkipForward size={18} /></button>
</div>
{/* Fake progress bar */}
<div className="w-full bg-slate-800 h-1 mt-4 rounded-full overflow-hidden">
<div className="bg-emerald-500 h-full w-1/4 rounded-full transition-all duration-1000 ease-linear"></div>
</div>
</div>
) : (
<div className="py-4 text-center">
<Headphones className="mx-auto text-slate-700 mb-2" size={24} />
<p className="text-xs text-slate-500 font-medium">Select an audio brief to begin</p>
</div>
)}
</div>
</div>
{/* Main Content Area */}
<main className="flex-1 overflow-y-auto p-8 relative scroll-smooth overflow-x-hidden">
{/* Ambient background glow */}
<div className="absolute top-[-20%] left-[-10%] w-[50%] h-[50%] bg-blue-600/5 rounded-full blur-[120px] pointer-events-none"></div>
<div className="max-w-6xl mx-auto h-full relative z-10">
{activeView === 'missionControl' && <MissionControlView setActiveView={setActiveView} setActiveAudio={setActiveAudio}/>}
{activeView === 'simulator' && <AICoPilotView />}
{activeView === 'audio' && <AudioLibraryView setActiveAudio={setActiveAudio}/>}
{activeView === 'wiki' && <MSLoopWikiView />}
</div>
</main>
</div>
);
}
+32
View File
@@ -0,0 +1,32 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
html, body, #root {
@apply h-full;
}
}
@layer utilities {
.custom-scrollbar::-webkit-scrollbar {
width: 4px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: transparent;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
@apply bg-slate-800 rounded;
}
.custom-scrollbar:hover::-webkit-scrollbar-thumb {
@apply bg-slate-700;
}
.hide-scrollbar::-webkit-scrollbar {
display: none;
}
.hide-scrollbar {
-ms-overflow-style: none;
scrollbar-width: none;
}
}
+10
View File
@@ -0,0 +1,10 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
import './index.css'
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)