From 8d4a35a7ee61865ae48fe389741778e59645434f Mon Sep 17 00:00:00 2001 From: Al- Saihan Date: Sat, 18 Apr 2026 20:40:21 +0600 Subject: [PATCH 01/20] feat: add Course Progression page with department selection and UI components --- src/app/(main)/course-progression/page.jsx | 81 ++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 src/app/(main)/course-progression/page.jsx diff --git a/src/app/(main)/course-progression/page.jsx b/src/app/(main)/course-progression/page.jsx new file mode 100644 index 0000000..5308c94 --- /dev/null +++ b/src/app/(main)/course-progression/page.jsx @@ -0,0 +1,81 @@ +"use client"; +import React from "react"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { useState } from "react"; + +const departments = [ + { code: "CSE", name: "Computer Science & Engineering" }, + { code: "CS", name: "Computer Science" }, + { code: "ARCH", name: "Architecture" }, + { code: "BBA", name: "Business Administration" }, + { code: "LAW", name: "Law" }, +]; + +export default function CourseProgressionPage() { + const [selectedDept, setSelectedDept] = useState("CSE"); + const [showComingSoon, setShowComingSoon] = useState(false); + + const handleDeptClick = (code) => { + if (code === "CSE") { + setSelectedDept("CSE"); + setShowComingSoon(false); + } else { + setSelectedDept(code); + setShowComingSoon(true); + } + }; + + return ( +
+
+
+ + CSE Degree Plan + +

+ Track Your{" "} + + Course Progression + +

+

+ Select the courses you have completed and plan your path to graduation! +

+
+
+ {departments.map((dept) => ( + + ))} +
+
+ {showComingSoon ? ( +
+ + {departments.find((d) => d.code === selectedDept)?.name} outline coming soon... + +
+ ) : ( +
+ Course progression tracker coming soon... +
+ )} +
+
+
+ ); +} From da689ed8a9b8ce2b97534b9bfb9046bc9a4028cf Mon Sep 17 00:00:00 2001 From: Al- Saihan Date: Sun, 19 Apr 2026 18:38:42 +0600 Subject: [PATCH 02/20] feat: implement course progression logic with prerequisite handling and total credits calculation --- src/app/(main)/course-progression/page.jsx | 558 ++++++++++++++++++++- src/constants/cseCurriculum.js | 131 +++++ 2 files changed, 673 insertions(+), 16 deletions(-) create mode 100644 src/constants/cseCurriculum.js diff --git a/src/app/(main)/course-progression/page.jsx b/src/app/(main)/course-progression/page.jsx index 5308c94..4c268cb 100644 --- a/src/app/(main)/course-progression/page.jsx +++ b/src/app/(main)/course-progression/page.jsx @@ -1,8 +1,9 @@ "use client"; -import React from "react"; +import React, { useState, useEffect, useCallback, useMemo } from "react"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; -import { useState } from "react"; +import { cseCurriculum, getTotalCredits } from "@/constants/cseCurriculum"; +import { CheckCircle2, Circle, Lock, Unlock, BookOpen, RefreshCw } from "lucide-react"; const departments = [ { code: "CSE", name: "Computer Science & Engineering" }, @@ -12,9 +13,387 @@ const departments = [ { code: "LAW", name: "Law" }, ]; +//! MARK: Prereq Parsing +function parsePrereqString(prereqStr) { + if (!prereqStr || typeof prereqStr !== "string" || !prereqStr.trim()) return null; + + let cleaned = prereqStr.trim(); + + if (cleaned.startsWith("(") && cleaned.endsWith(")")) { + const inner = cleaned.slice(1, -1); + if (inner.match(/^[A-Z]{3,}[0-9]{3,}$/)) { + return inner; + } + cleaned = inner; + } + + const tokens = []; + const regex = /\(|\)|AND|OR|[A-Z]{3,}[0-9]{3,}/gi; + let match; + + while ((match = regex.exec(cleaned)) !== null) { + tokens.push(match[0].toUpperCase()); + } + + if (tokens.length === 0) return null; + + let pos = 0; + + function parseExpression() { + let args = []; + let currentOp = null; + + while (pos < tokens.length) { + const token = tokens[pos]; + + if (token === "(") { + pos++; + args.push(parseExpression()); + } else if (token === ")") { + pos++; + break; + } else if (token === "AND" || token === "OR") { + currentOp = token; + pos++; + } else { + args.push(token); + pos++; + } + } + + if (args.length === 0) return null; + if (args.length === 1) return args[0]; + if (!currentOp && args.length > 1) return { op: "AND", args }; + return { op: currentOp, args }; + } + + return parseExpression(); +} + +//! MARK: Prereq Tree +function getAllPrerequisiteCodes(prereqTree) { + if (!prereqTree) return []; + if (typeof prereqTree === "string") return [prereqTree]; + if (Array.isArray(prereqTree)) return prereqTree.flatMap(getAllPrerequisiteCodes); + if (prereqTree.args) return prereqTree.args.flatMap(getAllPrerequisiteCodes); + return []; +} + +//! MARK: Prereq Satisfaction +function arePrerequisitesSatisfied(prereqTree, completedCourses) { + if (!prereqTree) return true; + + function evaluate(expr) { + if (typeof expr === "string") { + return completedCourses.includes(expr); + } + if (expr.op === "AND") { + return expr.args.every(evaluate); + } + if (expr.op === "OR") { + return expr.args.some(evaluate); + } + return false; + } + + return evaluate(prereqTree); +} + +//! MARK: Course Data Fetch +//! CDN +function useConnectCDN() { + const [courseMap, setCourseMap] = useState({}); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + async function fetchData() { + setLoading(true); + setError(null); + try { + const res = await fetch("https://usis-cdn.eniamza.com/connect.json"); + if (!res.ok) throw new Error("Failed to fetch connect.json"); + const data = await res.json(); + + const map = {}; + for (const c of data) { + if (!map[c.courseCode]) { + map[c.courseCode] = { + code: c.courseCode, + name: c.courseName, + credits: c.courseCredit, + prereqRaw: c.prerequisiteCourses, + prereqTree: parsePrereqString(c.prerequisiteCourses), + allPrereqs: getAllPrerequisiteCodes(parsePrereqString(c.prerequisiteCourses)), + }; + } + } + setCourseMap(map); + } catch (err) { + setError(err.message); + } finally { + setLoading(false); + } + } + fetchData(); + }, []); + + return { courseMap, loading, error }; +} + +//! MARK: Course Card +function CourseCard({ course, isCompleted, isAvailable, isSelected, onClick, courseDetails }) { + const [showTooltip, setShowTooltip] = useState(false); + + let statusColor = "border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800"; + let statusIcon = null; + let statusText = ""; + + if (isCompleted) { + statusColor = "border-green-500 bg-green-50 dark:bg-green-900/20 hover:border-green-600"; + statusIcon = ; + statusText = "Completed (Click to undo)"; + } else if (isSelected) { + statusColor = "border-blue-500 bg-blue-50 dark:bg-blue-900/20 ring-2 ring-blue-500"; + statusIcon = ; + statusText = "Selected"; + } else if (isAvailable) { + statusColor = "border-yellow-500 bg-yellow-50 dark:bg-yellow-900/20 hover:border-yellow-600"; + statusIcon = ; + statusText = "Available (Click to complete)"; + } else { + statusColor = "border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800/50 opacity-60"; + statusIcon = ; + statusText = "Locked - Prerequisites needed"; + } + + return ( +
onClick(course.code)} + onMouseEnter={() => setShowTooltip(true)} + onMouseLeave={() => setShowTooltip(false)} + > +
+
+
{course.code}
+
+ {course.name || courseDetails?.name || "Course"} +
+
{course.credits || courseDetails?.credits || 3} credits
+
+
{statusIcon}
+
+ + {/* Tooltip */} + {showTooltip && ( +
+ {statusText} + {courseDetails?.allPrereqs?.length > 0 && !isCompleted && !isAvailable && ( +
Requires: {courseDetails.allPrereqs.join(", ")}
+ )} + {isCompleted && courseDetails?.allPrereqs?.length > 0 && ( +
Note: Uncompleting will lock dependent courses
+ )} +
+
+ )} +
+ ); +} + +//! MARK: SectionCourses +function SectionCourses({ section, completedCourses, onCourseToggle, onCourseUntoggle }) { + const { courseMap, loading, error } = useConnectCDN(); + const [selectedCourse, setSelectedCourse] = useState(null); + + const sectionObj = cseCurriculum.find((s) => s.section === section); + + if (!sectionObj) return
Section not found.
; + + // Get all courses in this section + let allCourses = []; + if (sectionObj.courses) { + allCourses = sectionObj.courses; + } else if (sectionObj.streams) { + allCourses = sectionObj.streams.flatMap((stream) => stream.courses); + } + + // Calculate course statuses based on prerequisites + const getCourseStatus = useCallback( + (courseCode) => { + const isCompleted = completedCourses.includes(courseCode); + const isSelectedCourse = selectedCourse === courseCode; + + if (isCompleted) return "completed"; + if (isSelectedCourse) return "selected"; + + // Check if prerequisites are satisfied + const courseDetails = courseMap[courseCode]; + if (courseDetails && courseDetails.prereqTree) { + const prerequisitesMet = arePrerequisitesSatisfied(courseDetails.prereqTree, completedCourses); + if (prerequisitesMet) return "available"; + } else if (!courseDetails || !courseDetails.prereqTree) { + // No prerequisites means it's always available + return "available"; + } + + return "locked"; + }, + [completedCourses, selectedCourse, courseMap], + ); + + // Handle course click + const handleCourseClick = (courseCode) => { + const status = getCourseStatus(courseCode); + + // Allow clicking on completed courses to undo them + if (status === "completed") { + if ( + window.confirm( + `Are you sure you want to mark ${courseCode} as incomplete?\nThis will also lock any courses that depend on it.`, + ) + ) { + onCourseUntoggle(courseCode); + if (selectedCourse === courseCode) { + setSelectedCourse(null); + } + } + } + // Allow clicking on available courses to complete them + else if (status === "available") { + onCourseToggle(courseCode); + setSelectedCourse(courseCode); + } + // Allow clicking on selected to deselect + else if (status === "selected") { + setSelectedCourse(null); + } + }; + + if (loading) { + return ( +
+
+
+

Loading course data...

+
+
+ ); + } + + if (error) { + return ( +
Error loading course data: {error}
+ ); + } + + return ( +
+ {/* Section Description */} + {sectionObj.description && ( +
+

{sectionObj.description}

+

+ Total Credits: {sectionObj.credits} +

+
+ )} + + {/* Legend */} +
+
+
+ Completed (Click to undo) +
+
+
+ Selected +
+
+
+ Available +
+
+
+ Locked +
+
+ + {/* Streams/Categories */} + {sectionObj.streams ? ( +
+ {sectionObj.streams.map((stream) => ( +
+
+

{stream.name}

+ + {stream.credits} credits + +
+ {stream.note &&

{stream.note}

} +
+ {stream.courses.map((course) => { + const status = getCourseStatus(course.code); + return ( + + ); + })} +
+
+ ))} +
+ ) : ( +
+ {allCourses.map((course) => { + const status = getCourseStatus(course.code); + return ( + + ); + })} +
+ )} + + {/* Selected Course Info */} + {selectedCourse && courseMap[selectedCourse] && ( +
+

Selected Course: {selectedCourse}

+

{courseMap[selectedCourse].name}

+ {courseMap[selectedCourse].allPrereqs?.length > 0 && ( +
+ Prerequisites:{" "} + {courseMap[selectedCourse].allPrereqs.join(", ")} +
+ )} +
+ )} +
+ ); +} + +//! MARK: Main Page export default function CourseProgressionPage() { const [selectedDept, setSelectedDept] = useState("CSE"); const [showComingSoon, setShowComingSoon] = useState(false); + const [selectedSection, setSelectedSection] = useState("Foundation & Core Skills"); + const [completedCourses, setCompletedCourses] = useState([]); + const [showResetConfirm, setShowResetConfirm] = useState(false); const handleDeptClick = (code) => { if (code === "CSE") { @@ -26,16 +405,93 @@ export default function CourseProgressionPage() { } }; + // Get courseMap from CDN for dependency checks + const { courseMap } = useConnectCDN(); + + // Mark as completed + const handleCourseToggle = (courseCode) => { + if (!completedCourses.includes(courseCode)) { + setCompletedCourses([...completedCourses, courseCode]); + } + }; + + // Mark as incomplete and recursively lock dependents + const handleCourseUntoggle = useCallback( + (courseCode) => { + setCompletedCourses((prev) => { + // Find all dependents that should be removed + const toRemove = new Set([courseCode]); + + // Build a map of prerequisites for quick lookup + const prerequisiteMap = new Map(); + for (const [code, details] of Object.entries(courseMap)) { + if (details.prereqTree) { + prerequisiteMap.set(code, getAllPrerequisiteCodes(details.prereqTree)); + } + } + + // Recursively find all courses that depend on courseCode + let changed = true; + while (changed) { + changed = false; + for (const [code, prereqs] of prerequisiteMap) { + if (!toRemove.has(code) && prev.includes(code)) { + // Check if any prerequisite is being removed + const hasRemovedPrereq = prereqs.some((prereq) => toRemove.has(prereq)); + if (hasRemovedPrereq) { + // Check if prerequisites are still satisfied without the removed ones + const newCompletedSet = new Set(prev.filter((c) => !toRemove.has(c))); + const stillSatisfied = arePrerequisitesSatisfied( + courseMap[code]?.prereqTree, + Array.from(newCompletedSet), + ); + if (!stillSatisfied) { + toRemove.add(code); + changed = true; + } + } + } + } + } + + return prev.filter((c) => !toRemove.has(c)); + }); + }, + [courseMap], + ); + + // Reset all progress + const handleReset = () => { + if (window.confirm("Are you sure you want to reset all your progress? This cannot be undone.")) { + setCompletedCourses([]); + setShowResetConfirm(false); + } + }; + + const cseSections = cseCurriculum.map((s) => ({ + name: s.section, + credits: s.credits, + description: s.description, + })); + + // Calculate total completed credits + const totalCompletedCredits = completedCourses.reduce((total, code) => { + // Find course in curriculum + for (const section of cseCurriculum) { + let courses = []; + if (section.courses) courses = section.courses; + else if (section.streams) courses = section.streams.flatMap((s) => s.courses); + + const course = courses.find((c) => c.code === code); + if (course) return total + (course.credits || 3); + } + return total + 3; // Default 3 credits if not found + }, 0); + return (
-
-
- - CSE Degree Plan - +
+

