Skip to content

Commit 065cb74

Browse files
committed
render all requirement types
1 parent ea89947 commit 065cb74

1 file changed

Lines changed: 129 additions & 9 deletions

File tree

src/app/sfu/courses/[dept]/[number]/CourseGraph.tsx

Lines changed: 129 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -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

2232
type 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

Comments
 (0)