From 271604ab9608c65099da876752677975dc4e7807 Mon Sep 17 00:00:00 2001
From: Zaid Ahmad <109442753+zaidahmad16@users.noreply.github.com>
Date: Tue, 26 May 2026 13:23:49 -0400
Subject: [PATCH 1/2] feat: add Business concentrations/streams, fix layouts,
update calendar year
---
backend/main.py | 43 +++++++++++++++++----------
frontend/app/map/components/Notes.jsx | 2 +-
frontend/app/map/page.js | 33 +++++++++++++++++---
frontend/app/page.js | 8 ++---
4 files changed, 62 insertions(+), 24 deletions(-)
diff --git a/backend/main.py b/backend/main.py
index 5d267d0..f827449 100644
--- a/backend/main.py
+++ b/backend/main.py
@@ -261,30 +261,43 @@ def get_program(request: Request, program_id: int):
""", (program_id,))
edges = cur.fetchall()
- # for concentrations, merge in the base degree requirements so the full
- # program path is visible alongside the concentration-specific courses
+ # for concentrations and streams, merge the full base degree so the
+ # complete program path is visible alongside the specific courses
base_reqs = []
base_edges = []
- if degree.lower().startswith("concentration in"):
- cur.execute("""
- SELECT program_id, degree FROM programs
- WHERE dept_id = %s
- AND program_id != %s
- AND degree ILIKE ANY(ARRAY['%%B.A. Honours%%', '%%B.Sc. Honours%%', '%%B.Eng. Honours%%',
- '%%Bachelor%%Honours%%'])
- ORDER BY program_id
- LIMIT 1
- """, (dept_id, program_id))
+ deg_lower = degree.lower()
+ is_concentration = deg_lower.startswith("concentration in")
+ is_stream = deg_lower.startswith("stream in")
+ if is_concentration or is_stream:
+ if is_stream:
+ # streams belong to the International Business Honours degree
+ base_query = """
+ SELECT program_id, degree FROM programs
+ WHERE dept_id = %s
+ AND program_id != %s
+ AND degree ILIKE '%%International Business%%Honours%%'
+ ORDER BY program_id
+ LIMIT 1
+ """
+ else:
+ # concentrations belong to the first honours degree in the department
+ base_query = """
+ SELECT program_id, degree FROM programs
+ WHERE dept_id = %s
+ AND program_id != %s
+ AND degree ILIKE ANY(ARRAY['%%B.A. Honours%%', '%%B.Sc. Honours%%', '%%B.Eng. Honours%%',
+ '%%Bachelor%%Honours%%'])
+ ORDER BY program_id
+ LIMIT 1
+ """
+ cur.execute(base_query, (dept_id, program_id))
base_row = cur.fetchone()
if base_row:
base_prog_id = base_row[0]
- # only pull year 1 and 2 courses from the base program — year 3+ are
- # largely electives that the concentration already covers
cur.execute("""
SELECT type, courses, credits, description, layout_col, layout_row
FROM program_requirements
WHERE program_id = %s
- AND layout_col < 2
ORDER BY req_id
""", (base_prog_id,))
base_reqs = cur.fetchall()
diff --git a/frontend/app/map/components/Notes.jsx b/frontend/app/map/components/Notes.jsx
index 0d2ae76..9405152 100644
--- a/frontend/app/map/components/Notes.jsx
+++ b/frontend/app/map/components/Notes.jsx
@@ -80,7 +80,7 @@ export const Notes = ({ notes = [], degree, open: controlledOpen, onOpenChange }
- Course data reflects the 2025–2026 Undergraduate Calendar. Always verify with the official Carleton University calendar before enrolling.
+ Course data reflects the 2026–2027 Undergraduate Calendar. Always verify with the official Carleton University calendar before enrolling.
Not affiliated with or endorsed by Carleton University.
diff --git a/frontend/app/map/page.js b/frontend/app/map/page.js
index ed00cc4..40ef5b3 100644
--- a/frontend/app/map/page.js
+++ b/frontend/app/map/page.js
@@ -37,6 +37,20 @@ const shortenProgram = (degree = '') => {
return c.length > 30 ? c.slice(0, 28) + '…' : c
}
+ // Standalone "Concentration in X (N credits)" — show just the topic
+ const concM = degree.match(/^Concentration\s+in\s+(.+?)(?:\s*\(|$)/i)
+ if (concM) {
+ const c = concM[1].trim()
+ return c.length > 30 ? c.slice(0, 28) + '…' : c
+ }
+
+ // Standalone "Stream in X (N credits)" — show just the topic
+ const streamM = degree.match(/^Stream\s+in\s+(.+?)(?:\s*\(|$)/i)
+ if (streamM) {
+ const c = streamM[1].trim()
+ return c.length > 30 ? c.slice(0, 28) + '…' : c
+ }
+
const specifics = [
[/artificial intelligence|machine learning/i, 'AI & Machine Learning'],
[/cybersecurity|cyber security/i, 'Cybersecurity'],
@@ -51,8 +65,8 @@ const shortenProgram = (degree = '') => {
]
for (const [re, label] of specifics) if (re.test(degree)) return label
- // Cognitive Science concentrations — extract the concentration name
- const cogM = degree.match(/concentration\s+in\s+(.+?)(?:\s{2,}|$)/i)
+ // Any remaining "Concentration in X" embedded in a longer degree name
+ const cogM = degree.match(/concentration\s+in\s+(.+?)(?:\s*\(|\s{2,}|$)/i)
if (cogM) {
const c = cogM[1].trim()
return c.length > 30 ? c.slice(0, 28) + '…' : c
@@ -85,6 +99,10 @@ export default function MapPage() {
const [nodes, setNodes] = useState([])
const [edges, setEdges] = useState([])
const [selectedNode, setSelectedNode] = useState(null)
+ const pillScrollRef = useRef(null)
+ const scrollPills = (dir) => {
+ if (pillScrollRef.current) pillScrollRef.current.scrollBy({ left: dir * 200, behavior: 'smooth' })
+ }
const [showPicker, setShowPicker] = useState(false)
const [showNotes, setShowNotes] = useState(false)
const [showCompare, setShowCompare] = useState(false)
@@ -234,7 +252,12 @@ export default function MapPage() {
background: 'var(--color-paper)',
}}>
-
+
{/* nav bar */}
{stats.departments
- ? `All ${stats.departments} departments · ${stats.programs} programs · 2025–2026 Undergraduate Calendar`
- : 'Every department and program · 2025–2026 Undergraduate Calendar'}
+ ? `All ${stats.departments} departments · ${stats.programs} programs · 2026–2027 Undergraduate Calendar`
+ : 'Every department and program · 2026–2027 Undergraduate Calendar'}