Track Your{" "} @@ -43,17 +499,20 @@ export default function CourseProgressionPage() {

- Select the courses you have completed and plan your path to graduation! + Click on available courses (yellow) to mark them as completed. Click on completed courses (green) to undo + and automatically lock dependencies.

-
+ + {/* Department Selection */} +
{departments.map((dept) => ( ))}
+ + {/* Section Navigation for CSE */} + {selectedDept === "CSE" && ( +
+
+ {cseSections.map((section) => ( + + ))} +
+ + {/* Progress Stats with Reset Button */} +
+
+
+

+ Completed Courses:{" "} + + {completedCourses.length} + +

+

+ Completed Credits:{" "} + + {totalCompletedCredits} / {getTotalCredits()} + +

+
+
+
+
+ {completedCourses.length > 0 && ( + + )} +
+
+
+ )} + + {/* Main Content */}
{showComingSoon ? (
@@ -70,9 +593,12 @@ export default function CourseProgressionPage() {
) : ( -
- Course progression tracker coming soon... -
+ )}
diff --git a/src/constants/cseCurriculum.js b/src/constants/cseCurriculum.js new file mode 100644 index 0000000..25769c5 --- /dev/null +++ b/src/constants/cseCurriculum.js @@ -0,0 +1,131 @@ + +// ! Calculate total credits from curriculum +export function getTotalCredits() { + let total = 0; + for (const section of cseCurriculum) { + if (Array.isArray(section.courses)) { + total += section.courses.reduce((sum, c) => sum + (c.credits || 0), 0); + } + if (Array.isArray(section.streams)) { + for (const stream of section.streams) { + total += stream.courses.reduce((sum, c) => sum + (c.credits || 0), 0); + } + } + } + return total; +} + +export const cseCurriculum = [ + { + section: "Foundation & Core Skills", + credits: 51, // 39 + 12 = 51 total credits + description: "Combined University Core and School Core courses", + streams: [ + { + name: "Writing & Communication", + credits: 9, + courses: [ + { code: "ENG091", name: "Foundation Course (in English)", credits: 0, optional: false }, + { code: "ENG101", name: "Fundamentals of English", credits: 3, optional: false }, + { code: "ENG102", name: "English Composition I", credits: 3, optional: false }, + { code: "ENG103", name: "Advanced Writing Skills and Presentation", credits: 3, optional: false }, + ], + }, + { + name: "Mathematics & Natural Sciences", + credits: 18, + courses: [ + { code: "MAT092", name: "Remedial Mathematics", credits: 0, optional: false }, + { code: "MAT110", name: "Math I: Differential Calculus & Coordinate Geometry", credits: 3, optional: false }, + { + code: "MAT120", + name: "Math II: Integral Calculus and Differential Equations", + credits: 3, + optional: false, + }, + { + code: "MAT215", + name: "Math III: Complex Variables and Laplace Transformations", + credits: 3, + optional: false, + }, + { code: "MAT216", name: "Math IV: Linear Algebra and Fourier Analysis", credits: 3, optional: false }, + { code: "PHY111", name: "Principles of Physics I", credits: 3, optional: false }, + { code: "PHY112", name: "Principles of Physics II", credits: 3, optional: false }, + { code: "STA201", name: "Statistics and Probability", credits: 3, optional: false }, + ], + }, + { + name: "Arts & Humanities", + credits: 6, + courses: [ + { code: "HUM103", name: "Ethics and Culture", credits: 3, optional: false }, + { code: "BNG103", name: "Bangla Language and Literature", credits: 3, optional: false }, + ], + }, + { + name: "Social Sciences", + credits: 3, + courses: [{ code: "EMB101", name: "Bangladesh Studies", credits: 3, optional: false, alternatives: ["DEV101"] }], + }, + { + name: "Community Service & Transformation", + credits: 9, + courses: [ + { code: "CST201", name: "Community Service I", credits: 3, optional: true }, + { code: "CST301", name: "Community Service II", credits: 3, optional: true }, + { code: "CST310", name: "Community Leadership", credits: 3, optional: true }, + ], + note: "Choose any 3 credits from optional courses", + }, + ], + }, + { + section: "Program Core", + credits: 75, + description: "Core Computer Science and Engineering courses", + courses: [ + { code: "CSE110", name: "Programming Language I", credits: 3 }, + { code: "CSE111", name: "Programming Language II", credits: 3 }, + { code: "CSE220", name: "Data Structures", credits: 3 }, + { code: "CSE221", name: "Algorithms", credits: 3 }, + { code: "CSE230", name: "Discrete Mathematics", credits: 3 }, + { code: "CSE331", name: "Automata and Computability", credits: 3 }, + { code: "CSE250", name: "Circuits and Electronics", credits: 3 }, + { code: "CSE251", name: "Electronic Devices and Circuits", credits: 3 }, + { code: "CSE260", name: "Digital Logic Design", credits: 3 }, + { code: "CSE350", name: "Digital Electronics and Pulse Techniques", credits: 3 }, + { code: "CSE341", name: "Microprocessors", credits: 3 }, + { code: "CSE360", name: "Computer Interfacing", credits: 3 }, + { code: "CSE321", name: "Operating System", credits: 3 }, + { code: "CSE340", name: "Computer Architecture", credits: 3 }, + { code: "CSE320", name: "Data Communications", credits: 3 }, + { code: "CSE421", name: "Computer Networks", credits: 3 }, + { code: "CSE370", name: "Database Systems", credits: 3 }, + { code: "CSE330", name: "Numerical Methods", credits: 3 }, + { code: "CSE420", name: "Compiler Design", credits: 3 }, + { code: "CSE422", name: "Artificial Intelligence", credits: 3 }, + { code: "CSE423", name: "Computer Graphics", credits: 3 }, + { code: "CSE460", name: "VLSI Design", credits: 3 }, + { code: "CSE461", name: "Introduction to Robotics", credits: 3 }, + { code: "CSE470", name: "Software Engineering", credits: 3 }, + { code: "CSE471", name: "Systems Analysis and Design", credits: 3 }, + ], + }, + { + section: "Program Electives", + credits: 6, + description: "Choose elective courses to specialize", + courses: [ + { code: "CSEElective", name: "CSE Elective (Any 300/400 level CSE course)", credits: 3, elective: true }, + { code: "OpenElective", name: "Open Elective (CSE / Minor / GenEd)", credits: 3, elective: true }, + ], + }, + { + section: "Final Requirement", + credits: 4, + description: "Capstone project or thesis", + courses: [{ code: "CSE400", name: "Project / Thesis", credits: 4 }], + }, +]; + From ecf428d7ef6959cd97833bc25d724dee56fe9b65 Mon Sep 17 00:00:00 2001 From: Al- Saihan Date: Sun, 19 Apr 2026 22:17:56 +0600 Subject: [PATCH 03/20] feat: enhance course progression with prerequisite overrides and curriculum updates --- src/app/(main)/course-progression/page.jsx | 154 ++++++++++----------- src/constants/cseCurriculum.js | 132 +++++++++++++++--- 2 files changed, 186 insertions(+), 100 deletions(-) diff --git a/src/app/(main)/course-progression/page.jsx b/src/app/(main)/course-progression/page.jsx index 4c268cb..621b5b4 100644 --- a/src/app/(main)/course-progression/page.jsx +++ b/src/app/(main)/course-progression/page.jsx @@ -2,7 +2,7 @@ import React, { useState, useEffect, useCallback, useMemo } from "react"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; -import { cseCurriculum, getTotalCredits } from "@/constants/cseCurriculum"; +import { cseCurriculum, getTotalCredits, prerequisiteOverrides } from "@/constants/cseCurriculum"; import { CheckCircle2, Circle, Lock, Unlock, BookOpen, RefreshCw } from "lucide-react"; const departments = [ @@ -118,13 +118,17 @@ function useConnectCDN() { const map = {}; for (const c of data) { if (!map[c.courseCode]) { + const overridePrereq = prerequisiteOverrides[c.courseCode]; + const effectivePrereqRaw = overridePrereq ?? c.prerequisiteCourses; + const effectivePrereqTree = parsePrereqString(effectivePrereqRaw); + map[c.courseCode] = { code: c.courseCode, name: c.courseName, credits: c.courseCredit, - prereqRaw: c.prerequisiteCourses, - prereqTree: parsePrereqString(c.prerequisiteCourses), - allPrereqs: getAllPrerequisiteCodes(parsePrereqString(c.prerequisiteCourses)), + prereqRaw: effectivePrereqRaw, + prereqTree: effectivePrereqTree, + allPrereqs: getAllPrerequisiteCodes(effectivePrereqTree), }; } } @@ -180,7 +184,7 @@ function CourseCard({ course, isCompleted, isAvailable, isSelected, onClick, cou
{course.name || courseDetails?.name || "Course"}
-
{course.credits || courseDetails?.credits || 3} credits
+
{course.credits ?? courseDetails?.credits ?? 3} credits
{statusIcon}
@@ -247,28 +251,16 @@ function SectionCourses({ section, completedCourses, onCourseToggle, onCourseUnt const handleCourseClick = (courseCode) => { const status = getCourseStatus(courseCode); - // Allow clicking on completed courses to undo them + // Toggle completed state directly for any course. if (status === "completed") { - if ( - window.confirm( - `Are you sure you want to mark ${courseCode} as incomplete?\nThis will also lock any courses that depend on it.`, - ) - ) { - onCourseUntoggle(courseCode); - if (selectedCourse === courseCode) { - setSelectedCourse(null); - } + onCourseUntoggle(courseCode); + if (selectedCourse === courseCode) { + setSelectedCourse(null); } - } - // Allow clicking on available courses to complete them - else if (status === "available") { + } else { onCourseToggle(courseCode); setSelectedCourse(courseCode); } - // Allow clicking on selected to deselect - else if (status === "selected") { - setSelectedCourse(null); - } }; if (loading) { @@ -290,16 +282,6 @@ function SectionCourses({ section, completedCourses, onCourseToggle, onCourseUnt return (
- {/* Section Description */} - {sectionObj.description && ( -
-

{sectionObj.description}

-

- Total Credits: {sectionObj.credits} -

-
- )} - {/* Legend */}
@@ -389,12 +371,15 @@ function SectionCourses({ section, completedCourses, onCourseToggle, onCourseUnt //! MARK: Main Page export default function CourseProgressionPage() { - const [selectedDept, setSelectedDept] = useState("CSE"); + const [selectedDept, setSelectedDept] = useState(null); const [showComingSoon, setShowComingSoon] = useState(false); - const [selectedSection, setSelectedSection] = useState("Foundation & Core Skills"); + const [selectedSection, setSelectedSection] = useState("Program Core"); const [completedCourses, setCompletedCourses] = useState([]); const [showResetConfirm, setShowResetConfirm] = useState(false); + // Use CDN prerequisite graph to auto-mark prerequisite chains when selecting a course. + const { courseMap } = useConnectCDN(); + const handleDeptClick = (code) => { if (code === "CSE") { setSelectedDept("CSE"); @@ -405,60 +390,53 @@ export default function CourseProgressionPage() { } }; - // Get courseMap from CDN for dependency checks - const { courseMap } = useConnectCDN(); - // Mark as completed const handleCourseToggle = (courseCode) => { - if (!completedCourses.includes(courseCode)) { - setCompletedCourses([...completedCourses, courseCode]); - } - }; + setCompletedCourses((prev) => { + const next = new Set(prev); + const stack = [courseCode]; - // Mark as incomplete and recursively lock dependents - const handleCourseUntoggle = useCallback( - (courseCode) => { - setCompletedCourses((prev) => { - // Find all dependents that should be removed - const toRemove = new Set([courseCode]); - - // Build a map of prerequisites for quick lookup - const prerequisiteMap = new Map(); - for (const [code, details] of Object.entries(courseMap)) { - if (details.prereqTree) { - prerequisiteMap.set(code, getAllPrerequisiteCodes(details.prereqTree)); + while (stack.length > 0) { + const code = stack.pop(); + if (!code || next.has(code)) continue; + + next.add(code); + + const prereqCodes = getAllPrerequisiteCodes(courseMap[code]?.prereqTree); + for (const prereqCode of prereqCodes) { + if (!next.has(prereqCode)) { + stack.push(prereqCode); } } + } - // Recursively find all courses that depend on courseCode - let changed = true; - while (changed) { - changed = false; - for (const [code, prereqs] of prerequisiteMap) { - if (!toRemove.has(code) && prev.includes(code)) { - // Check if any prerequisite is being removed - const hasRemovedPrereq = prereqs.some((prereq) => toRemove.has(prereq)); - if (hasRemovedPrereq) { - // Check if prerequisites are still satisfied without the removed ones - const newCompletedSet = new Set(prev.filter((c) => !toRemove.has(c))); - const stillSatisfied = arePrerequisitesSatisfied( - courseMap[code]?.prereqTree, - Array.from(newCompletedSet), - ); - if (!stillSatisfied) { - toRemove.add(code); - changed = true; - } - } - } + return Array.from(next); + }); + }; + + // Mark as incomplete and remove its prerequisite chain. + const handleCourseUntoggle = (courseCode) => { + setCompletedCourses((prev) => { + const toRemove = new Set(); + const stack = [courseCode]; + + while (stack.length > 0) { + const code = stack.pop(); + if (!code || toRemove.has(code)) continue; + + toRemove.add(code); + + const prereqCodes = getAllPrerequisiteCodes(courseMap[code]?.prereqTree); + for (const prereqCode of prereqCodes) { + if (!toRemove.has(prereqCode)) { + stack.push(prereqCode); } } + } - return prev.filter((c) => !toRemove.has(c)); - }); - }, - [courseMap], - ); + return prev.filter((c) => !toRemove.has(c)); + }); + }; // Reset all progress const handleReset = () => { @@ -483,7 +461,7 @@ export default function CourseProgressionPage() { else if (section.streams) courses = section.streams.flatMap((s) => s.courses); const course = courses.find((c) => c.code === code); - if (course) return total + (course.credits || 3); + if (course) return total + Number(course.credits ?? 3); } return total + 3; // Default 3 credits if not found }, 0); @@ -531,9 +509,11 @@ export default function CourseProgressionPage() { key={section.name} variant={selectedSection === section.name ? "default" : "outline"} className={ - selectedSection === section.name - ? "bg-indigo-600 text-white border-indigo-600 hover:bg-indigo-700" - : "border-blue-200 dark:border-blue-800 text-blue-600 dark:text-blue-400" + `${ + selectedSection === section.name + ? "bg-indigo-600 text-white border-indigo-600 hover:bg-indigo-700" + : "border-blue-200 dark:border-blue-800 text-blue-600 dark:text-blue-400" + } px-5 py-6 text-sm` } onClick={() => setSelectedSection(section.name)} > @@ -586,7 +566,13 @@ export default function CourseProgressionPage() { {/* Main Content */}
- {showComingSoon ? ( + {!selectedDept ? ( +
+ + Select a department to view its course progression. + +
+ ) : showComingSoon ? (
{departments.find((d) => d.code === selectedDept)?.name} outline coming soon... diff --git a/src/constants/cseCurriculum.js b/src/constants/cseCurriculum.js index 25769c5..7af9dd1 100644 --- a/src/constants/cseCurriculum.js +++ b/src/constants/cseCurriculum.js @@ -17,13 +17,13 @@ export function getTotalCredits() { export const cseCurriculum = [ { - section: "Foundation & Core Skills", - credits: 51, // 39 + 12 = 51 total credits + section: "University & School Core", + credits: 51, description: "Combined University Core and School Core courses", streams: [ { - name: "Writing & Communication", - credits: 9, + name: "Stream 1: Writing & Communication", + credits: 6, courses: [ { code: "ENG091", name: "Foundation Course (in English)", credits: 0, optional: false }, { code: "ENG101", name: "Fundamentals of English", credits: 3, optional: false }, @@ -32,8 +32,8 @@ export const cseCurriculum = [ ], }, { - name: "Mathematics & Natural Sciences", - credits: 18, + name: "Stream 2: Mathematics & Natural Sciences + School Core", + credits: 21, courses: [ { code: "MAT092", name: "Remedial Mathematics", credits: 0, optional: false }, { code: "MAT110", name: "Math I: Differential Calculus & Coordinate Geometry", credits: 3, optional: false }, @@ -56,27 +56,105 @@ export const cseCurriculum = [ ], }, { - name: "Arts & Humanities", - credits: 6, + name: "Stream 3: Arts & Humanities", + credits: 9, courses: [ { code: "HUM103", name: "Ethics and Culture", credits: 3, optional: false }, { code: "BNG103", name: "Bangla Language and Literature", credits: 3, optional: false }, + { + code: "Required Course - S3", + name: "Arts & Humanities Elective (Minimum one option)", + credits: 3, + optional: false, + alternatives: [ + "HUM101", + "HUM102", + "HST102", + "HST103", + "HST104", + "HUM207", + "ENG110", + "ENG113", + "ENG114", + "ENG115", + "ENG333", + ], + }, + ], + note: "Minimum one from: HUM101, HUM102, HST102, HST103, HST104, HUM207, ENG110, ENG113, ENG114, ENG115, ENG333", + }, + { + name: "Stream 4: Social Sciences", + credits: 6, + courses: [ + { code: "EMB101", name: "Bangladesh Studies", credits: 3, optional: false, alternatives: ["DEV101"] }, + { + code: "Required Course - S4", + name: "Social Sciences Elective (Minimum one option)", + credits: 3, + optional: false, + alternatives: [ + "PSY101", + "SOC101", + "ANT101", + "POL101", + "BUS201", + "ECO101", + "ECO102", + "ECO105", + "BUS102", + "POL102", + "POL103", + "POL201", + "POL202", + "PSY102", + "DEV104", + "DEV201", + "SOC201/ANT202", + "ANT342", + "ANT351", + "BUS333", + "BUS334", + "BUS335", + ], + }, ], + note: "Minimum one from: PSY101, SOC101, ANT101, POL101, BUS201, ECO101, ECO102, ECO105, BUS102, POL102, POL103, POL201, POL202, PSY102, DEV104, DEV201, SOC201/ANT202, ANT342, ANT351, BUS333, BUS334, BUS335", }, { - name: "Social Sciences", + name: "Stream 5: Community Service & Transformation", credits: 3, - courses: [{ code: "EMB101", name: "Bangladesh Studies", credits: 3, optional: false, alternatives: ["DEV101"] }], + courses: [ + { + code: "Required Course - S5", + name: "Community Service Course (Minimum one option)", + credits: 3, + optional: true, + alternatives: [ + "CST201", + "CST301", + "CST302", + "CST303", + "CST304", + "CST305", + "CST306", + "CST307", + "CST308", + "CST309", + "CST310", + ], + }, + ], + note: "Minimum one from: CST201, CST301, CST302, CST303, CST304, CST305, CST306, CST307, CST308, CST309, CST310", }, { - name: "Community Service & Transformation", - credits: 9, + name: "Stream 6: Courses Out Of Department (COD)", + credits: 6, courses: [ - { code: "CST201", name: "Community Service I", credits: 3, optional: true }, - { code: "CST301", name: "Community Service II", credits: 3, optional: true }, - { code: "CST310", name: "Community Leadership", credits: 3, optional: true }, + { code: "COD - 1", name: "Courses Out Of Department Option 1", credits: 3, optional: true }, + { code: "COD - 2", name: "Courses Out Of Department Option 2", credits: 3, optional: true }, ], - note: "Choose any 3 credits from optional courses", + note: "Choose any two 3-credit courses (6 credits total). Minimum one from: HUM101, HUM102, HST102, HST103, HST104, HUM207, ENG110, ENG113, ENG114, ENG115, ENG333, PSY101, SOC101, ANT101, POL101, BUS201, ECO101, ECO102, ECO105, BUS102, POL102, POL103, POL201, POL202, PSY102, DEV104, DEV201, SOC201/ANT202, ANT342, ANT351, BUS333, BUS334, BUS335, CST201, CST301, CST302, CST303, CST304, CST305, CST306, CST307, CST308, CST309, CST310, CHE101, BIO101, ENV103", }, ], }, @@ -129,3 +207,25 @@ export const cseCurriculum = [ }, ]; +// ! Pre-req override map +// null means "use prerequisite from Connect CDN". +// Any non-null string overrides the CDN prerequisite for that course code. +const allCurriculumCourseCodes = Array.from( + new Set( + cseCurriculum.flatMap((section) => { + if (Array.isArray(section.courses)) { + return section.courses.map((course) => course.code); + } + if (Array.isArray(section.streams)) { + return section.streams.flatMap((stream) => stream.courses.map((course) => course.code)); + } + return []; + }), + ), +); + +export const prerequisiteOverrides = { + ...Object.fromEntries(allCurriculumCourseCodes.map((code) => [code, null])), + CSE260: "CSE251", +}; + From d80669e14ee88b5fb0bec1b06df633ca9d52be6f Mon Sep 17 00:00:00 2001 From: Al- Saihan Date: Sun, 19 Apr 2026 22:39:55 +0600 Subject: [PATCH 04/20] feat: enhance course navigation with section anchor IDs and course highlighting --- src/app/(main)/course-progression/page.jsx | 321 +++++++++++++-------- src/constants/cseCurriculum.js | 1 + 2 files changed, 206 insertions(+), 116 deletions(-) diff --git a/src/app/(main)/course-progression/page.jsx b/src/app/(main)/course-progression/page.jsx index 621b5b4..4beabfa 100644 --- a/src/app/(main)/course-progression/page.jsx +++ b/src/app/(main)/course-progression/page.jsx @@ -3,7 +3,7 @@ import React, { useState, useEffect, useCallback, useMemo } from "react"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { cseCurriculum, getTotalCredits, prerequisiteOverrides } from "@/constants/cseCurriculum"; -import { CheckCircle2, Circle, Lock, Unlock, BookOpen, RefreshCw } from "lucide-react"; +import { CheckCircle2, Lock, Unlock, RefreshCw } from "lucide-react"; const departments = [ { code: "CSE", name: "Computer Science & Engineering" }, @@ -13,6 +13,14 @@ const departments = [ { code: "LAW", name: "Law" }, ]; +function getSectionAnchorId(sectionName) { + return `section-${sectionName.toLowerCase().replace(/[^a-z0-9]+/g, "-")}`; +} + +function getCourseCardId(courseCode) { + return `course-card-${courseCode.toLowerCase().replace(/[^a-z0-9]+/g, "-")}`; +} + //! MARK: Prereq Parsing function parsePrereqString(prereqStr) { if (!prereqStr || typeof prereqStr !== "string" || !prereqStr.trim()) return null; @@ -146,7 +154,7 @@ function useConnectCDN() { } //! MARK: Course Card -function CourseCard({ course, isCompleted, isAvailable, isSelected, onClick, courseDetails }) { +function CourseCard({ course, isCompleted, isAvailable, isSelected, isHighlighted, onClick, courseDetails }) { const [showTooltip, setShowTooltip] = useState(false); let statusColor = "border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800"; @@ -157,10 +165,6 @@ function CourseCard({ course, isCompleted, isAvailable, isSelected, onClick, cou statusColor = "border-green-500 bg-green-50 dark:bg-green-900/20 hover:border-green-600"; statusIcon = ; statusText = "Completed (Click to undo)"; - } else if (isSelected) { - statusColor = "border-blue-500 bg-blue-50 dark:bg-blue-900/20 ring-2 ring-blue-500"; - statusIcon = ; - statusText = "Selected"; } else if (isAvailable) { statusColor = "border-yellow-500 bg-yellow-50 dark:bg-yellow-900/20 hover:border-yellow-600"; statusIcon = ; @@ -171,9 +175,14 @@ function CourseCard({ course, isCompleted, isAvailable, isSelected, onClick, cou statusText = "Locked - Prerequisites needed"; } + const highlightClass = isHighlighted + ? "ring-2 ring--500 dark:ring-white-400 ring-offset-2 ring-offset-white dark:ring-offset-gray-900 shadow-lg shadow-white-500/30" + : ""; + return (
onClick(course.code)} onMouseEnter={() => setShowTooltip(true)} onMouseLeave={() => setShowTooltip(false)} @@ -193,6 +202,7 @@ function CourseCard({ course, isCompleted, isAvailable, isSelected, onClick, cou {showTooltip && (
{statusText} + {isSelected &&
Selected
} {courseDetails?.allPrereqs?.length > 0 && !isCompleted && !isAvailable && (
Requires: {courseDetails.allPrereqs.join(", ")}
)} @@ -207,7 +217,7 @@ function CourseCard({ course, isCompleted, isAvailable, isSelected, onClick, cou } //! MARK: SectionCourses -function SectionCourses({ section, completedCourses, onCourseToggle, onCourseUntoggle }) { +function SectionCourses({ section, completedCourses, highlightedCourseCode, onCourseToggle, onCourseUntoggle }) { const { courseMap, loading, error } = useConnectCDN(); const [selectedCourse, setSelectedCourse] = useState(null); @@ -227,10 +237,8 @@ function SectionCourses({ section, completedCourses, onCourseToggle, onCourseUnt const getCourseStatus = useCallback( (courseCode) => { const isCompleted = completedCourses.includes(courseCode); - const isSelectedCourse = selectedCourse === courseCode; if (isCompleted) return "completed"; - if (isSelectedCourse) return "selected"; // Check if prerequisites are satisfied const courseDetails = courseMap[courseCode]; @@ -244,7 +252,7 @@ function SectionCourses({ section, completedCourses, onCourseToggle, onCourseUnt return "locked"; }, - [completedCourses, selectedCourse, courseMap], + [completedCourses, courseMap], ); // Handle course click @@ -282,26 +290,6 @@ function SectionCourses({ section, completedCourses, onCourseToggle, onCourseUnt return (
- {/* Legend */} -
-
-
- Completed (Click to undo) -
-
-
- Selected -
-
-
- Available -
-
-
- Locked -
-
- {/* Streams/Categories */} {sectionObj.streams ? (
@@ -324,6 +312,7 @@ function SectionCourses({ section, completedCourses, onCourseToggle, onCourseUnt isCompleted={status === "completed"} isAvailable={status === "available"} isSelected={status === "selected"} + isHighlighted={highlightedCourseCode === course.code} onClick={handleCourseClick} courseDetails={courseMap[course.code]} /> @@ -344,6 +333,7 @@ function SectionCourses({ section, completedCourses, onCourseToggle, onCourseUnt isCompleted={status === "completed"} isAvailable={status === "available"} isSelected={status === "selected"} + isHighlighted={highlightedCourseCode === course.code} onClick={handleCourseClick} courseDetails={courseMap[course.code]} /> @@ -352,19 +342,6 @@ function SectionCourses({ section, completedCourses, onCourseToggle, onCourseUnt
)} - {/* Selected Course Info */} - {selectedCourse && courseMap[selectedCourse] && ( -
-

Selected Course: {selectedCourse}

-

{courseMap[selectedCourse].name}

- {courseMap[selectedCourse].allPrereqs?.length > 0 && ( -
- Prerequisites:{" "} - {courseMap[selectedCourse].allPrereqs.join(", ")} -
- )} -
- )}
); } @@ -373,8 +350,9 @@ function SectionCourses({ section, completedCourses, onCourseToggle, onCourseUnt export default function CourseProgressionPage() { const [selectedDept, setSelectedDept] = useState(null); const [showComingSoon, setShowComingSoon] = useState(false); - const [selectedSection, setSelectedSection] = useState("Program Core"); const [completedCourses, setCompletedCourses] = useState([]); + const [highlightedCourseCode, setHighlightedCourseCode] = useState(null); + const [showAvailableNow, setShowAvailableNow] = useState(true); const [showResetConfirm, setShowResetConfirm] = useState(false); // Use CDN prerequisite graph to auto-mark prerequisite chains when selecting a course. @@ -450,8 +428,64 @@ export default function CourseProgressionPage() { name: s.section, credits: s.credits, description: s.description, + referenceLink: s.referenceLink, })); + const allSectionCourses = useMemo( + () => + cseCurriculum.flatMap((section) => { + const courses = section.courses + ? section.courses + : section.streams + ? section.streams.flatMap((stream) => stream.courses) + : []; + + return courses.map((course) => ({ + code: course.code, + name: course.name, + sectionName: section.section, + })); + }), + [], + ); + + const availableCourses = useMemo(() => { + const completedSet = new Set(completedCourses); + + return allSectionCourses.filter((course) => { + if (completedSet.has(course.code)) return false; + + const prereqTree = courseMap[course.code]?.prereqTree; + if (!prereqTree) return true; + + return arePrerequisitesSatisfied(prereqTree, completedCourses); + }); + }, [allSectionCourses, courseMap, completedCourses]); + + const handleJumpToCourse = useCallback((sectionName, courseCode) => { + setHighlightedCourseCode(courseCode); + const cardEl = document.getElementById(getCourseCardId(courseCode)); + if (cardEl) { + cardEl.scrollIntoView({ behavior: "smooth", block: "center" }); + return; + } + + const sectionEl = document.getElementById(getSectionAnchorId(sectionName)); + if (sectionEl) { + sectionEl.scrollIntoView({ behavior: "smooth", block: "start" }); + } + }, []); + + useEffect(() => { + if (!highlightedCourseCode) return; + + const timer = setTimeout(() => { + setHighlightedCourseCode(null); + }, 2600); + + return () => clearTimeout(timer); + }, [highlightedCourseCode]); + // Calculate total completed credits const totalCompletedCredits = completedCourses.reduce((total, code) => { // Find course in curriculum @@ -467,7 +501,7 @@ export default function CourseProgressionPage() { }, 0); return ( -
+

@@ -500,70 +534,6 @@ export default function CourseProgressionPage() { ))}

- {/* Section Navigation for CSE */} - {selectedDept === "CSE" && ( -
-
- {cseSections.map((section) => ( - - ))} -
- - {/* Progress Stats with Reset Button */} -
-
-
-

- Completed Courses:{" "} - - {completedCourses.length} - -

-

- Completed Credits:{" "} - - {totalCompletedCredits} / {getTotalCredits()} - -

-
-
-
-
- {completedCourses.length > 0 && ( - - )} -
-
-
- )} - {/* Main Content */}
{!selectedDept ? ( @@ -579,15 +549,134 @@ export default function CourseProgressionPage() {
) : ( - +
+ {cseSections.map((section) => ( +
+
+

{section.name}

+ + {section.credits} credits + +
+ {section.description && ( +

{section.description}

+ )} + {section.referenceLink && ( +

+ You can see electives from here: {" "} + + Electives Sheet + +

+ )} + +
+ ))} +
)}
+ + {/* Mini Sticky Progress (CSE) */} + {selectedDept === "CSE" && ( +
+
+
+
+
+

+ Completed Courses:{" "} + {completedCourses.length} +

+

+ Completed Credits:{" "} + + {totalCompletedCredits} / {getTotalCredits()} + +

+
+
+
+
+
+
+
+ Completed (Click to undo) +
+
+
+ Available +
+
+
+ Locked +
+
+ +
+
+

+ Available now ({availableCourses.length}) +

+ +
+ {showAvailableNow && ( +
+ {availableCourses.length > 0 ? ( + availableCourses.map((course) => ( + + )) + ) : ( + No available courses right now. + )} +
+ )} +
+
+ + {completedCourses.length > 0 && ( + + )} +
+
+
+ )}
); } diff --git a/src/constants/cseCurriculum.js b/src/constants/cseCurriculum.js index 7af9dd1..c2e75c3 100644 --- a/src/constants/cseCurriculum.js +++ b/src/constants/cseCurriculum.js @@ -194,6 +194,7 @@ export const cseCurriculum = [ section: "Program Electives", credits: 6, description: "Choose elective courses to specialize", + referenceLink: "https://docs.google.com/spreadsheets/d/1-JM6a-JM4y4TiqMv9M4OXMBTnIAbn0lhsvfVGlfR1OU/edit?gid=1207964579#gid=1207964579", courses: [ { code: "CSEElective", name: "CSE Elective (Any 300/400 level CSE course)", credits: 3, elective: true }, { code: "OpenElective", name: "Open Elective (CSE / Minor / GenEd)", credits: 3, elective: true }, From c4593d5daf27d1c7f6eb3a8138473be14c1382a1 Mon Sep 17 00:00:00 2001 From: Al- Saihan Date: Sun, 19 Apr 2026 23:02:23 +0600 Subject: [PATCH 05/20] feat: enhance CourseCard and SectionCourses components with prerequisite navigation and UI improvements --- src/app/(main)/course-progression/page.jsx | 56 +++++++++++++++++----- src/constants/cseCurriculum.js | 19 ++++---- 2 files changed, 54 insertions(+), 21 deletions(-) diff --git a/src/app/(main)/course-progression/page.jsx b/src/app/(main)/course-progression/page.jsx index 4beabfa..f0d6148 100644 --- a/src/app/(main)/course-progression/page.jsx +++ b/src/app/(main)/course-progression/page.jsx @@ -154,7 +154,7 @@ function useConnectCDN() { } //! MARK: Course Card -function CourseCard({ course, isCompleted, isAvailable, isSelected, isHighlighted, onClick, courseDetails }) { +function CourseCard({ course, isCompleted, isAvailable, isSelected, isHighlighted, onClick, onPrereqClick, courseDetails }) { const [showTooltip, setShowTooltip] = useState(false); let statusColor = "border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800"; @@ -170,13 +170,13 @@ function CourseCard({ course, isCompleted, isAvailable, isSelected, isHighlighte statusIcon = ; statusText = "Available (Click to complete)"; } else { - statusColor = "border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800/50 opacity-60"; - statusIcon = ; + statusColor = "border-gray-300 dark:border-gray-800 bg-gray-200 dark:bg-gray-900/50 text-black dark:text-white"; + statusIcon = ; statusText = "Locked - Prerequisites needed"; } const highlightClass = isHighlighted - ? "ring-2 ring--500 dark:ring-white-400 ring-offset-2 ring-offset-white dark:ring-offset-gray-900 shadow-lg shadow-white-500/30" + ? "ring-2 ring-cyan-500 dark:ring-cyan-400 ring-offset-2 ring-offset-white dark:ring-offset-gray-900 shadow-lg shadow-cyan-500/30" : ""; return ( @@ -194,6 +194,25 @@ function CourseCard({ course, isCompleted, isAvailable, isSelected, isHighlighte {course.name || courseDetails?.name || "Course"}
{course.credits ?? courseDetails?.credits ?? 3} credits
+ + {courseDetails?.allPrereqs?.length > 0 && ( +
+ {courseDetails.allPrereqs.map((prereq) => ( + + ))} +
+ )}
{statusIcon}
@@ -217,7 +236,7 @@ function CourseCard({ course, isCompleted, isAvailable, isSelected, isHighlighte } //! MARK: SectionCourses -function SectionCourses({ section, completedCourses, highlightedCourseCode, onCourseToggle, onCourseUntoggle }) { +function SectionCourses({ section, completedCourses, highlightedCourseCode, onCourseToggle, onCourseUntoggle, onPrereqClick }) { const { courseMap, loading, error } = useConnectCDN(); const [selectedCourse, setSelectedCourse] = useState(null); @@ -314,6 +333,7 @@ function SectionCourses({ section, completedCourses, highlightedCourseCode, onCo isSelected={status === "selected"} isHighlighted={highlightedCourseCode === course.code} onClick={handleCourseClick} + onPrereqClick={onPrereqClick} courseDetails={courseMap[course.code]} /> ); @@ -335,13 +355,13 @@ function SectionCourses({ section, completedCourses, highlightedCourseCode, onCo isSelected={status === "selected"} isHighlighted={highlightedCourseCode === course.code} onClick={handleCourseClick} + onPrereqClick={onPrereqClick} courseDetails={courseMap[course.code]} /> ); })}
)} -
); } @@ -476,6 +496,14 @@ export default function CourseProgressionPage() { } }, []); + const handlePrereqJump = useCallback((courseCode) => { + setHighlightedCourseCode(courseCode); + const cardEl = document.getElementById(getCourseCardId(courseCode)); + if (cardEl) { + cardEl.scrollIntoView({ behavior: "smooth", block: "center" }); + } + }, []); + useEffect(() => { if (!highlightedCourseCode) return; @@ -501,7 +529,7 @@ export default function CourseProgressionPage() { }, 0); return ( -
+

@@ -563,14 +591,13 @@ export default function CourseProgressionPage() { )} {section.referenceLink && (

- You can see electives from here: {" "} - Electives Sheet + Reference Link

)} @@ -580,6 +607,7 @@ export default function CourseProgressionPage() { highlightedCourseCode={highlightedCourseCode} onCourseToggle={handleCourseToggle} onCourseUntoggle={handleCourseUntoggle} + onPrereqClick={handlePrereqJump} />

))} @@ -597,7 +625,9 @@ export default function CourseProgressionPage() {

Completed Courses:{" "} - {completedCourses.length} + + {completedCourses.length} +

Completed Credits:{" "} @@ -648,14 +678,16 @@ export default function CourseProgressionPage() { key={`${course.sectionName}-${course.code}`} type="button" onClick={() => handleJumpToCourse(course.sectionName, course.code)} - className="px-2 py-1 text-xs rounded-md border border-yellow-400/70 bg-yellow-100/70 text-yellow-900 dark:bg-yellow-900/30 dark:text-yellow-100 dark:border-yellow-700/70 hover:bg-yellow-200/80 dark:hover:bg-yellow-800/40 transition-colors" + className="px-2 py-1 text-xs rounded-md border border-gray-300 dark:border-gray-600 bg-white/90 dark:bg-gray-800/80 text-gray-800 dark:text-gray-100 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors" title={`Jump to ${course.sectionName}`} > {course.code} )) ) : ( - No available courses right now. + + No available courses right now. + )}

)} diff --git a/src/constants/cseCurriculum.js b/src/constants/cseCurriculum.js index c2e75c3..2db76ae 100644 --- a/src/constants/cseCurriculum.js +++ b/src/constants/cseCurriculum.js @@ -1,4 +1,3 @@ - // ! Calculate total credits from curriculum export function getTotalCredits() { let total = 0; @@ -81,7 +80,7 @@ export const cseCurriculum = [ ], }, ], - note: "Minimum one from: HUM101, HUM102, HST102, HST103, HST104, HUM207, ENG110, ENG113, ENG114, ENG115, ENG333", + note: "One Course From: HUM101, HUM102, HST102, HST103, HST104, HUM207, ENG110, ENG113, ENG114, ENG115, ENG333", }, { name: "Stream 4: Social Sciences", @@ -119,7 +118,7 @@ export const cseCurriculum = [ ], }, ], - note: "Minimum one from: PSY101, SOC101, ANT101, POL101, BUS201, ECO101, ECO102, ECO105, BUS102, POL102, POL103, POL201, POL202, PSY102, DEV104, DEV201, SOC201/ANT202, ANT342, ANT351, BUS333, BUS334, BUS335", + note: "One Course From: PSY101, SOC101, ANT101, POL101, BUS201, ECO101, ECO102, ECO105, BUS102, POL102, POL103, POL201, POL202, PSY102, DEV104, DEV201, SOC201/ANT202, ANT342, ANT351, BUS333, BUS334, BUS335", }, { name: "Stream 5: Community Service & Transformation", @@ -145,7 +144,7 @@ export const cseCurriculum = [ ], }, ], - note: "Minimum one from: CST201, CST301, CST302, CST303, CST304, CST305, CST306, CST307, CST308, CST309, CST310", + note: "One Course From: CST201, CST301, CST302, CST303, CST304, CST305, CST306, CST307, CST308, CST309, CST310", }, { name: "Stream 6: Courses Out Of Department (COD)", @@ -154,7 +153,7 @@ export const cseCurriculum = [ { code: "COD - 1", name: "Courses Out Of Department Option 1", credits: 3, optional: true }, { code: "COD - 2", name: "Courses Out Of Department Option 2", credits: 3, optional: true }, ], - note: "Choose any two 3-credit courses (6 credits total). Minimum one from: HUM101, HUM102, HST102, HST103, HST104, HUM207, ENG110, ENG113, ENG114, ENG115, ENG333, PSY101, SOC101, ANT101, POL101, BUS201, ECO101, ECO102, ECO105, BUS102, POL102, POL103, POL201, POL202, PSY102, DEV104, DEV201, SOC201/ANT202, ANT342, ANT351, BUS333, BUS334, BUS335, CST201, CST301, CST302, CST303, CST304, CST305, CST306, CST307, CST308, CST309, CST310, CHE101, BIO101, ENV103", + note: "Two Courses From: HUM101, HUM102, HST102, HST103, HST104, HUM207, ENG110, ENG113, ENG114, ENG115, ENG333, PSY101, SOC101, ANT101, POL101, BUS201, ECO101, ECO102, ECO105, BUS102, POL102, POL103, POL201, POL202, PSY102, DEV104, DEV201, SOC201/ANT202, ANT342, ANT351, BUS333, BUS334, BUS335, CST201, CST301, CST302, CST303, CST304, CST305, CST306, CST307, CST308, CST309, CST310, CHE101, BIO101, ENV103", }, ], }, @@ -194,7 +193,8 @@ export const cseCurriculum = [ section: "Program Electives", credits: 6, description: "Choose elective courses to specialize", - referenceLink: "https://docs.google.com/spreadsheets/d/1-JM6a-JM4y4TiqMv9M4OXMBTnIAbn0lhsvfVGlfR1OU/edit?gid=1207964579#gid=1207964579", + referenceLink: + "https://docs.google.com/spreadsheets/d/1-JM6a-JM4y4TiqMv9M4OXMBTnIAbn0lhsvfVGlfR1OU/edit?gid=1207964579#gid=1207964579", courses: [ { code: "CSEElective", name: "CSE Elective (Any 300/400 level CSE course)", credits: 3, elective: true }, { code: "OpenElective", name: "Open Elective (CSE / Minor / GenEd)", credits: 3, elective: true }, @@ -203,8 +203,10 @@ export const cseCurriculum = [ { section: "Final Requirement", credits: 4, - description: "Capstone project or thesis", - courses: [{ code: "CSE400", name: "Project / Thesis", credits: 4 }], + description: "Thesis / Project / Internship", + referenceLink: + "https://docs.google.com/document/d/1pAMjuQAxSEcLgkbvmx9qJGvK2BlPQhvXhgev8OeJTas/edit?tab=t.0#heading=h.aczyuw2yex2w", + courses: [{ code: "CSE400", name: "Project / Thesis / Internship", credits: 4 }], }, ]; @@ -229,4 +231,3 @@ export const prerequisiteOverrides = { ...Object.fromEntries(allCurriculumCourseCodes.map((code) => [code, null])), CSE260: "CSE251", }; - From 4d478f4f3981a55391e160cf901136fb71ec6c59 Mon Sep 17 00:00:00 2001 From: Al- Saihan Date: Mon, 20 Apr 2026 02:58:17 +0600 Subject: [PATCH 06/20] feat: simplify total credits calculation and update course progression links in navigation --- src/app/(main)/course-progression/page.jsx | 2 +- src/constants/cseCurriculum.js | 13 +------------ src/constants/featureList.js | 8 ++++++++ src/constants/toolLinks.js | 1 + 4 files changed, 11 insertions(+), 13 deletions(-) diff --git a/src/app/(main)/course-progression/page.jsx b/src/app/(main)/course-progression/page.jsx index f0d6148..8673d68 100644 --- a/src/app/(main)/course-progression/page.jsx +++ b/src/app/(main)/course-progression/page.jsx @@ -525,7 +525,7 @@ export default function CourseProgressionPage() { const course = courses.find((c) => c.code === code); if (course) return total + Number(course.credits ?? 3); } - return total + 3; // Default 3 credits if not found + return total; }, 0); return ( diff --git a/src/constants/cseCurriculum.js b/src/constants/cseCurriculum.js index 2db76ae..97d6699 100644 --- a/src/constants/cseCurriculum.js +++ b/src/constants/cseCurriculum.js @@ -1,17 +1,6 @@ // ! Calculate total credits from curriculum export function getTotalCredits() { - let total = 0; - for (const section of cseCurriculum) { - if (Array.isArray(section.courses)) { - total += section.courses.reduce((sum, c) => sum + (c.credits || 0), 0); - } - if (Array.isArray(section.streams)) { - for (const stream of section.streams) { - total += stream.courses.reduce((sum, c) => sum + (c.credits || 0), 0); - } - } - } - return total; + return cseCurriculum.reduce((sum, section) => sum + (section.credits || 0), 0); } export const cseCurriculum = [ diff --git a/src/constants/featureList.js b/src/constants/featureList.js index 1e3e740..44fe112 100644 --- a/src/constants/featureList.js +++ b/src/constants/featureList.js @@ -35,6 +35,14 @@ const featureList = [ }, { index: 5, + title: "Course Progression", + description: "Track completed courses, check prerequisites, and visualize your graduation progress.", + footer: "🟡 Work In Progress", + href: "/course-progression", + dashboardHref: "/course-progression" + }, + { + index: 6, title: "Faculty Review", description: "Rate your faculty and provide feedback. Scrolling through useless reviews is a thing of the past!", footer: "🟡 Coming Soon" diff --git a/src/constants/toolLinks.js b/src/constants/toolLinks.js index e23e712..48c7e65 100644 --- a/src/constants/toolLinks.js +++ b/src/constants/toolLinks.js @@ -2,6 +2,7 @@ import { title } from "process"; const toolLinks = [ { title: 'PrePreReg', href: '/preprereg' }, + { title: 'Course Progression', href: '/course-progression' }, { title: 'Course Swap', href: '/courseswap' }, { title: 'Merge Routines', href: '/merge-routines' }, { title: 'Course Directory', href: '/course-materials' }, From b8192bf44d1f39c5dcd35d74b78eb32d632607a6 Mon Sep 17 00:00:00 2001 From: Al- Saihan Date: Mon, 20 Apr 2026 03:38:42 +0600 Subject: [PATCH 07/20] feat: enhance CourseProgressionPage with progress panel and UI improvements for course navigation --- src/app/(main)/course-progression/page.jsx | 243 +++++++++++++-------- 1 file changed, 149 insertions(+), 94 deletions(-) diff --git a/src/app/(main)/course-progression/page.jsx b/src/app/(main)/course-progression/page.jsx index 8673d68..1cba70c 100644 --- a/src/app/(main)/course-progression/page.jsx +++ b/src/app/(main)/course-progression/page.jsx @@ -3,7 +3,7 @@ import React, { useState, useEffect, useCallback, useMemo } from "react"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { cseCurriculum, getTotalCredits, prerequisiteOverrides } from "@/constants/cseCurriculum"; -import { CheckCircle2, Lock, Unlock, RefreshCw } from "lucide-react"; +import { CheckCircle2, ChevronDown, ChevronUp, Lock, Unlock, RefreshCw } from "lucide-react"; const departments = [ { code: "CSE", name: "Computer Science & Engineering" }, @@ -302,9 +302,7 @@ function SectionCourses({ section, completedCourses, highlightedCourseCode, onCo } if (error) { - return ( -
Error loading course data: {error}
- ); + return
Error loading course data: {error}
; } return ( @@ -366,13 +364,12 @@ function SectionCourses({ section, completedCourses, highlightedCourseCode, onCo ); } -//! MARK: Main Page export default function CourseProgressionPage() { const [selectedDept, setSelectedDept] = useState(null); const [showComingSoon, setShowComingSoon] = useState(false); const [completedCourses, setCompletedCourses] = useState([]); const [highlightedCourseCode, setHighlightedCourseCode] = useState(null); - const [showAvailableNow, setShowAvailableNow] = useState(true); + const [showProgressPanel, setShowProgressPanel] = useState(false); const [showResetConfirm, setShowResetConfirm] = useState(false); // Use CDN prerequisite graph to auto-mark prerequisite chains when selecting a course. @@ -528,8 +525,12 @@ export default function CourseProgressionPage() { return total; }, 0); + const totalCredits = getTotalCredits(); + const progressPercent = totalCredits > 0 ? Math.min(100, Math.round((totalCompletedCredits / totalCredits) * 100)) : 0; + const coursesLeft = Math.max(0, allSectionCourses.length - completedCourses.length); + return ( -
+

@@ -539,9 +540,26 @@ export default function CourseProgressionPage() {

- Click on available courses (yellow) to mark them as completed. Click on completed courses (green) to undo - and automatically lock dependencies. + Click any Available course to + mark it complete. Click a Completed{" "} + course to undo it; dependent courses will become Locked{" "} + again automatically.

+ +
+ + + Completed + + + + Available + + + + Locked + +
{/* Department Selection */} @@ -616,98 +634,135 @@ export default function CourseProgressionPage() {
- {/* Mini Sticky Progress (CSE) */} {selectedDept === "CSE" && ( -
-
-
-
-
-

- Completed Courses:{" "} - - {completedCourses.length} - -

-

- Completed Credits:{" "} - - {totalCompletedCredits} / {getTotalCredits()} - -

-
-
-
-
-
-
-
- Completed (Click to undo) -
-
-
- Available -
-
-
- Locked -
-
+ <> + {!showProgressPanel ? ( +
+
+ -
-
-

- Available now ({availableCourses.length}) -

- -
- {showAvailableNow && ( -
- {availableCourses.length > 0 ? ( - availableCourses.map((course) => ( - - )) - ) : ( - - No available courses right now. - + {completedCourses.length > 0 && ( + + )} +
+
+ ) : ( +
+
+
+
+
+ {completedCourses.length > 0 && ( + )} +
- )} + +
+
+
+ {progressPercent}% +
+
+ +
+

+ Courses {" "} + + {completedCourses.length} + +

+

+ Courses left {" "} + + {coursesLeft} + +

+

+ Credits {" "} + + {totalCompletedCredits} / {totalCredits} + +

+
+
+ +
+
+

+ Available now + + {availableCourses.length} + +

+
+ +

+ Click any course code to navigate. +

+ +
+ {availableCourses.length > 0 ? ( + availableCourses.map((course) => ( + + )) + ) : ( + + No available courses right now. + + )} +
+
+
- - {completedCourses.length > 0 && ( - - )}
-
-
+ )} + )}
); From 8b0269733bebf9c708d6c3c1b3dc1e3b8f9601ce Mon Sep 17 00:00:00 2001 From: Al- Saihan Date: Mon, 20 Apr 2026 03:48:11 +0600 Subject: [PATCH 08/20] feat: improve CourseCard styling and update course details display for better readability --- src/app/(main)/course-progression/page.jsx | 8 +-- src/constants/cseCurriculum.js | 84 +++++++++++++++++++--- 2 files changed, 80 insertions(+), 12 deletions(-) diff --git a/src/app/(main)/course-progression/page.jsx b/src/app/(main)/course-progression/page.jsx index 1cba70c..1abe1f5 100644 --- a/src/app/(main)/course-progression/page.jsx +++ b/src/app/(main)/course-progression/page.jsx @@ -189,11 +189,11 @@ function CourseCard({ course, isCompleted, isAvailable, isSelected, isHighlighte >
-
{course.code}
-
+
{course.code}
+
{course.name || courseDetails?.name || "Course"}
-
{course.credits ?? courseDetails?.credits ?? 3} credits
+
{course.credits ?? courseDetails?.credits ?? 3} credits
{courseDetails?.allPrereqs?.length > 0 && (
@@ -205,7 +205,7 @@ function CourseCard({ course, isCompleted, isAvailable, isSelected, isHighlighte event.stopPropagation(); onPrereqClick?.(prereq); }} - className="px-2 py-0.5 text-[10px] rounded-full border border-blue-300 dark:border-blue-700 text-blue-700 dark:text-blue-300 bg-blue-50/80 dark:bg-blue-900/30 hover:bg-blue-100 dark:hover:bg-blue-900/50 transition-colors" + className="px-2.5 py-0.5 text-[11px] rounded-full border border-blue-300 dark:border-blue-700 text-blue-700 dark:text-blue-300 bg-blue-50/80 dark:bg-blue-900/30 hover:bg-blue-100 dark:hover:bg-blue-900/50 transition-colors" title={`Jump to prerequisite ${prereq}`} > {prereq} diff --git a/src/constants/cseCurriculum.js b/src/constants/cseCurriculum.js index 97d6699..0caae57 100644 --- a/src/constants/cseCurriculum.js +++ b/src/constants/cseCurriculum.js @@ -1,3 +1,71 @@ +/* +! MARK: Data Structure Tree +! Adding This Here So Someone Else Understands What The Actual Fuck This Data Structure Is Supposed To Look Like +? And Of Course- For Future References +? But Mainly Because I believe I made it too complicated lmao (AI is also to partially blame for this) +cseCurriculum (Array) +│ +├── Section +│ ├── section: "University & School Core" +│ ├── credits: 51 +│ ├── description +│ └── streams (Array) +│ │ +│ ├── Stream +│ │ ├── name: "Writing & Communication" +│ │ ├── credits: 6 +│ │ └── courses (Array) +│ │ ├── Course +│ │ │ ├── code: "ENG091" +│ │ │ ├── name +│ │ │ ├── credits +│ │ │ └── optional +│ │ └── ... +│ │ +│ ├── Stream +│ │ ├── name: "Mathematics & Natural Sciences" +│ │ └── courses [...] +│ │ +│ └── ... (more streams) +│ +├── Section +│ ├── section: "Program Core" +│ ├── credits: 75 +│ ├── description +│ └── courses (Array) +│ ├── Course +│ │ ├── code: "CSE110" +│ │ ├── name +│ │ └── credits +│ └── ... +│ +├── Section +│ ├── section: "Program Electives" +│ ├── credits: 6 +│ ├── referenceLink +│ └── courses (Array) +│ ├── Elective Course +│ └── ... +│ +├── Section +│ ├── section: "Final Requirement" +│ ├── credits: 4 +│ └── courses (Array) +│ └── Project / Thesis / Internship +│ +└── (end) +*/ + + +/* +prerequisiteOverrides (Object) +│ +├── courseCode: null (default = use CDN) +├── courseCode: null +├── CSE260: "CSE251" // overridden prerequisite +└── ... +*/ +// ! MARK: JSON Starts Here // ! Calculate total credits from curriculum export function getTotalCredits() { return cseCurriculum.reduce((sum, section) => sum + (section.credits || 0), 0); @@ -50,8 +118,8 @@ export const cseCurriculum = [ { code: "HUM103", name: "Ethics and Culture", credits: 3, optional: false }, { code: "BNG103", name: "Bangla Language and Literature", credits: 3, optional: false }, { - code: "Required Course - S3", - name: "Arts & Humanities Elective (Minimum one option)", + code: "Stream 3 - COD", + name: "Pick one course from the provided options above", credits: 3, optional: false, alternatives: [ @@ -77,8 +145,8 @@ export const cseCurriculum = [ courses: [ { code: "EMB101", name: "Bangladesh Studies", credits: 3, optional: false, alternatives: ["DEV101"] }, { - code: "Required Course - S4", - name: "Social Sciences Elective (Minimum one option)", + code: "Stream 4 - COD", + name: "Pick one course from the provided options above", credits: 3, optional: false, alternatives: [ @@ -114,8 +182,8 @@ export const cseCurriculum = [ credits: 3, courses: [ { - code: "Required Course - S5", - name: "Community Service Course (Minimum one option)", + code: "Stream 5 - COD", + name: "Pick one course from the provided options above", credits: 3, optional: true, alternatives: [ @@ -139,8 +207,8 @@ export const cseCurriculum = [ name: "Stream 6: Courses Out Of Department (COD)", credits: 6, courses: [ - { code: "COD - 1", name: "Courses Out Of Department Option 1", credits: 3, optional: true }, - { code: "COD - 2", name: "Courses Out Of Department Option 2", credits: 3, optional: true }, + { code: "COD - 1", name: "Pick one course from the provided options above", credits: 3, optional: true }, + { code: "COD - 2", name: "Pick another course from the provided options above", credits: 3, optional: true }, ], note: "Two Courses From: HUM101, HUM102, HST102, HST103, HST104, HUM207, ENG110, ENG113, ENG114, ENG115, ENG333, PSY101, SOC101, ANT101, POL101, BUS201, ECO101, ECO102, ECO105, BUS102, POL102, POL103, POL201, POL202, PSY102, DEV104, DEV201, SOC201/ANT202, ANT342, ANT351, BUS333, BUS334, BUS335, CST201, CST301, CST302, CST303, CST304, CST305, CST306, CST307, CST308, CST309, CST310, CHE101, BIO101, ENV103", }, From e24bf05e567b9ba5ae4206121ef1d25d9bba18b4 Mon Sep 17 00:00:00 2001 From: Al- Saihan Date: Mon, 20 Apr 2026 18:15:05 +0600 Subject: [PATCH 09/20] feat: added CS curriculum and prerequisite overrides, enhance course navigation for multiple departments --- src/app/(main)/course-progression/page.jsx | 173 +++++++++---- src/constants/csCurriculum.js | 282 +++++++++++++++++++++ src/constants/cseCurriculum.js | 14 +- 3 files changed, 415 insertions(+), 54 deletions(-) create mode 100644 src/constants/csCurriculum.js diff --git a/src/app/(main)/course-progression/page.jsx b/src/app/(main)/course-progression/page.jsx index 1abe1f5..f722fe1 100644 --- a/src/app/(main)/course-progression/page.jsx +++ b/src/app/(main)/course-progression/page.jsx @@ -2,7 +2,16 @@ import React, { useState, useEffect, useCallback, useMemo } from "react"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; -import { cseCurriculum, getTotalCredits, prerequisiteOverrides } from "@/constants/cseCurriculum"; +import { + cseCurriculum, + getTotalCredits as getCseTotalCredits, + prerequisiteOverrides as csePrerequisiteOverrides, +} from "@/constants/cseCurriculum"; +import { + csCurriculum, + getTotalCredits as getCsTotalCredits, + prerequisiteOverrides as csPrerequisiteOverrides, +} from "@/constants/csCurriculum"; import { CheckCircle2, ChevronDown, ChevronUp, Lock, Unlock, RefreshCw } from "lucide-react"; const departments = [ @@ -13,6 +22,41 @@ const departments = [ { code: "LAW", name: "Law" }, ]; +const departmentCurricula = { + CSE: cseCurriculum, + CS: csCurriculum, +}; + +const departmentPrerequisiteOverrides = { + CSE: csePrerequisiteOverrides, + CS: csPrerequisiteOverrides, +}; + +const supportedDepartments = new Set(["CSE", "CS"]); + +function getCurriculumForDepartment(code) { + return departmentCurricula[code] ?? []; +} + +function getPrerequisiteOverridesForDepartment(code) { + return departmentPrerequisiteOverrides[code] ?? csePrerequisiteOverrides; +} + +function getCompletedCoursesForCurriculum(curriculum, completedCourses) { + return completedCourses.reduce((total, courseCode) => { + for (const section of curriculum) { + let courses = []; + if (section.courses) courses = section.courses; + else if (section.streams) courses = section.streams.flatMap((stream) => stream.courses); + + const course = courses.find((c) => c.code === courseCode); + if (course) return total + Number(course.credits ?? 3); + } + + return total; + }, 0); +} + function getSectionAnchorId(sectionName) { return `section-${sectionName.toLowerCase().replace(/[^a-z0-9]+/g, "-")}`; } @@ -109,7 +153,7 @@ function arePrerequisitesSatisfied(prereqTree, completedCourses) { //! MARK: Course Data Fetch //! CDN -function useConnectCDN() { +function useConnectCDN(prereqOverrides = csePrerequisiteOverrides) { const [courseMap, setCourseMap] = useState({}); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -126,7 +170,7 @@ function useConnectCDN() { const map = {}; for (const c of data) { if (!map[c.courseCode]) { - const overridePrereq = prerequisiteOverrides[c.courseCode]; + const overridePrereq = prereqOverrides[c.courseCode]; const effectivePrereqRaw = overridePrereq ?? c.prerequisiteCourses; const effectivePrereqTree = parsePrereqString(effectivePrereqRaw); @@ -148,7 +192,7 @@ function useConnectCDN() { } } fetchData(); - }, []); + }, [prereqOverrides]); return { courseMap, loading, error }; } @@ -236,11 +280,21 @@ function CourseCard({ course, isCompleted, isAvailable, isSelected, isHighlighte } //! MARK: SectionCourses -function SectionCourses({ section, completedCourses, highlightedCourseCode, onCourseToggle, onCourseUntoggle, onPrereqClick }) { - const { courseMap, loading, error } = useConnectCDN(); +function SectionCourses({ + section, + curriculum, + courseMap, + loading, + error, + completedCourses, + highlightedCourseCode, + onCourseToggle, + onCourseUntoggle, + onPrereqClick, +}) { const [selectedCourse, setSelectedCourse] = useState(null); - const sectionObj = cseCurriculum.find((s) => s.section === section); + const sectionObj = curriculum.find((s) => s.section === section); if (!sectionObj) return
Section not found.
; @@ -302,7 +356,9 @@ function SectionCourses({ section, completedCourses, highlightedCourseCode, onCo } if (error) { - return
Error loading course data: {error}
; + return ( +
Error loading course data: {error}
+ ); } return ( @@ -367,17 +423,20 @@ function SectionCourses({ section, completedCourses, highlightedCourseCode, onCo export default function CourseProgressionPage() { const [selectedDept, setSelectedDept] = useState(null); const [showComingSoon, setShowComingSoon] = useState(false); - const [completedCourses, setCompletedCourses] = useState([]); + const [completedCoursesByDept, setCompletedCoursesByDept] = useState({}); const [highlightedCourseCode, setHighlightedCourseCode] = useState(null); const [showProgressPanel, setShowProgressPanel] = useState(false); - const [showResetConfirm, setShowResetConfirm] = useState(false); - // Use CDN prerequisite graph to auto-mark prerequisite chains when selecting a course. - const { courseMap } = useConnectCDN(); + const activeCurriculum = getCurriculumForDepartment(selectedDept); + const activePrerequisiteOverrides = getPrerequisiteOverridesForDepartment(selectedDept); + const completedCourses = selectedDept ? (completedCoursesByDept[selectedDept] ?? []) : []; + + // Use the selected department's prerequisite graph to auto-mark prerequisite chains when selecting a course. + const { courseMap, loading, error } = useConnectCDN(activePrerequisiteOverrides); const handleDeptClick = (code) => { - if (code === "CSE") { - setSelectedDept("CSE"); + if (supportedDepartments.has(code)) { + setSelectedDept(code); setShowComingSoon(false); } else { setSelectedDept(code); @@ -387,8 +446,11 @@ export default function CourseProgressionPage() { // Mark as completed const handleCourseToggle = (courseCode) => { - setCompletedCourses((prev) => { - const next = new Set(prev); + if (!selectedDept || !supportedDepartments.has(selectedDept)) return; + + setCompletedCoursesByDept((prev) => { + const currentCompleted = prev[selectedDept] ?? []; + const next = new Set(currentCompleted); const stack = [courseCode]; while (stack.length > 0) { @@ -405,13 +467,19 @@ export default function CourseProgressionPage() { } } - return Array.from(next); + return { + ...prev, + [selectedDept]: Array.from(next), + }; }); }; // Mark as incomplete and remove its prerequisite chain. const handleCourseUntoggle = (courseCode) => { - setCompletedCourses((prev) => { + if (!selectedDept || !supportedDepartments.has(selectedDept)) return; + + setCompletedCoursesByDept((prev) => { + const currentCompleted = prev[selectedDept] ?? []; const toRemove = new Set(); const stack = [courseCode]; @@ -429,19 +497,26 @@ export default function CourseProgressionPage() { } } - return prev.filter((c) => !toRemove.has(c)); + return { + ...prev, + [selectedDept]: currentCompleted.filter((c) => !toRemove.has(c)), + }; }); }; // Reset all progress const handleReset = () => { + if (!selectedDept || !supportedDepartments.has(selectedDept)) return; + if (window.confirm("Are you sure you want to reset all your progress? This cannot be undone.")) { - setCompletedCourses([]); - setShowResetConfirm(false); + setCompletedCoursesByDept((prev) => ({ + ...prev, + [selectedDept]: [], + })); } }; - const cseSections = cseCurriculum.map((s) => ({ + const activeSections = activeCurriculum.map((s) => ({ name: s.section, credits: s.credits, description: s.description, @@ -450,7 +525,7 @@ export default function CourseProgressionPage() { const allSectionCourses = useMemo( () => - cseCurriculum.flatMap((section) => { + activeCurriculum.flatMap((section) => { const courses = section.courses ? section.courses : section.streams @@ -463,7 +538,7 @@ export default function CourseProgressionPage() { sectionName: section.section, })); }), - [], + [activeCurriculum], ); const availableCourses = useMemo(() => { @@ -512,20 +587,9 @@ export default function CourseProgressionPage() { }, [highlightedCourseCode]); // Calculate total completed credits - const totalCompletedCredits = completedCourses.reduce((total, code) => { - // Find course in curriculum - for (const section of cseCurriculum) { - let courses = []; - if (section.courses) courses = section.courses; - else if (section.streams) courses = section.streams.flatMap((s) => s.courses); + const totalCompletedCredits = getCompletedCoursesForCurriculum(activeCurriculum, completedCourses); - const course = courses.find((c) => c.code === code); - if (course) return total + Number(course.credits ?? 3); - } - return total; - }, 0); - - const totalCredits = getTotalCredits(); + const totalCredits = selectedDept === "CS" ? getCsTotalCredits(activeCurriculum) : getCseTotalCredits(activeCurriculum); const progressPercent = totalCredits > 0 ? Math.min(100, Math.round((totalCompletedCredits / totalCredits) * 100)) : 0; const coursesLeft = Math.max(0, allSectionCourses.length - completedCourses.length); @@ -541,9 +605,10 @@ export default function CourseProgressionPage() {

Click any Available course to - mark it complete. Click a Completed{" "} - course to undo it; dependent courses will become Locked{" "} - again automatically. + mark it complete. Click a{" "} + Completed course to undo it; + dependent courses will become{" "} + Locked again automatically.

@@ -596,7 +661,7 @@ export default function CourseProgressionPage() {
) : (
- {cseSections.map((section) => ( + {activeSections.map((section) => (

{section.name}

@@ -621,6 +686,10 @@ export default function CourseProgressionPage() { )}
- {selectedDept === "CSE" && ( + {selectedDept && ( <> {!showProgressPanel ? (
@@ -647,10 +716,10 @@ export default function CourseProgressionPage() { - Credits {totalCompletedCredits} / {totalCredits} + {selectedDept} credits {totalCompletedCredits} / {totalCredits} - {completedCourses.length > 0 && ( + {completedCourses.length > 0 && supportedDepartments.has(selectedDept) && (
+ + {!supportedDepartments.has(selectedDept) && ( +

+ Progress tracking for this department is not configured yet. +

+ )}
diff --git a/src/constants/csCurriculum.js b/src/constants/csCurriculum.js new file mode 100644 index 0000000..4a312b2 --- /dev/null +++ b/src/constants/csCurriculum.js @@ -0,0 +1,282 @@ +/* +! MARK: Data Structure Tree +! Adding This Here So Someone Else Understands What The Actual Fuck This Data Structure Is Supposed To Look Like +? And Of Course- For Future References +? But Mainly Because I believe I made it too complicated lmao (AI is also to partially blame for this) +cseCurriculum (Array) +│ +├── Section +│ ├── section: "University & School Core" +│ ├── credits: 51 +│ ├── description +│ └── streams (Array) +│ │ +│ ├── Stream +│ │ ├── name: "Writing & Communication" +│ │ ├── credits: 6 +│ │ └── courses (Array) +│ │ ├── Course +│ │ │ ├── code: "ENG091" + courses: [ + { code: "CS Elective - 1", name: "CSE Elective", credits: 3, elective: true }, + { code: "CS Elective - 2", name: "CSE / Minor / GenEd Elective", credits: 3, elective: true }, + { code: "CS Elective - 3", name: "CSE / Minor / GenEd Elective", credits: 3, elective: true }, + { code: "CS Elective - 4", name: "CSE / Minor / GenEd Elective", credits: 3, elective: true }, + { code: "CS Elective - 5", name: "CSE / Minor / GenEd Elective", credits: 3, elective: true }, + { code: "CS Elective - 6", name: "CSE / Minor / GenEd Elective", credits: 3, elective: true }, + { code: "CS Elective - 7", name: "CSE / Minor / GenEd Elective", credits: 3, elective: true }, + ], +│ │ ├── name: "Mathematics & Natural Sciences" +│ │ └── courses [...] +│ │ +│ └── ... (more streams) +│ + description: "Thesis / Project / Internship", + referenceLink: + "https://docs.google.com/document/d/1pAMjuQAxSEcLgkbvmx9qJGvK2BlPQhvXhgev8OeJTas/edit?tab=t.0#heading=h.aczyuw2yex2w", + courses: [{ code: "CSE400", name: "Project / Thesis / Internship", credits: 4 }], +│ └── ... +│ +├── Section +│ ├── section: "Program Electives" +│ ├── credits: 6 +│ ├── referenceLink +│ └── courses (Array) +│ ├── Elective Course +│ └── ... +│ +├── Section +│ ├── section: "Final Requirement" +│ ├── credits: 4 +│ └── courses (Array) +│ └── Project / Thesis / Internship +│ +└── (end) +*/ + +/* +prerequisiteOverrides (Object) +│ +├── courseCode: null (default = use CDN) +├── courseCode: null +├── CSE260: "CSE251" // overridden prerequisite +└── ... +*/ +// ! MARK: JSON Starts Here +export const csCurriculum = [ + { + section: "University & School Core", + credits: 51, + description: "University Core (39) + School Core (12)", + streams: [ + { + name: "Stream 1: Writing Comprehension", + credits: 6, + courses: [ + { code: "ENG091", name: "Foundation Course", credits: 0 }, + { code: "ENG101", name: "English Fundamentals", credits: 3 }, + { code: "ENG102", name: "English Composition I", credits: 3 }, + { code: "ENG103", name: "Advanced Writing Skills and Presentation (*only for ENG102 freshers)", credits: 3 }, + ], + }, + { + name: "Stream 2: Math & Natural Sciences", + credits: 9, + courses: [ + { code: "MAT092", name: "Remedial Mathematics", credits: 0 }, + { code: "MAT110", name: "Math I: Differential Calculus & Coordinate Geometry", credits: 3 }, + { code: "PHY111", name: "Principles of Physics I", credits: 3 }, + { code: "STA201", name: "Statistics & Probability", credits: 3 }, + { + code: "SCI_OPTION", + name: "Optional: CHE101 / BIO101 / ENV103", + credits: 0, + optional: true, + }, + ], + }, + { + name: "Stream 3: Arts & Humanities", + credits: 9, + courses: [ + { code: "HUM103", name: "Ethics and Culture", credits: 3 }, + { code: "BNG103", name: "Bangla Language and Literature", credits: 3 }, + { + code: "Stream 3 - COD", + name: "Choose one from list", + credits: 3, + alternatives: [ + "HUM101", + "HUM102", + "HST102", + "HST103", + "HST104", + "HUM207", + "ENG110", + "ENG113", + "ENG114", + "ENG115", + "ENG333", + ], + }, + ], + }, + { + name: "Stream 4: Social Sciences", + credits: 6, + courses: [ + { + code: "EMB101/DEV101", + name: "Emergence of Bangladesh / Bangladesh Studies", + credits: 3, + }, + { + code: "Stream 4 - COD", + name: "Choose one from list", + credits: 3, + alternatives: [ + "PSY101", + "SOC101", + "ANT101", + "POL101", + "BUS201", + "ECO101", + "ECO102", + "ECO105", + "BUS102", + "POL102", + "POL103", + "POL201", + "POL202", + "PSY102", + "DEV104", + "DEV201", + "SOC201/ANT202", + "ANT342", + "ANT351", + "BUS333", + "BUS334", + "BUS335", + ], + }, + ], + }, + { + name: "Stream 5: Community Transformation", + credits: 3, + courses: [ + { + code: "Stream 5 - COD", + name: "Choose one CST course", + credits: 3, + alternatives: [ + "CST201", + "CST301", + "CST302", + "CST303", + "CST304", + "CST305", + "CST306", + "CST307", + "CST308", + "CST309", + "CST310", + ], + }, + ], + }, + { + name: "GenEd Electives", + credits: 6, + courses: [ + { code: "COD - 4", name: "GenEd Elective", credits: 3, optional: true }, + { code: "COD - 5", name: "GenEd Elective", credits: 3, optional: true }, + ], + }, + { + name: "School Core", + credits: 12, + courses: [ + { code: "MAT120", name: "Math II: Integral Calculus & Differential Equations", credits: 3 }, + { code: "MAT215", name: "Math III: Complex Variables & Laplace Transform", credits: 3 }, + { code: "MAT216", name: "Math IV: Linear Algebra & Fourier Analysis", credits: 3 }, + { code: "PHY112", name: "Principles of Physics II", credits: 3 }, + ], + }, + ], + }, + + { + section: "Program Core", + credits: 48, + description: "Core CSE courses", + courses: [ + { code: "CSE110", name: "Programming Language I", credits: 3 }, + { code: "CSE111", name: "Programming Language II", credits: 3 }, + { code: "CSE220", name: "Data Structures", credits: 3 }, + { code: "CSE221", name: "Algorithms", credits: 3 }, + { code: "CSE230", name: "Discrete Mathematics", credits: 3 }, + { code: "CSE260", name: "Digital Logic Design", credits: 3 }, + { code: "CSE321", name: "Operating System", credits: 3 }, + { code: "CSE330", name: "Numerical Methods", credits: 3 }, + { code: "CSE331", name: "Automata and Computability", credits: 3 }, + { code: "CSE340", name: "Computer Architecture", credits: 3 }, + { code: "CSE370", name: "Database Systems", credits: 3 }, + { code: "CSE420", name: "Compiler Design", credits: 3 }, + { code: "CSE421", name: "Computer Networks", credits: 3 }, + { code: "CSE422", name: "Artificial Intelligence", credits: 3 }, + { code: "CSE423", name: "Computer Graphics", credits: 3 }, + { code: "CSE470", name: "Software Engineering", credits: 3 }, + ], + }, + + { + section: "Program Electives", + credits: 21, + description: + "Elective courses (minimum one must be a CSE elective) || Research the Elective Pre-Requisites Carefully On the Google Sheet", + referenceLink: + "https://docs.google.com/spreadsheets/d/1-JM6a-JM4y4TiqMv9M4OXMBTnIAbn0lhsvfVGlfR1OU/edit?gid=1207964579#gid=1207964579", + courses: [ + { code: "CS Elective - 1", name: "CSE Elective", credits: 3, elective: true }, + { code: "CS Elective - 2", name: "CSE / Minor / GenEd Elective", credits: 3, elective: true }, + { code: "CS Elective - 3", name: "CSE / Minor / GenEd Elective", credits: 3, elective: true }, + { code: "CS Elective - 4", name: "CSE / Minor / GenEd Elective", credits: 3, elective: true }, + { code: "CS Elective - 5", name: "CSE / Minor / GenEd Elective", credits: 3, elective: true }, + { code: "CS Elective - 6", name: "CSE / Minor / GenEd Elective", credits: 3, elective: true }, + { code: "CS Elective - 7", name: "CSE / Minor / GenEd Elective", credits: 3, elective: true }, + ], + }, + + { + section: "Final Requirement", + credits: 4, + description: "Thesis / Project / Internship", + referenceLink: + "https://docs.google.com/document/d/1pAMjuQAxSEcLgkbvmx9qJGvK2BlPQhvXhgev8OeJTas/edit?tab=t.0#heading=h.aczyuw2yex2w", + courses: [{ code: "CSE400", name: "Project / Thesis / Internship", credits: 4 }], + }, +]; + +// ! Calculate total credits from curriculum +export function getTotalCredits(curriculum = csCurriculum) { + return curriculum.reduce((sum, section) => sum + (section.credits || 0), 0); +} + +// ! Pre-req override map +const allCurriculumCourseCodes = Array.from( + new Set( + csCurriculum.flatMap((section) => { + if (section.courses) return section.courses.map((c) => c.code); + if (section.streams) return section.streams.flatMap((s) => s.courses.map((c) => c.code)); + return []; + }), + ), +); + +export const prerequisiteOverrides = { + ...Object.fromEntries(allCurriculumCourseCodes.map((code) => [code, null])), + CSE260: "CSE230", // more realistic prereq +}; + +export const cseCurriculum = csCurriculum; diff --git a/src/constants/cseCurriculum.js b/src/constants/cseCurriculum.js index 0caae57..93df079 100644 --- a/src/constants/cseCurriculum.js +++ b/src/constants/cseCurriculum.js @@ -56,7 +56,6 @@ cseCurriculum (Array) └── (end) */ - /* prerequisiteOverrides (Object) │ @@ -75,7 +74,7 @@ export const cseCurriculum = [ { section: "University & School Core", credits: 51, - description: "Combined University Core and School Core courses", + description: "University Core (39) + School Core (12)", streams: [ { name: "Stream 1: Writing & Communication", @@ -84,7 +83,12 @@ export const cseCurriculum = [ { code: "ENG091", name: "Foundation Course (in English)", credits: 0, optional: false }, { code: "ENG101", name: "Fundamentals of English", credits: 3, optional: false }, { code: "ENG102", name: "English Composition I", credits: 3, optional: false }, - { code: "ENG103", name: "Advanced Writing Skills and Presentation", credits: 3, optional: false }, + { + code: "ENG103", + name: "Advanced Writing Skills and Presentation (*only for ENG102 freshers)", + credits: 3, + optional: false, + }, ], }, { @@ -207,8 +211,8 @@ export const cseCurriculum = [ name: "Stream 6: Courses Out Of Department (COD)", credits: 6, courses: [ - { code: "COD - 1", name: "Pick one course from the provided options above", credits: 3, optional: true }, - { code: "COD - 2", name: "Pick another course from the provided options above", credits: 3, optional: true }, + { code: "COD - 4", name: "Pick one course from the provided options above", credits: 3, optional: true }, + { code: "COD - 5", name: "Pick another course from the provided options above", credits: 3, optional: true }, ], note: "Two Courses From: HUM101, HUM102, HST102, HST103, HST104, HUM207, ENG110, ENG113, ENG114, ENG115, ENG333, PSY101, SOC101, ANT101, POL101, BUS201, ECO101, ECO102, ECO105, BUS102, POL102, POL103, POL201, POL202, PSY102, DEV104, DEV201, SOC201/ANT202, ANT342, ANT351, BUS333, BUS334, BUS335, CST201, CST301, CST302, CST303, CST304, CST305, CST306, CST307, CST308, CST309, CST310, CHE101, BIO101, ENV103", }, From f392e9154ee5abc21f1881c1f0a308ad9ce02af4 Mon Sep 17 00:00:00 2001 From: Al- Saihan Date: Mon, 20 Apr 2026 18:26:50 +0600 Subject: [PATCH 10/20] feat: update csCurriculum and cseCurriculum for improved course structure --- src/constants/csCurriculum.js | 92 ++++++++++++++++++---------------- src/constants/cseCurriculum.js | 16 +++--- 2 files changed, 56 insertions(+), 52 deletions(-) diff --git a/src/constants/csCurriculum.js b/src/constants/csCurriculum.js index 4a312b2..9c18992 100644 --- a/src/constants/csCurriculum.js +++ b/src/constants/csCurriculum.js @@ -17,24 +17,26 @@ cseCurriculum (Array) │ │ └── courses (Array) │ │ ├── Course │ │ │ ├── code: "ENG091" - courses: [ - { code: "CS Elective - 1", name: "CSE Elective", credits: 3, elective: true }, - { code: "CS Elective - 2", name: "CSE / Minor / GenEd Elective", credits: 3, elective: true }, - { code: "CS Elective - 3", name: "CSE / Minor / GenEd Elective", credits: 3, elective: true }, - { code: "CS Elective - 4", name: "CSE / Minor / GenEd Elective", credits: 3, elective: true }, - { code: "CS Elective - 5", name: "CSE / Minor / GenEd Elective", credits: 3, elective: true }, - { code: "CS Elective - 6", name: "CSE / Minor / GenEd Elective", credits: 3, elective: true }, - { code: "CS Elective - 7", name: "CSE / Minor / GenEd Elective", credits: 3, elective: true }, - ], +│ │ │ ├── name +│ │ │ ├── credits +│ │ │ └── optional +│ │ └── ... +│ │ +│ ├── Stream │ │ ├── name: "Mathematics & Natural Sciences" │ │ └── courses [...] │ │ │ └── ... (more streams) │ - description: "Thesis / Project / Internship", - referenceLink: - "https://docs.google.com/document/d/1pAMjuQAxSEcLgkbvmx9qJGvK2BlPQhvXhgev8OeJTas/edit?tab=t.0#heading=h.aczyuw2yex2w", - courses: [{ code: "CSE400", name: "Project / Thesis / Internship", credits: 4 }], +├── Section +│ ├── section: "Program Core" +│ ├── credits: 75 +│ ├── description +│ └── courses (Array) +│ ├── Course +│ │ ├── code: "CSE110" +│ │ ├── name +│ │ └── credits │ └── ... │ ├── Section @@ -70,7 +72,7 @@ export const csCurriculum = [ description: "University Core (39) + School Core (12)", streams: [ { - name: "Stream 1: Writing Comprehension", + name: "Stream 1: Writing & Communication", credits: 6, courses: [ { code: "ENG091", name: "Foundation Course", credits: 0 }, @@ -80,19 +82,27 @@ export const csCurriculum = [ ], }, { - name: "Stream 2: Math & Natural Sciences", - credits: 9, + name: "Stream 2: Mathematics & Natural Sciences + School Core", + credits: 21, courses: [ - { code: "MAT092", name: "Remedial Mathematics", credits: 0 }, - { code: "MAT110", name: "Math I: Differential Calculus & Coordinate Geometry", credits: 3 }, - { code: "PHY111", name: "Principles of Physics I", credits: 3 }, - { code: "STA201", name: "Statistics & Probability", credits: 3 }, + { code: "MAT092", name: "Remedial Mathematics", credits: 0, optional: false }, + { code: "MAT110", name: "Math I: Differential Calculus & Coordinate Geometry", credits: 3, optional: false }, + { + code: "MAT120", + name: "Math II: Integral Calculus and Differential Equations", + credits: 3, + optional: false, + }, { - code: "SCI_OPTION", - name: "Optional: CHE101 / BIO101 / ENV103", - credits: 0, - optional: true, + code: "MAT215", + name: "Math III: Complex Variables and Laplace Transformations", + credits: 3, + optional: false, }, + { code: "MAT216", name: "Math IV: Linear Algebra and Fourier Analysis", credits: 3, optional: false }, + { code: "PHY111", name: "Principles of Physics I", credits: 3, optional: false }, + { code: "PHY112", name: "Principles of Physics II", credits: 3, optional: false }, + { code: "STA201", name: "Statistics and Probability", credits: 3, optional: false }, ], }, { @@ -120,6 +130,7 @@ export const csCurriculum = [ ], }, ], + note: "One Course From: HUM101, HUM102, HST102, HST103, HST104, HUM207, ENG110, ENG113, ENG114, ENG115, ENG333", }, { name: "Stream 4: Social Sciences", @@ -160,9 +171,10 @@ export const csCurriculum = [ ], }, ], + note: "One Course From: PSY101, SOC101, ANT101, POL101, BUS201, ECO101, ECO102, ECO105, BUS102, POL102, POL103, POL201, POL202, PSY102, DEV104, DEV201, SOC201/ANT202, ANT342, ANT351, BUS333, BUS334, BUS335", }, { - name: "Stream 5: Community Transformation", + name: "Stream 5: Community Service & Transformation", credits: 3, courses: [ { @@ -184,24 +196,16 @@ export const csCurriculum = [ ], }, ], + note: "One Course From: CST201, CST301, CST302, CST303, CST304, CST305, CST306, CST307, CST308, CST309, CST310", }, { - name: "GenEd Electives", + name: "Courses Out Of Department (COD)", credits: 6, courses: [ { code: "COD - 4", name: "GenEd Elective", credits: 3, optional: true }, { code: "COD - 5", name: "GenEd Elective", credits: 3, optional: true }, ], - }, - { - name: "School Core", - credits: 12, - courses: [ - { code: "MAT120", name: "Math II: Integral Calculus & Differential Equations", credits: 3 }, - { code: "MAT215", name: "Math III: Complex Variables & Laplace Transform", credits: 3 }, - { code: "MAT216", name: "Math IV: Linear Algebra & Fourier Analysis", credits: 3 }, - { code: "PHY112", name: "Principles of Physics II", credits: 3 }, - ], + note: "Two Courses From: HUM101, HUM102, HST102, HST103, HST104, HUM207, ENG110, ENG113, ENG114, ENG115, ENG333, PSY101, SOC101, ANT101, POL101, BUS201, ECO101, ECO102, ECO105, BUS102, POL102, POL103, POL201, POL202, PSY102, DEV104, DEV201, SOC201/ANT202, ANT342, ANT351, BUS333, BUS334, BUS335, CST201, CST301, CST302, CST303, CST304, CST305, CST306, CST307, CST308, CST309, CST310, CHE101, BIO101, ENV103", }, ], }, @@ -209,7 +213,7 @@ export const csCurriculum = [ { section: "Program Core", credits: 48, - description: "Core CSE courses", + description: "Core Computer Science and Engineering courses", courses: [ { code: "CSE110", name: "Programming Language I", credits: 3 }, { code: "CSE111", name: "Programming Language II", credits: 3 }, @@ -234,17 +238,17 @@ export const csCurriculum = [ section: "Program Electives", credits: 21, description: - "Elective courses (minimum one must be a CSE elective) || Research the Elective Pre-Requisites Carefully On the Google Sheet", + "Elective courses (minimum one must be a CSE elective) || Research the Elective Pre-Requisites Carefully From the Reference Link", referenceLink: "https://docs.google.com/spreadsheets/d/1-JM6a-JM4y4TiqMv9M4OXMBTnIAbn0lhsvfVGlfR1OU/edit?gid=1207964579#gid=1207964579", courses: [ { code: "CS Elective - 1", name: "CSE Elective", credits: 3, elective: true }, - { code: "CS Elective - 2", name: "CSE / Minor / GenEd Elective", credits: 3, elective: true }, - { code: "CS Elective - 3", name: "CSE / Minor / GenEd Elective", credits: 3, elective: true }, - { code: "CS Elective - 4", name: "CSE / Minor / GenEd Elective", credits: 3, elective: true }, - { code: "CS Elective - 5", name: "CSE / Minor / GenEd Elective", credits: 3, elective: true }, - { code: "CS Elective - 6", name: "CSE / Minor / GenEd Elective", credits: 3, elective: true }, - { code: "CS Elective - 7", name: "CSE / Minor / GenEd Elective", credits: 3, elective: true }, + { code: "Open Elective - 2", name: "CSE / Minor / GenEd Elective", credits: 3, elective: true }, + { code: "Open Elective - 3", name: "CSE / Minor / GenEd Elective", credits: 3, elective: true }, + { code: "Open Elective - 4", name: "CSE / Minor / GenEd Elective", credits: 3, elective: true }, + { code: "Open Elective - 5", name: "CSE / Minor / GenEd Elective", credits: 3, elective: true }, + { code: "Open Elective - 6", name: "CSE / Minor / GenEd Elective", credits: 3, elective: true }, + { code: "Open Elective - 7", name: "CSE / Minor / GenEd Elective", credits: 3, elective: true }, ], }, diff --git a/src/constants/cseCurriculum.js b/src/constants/cseCurriculum.js index 93df079..23c1109 100644 --- a/src/constants/cseCurriculum.js +++ b/src/constants/cseCurriculum.js @@ -66,8 +66,8 @@ prerequisiteOverrides (Object) */ // ! MARK: JSON Starts Here // ! Calculate total credits from curriculum -export function getTotalCredits() { - return cseCurriculum.reduce((sum, section) => sum + (section.credits || 0), 0); +export function getTotalCredits(curriculum = cseCurriculum) { + return curriculum.reduce((sum, section) => sum + (section.credits || 0), 0); } export const cseCurriculum = [ @@ -147,7 +147,7 @@ export const cseCurriculum = [ name: "Stream 4: Social Sciences", credits: 6, courses: [ - { code: "EMB101", name: "Bangladesh Studies", credits: 3, optional: false, alternatives: ["DEV101"] }, + { code: "EMB101", name: "Emergence of Bangladesh / Bangladesh Studies", credits: 3, optional: false, alternatives: ["DEV101"] }, { code: "Stream 4 - COD", name: "Pick one course from the provided options above", @@ -208,7 +208,7 @@ export const cseCurriculum = [ note: "One Course From: CST201, CST301, CST302, CST303, CST304, CST305, CST306, CST307, CST308, CST309, CST310", }, { - name: "Stream 6: Courses Out Of Department (COD)", + name: "Courses Out Of Department (COD)", credits: 6, courses: [ { code: "COD - 4", name: "Pick one course from the provided options above", credits: 3, optional: true }, @@ -253,12 +253,12 @@ export const cseCurriculum = [ { section: "Program Electives", credits: 6, - description: "Choose elective courses to specialize", + description: "Choose elective courses to specialize || Research the Elective Pre-Requisites Carefully From the Reference Link", referenceLink: "https://docs.google.com/spreadsheets/d/1-JM6a-JM4y4TiqMv9M4OXMBTnIAbn0lhsvfVGlfR1OU/edit?gid=1207964579#gid=1207964579", courses: [ - { code: "CSEElective", name: "CSE Elective (Any 300/400 level CSE course)", credits: 3, elective: true }, - { code: "OpenElective", name: "Open Elective (CSE / Minor / GenEd)", credits: 3, elective: true }, + { code: "CSE Elective", name: "CSE Elective", credits: 3, elective: true }, + { code: "Open Elective", name: "CSE / Minor / GenEd", credits: 3, elective: true }, ], }, { @@ -267,7 +267,7 @@ export const cseCurriculum = [ description: "Thesis / Project / Internship", referenceLink: "https://docs.google.com/document/d/1pAMjuQAxSEcLgkbvmx9qJGvK2BlPQhvXhgev8OeJTas/edit?tab=t.0#heading=h.aczyuw2yex2w", - courses: [{ code: "CSE400", name: "Project / Thesis / Internship", credits: 4 }], + courses: [{ code: "CSE400", name: "Project / Thesis / Internship", credits: 4 }], }, ]; From 394e17ae2797d06e72a1b775a9212e0d50a5172c Mon Sep 17 00:00:00 2001 From: Al- Saihan Date: Tue, 21 Apr 2026 21:37:51 +0600 Subject: [PATCH 11/20] feat: simplify course untoggle logic to mark courses incomplete without affecting prerequisites --- src/app/(main)/course-progression/page.jsx | 21 ++------------------- 1 file changed, 2 insertions(+), 19 deletions(-) diff --git a/src/app/(main)/course-progression/page.jsx b/src/app/(main)/course-progression/page.jsx index f722fe1..caf47fa 100644 --- a/src/app/(main)/course-progression/page.jsx +++ b/src/app/(main)/course-progression/page.jsx @@ -474,32 +474,15 @@ export default function CourseProgressionPage() { }); }; - // Mark as incomplete and remove its prerequisite chain. + // Mark as incomplete without affecting other completed courses. const handleCourseUntoggle = (courseCode) => { if (!selectedDept || !supportedDepartments.has(selectedDept)) return; setCompletedCoursesByDept((prev) => { const currentCompleted = prev[selectedDept] ?? []; - const toRemove = new Set(); - const stack = [courseCode]; - - while (stack.length > 0) { - const code = stack.pop(); - if (!code || toRemove.has(code)) continue; - - toRemove.add(code); - - const prereqCodes = getAllPrerequisiteCodes(courseMap[code]?.prereqTree); - for (const prereqCode of prereqCodes) { - if (!toRemove.has(prereqCode)) { - stack.push(prereqCode); - } - } - } - return { ...prev, - [selectedDept]: currentCompleted.filter((c) => !toRemove.has(c)), + [selectedDept]: currentCompleted.filter((c) => c !== courseCode), }; }); }; From 88c8eb39c1b2727ee4d2d0239d3cf3955cc2e0db Mon Sep 17 00:00:00 2001 From: Al- Saihan Date: Tue, 21 Apr 2026 21:49:42 +0600 Subject: [PATCH 12/20] feat: implement undo functionality for course completion and enhance course untoggle logic --- src/app/(main)/course-progression/page.jsx | 173 +++++++++++++++++---- 1 file changed, 143 insertions(+), 30 deletions(-) diff --git a/src/app/(main)/course-progression/page.jsx b/src/app/(main)/course-progression/page.jsx index caf47fa..05e7d5e 100644 --- a/src/app/(main)/course-progression/page.jsx +++ b/src/app/(main)/course-progression/page.jsx @@ -12,7 +12,7 @@ import { getTotalCredits as getCsTotalCredits, prerequisiteOverrides as csPrerequisiteOverrides, } from "@/constants/csCurriculum"; -import { CheckCircle2, ChevronDown, ChevronUp, Lock, Unlock, RefreshCw } from "lucide-react"; +import { CheckCircle2, ChevronDown, ChevronUp, Lock, Unlock, RefreshCw, Undo2 } from "lucide-react"; const departments = [ { code: "CSE", name: "Computer Science & Engineering" }, @@ -65,6 +65,27 @@ function getCourseCardId(courseCode) { return `course-card-${courseCode.toLowerCase().replace(/[^a-z0-9]+/g, "-")}`; } +function areCourseListsEqual(left, right) { + if (left.length !== right.length) return false; + return left.every((courseCode, index) => courseCode === right[index]); +} + +function addUndoSnapshot(setUndoStackByDept, dept, snapshot) { + setUndoStackByDept((prev) => { + const history = prev[dept] ?? []; + const lastSnapshot = history[history.length - 1]; + + if (lastSnapshot && areCourseListsEqual(lastSnapshot, snapshot)) { + return prev; + } + + return { + ...prev, + [dept]: [...history, snapshot].slice(-10), + }; + }); +} + //! MARK: Prereq Parsing function parsePrereqString(prereqStr) { if (!prereqStr || typeof prereqStr !== "string" || !prereqStr.trim()) return null; @@ -131,6 +152,30 @@ function getAllPrerequisiteCodes(prereqTree) { return []; } +function getDependentCompletedCourses(courseCode, completedCourses, courseMap) { + const toRemove = new Set([courseCode]); + const completedSet = new Set(completedCourses); + let changed = true; + + while (changed) { + changed = false; + + for (const completedCourse of completedCourses) { + if (toRemove.has(completedCourse)) continue; + + const prereqCodes = getAllPrerequisiteCodes(courseMap[completedCourse]?.prereqTree); + const dependsOnRemovedCourse = prereqCodes.some((prereqCode) => toRemove.has(prereqCode)); + + if (dependsOnRemovedCourse && completedSet.has(completedCourse)) { + toRemove.add(completedCourse); + changed = true; + } + } + } + + return toRemove; +} + //! MARK: Prereq Satisfaction function arePrerequisitesSatisfied(prereqTree, completedCourses) { if (!prereqTree) return true; @@ -424,12 +469,14 @@ export default function CourseProgressionPage() { const [selectedDept, setSelectedDept] = useState(null); const [showComingSoon, setShowComingSoon] = useState(false); const [completedCoursesByDept, setCompletedCoursesByDept] = useState({}); + const [undoStackByDept, setUndoStackByDept] = useState({}); const [highlightedCourseCode, setHighlightedCourseCode] = useState(null); const [showProgressPanel, setShowProgressPanel] = useState(false); const activeCurriculum = getCurriculumForDepartment(selectedDept); const activePrerequisiteOverrides = getPrerequisiteOverridesForDepartment(selectedDept); const completedCourses = selectedDept ? (completedCoursesByDept[selectedDept] ?? []) : []; + const undoStack = selectedDept ? (undoStackByDept[selectedDept] ?? []) : []; // Use the selected department's prerequisite graph to auto-mark prerequisite chains when selecting a course. const { courseMap, loading, error } = useConnectCDN(activePrerequisiteOverrides); @@ -448,43 +495,74 @@ export default function CourseProgressionPage() { const handleCourseToggle = (courseCode) => { if (!selectedDept || !supportedDepartments.has(selectedDept)) return; - setCompletedCoursesByDept((prev) => { - const currentCompleted = prev[selectedDept] ?? []; - const next = new Set(currentCompleted); - const stack = [courseCode]; + const currentCompleted = completedCoursesByDept[selectedDept] ?? []; + const next = new Set(currentCompleted); + const stack = [courseCode]; - while (stack.length > 0) { - const code = stack.pop(); - if (!code || next.has(code)) continue; + while (stack.length > 0) { + const code = stack.pop(); + if (!code || next.has(code)) continue; - next.add(code); + next.add(code); - const prereqCodes = getAllPrerequisiteCodes(courseMap[code]?.prereqTree); - for (const prereqCode of prereqCodes) { - if (!next.has(prereqCode)) { - stack.push(prereqCode); - } + const prereqCodes = getAllPrerequisiteCodes(courseMap[code]?.prereqTree); + for (const prereqCode of prereqCodes) { + if (!next.has(prereqCode)) { + stack.push(prereqCode); } } + } - return { - ...prev, - [selectedDept]: Array.from(next), - }; - }); + const nextCompleted = Array.from(next); + + if (areCourseListsEqual(currentCompleted, nextCompleted)) { + return; + } + + addUndoSnapshot(setUndoStackByDept, selectedDept, currentCompleted); + setCompletedCoursesByDept((prev) => ({ + ...prev, + [selectedDept]: nextCompleted, + })); }; - // Mark as incomplete without affecting other completed courses. + // Mark as incomplete and remove any completed courses that depend on it. const handleCourseUntoggle = (courseCode) => { if (!selectedDept || !supportedDepartments.has(selectedDept)) return; - setCompletedCoursesByDept((prev) => { - const currentCompleted = prev[selectedDept] ?? []; - return { - ...prev, - [selectedDept]: currentCompleted.filter((c) => c !== courseCode), - }; - }); + const currentCompleted = completedCoursesByDept[selectedDept] ?? []; + + if (!currentCompleted.includes(courseCode)) { + return; + } + + const toRemove = getDependentCompletedCourses(courseCode, currentCompleted, courseMap); + const nextCompleted = currentCompleted.filter((c) => !toRemove.has(c)); + + addUndoSnapshot(setUndoStackByDept, selectedDept, currentCompleted); + setCompletedCoursesByDept((prev) => ({ + ...prev, + [selectedDept]: nextCompleted, + })); + }; + + const handleUndo = () => { + if (!selectedDept || !supportedDepartments.has(selectedDept)) return; + + const history = undoStackByDept[selectedDept] ?? []; + if (history.length === 0) return; + + const previousCompleted = history[history.length - 1]; + + setCompletedCoursesByDept((prev) => ({ + ...prev, + [selectedDept]: previousCompleted, + })); + + setUndoStackByDept((prev) => ({ + ...prev, + [selectedDept]: history.slice(0, -1), + })); }; // Reset all progress @@ -492,6 +570,11 @@ export default function CourseProgressionPage() { if (!selectedDept || !supportedDepartments.has(selectedDept)) return; if (window.confirm("Are you sure you want to reset all your progress? This cannot be undone.")) { + setUndoStackByDept((prev) => ({ + ...prev, + [selectedDept]: [...(prev[selectedDept] ?? []), completedCourses].slice(-10), + })); + setCompletedCoursesByDept((prev) => ({ ...prev, [selectedDept]: [], @@ -691,6 +774,19 @@ export default function CourseProgressionPage() { {!showProgressPanel ? (
+ {supportedDepartments.has(selectedDept) && ( + + )} + + )} +
+ +
+ {completedCourses.length > 0 && supportedDepartments.has(selectedDept) && ( +
From 943118d141d320a0c2dc703bbbd621f04b7c76a2 Mon Sep 17 00:00:00 2001 From: Al- Saihan Date: Tue, 21 Apr 2026 21:52:09 +0600 Subject: [PATCH 13/20] feat: enhance undo button display to show count of actions available --- src/app/(main)/course-progression/page.jsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/app/(main)/course-progression/page.jsx b/src/app/(main)/course-progression/page.jsx index 05e7d5e..d8479ed 100644 --- a/src/app/(main)/course-progression/page.jsx +++ b/src/app/(main)/course-progression/page.jsx @@ -783,7 +783,7 @@ export default function CourseProgressionPage() { title={undoStack.length === 0 ? "Nothing to undo" : "Undo last change"} > - Undo + Undo({undoStack.length}) )} @@ -798,7 +798,7 @@ export default function CourseProgressionPage() { {selectedDept} credits {totalCompletedCredits} / {totalCredits} - {completedCourses.length > 0 && supportedDepartments.has(selectedDept) && ( + {supportedDepartments.has(selectedDept) && ( )}
- {completedCourses.length > 0 && supportedDepartments.has(selectedDept) && ( + {supportedDepartments.has(selectedDept) && ( + )} + - )} - + +
diff --git a/src/constants/cseCurriculum.js b/src/constants/cseCurriculum.js index 23c1109..b4d4f27 100644 --- a/src/constants/cseCurriculum.js +++ b/src/constants/cseCurriculum.js @@ -147,7 +147,13 @@ export const cseCurriculum = [ name: "Stream 4: Social Sciences", credits: 6, courses: [ - { code: "EMB101", name: "Emergence of Bangladesh / Bangladesh Studies", credits: 3, optional: false, alternatives: ["DEV101"] }, + { + code: "EMB101", + name: "Emergence of Bangladesh / Bangladesh Studies", + credits: 3, + optional: false, + alternatives: ["DEV101"], + }, { code: "Stream 4 - COD", name: "Pick one course from the provided options above", @@ -253,7 +259,8 @@ export const cseCurriculum = [ { section: "Program Electives", credits: 6, - description: "Choose elective courses to specialize || Research the Elective Pre-Requisites Carefully From the Reference Link", + description: + "Choose elective courses to specialize || Research the Elective Pre-Requisites Carefully From the Reference Link", referenceLink: "https://docs.google.com/spreadsheets/d/1-JM6a-JM4y4TiqMv9M4OXMBTnIAbn0lhsvfVGlfR1OU/edit?gid=1207964579#gid=1207964579", courses: [ From 2318999559b9d32cab800ee017c21830f9a757ec Mon Sep 17 00:00:00 2001 From: Al- Saihan Date: Tue, 21 Apr 2026 22:00:30 +0600 Subject: [PATCH 15/20] feat: add flashing animation to course cards for better visibility --- src/app/(main)/course-progression/page.jsx | 11 ++++++---- src/app/globals.css | 24 ++++++++++++++++++++++ 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/src/app/(main)/course-progression/page.jsx b/src/app/(main)/course-progression/page.jsx index adbb76f..7153cea 100644 --- a/src/app/(main)/course-progression/page.jsx +++ b/src/app/(main)/course-progression/page.jsx @@ -265,17 +265,17 @@ function CourseCard({ course, isCompleted, isAvailable, isSelected, isHighlighte statusText = "Locked - Prerequisites needed"; } - const highlightClass = isHighlighted - ? "ring-2 ring-cyan-500 dark:ring-cyan-400 ring-offset-2 ring-offset-white dark:ring-offset-gray-900 shadow-lg shadow-cyan-500/30" - : ""; + const highlightClass = isHighlighted ? "course-card-flash" : ""; return (
onClick(course.code)} onMouseEnter={() => setShowTooltip(true)} onMouseLeave={() => setShowTooltip(false)} + title="Click to complete or undo. Prerequisite tags jump to courses." >
@@ -315,6 +315,9 @@ function CourseCard({ course, isCompleted, isAvailable, isSelected, isHighlighte {courseDetails?.allPrereqs?.length > 0 && !isCompleted && !isAvailable && (
Requires: {courseDetails.allPrereqs.join(", ")}
)} + {courseDetails?.allPrereqs?.length > 0 && ( +
Click prerequisite tags to jump
+ )} {isCompleted && courseDetails?.allPrereqs?.length > 0 && (
Note: Uncompleting will lock dependent courses
)} @@ -902,7 +905,7 @@ export default function CourseProgressionPage() {

- Click any course code to navigate. + Click any course code to jump to that course.

diff --git a/src/app/globals.css b/src/app/globals.css index 563ef1d..ac1c792 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -167,3 +167,27 @@ .dark .faculty-dropdown-scroll::-webkit-scrollbar-thumb:hover { background: #2563eb; } + +@keyframes course-card-flash { + 0% { + box-shadow: 0 0 0 0 rgba(255, 255, 255, 0), 0 0 0 0 rgba(255, 255, 255, 0); + filter: brightness(1); + transform: scale(1); + } + + 20% { + box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.95), 0 0 30px 8px rgba(255, 255, 255, 0.75); + filter: brightness(1.08); + transform: scale(1.01); + } + + 100% { + box-shadow: 0 0 0 0 rgba(255, 255, 255, 0), 0 0 0 0 rgba(255, 255, 255, 0); + filter: brightness(1); + transform: scale(1); + } +} + +.course-card-flash { + animation: course-card-flash 900ms ease-out; +} From 43bb835d69e6a8d05ead61b14c7ed4d95b6bd03f Mon Sep 17 00:00:00 2001 From: Al- Saihan Date: Tue, 21 Apr 2026 22:00:30 +0600 Subject: [PATCH 16/20] feat: add flashing animation to course cards for better visibility --- src/app/(main)/course-progression/page.jsx | 11 ++++++---- src/app/globals.css | 24 ++++++++++++++++++++++ 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/src/app/(main)/course-progression/page.jsx b/src/app/(main)/course-progression/page.jsx index adbb76f..7153cea 100644 --- a/src/app/(main)/course-progression/page.jsx +++ b/src/app/(main)/course-progression/page.jsx @@ -265,17 +265,17 @@ function CourseCard({ course, isCompleted, isAvailable, isSelected, isHighlighte statusText = "Locked - Prerequisites needed"; } - const highlightClass = isHighlighted - ? "ring-2 ring-cyan-500 dark:ring-cyan-400 ring-offset-2 ring-offset-white dark:ring-offset-gray-900 shadow-lg shadow-cyan-500/30" - : ""; + const highlightClass = isHighlighted ? "course-card-flash" : ""; return (
onClick(course.code)} onMouseEnter={() => setShowTooltip(true)} onMouseLeave={() => setShowTooltip(false)} + title="Click to complete or undo. Prerequisite tags jump to courses." >
@@ -315,6 +315,9 @@ function CourseCard({ course, isCompleted, isAvailable, isSelected, isHighlighte {courseDetails?.allPrereqs?.length > 0 && !isCompleted && !isAvailable && (
Requires: {courseDetails.allPrereqs.join(", ")}
)} + {courseDetails?.allPrereqs?.length > 0 && ( +
Click prerequisite tags to jump
+ )} {isCompleted && courseDetails?.allPrereqs?.length > 0 && (
Note: Uncompleting will lock dependent courses
)} @@ -902,7 +905,7 @@ export default function CourseProgressionPage() {

- Click any course code to navigate. + Click any course code to jump to that course.

diff --git a/src/app/globals.css b/src/app/globals.css index 563ef1d..3edba6e 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -167,3 +167,27 @@ .dark .faculty-dropdown-scroll::-webkit-scrollbar-thumb:hover { background: #2563eb; } + +@keyframes course-card-flash { + 0% { + box-shadow: 0 0 0 0 rgba(255, 255, 255, 0), 0 0 0 0 rgba(255, 255, 255, 0); + filter: brightness(1); + transform: scale(1); + } + + 50% { + box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.95), 0 0 30px 8px rgba(255, 255, 255, 0.75); + filter: brightness(1.08); + transform: scale(1.01); + } + + 100% { + box-shadow: 0 0 0 0 rgba(255, 255, 255, 0), 0 0 0 0 rgba(255, 255, 255, 0); + filter: brightness(1); + transform: scale(1); + } +} + +.course-card-flash { + animation: course-card-flash 2000ms ease-out; +} From f0b547abcef4529a0f4616d755b3037190f13548 Mon Sep 17 00:00:00 2001 From: Al- Saihan Date: Tue, 21 Apr 2026 22:21:01 +0600 Subject: [PATCH 17/20] feat: add instructional details section to Course Progression Page --- src/app/(main)/course-progression/page.jsx | 42 +++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/src/app/(main)/course-progression/page.jsx b/src/app/(main)/course-progression/page.jsx index 7153cea..8aa3e54 100644 --- a/src/app/(main)/course-progression/page.jsx +++ b/src/app/(main)/course-progression/page.jsx @@ -682,7 +682,6 @@ export default function CourseProgressionPage() { dependent courses will become{" "} Locked again automatically.

-
@@ -697,6 +696,47 @@ export default function CourseProgressionPage() { Locked
+
+ + How it works + + + + + +
+
Select a department to begin
+ +
+ +
+ Click courses to complete or undo them +
+ +
+ +
+ Undo reverses up to 10 recent steps +
+ +
+ +
+ Click Mini CourseCode Chups to jump quickly to the specific courses +
+
+
{/* Department Selection */} From d4da18b8a637f07d2156d1ed87b9beb4f8c5f046 Mon Sep 17 00:00:00 2001 From: Al- Saihan Date: Tue, 21 Apr 2026 22:23:42 +0600 Subject: [PATCH 18/20] feat: add Course Progression item to sidebar for tracking course completion --- src/constants/sideBarItems.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/constants/sideBarItems.js b/src/constants/sideBarItems.js index 7d7328a..14d7037 100644 --- a/src/constants/sideBarItems.js +++ b/src/constants/sideBarItems.js @@ -1,4 +1,4 @@ -import { Home, Sigma, Star, ChevronsLeftRightEllipsis, Hammer, Cable, BookOpen, Users, ArrowRightLeft, FileText } from "lucide-react" +import { Home, Sigma, Star, ChevronsLeftRightEllipsis, Hammer, Cable, BookOpen, Users, ArrowRightLeft, FileText, CheckCircle2 } from "lucide-react" const sidebarGroups = [ { @@ -43,6 +43,13 @@ const sidebarGroups = [ description: 'Merge multiple course routines into one optimized schedule. Perfect for group studies and project collaborations!', icon: Cable, enabled: true + }, + { + title: "Course Progression", + href: '/course-progression', + description: 'Track your course completion progress and visualize prerequisites.', + icon: CheckCircle2, + enabled: true } ] }, From 556ba35b8b03012ed1053556e8b42982e83458ed Mon Sep 17 00:00:00 2001 From: Al- Saihan Date: Thu, 23 Apr 2026 04:15:27 +0600 Subject: [PATCH 19/20] dont ask --- src/app/(main)/course-progression/page.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/(main)/course-progression/page.jsx b/src/app/(main)/course-progression/page.jsx index 8aa3e54..356c28d 100644 --- a/src/app/(main)/course-progression/page.jsx +++ b/src/app/(main)/course-progression/page.jsx @@ -733,7 +733,7 @@ export default function CourseProgressionPage() {
- Click Mini CourseCode Chups to jump quickly to the specific courses + Click Mini CourseCode Chips to jump quickly to the specific courses
From be0c0faa912f256739c314c71b7f0e2711fe06e8 Mon Sep 17 00:00:00 2001 From: Al- Saihan Date: Thu, 4 Jun 2026 21:36:51 +0600 Subject: [PATCH 20/20] I forgot what I did so take so ignore this commit? Maybe? I just wanna change branch... --- src/app/(main)/course-progression/page.jsx | 274 ++++++++++++-- src/constants/cseCurriculum.js | 2 +- src/constants/microbioCurriculum.js | 415 +++++++++++++++++++++ 3 files changed, 664 insertions(+), 27 deletions(-) create mode 100644 src/constants/microbioCurriculum.js diff --git a/src/app/(main)/course-progression/page.jsx b/src/app/(main)/course-progression/page.jsx index 356c28d..114cead 100644 --- a/src/app/(main)/course-progression/page.jsx +++ b/src/app/(main)/course-progression/page.jsx @@ -12,27 +12,37 @@ import { getTotalCredits as getCsTotalCredits, prerequisiteOverrides as csPrerequisiteOverrides, } from "@/constants/csCurriculum"; -import { CheckCircle2, ChevronDown, ChevronUp, Lock, Unlock, RefreshCw, Undo2 } from "lucide-react"; +import { + microbiologyCurriculum, + getTotalCredits as getMicrobioTotalCredits, + prerequisiteOverrides as microbioPrerequisiteOverrides, +} from "@/constants/microbioCurriculum"; +import { CheckCircle2, ChevronDown, ChevronUp, Lock, Search, Unlock, RefreshCw, Undo2 } from "lucide-react"; -const departments = [ +const allDepartments = [ { code: "CSE", name: "Computer Science & Engineering" }, { code: "CS", name: "Computer Science" }, + { code: "MIC", name: "Microbiology" }, { code: "ARCH", name: "Architecture" }, { code: "BBA", name: "Business Administration" }, { code: "LAW", name: "Law" }, ]; +const departments = allDepartments.filter((department) => !["ARCH", "BBA", "LAW"].includes(department.code)); + const departmentCurricula = { CSE: cseCurriculum, CS: csCurriculum, + MIC: microbiologyCurriculum, }; const departmentPrerequisiteOverrides = { CSE: csePrerequisiteOverrides, CS: csPrerequisiteOverrides, + MIC: microbioPrerequisiteOverrides, }; -const supportedDepartments = new Set(["CSE", "CS"]); +const supportedDepartments = new Set(["CSE", "CS", "MIC"]); function getCurriculumForDepartment(code) { return departmentCurricula[code] ?? []; @@ -42,6 +52,12 @@ function getPrerequisiteOverridesForDepartment(code) { return departmentPrerequisiteOverrides[code] ?? csePrerequisiteOverrides; } +function getTotalCreditsForDepartment(code, curriculum) { + if (code === "CS") return getCsTotalCredits(curriculum); + if (code === "MIC") return getMicrobioTotalCredits(curriculum); + return getCseTotalCredits(curriculum); +} + function getCompletedCoursesForCurriculum(curriculum, completedCourses) { return completedCourses.reduce((total, courseCode) => { for (const section of curriculum) { @@ -65,6 +81,50 @@ function getCourseCardId(courseCode) { return `course-card-${courseCode.toLowerCase().replace(/[^a-z0-9]+/g, "-")}`; } +function normalizeSearchText(value) { + return value?.toString().trim().toLowerCase() ?? ""; +} + +function getSearchTokens(query) { + return normalizeSearchText(query) + .split(/\s+/) + .filter(Boolean); +} + +function findCourseMatches(courses, query) { + const searchTokens = getSearchTokens(query); + + if (searchTokens.length === 0) return []; + + return courses + .map((course) => { + const code = normalizeSearchText(course.code); + const name = normalizeSearchText(course.name); + const sectionName = normalizeSearchText(course.sectionName); + const streamName = normalizeSearchText(course.streamName); + const alternatives = Array.isArray(course.alternatives) ? course.alternatives : []; + + const courseText = [code, name, sectionName, streamName].filter(Boolean).join(" "); + + const matchedAlternatives = alternatives.filter((alternative) => { + const normalizedAlternative = normalizeSearchText(alternative); + return searchTokens.some((token) => normalizedAlternative.includes(token)); + }); + + const matchesQuery = + searchTokens.some((token) => courseText.includes(token)) || matchedAlternatives.length > 0; + + if (!matchesQuery) return null; + + return { + course, + matchedAlternatives, + locationLabel: course.streamName ? `${course.sectionName} · ${course.streamName}` : course.sectionName, + }; + }) + .filter(Boolean); +} + //! MARK: Helpers function areCourseListsEqual(left, right) { if (left.length !== right.length) return false; @@ -244,8 +304,20 @@ function useConnectCDN(prereqOverrides = csePrerequisiteOverrides) { } //! MARK: Course Card -function CourseCard({ course, isCompleted, isAvailable, isSelected, isHighlighted, onClick, onPrereqClick, courseDetails }) { +function CourseCard({ + course, + isCompleted, + isAvailable, + isSelected, + isHighlighted, + onClick, + onPrereqClick, + courseDetails, + searchQuery, +}) { const [showTooltip, setShowTooltip] = useState(false); + const normalizedSearchQuery = normalizeSearchText(searchQuery); + const hasAlternatives = Array.isArray(course.alternatives) && course.alternatives.length > 0; let statusColor = "border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800"; let statusIcon = null; @@ -265,7 +337,7 @@ function CourseCard({ course, isCompleted, isAvailable, isSelected, isHighlighte statusText = "Locked - Prerequisites needed"; } - const highlightClass = isHighlighted ? "course-card-flash" : ""; + const highlightClass = isHighlighted ? "course-card-flash ring-2 ring-blue-500 ring-offset-2 ring-offset-white dark:ring-offset-gray-900 z-10" : ""; return (
)} + + {hasAlternatives && ( +
+
+ Includes +
+
+ {course.alternatives.slice(0, 4).map((alternative) => { + const isHighlightedAlternative = normalizeSearchText(alternative) === normalizedSearchQuery; + + return ( + + {alternative} + + ); + })} + {course.alternatives.length > 4 && ( + + +{course.alternatives.length - 4} more + + )} +
+
+ )}
{statusIcon}
@@ -340,6 +443,7 @@ function SectionCourses({ onCourseToggle, onCourseUntoggle, onPrereqClick, + searchQuery, }) { const [selectedCourse, setSelectedCourse] = useState(null); @@ -438,6 +542,7 @@ function SectionCourses({ onClick={handleCourseClick} onPrereqClick={onPrereqClick} courseDetails={courseMap[course.code]} + searchQuery={searchQuery} /> ); })} @@ -460,6 +565,7 @@ function SectionCourses({ onClick={handleCourseClick} onPrereqClick={onPrereqClick} courseDetails={courseMap[course.code]} + searchQuery={searchQuery} /> ); })} @@ -476,6 +582,8 @@ export default function CourseProgressionPage() { const [completedCoursesByDept, setCompletedCoursesByDept] = useState({}); const [undoStackByDept, setUndoStackByDept] = useState({}); const [highlightedCourseCode, setHighlightedCourseCode] = useState(null); + const [courseSearchDraft, setCourseSearchDraft] = useState(""); + const [courseSearchQuery, setCourseSearchQuery] = useState(""); const [showProgressPanel, setShowProgressPanel] = useState(false); const activeCurriculum = getCurriculumForDepartment(selectedDept); @@ -597,21 +705,31 @@ export default function CourseProgressionPage() { const allSectionCourses = useMemo( () => activeCurriculum.flatMap((section) => { - const courses = section.courses - ? section.courses - : section.streams - ? section.streams.flatMap((stream) => stream.courses) - : []; - - return courses.map((course) => ({ - code: course.code, - name: course.name, - sectionName: section.section, - })); + if (section.courses) { + return section.courses.map((course) => ({ + ...course, + sectionName: section.section, + streamName: null, + })); + } + + if (section.streams) { + return section.streams.flatMap((stream) => + stream.courses.map((course) => ({ + ...course, + sectionName: section.section, + streamName: stream.name, + })), + ); + } + + return []; }), [activeCurriculum], ); + const courseSearchMatches = useMemo(() => findCourseMatches(allSectionCourses, courseSearchQuery), [allSectionCourses, courseSearchQuery]); + const availableCourses = useMemo(() => { const completedSet = new Set(completedCourses); @@ -647,22 +765,57 @@ export default function CourseProgressionPage() { } }, []); + const handleCourseSearchSubmit = useCallback( + (event) => { + event.preventDefault(); + + const query = courseSearchDraft.trim(); + setCourseSearchQuery(query); + + if (!query) { + setHighlightedCourseCode(null); + return; + } + + const matches = findCourseMatches(allSectionCourses, query); + const target = matches[0]; + + if (!target) return; + + setHighlightedCourseCode(target.course.code); + + const cardEl = document.getElementById(getCourseCardId(target.course.code)); + if (cardEl) { + cardEl.scrollIntoView({ behavior: "smooth", block: "center" }); + } + }, + [allSectionCourses, courseSearchDraft], + ); + useEffect(() => { - if (!highlightedCourseCode) return; + if (!highlightedCourseCode || courseSearchQuery.trim()) return; const timer = setTimeout(() => { setHighlightedCourseCode(null); }, 2600); return () => clearTimeout(timer); - }, [highlightedCourseCode]); + }, [highlightedCourseCode, courseSearchQuery]); + + useEffect(() => { + setCourseSearchDraft(""); + setCourseSearchQuery(""); + setHighlightedCourseCode(null); + }, [selectedDept]); // Calculate total completed credits const totalCompletedCredits = getCompletedCoursesForCurriculum(activeCurriculum, completedCourses); - const totalCredits = selectedDept === "CS" ? getCsTotalCredits(activeCurriculum) : getCseTotalCredits(activeCurriculum); + const totalCredits = selectedDept ? getTotalCreditsForDepartment(selectedDept, activeCurriculum) : 0; const progressPercent = totalCredits > 0 ? Math.min(100, Math.round((totalCompletedCredits / totalCredits) * 100)) : 0; const coursesLeft = Math.max(0, allSectionCourses.length - completedCourses.length); + const canUndo = undoStack.length > 0; + const canReset = completedCourses.length > 0; //! MARK: Bottom Panel return ( @@ -773,6 +926,76 @@ export default function CourseProgressionPage() {
) : (
+
+
+
+ + setCourseSearchDraft(event.target.value)} + placeholder="Find a course, stream, or code like HUM101" + className="w-full rounded-lg border border-gray-300 bg-white py-2.5 pl-10 pr-3 text-sm text-gray-900 shadow-sm outline-none transition focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20 dark:border-gray-700 dark:bg-gray-900 dark:text-white" + /> +
+
+ + +
+
+ +
+ {courseSearchQuery.trim() ? ( + courseSearchMatches.length > 0 ? ( + <> +

+ Showing {courseSearchMatches.length} match{courseSearchMatches.length > 1 ? "es" : ""} +

+
+ {courseSearchMatches.slice(0, 6).map((match) => ( + + ))} +
+ + ) : ( +

+ No match found. Try a course code, title, or stream name. +

+ ) + ) : ( +

+ Search by code, title, or a hidden option like HUM101 to jump to the stream that contains it. +

+ )} +
+
+ {activeSections.map((section) => (
@@ -807,6 +1030,7 @@ export default function CourseProgressionPage() { onCourseToggle={handleCourseToggle} onCourseUntoggle={handleCourseUntoggle} onPrereqClick={handlePrereqJump} + searchQuery={courseSearchQuery} />
))} @@ -820,13 +1044,12 @@ export default function CourseProgressionPage() { {!showProgressPanel ? (
- {supportedDepartments.has(selectedDept) && ( + {supportedDepartments.has(selectedDept) && canUndo && ( - {supportedDepartments.has(selectedDept) && ( + {supportedDepartments.has(selectedDept) && canReset && (
- {supportedDepartments.has(selectedDept) && ( + {supportedDepartments.has(selectedDept) && canReset && (