Skip to content

Commit b417322

Browse files
committed
Add fetchOutlines API utility and update CourseGraph and page components for improved course data handling
1 parent dc21c04 commit b417322

4 files changed

Lines changed: 89 additions & 28 deletions

File tree

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
44
import { type Course } from '../../../../../utils/courseApi';
5+
import Link from "next/link";
56

67
type SFUPrereqNode = {
78
type: string;
@@ -1287,8 +1288,8 @@ export default function CourseGraph({ courseId: initialCourseId, courses }: Cour
12871288
>
12881289
{/* Home button - top left */}
12891290
<div data-pan-block className="absolute top-2 left-2 z-20">
1290-
<a
1291-
href="/sfu"
1291+
<Link
1292+
href="/sfu/courses"
12921293
className="inline-flex items-center gap-2 bg-white/90 dark:bg-neutral-800/90 backdrop-blur rounded-lg border border-gray-200 dark:border-gray-700 shadow px-3 py-2 text-xs hover:bg-neutral-50 dark:hover:bg-neutral-700 text-gray-800 dark:text-gray-200"
12931294
title="Home"
12941295
>
@@ -1297,7 +1298,7 @@ export default function CourseGraph({ courseId: initialCourseId, courses }: Cour
12971298
<path d="M5 10v9a1 1 0 001 1h4v-6h4v6h4a1 1 0 001-1v-9" />
12981299
</svg>
12991300
<span className="hidden sm:inline">Home</span>
1300-
</a>
1301+
</Link>
13011302
</div>
13021303
{/* Fullscreen button - top right */}
13031304
<div data-pan-block className="absolute top-2 right-2 z-20">

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

Lines changed: 38 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import CourseGraph from './CourseGraph';
22
import { getAllCourses, getCourseByDeptAndNumber } from '../../../../../utils/courseApi';
3+
import { fetchOutlines } from '../../../../../utils/sfuOutlinesApi';
34

45
interface PageProps {
56
params: Promise<{
@@ -11,61 +12,74 @@ interface PageProps {
1112
// Generate static paths for all courses from API
1213
export async function generateStaticParams() {
1314
try {
14-
const courses = await getAllCourses();
15-
const params = courses.map((course) => ({
16-
// Keep original casing to match links like /sfu/courses/CMPT/105W
17-
dept: String(course.dept),
18-
number: String(course.number),
15+
// Prefer official outlines API to enumerate ALL courses
16+
const outlines = await fetchOutlines();
17+
const params = outlines.map((o) => ({
18+
dept: String(o.dept).toLowerCase(),
19+
number: String(o.number).toLowerCase(),
1920
}));
20-
console.log(`Generated ${params.length} static params for courses`);
21+
// Fallback: if outlines unexpectedly empty, use crowdsourced list
22+
if (params.length === 0) {
23+
const courses = await getAllCourses();
24+
return courses.map((course) => ({ dept: String(course.dept).toLowerCase(), number: String(course.number).toLowerCase() }));
25+
}
2126
return params;
2227
} catch (error) {
23-
console.error('Error generating static params:', error);
24-
return [];
28+
console.error('Error generating static params from outlines:', error);
29+
try {
30+
const courses = await getAllCourses();
31+
return courses.map((course) => ({ dept: String(course.dept).toLowerCase(), number: String(course.number).toLowerCase() }));
32+
} catch (e) {
33+
console.error('Error falling back to crowdsourced params:', e);
34+
return [];
35+
}
2536
}
2637
}
2738

2839
// With static export, only the paths from generateStaticParams are valid
29-
// export const dynamicParams = false;
40+
export const dynamicParams = false;
3041
// Ensure this route is fully static
31-
// export const revalidate = false;
42+
export const revalidate = false;
3243

3344
export default async function CoursePage({ params }: PageProps) {
3445
const resolvedParams = await params;
35-
const courseId = `${resolvedParams.dept.toUpperCase()} ${resolvedParams.number}`;
36-
37-
// Fetch the course and all courses from API at build time
38-
const [course, allCourses] = await Promise.all([
39-
getCourseByDeptAndNumber(resolvedParams.dept, resolvedParams.number),
40-
getAllCourses()
46+
const courseId = `${resolvedParams.dept.toUpperCase()} ${resolvedParams.number.toUpperCase()}`;
47+
48+
// Fetch the crowdsourced data (may not include every course)
49+
const [allCourses, course] = await Promise.all([
50+
getAllCourses(),
51+
getCourseByDeptAndNumber(resolvedParams.dept, resolvedParams.number.toUpperCase()),
4152
]);
4253

54+
// If this course exists in outlines but not in our crowdsourced data, show a helpful message
4355
if (!course) {
4456
return (
4557
<div className="flex items-center justify-center h-screen bg-white dark:bg-black">
46-
<div className="text-center">
47-
<h1 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
48-
Course Not Found
58+
<div className="text-center px-6 max-w-xl">
59+
<h1 className="text-2xl font-bold text-gray-900 dark:text-white mb-3">
60+
No prerequisite data yet
4961
</h1>
50-
<p className="text-gray-600 dark:text-gray-400">
51-
No prerequisite data available for {courseId}
62+
<p className="text-gray-700 dark:text-gray-300 mb-2">
63+
We have not yet parsed {courseId}.
5264
</p>
5365
<p className="text-gray-600 dark:text-gray-400">
54-
Contribute today at{' '}
66+
You can help by contributing at
67+
{' '}
5568
<a
5669
href="https://crowdsource.sfucourses.com"
5770
target="_blank"
5871
rel="noopener noreferrer"
59-
className="text-blue-500 hover:underline"
72+
className="underline hover:opacity-80"
6073
>
6174
crowdsource.sfucourses.com
62-
</a>
75+
</a>.
6376
</p>
6477
</div>
6578
</div>
6679
);
6780
}
6881

82+
// Render the graph regardless; the component will indicate when data is missing for a course
6983
return (
7084
<div className="w-full h-screen bg-white dark:bg-black">
7185
<CourseGraph courseId={courseId} courses={allCourses} />

src/app/sfu/courses/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export default async function SFUCoursesPage() {
4040
.map((course) => (
4141
<Link
4242
key={course.id}
43-
href={`/sfu/courses/${course.dept}/${course.number}`}
43+
href={`/sfu/courses/${course.dept.toLowerCase()}/${course.number.toLowerCase()}`}
4444
className="block p-4 border border-gray-200 dark:border-gray-600 rounded-lg hover:border-blue-400 dark:hover:border-blue-500 hover:bg-blue-50 dark:hover:bg-gray-800 transition-colors bg-white dark:bg-gray-900"
4545
>
4646
<div className="font-semibold text-gray-900 dark:text-white">

src/utils/sfuOutlinesApi.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
export type SfuOutline = {
2+
corequisites?: string;
3+
degreeLevel?: string;
4+
deliveryMethod?: string;
5+
dept: string;
6+
description?: string;
7+
designation?: string;
8+
notes?: string;
9+
number: string;
10+
prerequisites?: string;
11+
title?: string;
12+
units?: string;
13+
offerings?: Array<{
14+
instructors?: string[];
15+
term?: string;
16+
}>;
17+
};
18+
19+
// Fetch outlines; if dept/number omitted, API is expected to return all outlines
20+
export async function fetchOutlines(params?: { dept?: string; number?: string }): Promise<SfuOutline[]> {
21+
const base = 'https://api.sfucourses.com/v1/rest/outlines';
22+
const url = new URL(base);
23+
if (params?.dept != null) url.searchParams.set('dept', params.dept.toUpperCase());
24+
if (params?.number != null) url.searchParams.set('number', params.number);
25+
26+
const res = await fetch(url.toString(), {
27+
headers: { Accept: 'application/json' },
28+
// Cache at build time; for client-side it will use the browser cache
29+
cache: 'force-cache',
30+
});
31+
if (!res.ok) {
32+
// Return empty array on failure so callers can treat as not found
33+
return [];
34+
}
35+
const data = await res.json();
36+
return Array.isArray(data) ? (data as SfuOutline[]) : [];
37+
}
38+
39+
export async function isValidCourseSFU(dept: string, number: string): Promise<boolean> {
40+
try {
41+
const outlines = await fetchOutlines({ dept, number });
42+
return outlines.length > 0;
43+
} catch {
44+
return false;
45+
}
46+
}

0 commit comments

Comments
 (0)