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:
111
app/src/components/CourseSelection.tsx
Normal file
111
app/src/components/CourseSelection.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user