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 && (
))}
)}
{/* ── Header ──────────────────────────────────────────────────────── */}
{/* ── Body ────────────────────────────────────────────────────────── */}
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(); }}
/>
Opportunity Matrix
Weighted decision framework · {companies.length} {companies.length === 1 ? "opportunity" : "opportunities"} · {criteria.length + 1} criteria
{["matrix", "results", "settings"].map(t => (
))}
{/* ══ SCORE TAB ═════════════════════════════════════════════════ */}
{activeTab === "matrix" && (
)}
)}
{/* ══ SETTINGS TAB ══════════════════════════════════════════════ */}
{activeTab === "settings" && (
);
}
{companies.length === 0 ? (
) : (
<>
{/* Notes */}
>
)}
)}
{/* ══ RESULTS TAB ═══════════════════════════════════════════════ */}
{activeTab === "results" && (
⊞
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}
))}
| Criterion | Wt | {companies.map(c => ({c.name} | ))}|
|---|---|---|---|
|
{c.name}
{c.description && {c.description} }
|
{c.weight}
|
{companies.map(co => {
const s = getScore(co.id, c.id);
return (
{[1,2,3,4,5].map(v =>
{s > 0 && {SCORE_LABELS[s]} · {s * c.weight} pts }
|
);
})}
|
|
📍 Location Desirability
1 = Least desirable · 10 = Most desirable
|
{LOCATION_WEIGHT}
|
{companies.map(co => {
const ls = getLocScore(co.id);
const lc = locColor(ls);
return (
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
|
);
})}
Qualitative Notes
{companies.map(co => (
))}
{co.name}
Ranked by weighted score (0–10) · Criteria (1–5) normalized to 0–10 for fair comparison with location
{/* Cards */}
{weightedScores.map((d, idx) => {
const lc = locColor(d.locScore);
return (
{/* Head-to-head */}
{companies.length > 1 && (
0 ? "1px solid " + d.company.color + "44" : "1px solid #1e293b", position: "relative", overflow: "hidden" }}>
{idx === 0 && d.rawScore > 0 && (
{d.locScore > 0 && (
))}
{d.criteriaScores.filter(cs => cs.score === 0).length > 0 && (
);
})}
Top Pick
)}
#{idx + 1}
{d.company.name}
{d.completeness}% criteria scored
0 ? d.company.color : "#334155", lineHeight: 1 }}>{d.rawScore > 0 ? d.rawScore.toFixed(1) : "—"}
out of 10
📍
Location
· {d.locScore}/10
)}
Score breakdown
{d.criteriaScores.filter(cs => cs.score > 0).sort((a, b) => b.score - a.score).slice(0, 5).map(cs => (
{cs.score}
{cs.criteria.name}
w:{cs.criteria.weight}
{d.criteriaScores.filter(cs => cs.score === 0).length} criteria unscored
)}
{notes[d.company.id] && (
{notes[d.company.id]}
)}
Head-to-Head Comparison
{[
...criteria.map(c => ({ id: c.id, name: c.name, weight: c.weight, isLocation: false })),
{ id: "loc", name: "Location", weight: LOCATION_WEIGHT, isLocation: true },
].map(row => {
const vals = companies.map(co => ({ company: co, raw: row.isLocation ? getLocScore(co.id) : getScore(co.id, row.id), outOf: row.isLocation ? 10 : 5 }));
const maxRaw = Math.max(...vals.map(v => v.raw));
return (
{row.isLocation ? "📍 Location" : row.name}
weight: {row.weight}
{vals.map(({ company, raw, outOf }) => (
))}
);
})}
{company.name}
0 ? (raw / outOf * 100) + "%" : "0%", height: "100%", backgroundColor: raw === maxRaw && raw > 0 ? company.color : company.color + "44", borderRadius: 3, transition: "width 0.4s ease" }} />
0 ? (raw === maxRaw ? company.color : "#64748b") : "#334155", minWidth: 20 }}>{raw || "—"}
{/* Criteria panel */}
{/* Companies panel */}
)}
Evaluation Criteria
setNewCriteriaName(e.target.value)} onKeyDown={e => e.key === "Enter" && addCriteria()} />
{/* Location — fixed, read-only display */}
📍 Location Desirability (fixed)
1–10 slider · 1 = Least desirable · 10 = Most desirable
Weight: {LOCATION_WEIGHT}
{criteria.map(c => (
))}
{editingCriteria === c.id
? setCriteria(p => p.map(cr => cr.id === c.id ? { ...cr, name: e.target.value } : cr))} onBlur={() => setEditingCriteria(null)} autoFocus />
:
setEditingCriteria(c.id)}>{c.name}
}
Weight:
setCriteria(p => p.map(cr => cr.id === c.id ? { ...cr, weight: v } : cr))} />
setCriteria(p => p.map(cr => cr.id === c.id ? { ...cr, description: e.target.value } : cr))} />
Opportunities
setNewCompanyName(e.target.value)} onKeyDown={e => e.key === "Enter" && addCompany()} />
{companies.map((c, idx) => (
{/* Score guide */}
))}
{idx + 1}
{editingCompany === c.id
? setCompanies(p => p.map(co => co.id === c.id ? { ...co, name: e.target.value } : co))} onBlur={() => setEditingCompany(null)} autoFocus />
: setEditingCompany(c.id)}>{c.name}
}
{COMPANY_COLORS.map(color => (
))}
setCompanies(p => p.map(co => co.id === c.id ? { ...co, color } : co))}
style={{ width: 14, height: 14, borderRadius: 3, backgroundColor: color, cursor: "pointer", border: c.color === color ? "2px solid white" : "2px solid transparent", opacity: c.color === color ? 1 : 0.5 }} />
))}
Score Guide
{Object.entries(SCORE_LABELS).map(([s, label]) => (
{s}
{label}
Criteria (1–5) are normalized to 0–10 for fair comparison with location. Weight controls each criterion's influence on the final score.