From 834a787d8d50fd31f39c49de41b12256874320f0 Mon Sep 17 00:00:00 2001 From: Zaid Ahmad <109442753+zaidahmad16@users.noreply.github.com> Date: Wed, 27 May 2026 14:32:39 -0400 Subject: [PATCH 1/2] fix(db): add MATH 2000+ elective requirement to CS programs Added missing 0.5 credit MATH 2000-level-or-above choose block to CS Honours (60) and CS AI/ML Stream (62). Also fixed AI/ML stream choose block which had an empty courses array. --- frontend/app/api/rmp/route.js | 70 ++++-- frontend/app/map/components/CoursePanel.jsx | 222 ++++++++++++++------ 2 files changed, 213 insertions(+), 79 deletions(-) diff --git a/frontend/app/api/rmp/route.js b/frontend/app/api/rmp/route.js index 715ce6f..0df1602 100644 --- a/frontend/app/api/rmp/route.js +++ b/frontend/app/api/rmp/route.js @@ -2,10 +2,21 @@ import { RMPClient } from 'ratemyprofessors-client' import { NextResponse } from 'next/server' const CARLETON_SCHOOL_ID = '1420' +const CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1000 const client = new RMPClient() -// Cache results in memory for the lifetime of the server process -const cache = new Map() +const cache = new Map() // name → { data, expiresAt } + +function getCached(name) { + const entry = cache.get(name) + if (!entry) return null + if (Date.now() > entry.expiresAt) { cache.delete(name); return null } + return entry.data +} + +function setCached(name, data) { + cache.set(name, { data, expiresAt: Date.now() + CACHE_TTL_MS }) +} export async function GET(request) { const { searchParams } = new URL(request.url) @@ -15,29 +26,48 @@ export async function GET(request) { return NextResponse.json({ error: 'name is required' }, { status: 400 }) } - if (cache.has(name)) { - return NextResponse.json(cache.get(name)) - } + const cached = getCached(name) + if (cached) return NextResponse.json(cached) try { const result = await client.searchProfessors(name, CARLETON_SCHOOL_ID) - // Pick the first result that belongs to Carleton const prof = result.professors?.find(p => p.school?.id === CARLETON_SCHOOL_ID) ?? null - const data = prof - ? { - found: true, - name: prof.name, - department: prof.department, - overall_rating: prof.overall_rating, - difficulty: prof.level_of_difficulty, - num_ratings: prof.num_ratings, - would_take_again: prof.percent_take_again >= 0 ? Math.round(prof.percent_take_again) : null, - rmp_url: `https://www.ratemyprofessors.com/professor/${prof.id}`, - } - : { found: false } - - cache.set(name, data) + if (!prof) { + const data = { found: false } + setCached(name, data) + return NextResponse.json(data) + } + + // Fetch up to 5 recent ratings + let ratings = [] + try { + const page = await client.getProfessorRatingsPage(prof.id) + ratings = (page.ratings ?? []).slice(0, 5).map(r => ({ + date: r.date, + comment: r.comment, + quality: r.quality, + difficulty: r.difficulty, + course: r.course_raw ?? null, + tags: r.tags ?? [], + })) + } catch { + // ratings are a bonus; don't fail the whole request + } + + const data = { + found: true, + name: prof.name, + department: prof.department, + overall_rating: prof.overall_rating, + difficulty: prof.level_of_difficulty, + num_ratings: prof.num_ratings, + would_take_again: prof.percent_take_again >= 0 ? Math.round(prof.percent_take_again) : null, + rmp_url: `https://www.ratemyprofessors.com/professor/${prof.id}`, + ratings, + } + + setCached(name, data) return NextResponse.json(data) } catch (err) { console.error('RMP lookup failed for', name, err) diff --git a/frontend/app/map/components/CoursePanel.jsx b/frontend/app/map/components/CoursePanel.jsx index 783f991..e1a632e 100644 --- a/frontend/app/map/components/CoursePanel.jsx +++ b/frontend/app/map/components/CoursePanel.jsx @@ -264,77 +264,181 @@ export const CoursePanel = ({ node, onClose, isMobile }) => { ) } -const ProfCard = ({ name, rmp }) => ( -
+const ProfCard = ({ name, rmp }) => { + const [showReviews, setShowReviews] = useState(false) + const reviews = rmp?.ratings ?? [] + + return (
- - {name} - - {rmp?.found && ( - - RMP ↗ - - )} -
- - {rmp?.found ? (
- - - {rmp.would_take_again != null && ( - - )} - {rmp.num_ratings} rating{rmp.num_ratings !== 1 ? 's' : ''} + {name} + {rmp?.found && ( + + RMP ↗ + + )}
- ) : ( + + {rmp?.found ? ( + <> +
+ + + {rmp.would_take_again != null && ( + + )} + + {rmp.num_ratings} rating{rmp.num_ratings !== 1 ? 's' : ''} + +
+ + {reviews.length > 0 && ( + <> + + + {showReviews && ( +
+ {reviews.map((r, i) => ( + + ))} +
+ )} + + )} + + ) : ( +
+ Not on RateMyProfessors +
+ )} +
+ ) +} + +const ReviewCard = ({ review }) => { + const year = review.date ? new Date(review.date).getFullYear() : null + return ( +
- Not on RateMyProfessors +
+ {review.quality != null && ( + + {review.quality}/5 + + )} + {review.difficulty != null && ( + + Difficulty {review.difficulty}/5 + + )} +
+ + {review.course ? `${review.course}${year ? ` · ${year}` : ''}` : year} +
- )} -
-) + {review.comment && ( +

+ {review.comment} +

+ )} + {review.tags?.length > 0 && ( +
+ {review.tags.map(tag => ( + + {tag} + + ))} +
+ )} + + ) +} const RmpStat = ({ label, value, color }) => (
From 13f83e1dfb5a062f124091ff46288c8e0cae50b2 Mon Sep 17 00:00:00 2001 From: Zaid Ahmad <109442753+zaidahmad16@users.noreply.github.com> Date: Wed, 27 May 2026 14:53:50 -0400 Subject: [PATCH 2/2] feat: RMP reviews, features section, UI polish, and DB fixes --- README.md | 56 +++++------ frontend/app/icon.svg | 31 ++++-- frontend/app/page.js | 217 ++++++++++++++++++++++++------------------ 3 files changed, 170 insertions(+), 134 deletions(-) diff --git a/README.md b/README.md index 389c3f2..ea9e8b8 100644 --- a/README.md +++ b/README.md @@ -1,56 +1,52 @@ # CarletonCourseMap -An interactive course map that helps Carleton University students visualize their program requirements and course prerequisites. See your entire four-year program at a glance—understand which courses block what, which semesters are heavy, and how your choices cascade. +Prerequisite maps for every Carleton undergrad program. Pick a degree, see four years of courses laid out as a graph — what you need, when, and what unlocks what. -**Live site:** https://www.carletoncoursemap.ca +**Live:** https://carletoncoursemap.ca/ | **GitHub:** https://github.com/zaidahmad16/CarletonCourseMap ---- +Used by 500+ Carleton students. -## Why This Exists +--- -Planning a degree is harder than it should be. Carleton's calendar lists requirements, but doesn't show the structure. This tool does. +## What it does -Students shouldn't have to manually trace prerequisites across departments. Advisors shouldn't repeat the same explanations. Course dependencies should be visual, not mental math. +- Prerequisite chains drawn from the actual 2026-2027 calendar +- Courses arranged by year and term, not just listed +- Elective and breadth slots marked so you can see where you have flexibility +- Click any course for the description, credit weight, term offerings, and prerequisites +- Professor info for Fall 2026 / Winter 2027, with RMP ratings, difficulty scores, and recent student reviews +- 240+ programs, including streams and concentrations -CarletonCourseMap puts the actual course structure in front of you—50+ programs, all visualized the same way. +No account needed. Just open it. --- -## What You Get +## Stack -- **See your entire program** - All four years, all requirements, in one interactive map -- **Understand prerequisites** - Click a course to see what you need before taking it -- **Plan ahead** - Know which semesters will be heavy and which courses unlock what -- **Compare programs** - Switch between majors to see how requirements differ -- **Browse 50+ programs** - Computer Science, Biology, Law, Engineering, and more +| Layer | Tech | +|---|---| +| Frontend | Next.js 15 (App Router), ReactFlow, deployed on Vercel | +| Backend | FastAPI, deployed on Fly.io | +| Database | PostgreSQL on Neon | +| Scraper | Python -- Carleton undergraduate calendar + Carleton Central timetable | +| Professor ratings | `ratemyprofessors-client` via a Next.js API route | --- -## How It Works +## How the data gets in -Pick a program. The map shows every course requirement, organized by year and semester. Gray lines connect courses to their prerequisites. That's it—no login, no tracking, no setup. Just open and explore. +One JSON file per department, built by hand from the undergraduate calendar. Each one lists programs, requirements, credit weights, and layout positions for the graph. A scraper pulls Fall 2026 / Winter 2027 instructor assignments from Carleton Central and writes them to the database. Seeding scripts push everything into Neon. --- -## Technical Stack - -- **Frontend:** React 19 with Next.js, deployed on Railway -- **Backend:** FastAPI with rate limiting, input validation, and API key protection -- **Data:** PostgreSQL on Neon, handling 50+ program structures and 10,000+ courses -- **Visualization:** ReactFlow + Dagre for interactive course dependency diagrams -- **Scraper:** Python with parsel (XPath + CSS) — scrapes the Carleton undergraduate calendar and the Registrar's class schedule for Fall 2026 / Winter 2027 offerings +## Coming soon -The backend includes production-grade security: rate limiting, SQL injection protection, CORS restrictions, API authentication, and comprehensive error handling. +Elective recommendations. When your program has a free or breadth elective slot, the site will suggest courses that actually fit, like "any MATH 2000-level or above" resolved into a real list with ratings and prereqs attached. --- +## Not affiliated with Carleton University -## Status - -Live and in active use. Security and performance are continuously monitored. +Student project. Data is from the public 2026-2027 undergraduate calendar. Verify your actual requirements with an advisor and the official calendar at https://calendar.carleton.ca. --- - -## License - -MIT diff --git a/frontend/app/icon.svg b/frontend/app/icon.svg index 6e9445d..025233e 100644 --- a/frontend/app/icon.svg +++ b/frontend/app/icon.svg @@ -1,11 +1,24 @@ - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/app/page.js b/frontend/app/page.js index 9cbeb19..3ffa6a3 100644 --- a/frontend/app/page.js +++ b/frontend/app/page.js @@ -4,21 +4,14 @@ import { useState, useEffect, useMemo, useCallback } from 'react' import Link from 'next/link' +import { useRouter } from 'next/navigation' import { API } from './map/utils/constants' -const trimDegree = (deg = '') => { - const s = deg - .replace(/^(Honours\s+)?Bachelor\s+of\s+\S+\s+/i, '') - .replace(/,?\s*(Honours|Major|Minor|Concentration|Stream)\s*$/i, '') - .trim() - if (!s) return deg.split(' ').slice(0, 4).join(' ') - return s.length > 38 ? s.slice(0, 36) + '…' : s -} export default function Home() { - const [stats, setStats] = useState({ departments: null, programs: null, courses: null }) - const [featured, setFeatured] = useState([]) - const [depts, setDepts] = useState({}) + const router = useRouter() + const [stats, setStats] = useState({ departments: null, programs: null, courses: null }) + const [depts, setDepts] = useState({}) const [allPrograms, setAllPrograms] = useState(null) const [searchQ, setSearchQ] = useState('') const [searchOpen, setSearchOpen] = useState(false) @@ -30,15 +23,13 @@ export default function Home() { .then(d => { if (d) setStats(d) }) .catch(() => {}) - Promise.all([ - fetch(`${API}/departments`).then(r => r.json()), - fetch(`${API}/programs/featured`).then(r => r.json()), - ]).then(([deps, feat]) => { - const deptMap = {} - for (const d of deps) deptMap[d.dept_id] = d.name - setDepts(deptMap) - setFeatured(feat) - }).catch(() => {}) + fetch(`${API}/departments`) + .then(r => r.json()) + .then(deps => { + const deptMap = {} + for (const d of deps) deptMap[d.dept_id] = d.name + setDepts(deptMap) + }).catch(() => {}) }, []) const loadAllPrograms = useCallback(() => { @@ -73,20 +64,17 @@ export default function Home() { }}> @@ -111,21 +99,47 @@ export default function Home() { CarletonCourseMap
- - Open Map - +
+ + + GitHub + + + Open Map + +
{/* hero */} @@ -200,7 +214,7 @@ export default function Home() { }} onKeyDown={e => { if (e.key === 'Enter' && searchResults.length > 0) { - window.location.href = `/map?dept=${searchResults[0].dept_id}&p=${searchResults[0].program_id}` + router.push(`/map?dept=${searchResults[0].dept_id}&p=${searchResults[0].program_id}`) } }} placeholder="Search for a program or department…" @@ -270,13 +284,12 @@ export default function Home() {

- or{' '} - browse all programs → + Browse all programs →

@@ -295,9 +308,9 @@ export default function Home() { gridTemplateColumns: 'repeat(3, 1fr)', gap: 'var(--space-lg)', }}> - - - + + + @@ -343,6 +356,62 @@ export default function Home() { + {/* what's on the map */} +
+

+ What's on the map +

+
    + {[ + 'Prerequisite chain visualization', + 'Four-year semester grid', + 'Elective and breadth slot markers', + 'Course details, credits, and term offerings', + 'Professor info and RateMyProfessors ratings', + 'Recent student reviews per professor', + 'All programs from the 2026-2027 calendar', + 'Works on mobile', + ].map(f => ( +
  • + + {f} +
  • + ))} +
+
+ {/* CTA band */}
( +const Stat = ({ value, label, fixed }) => (
( marginBottom: 6, letterSpacing: '-0.02em', }}> - {value != null ? `${value.toLocaleString()}+` : '—'} + {fixed ? value : (value != null ? `${value.toLocaleString()}+` : '—')}
(
) -// ProgramCard - -const ProgramCard = ({ program }) => ( - -
- {trimDegree(program.degree)} -
-
- {program.dept_name} -
-
- View map → -
-
-)