v1.1.0: Add cancelled course, duplicate prevention, and credit bar ticks
- Mark "Managing Growing Companies" as cancelled with visual indicator and solver exclusion - Prevent selecting duplicate courses across elective sets (e.g., same course in Spring and Summer) - Add 2.5-credit interval tick marks to specialization progress bars - Bump version to 1.1.0 with date display in UI header
This commit is contained in:
@@ -16,6 +16,7 @@ interface CourseSelectionProps {
|
||||
pinnedCourses: Record<string, string | null>;
|
||||
treeResults: SetAnalysis[];
|
||||
treeLoading: boolean;
|
||||
disabledCourseIds: Set<string>;
|
||||
onPin: (setId: string, courseId: string) => void;
|
||||
onUnpin: (setId: string) => void;
|
||||
onClearAll: () => void;
|
||||
@@ -27,6 +28,7 @@ function ElectiveSet({
|
||||
pinnedCourseId,
|
||||
analysis,
|
||||
loading,
|
||||
disabledCourseIds,
|
||||
onPin,
|
||||
onUnpin,
|
||||
}: {
|
||||
@@ -35,6 +37,7 @@ function ElectiveSet({
|
||||
pinnedCourseId: string | null | undefined;
|
||||
analysis?: SetAnalysis;
|
||||
loading: boolean;
|
||||
disabledCourseIds: Set<string>;
|
||||
onPin: (courseId: string) => void;
|
||||
onUnpin: () => void;
|
||||
}) {
|
||||
@@ -101,23 +104,47 @@ function ElectiveSet({
|
||||
}}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
|
||||
{courses.map((course) => {
|
||||
const isCancelled = !!course.cancelled;
|
||||
const isDisabled = disabledCourseIds.has(course.id);
|
||||
const isUnavailable = isCancelled || isDisabled;
|
||||
const ceiling = ceilingMap.get(course.id);
|
||||
const reqFor = requiredForSpec[course.id];
|
||||
const showSkeleton = loading && !analysis;
|
||||
return (
|
||||
<button
|
||||
key={course.id}
|
||||
onClick={() => onPin(course.id)}
|
||||
onClick={isUnavailable ? undefined : () => onPin(course.id)}
|
||||
disabled={isUnavailable}
|
||||
style={{
|
||||
display: 'flex', flexDirection: 'column', alignItems: 'stretch',
|
||||
textAlign: 'left', padding: '6px 10px',
|
||||
border: '1px solid #e5e7eb', borderRadius: '4px',
|
||||
background: '#fff', cursor: 'pointer', fontSize: '13px', color: '#333',
|
||||
background: isUnavailable ? '#f5f5f5' : '#fff',
|
||||
cursor: isUnavailable ? 'default' : 'pointer',
|
||||
fontSize: '13px',
|
||||
color: isUnavailable ? '#bbb' : '#333',
|
||||
pointerEvents: isUnavailable ? 'none' : 'auto',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: '8px' }}>
|
||||
<span style={{ flex: 1 }}>{course.name}</span>
|
||||
{showSkeleton ? (
|
||||
<span style={{
|
||||
flex: 1,
|
||||
textDecoration: isCancelled ? 'line-through' : 'none',
|
||||
fontStyle: isCancelled ? 'italic' : 'normal',
|
||||
}}>
|
||||
{course.name}
|
||||
{isCancelled && (
|
||||
<span style={{ fontSize: '11px', color: '#999', marginLeft: '6px', fontStyle: 'normal', textDecoration: 'none' }}>
|
||||
(Cancelled)
|
||||
</span>
|
||||
)}
|
||||
{!isCancelled && isDisabled && (
|
||||
<span style={{ fontSize: '11px', color: '#999', marginLeft: '6px' }}>
|
||||
(Already selected)
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
{!isUnavailable && showSkeleton ? (
|
||||
<span
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
@@ -129,7 +156,7 @@ function ElectiveSet({
|
||||
animation: 'skeleton-pulse 1.5s ease-in-out infinite',
|
||||
}}
|
||||
/>
|
||||
) : ceiling ? (
|
||||
) : !isUnavailable && ceiling ? (
|
||||
<span style={{
|
||||
fontSize: '11px', whiteSpace: 'nowrap', fontWeight: 600,
|
||||
color: ceiling.ceilingCount >= 3 ? '#16a34a' : ceiling.ceilingCount >= 2 ? '#2563eb' : '#666',
|
||||
@@ -143,7 +170,7 @@ function ElectiveSet({
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
{reqFor && (
|
||||
{reqFor && !isUnavailable && (
|
||||
<span style={{ fontSize: '11px', color: '#92400e', marginTop: '2px' }}>
|
||||
Required for {reqFor.join(', ')}
|
||||
</span>
|
||||
@@ -159,7 +186,7 @@ function ElectiveSet({
|
||||
|
||||
const skeletonStyle = `@keyframes skeleton-pulse { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }`;
|
||||
|
||||
export function CourseSelection({ pinnedCourses, treeResults, treeLoading, onPin, onUnpin, onClearAll }: CourseSelectionProps) {
|
||||
export function CourseSelection({ pinnedCourses, treeResults, treeLoading, disabledCourseIds, onPin, onUnpin, onClearAll }: CourseSelectionProps) {
|
||||
const terms: Term[] = ['Spring', 'Summer', 'Fall'];
|
||||
const hasPinned = Object.keys(pinnedCourses).length > 0;
|
||||
|
||||
@@ -200,6 +227,7 @@ export function CourseSelection({ pinnedCourses, treeResults, treeLoading, onPin
|
||||
pinnedCourseId={pinnedCourses[set.id]}
|
||||
analysis={treeBySet.get(set.id)}
|
||||
loading={treeLoading}
|
||||
disabledCourseIds={disabledCourseIds}
|
||||
onPin={(courseId) => onPin(set.id, courseId)}
|
||||
onUnpin={() => onUnpin(set.id)}
|
||||
/>
|
||||
|
||||
@@ -33,6 +33,12 @@ function CreditBar({ allocated, potential, threshold }: { allocated: number; pot
|
||||
const allocPct = Math.min((allocated / maxWidth) * 100, 100);
|
||||
const potentialPct = Math.min((potential / maxWidth) * 100, 100);
|
||||
|
||||
// Generate tick marks at 2.5 credit intervals
|
||||
const ticks: number[] = [];
|
||||
for (let t = 2.5; t < maxWidth; t += 2.5) {
|
||||
ticks.push(t);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ position: 'relative', height: '6px', background: '#e5e7eb', borderRadius: '3px', marginTop: '4px' }}>
|
||||
{potential > allocated && (
|
||||
@@ -51,11 +57,22 @@ function CreditBar({ allocated, potential, threshold }: { allocated: number; pot
|
||||
borderRadius: '3px', transition: 'width 300ms ease-out',
|
||||
}}
|
||||
/>
|
||||
{/* Tick marks at 2.5 credit intervals — rendered above bar fills */}
|
||||
{ticks.map((t) => (
|
||||
<div
|
||||
key={t}
|
||||
style={{
|
||||
position: 'absolute', left: `${(t / maxWidth) * 100}%`, top: 0,
|
||||
width: '1px', height: '6px', background: 'rgba(0,0,0,0.2)',
|
||||
zIndex: 1, transition: 'left 300ms ease-out',
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute', left: `${(threshold / maxWidth) * 100}%`, top: '-2px',
|
||||
width: '2px', height: '10px', background: '#666',
|
||||
transition: 'left 300ms ease-out',
|
||||
zIndex: 2, transition: 'left 300ms ease-out',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user