import React, { useState, useMemo } from 'react';
import { ScatterChart, Scatter, XAxis, YAxis, ZAxis, Tooltip, ResponsiveContainer, Cell, ReferenceLine, Label } from 'recharts';
import { SKILL_DATABASE } from './constants';
import { SkillCard } from './components/SkillCard';
import { Category, SkillData, StrategicAction } from './types';
import { LayoutGrid, BarChart2, Filter, Info, Layers, X, RefreshCw, ChevronDown, Menu } from 'lucide-react';
const getStrategicAction = (value: number, maturity: number): StrategicAction => {
const isHighValue = value >= 4;
const isMature = maturity >= 3;
if (isHighValue && !isMature) return StrategicAction.Invest;
if (isHighValue && isMature) return StrategicAction.Leverage;
if (!isHighValue && isMature) return StrategicAction.Maintain;
return StrategicAction.Monitor;
};
const App: React.FC = () => {
const [userState, setUserState] = useState<Record<string, { maturity: number, effort: number }>>({});
const [activeCategory, setActiveCategory] = useState<Category | 'All'>('All');
const [activeAction, setActiveAction] = useState<StrategicAction | 'All'>('All');
const [viewMode, setViewMode] = useState<'board' | 'matrix'>('board');
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
const fullSkillData: SkillData[] = useMemo(() => {
return SKILL_DATABASE.map(skill => {
const state = userState[skill.id] || { maturity: 1, effort: skill.defaultCost };
return {
...skill,
maturity: state.maturity,
effort: state.effort,
strategicAction: getStrategicAction(skill.defaultValue, state.maturity)
};
});
}, [userState]);
const filteredSkills = useMemo(() => {
return fullSkillData.filter(skill => {
const catMatch = activeCategory === 'All' || skill.category === activeCategory;
const actionMatch = activeAction === 'All' || skill.strategicAction === activeAction;
return catMatch && actionMatch;
});
}, [fullSkillData, activeCategory, activeAction]);
const isFiltered = activeCategory !== 'All' || activeAction !== 'All';
const handleUpdateMaturity = (id: string, val: number) => {
setUserState(prev => ({
...prev,
[id]: {
...(prev[id] || { effort: SKILL_DATABASE.find(s => s.id === id)?.defaultCost || 3 }),
maturity: val
}
}));
};
const handleUpdateEffort = (id: string, val: number) => {
setUserState(prev => ({
...prev,
[id]: {
...(prev[id] || { maturity: 1 }),
effort: val
}
}));
};
const handleResetFilters = () => {
setActiveCategory('All');
setActiveAction('All');
};
const stats = useMemo(() => {
return {
invest: fullSkillData.filter(s => s.strategicAction === StrategicAction.Invest).length,
leverage: fullSkillData.filter(s => s.strategicAction === StrategicAction.Leverage).length,
total: fullSkillData.length
}
}, [fullSkillData]);
const CustomTooltip = ({ active, payload }: any) => {
if (active && payload && payload.length) {
const data = payload[0].payload;
return (
<div className="bg-white border border-slate-200 p-3 rounded-lg shadow-xl text-xs">
<p className="font-bold text-slate-900">{data.name}</p>
<p className="text-slate-500 mb-2">{data.subCategory}</p>
<div className="grid grid-cols-2 gap-x-4 gap-y-1 text-slate-600">
<span>Val: {data.defaultValue}</span>
<span>Mat: {data.maturity}</span>
<span className="col-span-2 text-slate-400">Effort: {data.effort}</span>
</div>
</div>
);
}
return null;
};
return (
<div className="flex h-screen bg-slate-50 text-slate-900 overflow-hidden font-sans relative">
{}
{isSidebarOpen && (
<div
className="fixed inset-0 bg-slate-900/20 backdrop-blur-sm z-30 md:hidden transition-opacity"
onClick={() => setIsSidebarOpen(false)}
/>
)}
{}
<aside className={`
fixed inset-y-0 left-0 z-40 w-72 bg-white border-r border-slate-200 flex flex-col shrink-0 transition-transform duration-300 ease-out shadow-2xl md:shadow-none md:relative md:translate-x-0
${isSidebarOpen ? 'translate-x-0' : '-translate-x-full'}
`}>
<div className="p-6 border-b border-slate-100 flex justify-between items-center">
<div>
<h1 className="text-xl font-bold text-slate-900 tracking-tight flex items-center gap-2">
<Layers className="text-emerald-500" />
UI/UX Strategy
</h1>
<p className="text-xs text-slate-500 mt-1">Skill Mapping & Growth Board</p>
</div>
<button
onClick={() => setIsSidebarOpen(false)}
className="md:hidden text-slate-400 hover:text-slate-600 p-1 rounded-md hover:bg-slate-100"
>
<X size={20} />
</button>
</div>
<div className="flex-1 overflow-y-auto p-5 space-y-8 custom-scrollbar">
{}
<div>
<label className="text-xs font-bold text-slate-400 uppercase tracking-wider mb-3 block">View Mode</label>
<div className="grid grid-cols-2 gap-1 bg-slate-100 p-1 rounded-lg">
<button
onClick={() => setViewMode('board')}
className={`flex items-center justify-center gap-2 px-3 py-2 rounded-md text-xs font-semibold transition-all shadow-sm ${viewMode === 'board' ? 'bg-white text-slate-900 shadow-slate-200' : 'text-slate-500 hover:text-slate-700 bg-transparent shadow-none'}`}
>
<LayoutGrid size={14} /> Board
</button>
<button
onClick={() => setViewMode('matrix')}
className={`flex items-center justify-center gap-2 px-3 py-2 rounded-md text-xs font-semibold transition-all shadow-sm ${viewMode === 'matrix' ? 'bg-white text-slate-900 shadow-slate-200' : 'text-slate-500 hover:text-slate-700 bg-transparent shadow-none'}`}
>
<BarChart2 size={14} /> Matrix
</button>
</div>
</div>
{}
<div>
<div className="flex justify-between items-end mb-3">
<label className="text-xs font-bold text-slate-400 uppercase tracking-wider flex items-center gap-2">
<Filter size={12} /> Filters
</label>
{isFiltered && (
<button
onClick={handleResetFilters}
className="text-[10px] font-medium text-indigo-600 hover:text-indigo-800 flex items-center gap-1 bg-indigo-50 px-2 py-1 rounded-full transition-colors"
>
<RefreshCw size={10} /> Reset
</button>
)}
</div>
<div className="space-y-4">
{}
<div>
<label className="text-xs font-medium text-slate-500 mb-1.5 ml-1 block">Domain</label>
<div className="relative">
<select
value={activeCategory}
onChange={(e) => setActiveCategory(e.target.value as Category | 'All')}
className="w-full appearance-none bg-slate-50 border border-slate-200 text-slate-700 text-xs font-medium rounded-lg py-2.5 px-3 pr-8 focus:outline-none focus:ring-2 focus:ring-indigo-500/20 focus:border-indigo-500 transition-all cursor-pointer hover:border-slate-300 shadow-sm"
>
<option value="All">All Domains</option>
{Object.values(Category).map((cat) => (
<option key={cat} value={cat}>{cat}</option>
))}
</select>
<ChevronDown size={14} className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 pointer-events-none" />
</div>
</div>
{}
<div>
<label className="text-xs font-medium text-slate-500 mb-1.5 ml-1 block">Strategic Action</label>
<div className="relative">
<select
value={activeAction}
onChange={(e) => setActiveAction(e.target.value as StrategicAction | 'All')}
className="w-full appearance-none bg-slate-50 border border-slate-200 text-slate-700 text-xs font-medium rounded-lg py-2.5 px-3 pr-8 focus:outline-none focus:ring-2 focus:ring-indigo-500/20 focus:border-indigo-500 transition-all cursor-pointer hover:border-slate-300 shadow-sm"
>
<option value="All">All Actions</option>
{Object.values(StrategicAction).map((action) => (
<option key={action} value={action}>{action}</option>
))}
</select>
<ChevronDown size={14} className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 pointer-events-none" />
</div>
</div>
</div>
</div>
</div>
{}
<div className="p-5 bg-slate-50/50 border-t border-slate-200 text-xs">
<div className="flex justify-between mb-2">
<span className="text-slate-500 font-medium">Growth Opportunities</span>
<span className="text-emerald-600 font-bold bg-emerald-100 px-2 py-0.5 rounded-full">{stats.invest}</span>
</div>
<div className="flex justify-between">
<span className="text-slate-500 font-medium">Core Strengths</span>
<span className="text-blue-600 font-bold bg-blue-100 px-2 py-0.5 rounded-full">{stats.leverage}</span>
</div>
</div>
</aside>
{}
<main className="flex-1 overflow-y-auto bg-slate-50 relative custom-scrollbar w-full">
{}
<div className="sticky top-0 z-20 bg-white/80 backdrop-blur-md px-4 py-4 md:px-8 md:py-6 border-b border-slate-200 shadow-sm transition-all">
<div className="flex flex-col md:flex-row md:justify-between md:items-end gap-4 md:gap-0">
<div className="flex items-center gap-3">
{}
<button
onClick={() => setIsSidebarOpen(true)}
className="md:hidden p-2 -ml-2 text-slate-500 hover:bg-slate-100 rounded-lg hover:text-slate-900 transition-colors"
aria-label="Open Menu"
>
<Menu size={24} />
</button>
<div>
<h2 className="text-xl md:text-2xl font-bold text-slate-900 tracking-tight">
{viewMode === 'board' ? 'Skill Inventory' : 'Strategic Matrix'}
</h2>
<p className="text-xs md:text-sm text-slate-500 mt-0.5 md:mt-1 font-medium">
{activeCategory === 'All' ? 'All Skills' : activeCategory} • <span className="text-slate-900">{filteredSkills.length}</span> items
</p>
</div>
</div>
{}
<div className="flex gap-3 md:gap-4 text-[10px] md:text-xs font-medium text-slate-600 overflow-x-auto pb-1 md:pb-0 hide-scrollbar">
<div className="flex items-center gap-1.5 md:gap-2 whitespace-nowrap">
<span className="w-2 h-2 md:w-2.5 md:h-2.5 rounded-full bg-emerald-500"></span> Invest
</div>
<div className="flex items-center gap-1.5 md:gap-2 whitespace-nowrap">
<span className="w-2 h-2 md:w-2.5 md:h-2.5 rounded-full bg-blue-500"></span> Leverage
</div>
<div className="flex items-center gap-1.5 md:gap-2 whitespace-nowrap">
<span className="w-2 h-2 md:w-2.5 md:h-2.5 rounded-full bg-slate-400"></span> Maintain
</div>
</div>
</div>
</div>
<div className="p-4 md:p-8">
{viewMode === 'board' ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6 pb-20">
{filteredSkills.map(skill => (
<SkillCard
key={skill.id}
skill={skill}
onUpdateMaturity={handleUpdateMaturity}
onUpdateEffort={handleUpdateEffort}
/>
))}
{filteredSkills.length === 0 && (
<div className="col-span-full h-80 flex flex-col items-center justify-center text-slate-400 border-2 border-dashed border-slate-200 rounded-xl bg-slate-50/50">
<Info size={40} className="mb-3 text-slate-300" />
<p className="font-medium">No skills match current filters.</p>
<button onClick={handleResetFilters} className="mt-4 text-sm text-indigo-600 hover:text-indigo-700 font-semibold">
Clear Filters
</button>
</div>
)}
</div>
) : (
<div className="h-[400px] md:h-[600px] w-full bg-white rounded-xl border border-slate-200 p-4 md:p-6 relative shadow-sm">
<ResponsiveContainer width="100%" height="100%">
<ScatterChart margin={{ top: 20, right: 20, bottom: 20, left: 20 }}>
<XAxis
type="number"
dataKey="maturity"
name="Maturity"
domain={[0, 6]}
tickCount={6}
stroke="#94a3b8"
tick={{ fill: '#64748b', fontSize: 12 }}
label={{ value: 'Maturity', position: 'insideBottom', offset: -10, fill: '#64748b', fontSize: 12 }}
/>
<YAxis
type="number"
dataKey="defaultValue"
name="Business Value"
domain={[0, 6]}
tickCount={6}
stroke="#94a3b8"
tick={{ fill: '#64748b', fontSize: 12 }}
label={{ value: 'Value', angle: -90, position: 'insideLeft', fill: '#64748b', fontSize: 12 }}
/>
<ZAxis type="number" dataKey="effort" range={[50, 400]} name="Effort" />
<Tooltip content={<CustomTooltip />} cursor={{ strokeDasharray: '3 3', stroke: '#cbd5e1' }} />
{}
<ReferenceLine x={2.5} stroke="#e2e8f0" strokeDasharray="3 3" />
<ReferenceLine y={3.5} stroke="#e2e8f0" strokeDasharray="3 3" />
{}
<Label value="INVEST" position="top" offset={10} fill="#64748b" fontSize={10} />
<Scatter name="Skills" data={filteredSkills} onClick={() => {}}>
{filteredSkills.map((entry, index) => {
let color = '#94a3b8';
if (entry.strategicAction === StrategicAction.Invest) color = '#10b981';
if (entry.strategicAction === StrategicAction.Leverage) color = '#3b82f6';
if (entry.strategicAction === StrategicAction.Monitor) color = '#fbbf24';
return <Cell key={`cell-${index}`} fill={color} fillOpacity={0.8} stroke={color} />;
})}
</Scatter>
</ScatterChart>
</ResponsiveContainer>
{}
<div className="absolute top-4 left-4 text-[10px] md:text-xs font-bold text-emerald-600/70 pointer-events-none">INVEST</div>
<div className="absolute top-4 right-4 text-[10px] md:text-xs font-bold text-blue-600/70 pointer-events-none">LEVERAGE</div>
<div className="absolute bottom-12 left-4 text-[10px] md:text-xs font-bold text-amber-500/70 pointer-events-none">MONITOR</div>
<div className="absolute bottom-12 right-4 text-[10px] md:text-xs font-bold text-slate-500/70 pointer-events-none">MAINTAIN</div>
</div>
)}
</div>
</main>
</div>
);
};
export default App;