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/map/components/CoursePanel.jsx b/frontend/app/map/components/CoursePanel.jsx index 0b73d6a..998fdbc 100644 --- a/frontend/app/map/components/CoursePanel.jsx +++ b/frontend/app/map/components/CoursePanel.jsx @@ -267,77 +267,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 }) => (
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 → -
-
-)