Implement EMBA Specialization Solver web app

Full React+TypeScript app with LP-based optimization engine,
drag-and-drop specialization ranking (with touch/arrow support),
course selection UI, results dashboard with decision tree, and
two optimization modes (maximize-count, priority-order).
This commit is contained in:
2026-02-28 20:43:00 -05:00
parent e62afa631b
commit 9e00901179
43 changed files with 10098 additions and 0 deletions

View File

@@ -0,0 +1,111 @@
import { ELECTIVE_SETS } from '../data/electiveSets';
import { coursesBySet } from '../data/lookups';
import type { Term } from '../data/types';
interface CourseSelectionProps {
pinnedCourses: Record<string, string | null>;
onPin: (setId: string, courseId: string) => void;
onUnpin: (setId: string) => void;
}
function ElectiveSet({
setId,
setName,
pinnedCourseId,
onPin,
onUnpin,
}: {
setId: string;
setName: string;
pinnedCourseId: string | null | undefined;
onPin: (courseId: string) => void;
onUnpin: () => void;
}) {
const courses = coursesBySet[setId];
const isPinned = pinnedCourseId != null;
const pinnedCourse = isPinned ? courses.find((c) => c.id === pinnedCourseId) : null;
return (
<div
style={{
border: isPinned ? '1px solid #3b82f6' : '1px dashed #ccc',
borderRadius: '8px',
padding: '12px',
marginBottom: '8px',
background: isPinned ? '#eff6ff' : '#fafafa',
}}
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '8px' }}>
<h4 style={{ fontSize: '13px', margin: 0, color: '#444' }}>{setName}</h4>
{isPinned && (
<button
onClick={onUnpin}
style={{
fontSize: '11px',
border: 'none',
background: 'none',
color: '#3b82f6',
cursor: 'pointer',
padding: '2px 4px',
}}
>
clear
</button>
)}
</div>
{isPinned ? (
<div style={{ fontSize: '14px', fontWeight: 600, color: '#1e40af' }}>
{pinnedCourse?.name}
</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
{courses.map((course) => (
<button
key={course.id}
onClick={() => onPin(course.id)}
style={{
textAlign: 'left',
padding: '6px 10px',
border: '1px solid #e5e7eb',
borderRadius: '4px',
background: '#fff',
cursor: 'pointer',
fontSize: '13px',
color: '#333',
}}
>
{course.name}
</button>
))}
</div>
)}
</div>
);
}
export function CourseSelection({ pinnedCourses, onPin, onUnpin }: CourseSelectionProps) {
const terms: Term[] = ['Spring', 'Summer', 'Fall'];
return (
<div>
<h2 style={{ fontSize: '16px', marginBottom: '12px' }}>Course Selection</h2>
{terms.map((term) => (
<div key={term} style={{ marginBottom: '16px' }}>
<h3 style={{ fontSize: '13px', color: '#888', marginBottom: '8px', textTransform: 'uppercase', letterSpacing: '0.5px' }}>
{term}
</h3>
{ELECTIVE_SETS.filter((s) => s.term === term).map((set) => (
<ElectiveSet
key={set.id}
setId={set.id}
setName={set.name}
pinnedCourseId={pinnedCourses[set.id]}
onPin={(courseId) => onPin(set.id, courseId)}
onUnpin={() => onUnpin(set.id)}
/>
))}
</div>
))}
</div>
);
}