import { useState, useMemo } from "react"; const COMPANY_COLORS = ["#f59e0b", "#3b82f6", "#a855f7", "#ec4899", "#10b981", "#f97316", "#06b6d4"]; const DEFAULT_CRITERIA = [ { id: 1, name: "Compensation & Equity", weight: 20, description: "Total comp, equity, and upside" }, { id: 2, name: "Role Scope & Mandate", weight: 18, description: "Clarity of charter, authority, influence" }, { id: 3, name: "Reporting Structure", weight: 8, description: "Who you report to, access to C-suite" }, { id: 4, name: "Company Growth Stage", weight: 7, description: "Alignment with your ideal inflection point" }, { id: 5, name: "Culture & Values Fit", weight: 9, description: "Lived values, psychological safety" }, { id: 6, name: "Team Quality", weight: 7, description: "Caliber of team you'd inherit or build" }, { id: 7, name: "Executive Sponsorship", weight: 8, description: "Exec buy-in and air cover for your work" }, { id: 8, name: "Brand & Market Position", weight: 10, description: "Company reputation, category leader?" }, { id: 9, name: "Remote/Flexibility", weight: 6, description: "WFH policy, travel, schedule autonomy" }, { id: 10, name: "Mission Alignment", weight: 7, description: "Do you believe in what they're building?" }, ]; const LOCATION_WEIGHT = 16; const SCORE_LABELS = { 1: "Poor", 2: "Below Avg", 3: "Average", 4: "Good", 5: "Excellent" }; const SCORE_COLORS = { 1: "#ef4444", 2: "#f97316", 3: "#eab308", 4: "#84cc16", 5: "#22c55e" }; function locColor(score) { if (score === 0) return "#334155"; if (score <= 3) return "#ef4444"; if (score <= 6) return "#eab308"; return "#22c55e"; } function ScoreButton({ value, current, onChange }) { const isSelected = current === value; return ( ); } function WeightSlider({ value, onChange }) { return (
onChange(Number(e.target.value))} style={{ width: 80, accentColor: "#f59e0b", cursor: "pointer" }} /> {value}
); } export default function OpportunityMatrix() { const [criteria, setCriteria] = useState(DEFAULT_CRITERIA); const [companies, setCompanies] = useState([]); const [scores, setScores] = useState({}); const [locationScores, setLocationScores] = useState({}); const [notes, setNotes] = useState({}); const [activeTab, setActiveTab] = useState("matrix"); const [editingCriteria, setEditingCriteria] = useState(null); const [editingCompany, setEditingCompany] = useState(null); const [newCriteriaName, setNewCriteriaName] = useState(""); const [newCompanyName, setNewCompanyName] = useState(""); const [nextCriteriaId, setNextCriteriaId] = useState(11); const [nextCompanyId, setNextCompanyId] = useState(1); const [showOnboarding, setShowOnboarding] = useState(true); const [onboardingInputs, setOnboardingInputs] = useState(["", "", "", "", ""]); // ── Onboarding ──────────────────────────────────────────────────────────── const handleOnboardingSubmit = () => { const names = onboardingInputs.map(n => n.trim()).filter(Boolean); if (!names.length) return; const built = names.map((name, i) => ({ id: i + 1, name, color: COMPANY_COLORS[i % COMPANY_COLORS.length] })); setCompanies(built); setNextCompanyId(built.length + 1); setShowOnboarding(false); }; // ── Score helpers ───────────────────────────────────────────────────────── const setScore = (cId, crId, v) => setScores(prev => ({ ...prev, [cId + "-" + crId]: v })); const getScore = (cId, crId) => scores[cId + "-" + crId] || 0; const getLocScore = (cId) => locationScores[cId] || 0; // ── Weighted totals ─────────────────────────────────────────────────────── const weightedScores = useMemo(() => { return companies.map(company => { let wSum = 0, wTotal = 0; criteria.forEach(c => { const s = getScore(company.id, c.id); if (s > 0) { wSum += (s / 5) * 10 * c.weight; wTotal += c.weight; } }); const ls = getLocScore(company.id); if (ls > 0) { wSum += ls * LOCATION_WEIGHT; wTotal += LOCATION_WEIGHT; } const rawScore = wTotal > 0 ? wSum / wTotal : 0; const totalFields = criteria.length + 1; const scoredFields = criteria.filter(c => getScore(company.id, c.id) > 0).length + (ls > 0 ? 1 : 0); return { company, rawScore, locScore: ls, completeness: Math.round((scoredFields / totalFields) * 100), criteriaScores: criteria.map(c => ({ criteria: c, score: getScore(company.id, c.id) })), }; }).sort((a, b) => b.rawScore - a.rawScore); }, [companies, criteria, scores, locationScores]); // ── CRUD ────────────────────────────────────────────────────────────────── const addCriteria = () => { if (!newCriteriaName.trim()) return; setCriteria(p => [...p, { id: nextCriteriaId, name: newCriteriaName.trim(), weight: 5, description: "" }]); setNextCriteriaId(n => n + 1); setNewCriteriaName(""); }; const removeCriteria = (id) => setCriteria(p => p.filter(c => c.id !== id)); const addCompany = () => { if (!newCompanyName.trim()) return; setCompanies(p => [...p, { id: nextCompanyId, name: newCompanyName.trim(), color: COMPANY_COLORS[p.length % COMPANY_COLORS.length] }]); setNextCompanyId(n => n + 1); setNewCompanyName(""); }; const removeCompany = (id) => setCompanies(p => p.filter(c => c.id !== id)); // ── Style helpers ───────────────────────────────────────────────────────── const inputStyle = { backgroundColor: "#0f172a", border: "1px solid #334155", borderRadius: 6, color: "#e2e8f0", padding: "8px 12px", fontFamily: "'DM Sans', sans-serif", fontSize: 14, outline: "none" }; const btn = (v) => ({ padding: "8px 16px", borderRadius: 6, border: "none", cursor: "pointer", fontFamily: "'DM Sans', sans-serif", fontSize: 13, fontWeight: 600, transition: "all 0.15s ease", backgroundColor: v === "primary" ? "#f59e0b" : v === "danger" ? "#ef444422" : "#1e293b", color: v === "primary" ? "#0f172a" : v === "danger" ? "#ef4444" : "#94a3b8" }); const tabBtn = (t) => ({ padding: "8px 20px", borderRadius: 6, border: "none", cursor: "pointer", fontFamily: "'DM Sans', sans-serif", fontSize: 14, fontWeight: 600, transition: "all 0.2s ease", backgroundColor: activeTab === t ? "#f59e0b" : "transparent", color: activeTab === t ? "#0f172a" : "#64748b" }); // ── Render ──────────────────────────────────────────────────────────────── return (
{/* ── Onboarding ──────────────────────────────────────────────────── */} {showOnboarding && (
Opportunity Matrix
Enter the companies you're evaluating — up to 5. You can rename or add more anytime in Settings.
{onboardingInputs.map((val, i) => (
{i + 1}
{ const u = [...onboardingInputs]; u[i] = e.target.value; setOnboardingInputs(u); }} onKeyDown={e => { if (e.key === "Enter" && i === 4) handleOnboardingSubmit(); }} />
))}
)} {/* ── Header ──────────────────────────────────────────────────────── */}
Opportunity Matrix
Weighted decision framework · {companies.length} {companies.length === 1 ? "opportunity" : "opportunities"} · {criteria.length + 1} criteria
{["matrix", "results", "settings"].map(t => ( ))}
{/* ── Body ────────────────────────────────────────────────────────── */}
{/* ══ SCORE TAB ═════════════════════════════════════════════════ */} {activeTab === "matrix" && (
{companies.length === 0 ? (
No companies yet
Click "Change Companies" to get started
) : ( <>
Score each opportunity 1–5 per criterion · Slide location desirability · Totals update live
{companies.map(c => (
{c.name}
))}
{companies.map(c => ( ))} {/* Standard criteria rows */} {criteria.map(c => ( {companies.map(co => { const s = getScore(co.id, c.id); return ( ); })} ))} {/* Location row */} {companies.map(co => { const ls = getLocScore(co.id); const lc = locColor(ls); return ( ); })} {/* Totals row */} ); })}
Criterion Wt{c.name}
{c.name}
{c.description &&
{c.description}
}
{c.weight}
{[1,2,3,4,5].map(v => setScore(co.id, c.id, val)} />)}
{s > 0 &&
{SCORE_LABELS[s]} · {s * c.weight} pts
}
📍 Location Desirability
1 = Least desirable · 10 = Most desirable
{LOCATION_WEIGHT}
setLocationScores(prev => ({ ...prev, [co.id]: Number(e.target.value) }))} style={{ width: 100, accentColor: lc, cursor: "pointer" }} /> {ls > 0 ? ls : "—"}
{ls > 0 &&
{ls}/10 · {ls * LOCATION_WEIGHT} pts
}
Weighted Score
{companies.map(co => { const d = weightedScores.find(w => w.company.id === co.id); return (
0 ? co.color : "#334155" }}> {d && d.rawScore > 0 ? d.rawScore.toFixed(1) : "—"} {d && d.rawScore > 0 && /10}
{d ? d.completeness : 0}% complete
{/* Notes */}
Qualitative Notes
{companies.map(co => (
{co.name}