Show required specialization labels on course buttons
Replace top-level mutual exclusion banner with dynamic per-course "Required for ..." labels derived from specialization data. Labels appear on any course that is a specialization prerequisite, across all elective sets.
This commit is contained in:
@@ -5,7 +5,7 @@ import { SpecializationRanking } from './components/SpecializationRanking';
|
|||||||
import { ModeToggle } from './components/ModeToggle';
|
import { ModeToggle } from './components/ModeToggle';
|
||||||
import { CourseSelection } from './components/CourseSelection';
|
import { CourseSelection } from './components/CourseSelection';
|
||||||
import { CreditLegend } from './components/CreditLegend';
|
import { CreditLegend } from './components/CreditLegend';
|
||||||
import { ModeComparison, MutualExclusionWarnings } from './components/Notifications';
|
import { ModeComparison } from './components/Notifications';
|
||||||
import { optimize } from './solver/optimizer';
|
import { optimize } from './solver/optimizer';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
@@ -53,7 +53,6 @@ function App() {
|
|||||||
|
|
||||||
<ModeToggle mode={state.mode} onSetMode={setMode} />
|
<ModeToggle mode={state.mode} onSetMode={setMode} />
|
||||||
|
|
||||||
<MutualExclusionWarnings pinnedCourses={state.pinnedCourses} />
|
|
||||||
<ModeComparison
|
<ModeComparison
|
||||||
result={optimizationResult}
|
result={optimizationResult}
|
||||||
altResult={altResult}
|
altResult={altResult}
|
||||||
|
|||||||
@@ -1,8 +1,17 @@
|
|||||||
import { ELECTIVE_SETS } from '../data/electiveSets';
|
import { ELECTIVE_SETS } from '../data/electiveSets';
|
||||||
|
import { SPECIALIZATIONS } from '../data/specializations';
|
||||||
import { coursesBySet } from '../data/lookups';
|
import { coursesBySet } from '../data/lookups';
|
||||||
import type { Term } from '../data/types';
|
import type { Term } from '../data/types';
|
||||||
import type { SetAnalysis } from '../solver/decisionTree';
|
import type { SetAnalysis } from '../solver/decisionTree';
|
||||||
|
|
||||||
|
// Reverse map: courseId → specialization names that require it
|
||||||
|
const requiredForSpec: Record<string, string[]> = {};
|
||||||
|
for (const spec of SPECIALIZATIONS) {
|
||||||
|
if (spec.requiredCourseId) {
|
||||||
|
(requiredForSpec[spec.requiredCourseId] ??= []).push(spec.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
interface CourseSelectionProps {
|
interface CourseSelectionProps {
|
||||||
pinnedCourses: Record<string, string | null>;
|
pinnedCourses: Record<string, string | null>;
|
||||||
treeResults: SetAnalysis[];
|
treeResults: SetAnalysis[];
|
||||||
@@ -79,30 +88,37 @@ function ElectiveSet({
|
|||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
|
||||||
{courses.map((course) => {
|
{courses.map((course) => {
|
||||||
const ceiling = ceilingMap.get(course.id);
|
const ceiling = ceilingMap.get(course.id);
|
||||||
|
const reqFor = requiredForSpec[course.id];
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={course.id}
|
key={course.id}
|
||||||
onClick={() => onPin(course.id)}
|
onClick={() => onPin(course.id)}
|
||||||
style={{
|
style={{
|
||||||
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
|
display: 'flex', flexDirection: 'column', alignItems: 'stretch',
|
||||||
textAlign: 'left', padding: '6px 10px',
|
textAlign: 'left', padding: '6px 10px',
|
||||||
border: '1px solid #e5e7eb', borderRadius: '4px',
|
border: '1px solid #e5e7eb', borderRadius: '4px',
|
||||||
background: '#fff', cursor: 'pointer', fontSize: '13px', color: '#333',
|
background: '#fff', cursor: 'pointer', fontSize: '13px', color: '#333',
|
||||||
gap: '8px',
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span style={{ flex: 1 }}>{course.name}</span>
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: '8px' }}>
|
||||||
{ceiling && (
|
<span style={{ flex: 1 }}>{course.name}</span>
|
||||||
<span style={{
|
{ceiling && (
|
||||||
fontSize: '11px', whiteSpace: 'nowrap', fontWeight: 600,
|
<span style={{
|
||||||
color: ceiling.ceilingCount >= 3 ? '#16a34a' : ceiling.ceilingCount >= 2 ? '#2563eb' : '#666',
|
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 && (
|
{ceiling.ceilingCount} spec{ceiling.ceilingCount !== 1 ? 's' : ''}
|
||||||
<span style={{ fontWeight: 400, color: '#888', marginLeft: '3px' }}>
|
{ceiling.ceilingSpecs.length > 0 && (
|
||||||
({ceiling.ceilingSpecs.join(', ')})
|
<span style={{ fontWeight: 400, color: '#888', marginLeft: '3px' }}>
|
||||||
</span>
|
({ceiling.ceilingSpecs.join(', ')})
|
||||||
)}
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{reqFor && (
|
||||||
|
<span style={{ fontSize: '11px', color: '#92400e', marginTop: '2px' }}>
|
||||||
|
Required for {reqFor.join(', ')}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -33,33 +33,3 @@ export function ModeComparison({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MutualExclusionWarnings({ pinnedCourses }: { pinnedCourses: Record<string, string | null> }) {
|
|
||||||
const warnings: string[] = [];
|
|
||||||
const spr4Pin = pinnedCourses['spr4'];
|
|
||||||
|
|
||||||
if (!spr4Pin) {
|
|
||||||
warnings.push('Spring Set 4: choosing Sustainability for Competitive Advantage eliminates Entrepreneurship & Innovation (and vice versa).');
|
|
||||||
} else if (spr4Pin === 'spr4-sustainability') {
|
|
||||||
warnings.push('Entrepreneurship & Innovation is permanently unavailable (required course is in Spring Set 4, pinned to Sustainability).');
|
|
||||||
} else if (spr4Pin === 'spr4-foundations-entrepreneurship') {
|
|
||||||
warnings.push('Sustainable Business & Innovation is permanently unavailable (required course is in Spring Set 4, pinned to Foundations of Entrepreneurship).');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (warnings.length === 0) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={{ marginBottom: '8px' }}>
|
|
||||||
{warnings.map((w, i) => (
|
|
||||||
<div
|
|
||||||
key={i}
|
|
||||||
style={{
|
|
||||||
background: '#fef3c7', border: '1px solid #fcd34d', borderRadius: '6px',
|
|
||||||
padding: '8px 10px', fontSize: '12px', color: '#92400e', marginBottom: '4px',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{w}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user