diff --git a/backend/clone.py b/backend/clone.py index c0f22bc..80c0221 100644 --- a/backend/clone.py +++ b/backend/clone.py @@ -6,13 +6,12 @@ { "metadata": { "name": "Computer Science", - "abbreviation": "CPSC", "degreeType": "BACH_ART", "stats": { - "courses": 10, + "courses": 12, "rating": 0, "workload": 0, - "type": "QR" + "type": "So" }, "students": 0, "about": ( @@ -42,8 +41,8 @@ "flexible": True, "courses": [ { - "status": "DA_COMPLETE", - "term": 202203, + "status": "NA", + "term": 0, "course": { "codes": ["CPSC 201"], "title": "Introduction to Computer Science", @@ -88,8 +87,8 @@ "flexible": False, "courses": [ { - "status": "DA_COMPLETE", - "term": 202301, + "status": "NA", + "term": 0, "course": { "codes": ["CPSC 223"], "title": "Data Structures", @@ -105,8 +104,8 @@ "flexible": False, "courses": [ { - "status": "DA_COMPLETE", - "term": 202401, + "status": "NA", + "term": 0, "course": { "codes": ["CPSC 323"], "title": "Introduction to Systems Programming and Computer Organization", @@ -156,3 +155,112 @@ } ] } + + + +ECON_Program = { + "name": "Economics", + "abbreviation": "ECON", + "degrees": [ + { + "metadata": { + "name": "Economics", + "degreeType": "BACH_ART", + "stats": { + "courses": 10, + "rating": 0, + "workload": 0, + "type": "So" + }, + "students": 0, + "about": "Economics is much broader than the study of recessions and inflation or stocks and bonds. Economists study decision making and incentives such as how taxes create incentives for labor market and savings behavior. Many current public policy debates concern questions of economics, including causes and consequences of inequality and gender and racial wage gaps; how to address poverty; the impact of immigration and trade on the well-being of a country’s citizens; the cause of the Great Recession; and how to predict future downturns.", + "dus": { + "name": "Giovanni Maggi", + "address": "115 Prospect St., Rosenkranz Hall, Room 334", + "email": "cpsc.yale.edu" + }, + "catologLink": "https://catalog.yale.edu/ycps/subjects-of-instruction/economics/", + "wesbiteLink": "economics.yale.edu/undergraduate-program" + }, + "codesCore": ["ECON 490"], + "codesAdded": [], + "requirements": [ + { + "name": "ELECTIVES", + "description": "Usually, courses with course numbers above 200 work for this requirement.", + "subsections": [ + { + "flexible": True, + "courses": [] + } + ] + }, + { + "name": "SENIOR REQUIREMENT", + "subsections": [ + { + "flexible": False, + "courses": [ + { + "status": "NA", + "term": 0, + "course": { + "codes": ["ECON 490"], + "title": "Project", + "credit": 1, + "areas": [], + "skills": ["QR"], + "seasons": ["Fall", "Spring"] + } + } + ] + } + ] + } + ] + } + ] +} + + +HIST_Program = { + "name": "History", + "abbreviation": "HIST", + "degrees": [ + { + "metadata": { + "name": "History", + "degreeType": "BACH_ART", + "stats": { + "courses": 10, + "rating": 0, + "workload": 0, + "type": "So" + }, + "students": 0, + "about": "Economics is much broader than the study of recessions and inflation or stocks and bonds. Economists study decision making and incentives such as how taxes create incentives for labor market and savings behavior. Many current public policy debates concern questions of economics, including causes and consequences of inequality and gender and racial wage gaps; how to address poverty; the impact of immigration and trade on the well-being of a country’s citizens; the cause of the Great Recession; and how to predict future downturns.", + "dus": { + "name": "Giovanni Maggi", + "address": "115 Prospect St., Rosenkranz Hall, Room 334", + "email": "cpsc.yale.edu" + }, + "catologLink": "https://catalog.yale.edu/ycps/subjects-of-instruction/economics/", + "wesbiteLink": "economics.yale.edu/undergraduate-program" + }, + "codesCore": [], + "codesAdded": [], + "requirements": [ + { + "name": "ELECTIVES", + "description": "Usually, courses with course numbers above 200 work for this requirement.", + "subsections": [ + { + "flexible": True, + "courses": [] + } + ] + } + ] + } + ] +} diff --git a/backend/ct-script.py b/backend/ct-script.py new file mode 100644 index 0000000..030df76 --- /dev/null +++ b/backend/ct-script.py @@ -0,0 +1,85 @@ +import http.client +import json + +def get_ct_courses(): + cookies = { + 'session': 'enter_session_here', + 'session.sig': 'enter_session_sig_here' + } + + conn = http.client.HTTPSConnection("api.coursetable.com") + headers = { + 'Cookie': f'session={cookies["session"]}; session.sig={cookies["session.sig"]}' + } + + conn.request("GET", "/api/catalog/public/202301", headers=headers) + response = conn.getresponse() + data = response.read() + course_data = json.loads(data.decode("utf-8")) + unique_courses = singalize(course_data) + transformed_data = simplify(unique_courses) + + # Splitting the data into four parts + quarter_size = len(transformed_data) // 4 + first_part = transformed_data[:quarter_size] + second_part = transformed_data[quarter_size:2*quarter_size] + third_part = transformed_data[2*quarter_size:3*quarter_size] + fourth_part = transformed_data[3*quarter_size:] + + # Writing each part to separate files without indents and extra whitespace + with open("results1.txt", "w") as f: + json.dump(first_part, f, separators=(",", ":")) + + with open("results2.txt", "w") as f: + json.dump(second_part, f, separators=(",", ":")) + + with open("results3.txt", "w") as f: + json.dump(third_part, f, separators=(",", ":")) + + with open("results4.txt", "w") as f: + json.dump(fourth_part, f, separators=(",", ":")) + + conn.close() + + return transformed_data, 200 + + +def singalize(courses): + record = set() + unique = [] + for obj in courses: + code = obj.get("course_code") + if code not in record: + record.add(code) + unique.append(obj) + return unique + +def simplify(courses): + dict = {} + + for obj in courses: + course_code = obj.get("course_code") + listings = obj["course"].get("listings", []) + + found_key = None + for code in [listing["course_code"] for listing in listings]: + if code in dict: + found_key = code + break + + if found_key: + dict[found_key]["c"].append(course_code) + else: + dict[course_code] = { + "c": [course_code], + "t": obj["course"].get("title"), + "r": obj["course"].get("credits"), + "d": obj["course"].get("skills", []) + obj["course"].get("areas", []), + } + + transform = list(dict.values()) + return transform + + +if __name__ == "__main__": + get_ct_courses() diff --git a/backend/main.py b/backend/main.py index 962746d..1c74d48 100644 --- a/backend/main.py +++ b/backend/main.py @@ -182,36 +182,45 @@ def getUser(): return make_response(response_body, 200) -@app.get("/getCTCourses") -def getCTCourses(): - key = request.args.get('key') - if not key: - result = {"Error": "Missing Param"} - status_code = 400 - - cookies = { - 'session': 'enter_session_here', - 'session.sig': 'enter_session_sig_here' - } - url = f"https://api.coursetable.com/api/catalog/public/{key}" - - try: - response = requests.get(url, cookies=cookies) - course_data = response.json() - transformed_data = simplify_CT_courses(course_data) - result = transformed_data - status_code = 200 - except requests.exceptions.RequestException as e: - result = {"Error": str(e)} - status_code = 500 - - # output_file = "output.json" - # with open(output_file, "w") as f: - # json.dump(result, f, indent=2) - # print(f"Result -> {output_file}") - - return jsonify(result), status_code - +@app.get("/getCatalog") +def getCatalog(): + key = request.args.get('key') + if not key: + print("Error: Missing 'key' parameter in request.") + return jsonify({"Error": "Missing Param"}), 400 + + try: + collections = db.collections() + root_collection_names = [collection.id for collection in collections] + + if 'Catalogs' not in root_collection_names: + return jsonify({"Error": "Catalogs Nonexistent"}), 404 + + print(f"Key: {key}") + slices_ref = db.collection("Catalogs").document(key).collection("Slices") + slices_docs = list(slices_ref.stream()) + + print(f"# Slices: {len(slices_docs)}") + + combined_list = [] + for slice_doc in slices_docs: + slice_data = slice_doc.to_dict().get('C', "") + + try: + parsed_data = json.loads(slice_data) + combined_list.extend(parsed_data) + except json.JSONDecodeError as e: + return jsonify({"Error": f"Invalid JSON Document {slice_doc.id}"}), 500 + + if not combined_list: + return jsonify({"Error": "Data Not Found"}), 404 + + return jsonify(combined_list), 200 + + except Exception as e: + print(f"Error Firestore: {str(e)}") + return jsonify({"Error": str(e)}), 500 + # * * * POST * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * diff --git a/backend/program.py b/backend/program.py index 8f38243..5847831 100644 --- a/backend/program.py +++ b/backend/program.py @@ -1,8 +1,8 @@ from copy import deepcopy -from clone import CPSC_Program +from clone import CPSC_Program, ECON_Program, HIST_Program -all_programs = [CPSC_Program] +all_programs = [CPSC_Program, ECON_Program, HIST_Program] def clone_programs(studentCourses): # Extract course codes from student courses @@ -21,7 +21,6 @@ def clone_programs(studentCourses): for course in subsection['courses']: for studentCourse in studentCourses: if set(course['course']['codes']).intersection(studentCourse['course']['codes']): - # Update course's term and status if there's a match course['term'] = studentCourse['term'] course['status'] = studentCourse['status'] break # Break after updating to avoid multiple updates diff --git a/docs/README.md b/docs/README.md index 9c9e326..0945d30 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,53 +1,132 @@ # MajorAudit -# Repository Layout +## 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 +- `/scrapers`: Chrome extensions for web scraping. +- `/docs`: Documentation. + +## Local Development Environment -# Local DevEnv's We're working fullstack. -* Base Firebase -1. In root directory, run: npm install -g firebase-tools -* Backend Venv -2. Update to python3.12 -3. Navigate to backend -4. Run: python3.12 -m venv venv -5. Run: source venv/bin/activate -6. Run: pip install -r requirements.txt -7. Run: deactivate -* Secrets -8. Make a "secrets" directory in backend -9. Go to Firebase Console -10. Select majoraudit -11. Click on the gear icon next to Project Overview and select Project Settings -12. Select Service Accounts -13. Generate a new Node.js private key -14. Move the file to your secrets directory -15. Update the cred = credentials.Certificate(r'...') line in main.py to path to that file -* Go -16. In frontend directory, run: npm run build -16. In root or frontend directory, run: firebase emulators:start -17. Troubleshoot any errors -* Notes -- Anytime you change the frontend, you need to cut the emulators and rebuild. They only host the most recent build. -- Anyime you change the webscraper, you need to remove and reconfigure the extension in chrome. -- You can change backend code as you go. Whenever you save, the emulators will automatically restart. -* Strategies -- If focused purely frontend development: - 1. Change the useState(auth) value in App.tsx to true - 2. Change the initLocalStorage() method in Graduation.tsx to yield data from MockStudent rather than the getData() API - 3. In frontend directory, run: npm start - 4. The frontend will now update as you go. - -# Contributing -1. Create a branch for your feature. Likely, `git checkout -b /` -2. _make changes_ -3. Create some commits and push your changes to the origin. -4. Create a pull request and add a few reviewers. In the pull request, be sure to reference any relevant issue numbers. -5. Once the pull request has been approved, merge it into the master branch. - -# Roadmap -We use GitHub issues to track bugs and feature requests: [https://github.com/YaleComputerSociety/MajorAudit/issues](https://github.com/YaleComputerSociety/MajorAudit/issues). -We use GitHub projects to manage everything and do planning: [https://github.com/orgs/YaleComputerSociety/projects/2/](https://github.com/orgs/YaleComputerSociety/projects/2/). + +### 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/). diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index c5b7a37..a096e26 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -15,52 +15,52 @@ import Majors from "./pages/Majors/Majors"; import { getAuth, getUser, syncUser } from "./api/api"; import { AuthState, nullAuthState, User, nullUser } from "./commons/types/TypeUser"; -// import { Ryan } from "./commons/mock/MockStudent"; +import { Ryan } from "./commons/mock/MockStudent"; function App(){ - const [auth, setAuth] = useState(nullAuthState); - // const [auth, setAuth] = useState({ loggedIn: true, onboard: true }); + // const [auth, setAuth] = useState(nullAuthState); + const [auth, setAuth] = useState({ loggedIn: true, onboard: true }); const checkAuth = async () => { const response = await getAuth(); - console.log("checkAuth() -> API: getAuth() -> ", response); + // console.log("checkAuth() -> API: getAuth() -> ", response); setAuth({ loggedIn: response.loggedIn, onboard: response.onboard, }); }; - const [user, setUser] = useState(nullUser); - // const [user, setUser] = useState(Ryan); + // const [user, setUser] = useState(nullUser); + const [user, setUser] = useState(Ryan); const initUser = async () => { const response = await getUser(); - console.log("initUser() -> API: getUser() -> ", response) + // console.log("initUser() -> API: getUser() -> ", response) setUser({ netID: response.netID, onboard: response.onboard, name: response.name, - degrees: response.degrees, + studentDegrees: response.studentDegrees, studentCourses: response.studentCourses, programs: response.programs, language: response.language }); }; - useEffect(() => { - checkAuth(); - }, []); + // useEffect(() => { + // checkAuth(); + // }, []); - useEffect(() => { - if(auth.loggedIn && auth.onboard){ - initUser(); - } - }, [auth]); + // useEffect(() => { + // if(auth.loggedIn && auth.onboard){ + // initUser(); + // } + // }, [auth]); - useEffect(() => { - if(auth.loggedIn && auth.onboard){ - syncUser(user); - } - }, [user]); + // useEffect(() => { + // if(auth.loggedIn && auth.onboard){ + // syncUser(user); + // } + // }, [user]); const ProtectedRoute = (element: JSX.Element) => { if(!auth.loggedIn){ @@ -79,9 +79,9 @@ function App(){ )}/> : (!auth.onboard ? : )}/> : }/> - )}/> - )}/> - )}/> + )}/> + )}/> + )}/> diff --git a/frontend/src/api/api.tsx b/frontend/src/api/api.tsx index ca05085..1f07e4c 100644 --- a/frontend/src/api/api.tsx +++ b/frontend/src/api/api.tsx @@ -33,11 +33,10 @@ export const getUser = () => { }; - -export const getCTCourses = (key: string): Promise => { +export const getCatalog = (key: string): Promise => { return new Promise((resolve, reject) => { $.ajax({ - url: `http://127.0.0.1:5001/majoraudit/us-central1/functions/getCTCourses?key=${key}`, + url: `http://127.0.0.1:5001/majoraudit/us-central1/functions/getCatalog?key=${key}`, method: "GET", xhrFields: { withCredentials: true } }).done((data: any) => { diff --git a/frontend/src/commons/components/icons/CourseIcon.tsx b/frontend/src/commons/components/icons/CourseIcon.tsx index b9eeef6..5f0a526 100644 --- a/frontend/src/commons/components/icons/CourseIcon.tsx +++ b/frontend/src/commons/components/icons/CourseIcon.tsx @@ -1,7 +1,7 @@ import React from "react"; import styles from "./CourseIcon.module.css"; import "react-tooltip/dist/react-tooltip.css"; -import { Course, StudentCourse, AmbiCourse } from "../../types/TypeCourse"; +import { StudentCourse } from "../../types/TypeCourse"; import img_fall from "./../../images/fall.png"; import img_spring from "./../../images/spring.png"; @@ -66,7 +66,7 @@ export function StudentCourseIcon(props: { studentCourse: StudentCourse, utility return
{mark}
; }; - const dist = [...(props.studentCourse.course.areas || []), ...(props.studentCourse.course.skills || [])]; + const dist = props.studentCourse.course.dist || []; return (
; // ["FREN 403", "HUMS 409"] title: string; // "Proust Interpretations: Reading Remembrance of Things Past" credit: number // 1 - areas: Array; // ["Hu"] - skills: Array // ["WR"] + dist: Array; // ["Hu"] # Combine pt. 1 seasons: Array; // ["Spring"] # Figure This Out } @@ -17,4 +16,14 @@ export interface StudentCourse { status: string; // "DA_COMPLETE" | "DA_PROSPECT" | "MA_VALID" | "MA_HYPOTHETICAL" } -export type AmbiCourse = Course | StudentCourse; +export interface AddCourseDisplay { + active: boolean; + dropVis: boolean; +} + +export const nullAddCourseDisplay: AddCourseDisplay = { + active: false, + dropVis: false, +} + + diff --git a/frontend/src/commons/types/TypeUser.ts b/frontend/src/commons/types/TypeUser.ts index 9a5f05b..f1714d6 100644 --- a/frontend/src/commons/types/TypeUser.ts +++ b/frontend/src/commons/types/TypeUser.ts @@ -9,11 +9,17 @@ export interface Year { spring: Array; } +export interface StudentDegree { + status: string; // DA | ADD | PIN + programIndex: number; + degreeIndex: number; +} + export interface User { netID: string; onboard: boolean; name: string; - degrees: string[]; + studentDegrees: StudentDegree[]; studentCourses: StudentCourse[]; programs: Program[]; language: string; @@ -23,7 +29,7 @@ export const nullUser: User = { netID: "", onboard: false, name: "", - degrees: [], + studentDegrees: [], studentCourses: [], programs: [], language: "", diff --git a/frontend/src/navbar/NavBar.module.css b/frontend/src/navbar/NavBar.module.css index f3386cb..9356693 100644 --- a/frontend/src/navbar/NavBar.module.css +++ b/frontend/src/navbar/NavBar.module.css @@ -3,6 +3,11 @@ flex-direction: row; } +.Row { + display: flex; + flex-direction: row; +} + .NavBar { position: fixed; z-index: 2; @@ -39,3 +44,10 @@ margin-left: 10px; transition: color 0.3s; } + +.Logo { + width: 150px; + height: auto; + margin-right: 10px; + margin-left: 20px; +} \ No newline at end of file diff --git a/frontend/src/navbar/NavBar.tsx b/frontend/src/navbar/NavBar.tsx new file mode 100644 index 0000000..42975ce --- /dev/null +++ b/frontend/src/navbar/NavBar.tsx @@ -0,0 +1,19 @@ + +import Style from "./NavBar.module.css" +import LOGO from "./../commons/images/ma_logo.png"; + +import PageLinks from "./PageLinks"; + +function Bar(props: { utility?: React.ReactNode }) { + return ( +
+
+ + {props.utility} +
+ +
+ ); +} + +export default Bar; diff --git a/frontend/src/navbar/PageLinks.tsx b/frontend/src/navbar/PageLinks.tsx index 50258dd..27ebf64 100644 --- a/frontend/src/navbar/PageLinks.tsx +++ b/frontend/src/navbar/PageLinks.tsx @@ -1,37 +1,23 @@ + import React from "react"; import styles from "./NavBar.module.css"; import { NavLink } from "react-router-dom"; function PageLinks() { - return ( + return(
- - isActive ? styles.activeLink : styles.dormantLink - } - > + isActive ? styles.activeLink : styles.dormantLink}> Graduation - - isActive ? styles.activeLink : styles.dormantLink - } - > + isActive ? styles.activeLink : styles.dormantLink}> Courses - - isActive ? styles.activeLink : styles.dormantLink - } - > + isActive ? styles.activeLink : styles.dormantLink}> Majors
); } -export default PageLinks; \ No newline at end of file +export default PageLinks; diff --git a/frontend/src/navbar/account/MeDropdown.tsx b/frontend/src/navbar/account/MeDropdown.tsx index a2f4ba7..7fab458 100644 --- a/frontend/src/navbar/account/MeDropdown.tsx +++ b/frontend/src/navbar/account/MeDropdown.tsx @@ -12,7 +12,7 @@ import { scrollToTop, useComponentVisible, } from "../../commons/utilities/display"; -import { SurfaceComponent, TextComponent, HoverText } from "../Typography"; +import { SurfaceComponent, TextComponent, HoverText } from "../misc/Typography"; function DropdownItem({ icon: Icon, diff --git a/frontend/src/navbar/InfoButton.module.css b/frontend/src/navbar/misc/InfoButton.module.css similarity index 100% rename from frontend/src/navbar/InfoButton.module.css rename to frontend/src/navbar/misc/InfoButton.module.css diff --git a/frontend/src/navbar/InfoButton.tsx b/frontend/src/navbar/misc/InfoButton.tsx similarity index 100% rename from frontend/src/navbar/InfoButton.tsx rename to frontend/src/navbar/misc/InfoButton.tsx diff --git a/frontend/src/navbar/Typography.module.css b/frontend/src/navbar/misc/Typography.module.css similarity index 100% rename from frontend/src/navbar/Typography.module.css rename to frontend/src/navbar/misc/Typography.module.css diff --git a/frontend/src/navbar/Typography.tsx b/frontend/src/navbar/misc/Typography.tsx similarity index 100% rename from frontend/src/navbar/Typography.tsx rename to frontend/src/navbar/misc/Typography.tsx diff --git a/frontend/src/pages/Courses/Courses.module.css b/frontend/src/pages/Courses/Courses.module.css index bf8e2fa..d8d8364 100644 --- a/frontend/src/pages/Courses/Courses.module.css +++ b/frontend/src/pages/Courses/Courses.module.css @@ -1,20 +1,11 @@ @import url('https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300..800;1,300..800&display=swap'); -/* UTIL */ - -.row { - display: flex; - flex-direction: row; -} - -.column { +.Column { display: flex; flex-direction: column; } -/* CONTAIN */ - .CoursesPage { position: absolute; top: 75px; @@ -29,7 +20,7 @@ padding-bottom: 200px; } -.AddCourseButton { +.EditButton { position: fixed; top: 95px; @@ -50,356 +41,3 @@ z-index: 2; } - -.AddCourseButton:active { - background-color: #9cccff; -} - -.AddCourseMenuDormant { - position: fixed; - - top: 95px; - left: 20px; - - width: 30px; - height: 30px; - - border-radius: 50%; - transform: scale(0); - transition: transform 0.5s, width 0.5s, height 0.5s, border-radius 0.5s; - background-color: white; - /* border: 1px solid #61ADFE; */ - box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.125); - z-index: 1; -} - -.AddCourseMenuActive { - top: 135px; - - width: 315px; - height: 700px; - - border-radius: 15px; - transform: scale(1); - - /* border: 1px solid blue; */ -} - -/* COURSES */ - -.addCourseButton { - display: flex; - justify-content: center; /* Center content horizontally */ - align-items: center; /* Center content vertically */ - - width: 36px; - height: 36px; - - border-radius: 50%; /* Use 50% for a perfect circle */ - margin-bottom: 5px; - - background-color: #F5F5F5; - - cursor: pointer; /* Change cursor on hover */ -} - -.addCourseButton:hover, -.addCourseButton:active { - background-color: #E0E0E0; /* Lighter color when hovered or clicked */ -} - - -.courseBox { - display: flex; - flex-direction: row; - justify-content: space-between; - align-items: center; - - width: 425px; - height: 36px; - - border-radius: 16px; - margin-bottom: 5px; - - padding-left: 10px; - padding-right: 10px; - - background-color: #F5F5F5; - transition: filter 0.4s ease; -} - -.courseBox:hover { - cursor: pointer; - filter: brightness(95%); -} - -.checkmark { - justify-content: center; - text-align: center; - - font-weight: 550; - margin-right: 2px; -} - -/* top top */ - -.Grade { - font-weight: 600; - font-size: 25px; - margin-right: 10px; -} - - - - -.yearComponent { - display: flex; - flex-direction: column; - margin-bottom: 20px; - transition: transform 0.5s ease, height 0.5s ease, width 0.5s ease; - } - - - - - - - - - - - - - - - - - - - -/* sem meta */ - -.MetadataColumn { - font-size:small; - display: flex; - flex-direction: column; - margin-right: 4px; -} - -.MetadataHeading { - color: #727272; - margin-bottom: 2px; -} - -.evaluateBox { - display: inline-flex; - align-items: center; - justify-content: center; - - width: max-content; - height: 20px; - padding: 0 6px; - - font-size: 11px; - font-weight: 510; - - border-radius: 4px; - color: #5aac08; - background-color: #D9F4CF; -} - -.distBox { - display: inline-flex; - align-items: center; - justify-content: center; - - width: max-content; - height: 20px; - padding: 0 6px; - - font-size: 12px; - font-weight: 510; - - border-radius: 4px; -} - -.countBox { - display: inline-flex; - align-items: center; - justify-content: center; - margin-right: 10px; - - width: max-content; - height: 18px; - padding: 0 6px; - - font-size: 12px; - font-weight: 500; - - border: 1px solid grey; - border-radius: 4px; - color: black; -} - - - -.optionsButton { - background-color: white; - border: none; - border-radius: 8px; - color: #598FF4; - padding-right: 10px; - padding-left: 10px; - height: 30px; - font-size: 18px; - cursor: pointer; -} - -.optionsButton:hover { - background-color: #fafafa; /* Grey color */ -} - -.optionsChoice { - background-color: white; - border: none; - border-radius: 8px; - color: black; - padding-right: 10px; - padding-left: 10px; - height: 20px; - font-size: 12px; - cursor: pointer; - align-content: center; - text-align: center; -} - -.optionsChoice:hover { - background-color: #fafafa; /* Grey color */ -} - -.activeButton { - background-color: #f0f0f0 -} - - - - - - - - -.CodeSearch { - background-color: white; /* Very light grey background */ - border: grey; - height: 22px; - width: 100px; - padding-left: 10px; /* Optional: adds padding inside the input */ - margin-left: 10px; /* Adds space between the minus button and input */ - outline: none; /* Ensure no outline when focused */ - font-size: 12px; /* Set font size to 12px */ - font-weight: 500; /* Set font weight to 500 */ - border-radius: 8px; /* Rounded corners */ -} - -.CodeSearch:focus { - border: grey; /* Ensure no border when focused */ - outline: none; /* Ensure no outline when focused */ -} - -.CodeSearch::placeholder { - color: grey; - font-style: italic; /* Make placeholder text italic */ -} - -.AddCourseBox { - display: flex; - flex-direction: row; - justify-content: space-between; - align-items: center; - - width: 425px; - height: 36px; - - border-radius: 16px; - margin-bottom: 5px; - - padding-left: 10px; - padding-right: 10px; - - background-color: #F5F5F5; - transition: filter 0.4s ease; -} - - - - - -.TermSelect { - border: 1px solid white; - padding: 0 5px; /* Adjust padding to fit within the desired height */ - background-color: white; - cursor: pointer; - border-radius: 4px; - font-size: 12px; /* Make the font smaller */ - height: 22px; /* Set height */ - display: flex; /* Align items within */ - align-items: center; /* Center items vertically */ - box-sizing: border-box; /* Include border and padding in the height */ - color: black; /* Make text black */ - position: relative; /* Make it the reference point for absolute positioning */ -} - -.TermDrop { - position: absolute; - background-color: white; - border: 1px solid white; /* Make the border white */ - border-radius: 4px; - top: 100%; /* Position it directly below the TermSelect element */ - margin-top: 5px; /* Additional spacing if needed */ - z-index: 10; - width: 100px; /* Make the dropdown wider */ - font-size: 12px; /* Make the font smaller */ - box-sizing: border-box; -} - -.TermDrop div { - padding: 5px; - cursor: pointer; - color: black; /* Make text black */ -} - -.TermDrop div:nth-child(odd) { - background-color: white; /* Lighter grey background */ -} - -.TermDrop div:nth-child(even) { - background-color: #f0f0f0; /* Lighter grey background */ -} - -.TermDrop div:hover { - background-color: #e0e0e0; /* Light grey on hover */ -} - - - - - - -.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 */ -} - diff --git a/frontend/src/pages/Courses/Courses.tsx b/frontend/src/pages/Courses/Courses.tsx index 5b694c4..4f63698 100644 --- a/frontend/src/pages/Courses/Courses.tsx +++ b/frontend/src/pages/Courses/Courses.tsx @@ -2,9 +2,9 @@ import { useState, useEffect } from "react"; import { Year } from "../../commons/types/TypeUser"; -import styles from "./Courses.module.css"; +import Style from "./Courses.module.css"; -import YearBox from "./components/YearBox"; +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"; @@ -12,7 +12,7 @@ import PageLinks from "./../../navbar/PageLinks"; import { User } from "../../commons/types/TypeUser"; // import { StudentCourse } from "../../commons/types/TypeCourse"; -import { yearTreeify } from "./utils/CoursesUtils"; +import { yearTreeify } from "./CoursesUtils"; function NavBar() { return ( @@ -50,11 +50,11 @@ function Courses(props: { user: User, setUser: Function }){ return(
-
- -
+
{renderedYears}
diff --git a/frontend/src/pages/Courses/utils/CoursesUtils.ts b/frontend/src/pages/Courses/CoursesUtils.ts similarity index 95% rename from frontend/src/pages/Courses/utils/CoursesUtils.ts rename to frontend/src/pages/Courses/CoursesUtils.ts index 09dd494..9927671 100644 --- a/frontend/src/pages/Courses/utils/CoursesUtils.ts +++ b/frontend/src/pages/Courses/CoursesUtils.ts @@ -1,6 +1,6 @@ -import { User, Year } from "../../../commons/types/TypeUser"; -import { StudentCourse } from "../../../commons/types/TypeCourse"; +import { User, Year } from "../../commons/types/TypeUser"; +import { StudentCourse } from "../../commons/types/TypeCourse"; export const yearTreeify = (courses: StudentCourse[]): Year[] => { const academicYears: { [key: number]: Year } = {}; diff --git a/frontend/src/pages/Courses/components/AddButton.tsx b/frontend/src/pages/Courses/components/AddButton.tsx deleted file mode 100644 index 30541f0..0000000 --- a/frontend/src/pages/Courses/components/AddButton.tsx +++ /dev/null @@ -1,169 +0,0 @@ - -import { useRef, useState, useEffect } from "react"; -import styles from "./../Courses.module.css"; - -import { getCTCourses } from "./../../../api/api"; -import { StudentCourse } from "../../../commons/types/TypeCourse"; -import { User } from "../../../commons/types/TypeUser"; -import { xCheckMajorsAndSet } from "../utils/CoursesUtils"; - - -const termMappings: { [key: string]: number } = { - "Fall 2022": 202203, - "Spring 2023": 202301, - "Fall 2023": 202303, - "Spring 2024": 202401, - "Fall 2024": 202403, - "Spring 2025": 202501, -}; -const terms = Object.keys(termMappings); - -function TermSelector(props: { selectedTerm: number, onSelectTerm: Function }) { - - const [dropVis, setDropVis] = useState(false); - const termSelectRef = useRef(null); - - const toggleDrop = () => { - setDropVis(!dropVis); - }; - - const selectTerm = (term: string) => { - props.onSelectTerm(termMappings[term]); - setDropVis(false); - }; - - const handleClickOutside = (event: MouseEvent) => { - if (termSelectRef.current && !termSelectRef.current.contains(event.target as Node)) { - setDropVis(false); - } - }; - - useEffect(() => { - if (dropVis) { - document.addEventListener('click', handleClickOutside); - } else { - document.removeEventListener('click', handleClickOutside); - } - return () => { - document.removeEventListener('click', handleClickOutside); - }; - }, [dropVis]); - - return ( -
- {Object.keys(termMappings).find(key => termMappings[key] === props.selectedTerm)} - {dropVis && ( -
- {terms.map((term, index) => ( -
selectTerm(term)}> - {term} -
- ))} -
- )} -
- ); -} - -function AddButton(props: { term: number, user: User, setUser: Function }) { - - const inputRef = useRef(null); - const [active, setActive] = useState(false); - const [searchData, setSearchData] = useState([]); - const [selectedTerm, setSelectedTerm] = useState(props.term); - - useEffect(() => { - if(active){ - inputRef.current?.focus(); - const cachedData = localStorage.getItem(`courses-${selectedTerm}`); - if(cachedData){ - setSearchData(JSON.parse(cachedData)); - console.log("Loaded From Cache"); - }else{ - getCTCourses(selectedTerm.toString()).then(data => { - setSearchData(data); - try { - localStorage.setItem(`courses-${selectedTerm}`, JSON.stringify(data)); - console.log("Retrieved & Cached"); - } catch(e: any) { - if (e.name === 'QuotaExceededError' || e.code === 22) { - console.error("Quota Exceeded: ", e); - } else { - console.error("Error Unknown: ", e); - } - } - }).catch(error => { - console.error("Error Retrieving: ", error); - }); - } - } - }, [active, selectedTerm]); - - const activate = () => { - setActive(true); - }; - - const deactivate = () => { - setActive(false); - }; - - const handleKeyPress = (event: React.KeyboardEvent) => { - if (event.key === "Enter" && inputRef.current) { - const code = inputRef.current.value; - const offering = searchData.find(course => course["course_code"] === code); - - if (offering) { - const codes = offering["listings"]; - const title = offering["title"]; - const credit = offering["credits"]; - const areas = offering["areas"]; - const skills = offering["skills"]; - const seasons = ["Fall", "Spring"]; - const course = { codes, title, credit, areas, skills, seasons }; - const status = (selectedTerm === props.term) ? "MA_VALID" : "MA_HYPOTHETICAL"; - const term = props.term; - const newCourse: StudentCourse = { course, term, status }; - - const isDuplicate = props.user.studentCourses.some(existingCourse => - existingCourse.course.title === newCourse.course.title && - existingCourse.term === newCourse.term - ); - - if(isDuplicate){ - console.log("Duplicate"); - }else{ - xCheckMajorsAndSet(props.user, newCourse, props.setUser); - deactivate(); - } - } - } - }; - - return ( -
- {!active ? ( -
- + -
- ) : ( -
-
-
-
- - -
-
- )} -
- ); -} - -export default AddButton; \ No newline at end of file diff --git a/frontend/src/pages/Courses/year/YearBox.module.css b/frontend/src/pages/Courses/year/YearBox.module.css new file mode 100644 index 0000000..c51d845 --- /dev/null +++ b/frontend/src/pages/Courses/year/YearBox.module.css @@ -0,0 +1,18 @@ + +.row { + display: flex; + flex-direction: row; +} + +.Grade { + font-weight: 600; + font-size: 25px; + margin-right: 10px; +} + +.yearComponent { + display: flex; + flex-direction: column; + margin-bottom: 20px; + transition: transform 0.5s ease, height 0.5s ease, width 0.5s ease; +} \ No newline at end of file diff --git a/frontend/src/pages/Courses/components/YearBox.tsx b/frontend/src/pages/Courses/year/YearBox.tsx similarity index 76% rename from frontend/src/pages/Courses/components/YearBox.tsx rename to frontend/src/pages/Courses/year/YearBox.tsx index b864196..1f0faee 100644 --- a/frontend/src/pages/Courses/components/YearBox.tsx +++ b/frontend/src/pages/Courses/year/YearBox.tsx @@ -1,8 +1,8 @@ -import styles from "./../Courses.module.css"; -import SemesterBox from "./SemesterBox"; +import React from "react"; +import Style from "./YearBox.module.css"; +import SemesterBox from "./semester/SemesterBox"; import { User, Year } from "../../../commons/types/TypeUser"; -// import { StudentCourse } from "../../../commons/types/TypeCourse"; const convertGrade = (grade: number) => { switch (grade) { @@ -22,10 +22,10 @@ const convertGrade = (grade: number) => { export default function YearBox(props: {year: Year, edit: boolean, user: User, setUser: Function }){ return( -
+
-
-
+
+
{convertGrade(props.year["grade"])}
@@ -33,7 +33,7 @@ export default function YearBox(props: {year: Year, edit: boolean, user: User, s
-
+
diff --git a/frontend/src/pages/Courses/year/semester/SemesterBox.module.css b/frontend/src/pages/Courses/year/semester/SemesterBox.module.css new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/pages/Courses/components/SemesterBox.tsx b/frontend/src/pages/Courses/year/semester/SemesterBox.tsx similarity index 93% rename from frontend/src/pages/Courses/components/SemesterBox.tsx rename to frontend/src/pages/Courses/year/semester/SemesterBox.tsx index f246087..6ea75d7 100644 --- a/frontend/src/pages/Courses/components/SemesterBox.tsx +++ b/frontend/src/pages/Courses/year/semester/SemesterBox.tsx @@ -1,12 +1,12 @@ import React from "react"; -import styles from "./../Courses.module.css"; +import Style from "./SemesterBox.module.css" -import { StudentCourse } from "../../../commons/types/TypeCourse"; -import { User } from "../../../commons/types/TypeUser"; +import { StudentCourse } from "../../../../commons/types/TypeCourse"; +import { User } from "../../../../commons/types/TypeUser"; -import CourseBox from "./CourseBox"; -import AddButton from "./AddButton"; +import CourseBox from "./course/CourseBox"; +import AddButton from "./add/AddButton"; function SemesterBox(props: { edit: boolean, user: User, setUser: Function; term: number, TermSC: StudentCourse[] }) { @@ -15,7 +15,7 @@ function SemesterBox(props: { edit: boolean, user: User, setUser: Function; term )); return ( -
+
{SCBoxes} diff --git a/frontend/src/pages/Courses/year/semester/add/AddButton.module.css b/frontend/src/pages/Courses/year/semester/add/AddButton.module.css new file mode 100644 index 0000000..b6dc39b --- /dev/null +++ b/frontend/src/pages/Courses/year/semester/add/AddButton.module.css @@ -0,0 +1,156 @@ + +.Row { + display: flex; + flex-direction: row; +} + +/* */ + +.AddButton { + display: flex; + justify-content: center; + align-items: center; + width: 36px; + height: 36px; + border-radius: 50%; + margin-bottom: 5px; + background-color: #F5F5F5; + cursor: pointer; +} +.AddButton:hover, +.AddButton:active { + background-color: #E0E0E0; +} + +.AddCanvas { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + width: 425px; + height: 36px; + border-radius: 16px; + margin-bottom: 5px; + padding-left: 10px; + padding-right: 10px; + background-color: #F5F5F5; + transition: filter 0.4s ease; +} + +/* */ + +.TermBox { + border: 1px solid #ccc; + padding: 0 8px; + background-color: white; + cursor: pointer; + border-radius: 4px; + font-size: 12px; + height: 22px; + width: 90px; + display: flex; + align-items: center; + box-sizing: border-box; + color: black; + position: relative; + transition: border-color 0.3s ease; +} +.TermBox:hover { + border-color: #aaa; +} + +/* */ + +.TermOptions { + 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; +} + +.TermOptions div { + padding: 8px; + cursor: pointer; + color: black; + background-color: white; +} + +.TermOptions 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 */ +} + +/* */ + +.CodeBox { + background-color: white; + border: 1px solid #ccc; + height: 22px; + width: 90px; + padding-left: 8px; + margin-left: 10px; + outline: none; + font-size: 12px; + font-weight: 500; + border-radius: 4px; + box-sizing: border-box; + margin-right: 4px; + + /* box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); */ +} +.CodeBox:focus { + border-color: #aaa; + outline: none; +} +.CodeBox::placeholder { + color: grey; + font-style: italic; +} + +/* */ + +.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; + border-radius: 50%; + background-color: #ededed; + cursor: pointer; + margin-right: 5px; +} +.RemoveButton:hover { + background-color: #D3D3D3; +} diff --git a/frontend/src/pages/Courses/year/semester/add/AddButton.tsx b/frontend/src/pages/Courses/year/semester/add/AddButton.tsx new file mode 100644 index 0000000..f70ba8a --- /dev/null +++ b/frontend/src/pages/Courses/year/semester/add/AddButton.tsx @@ -0,0 +1,101 @@ + +import { useRef, useState, useEffect } from "react"; +import Style from "./AddButton.module.css"; +import { User } from "../../../../../commons/types/TypeUser"; +import { fetchAndCacheCourses, handleAddCourse } from "./AddUtils"; + +import { AddCourseDisplay, nullAddCourseDisplay } from "../../../../../commons/types/TypeCourse"; + +const termMappings: { [key: string]: number } = { + "Spring 2025": 202501, + "Fall 2024": 202403, + "Spring 2024": 202401, + "Fall 2023": 202303, + "Spring 2023": 202301, + "Fall 2022": 202203, +}; +const terms = Object.keys(termMappings); + +function AddButton(props: { term: number; user: User; setUser: Function }) { + + const inputRef = useRef(null); + const addRef = useRef(null); + + const [addDisplay, setAddDisplay] = useState(nullAddCourseDisplay); + + const [selectedTerm, setSelectedTerm] = useState(props.term); + const [searchData, setSearchData] = useState([]); + + useEffect(() => { + if(addDisplay.active){ + document.addEventListener("mousedown", handleClickOutside); + inputRef.current?.focus(); + fetchAndCacheCourses(selectedTerm, setSearchData); + } + + 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"){ + handleAddCourse(inputRef, searchData, selectedTerm, props, setAddDisplay); + } + }; + + return ( +
+ {!addDisplay.active ? ( +
setAddDisplay((prevState) => ({...prevState, active: true}))}> + + +
+ ) : ( +
+
+
setAddDisplay((prevState) => ({...prevState, active: false}))}> + +
+
setAddDisplay((prevState) => ({...prevState, dropVis: !addDisplay.dropVis}))}> + {Object.keys(termMappings).find((key) => termMappings[key] === selectedTerm)} + {addDisplay.dropVis && ( +
+ {terms.map((term, index) => ( +
setSelectedTerm(termMappings[term])} className={termMappings[term] === selectedTerm ? Style.SelectedTerm : ""}> + {term} +
+ ))} +
+ )} +
+ + + +
handleAddCourse(inputRef, searchData, selectedTerm, props, setAddDisplay)}> + +
+
+
+ )} +
+ ); +} + +export default AddButton; diff --git a/frontend/src/pages/Courses/year/semester/add/AddUtils.ts b/frontend/src/pages/Courses/year/semester/add/AddUtils.ts new file mode 100644 index 0000000..be6786d --- /dev/null +++ b/frontend/src/pages/Courses/year/semester/add/AddUtils.ts @@ -0,0 +1,77 @@ + +import { StudentCourse } from "../../../../../commons/types/TypeCourse"; +import { User } from "../../../../../commons/types/TypeUser"; +import { xCheckMajorsAndSet } from "./../../../CoursesUtils"; +import { getCatalog } from "../../../../../api/api"; +import { AddCourseDisplay } from "../../../../../commons/types/TypeCourse"; + +export async function fetchAndCacheCourses( + selectedTerm: number, + setSearchData: Function +) { + const cachedData = localStorage.getItem(`courses-${selectedTerm}`); + if(cachedData){ + setSearchData(JSON.parse(cachedData)); + console.log("Loaded From Cache"); + }else{ + try{ + const data = await getCatalog(selectedTerm.toString()); + setSearchData(data); + try { + localStorage.setItem(`courses-${selectedTerm}`, JSON.stringify(data)); + console.log("Retrieved & Cached"); + } catch (e: any) { + if (e.name === "QuotaExceededError" || e.code === 22) { + console.error("Quota Exceeded: ", e); + } else { + console.error("Error Unknown: ", e); + } + } + } catch (error) { + console.error("Error Retrieving: ", error); + } + } +} + +export function handleAddCourse( + inputRef: React.RefObject, + searchData: any[], + selectedTerm: number, + props: { term: number; user: User; setUser: Function }, + setAddDisplay: Function +){ + if(inputRef.current){ + const targetCode = inputRef.current.value; + const targetCourse = searchData.find((fireCourse) => fireCourse["c"].includes(targetCode)); + + if(targetCourse){ + const codes = targetCourse["c"]; + const title = targetCourse["t"]; + const credit = targetCourse["r"]; + const dist = targetCourse["d"]; + const seasons = ["Fall", "Spring"]; + + const course = { codes, title, credit, dist, seasons }; + const status = selectedTerm === props.term ? "MA_VALID" : "MA_HYPOTHETICAL"; + const term = props.term; + const newCourse: StudentCourse = { course, status, term }; + + const isDuplicate = props.user.studentCourses.some( + (existingCourse) => + existingCourse.course.title === newCourse.course.title && + existingCourse.term === newCourse.term + ); + + if (isDuplicate) { + console.log("Duplicate"); + } else { + xCheckMajorsAndSet(props.user, newCourse, props.setUser); + setAddDisplay((prevState: AddCourseDisplay) => ({ + ...prevState, + active: false, + })); + } + } + } +} + diff --git a/frontend/src/pages/Courses/year/semester/course/CourseBox.module.css b/frontend/src/pages/Courses/year/semester/course/CourseBox.module.css new file mode 100644 index 0000000..68bef23 --- /dev/null +++ b/frontend/src/pages/Courses/year/semester/course/CourseBox.module.css @@ -0,0 +1,51 @@ + +.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: 425px; + height: 36px; + + border-radius: 16px; + margin-bottom: 5px; + + padding-left: 10px; + padding-right: 10px; + + background-color: #F5F5F5; + transition: filter 0.4s ease; +} \ No newline at end of file diff --git a/frontend/src/pages/Courses/components/CourseBox.tsx b/frontend/src/pages/Courses/year/semester/course/CourseBox.tsx similarity index 77% rename from frontend/src/pages/Courses/components/CourseBox.tsx rename to frontend/src/pages/Courses/year/semester/course/CourseBox.tsx index 8c0e3ff..d477caa 100644 --- a/frontend/src/pages/Courses/components/CourseBox.tsx +++ b/frontend/src/pages/Courses/year/semester/course/CourseBox.tsx @@ -1,13 +1,13 @@ -import styles from "./../Courses.module.css"; +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 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 { 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 }){ @@ -56,7 +56,7 @@ function RemoveCourse(props: { SC: StudentCourse, user: User, setUser: Function }; return( -
+
) @@ -74,32 +74,32 @@ function CourseBox(props: {edit: boolean, SC: StudentCourse, user: User, setUser 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
; + return
; }; const getBackgroundColor = () => (status === "DA_COMPLETE" ? "#E1E9F8" : "#F5F5F5"); const getSeasonImage = () => (String(term).endsWith("3") ? img_fall : img_spring); return ( -
+
{/* onClick={openModal} */} -
+
{renderMark()}
@@ -112,8 +112,8 @@ function CourseBox(props: {edit: boolean, SC: StudentCourse, user: User, setUser
-
- +
+
diff --git a/frontend/src/pages/Graduation/Graduation.tsx b/frontend/src/pages/Graduation/Graduation.tsx index ba3dc7d..7708bd9 100644 --- a/frontend/src/pages/Graduation/Graduation.tsx +++ b/frontend/src/pages/Graduation/Graduation.tsx @@ -3,12 +3,14 @@ import { useState } from "react"; import styles from "./Graduation.module.css"; import GraduationDistribution from "./components/Distribution"; -import GraduationOverview from "./components/Overview"; +// import GraduationOverview from "./components/Overview"; import nav_styles from "./../../navbar/NavBar.module.css"; import img_logo from "./../../commons/images/ma_logo.png"; import PageLinks from "./../../navbar/PageLinks"; +import { User } from "../../commons/types/TypeUser"; + function NavBar() { return (
@@ -21,15 +23,15 @@ function NavBar() { ); } -function Recommendations() { - return( -
-
Hello, Ryn!
-
- ); -} +// function Recommendations() { +// return( +//
+ +//
+// ); +// } -function Graduation(){ +function Graduation(props: { user: User, setUser: Function }){ const UserYear = () => { return 2; @@ -45,7 +47,12 @@ function Graduation(){
- + {/* */} +
+
+ Hello, {props.user.name}! +
+
diff --git a/frontend/src/pages/Graduation/components/Distribution.tsx b/frontend/src/pages/Graduation/components/Distribution.tsx index fb85638..6b550d9 100644 --- a/frontend/src/pages/Graduation/components/Distribution.tsx +++ b/frontend/src/pages/Graduation/components/Distribution.tsx @@ -6,7 +6,7 @@ import styles from "./../Graduation.module.css"; import DistributionBox from "../../../commons/components/courses/DistributionBoxLarge"; import { StudentCourseIcon } from "../../../commons/components/icons/CourseIcon"; -import InfoButton from "../../../navbar/InfoButton"; +import InfoButton from "../../../navbar/misc/InfoButton"; import { StudentCourse } from "../../../commons/types/TypeCourse"; @@ -201,27 +201,27 @@ function DistributionTable(props: { year: number; studentCourses: StudentCourse[ let LList: StudentCourse[] = []; props.studentCourses.forEach((studentCourse) => { - const { areas, skills } = studentCourse.course; - - if (areas.includes('Hu')) { - HuList.push(studentCourse); - } - if (areas.includes('So')) { - SoList.push(studentCourse); - } - if (areas.includes('Sc')) { - ScList.push(studentCourse); - } - - if (skills.includes('QR')) { - QRList.push(studentCourse); - } - if (skills.includes('WR')) { - WRList.push(studentCourse); - } - if (skills.some(skill => skill.startsWith('L'))) { - LList.push(studentCourse); - } + const dist = studentCourse.course.dist; + + if (dist && dist.includes('Hu')) { + HuList.push(studentCourse); + } + if (dist && dist.includes('So')) { + SoList.push(studentCourse); + } + if (dist && dist.includes('Sc')) { + ScList.push(studentCourse); + } + + if (dist && dist.includes('QR')) { + QRList.push(studentCourse); + } + if (dist && dist.includes('WR')) { + WRList.push(studentCourse); + } + if (dist && dist.some(skill => skill.startsWith('L'))) { + LList.push(studentCourse); + } }); if(props.year === 1){ diff --git a/frontend/src/pages/Majors/Majors.tsx b/frontend/src/pages/Majors/Majors.tsx index 67e04be..2ada2d1 100644 --- a/frontend/src/pages/Majors/Majors.tsx +++ b/frontend/src/pages/Majors/Majors.tsx @@ -1,48 +1,25 @@ import { useState } from "react"; - import Style from "./Majors.module.css"; -import NavStyle from "./../../navbar/NavBar.module.css"; - -import Logo from "./../../commons/images/ma_logo.png"; -import PageLinks from "./../../navbar/PageLinks"; - -import Requirements from "./requirements/Requirements"; -import Metadata from "./metadata/Metadata"; - import { User } from "../../commons/types/TypeUser"; -import { Program } from "./../../commons/types/TypeProgram"; -function NavBar() { - return ( -
-
- -
- -
- ); -} +import NavBar from "./../../navbar/NavBar" +import Overhead from "./overhead/Overhead"; +import Metadata from "./metadata/Metadata"; +import Requirements from "./requirements/Requirements"; function Majors(props: { user: User, setUser: Function }){ const [currdex, setCurrdex] = useState(0); const [currDegree, setCurrDegree] = useState(0); - let programs: Program[] = props.user.programs; - - const alterCurrdex = (dir: number) => { - if(programs && programs.length > 0){ - setCurrdex((currdex + dir + programs.length) % programs.length); - setCurrDegree(0); - } + const alterCurrdex = (dir: number) => { + setCurrdex((currdex + dir + props.user.programs.length) % props.user.programs.length); + setCurrDegree(0); }; const seeProgram = (dir: number) => { - if(programs && programs.length > 0){ - return programs[(currdex + dir + programs.length) % programs.length]; - } - return null; + return props.user.programs[(currdex + dir + props.user.programs.length) % props.user.programs.length]; }; const alterCurrDegree = (num: number) => { @@ -51,10 +28,13 @@ function Majors(props: { user: User, setUser: Function }){ return (
- + }/>
diff --git a/frontend/src/pages/Majors/metadata/Metadata.module.css b/frontend/src/pages/Majors/metadata/Metadata.module.css index 6c50164..adf4b8b 100644 --- a/frontend/src/pages/Majors/metadata/Metadata.module.css +++ b/frontend/src/pages/Majors/metadata/Metadata.module.css @@ -53,10 +53,25 @@ } .majorContainer { - padding: 20px; + padding: 20px 20px 20px 0; width: auto; height: 400px; background-color: white; margin-right: 10px; /* border: 1px solid black; */ +} + +.thumbtack { + font-size: 28px; + margin-right: 10px; + cursor: pointer; + transition: opacity 0.2s; +} + +.thumbtack:hover { + opacity: 0.7; /* Slightly lighter on hover */ +} + +.thumbtack:active { + opacity: 0.5; /* Even lighter on click */ } \ No newline at end of file diff --git a/frontend/src/pages/Majors/metadata/Metadata.tsx b/frontend/src/pages/Majors/metadata/Metadata.tsx index c99bc32..b72a662 100644 --- a/frontend/src/pages/Majors/metadata/Metadata.tsx +++ b/frontend/src/pages/Majors/metadata/Metadata.tsx @@ -2,27 +2,105 @@ import React, { useState, useEffect } from "react"; import Style from "./Metadata.module.css"; +import { User } from "../../../commons/types/TypeUser"; + import { Button } from "react-bootstrap"; import { Link } from 'react-router-dom'; import lgsIcon from "../../../commons/images/little_guys.png"; -// import img_plus from "../../../commons/images/plus.png"; -// import img_arrowup from "../../../commons/images/arrowup.png"; -// import img_arrowdown from "../../../commons/images/arrowdown.png"; +import img_plus from "../../../commons/images/plus.png"; +import illegal_pin from "../../../commons/images/illegal_pin.png" import { Program, Degree } from "../../../commons/types/TypeProgram"; -function MetadataTopshelf(props: { program: Program, degree: Degree }){ - return( +function MetadataTopshelf(props: { user: User, setUser: Function, currProgram: number, currDegree: number, program: Program, degree: Degree }) { + + const pinProgram = () => { + const { currProgram, user, setUser } = props; + const existingDegree = user.studentDegrees.find(degree => degree.programIndex === currProgram); + + if (existingDegree) { + if (existingDegree.status === "PIN") { + // Unpin the program (remove the studentDegree) + const updatedUser = { + ...user, + studentDegrees: user.studentDegrees.filter(degree => degree.programIndex !== currProgram) + }; + setUser(updatedUser); + } else { + // Do nothing if it's already added (status: "ADD") + } + } else { + // Pin the program if it's not already pinned or added + const newStudentDegree = { + status: "PIN", + programIndex: currProgram, + degreeIndex: props.currDegree + }; + + const updatedUser = { + ...user, + studentDegrees: [...user.studentDegrees, newStudentDegree] + }; + + setUser(updatedUser); + } + }; + + const addProgram = () => { + const { currProgram, user, setUser } = props; + const existingDegree = user.studentDegrees.find(degree => degree.programIndex === currProgram); + + if (existingDegree) { + if (existingDegree.status === "ADD") { + // Unadd the program (remove the studentDegree) + const updatedUser = { + ...user, + studentDegrees: user.studentDegrees.filter(degree => degree.programIndex !== currProgram) + }; + setUser(updatedUser); + } else if (existingDegree.status === "PIN") { + // Change status from "PIN" to "ADD" + const updatedUser = { + ...user, + studentDegrees: user.studentDegrees.map(degree => + degree.programIndex === currProgram + ? { ...degree, status: "ADD" } + : degree + ) + }; + setUser(updatedUser); + } + } else { + // Add the program if it's not already pinned or added + const newStudentDegree = { + status: "ADD", + programIndex: currProgram, + degreeIndex: props.currDegree + }; + + const updatedUser = { + ...user, + studentDegrees: [...user.studentDegrees, newStudentDegree] + }; + + setUser(updatedUser); + } + }; + + return (
- {/* */} +
+ Add Program +
+
+ Add Program +
{props.degree.metadata.name}
- +
{props.degree.metadata.students}
MAJOR
@@ -97,24 +175,35 @@ function MetadataStats(degree: Degree){ ); } -function MetadataContent(props: {program: Program, whichDegree: number, alterCurrDegree: Function}){ +function MetadataContent(props: { user: User, setUser: Function, currProgram: number, program: Program, whichDegree: number, alterCurrDegree: Function }){ let currDegree = props.program.degrees[props.whichDegree]; return (
- - - - -
ABOUT
-
{currDegree.metadata.about}
- -
DUS
-
{currDegree.metadata.dus.name}; {currDegree.metadata.dus.address}
- -
-
MAJOR CATALOG
-
MAJOR WEBSITE
-
+ + +
+ + + +
+ ABOUT +
+
+ {currDegree.metadata.about} +
+ +
+ DUS +
+
+ {currDegree.metadata.dus.name}; {currDegree.metadata.dus.address} +
+ +
+
MAJOR CATALOG
+
MAJOR WEBSITE
+
+
); } @@ -123,7 +212,6 @@ function MetadataScrollButton(props: {scrollProgram: Function, seeProgram: Funct return(