UI improvements: responsive layout, unified panels, credit legend
- Add responsive 2-panel layout (mobile single-col, tablet/desktop grid) - Unify specialization ranking with credit bars, status badges, and expandable allocation breakdowns (remove standalone ResultsDashboard) - Inline decision tree ceiling data on course buttons with spec counts - Add Clear All button to reset all course selections - Add collapsible CreditLegend explaining bars, badges, and limits - Extract ModeComparison and MutualExclusionWarnings to Notifications - Add useMediaQuery hook with matchMedia-based breakpoint detection
This commit is contained in:
@@ -1,23 +1,31 @@
|
||||
import { ELECTIVE_SETS } from '../data/electiveSets';
|
||||
import { coursesBySet } from '../data/lookups';
|
||||
import type { Term } from '../data/types';
|
||||
import type { SetAnalysis } from '../solver/decisionTree';
|
||||
|
||||
interface CourseSelectionProps {
|
||||
pinnedCourses: Record<string, string | null>;
|
||||
treeResults: SetAnalysis[];
|
||||
treeLoading: boolean;
|
||||
onPin: (setId: string, courseId: string) => void;
|
||||
onUnpin: (setId: string) => void;
|
||||
onClearAll: () => void;
|
||||
}
|
||||
|
||||
function ElectiveSet({
|
||||
setId,
|
||||
setName,
|
||||
pinnedCourseId,
|
||||
analysis,
|
||||
loading,
|
||||
onPin,
|
||||
onUnpin,
|
||||
}: {
|
||||
setId: string;
|
||||
setName: string;
|
||||
pinnedCourseId: string | null | undefined;
|
||||
analysis?: SetAnalysis;
|
||||
loading: boolean;
|
||||
onPin: (courseId: string) => void;
|
||||
onUnpin: () => void;
|
||||
}) {
|
||||
@@ -25,6 +33,12 @@ function ElectiveSet({
|
||||
const isPinned = pinnedCourseId != null;
|
||||
const pinnedCourse = isPinned ? courses.find((c) => c.id === pinnedCourseId) : null;
|
||||
|
||||
// Build a map from courseId to ceiling choice data
|
||||
const ceilingMap = new Map(
|
||||
(analysis?.choices ?? []).map((ch) => [ch.courseId, ch]),
|
||||
);
|
||||
const hasHighImpact = analysis && analysis.impact > 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
@@ -36,17 +50,21 @@ function ElectiveSet({
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '8px' }}>
|
||||
<h4 style={{ fontSize: '13px', margin: 0, color: '#444' }}>{setName}</h4>
|
||||
<h4 style={{ fontSize: '13px', margin: 0, color: '#444' }}>
|
||||
{setName}
|
||||
{!isPinned && hasHighImpact && (
|
||||
<span style={{ fontSize: '11px', color: '#d97706', marginLeft: '8px', fontWeight: 400 }}>high impact</span>
|
||||
)}
|
||||
{!isPinned && loading && !analysis && (
|
||||
<span style={{ fontSize: '11px', color: '#888', marginLeft: '8px', fontWeight: 400 }}>analyzing...</span>
|
||||
)}
|
||||
</h4>
|
||||
{isPinned && (
|
||||
<button
|
||||
onClick={onUnpin}
|
||||
style={{
|
||||
fontSize: '11px',
|
||||
border: 'none',
|
||||
background: 'none',
|
||||
color: '#3b82f6',
|
||||
cursor: 'pointer',
|
||||
padding: '2px 4px',
|
||||
fontSize: '11px', border: 'none', background: 'none',
|
||||
color: '#3b82f6', cursor: 'pointer', padding: '2px 4px',
|
||||
}}
|
||||
>
|
||||
clear
|
||||
@@ -59,36 +77,66 @@ function ElectiveSet({
|
||||
</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>
|
||||
))}
|
||||
{courses.map((course) => {
|
||||
const ceiling = ceilingMap.get(course.id);
|
||||
return (
|
||||
<button
|
||||
key={course.id}
|
||||
onClick={() => onPin(course.id)}
|
||||
style={{
|
||||
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
|
||||
textAlign: 'left', padding: '6px 10px',
|
||||
border: '1px solid #e5e7eb', borderRadius: '4px',
|
||||
background: '#fff', cursor: 'pointer', fontSize: '13px', color: '#333',
|
||||
gap: '8px',
|
||||
}}
|
||||
>
|
||||
<span style={{ flex: 1 }}>{course.name}</span>
|
||||
{ceiling && (
|
||||
<span style={{
|
||||
fontSize: '11px', whiteSpace: 'nowrap', fontWeight: 600,
|
||||
color: ceiling.ceilingCount >= 3 ? '#16a34a' : ceiling.ceilingCount >= 2 ? '#2563eb' : '#666',
|
||||
}}>
|
||||
{ceiling.ceilingCount} spec{ceiling.ceilingCount !== 1 ? 's' : ''}
|
||||
{ceiling.ceilingSpecs.length > 0 && (
|
||||
<span style={{ fontWeight: 400, color: '#888', marginLeft: '3px' }}>
|
||||
({ceiling.ceilingSpecs.join(', ')})
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function CourseSelection({ pinnedCourses, onPin, onUnpin }: CourseSelectionProps) {
|
||||
export function CourseSelection({ pinnedCourses, treeResults, treeLoading, onPin, onUnpin, onClearAll }: CourseSelectionProps) {
|
||||
const terms: Term[] = ['Spring', 'Summer', 'Fall'];
|
||||
const hasPinned = Object.keys(pinnedCourses).length > 0;
|
||||
|
||||
// Index tree results by setId for O(1) lookup
|
||||
const treeBySet = new Map(treeResults.map((a) => [a.setId, a]));
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 style={{ fontSize: '16px', marginBottom: '12px' }}>Course Selection</h2>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '12px' }}>
|
||||
<h2 style={{ fontSize: '16px', margin: 0 }}>Course Selection</h2>
|
||||
{hasPinned && (
|
||||
<button
|
||||
onClick={onClearAll}
|
||||
style={{
|
||||
fontSize: '12px', border: 'none', background: 'none',
|
||||
color: '#ef4444', cursor: 'pointer', padding: '2px 6px',
|
||||
}}
|
||||
>
|
||||
Clear All
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{terms.map((term) => (
|
||||
<div key={term} style={{ marginBottom: '16px' }}>
|
||||
<h3 style={{ fontSize: '13px', color: '#888', marginBottom: '8px', textTransform: 'uppercase', letterSpacing: '0.5px' }}>
|
||||
@@ -100,6 +148,8 @@ export function CourseSelection({ pinnedCourses, onPin, onUnpin }: CourseSelecti
|
||||
setId={set.id}
|
||||
setName={set.name}
|
||||
pinnedCourseId={pinnedCourses[set.id]}
|
||||
analysis={treeBySet.get(set.id)}
|
||||
loading={treeLoading}
|
||||
onPin={(courseId) => onPin(set.id, courseId)}
|
||||
onUnpin={() => onUnpin(set.id)}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user