diff --git a/.firebaserc b/.firebaserc deleted file mode 100644 index 43475cc..0000000 --- a/.firebaserc +++ /dev/null @@ -1,5 +0,0 @@ -{ - "projects": { - "default": "majoraudit" - } -} diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..879656d --- /dev/null +++ b/docs/README.md @@ -0,0 +1,132 @@ +# MajorAudit + +## Repository Layout +- `/frontend`: The current face of the site, built with React. +- `/backend`: The backend logic for the site, built with Flask. +- `/scrapers`: Chrome extensions for web scraping. +- `/docs`: Documentation. + +## Local Development Environment + +We're working fullstack. + +### Requirements +- Access to MajorAudit GitHub repository. +- npm (Node Package Manager). + +### Setup Instructions + +0. Clone the MajorAudit Repository: + ```bash + git clone + ``` + +### Base Firebase Setup +1. In the root directory, run: + ```bash + npm install -g firebase-tools + ``` + _Note: If it throws permission errors, prepend the command with `sudo`:_ + ```bash + sudo npm install -g firebase-tools + ``` + +### Backend Setup (Python Virtual Environment) +2. Update Python to version 3.12. + - You can use [Homebrew](https://brew.sh/) to install the latest version of Python: + ```bash + brew install python@3.12 + ``` + +3. Navigate to the `/backend` directory. +4. Create a virtual environment: + ```bash + python3.12 -m venv venv + ``` +5. Activate the virtual environment: + ```bash + source venv/bin/activate + ``` +6. Install the required dependencies: + ```bash + pip install -r requirements.txt + ``` +7. Deactivate the virtual environment: + ```bash + deactivate + ``` + +### Secrets Setup +8. Create a `secrets` directory in the `/backend` folder: + ```bash + mkdir secrets + ``` +9. Go to the [Firebase Console](https://console.firebase.google.com/). +10. Select the `majoraudit` project. +11. Click the gear icon next to "Project Overview" and select "Project Settings". +12. Navigate to the "Service Accounts" tab. +13. Generate a new Node.js private key. +14. Move the generated key file to your `secrets` directory. +15. Update the path to the key file in `main.py`: + ```python + cred = credentials.Certificate(r'path_to_secrets_file') + ``` + +### Running the Project +1. Install the required frontend dependencies: + ```bash + cd frontend + npm i + ``` + +2. Ensure you have Java version >= 20 installed. + +3. Log in to Firebase: + ```bash + firebase login + ``` + +4. In the `/frontend` directory, build the frontend: + ```bash + npm run build + ``` + +5. In the root or `/frontend` directory, start the Firebase emulators: + ```bash + firebase emulators:start + ``` + +6. Troubleshoot any errors as needed. + +### Notes +- **Frontend Changes**: Anytime you change the frontend code, stop the emulators, rebuild the frontend, and restart the emulators. The emulators only host the most recent build. +- **Web Scraper Changes**: If you modify the web scraper, remove and reconfigure the extension in Chrome. +- **Backend Changes**: You can modify the backend code on the fly. The emulators will automatically restart when you save changes. + +### Strategies for Development +- **Frontend-Only Development**: + 1. Change the `useState(auth)` value in `App.tsx` to `true`. + 2. Modify the `initLocalStorage()` method in `Graduation.tsx` to use `MockStudent` instead of calling the `getData()` API. + 3. Run the frontend in development mode: + ```bash + npm start + ``` + 4. The frontend will now automatically update as you make changes. + +## Contributing +1. Create a branch for your feature: + ```bash + git checkout -b / + ``` +2. Make your changes. +3. Commit and push your changes to the origin: + ```bash + git commit -m "Your commit message" + git push origin + ``` +4. Create a pull request and add reviewers. In the pull request, reference any relevant issue numbers. +5. Once the pull request is approved, merge it into the master branch. + +## Roadmap +- We use GitHub issues to track bugs and feature requests: [GitHub Issues](https://github.com/YaleComputerSociety/MajorAudit/issues). +- We use GitHub projects to manage everything and do planning: [GitHub Projects](https://github.com/orgs/YaleComputerSociety/projects/2/). \ No newline at end of file diff --git a/firebase.json b/firebase.json deleted file mode 100644 index 2817460..0000000 --- a/firebase.json +++ /dev/null @@ -1,66 +0,0 @@ -{ - "functions": [ - { - "source": "backend", - "codebase": "default", - "ignore": [ - "__pycache__", - "venv", - ".git", - "firebase-debug.log", - "firebase-debug.*.log" - ] - } - ], - "database": { - "rules": "database.rules.json" - }, - "hosting": { - "public": "frontend/build", - "ignore": [ - "firebase.json", - "**/.*", - "**/node_modules/**", - "README.md" - ], - "rewrites": [ - { - "source": "**", - "destination": "/index.html" - }], - "cleanUrls": true - }, - "storage": { - "rules": "storage.rules" - }, - "emulators": { - "auth": { - "port": 9099 - }, - "functions": { - "port": 5001 - }, - "firestore": { - "port": 8080 - }, - "database": { - "port": 9000 - }, - "hosting": { - "port": 3000 - }, - "pubsub": { - "port": 8085 - }, - "storage": { - "port": 9199 - }, - "eventarc": { - "port": 9299 - }, - "ui": { - "enabled": true - }, - "singleProjectMode": true - } -} diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 921eccc..d189040 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,11 +1,11 @@ { - "name": "new-audit", + "name": "majoraudit", "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "new-audit", + "name": "majoraudit", "version": "0.1.0", "dependencies": { "d3": "^7.9.0", diff --git a/frontend/package.json b/frontend/package.json index d1864bc..7c391cf 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,5 +1,5 @@ { - "name": "new-audit", + "name": "majoraudit", "version": "0.1.0", "private": true, "scripts": { diff --git a/cra-oldend/src/commons/images/profile.png b/frontend/public/account.png similarity index 100% rename from cra-oldend/src/commons/images/profile.png rename to frontend/public/account.png diff --git a/frontend/public/guy.jpg b/frontend/public/guy.jpg new file mode 100644 index 0000000..dabaf1d Binary files /dev/null and b/frontend/public/guy.jpg differ diff --git a/frontend/public/profile.png b/frontend/public/profile.png new file mode 100644 index 0000000..abcb870 Binary files /dev/null and b/frontend/public/profile.png differ diff --git a/frontend/public/spring.svg b/frontend/public/spring.svg new file mode 100644 index 0000000..1a67e09 --- /dev/null +++ b/frontend/public/spring.svg @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/src/api/api.ts b/frontend/src/api/api.ts new file mode 100644 index 0000000..bba7e6b --- /dev/null +++ b/frontend/src/api/api.ts @@ -0,0 +1,19 @@ + +import { Ryan } from "@/database/data-user"; +import { NextRequest, NextResponse } from "next/server"; + +export function login(req: NextRequest) { + const user = Ryan; + + const response = NextResponse.json(user, { status: 200 }); + + response.cookies.set("session", "true", { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "strict", + maxAge: 60 * 60, + path: "/", + }); + + return response; +} diff --git a/frontend/src/app/account/Account.module.css b/frontend/src/app/account/Account.module.css new file mode 100644 index 0000000..e7cf6b9 --- /dev/null +++ b/frontend/src/app/account/Account.module.css @@ -0,0 +1,35 @@ + +.Row { + display: flex; + flex-direction: row; +} + +.Column { + display: flex; + flex-direction: column; +} + +.AccountPage { + position: absolute; + top: 75px; + + display: flex; + flex-direction: column; + align-items: center; + + width: 100%; + padding-top: 50px; + padding-bottom: 200px; +} + +.AccountContent { + width: 600px; /* Adjust the width as needed */ + padding: 20px; + background-color: white; /* Optional, just to make the box visible */ + border-radius: 10px; /* Optional, for rounded corners */ + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); /* Optional, for a subtle shadow */ + + display: flex; + flex-direction: column; + align-items: flex-start; /* Ensures text and elements inside are left-aligned */ +} diff --git a/frontend/src/app/account/meta-inputs/MetaInputs.module.css b/frontend/src/app/account/meta-inputs/MetaInputs.module.css new file mode 100644 index 0000000..dc3d033 --- /dev/null +++ b/frontend/src/app/account/meta-inputs/MetaInputs.module.css @@ -0,0 +1,90 @@ + + +.Row { + display: flex; + flex-direction: row; +} + +.Column { + display: flex; + flex-direction: column; +} + +.InputContainer { + display: flex; + flex-direction: row; + align-items: flex-end; +} + +.Label { + font-weight: bold; + margin-right: 4px; + margin-bottom: 3px; +} + +.InputBox { + height: 10px; + padding: 4px; + border: 2px solid #ccc; + border-radius: 5px; + font-size: 14px; + transition: border-color 0.3s ease-in-out; +} + +.InputBox:focus { + border-color: #82beff; /* Blue border on focus */ + outline: none; +} + +.LanguageLevelBox { + height: 24px; /* Matches InputBox height */ + padding: 5px 7px; /* Matches InputBox padding */ + border: 2px solid #ccc; /* Matches InputBox border */ + border-radius: 5px; /* Matches InputBox border-radius */ + font-size: 14px; /* Matches InputBox font-size */ + background-color: white; + cursor: pointer; + display: flex; + align-items: center; + box-sizing: border-box; + color: black; + position: relative; + transition: border-color 0.3s ease-in-out; +} + +.LanguageLevel:hover { + border-color: #aaa; +} + +.LanguageLevelOptions { + position: absolute; + background-color: #fff; + border: 1px solid #ccc; + border-radius: 4px; + top: 100%; + left: 0; + margin-top: 5px; + z-index: 9999; + /* width: 90px; */ + box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.1); + max-height: 200px; + overflow-y: auto; + font-size: 12px; + box-sizing: border-box; +} + +.LanguageLevelOptions div { + padding: 8px; + cursor: pointer; + color: black; + background-color: white; +} + +.LanguageLevelOptions div:hover { + background-color: #f0f0f0; +} + +.SelectedTerm { + background-color: #93c7ff !important; /* Light blue background for the selected term */ + color: black; /* Optional: change text color for selected term */ +} \ No newline at end of file diff --git a/frontend/src/app/account/meta-inputs/MetaInputs.tsx b/frontend/src/app/account/meta-inputs/MetaInputs.tsx new file mode 100644 index 0000000..00f103a --- /dev/null +++ b/frontend/src/app/account/meta-inputs/MetaInputs.tsx @@ -0,0 +1,103 @@ + +"use client"; +import { useState, useEffect, useRef } from "react"; +import Style from "./MetaInputs.module.css"; + +const languageList: string[] = ["Spanish", "French", "German", "Chinese", "Japanese", "Italian", "Latin", "Greek"]; +const levelList: string[] = ["L1", "L2", "L3", "L4", "L5"]; + +function LanguagePlacement(){ + const [languageDropdownVisible, setLanguageDropdownVisible] = useState(false); + const [levelDropdownVisible, setLevelDropdownVisible] = useState(false); + const [selectedLanguage, setSelectedLanguage] = useState(""); + const [selectedLevel, setSelectedLevel] = useState(""); + + const languageDropdownRef = useRef(null); + const levelDropdownRef = useRef(null); + + // Click outside handler + const handleClickOutside = (event: MouseEvent) => { + if (languageDropdownRef.current && !languageDropdownRef.current.contains(event.target as Node)) { + setLanguageDropdownVisible(false); + } + if (levelDropdownRef.current && !levelDropdownRef.current.contains(event.target as Node)) { + setLevelDropdownVisible(false); + } + }; + + // Attach event listener when dropdowns are open + useEffect(() => { + if (languageDropdownVisible || levelDropdownVisible) { + document.addEventListener("mousedown", handleClickOutside); + } else { + document.removeEventListener("mousedown", handleClickOutside); + } + return () => document.removeEventListener("mousedown", handleClickOutside); + }, [languageDropdownVisible, levelDropdownVisible]); + + return ( +
+
+
Language Placement:
+
setLanguageDropdownVisible(!languageDropdownVisible)}> + {selectedLanguage} + {languageDropdownVisible && ( +
+ {languageList.map((lang, index) => ( +
{ setSelectedLanguage(lang); setLanguageDropdownVisible(false); }} className={lang === selectedLanguage ? Style.SelectedLanguageLevel : ""}> + {lang} +
+ ))} +
+ )} +
+
setLevelDropdownVisible(!levelDropdownVisible)}> + {selectedLevel} + {levelDropdownVisible && ( +
+ {levelList.map((level, index) => ( +
{ setSelectedLevel(level); setLevelDropdownVisible(false); }} className={level === selectedLevel ? Style.SelectedLanguageLevel : ""}> + {level} +
+ ))} +
+ )} +
+
+
+ ); +} + + + +function FirstShelf() { + const [formData, setFormData] = useState({ + name: "", + gradYear: "", + languagePlacement: "", + }); + + const handleChange = (e: any) => { + setFormData({ ...formData, [e.target.name]: e.target.value }); + }; + + return ( +
+
+
+ Name +
+ +
+
+
+ Year +
+ +
+ +
+ ); +} + +export default FirstShelf; \ No newline at end of file diff --git a/frontend/src/app/account/page.tsx b/frontend/src/app/account/page.tsx new file mode 100644 index 0000000..99924e5 --- /dev/null +++ b/frontend/src/app/account/page.tsx @@ -0,0 +1,26 @@ + +import Style from "./Account.module.css"; +import NavBar from "@/components/navbar/NavBar"; + +import FirstShelf from "./meta-inputs/MetaInputs"; + +function Account(){ + return( +
+ +
+
+
+ Profile +
+
+ Configure basic degree data. +
+ +
+
+
+ ) +} + +export default Account; \ No newline at end of file diff --git a/frontend/src/app/api/login/route.ts b/frontend/src/app/api/login/route.ts new file mode 100644 index 0000000..7f9e03a --- /dev/null +++ b/frontend/src/app/api/login/route.ts @@ -0,0 +1,7 @@ + +import { NextRequest } from "next/server"; +import { login } from "@/api/api"; + +export async function GET(req: NextRequest) { + return login(req); +} diff --git a/frontend/src/app/courses/Courses.module.css b/frontend/src/app/courses/Courses.module.css index bd394d4..68558cf 100644 --- a/frontend/src/app/courses/Courses.module.css +++ b/frontend/src/app/courses/Courses.module.css @@ -4,6 +4,11 @@ flex-direction: column; } +.Row { + display: flex; + flex-direction: row; +} + .CoursesPage { position: absolute; top: 75px; diff --git a/frontend/src/app/courses/CoursesUtils.tsx b/frontend/src/app/courses/CoursesUtils.tsx new file mode 100644 index 0000000..0a341f4 --- /dev/null +++ b/frontend/src/app/courses/CoursesUtils.tsx @@ -0,0 +1,27 @@ +import { User, StudentSemester, StudentYear } from "@/types/type-user"; + +export function BuildStudentYears(user: User): StudentYear[] +{ + const { studentTermArrangement, studentCourses } = user.FYP; + + const firstYearTerms = studentTermArrangement.first_year; + const sophomoreTerms = studentTermArrangement.sophomore; + const juniorTerms = studentTermArrangement.junior; + const seniorTerms = studentTermArrangement.senior; + + const buildSemesters = (terms: number[]): StudentSemester[] => { + return terms.map(term => ({ + term, + studentCourses: studentCourses.filter(course => course.term === term), + })); + }; + + const studentYears: StudentYear[] = [ + { grade: "First-Year", studentSemesters: buildSemesters(firstYearTerms) }, + { grade: "Sophomore", studentSemesters: buildSemesters(sophomoreTerms) }, + { grade: "Junior", studentSemesters: buildSemesters(juniorTerms) }, + { grade: "Senior", studentSemesters: buildSemesters(seniorTerms) }, + ]; + + return studentYears; +} diff --git a/frontend/src/app/courses/add-semester/AddSemesterButton.tsx b/frontend/src/app/courses/add-semester/AddSemesterButton.tsx deleted file mode 100644 index 322de2f..0000000 --- a/frontend/src/app/courses/add-semester/AddSemesterButton.tsx +++ /dev/null @@ -1,83 +0,0 @@ - -import { useRef, useState, useEffect } from "react"; -import Style from "./AddSemesterButton.module.css"; - -import { User, StudentSemester } from "@/types/type-user"; - -function executeAddSemester(props: { user: User; setUser: Function }, inputRef: React.RefObject, setDropVis: Function) -{ - if(inputRef.current) - { - const newTermString = inputRef.current.value.trim(); - if(!/^\d{6}$/.test(newTermString)){ - return; - } - - const newTermNumber = Number(newTermString); - const newSemester: StudentSemester = { - season: newTermNumber, - studentCourses: [], - }; - - const updatedSemesters = [...props.user.FYP.studentSemesters, newSemester]; - props.setUser({ ...props.user, FYP: { ...props.user.FYP, studentSemesters: updatedSemesters } }); - setDropVis(false); - } -} - - -function AddSemesterButton(props: { user: User; setUser: Function }) -{ - const [dropVis, setDropVis] = useState(false); - const buttonRef = useRef(null); - const inputRef = useRef(null); - - - useEffect(() => { - function handleClickOutside(event: MouseEvent) { - if (buttonRef.current && !buttonRef.current.contains(event.target as Node)) { - setDropVis(false); - } - } - - if(dropVis){ - document.addEventListener("mousedown", handleClickOutside); - } - - return () => { - document.removeEventListener("mousedown", handleClickOutside); - }; - }, [dropVis]); - - const handleKeyPress = (event: React.KeyboardEvent) => { - if(event.key === "Enter"){ - executeAddSemester(props, inputRef, setDropVis); - } - }; - - return ( -
- {!dropVis ? ( - - ) : ( -
-
-
setDropVis(false)}> - -
- - - -
executeAddSemester(props, inputRef, setDropVis)}> - -
-
-
- )} -
- ); -} - -export default AddSemesterButton; diff --git a/frontend/src/app/courses/page.tsx b/frontend/src/app/courses/page.tsx index ef80a8a..c237f74 100644 --- a/frontend/src/app/courses/page.tsx +++ b/frontend/src/app/courses/page.tsx @@ -4,39 +4,43 @@ import React, { useState, useEffect } from "react"; import Style from "./Courses.module.css"; import { useAuth } from "../providers"; -import { StudentSemester } from "@/types/type-user"; +import { StudentYear } from "@/types/type-user"; +import { BuildStudentYears } from "./CoursesUtils"; import NavBar from "@/components/navbar/NavBar"; -import SemesterBox from "./semester/SemesterBox"; -import AddSemesterButton from "./add-semester/AddSemesterButton"; +import YearBox from "./years/YearBox"; -function Courses() -{ +function Courses(){ const { user, setUser } = useAuth(); - const [renderedSemesters, setRenderedSemesters] = useState([]); - + const [edit, setEdit] = useState(false); - const toggleEdit = () => { - setEdit(!edit); - }; + const toggleEdit = () => { setEdit(!edit); }; + + const [columns, setColumns] = useState(true); + const toggleColumns = () => { setColumns(!columns); } + + const [studentYears, setStudentYears] = useState(() => BuildStudentYears(user)); + const [renderedYears, setRenderedYears] = useState([]); useEffect(() => { - const newRenderedSemesters = user.FYP.studentSemesters.map((semester: StudentSemester, index: number) => ( - + setStudentYears(BuildStudentYears(user)); + }, [user]); + + useEffect(() => { + const newRenderedYears = studentYears.map((studentYear: StudentYear, index: number) => ( + )); - setRenderedSemesters(newRenderedSemesters); - }, [edit, user, setUser]); + setRenderedYears(newRenderedYears); + }, [edit, columns, studentYears, user]); return(
- + +
- {renderedSemesters} - {edit && } + {renderedYears}
diff --git a/frontend/src/app/courses/semester/SemesterBox.tsx b/frontend/src/app/courses/semester/SemesterBox.tsx deleted file mode 100644 index 020d34e..0000000 --- a/frontend/src/app/courses/semester/SemesterBox.tsx +++ /dev/null @@ -1,29 +0,0 @@ - -import React from "react"; -import Style from "./SemesterBox.module.css" - -import { StudentSemester, User } from "@/types/type-user"; - -import CourseBox from "./course/CourseBox"; -import AddCourseButton from "./add-course/AddCourseButton"; - -function SemesterBox(props: { edit: boolean, studentSemester: StudentSemester, user: User, setUser: Function }) { - - let studentCourseBoxes = props.studentSemester.studentCourses.map((studentCourse, index) => ( - - )); - - return( -
-
- {props.studentSemester.season} -
-
- {studentCourseBoxes} - {props.edit && } -
-
- ); -} - -export default SemesterBox; diff --git a/frontend/src/app/courses/semester/add-course/AddCourseButton.tsx b/frontend/src/app/courses/semester/add-course/AddCourseButton.tsx deleted file mode 100644 index a391fdb..0000000 --- a/frontend/src/app/courses/semester/add-course/AddCourseButton.tsx +++ /dev/null @@ -1,121 +0,0 @@ - -import { useRef, useState, useEffect } from "react"; -import Style from "./AddCourseButton.module.css"; - -import { User, StudentCourse } from "@/types/type-user"; -import { getCatalogCourse } from "@/database/data-catalog"; - -interface AddCourseDisplay { - active: boolean; - dropVis: boolean; -} - -const terms = [202203, 202301]; - -function executeAddCourse( - props: { term: number; user: User; setUser: Function }, - inputRef: React.RefObject, - selectedTerm: number, - setAddDisplay: Function -){ - if(inputRef.current){ - const targetCode = inputRef.current.value; - const targetCourse = getCatalogCourse(selectedTerm, targetCode); - - if(targetCourse){ - const status = selectedTerm === props.term ? "MA_VALID" : "MA_HYPOTHETICAL"; - const newCourse: StudentCourse = { course: targetCourse, status, term: props.term }; - - const updatedSemesters = props.user.FYP.studentSemesters.map((semester) => { - if (semester.season === selectedTerm) { - return { ...semester, studentCourses: [...semester.studentCourses, newCourse] }; - } - return semester; - }); - - props.setUser({ ...props.user, FYP: { ...props.user.FYP, studentSemesters: updatedSemesters } }); - setAddDisplay((prevState: AddCourseDisplay) => ({ ...prevState, active: false })); - } - } -} - -function AddCourseButton(props: { term: number; user: User; setUser: Function }) { - - const inputRef = useRef(null); - const addRef = useRef(null); - - const [addDisplay, setAddDisplay] = useState({ active: false, dropVis: false }); - const [selectedTerm, setSelectedTerm] = useState(props.term); - - useEffect(() => { - if(addDisplay.active){ - document.addEventListener("mousedown", handleClickOutside); - inputRef.current?.focus(); - } - - return () => { - if(addDisplay.active){ - document.removeEventListener("mousedown", handleClickOutside); - } - }; - }, [addDisplay]); - - const handleClickOutside = (event: MouseEvent) => { - if(addRef.current && !addRef.current.contains(event.target as Node)){ - if(addDisplay.dropVis){ - setAddDisplay((prevState) => ({...prevState, dropVis: false})); - setTimeout(() => { - if(inputRef.current){ - inputRef.current.focus(); - } - }, 0); - }else{ - setAddDisplay((prevState) => ({...prevState, active: false})); - } - } - }; - - const handleKeyPress = (event: React.KeyboardEvent) => { - if(event.key === "Enter"){ - executeAddCourse(props, inputRef, selectedTerm, setAddDisplay); - } - }; - - return( -
- {!addDisplay.active ? ( -
setAddDisplay((prevState) => ({...prevState, active: true}))}> - + -
- ) : ( -
-
-
setAddDisplay((prevState) => ({...prevState, active: false}))}> - -
-
setAddDisplay((prevState) => ({...prevState, dropVis: !addDisplay.dropVis}))}> - {selectedTerm} - {addDisplay.dropVis && ( -
- {terms.map((term, index) => ( -
setSelectedTerm(term)} className={term === selectedTerm ? Style.SelectedTerm : ""}> - {term} -
- ))} -
- )} -
- - - -
executeAddCourse(props, inputRef, selectedTerm, setAddDisplay)}> - -
-
-
- )} -
- ); -} - -export default AddCourseButton; diff --git a/frontend/src/app/courses/semester/course/CourseBox.tsx b/frontend/src/app/courses/semester/course/CourseBox.tsx deleted file mode 100644 index 6cc7bfb..0000000 --- a/frontend/src/app/courses/semester/course/CourseBox.tsx +++ /dev/null @@ -1,50 +0,0 @@ - -import Style from "./CourseBox.module.css"; -import { User, StudentCourse } from "@/types/type-user"; - -import { RenderMark } from "./CourseBoxUtils"; -import { SeasonIcon } from "./CourseBoxUtils"; -import DistributionCircle from "@/components/distribution-circle/DistributionsCircle"; - -// import img_fall from "./../../../../commons/images/fall.png"; -// import img_spring from "./../../../../commons/images/spring.png"; - - - -// import { useModal } from "../../../hooks/modalContext"; - -function CourseBox(props: {edit: boolean, studentCourse: StudentCourse, user: User, setUser: Function }) -{ - // const { setModalOpen } = useModal(); - // function openModal() { - // setModalOpen(props.SC.course) - // } - - const getBackgroundColor = () => (props.studentCourse.status === "DA_COMPLETE" ? "#E1E9F8" : "#F5F5F5"); - - - return ( -
- {/* onClick={openModal} */} -
- - -
-
- {props.studentCourse.course.codes[0]} -
-
- {props.studentCourse.course.title} -
-
-
-
-
- -
-
-
- ); -} - -export default CourseBox; diff --git a/frontend/src/app/courses/semester/course/CourseBoxUtils.tsx b/frontend/src/app/courses/semester/course/CourseBoxUtils.tsx deleted file mode 100644 index b85ed43..0000000 --- a/frontend/src/app/courses/semester/course/CourseBoxUtils.tsx +++ /dev/null @@ -1,66 +0,0 @@ - -import Style from "./CourseBox.module.css" -import Image from "next/image"; - -import { StudentCourse, User } from "@/types/type-user"; - -function RemoveCourse(props: { studentCourse: StudentCourse; user: User; setUser: Function }) -{ - const remove = () => { - const updatedStudentSemesters = props.user.FYP.studentSemesters.map((semester) => { - if(semester.season === props.studentCourse.term){ - return{ - ...semester, - studentCourses: semester.studentCourses.filter( - (studentCourse) => - studentCourse.course.title !== props.studentCourse.course.title - ), - }; - } - return semester; - }); - - const updatedUser = { ...props.user, FYP: { ...props.user.FYP, studentSemesters: updatedStudentSemesters } }; - props.setUser(updatedUser); - }; - - return ( -
- -
- ); -} - -export function RenderMark(props: { edit: boolean, studentCourse: StudentCourse, user: User, setUser: Function }) -{ - if(props.studentCourse.status === "DA_COMPLETE" || props.studentCourse.status === "DA_PROSPECT"){ - return( -
- ✓ -
- ); - }else - if(props.studentCourse.status === "MA_HYPOTHETICAL" || props.studentCourse.status === "MA_VALID"){ - const mark = props.studentCourse.status === "MA_HYPOTHETICAL" ? "⚠" : "☑"; - return( -
- {props.edit && } -
- {mark} -
-
- - ); - } - return
; -} - -export function SeasonIcon(props: { studentCourse: StudentCourse }) -{ - // const getSeasonImage = () => (String(props.studentCourse.term).endsWith("3") ? fall : fall); - return( -
- -
- ) -} diff --git a/frontend/src/app/courses/years/YearBox.Module.css b/frontend/src/app/courses/years/YearBox.Module.css new file mode 100644 index 0000000..1e68602 --- /dev/null +++ b/frontend/src/app/courses/years/YearBox.Module.css @@ -0,0 +1,17 @@ + +.Column { + display: flex; + flex-direction: column; +} + +.Row { + display: flex; + flex-direction: row; +} + +.Grade { + font-weight: 600; + font-size: 25px; + margin-right: 10px; + } + \ No newline at end of file diff --git a/frontend/src/app/courses/years/YearBox.tsx b/frontend/src/app/courses/years/YearBox.tsx new file mode 100644 index 0000000..bc166bf --- /dev/null +++ b/frontend/src/app/courses/years/YearBox.tsx @@ -0,0 +1,47 @@ + +import { useState, useEffect } from "react"; +import Style from "./YearBox.module.css"; +import { User, StudentYear, StudentSemester } from "@/types/type-user"; + +import SemesterBox from "./semester/SemesterBox" +import AddSemesterButton from "./add-semester/AddSemesterButton" + +function RenderSemesters(props: { edit: boolean; columns: boolean; studentYear: StudentYear, setStudentYears: Function, user: User, setUser: Function }) +{ + const newRenderedSemesters = props.studentYear.studentSemesters + .filter((studentSemester: StudentSemester) => studentSemester.term !== 0) + .map((studentSemester: StudentSemester) => ( + + )); + + return( +
+ {newRenderedSemesters} +
+ ); +} + +function YearBox(props: { edit: boolean, columns: boolean, studentYear: StudentYear, setStudentYears: Function, user: User, setUser: Function }) +{ + const [renderedSemesters, setRenderedSemesters] = useState(null); + + useEffect(() => { + setRenderedSemesters( + + ); + }, [props.edit, props.columns, props.studentYear, props.user]); + + return( +
+
+ {props.studentYear.grade} +
+
+ {renderedSemesters} + {(props.edit && (props.studentYear.studentSemesters.length < 3)) && } +
+
+ ); +} + +export default YearBox; diff --git a/frontend/src/app/courses/add-semester/AddSemesterButton.module.css b/frontend/src/app/courses/years/add-semester/AddSemesterButton.module.css similarity index 87% rename from frontend/src/app/courses/add-semester/AddSemesterButton.module.css rename to frontend/src/app/courses/years/add-semester/AddSemesterButton.module.css index b6dc39b..1a8922f 100644 --- a/frontend/src/app/courses/add-semester/AddSemesterButton.module.css +++ b/frontend/src/app/courses/years/add-semester/AddSemesterButton.module.css @@ -27,7 +27,7 @@ flex-direction: row; justify-content: space-between; align-items: center; - width: 425px; + width: 200px; height: 36px; border-radius: 16px; margin-bottom: 5px; @@ -123,31 +123,14 @@ /* */ -.ConfirmButton { - display: flex; - justify-content: center; - align-items: center; - width: 18px; - height: 18px; - border-radius: 50%; - background-color: #dbdbdb; - cursor: pointer; - margin-right: 5px; -} -.ConfirmButton:hover { - background-color: #D3D3D3; -} - -/* */ - .RemoveButton { display: flex; justify-content: center; align-items: center; - width: 16px; - height: 16px; + width: 20px; + height: 20px; border-radius: 50%; - background-color: #ededed; + background-color: white; cursor: pointer; margin-right: 5px; } diff --git a/frontend/src/app/courses/years/add-semester/AddSemesterButton.tsx b/frontend/src/app/courses/years/add-semester/AddSemesterButton.tsx new file mode 100644 index 0000000..58a896b --- /dev/null +++ b/frontend/src/app/courses/years/add-semester/AddSemesterButton.tsx @@ -0,0 +1,100 @@ + +import { useRef, useState, useEffect } from "react"; +import Style from "./AddSemesterButton.module.css"; + +import { StudentSemester, StudentYear } from "@/types/type-user"; + +function executeAddSemester( + props: { studentYear: StudentYear, setStudentYears: Function }, + inputRef: React.RefObject, + setDropVis: Function +) { + if (inputRef.current) { + const newTermString = inputRef.current.value.trim(); + + // Ensure input is a valid 6-digit number + if (!/^\d{6}$/.test(newTermString)) { + return; + } + + const newTermNumber = Number(newTermString); + + // Create a new semester object + const newSemester: StudentSemester = { + term: newTermNumber, + studentCourses: [], + active: true, + }; + + // Update studentYears with a new array reference + props.setStudentYears((prevYears: StudentYear[]) => { + return prevYears.map(year => { + if (year.grade === props.studentYear.grade) { + return { + ...year, + studentSemesters: [...year.studentSemesters, newSemester] // Create new array + }; + } + return year; + }); + }); + + setDropVis(false); + } +} + + + +function AddSemesterButton(props: { studentYear: StudentYear, setStudentYears: Function }) { + const [dropVis, setDropVis] = useState(false); + const buttonRef = useRef(null); + const inputRef = useRef(null); + + useEffect(() => { + function handleClickOutside(event: MouseEvent) { + if (buttonRef.current && !buttonRef.current.contains(event.target as Node)) { + setDropVis(false); + } + } + + if (dropVis) { + document.addEventListener("mousedown", handleClickOutside); + } + + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, [dropVis]); + + // const handleKeyPress = (event: React.KeyboardEvent) => { + // if (event.key === "Enter") { + // executeAddSemester(props, inputRef, setDropVis); + // } + // }; + + return ( +
+ {!dropVis ? ( + + ) : ( +
+
+
setDropVis(false)}>
+ +
executeAddSemester(props, inputRef, setDropVis)}>
+
+
+ )} +
+ ); +} + + +export default AddSemesterButton; diff --git a/frontend/src/app/courses/semester/SemesterBox.module.css b/frontend/src/app/courses/years/semester/SemesterBox.module.css similarity index 100% rename from frontend/src/app/courses/semester/SemesterBox.module.css rename to frontend/src/app/courses/years/semester/SemesterBox.module.css diff --git a/frontend/src/app/courses/years/semester/SemesterBox.tsx b/frontend/src/app/courses/years/semester/SemesterBox.tsx new file mode 100644 index 0000000..5cc3625 --- /dev/null +++ b/frontend/src/app/courses/years/semester/SemesterBox.tsx @@ -0,0 +1,47 @@ + +import React, {useState, useEffect} from "react"; +import Style from "./SemesterBox.module.css" + +import { StudentSemester, User } from "@/types/type-user"; +import { TransformTermNumber, IsTermActive } from "@/utils/CourseDisplay"; + +import CourseBox from "./course/CourseBox"; +import AddCourseButton from "./add-course/AddCourseButton"; + +function RenderCourses(props: { edit: boolean, studentSemester: StudentSemester, user: User, setUser: Function }) +{ + const renderedCourses = props.studentSemester.studentCourses.map((studentCourse, index) => ( + + )); + + return( +
+ {renderedCourses} +
+ ); + } + +function SemesterBox(props: { edit: boolean, studentSemester: StudentSemester, user: User, setUser: Function }) { + + const [renderedCourses, setRenderedCourses] = useState(null); + + useEffect(() => { + setRenderedCourses( + + ); + }, [props.edit, props.studentSemester, props.user]); + + return( +
+
+ {TransformTermNumber(props.studentSemester.term)} +
+
+ {renderedCourses} + {props.edit && } +
+
+ ); +} + +export default SemesterBox; diff --git a/frontend/src/app/courses/semester/add-course/AddCourseButton.module.css b/frontend/src/app/courses/years/semester/add-course/AddCourseButton.module.css similarity index 84% rename from frontend/src/app/courses/semester/add-course/AddCourseButton.module.css rename to frontend/src/app/courses/years/semester/add-course/AddCourseButton.module.css index b6dc39b..86542a7 100644 --- a/frontend/src/app/courses/semester/add-course/AddCourseButton.module.css +++ b/frontend/src/app/courses/years/semester/add-course/AddCourseButton.module.css @@ -27,7 +27,7 @@ flex-direction: row; justify-content: space-between; align-items: center; - width: 425px; + width: 420px; height: 36px; border-radius: 16px; margin-bottom: 5px; @@ -59,9 +59,29 @@ border-color: #aaa; } +.ResultBox { + border: 1px solid #ccc; + padding: 0 8px; + background-color: white; + cursor: pointer; + border-radius: 4px; + font-size: 12px; + height: 22px; + width: 60px; + display: flex; + align-items: center; + box-sizing: border-box; + color: black; + position: relative; + transition: border-color 0.3s ease; +} +.ResultBox:hover { + border-color: #aaa; +} + /* */ -.TermOptions { +.DropdownOptions { position: absolute; background-color: #fff; border: 1px solid #ccc; @@ -78,18 +98,18 @@ box-sizing: border-box; } -.TermOptions div { +.DropdownOptions div { padding: 8px; cursor: pointer; color: black; background-color: white; } -.TermOptions div:hover { +.DropdownOptions div:hover { background-color: #f0f0f0; } -.SelectedTerm { +.SelectedDropdownOption { background-color: #93c7ff !important; /* Light blue background for the selected term */ color: black; /* Optional: change text color for selected term */ } diff --git a/frontend/src/app/courses/years/semester/add-course/AddCourseButton.tsx b/frontend/src/app/courses/years/semester/add-course/AddCourseButton.tsx new file mode 100644 index 0000000..bffdeaf --- /dev/null +++ b/frontend/src/app/courses/years/semester/add-course/AddCourseButton.tsx @@ -0,0 +1,129 @@ + +import { useRef, useState, useEffect } from "react"; +import Style from "./AddCourseButton.module.css"; + +import { User, StudentCourse } from "@/types/type-user"; +import { getCatalogCourse, getCatalogTerms } from "@/database/data-catalog"; + +interface AddCourseDisplay { + active: boolean; + termDropVis: boolean; + resultDropVis: boolean; +} + +function executeAddCourse( + props: { term: number; user: User; setUser: Function }, + inputRef: React.RefObject, + selectedTerm: number, + selectedResult: string, + setAddDisplay: Function +){ + if(inputRef.current){ + const targetCode = inputRef.current.value; + const targetCourse = getCatalogCourse(selectedTerm, targetCode); + + if(targetCourse){ + const status = selectedTerm === props.term ? "DA" : "MA"; + const newCourse: StudentCourse = { course: targetCourse, status, term: props.term, result: selectedResult }; + + const updatedCourses = [...props.user.FYP.studentCourses, newCourse]; + + props.setUser({ ...props.user, FYP: { ...props.user.FYP, studentCourses: updatedCourses } }); + setAddDisplay((prevState: AddCourseDisplay) => ({ ...prevState, active: false })); + } + } +} + +function AddCourseButton(props: { term: number; user: User; setUser: Function }) { + + const inputRef = useRef(null); + const addRef = useRef(null); + + const [addDisplay, setAddDisplay] = useState({ active: false, termDropVis: false, resultDropVis: false }); + const [selectedTerm, setSelectedTerm] = useState(props.term); + const [selectedResult, setSelectedResult] = useState(""); + const resultOptions = ["GRADE", "CR", "D/F"]; + + const catalogTerms = getCatalogTerms() + + useEffect(() => { + if(addDisplay.active){ + document.addEventListener("mousedown", handleClickOutside); + inputRef.current?.focus(); + } + + return () => { + if(addDisplay.active){ + document.removeEventListener("mousedown", handleClickOutside); + } + }; + }, [addDisplay]); + + const handleClickOutside = (event: MouseEvent) => { + if(addRef.current && !addRef.current.contains(event.target as Node)){ + if(addDisplay.termDropVis || addDisplay.resultDropVis){ + setAddDisplay((prevState) => ({...prevState, termDropVis: false, resultDropVis: false})); + setTimeout(() => { + if(inputRef.current){ + inputRef.current.focus(); + } + }, 0); + }else{ + setAddDisplay((prevState) => ({...prevState, active: false})); + } + } + }; + + const handleKeyPress = (event: React.KeyboardEvent) => { + if(event.key === "Enter"){ + executeAddCourse(props, inputRef, selectedTerm, selectedResult, setAddDisplay); + } + }; + + return( +
+ {!addDisplay.active ? ( +
setAddDisplay((prevState) => ({...prevState, active: true}))}> + + +
+ ) : ( +
+
+
setAddDisplay((prevState) => ({...prevState, active: false}))}> +
+
setAddDisplay((prevState) => ({...prevState, termDropVis: !prevState.termDropVis}))}> + {selectedTerm} + {addDisplay.termDropVis && ( +
+ {catalogTerms.map((term, index) => ( +
setSelectedTerm(term)} className={term === selectedTerm ? Style.SelectedDropdownOption : ""}> + {term} +
+ ))} +
+ )} +
+ + +
setAddDisplay((prevState) => ({...prevState, resultDropVis: !prevState.resultDropVis}))}> + {selectedResult || ""} + {addDisplay.resultDropVis && ( +
+ {resultOptions.map((result, index) => ( +
setSelectedResult(result)} className={result === selectedResult ? Style.SelectedDropdownOption : ""}> + {result} +
+ ))} +
+ )} +
+
executeAddCourse(props, inputRef, selectedTerm, selectedResult, setAddDisplay)}> +
+
+
+ )} +
+ ); +} + +export default AddCourseButton; diff --git a/frontend/src/app/courses/semester/add-course/AddCourseUtils.ts b/frontend/src/app/courses/years/semester/add-course/AddCourseUtils.ts similarity index 100% rename from frontend/src/app/courses/semester/add-course/AddCourseUtils.ts rename to frontend/src/app/courses/years/semester/add-course/AddCourseUtils.ts diff --git a/frontend/src/app/courses/semester/course/CourseBox.module.css b/frontend/src/app/courses/years/semester/course/CourseBox.module.css similarity index 98% rename from frontend/src/app/courses/semester/course/CourseBox.module.css rename to frontend/src/app/courses/years/semester/course/CourseBox.module.css index 739f3ee..c44cf5f 100644 --- a/frontend/src/app/courses/semester/course/CourseBox.module.css +++ b/frontend/src/app/courses/years/semester/course/CourseBox.module.css @@ -37,7 +37,7 @@ justify-content: space-between; align-items: center; - width: 425px; + width: 420px; height: 36px; border-radius: 16px; diff --git a/frontend/src/app/courses/years/semester/course/CourseBox.tsx b/frontend/src/app/courses/years/semester/course/CourseBox.tsx new file mode 100644 index 0000000..7c460ec --- /dev/null +++ b/frontend/src/app/courses/years/semester/course/CourseBox.tsx @@ -0,0 +1,59 @@ + +import Style from "./CourseBox.module.css"; +import { User, StudentCourse } from "@/types/type-user"; + +import { RenderMark, SeasonIcon, GetCourseColor, IsTermActive } from "./../../../../../utils/CourseDisplay"; +import DistributionCircle from "@/components/distribution-circle/DistributionsCircle"; + +// import { useModal } from "../../../hooks/modalContext"; +// const { setModalOpen } = useModal(); function openModal() { setModalOpen(props.SC.course) } // onClick={openModal} + +function RemoveCourse(props: { studentCourse: StudentCourse; user: User; setUser: Function }) +{ + const remove = () => { + const updatedStudentCourses = props.user.FYP.studentCourses.filter( + (course) => course.course.title !== props.studentCourse.course.title + ); + + const updatedUser = { + ...props.user, + FYP: { + ...props.user.FYP, + studentCourses: updatedStudentCourses + } + }; + + props.setUser(updatedUser); + }; + + return ( +
+ ); +} + +function CourseBox(props: {edit: boolean, studentCourse: StudentCourse, user: User, setUser: Function }){ + return( +
+
+ {(props.edit && IsTermActive(props.studentCourse.term)) && } + + +
+
+ {props.studentCourse.course.codes[0]} +
+
+ {props.studentCourse.course.title} +
+
+
+
+
+ +
+
+
+ ); +} + +export default CourseBox; diff --git a/frontend/src/app/login/Login.module.css b/frontend/src/app/login/Login.module.css new file mode 100644 index 0000000..11b2fcd --- /dev/null +++ b/frontend/src/app/login/Login.module.css @@ -0,0 +1,62 @@ +.centerDiv { + min-height: 100vh; + text-align: center; + max-width: 1000px; + margin-left: auto; + margin-right: auto; + justify-content: center; + align-items: center; + padding: 1rem; + display: flex; + flex-wrap: wrap; +} + +.featureListStyle { + list-style: none; + display: flex; + flex-direction: column; + gap: .5rem; + margin: 0; + padding: 0; +} + +.featureItemStyle { + font-weight: 500; + transition: transform .3s; + cursor: default; + text-align: left; +} + +.featureItemStyle:hover { + transform: translateX(7px); +} + +.loginButtons { + display: flex; + margin-left: auto; + margin-right: auto; + margin-top: 3px; + justify-content: flex-start; + justify-content: center; +} + +.btn { + text-decoration: none; + padding: 8px 24px; + border-radius: 10px; + font-weight: 500; + color: black; + background-color: lightblue; + transition: + transform 0.3s, + filter 0.3s; + margin-top: 8px; + font-size: large; + cursor: pointer; +} + +.btn:hover { + text-decoration: none; + transform: translateY(-5px); + filter: brightness(90%); +} \ No newline at end of file diff --git a/frontend/src/app/login/page.tsx b/frontend/src/app/login/page.tsx new file mode 100644 index 0000000..4eb3ab4 --- /dev/null +++ b/frontend/src/app/login/page.tsx @@ -0,0 +1,62 @@ + +"use client"; +import { useRouter } from "next/navigation"; +import Style from "./Login.module.css"; + +import { useAuth } from "../providers"; +import NavBar from "@/components/navbar/NavBar"; + +function Login() +{ + const router = useRouter(); + const { setAuth, setUser } = useAuth(); + + const handleLogin = async () => { + try { + const response = await fetch("/api/login", { method: "GET" }); + + if (!response.ok) { + throw new Error("Login failed"); + } + + const data = await response.json(); + setAuth({ loggedIn: true }); + setUser(data); + + if(!data.onboard){ + router.push("/account"); + }else{ + router.push("/graduation"); + } + + } catch (error) { + console.error("❌ Login error:", error); + } + }; + + + return ( +
+ +
+
+

Plan Your Major @ Yale

+
    +
  • Explore 80+ Majors
  • +
  • Check Distributional Requirements
  • +
  • Plan Four-Year Plan
  • +
  • Cool Guy
  • +
+
+
+ Login w/ CAS +
+
+
+ Landing Page +
+
+ ); +} + +export default Login; diff --git a/frontend/src/app/majors/Majors.module.css b/frontend/src/app/majors/Majors.module.css index df741d6..0243411 100644 --- a/frontend/src/app/majors/Majors.module.css +++ b/frontend/src/app/majors/Majors.module.css @@ -1,11 +1,28 @@ .MajorsPage { + /* border: 1px solid blue; */ + position: absolute; + top: 75px; display: flex; flex-direction: row; + justify-content: center; - padding: 20px calc(50% - 500px); - margin-top: 100px; + width: 100%; + height: calc(100vh - 75px); + + /* padding-top: 50px; */ +} + +.Divider { + border-right: 1px solid gray; + + margin-top: 50px; + margin-right: 25px; + margin-left: 25px; + + width: 1px; + height: calc(100% - 100px); } diff --git a/frontend/src/app/majors/metadata/Metadata.module.css b/frontend/src/app/majors/metadata/Metadata.module.css index adf4b8b..eefa8bd 100644 --- a/frontend/src/app/majors/metadata/Metadata.module.css +++ b/frontend/src/app/majors/metadata/Metadata.module.css @@ -1,4 +1,23 @@ +.Column { + display: flex; + flex-direction: column; +} + +.Row{ + display: flex; + flex-direction: row; +} + +.MetadataContainer{ + /* border: 1px solid red; */ + + margin-top: 75px; + + width: 550px; + height: 600px; +} + .countBox { display: inline-block; /* Display as inline block */ width: max-content; /* Set width to maximum content size */ @@ -52,13 +71,12 @@ margin-right: 10px; } -.majorContainer { - padding: 20px 20px 20px 0; +.MajorContainer { + /* border: 1px solid gray; */ + width: auto; height: 400px; background-color: white; - margin-right: 10px; - /* border: 1px solid black; */ } .thumbtack { diff --git a/frontend/src/app/majors/metadata/Metadata.tsx b/frontend/src/app/majors/metadata/Metadata.tsx index e5d4fc4..87316bc 100644 --- a/frontend/src/app/majors/metadata/Metadata.tsx +++ b/frontend/src/app/majors/metadata/Metadata.tsx @@ -86,7 +86,7 @@ function MetadataContent(props: { programIndex: number, }){ return ( -
+
@@ -152,7 +152,7 @@ function Metadata(props: { peekProgram: Function }) { return ( -
+
diff --git a/frontend/src/app/majors/page.tsx b/frontend/src/app/majors/page.tsx index 4fb5166..1ebc057 100644 --- a/frontend/src/app/majors/page.tsx +++ b/frontend/src/app/majors/page.tsx @@ -20,7 +20,7 @@ function Majors() const allProgramMetadatas: DegreeMetadata[][] = ALL_PROGRAM_METADATAS; - const shiftProgramIndex = (dir: number) => { + const shiftProgramIndex: Function = (dir: number) => { setProgramIndex((programIndex + dir + allProgramMetadatas.length) % allProgramMetadatas.length); }; @@ -40,6 +40,7 @@ function Majors() shiftProgramIndex={shiftProgramIndex} peekProgram={peekProgram} /> +
+ ); + } + + const matchingStudentCourse = props.subreq.student_courses_satisfying.find( + (studentCourse) => studentCourse.course === props.course + ); + + return( +
+ +
+ ) +} + +function RenderSubrequirement(props: { subreq: DegreeSubrequirement, user: User }) +{ + const [showAll, setShowAll] = useState(false); + + // Extract non-null courses + const nonNullCourses = props.subreq.courses_options.filter((course) => course !== null) as Course[]; + const satisfiedCourses = props.subreq.student_courses_satisfying.map((studentCourse) => studentCourse.course); + + // Determine which courses to show based on satisfaction condition + const isSatisfied = props.subreq.student_courses_satisfying.length === props.subreq.courses_required; + const displayedCourses = showAll + ? nonNullCourses + : isSatisfied + ? satisfiedCourses + : nonNullCourses.slice(0, 4); + + const extraCoursesCount = showAll ? 0 : nonNullCourses.length - displayedCourses.length; + + return( +
+
+ {props.subreq.student_courses_satisfying.length}|{props.subreq.courses_required} {props.subreq.subreq_name} +
+
+ {props.subreq.subreq_desc} +
+
+ {displayedCourses.map((course, index) => ( +
+ +
+ ))} + {/* Show More / Show Less Button */} + {nonNullCourses.length > 4 && ( +
setShowAll(!showAll)}> + {showAll ? "<<" : ">>"} +
) + } +
+
+ ) +} + +function RenderRequirement(props: { req: DegreeRequirement, user: User }) +{ + return( +
+ +
+
+ {props.req.req_name} +
+
+ {props.req.courses_satisfied_count}|{props.req.courses_required_count} +
+
+ +
+ {props.req.subreqs_list.map((subreq, index) => ( + + ))} +
+
+ ) +} + +function RequirementsContent(props: { edit: boolean, degreeConfiguration: DegreeConfiguration, user: User, setUser: Function }) +{ return( -
- +
+ {props.degreeConfiguration.reqs_list.map((req, index) => ( + + ))}
); } -function Requirements(props: { - user: User, - setUser: Function, - degreeConfiguration: DegreeConfiguration -}){ +function Requirements(props: { user: User, setUser: Function, degreeConfiguration: DegreeConfiguration }) +{ const [edit, setEdit] = useState(false); const updateEdit = () => { @@ -34,7 +116,7 @@ function Requirements(props: { }; return( -
+
Requirements @@ -45,7 +127,9 @@ function Requirements(props: {
- +
+ +
); } diff --git a/frontend/src/app/not-found.tsx b/frontend/src/app/not-found.tsx new file mode 100644 index 0000000..0b7ddd6 --- /dev/null +++ b/frontend/src/app/not-found.tsx @@ -0,0 +1,20 @@ + +"use client"; +import { useEffect } from "react"; +import { useRouter } from "next/navigation"; +import { useAuth } from "@/app/providers"; + +export default function NotFoundPage() { + const { auth } = useAuth(); + const router = useRouter(); + + useEffect(() => { + if (auth.loggedIn) { + router.replace("/graduation"); + } else { + router.replace("/login"); + } + }, [auth.loggedIn, router]); + + return
Redirecting...
; +} diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index b0171eb..839aa68 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -4,18 +4,16 @@ import { useEffect } from "react"; import { useRouter } from "next/navigation"; import { useAuth } from "./providers"; -export default function MajorAudit() +export default function MajorAudit() { const router = useRouter(); - const { auth } = useAuth(); + const { auth } = useAuth(); useEffect(() => { - if(!auth.loggedIn){ - router.push("/login"); - }else if(!auth.onboard){ - router.push("/onboard"); - }else{ - router.push("/graduation"); + if (auth.loggedIn) { + router.replace("/graduation"); + } else { + router.replace("/login"); } }, [auth, router]); diff --git a/frontend/src/app/providers.tsx b/frontend/src/app/providers.tsx index 7b73a64..020a3cb 100644 --- a/frontend/src/app/providers.tsx +++ b/frontend/src/app/providers.tsx @@ -1,17 +1,21 @@ "use client"; -import { createContext, useContext, useState } from "react"; +import { createContext, useContext, useState, useEffect } from "react"; -import { User } from "../types/type-user"; -import { Ryan } from "./../database/data-user"; +import { User } from "@/types/type-user"; +import { NullUser, Ryan } from "@/database/data-user"; const AuthContext = createContext(null); -export function AuthProvider({children}: {children: React.ReactNode}) -{ - const [auth, setAuth] = useState({ loggedIn: true, onboard: true }); +export function AuthProvider({ children }: { children: React.ReactNode }) { + const [auth, setAuth] = useState({ loggedIn: false }); const [user, setUser] = useState(Ryan); + // uh this isnt right + useEffect(() => { + setUser(Ryan); + }, []); + return( {children} @@ -19,7 +23,6 @@ export function AuthProvider({children}: {children: React.ReactNode}) ); } -export function useAuth() -{ +export function useAuth() { return useContext(AuthContext); } diff --git a/frontend/src/components/course-icon/CourseIcon.module.css b/frontend/src/components/course-icon/CourseIcon.module.css index 1c4e427..7efaaa5 100644 --- a/frontend/src/components/course-icon/CourseIcon.module.css +++ b/frontend/src/components/course-icon/CourseIcon.module.css @@ -15,8 +15,6 @@ background-color: #F5F5F5; transition: filter 0.4s ease; - - margin-right: 2px; } .CourseIcon:hover { diff --git a/frontend/src/components/course-icon/CourseIcon.tsx b/frontend/src/components/course-icon/CourseIcon.tsx index cdaf3f3..31eb639 100644 --- a/frontend/src/components/course-icon/CourseIcon.tsx +++ b/frontend/src/components/course-icon/CourseIcon.tsx @@ -1,18 +1,17 @@ import React from "react"; import styles from "./CourseIcon.module.css"; -import "react-tooltip/dist/react-tooltip.css"; -import { StudentCourse } from "@/types/type-user"; -import fall from "./fall.svg"; +import { StudentCourse, Course } from "@/types/type-user"; +import { RenderMark, GetCourseColor } from "@/utils/CourseDisplay"; + import DistributionCircle from "../distribution-circle/DistributionsCircle"; -// import { useModal } from "../../../hooks/modalContext"; function CourseSeasonIcon(props: { seasons: Array }) { const seasonImageMap: { [key: string]: string } = { - "Fall": fall, - "Spring": fall, + "Fall": "./fall.svg", + "Spring": "./spring.svg", }; return ( @@ -32,12 +31,18 @@ function CourseSeasonIcon(props: { seasons: Array }) { ); } -function DistCircDiv(props: { dist: Array }) { - if (!Array.isArray(props.dist) || props.dist.length === 0) { - return
; + +function DistCircDiv(props: { dist: string[] }) +{ + if(!Array.isArray(props.dist) || props.dist.length === 0){ + return( +
+ +
+ ); } - return ( + return(
@@ -47,39 +52,37 @@ function DistCircDiv(props: { dist: Array }) { export function StudentCourseIcon(props: { studentCourse: StudentCourse, utilityButton?: React.ReactNode }) { - const mark = (status: string) => { - let mark = ""; - switch (status) { - case "DA_COMPLETE": - case "DA_PROSPECT": - mark = "✓"; - break; - case "MA_HYPOTHETICAL": - mark = "⚠"; - break; - case "MA_VALID": - mark = "☑"; - break; - default: - return
; - } - return
{mark}
; - }; - const dist = props.studentCourse.course.dist || []; + // style={{ backgroundColor: GetCourseColor(props.studentCourse.term) }} + return ( -
+
{props.utilityButton && props.utilityButton} - {props.studentCourse.status === "NA" + {props.studentCourse.status === "" ? - : mark(props.studentCourse.status) + : } {props.studentCourse.course.codes[0]} - + {/* */} +
+ ); +} + + +export function CourseIcon(props: { course: Course, studentCourse?: StudentCourse }){ + + if(props.studentCourse){ + return( + + ); + } + + return( +
+ + {props.course.codes[0]} +
); } diff --git a/frontend/src/components/navbar/NavBar.module.css b/frontend/src/components/navbar/NavBar.module.css index 59bc85f..95ce719 100644 --- a/frontend/src/components/navbar/NavBar.module.css +++ b/frontend/src/components/navbar/NavBar.module.css @@ -42,8 +42,23 @@ } .Logo { - width: 150px; - height: auto; margin-right: 10px; margin-left: 20px; +} + +.Circle { + width: 35px; + height: 35px; + display: flex; + align-items: center; + justify-content: center; + background-color: white; + border-radius: 50%; + border: 2px solid transparent; /* Default border is invisible */ + + transition: background-color 0.3s ease-in-out, border-color 0.3s ease-in-out; +} + +.Circle:hover { + border-color: #cce5ff; /* Blue border appears on hover */ } \ No newline at end of file diff --git a/frontend/src/components/navbar/NavBar.tsx b/frontend/src/components/navbar/NavBar.tsx index aa313f1..c0806ad 100644 --- a/frontend/src/components/navbar/NavBar.tsx +++ b/frontend/src/components/navbar/NavBar.tsx @@ -1,17 +1,50 @@ "use client"; -import Image from "next/image"; import Style from "./NavBar.module.css"; -import PageLinks from "./PageLinks"; -function NavBar({utility}: {utility?: React.ReactNode}) { +import { usePathname } from "next/navigation"; +import Image from "next/image"; +import Link from "next/link"; + +function AccountButton() { + return( +
+ +
+ ); +} + +function PageLinks() +{ + const pathname = usePathname(); + + return( +
+ + Graduation + + + Courses + + + Majors + + + + +
+ ); +} + + +function NavBar({utility, loggedIn = true }: { utility?: React.ReactNode; loggedIn?: boolean }) { return(
- + {utility}
- + {loggedIn && }
); } diff --git a/frontend/src/components/navbar/PageLinks.tsx b/frontend/src/components/navbar/PageLinks.tsx deleted file mode 100644 index 13a9d62..0000000 --- a/frontend/src/components/navbar/PageLinks.tsx +++ /dev/null @@ -1,26 +0,0 @@ - -"use client"; -import { usePathname } from "next/navigation"; -import Link from "next/link"; -import Style from "./NavBar.module.css"; - -function PageLinks() -{ - const pathname = usePathname(); - - return( -
- - Graduation - - - Courses - - - Majors - -
- ); -} - -export default PageLinks; diff --git a/frontend/src/database/data-catalog.ts b/frontend/src/database/data-catalog.ts index 212e244..70a9827 100644 --- a/frontend/src/database/data-catalog.ts +++ b/frontend/src/database/data-catalog.ts @@ -1,5 +1,6 @@ import { Course } from "@/types/type-user"; +import { HSAR_401 } from "./data-courses"; interface Catalog { number: number; @@ -7,8 +8,14 @@ interface Catalog { } export const Catalogs: Catalog[] = [ - { number: 202203, courses: [{ codes: ["HSAR 401"], title: "Critical Approaches", credit: 1, dist: [], seasons: [] }] }, - { number: 202301, courses: [{ codes: ["HSAR 401"], title: "Critical Approaches", credit: 1, dist: [], seasons: [] }] }, + { number: 202203, courses: [HSAR_401] }, + { number: 202301, courses: [HSAR_401] }, + { number: 202302, courses: [HSAR_401] }, + { number: 202303, courses: [HSAR_401] }, + { number: 202401, courses: [HSAR_401] }, + { number: 202402, courses: [HSAR_401] }, + { number: 202403, courses: [HSAR_401] }, + { number: 202501, courses: [HSAR_401] }, ] export const getCatalogCourse = (catalogNumber: number, courseCode: string): Course | null => { @@ -19,3 +26,7 @@ export const getCatalogCourse = (catalogNumber: number, courseCode: string): Cou const course = catalog.courses.find((course) => course.codes.includes(courseCode)); return course || null; }; + +export const getCatalogTerms = (): number[] => { + return Catalogs.map((catalog) => catalog.number).sort((a, b) => a - b); +} diff --git a/frontend/src/database/data-courses.ts b/frontend/src/database/data-courses.ts new file mode 100644 index 0000000..c2c42f8 --- /dev/null +++ b/frontend/src/database/data-courses.ts @@ -0,0 +1,15 @@ + +import { Course } from "@/types/type-user" + +// HSAR ONE +export const HSAR_401: Course = { codes: ["HSAR 401"], title: "Critical Approaches To Art History", credit: 1, dist: ["QR"], seasons: ["Fall", "Spring"] } + +// CPSC PROGRAM +export const CPSC_201: Course = { codes: ["CPSC 201"], title: "Introduction To Computer Science", credit: 1, dist: ["QR"], seasons: ["Fall", "Spring"] } +export const CPSC_202: Course = { codes: ["CPSC 202"], title: "Math Tools For Computer Scientists", credit: 1, dist: ["QR"], seasons: ["Fall", "Spring"] } +export const MATH_244: Course = { codes: ["MATH 244"], title: "Discrete Mathematics", credit: 1, dist: ["QR"], seasons: ["Fall", "Spring"] } +export const CPSC_223: Course = { codes: ["CPSC 223"], title: "Data Structures And Programming Techniques", credit: 1, dist: ["QR"], seasons: ["Fall", "Spring"] } +export const CPSC_323: Course = { codes: ["CPSC 323"], title: "Introduction To Systems Programming", credit: 1, dist: ["QR"], seasons: ["Fall", "Spring"] } +export const CPSC_365: Course = { codes: ["CPSC 365"], title: "Algorithms", credit: 1, dist: ["QR"], seasons: ["Fall", "Spring"] } +export const CPSC_366: Course = { codes: ["CPSC 366"], title: "Intensive Algorithms", credit: 1, dist: ["QR"], seasons: ["Fall", "Spring"] } +export const CPSC_490: Course = { codes: ["CPSC 490"], title: "Senior Project", credit: 1, dist: ["QR"], seasons: ["Fall", "Spring"] } diff --git a/frontend/src/database/data-cpsc.ts b/frontend/src/database/data-cpsc.ts new file mode 100644 index 0000000..6ddd52e --- /dev/null +++ b/frontend/src/database/data-cpsc.ts @@ -0,0 +1,130 @@ + +import { DegreeConfiguration, DegreeRequirement, DegreeSubrequirement } from "@/types/type-program"; + +import { CPSC_201, CPSC_202, MATH_244, CPSC_223, CPSC_323, CPSC_365, CPSC_366, CPSC_490 } from "./data-courses"; +import { SC_CPSC_201, SC_CPSC_202, SC_CPSC_223, SC_CPSC_323 } from "./data-studentcourses"; + +const CPSC_INTRO: DegreeSubrequirement = { + subreq_type_id: 1, + subreq_name: "INTRO", + subreq_desc: "", + courses_required: 1, + courses_options: [CPSC_201], + courses_elective_range: null, + courses_any_bool: false, + student_courses_satisfying: [SC_CPSC_201], +} + +const CPSC_MATH: DegreeSubrequirement = { + subreq_type_id: 1, + subreq_name: "DISCRETE MATH", + subreq_desc: "", + courses_required: 1, + courses_options: [CPSC_202, MATH_244], + courses_elective_range: null, + courses_any_bool: false, + student_courses_satisfying: [], +} + +const CPSC_DATA: DegreeSubrequirement = { + subreq_type_id: 1, + subreq_name: "DATA STRUCTURES", + subreq_desc: "", + courses_required: 1, + courses_options: [CPSC_223], + courses_elective_range: null, + courses_any_bool: false, + student_courses_satisfying: [SC_CPSC_223], +} + +const CPSC_SYSTEMS: DegreeSubrequirement = { + subreq_type_id: 1, + subreq_name: "SYSTEMS", + subreq_desc: "", + courses_required: 1, + courses_options: [CPSC_323], + courses_elective_range: null, + courses_any_bool: false, + student_courses_satisfying: [SC_CPSC_323], +} + +const CPSC_ALGOS: DegreeSubrequirement = { + subreq_type_id: 1, + subreq_name: "ALGORITHMS", + subreq_desc: "", + courses_required: 1, + courses_options: [CPSC_365, CPSC_366], + courses_elective_range: null, + courses_any_bool: false, + student_courses_satisfying: [], +} + +const CPSC_CORE: DegreeRequirement = { + req_type_id: 1, + req_name: "CORE", + req_desc: "", + + courses_required_count: 5, + courses_satisfied_count: 3, + + subreqs_list: [CPSC_INTRO, CPSC_MATH, CPSC_DATA, CPSC_SYSTEMS, CPSC_ALGOS] +} + +const CPSC_RANGE_ELECS: DegreeSubrequirement = { + subreq_type_id: 1, + subreq_name: "", + subreq_desc: "Standard elective or DUS approved extra-department substitution.", + courses_required: 1, + courses_options: [null], + courses_elective_range: { dept: "CPSC", min_code: 300, max_code: 999 }, + courses_any_bool: false, + student_courses_satisfying: [] +} + +const CPSC_SUB_ELEC: DegreeSubrequirement = { + subreq_type_id: 1, + subreq_name: "", + subreq_desc: "Intermediate or advanced CPSC courses, traditionally numbered 300+.", + courses_required: 3, + courses_options: [null, null, null], + courses_elective_range: { dept: "CPSC", min_code: 300, max_code: 999 }, + courses_any_bool: true, + student_courses_satisfying: [] +} + +const CPSC_ELECTIVES: DegreeRequirement = { + req_type_id: 1, + req_name: "ELECTIVE", + req_desc: "", + + courses_required_count: 4, + courses_satisfied_count: 0, + + subreqs_list: [CPSC_SUB_ELEC, CPSC_RANGE_ELECS] +} + +const CPSC_SENPROJ: DegreeSubrequirement = { + subreq_type_id: 1, + subreq_name: "SENIOR PROJECT", + subreq_desc: "", + courses_required: 1, + courses_options: [CPSC_490], + courses_elective_range: null, + courses_any_bool: false, + student_courses_satisfying: [] +} + +const CPSC_SENIOR: DegreeRequirement = { + req_type_id: 1, + req_name: "SENIOR", + req_desc: "", + + courses_required_count: 1, + courses_satisfied_count: 0, + + subreqs_list: [CPSC_SENPROJ] +} + +export const CPSC_CONFIG: DegreeConfiguration = { + reqs_list: [CPSC_CORE, CPSC_ELECTIVES, CPSC_SENIOR] +} \ No newline at end of file diff --git a/frontend/src/database/data-studentcourses.ts b/frontend/src/database/data-studentcourses.ts new file mode 100644 index 0000000..1fedb89 --- /dev/null +++ b/frontend/src/database/data-studentcourses.ts @@ -0,0 +1,11 @@ + +import { StudentCourse } from "@/types/type-user" +import { CPSC_201, CPSC_202, CPSC_223, CPSC_323 } from "./data-courses" + +export const SC_CPSC_201: StudentCourse = { term: 202403, status: "DA", result: "GRADE_PASS", course: CPSC_201 } +export const SC_CPSC_202: StudentCourse = { term: 202403, status: "DA", result: "GRADE_PASS", course: CPSC_202 } +export const SC_CPSC_223: StudentCourse = { term: 202501, status: "DA", result: "GRADE_PASS", course: CPSC_223 } +export const SC_CPSC_323: StudentCourse = { term: 202503, status: "MA", result: "IP", course: CPSC_323 } + + + diff --git a/frontend/src/database/data-user.ts b/frontend/src/database/data-user.ts index dd7f056..ea0c37b 100644 --- a/frontend/src/database/data-user.ts +++ b/frontend/src/database/data-user.ts @@ -1,16 +1,51 @@ import { User } from "./../types/type-user"; +import { SC_CPSC_201, SC_CPSC_223, SC_CPSC_323 } from "./data-studentcourses"; +import { CPSC_CONFIG } from "./data-cpsc"; + export const Ryan: User = { name: "Ryan", netID: "rgg32", - onboard: true, + onboard: false, FYP: { - studentSemesters: [ - { season: 202203, studentCourses: [{ term: 202203, status: "DA_COMPLETE", course: { codes: ["CPSC 223"], title: "Data Structures", credit: 1, dist: ["QR"], seasons: [] } }] }, - { season: 202301, studentCourses: [{ term: 202301, status: "MA_HYPOTHETICAL", course: { codes: ["CPSC 323"], title: "Systems Programming", credit: 1, dist: ["QR"], seasons: [] } }] }, + studentCourses: [SC_CPSC_201, SC_CPSC_223, SC_CPSC_323], + studentTermArrangement: { + first_year: [0, 202403, 202501], + sophomore: [0, 202503, 202601], + junior: [0, 202603, 202701], + senior: [0, 202703, 202801], + }, + languagePlacement: { + language: "Spanish", + level: 5, + }, + degreeDeclarations: [], + degreeConfigurations: [ + [CPSC_CONFIG], + [], + [], + [] ], - languageRequirement: "", + } +} + +export const NullUser: User = { + name: "", + netID: "", + onboard: false, + FYP: { + studentCourses: [], + studentTermArrangement: { + first_year: [], + sophomore: [], + junior: [], + senior: [], + }, + languagePlacement: { + language: "", + level: 0, + }, degreeDeclarations: [], degreeConfigurations: [ [ @@ -24,3 +59,6 @@ export const Ryan: User = { ], } } + + + diff --git a/frontend/src/middleware.ts b/frontend/src/middleware.ts new file mode 100644 index 0000000..d0737e3 --- /dev/null +++ b/frontend/src/middleware.ts @@ -0,0 +1,29 @@ + +import { NextRequest, NextResponse } from "next/server"; + +const protectedRoutes = ["/graduation", "/courses", "/majors"]; + +export default function middleware(req: NextRequest) { + const path = req.nextUrl.pathname; + + const sessionCookie = req.cookies.get("session")?.value; + const isLoggedIn = sessionCookie === "true"; + + if (protectedRoutes.includes(path) && !isLoggedIn) { + return NextResponse.redirect(new URL("/login", req.nextUrl)); + } + + if (path === "/login" && isLoggedIn) { + return NextResponse.redirect(new URL("/graduation", req.nextUrl)); + } + + if (path === "/") { + return NextResponse.redirect(new URL(isLoggedIn ? "/graduation" : "/login", req.nextUrl)); + } + + return NextResponse.next(); +} + +export const config = { + matcher: ["/((?!api|_next/static|_next/image|.*\\.png$).*)"], +}; diff --git a/frontend/src/pages/Courses/Courses.tsx b/frontend/src/pages/Courses/Courses.tsx deleted file mode 100644 index 0dacf99..0000000 --- a/frontend/src/pages/Courses/Courses.tsx +++ /dev/null @@ -1,142 +0,0 @@ - -import { useState, useEffect } from "react"; -import { Year } from "../../commons/types/TypeUser"; - -import Style from "./Courses.module.css"; - -import YearBox from "./year/YearBox"; -import nav_styles from "./../../navbar/NavBar.module.css"; -import logo from "./../../commons/images/ma_logo.png"; -import PageLinks from "./../../navbar/PageLinks"; - -import { User } from "../../commons/types/TypeUser"; -// import { StudentCourse } from "../../commons/types/TypeCourse"; - -import { yearTreeify } from "./CoursesUtils"; - -function NavBar() { - return ( -
-
- -
- -
- ); -} - -function Courses(props: { user: User, setUser: Function }){ - - const [yearTree, setYearTree] = useState([]); - const [renderedYears, setRenderedYears] = useState([]); - const [edit, setEdit] = useState(false); - - const updateEdit = () => { - setEdit(!edit); - }; - - useEffect(() => { - const transformedData = yearTreeify(props.user.FYP.studentCourses); - setYearTree(transformedData); - }, [props.user.FYP.studentCourses]); - - useEffect(() => { - const newRenderedYears = yearTree.map((year, index) => ( - - )); - setRenderedYears(newRenderedYears); - }, [edit, yearTree, props.setUser, props.user]); - - return( -
- -
- -
- {renderedYears} -
-
-
- ); -} - -export default Courses; - - - - -// export interface DisplaySetting { -// rating: boolean; -// workload: boolean; -// } - -// const defaultDisplaySetting = { rating: true, workload: true }; - -// function Settings(props: { -// displaySetting: DisplaySetting; -// updateDisplaySetting: Function; -// }) { -// const [isOpen, setIsOpen] = useState(false); -// const toggleDropdown = () => { -// setIsOpen(!isOpen); -// }; - -// const throwBack = (key: string) => { -// if (key === "rating") { -// const newSetting = { -// ...props.displaySetting, -// rating: !props.displaySetting.rating, -// }; -// props.updateDisplaySetting(newSetting); -// } else if (key === "workload") { -// const newSetting = { -// ...props.displaySetting, -// workload: !props.displaySetting.workload, -// }; -// props.updateDisplaySetting(newSetting); -// } -// }; - -// return ( -//
-// -// {isOpen && ( -//
-// -// -//
-// )} -//
-// ); -// } - - // const [displaySetting, setDisplaySetting] = useState(defaultDisplaySetting); - // const updateDisplaySetting = (newSetting: DisplaySetting) => { - // setDisplaySetting(newSetting); - // }; - // useEffect(() => {}, [displaySetting]); - - // yearTree \ No newline at end of file diff --git a/frontend/src/pages/Courses/CoursesUtils.ts b/frontend/src/pages/Courses/CoursesUtils.ts deleted file mode 100644 index cb1e679..0000000 --- a/frontend/src/pages/Courses/CoursesUtils.ts +++ /dev/null @@ -1,101 +0,0 @@ - -import { User, Year } from "../../commons/types/TypeUser"; -import { StudentCourse } from "../../commons/types/TypeCourse"; - -export const yearTreeify = (courses: StudentCourse[]): Year[] => { - const academicYears: { [key: number]: Year } = {}; - - courses.forEach(course => { - const year = Math.floor(course.term / 100); - const seasonCode = course.term % 100; - const academicYearKey = seasonCode === 3 ? year : year - 1; - - if (!academicYears[academicYearKey]) { - academicYears[academicYearKey] = { - grade: 0, - terms: [academicYearKey * 100 + 3, (academicYearKey + 1) * 100 + 1], - fall: [], - spring: [], - }; - } - - if (seasonCode === 3) { - academicYears[academicYearKey].fall.push(course); - } else { - academicYears[academicYearKey].spring.push(course); - } - }); - - const sortedYears = Object.keys(academicYears) - .map(key => parseInt(key)) - .sort((a, b) => a - b) - .map((key, idx) => { - academicYears[key].grade = idx + 1; - return academicYears[key]; - }); - - const lastYearKey = parseInt(Object.keys(academicYears).pop()!); - for (let i = sortedYears.length; i < 4; i++) { - const nextYearKey = lastYearKey + i - sortedYears.length + 1; - sortedYears.push({ - grade: sortedYears.length + 1, - terms: [nextYearKey * 100 + 3, (nextYearKey + 1) * 100 + 1], - fall: [], - spring: [], - }); - } - - return sortedYears; -}; - - - -export const xCheckMajorsAndSet = ( user: User, newCourse: StudentCourse, setUser: Function ): void => { - - // Update student courses - let updatedStudentCourses = user.FYP.studentCourses.map(existingCourse => { - if (existingCourse.course.codes.some(code => newCourse.course.codes.includes(code))) { - return newCourse; - } - return existingCourse; - }); - - // Check if newCourse was added to studentCourses - const courseExists = updatedStudentCourses.some(course => - course.course.codes.some(code => newCourse.course.codes.includes(code)) - ); - if (!courseExists) { - updatedStudentCourses.push(newCourse); - } - - // const updatedDegreeConfigurations = user.FYP.degreeConfigurations.map(configurationList => - // configurationList.map(configuration => { - // const updatedRequirements = configuration.degreeRequirements.map(requirement => { - // const updatedSubsections = requirement.subsections.map(subsection => ({ - // ...subsection, - // courses: subsection.courses.map(course => { - // if (course.course.codes.some(code => newCourse.course.codes.includes(code))) { - // return { ...course, term: newCourse.term, status: newCourse.status }; - // } - // return course; - // }) - // })); - // return { ...requirement, subsections: updatedSubsections }; - // }); - - // return { - // ...configuration, - // degreeRequirements: updatedRequirements - // }; - // }) - // ); - - // setUser({ - // ...user, - // FYP: { - // ...user.FYP, - // studentCourses: updatedStudentCourses, - // degreeConfigurations: updatedDegreeConfigurations - // } - // }); -}; diff --git a/frontend/src/pages/Courses/year/semester/course/CourseBox.tsx b/frontend/src/pages/Courses/year/semester/course/CourseBox.tsx deleted file mode 100644 index 2bb1dfe..0000000 --- a/frontend/src/pages/Courses/year/semester/course/CourseBox.tsx +++ /dev/null @@ -1,120 +0,0 @@ - -import Style from "./CourseBox.module.css"; -import "react-tooltip/dist/react-tooltip.css"; - -import img_fall from "./../../../../../commons/images/fall.png"; -import img_spring from "./../../../../../commons/images/spring.png"; -import DistributionsCircle from "./../../../../../commons/components/icons/DistributionsCircle" - -import { StudentCourse } from "../../../../../commons/types/TypeCourse"; -import { User } from "../../../../../commons/types/TypeUser"; -// import { useModal } from "../../../hooks/modalContext"; - -function RemoveCourse(props: { SC: StudentCourse, user: User, setUser: Function }){ - - const remove = () => { - // const updatedStudentCourses = props.user.FYP.studentCourses.filter( - // (course) => course.course.title !== props.SC.course.title || course.term !== props.SC.term - // ); - - const updatedDegreeConfigurations = props.user.FYP.degreeConfigurations.map((configurationList) => - configurationList.map((configuration) => { - // const updatedRequirements = configuration.degreeRequirements.map((requirement) => { - // const updatedSubsections = requirement.subsections.map((subsection) => { - // const updatedCourses = subsection.courses.filter( - // (course) => course.course.title !== props.SC.course.title - // ); - // return { ...subsection, courses: updatedCourses }; - // }); - - // return { ...requirement, subsections: updatedSubsections }; - // }); - - const newCodesAdded = configuration.codesAdded.filter( - (code) => !props.SC.course.codes.includes(code) - ); - - return { - ...configuration, - codesAdded: newCodesAdded - }; - }) - ); - - const updatedUser = { - ...props.user, - FYP: { - ...props.user.FYP, - degreeConfigurations: updatedDegreeConfigurations - } - }; - - props.setUser(updatedUser); - }; - - return( -
- -
- ) -} - -function CourseBox(props: {edit: boolean, SC: StudentCourse, user: User, setUser: Function }) { - - // const { setModalOpen } = useModal(); - // function openModal() { - // setModalOpen(props.SC.course) - // } - - const { status, term, course } = props.SC; - - const renderMark = () => { - if(status === "DA_COMPLETE" || status === "DA_PROSPECT"){ - return ( -
- ✓ -
- ); - }else if(status === "MA_HYPOTHETICAL" || "MA_VALID"){ - const mark = (status === "MA_HYPOTHETICAL") ? "⚠" : "☑"; - return ( -
- {props.edit && } -
- {mark} -
-
- ); - } - return
; - }; - - const getBackgroundColor = () => (status === "DA_COMPLETE" ? "#E1E9F8" : "#F5F5F5"); - const getSeasonImage = () => (String(term).endsWith("3") ? img_fall : img_spring); - - return ( -
- {/* onClick={openModal} */} - -
- {renderMark()} - -
-
- {course.codes[0]} -
-
- {course.title} -
-
-
-
-
- -
-
-
- ); -} - -export default CourseBox; diff --git a/frontend/src/types/type-program.ts b/frontend/src/types/type-program.ts index 4aae385..2eda0b0 100644 --- a/frontend/src/types/type-program.ts +++ b/frontend/src/types/type-program.ts @@ -1,5 +1,11 @@ -import { StudentCourse } from "./type-user"; +import { Course, StudentCourse } from "./type-user"; + +export interface StudentDegree { + status: string; // DA | ADD | PIN + programIndex: number; + degreeIndex: number; +} interface DUS { name: string; @@ -26,29 +32,54 @@ export interface DegreeMetadata { wesbiteLink: string; } +// \BEGIN{MAJOR MAGIC} +export interface ElectiveRange { + dept: string; + min_code: number; + max_code: number; +} +export type SubreqElectiveRange = ElectiveRange| null; +// subreq: 1 | set options +export interface DegreeSubrequirement { + subreq_type_id: number; + subreq_name: string; + subreq_desc: string; + courses_required: number; + courses_options: (Course | null)[]; + courses_elective_range: SubreqElectiveRange; + courses_any_bool: boolean; - -// \BEGIN{MAJOR MAGIC} - -interface DegreeRequirementsSubsection { - name?: string; - description?: string; - flexible: boolean; - courses: StudentCourse[]; + student_courses_satisfying: StudentCourse[]; } -interface DegreeRequirement { - name: string; - description?: string; - subsections: DegreeRequirementsSubsection[]; +export interface DegreeRequirement { + req_type_id: number; + req_name: string; + req_desc: string; + + courses_required_count: number; + courses_satisfied_count: number; + + subreqs_list: DegreeSubrequirement[]; } +// export interface DegreeRequirementTypeTwo { +// req_type_id: number; +// req_name: string; +// req_desc: string; + +// checkbox_bool: boolean; +// user_courses_satisfying: Course[]; +// } + +// export type DegreeRequirement = DegreeRequirementTypeOne | DegreeRequirementTypeTwo; + export interface DegreeConfiguration { - degreeRequirements: DegreeRequirement[]; + reqs_list: DegreeRequirement[]; } export interface Degree { @@ -57,15 +88,3 @@ export interface Degree { } // \END{MAJOR MAGIC} - - - - - - - -export interface StudentDegree { - status: string; // DA | ADD | PIN - programIndex: number; - degreeIndex: number; -} diff --git a/frontend/src/types/type-user.ts b/frontend/src/types/type-user.ts index 1fd1884..ce21d91 100644 --- a/frontend/src/types/type-user.ts +++ b/frontend/src/types/type-user.ts @@ -1,6 +1,11 @@ import { DegreeConfiguration, StudentDegree } from "./type-program"; +export interface LanguagePlacement { + language: string; + level: number; +} + export interface Course { codes: string[]; // ["FREN 403", "HUMS 409"] title: string; // "Proust Interpretations: Reading Remembrance of Things Past" @@ -12,17 +17,31 @@ export interface Course { export interface StudentCourse { course: Course; term: number; // 202401 - status: string; // "DA_COMPLETE" | "DA_PROSPECT" | "MA_VALID" | "MA_HYPOTHETICAL" + status: string; // "DA" or "MA" + result: string; // "IP" or "GRADE_PASS" or "GRADE_FAIL" or "CR" or "W" } export interface StudentSemester { - season: number; + term: number; studentCourses: StudentCourse[]; } +export interface StudentYear { + grade: string; // "First-Year" | "Sophomore" | "Junior" | "Senior" + studentSemesters: StudentSemester[]; +} + +export interface StudentTermArrangement { + first_year: number[]; + sophomore: number[]; + junior: number[]; + senior: number[]; +} + export interface FYP { - languageRequirement: string; - studentSemesters: StudentSemester[] + languagePlacement: LanguagePlacement; + studentCourses: StudentCourse[]; + studentTermArrangement: StudentTermArrangement; degreeConfigurations: DegreeConfiguration[][]; degreeDeclarations: StudentDegree[]; } diff --git a/frontend/src/utils/CourseDisplay.module.css b/frontend/src/utils/CourseDisplay.module.css new file mode 100644 index 0000000..c44cf5f --- /dev/null +++ b/frontend/src/utils/CourseDisplay.module.css @@ -0,0 +1,66 @@ + +.row { + display: flex; + flex-direction: row; +} + + +.RemoveButton { + display: flex; + justify-content: center; + align-items: center; + width: 16px; /* adjust size as needed */ + height: 16px; + border-radius: 50%; + background-color: #ededed; + cursor: pointer; + margin-right: 5px; + /* optional border for better visibility */ + /* border: 1px solid #C0C0C0; */ +} + +.RemoveButton:hover { + background-color: #D3D3D3; /* slightly darker grey on hover */ +} + +.Checkmark { + justify-content: center; + text-align: center; + + font-weight: 550; + margin-right: 2px; +} + +.courseBox { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + + width: 420px; + height: 36px; + + border-radius: 16px; + margin-bottom: 5px; + + padding-left: 10px; + padding-right: 10px; + + background-color: #F5F5F5; + transition: filter 0.4s ease; +} + +.CourseCode { + font-size: 12px; + font-weight: 500; +} + +.CourseTitle { + font-size: 8px; + font-weight: 500; +} + +.SeasonImage { + margin-top: 3px; + margin-right: 6px; +} diff --git a/frontend/src/utils/CourseDisplay.tsx b/frontend/src/utils/CourseDisplay.tsx new file mode 100644 index 0000000..b09f91d --- /dev/null +++ b/frontend/src/utils/CourseDisplay.tsx @@ -0,0 +1,83 @@ + +import Style from "./CourseDisplay.module.css"; +import Image from "next/image"; +import { User, StudentCourse } from "@/types/type-user"; + +export function TransformTermNumber(term: number | string): string { + const termStr = term.toString(); + if (termStr.length !== 6) { + return "Invalid term format"; + } + + const year = termStr.substring(0, 4); + const seasonCode = termStr.substring(4, 6); + + let season = ""; + switch (seasonCode) { + case "01": + season = "Spring"; + break; + case "02": + season = "Summer"; + break; + case "03": + season = "Fall"; + break; + default: + return "Invalid term format"; + } + + return `${season} ${year}`; +} + +export function IsTermActive(term: number): boolean { + const currentYearMonth = new Date().toISOString().slice(0, 7); // "YYYY-MM" + + const termStr = String(term); + if (termStr.length !== 6) return true; // Treat invalid term formats as ended + + const year = termStr.slice(0, 4); // Extract year (YYYY) + const season = termStr.slice(4, 6); // Extract season (01, 02, or 03) + + // Define the cutoff month for each season + const seasonCutoff: { [key: string]: string } = { + "01": `${year}-06`, // Spring ends in June + "02": `${year}-09`, // Summer ends in September + "03": `${Number(year) + 1}-01`, // Fall ends in January of next year + }; + + return currentYearMonth < seasonCutoff[season]; +} + +export function GetCourseColor(term: number): string { + return IsTermActive(term) ? "#F5F5F5" : "#E1E9F8"; +} + +export function RenderMark(props: { status: string }) +{ + if(props.status === "DA"){ + return( +
+ ✓ +
+ ); + }else + if(props.status === "MA"){ + return( +
+ ⚠ +
+ ); + } + return
; +} + +export function SeasonIcon(props: { studentCourse: StudentCourse }) +{ + const getSeasonImage = () => (String(props.studentCourse.term).endsWith("3") ? "/fall.svg" : "/spring.svg"); + return( +
+ +
+ ) +} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index c133409..cbe75e2 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -22,6 +22,6 @@ "@/*": ["./src/*"] } }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "src/middleware.ts"], "exclude": ["node_modules"] } diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 5d0f8a1..0000000 --- a/package-lock.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "MajorAudit", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "dependencies": { - "jquery": "^3.7.1" - }, - "devDependencies": { - "@types/jquery": "^3.5.29" - } - }, - "node_modules/@types/jquery": { - "version": "3.5.29", - "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.29.tgz", - "integrity": "sha512-oXQQC9X9MOPRrMhPHHOsXqeQDnWeCDT3PelUIg/Oy8FAbzSZtFHRjc7IpbfFVmpLtJ+UOoywpRsuO5Jxjybyeg==", - "dev": true, - "dependencies": { - "@types/sizzle": "*" - } - }, - "node_modules/@types/sizzle": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.8.tgz", - "integrity": "sha512-0vWLNK2D5MT9dg0iOo8GlKguPAU02QjmZitPEsXRuJXU/OGIOt9vT9Fc26wtYuavLxtO45v9PGleoL9Z0k1LHg==", - "dev": true - }, - "node_modules/jquery": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", - "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==" - } - } -} diff --git a/package.json b/package.json deleted file mode 100644 index 85d3341..0000000 --- a/package.json +++ /dev/null @@ -1,9 +0,0 @@ - -{ - "dependencies": { - "jquery": "^3.7.1" - }, - "devDependencies": { - "@types/jquery": "^3.5.29" - } -}