@@ -8,15 +8,25 @@ type SFUPrereqNode = {
88 type : string ;
99 logic ?: string ;
1010 children ?: SFUPrereqNode [ ] ;
11+ // course fields
1112 department ?: string ;
1213 number ?: string ;
1314 minGrade ?: string ;
14- course ?: string ;
15+ canBeTakenConcurrently ?: string ;
1516 orEquivalent ?: string ;
17+ // HSCourse fields
18+ course ?: string ;
19+ // creditCount/courseCount fields
1620 count ?: number ;
1721 level ?: string ;
1822 credits ?: number ;
23+ // permission/other fields
1924 note ?: string ;
25+ // program fields
26+ program ?: string ;
27+ // CGPA/UDGPA fields
28+ minCGPA ?: number ;
29+ minUDGPA ?: number ;
2030} ;
2131
2232type PrereqNode = SFUPrereqNode ;
@@ -186,15 +196,72 @@ function NodeCard({
186196 const renderNode = ( node : PrereqNode ) : React . ReactNode => {
187197 if ( ! node ) return null ;
188198
199+ // course: department, number, minGrade? (optional), canBeTakenConcurrently? (optional), orEquivalent? (optional)
189200 if ( node . type === "course" && node . department && node . number ) {
190201 const courseId = `${ node . department } ${ node . number } ` ;
191- return renderTranscript ( courseId , node . minGrade ) ;
202+ const extras : string [ ] = [ ] ;
203+ if ( node . minGrade ) extras . push ( `min: ${ node . minGrade } ` ) ;
204+ if ( node . canBeTakenConcurrently === "true" ) extras . push ( "concurrent" ) ;
205+ if ( node . orEquivalent === "true" ) extras . push ( "or equiv" ) ;
206+
207+ return (
208+ < div className = "inline-flex items-center gap-1" >
209+ { renderTranscript ( courseId , undefined ) }
210+ { extras . length > 0 && (
211+ < span className = "text-gray-500 dark:text-gray-400 text-[10px]" >
212+ ({ extras . join ( ", " ) } )
213+ </ span >
214+ ) }
215+ </ div >
216+ ) ;
192217 }
193218
219+ // HSCourse: course, minGrade? (optional), orEquivalent? (optional)
194220 if ( node . type === "HSCourse" && node . course ) {
195- return renderTranscript ( node . course , node . minGrade ) ;
221+ const extras : string [ ] = [ ] ;
222+ if ( node . minGrade ) extras . push ( `min: ${ node . minGrade } ` ) ;
223+ if ( node . orEquivalent === "true" ) extras . push ( "or equiv" ) ;
224+
225+ return (
226+ < div className = "inline-flex items-center gap-1" >
227+ { renderTranscript ( node . course , undefined ) }
228+ { extras . length > 0 && (
229+ < span className = "text-gray-500 dark:text-gray-400 text-[10px]" >
230+ ({ extras . join ( ", " ) } )
231+ </ span >
232+ ) }
233+ </ div >
234+ ) ;
196235 }
197236
237+ // program: program (required)
238+ if ( node . type === "program" && ( node as any ) . program ) {
239+ return (
240+ < div className = "text-gray-700 dark:text-gray-300 text-xs" >
241+ Must be in < span className = "font-medium" > { ( node as any ) . program } </ span > program
242+ </ div >
243+ ) ;
244+ }
245+
246+ // CGPA: minCGPA (required)
247+ if ( node . type === "CGPA" && ( node as any ) . minCGPA != null ) {
248+ return (
249+ < div className = "text-gray-700 dark:text-gray-300 text-xs" >
250+ Min CGPA: < span className = "font-medium" > { ( node as any ) . minCGPA } </ span >
251+ </ div >
252+ ) ;
253+ }
254+
255+ // UDGPA: minUDGPA (required)
256+ if ( node . type === "UDGPA" && ( node as any ) . minUDGPA != null ) {
257+ return (
258+ < div className = "text-gray-700 dark:text-gray-300 text-xs" >
259+ Min Upper Division GPA: < span className = "font-medium" > { ( node as any ) . minUDGPA } </ span >
260+ </ div >
261+ ) ;
262+ }
263+
264+ // group: logic (ALL_OF, ONE_OF, TWO_OF), children
198265 if ( node . type === "group" ) {
199266 type Child = Exclude < PrereqNode , null > ;
200267 const children = ( node . children ?? [ ] ) . filter ( Boolean ) as Child [ ] ;
@@ -203,6 +270,27 @@ function NodeCard({
203270 const isCourseNode = ( n : PrereqNode ) : boolean =>
204271 ! ! n && ( ( n . type === "course" && ! ! n . department && ! ! n . number ) || ( n . type === "HSCourse" && ! ! n . course ) ) ;
205272
273+ // Handle TWO_OF logic
274+ if ( node . logic === "TWO_OF" ) {
275+ return (
276+ < div className = "flex flex-col gap-1 text-xs" >
277+ < div className = "text-gray-600 dark:text-gray-400 text-[10px] font-medium mb-0.5" >
278+ (Choose 2 of the following)
279+ </ div >
280+ { children . map ( ( c , idx ) => (
281+ < React . Fragment key = { idx } >
282+ < div className = "pl-2 border-l border-dashed border-gray-300 dark:border-gray-600" >
283+ { renderNode ( c ) }
284+ </ div >
285+ { idx < children . length - 1 ? (
286+ < div className = "text-gray-500 dark:text-gray-400 pl-2" > or</ div >
287+ ) : null }
288+ </ React . Fragment >
289+ ) ) }
290+ </ div >
291+ ) ;
292+ }
293+
206294 if ( node . logic === "ONE_OF" ) {
207295 const allCourses = children . every ( ( c ) => isCourseNode ( c ) ) ;
208296 if ( allCourses ) {
@@ -263,15 +351,47 @@ function NodeCard({
263351 </ div >
264352 ) ;
265353 }
266- if ( node . type === "creditCount" ) {
267- return < div className = "text-gray-600 dark:text-gray-400 text-xs" > { node . credits } credits</ div > ;
354+
355+ // creditCount: credits (required), department? (optional), level? (optional), minGrade? (optional), canBeTakenConcurrently? (optional)
356+ if ( node . type === "creditCount" && node . credits != null ) {
357+ const parts : string [ ] = [ `${ node . credits } credits` ] ;
358+ if ( node . department ) {
359+ const deptStr = Array . isArray ( node . department ) ? node . department . join ( "/" ) : node . department ;
360+ parts . push ( `in ${ deptStr } ` ) ;
361+ }
362+ if ( node . level ) parts . push ( `(${ node . level } )` ) ;
363+ if ( node . minGrade ) parts . push ( `min: ${ node . minGrade } ` ) ;
364+ if ( node . canBeTakenConcurrently === "true" ) parts . push ( "(concurrent)" ) ;
365+ return < div className = "text-gray-600 dark:text-gray-400 text-xs" > { parts . join ( " " ) } </ div > ;
268366 }
269- if ( node . type === "courseCount" ) {
270- return < div className = "text-gray-600 dark:text-gray-400 text-xs" > { node . count } courses from { node . department } { node . level } </ div > ;
367+
368+ // courseCount: count (required), department? (optional), level? (optional), minGrade? (optional), canBeTakenConcurrently? (optional)
369+ if ( node . type === "courseCount" && node . count != null ) {
370+ const parts : string [ ] = [ `${ node . count } ${ node . count === 1 ? "course" : "courses" } ` ] ;
371+ if ( node . department ) {
372+ const deptStr = Array . isArray ( node . department ) ? node . department . join ( "/" ) : node . department ;
373+ parts . push ( `from ${ deptStr } ` ) ;
374+ }
375+ if ( node . level ) parts . push ( `(${ node . level } )` ) ;
376+ if ( node . minGrade ) parts . push ( `min: ${ node . minGrade } ` ) ;
377+ if ( node . canBeTakenConcurrently === "true" ) parts . push ( "(concurrent)" ) ;
378+ return < div className = "text-gray-600 dark:text-gray-400 text-xs" > { parts . join ( " " ) } </ div > ;
271379 }
272- if ( node . type === "permission" || node . type === "other" ) {
273- return < div className = "text-gray-800 dark:text-gray-200 text-xs" > { node . note } </ div > ;
380+
381+ // permission: note (required)
382+ if ( node . type === "permission" && node . note ) {
383+ return (
384+ < div className = "text-gray-700 dark:text-gray-300 text-xs" >
385+ < span className = "font-medium" > Permission required:</ span > { node . note }
386+ </ div >
387+ ) ;
274388 }
389+
390+ // other: note (required)
391+ if ( node . type === "other" && node . note ) {
392+ return < div className = "text-gray-700 dark:text-gray-300 text-xs" > { node . note } </ div > ;
393+ }
394+
275395 return null ;
276396 } ;
277397
0 commit comments