Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 26 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
@@ -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
31 changes: 22 additions & 9 deletions frontend/app/icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
222 changes: 163 additions & 59 deletions frontend/app/map/components/CoursePanel.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -267,77 +267,181 @@ export const CoursePanel = ({ node, onClose, isMobile }) => {
)
}

const ProfCard = ({ name, rmp }) => (
<div style={{
background: 'var(--color-paper-2)',
border: '1px solid var(--color-rule)',
borderRadius: 8,
padding: '10px 12px',
marginBottom: 8,
}}>
const ProfCard = ({ name, rmp }) => {
const [showReviews, setShowReviews] = useState(false)
const reviews = rmp?.ratings ?? []

return (
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'flex-start',
gap: 8,
background: 'var(--color-paper-2)',
border: '1px solid var(--color-rule)',
borderRadius: 8,
padding: '10px 12px',
marginBottom: 8,
}}>
<span style={{
fontSize: 'var(--text-sm)',
fontWeight: 600,
color: 'var(--color-ink)',
lineHeight: 1.3,
}}>
{name}
</span>
{rmp?.found && (
<a
href={rmp.rmp_url}
target="_blank"
rel="noopener noreferrer"
style={{
fontSize: 'var(--text-xs)',
color: 'var(--color-accent)',
textDecoration: 'none',
whiteSpace: 'nowrap',
flexShrink: 0,
}}
>
RMP ↗
</a>
)}
</div>

{rmp?.found ? (
<div style={{
display: 'flex',
gap: 'var(--space-sm)',
marginTop: 6,
flexWrap: 'wrap',
justifyContent: 'space-between',
alignItems: 'flex-start',
gap: 8,
}}>
<RmpStat label="Rating" value={rmp.overall_rating?.toFixed(1)} outOf={5} color={ratingColor(rmp.overall_rating)} />
<RmpStat label="Difficulty" value={rmp.difficulty?.toFixed(1)} outOf={5} color={difficultyColor(rmp.difficulty)} />
{rmp.would_take_again != null && (
<RmpStat label="Take again" value={`${rmp.would_take_again}%`} />
)}
<span style={{
fontSize: 'var(--text-xs)',
color: 'var(--color-ink-3)',
alignSelf: 'flex-end',
fontSize: 'var(--text-sm)',
fontWeight: 600,
color: 'var(--color-ink)',
lineHeight: 1.3,
}}>
{rmp.num_ratings} rating{rmp.num_ratings !== 1 ? 's' : ''}
{name}
</span>
{rmp?.found && (
<a
href={rmp.rmp_url}
target="_blank"
rel="noopener noreferrer"
style={{
fontSize: 'var(--text-xs)',
color: 'var(--color-accent)',
textDecoration: 'none',
whiteSpace: 'nowrap',
flexShrink: 0,
}}
>
RMP ↗
</a>
)}
</div>
) : (

{rmp?.found ? (
<>
<div style={{
display: 'flex',
gap: 'var(--space-sm)',
marginTop: 6,
flexWrap: 'wrap',
}}>
<RmpStat label="Rating" value={rmp.overall_rating?.toFixed(1)} color={ratingColor(rmp.overall_rating)} />
<RmpStat label="Difficulty" value={rmp.difficulty?.toFixed(1)} color={difficultyColor(rmp.difficulty)} />
{rmp.would_take_again != null && (
<RmpStat label="Take again" value={`${rmp.would_take_again}%`} />
)}
<span style={{
fontSize: 'var(--text-xs)',
color: 'var(--color-ink-3)',
alignSelf: 'flex-end',
}}>
{rmp.num_ratings} rating{rmp.num_ratings !== 1 ? 's' : ''}
</span>
</div>

{reviews.length > 0 && (
<>
<button
onClick={() => setShowReviews(v => !v)}
style={{
marginTop: 8,
background: 'none',
border: 'none',
padding: 0,
cursor: 'pointer',
fontSize: 'var(--text-xs)',
color: 'var(--color-accent)',
fontFamily: 'var(--font-body)',
}}
>
{showReviews ? 'Hide reviews' : `Show ${reviews.length} recent review${reviews.length !== 1 ? 's' : ''}`}
</button>

{showReviews && (
<div style={{ marginTop: 8, display: 'flex', flexDirection: 'column', gap: 8 }}>
{reviews.map((r, i) => (
<ReviewCard key={i} review={r} />
))}
</div>
)}
</>
)}
</>
) : (
<div style={{
fontSize: 'var(--text-xs)',
color: 'var(--color-ink-3)',
marginTop: 4,
}}>
Not on RateMyProfessors
</div>
)}
</div>
)
}

const ReviewCard = ({ review }) => {
const year = review.date ? new Date(review.date).getFullYear() : null
return (
<div style={{
background: 'var(--color-paper)',
border: '1px solid var(--color-rule)',
borderRadius: 6,
padding: '8px 10px',
}}>
<div style={{
fontSize: 'var(--text-xs)',
color: 'var(--color-ink-3)',
marginTop: 4,
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 4,
gap: 8,
}}>
Not on RateMyProfessors
<div style={{ display: 'flex', gap: 8 }}>
{review.quality != null && (
<span style={{
fontSize: 'var(--text-xs)',
fontWeight: 700,
color: ratingColor(review.quality),
}}>
{review.quality}/5
</span>
)}
{review.difficulty != null && (
<span style={{
fontSize: 'var(--text-xs)',
color: 'var(--color-ink-3)',
}}>
Difficulty {review.difficulty}/5
</span>
)}
</div>
<span style={{ fontSize: 'var(--text-xs)', color: 'var(--color-ink-3)', flexShrink: 0 }}>
{review.course ? `${review.course}${year ? ` · ${year}` : ''}` : year}
</span>
</div>
)}
</div>
)
{review.comment && (
<p style={{
margin: 0,
fontSize: 'var(--text-xs)',
color: 'var(--color-ink-2)',
lineHeight: 1.6,
}}>
{review.comment}
</p>
)}
{review.tags?.length > 0 && (
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 4, marginTop: 6 }}>
{review.tags.map(tag => (
<span key={tag} style={{
fontSize: 10,
background: 'var(--color-paper-2)',
border: '1px solid var(--color-rule)',
borderRadius: 4,
padding: '1px 6px',
color: 'var(--color-ink-3)',
}}>
{tag}
</span>
))}
</div>
)}
</div>
)
}

const RmpStat = ({ label, value, color }) => (
<div style={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
Expand Down
Loading
Loading