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..b7d9e31 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,13 +1,15 @@ { - "name": "new-audit", + "name": "majoraudit", "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "new-audit", + "name": "majoraudit", "version": "0.1.0", "dependencies": { + "@prisma/client": "^6.5.0", + "@supabase/supabase-js": "^2.49.1", "d3": "^7.9.0", "next": "15.1.6", "react": "^19.0.0", @@ -21,6 +23,7 @@ "@types/react-dom": "^19", "eslint": "^9", "eslint-config-next": "15.1.6", + "prisma": "^6.5.0", "typescript": "^5" } }, @@ -33,6 +36,406 @@ "tslib": "^2.4.0" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.1.tgz", + "integrity": "sha512-kfYGy8IdzTGy+z0vFGvExZtxkFlA4zAxgKEahG9KE1ScBjpQnFsNOX8KTU5ojNru5ed5CVoJYXFtoxaq5nFbjQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.1.tgz", + "integrity": "sha512-dp+MshLYux6j/JjdqVLnMglQlFu+MuVeNrmT5nk6q07wNhCdSnB7QZj+7G8VMUGh1q+vj2Bq8kRsuyA00I/k+Q==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.1.tgz", + "integrity": "sha512-50tM0zCJW5kGqgG7fQ7IHvQOcAn9TKiVRuQ/lN0xR+T2lzEFvAi1ZcS8DiksFcEpf1t/GYOeOfCAgDHFpkiSmA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.1.tgz", + "integrity": "sha512-GCj6WfUtNldqUzYkN/ITtlhwQqGWu9S45vUXs7EIYf+7rCiiqH9bCloatO9VhxsL0Pji+PF4Lz2XXCES+Q8hDw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.1.tgz", + "integrity": "sha512-5hEZKPf+nQjYoSr/elb62U19/l1mZDdqidGfmFutVUjjUZrOazAtwK+Kr+3y0C/oeJfLlxo9fXb1w7L+P7E4FQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.1.tgz", + "integrity": "sha512-hxVnwL2Dqs3fM1IWq8Iezh0cX7ZGdVhbTfnOy5uURtao5OIVCEyj9xIzemDi7sRvKsuSdtCAhMKarxqtlyVyfA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.1.tgz", + "integrity": "sha512-1MrCZs0fZa2g8E+FUo2ipw6jw5qqQiH+tERoS5fAfKnRx6NXH31tXBKI3VpmLijLH6yriMZsxJtaXUyFt/8Y4A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.1.tgz", + "integrity": "sha512-0IZWLiTyz7nm0xuIs0q1Y3QWJC52R8aSXxe40VUxm6BB1RNmkODtW6LHvWRrGiICulcX7ZvyH6h5fqdLu4gkww==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.1.tgz", + "integrity": "sha512-NdKOhS4u7JhDKw9G3cY6sWqFcnLITn6SqivVArbzIaf3cemShqfLGHYMx8Xlm/lBit3/5d7kXvriTUGa5YViuQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.1.tgz", + "integrity": "sha512-jaN3dHi0/DDPelk0nLcXRm1q7DNJpjXy7yWaWvbfkPvI+7XNSc/lDOnCLN7gzsyzgu6qSAmgSvP9oXAhP973uQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.1.tgz", + "integrity": "sha512-OJykPaF4v8JidKNGz8c/q1lBO44sQNUQtq1KktJXdBLn1hPod5rE/Hko5ugKKZd+D2+o1a9MFGUEIUwO2YfgkQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.1.tgz", + "integrity": "sha512-nGfornQj4dzcq5Vp835oM/o21UMlXzn79KobKlcs3Wz9smwiifknLy4xDCLUU0BWp7b/houtdrgUz7nOGnfIYg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.1.tgz", + "integrity": "sha512-1osBbPEFYwIE5IVB/0g2X6i1qInZa1aIoj1TdL4AaAb55xIIgbg8Doq6a5BzYWgr+tEcDzYH67XVnTmUzL+nXg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.1.tgz", + "integrity": "sha512-/6VBJOwUf3TdTvJZ82qF3tbLuWsscd7/1w+D9LH0W/SqUgM5/JJD0lrJ1fVIfZsqB6RFmLCe0Xz3fmZc3WtyVg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.1.tgz", + "integrity": "sha512-nSut/Mx5gnilhcq2yIMLMe3Wl4FK5wx/o0QuuCLMtmJn+WeWYoEGDN1ipcN72g1WHsnIbxGXd4i/MF0gTcuAjQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.1.tgz", + "integrity": "sha512-cEECeLlJNfT8kZHqLarDBQso9a27o2Zd2AQ8USAEoGtejOrCYHNtKP8XQhMDJMtthdF4GBmjR2au3x1udADQQQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.1.tgz", + "integrity": "sha512-xbfUhu/gnvSEg+EGovRc+kjBAkrvtk38RlerAzQxvMzlB4fXpCFCeUAYzJvrnhFtdeyVCDANSjJvOvGYoeKzFA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.1.tgz", + "integrity": "sha512-O96poM2XGhLtpTh+s4+nP7YCCAfb4tJNRVZHfIE7dgmax+yMP2WgMd2OecBuaATHKTHsLWHQeuaxMRnCsH8+5g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.1.tgz", + "integrity": "sha512-X53z6uXip6KFXBQ+Krbx25XHV/NCbzryM6ehOAeAil7X7oa4XIq+394PWGnwaSQ2WRA0KI6PUO6hTO5zeF5ijA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.1.tgz", + "integrity": "sha512-Na9T3szbXezdzM/Kfs3GcRQNjHzM6GzFBeU1/6IV/npKP5ORtp9zbQjvkDJ47s6BCgaAZnnnu/cY1x342+MvZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.1.tgz", + "integrity": "sha512-T3H78X2h1tszfRSf+txbt5aOp/e7TAz3ptVKu9Oyir3IAOFPGV6O9c2naym5TOriy1l0nNf6a4X5UXRZSGX/dw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.1.tgz", + "integrity": "sha512-2H3RUvcmULO7dIE5EWJH8eubZAI4xw54H1ilJnRNZdeo8dTADEZ21w6J22XBkXqGJbe0+wnNJtw3UXRoLJnFEg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.1.tgz", + "integrity": "sha512-GE7XvrdOzrb+yVKB9KsRMq+7a2U/K5Cf/8grVFRAGJmfADr/e/ODQ134RK2/eeHqYV5eQRFxb1hY7Nr15fv1NQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.1.tgz", + "integrity": "sha512-uOxSJCIcavSiT6UnBhBzE8wy3n0hOkJsBOzy7HDAuTDE++1DJMRRVCPGisULScHL+a/ZwdXPpXD3IyFKjA7K8A==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.1.tgz", + "integrity": "sha512-Y1EQdcfwMSeQN/ujR5VayLOJ1BHaK+ssyk0AEzPjC+t1lITgsnccPqFjb6V+LsTp/9Iov4ysfjxLaGJ9RPtkVg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", @@ -733,6 +1136,82 @@ "node": ">=12.4.0" } }, + "node_modules/@prisma/client": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.5.0.tgz", + "integrity": "sha512-M6w1Ql/BeiGoZmhMdAZUXHu5sz5HubyVcKukbLs3l0ELcQb8hTUJxtGEChhv4SVJ0QJlwtLnwOLgIRQhpsm9dw==", + "hasInstallScript": true, + "engines": { + "node": ">=18.18" + }, + "peerDependencies": { + "prisma": "*", + "typescript": ">=5.1.0" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@prisma/config": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.5.0.tgz", + "integrity": "sha512-sOH/2Go9Zer67DNFLZk6pYOHj+rumSb0VILgltkoxOjYnlLqUpHPAN826vnx8HigqnOCxj9LRhT6U7uLiIIWgw==", + "devOptional": true, + "dependencies": { + "esbuild": ">=0.12 <1", + "esbuild-register": "3.6.0" + } + }, + "node_modules/@prisma/debug": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.5.0.tgz", + "integrity": "sha512-fc/nusYBlJMzDmDepdUtH9aBsJrda2JNErP9AzuHbgUEQY0/9zQYZdNlXmKoIWENtio+qarPNe/+DQtrX5kMcQ==", + "devOptional": true + }, + "node_modules/@prisma/engines": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.5.0.tgz", + "integrity": "sha512-FVPQYHgOllJklN9DUyujXvh3hFJCY0NX86sDmBErLvoZjy2OXGiZ5FNf3J/C4/RZZmCypZBYpBKEhx7b7rEsdw==", + "devOptional": true, + "hasInstallScript": true, + "dependencies": { + "@prisma/debug": "6.5.0", + "@prisma/engines-version": "6.5.0-73.173f8d54f8d52e692c7e27e72a88314ec7aeff60", + "@prisma/fetch-engine": "6.5.0", + "@prisma/get-platform": "6.5.0" + } + }, + "node_modules/@prisma/engines-version": { + "version": "6.5.0-73.173f8d54f8d52e692c7e27e72a88314ec7aeff60", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.5.0-73.173f8d54f8d52e692c7e27e72a88314ec7aeff60.tgz", + "integrity": "sha512-iK3EmiVGFDCmXjSpdsKGNqy9hOdLnvYBrJB61far/oP03hlIxrb04OWmDjNTwtmZ3UZdA5MCvI+f+3k2jPTflQ==", + "devOptional": true + }, + "node_modules/@prisma/fetch-engine": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.5.0.tgz", + "integrity": "sha512-3LhYA+FXP6pqY8FLHCjewyE8pGXXJ7BxZw2rhPq+CZAhvflVzq4K8Qly3OrmOkn6wGlz79nyLQdknyCG2HBTuA==", + "devOptional": true, + "dependencies": { + "@prisma/debug": "6.5.0", + "@prisma/engines-version": "6.5.0-73.173f8d54f8d52e692c7e27e72a88314ec7aeff60", + "@prisma/get-platform": "6.5.0" + } + }, + "node_modules/@prisma/get-platform": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.5.0.tgz", + "integrity": "sha512-xYcvyJwNMg2eDptBYFqFLUCfgi+wZLcj6HDMsj0Qw0irvauG4IKmkbywnqwok0B+k+W+p+jThM2DKTSmoPCkzw==", + "devOptional": true, + "dependencies": { + "@prisma/debug": "6.5.0" + } + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -745,6 +1224,73 @@ "integrity": "sha512-kkKUDVlII2DQiKy7UstOR1ErJP8kUKAQ4oa+SQtM0K+lPdmmjj0YnnxBgtTVYH7mUKtbsxeFC9y0AmK7Yb78/A==", "dev": true }, + "node_modules/@supabase/auth-js": { + "version": "2.68.0", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.68.0.tgz", + "integrity": "sha512-odG7nb7aOmZPUXk6SwL2JchSsn36Ppx11i2yWMIc/meUO2B2HK9YwZHPK06utD9Ql9ke7JKDbwGin/8prHKxxQ==", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/functions-js": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.4.4.tgz", + "integrity": "sha512-WL2p6r4AXNGwop7iwvul2BvOtuJ1YQy8EbOd0dhG1oN1q8el/BIRSFCFnWAMM/vJJlHWLi4ad22sKbKr9mvjoA==", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/node-fetch": { + "version": "2.6.15", + "resolved": "https://registry.npmjs.org/@supabase/node-fetch/-/node-fetch-2.6.15.tgz", + "integrity": "sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + } + }, + "node_modules/@supabase/postgrest-js": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-1.19.2.tgz", + "integrity": "sha512-MXRbk4wpwhWl9IN6rIY1mR8uZCCG4MZAEji942ve6nMwIqnBgBnZhZlON6zTTs6fgveMnoCILpZv1+K91jN+ow==", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/realtime-js": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.11.2.tgz", + "integrity": "sha512-u/XeuL2Y0QEhXSoIPZZwR6wMXgB+RQbJzG9VErA3VghVt7uRfSVsjeqd7m5GhX3JR6dM/WRmLbVR8URpDWG4+w==", + "dependencies": { + "@supabase/node-fetch": "^2.6.14", + "@types/phoenix": "^1.5.4", + "@types/ws": "^8.5.10", + "ws": "^8.18.0" + } + }, + "node_modules/@supabase/storage-js": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.7.1.tgz", + "integrity": "sha512-asYHcyDR1fKqrMpytAS1zjyEfvxuOIp1CIXX7ji4lHHcJKqyk+sLl/Vxgm4sN6u8zvuUtae9e4kDxQP2qrwWBA==", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/supabase-js": { + "version": "2.49.1", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.49.1.tgz", + "integrity": "sha512-lKaptKQB5/juEF5+jzmBeZlz69MdHZuxf+0f50NwhL+IE//m4ZnOeWlsKRjjsM0fVayZiQKqLvYdBn0RLkhGiQ==", + "dependencies": { + "@supabase/auth-js": "2.68.0", + "@supabase/functions-js": "2.4.4", + "@supabase/node-fetch": "2.6.15", + "@supabase/postgrest-js": "1.19.2", + "@supabase/realtime-js": "2.11.2", + "@supabase/storage-js": "2.7.1" + } + }, "node_modules/@swc/counter": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", @@ -1039,11 +1585,15 @@ "version": "20.17.16", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.16.tgz", "integrity": "sha512-vOTpLduLkZXePLxHiHsBLp98mHGnl8RptV4YAO3HfKO5UHjDvySGbxKtpYfy8Sx5+WKcgc45qNreJJRVM3L6mw==", - "dev": true, "dependencies": { "undici-types": "~6.19.2" } }, + "node_modules/@types/phoenix": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.6.tgz", + "integrity": "sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==" + }, "node_modules/@types/react": { "version": "19.0.8", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.8.tgz", @@ -1062,6 +1612,14 @@ "@types/react": "^19.0.0" } }, + "node_modules/@types/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8svvI3hMyvN0kKCJMvTJP/x6Y/EoQbepff882wL+Sn5QsXb3etnamgrJq4isrBxSJj5L2AuXcI0+bgkoAXGUJw==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.22.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.22.0.tgz", @@ -2191,7 +2749,7 @@ "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", - "dev": true, + "devOptional": true, "dependencies": { "ms": "^2.1.3" }, @@ -2469,6 +3027,58 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/esbuild": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.1.tgz", + "integrity": "sha512-BGO5LtrGC7vxnqucAe/rmvKdJllfGaYWdyABvyMoXQlfYMb2bbRuReWR5tEGE//4LcNJj9XrkovTqNYRFZHAMQ==", + "devOptional": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.1", + "@esbuild/android-arm": "0.25.1", + "@esbuild/android-arm64": "0.25.1", + "@esbuild/android-x64": "0.25.1", + "@esbuild/darwin-arm64": "0.25.1", + "@esbuild/darwin-x64": "0.25.1", + "@esbuild/freebsd-arm64": "0.25.1", + "@esbuild/freebsd-x64": "0.25.1", + "@esbuild/linux-arm": "0.25.1", + "@esbuild/linux-arm64": "0.25.1", + "@esbuild/linux-ia32": "0.25.1", + "@esbuild/linux-loong64": "0.25.1", + "@esbuild/linux-mips64el": "0.25.1", + "@esbuild/linux-ppc64": "0.25.1", + "@esbuild/linux-riscv64": "0.25.1", + "@esbuild/linux-s390x": "0.25.1", + "@esbuild/linux-x64": "0.25.1", + "@esbuild/netbsd-arm64": "0.25.1", + "@esbuild/netbsd-x64": "0.25.1", + "@esbuild/openbsd-arm64": "0.25.1", + "@esbuild/openbsd-x64": "0.25.1", + "@esbuild/sunos-x64": "0.25.1", + "@esbuild/win32-arm64": "0.25.1", + "@esbuild/win32-ia32": "0.25.1", + "@esbuild/win32-x64": "0.25.1" + } + }, + "node_modules/esbuild-register": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/esbuild-register/-/esbuild-register-3.6.0.tgz", + "integrity": "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==", + "devOptional": true, + "dependencies": { + "debug": "^4.3.4" + }, + "peerDependencies": { + "esbuild": ">=0.12 <1" + } + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -3042,6 +3652,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -3954,7 +4578,7 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true + "devOptional": true }, "node_modules/nanoid": { "version": "3.3.8", @@ -4308,6 +4932,34 @@ "node": ">= 0.8.0" } }, + "node_modules/prisma": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.5.0.tgz", + "integrity": "sha512-yUGXmWqv5F4PByMSNbYFxke/WbnyTLjnJ5bKr8fLkcnY7U5rU9rUTh/+Fja+gOrRxEgtCbCtca94IeITj4j/pg==", + "devOptional": true, + "hasInstallScript": true, + "dependencies": { + "@prisma/config": "6.5.0", + "@prisma/engines": "6.5.0" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=18.18" + }, + "optionalDependencies": { + "fsevents": "2.3.3" + }, + "peerDependencies": { + "typescript": ">=5.1.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -4974,6 +5626,11 @@ "node": ">=8.0" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, "node_modules/ts-api-utils": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.0.tgz", @@ -5093,7 +5750,7 @@ "version": "5.7.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", - "dev": true, + "devOptional": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -5123,8 +5780,7 @@ "node_modules/undici-types": { "version": "6.19.8", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", - "dev": true + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" }, "node_modules/uri-js": { "version": "4.4.1", @@ -5135,6 +5791,20 @@ "punycode": "^2.1.0" } }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -5243,6 +5913,26 @@ "node": ">=0.10.0" } }, + "node_modules/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index d1864bc..1887d5c 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": { @@ -9,6 +9,8 @@ "lint": "next lint" }, "dependencies": { + "@prisma/client": "^6.5.0", + "@supabase/supabase-js": "^2.49.1", "d3": "^7.9.0", "next": "15.1.6", "react": "^19.0.0", @@ -22,6 +24,7 @@ "@types/react-dom": "^19", "eslint": "^9", "eslint-config-next": "15.1.6", + "prisma": "^6.5.0", "typescript": "^5" } } diff --git a/frontend/prisma/schema.prisma b/frontend/prisma/schema.prisma new file mode 100644 index 0000000..2647720 --- /dev/null +++ b/frontend/prisma/schema.prisma @@ -0,0 +1,140 @@ +// This is your Prisma schema file, +// learn more about it in the docs: https://pris.ly/d/prisma-schema + +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +model Program { + id Int @id @default(autoincrement()) + name String + abbreviation String + + + student_count Int? + website_link String? + catalog_link String? + + degrees Degree[] + + @@map("programs") +} + +model Degree { + id Int @id @default(autoincrement()) + type String @db.Text + program_id Int + note String? @db.Text + program Program @relation(fields: [program_id], references: [id]) + concentrations Concentration[] + + @@map("degrees") +} + +model Concentration { + id Int @id @default(autoincrement()) + name String? @db.Text + note String? @db.Text + degree_id Int + description String? @db.Text + degree Degree @relation(fields: [degree_id], references: [id]) + concentration_requirements ConcentrationRequirement[] + + @@map("concentrations") +} + +model ConcentrationRequirement { + id Int @id @default(autoincrement()) + requirement_index Int + concentration_id Int + requirement_id Int + concentration Concentration @relation(fields: [concentration_id], references: [id]) + requirement Requirement @relation(fields: [requirement_id], references: [id]) + + @@map("concentration_requirements") +} + +model Requirement { + id Int @id @default(autoincrement()) + name String + description String? + courses_required_count Int? + subreqs_required_count Int? + checkbox Boolean? + note String? + concentration_requirements ConcentrationRequirement[] + requirement_subrequirements RequirementSubrequirement[] + + @@map("requirements") +} + +model Subrequirement { + id Int @id @default(autoincrement()) + name String? + description String? + courses_required_count Int? + requirement_subrequirements RequirementSubrequirement[] + subrequirement_options SubrequirementOption[] + + @@map("subrequirements") +} + +model RequirementSubrequirement { + id Int @id @default(autoincrement()) + subrequirement_index Int? + description String? + requirement_id Int + subrequirement_id Int + requirement Requirement @relation(fields: [requirement_id], references: [id]) + subrequirement Subrequirement @relation(fields: [subrequirement_id], references: [id]) + + @@map("requirement_subrequirements") +} + +model SubrequirementOption { + id Int @id @default(autoincrement()) + option_index Int? + note String? @db.Text + subrequirement_id Int + option_id Int? + subrequirement Subrequirement @relation(fields: [subrequirement_id], references: [id]) + option Option? @relation(fields: [option_id], references: [id]) + + @@map("subrequirement_options") +} + +model Option { + id Int @id @default(autoincrement()) + option_course_id String? @db.Text + elective_range String? @db.Text + is_any_okay Boolean? + flags String? @db.Text + subrequirement_options SubrequirementOption[] + course Course? @relation(fields: [option_course_id], references: [id]) + + @@map("options") +} + +model Course { + id String @id @db.Text + title String @db.Text + description String? @db.Text + requirements String? @db.Text + professors String? @db.Text + distributions String? @db.Text + flags String? @db.Text + credits Float @db.Real + term String @db.Text + is_colsem Boolean? @default(false) + is_fysem Boolean? @default(false) + is_sysem Boolean? @default(false) + codes String[] @db.Text + options Option[] + + @@map("courses") +} \ No newline at end of file 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/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/courses/[term]/[code]/route.ts b/frontend/src/app/api/courses/[term]/[code]/route.ts new file mode 100644 index 0000000..14820be --- /dev/null +++ b/frontend/src/app/api/courses/[term]/[code]/route.ts @@ -0,0 +1,30 @@ + +import { NextResponse } from "next/server"; +import { supabaseAdmin } from "@/utils/supabase"; + +// API Route: /api/courses/[season]/[code] +export async function GET( + request: Request, + { params }: { params: { term: number; code: string } } +) { + const { term, code } = params; // Extract URL parameters + + try { + // Query Supabase: match season_code & check if code exists in codes array + const { data, error } = await supabaseAdmin + .from("courses") + .select("*") + .eq("term", term) + .contains("codes", [code]); // Assuming "codes" is stored as an array + + if (error) throw error; + + return NextResponse.json(data); + } catch (error) { + console.error("Error fetching course:", error); + return NextResponse.json( + { error: "Failed to fetch course" }, + { status: 500 } + ); + } +} diff --git a/frontend/src/app/api/courses/route.ts b/frontend/src/app/api/courses/route.ts new file mode 100644 index 0000000..bb45f43 --- /dev/null +++ b/frontend/src/app/api/courses/route.ts @@ -0,0 +1,21 @@ + +import { NextResponse } from 'next/server'; +import { supabaseAdmin } from '@/utils/supabase'; + +export async function GET() { + try { + const { data, error } = await supabaseAdmin + .from('courses') + .select('*'); + + if (error) throw error; + + return NextResponse.json(data); + } catch (error) { + console.error('Error fetching courses:', error); + return NextResponse.json( + { error: 'Failed to fetch courses' }, + { status: 500 } + ); + } +} diff --git a/frontend/src/app/api/login/route.ts b/frontend/src/app/api/login/route.ts new file mode 100644 index 0000000..9e7daea --- /dev/null +++ b/frontend/src/app/api/login/route.ts @@ -0,0 +1,19 @@ + +import { NextResponse } from "next/server"; +import { Ryan } from "@/database/mock/data-user"; + +export async function GET() { + 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/api/programs/db-service.ts b/frontend/src/app/api/programs/db-service.ts new file mode 100644 index 0000000..ecc9550 --- /dev/null +++ b/frontend/src/app/api/programs/db-service.ts @@ -0,0 +1,41 @@ +import prisma from '@/database/client' + +export async function fetchProgramHierarchy() { + return prisma.program.findMany({ + include: { + degrees: { + include: { + concentrations: { + include: { + concentration_requirements: { + include: { + requirement: { + include: { + requirement_subrequirements: { + include: { + subrequirement: { + include: { + subrequirement_options: { + include: { + option: { + include: { + course: true + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + }) +} diff --git a/frontend/src/app/api/programs/route.ts b/frontend/src/app/api/programs/route.ts new file mode 100644 index 0000000..f37bfd0 --- /dev/null +++ b/frontend/src/app/api/programs/route.ts @@ -0,0 +1,23 @@ +// route.ts +import { NextResponse } from 'next/server'; +import { fetchProgramHierarchy } from './db-service'; +import { transformProgram, createProgramDict } from './transformers'; + +export async function GET() { + try { + // Fetch data + const enrichedPrograms = await fetchProgramHierarchy(); + + // Transform to frontend types + const transformedPrograms = enrichedPrograms.map(transformProgram); + + // Create program dictionary + const programDict = createProgramDict(transformedPrograms); + + return NextResponse.json(programDict); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + console.error('Error fetching programs:', errorMessage); + return NextResponse.json({ error: 'Failed to fetch programs' }, { status: 500 }); + } +} diff --git a/frontend/src/app/api/programs/transformers.ts b/frontend/src/app/api/programs/transformers.ts new file mode 100644 index 0000000..2c64c13 --- /dev/null +++ b/frontend/src/app/api/programs/transformers.ts @@ -0,0 +1,116 @@ + +// transformers.ts +import { Option, Subrequirement, Requirement, Concentration, Degree, Program, ProgramDict } from "@/types/type-program"; +import { Course } from "@/types/type-user"; + +export function transformCourse(courseData: any): Course | null { + if (!courseData) return null; + + return { + id: courseData.id, + title: courseData.title, + description: courseData.description || "", + requirements: courseData.requirements || "", + professors: courseData.professors || "", + distributions: courseData.distributions || "", + flags: courseData.flags || "", + credits: courseData.credits, + term: courseData.term, + is_colsem: courseData.is_colsem || false, + is_fysem: courseData.is_fysem || false, + is_sysem: courseData.is_sysem || false, + codes: courseData.codes || [] + }; +} + +export function transformOption(optionData: any): Option { + return { + option: optionData.option?.course ? transformCourse(optionData.option.course) : null, + satisfier: null, // This is for student data + elective_range: optionData.option?.elective_range || undefined, + flags: optionData.option?.flags ? optionData.option.flags.split(',').filter(Boolean) : undefined, + is_any_okay: optionData.option?.is_any_okay + }; +} + +export function transformSubrequirement(subrequirementData: any, index: number): Subrequirement { + return { + name: subrequirementData.name || "", + description: subrequirementData.description || "", + courses_required_count: subrequirementData.courses_required_count || 0, + options: subrequirementData.subrequirement_options.map(transformOption), + index: index + }; +} + +export function transformRequirement(requirementData: any, index: number): Requirement { + // Get subrequirements with their indices + const subrequirements = requirementData.requirement_subrequirements.map( + (rs: any) => ({ + data: rs.subrequirement, + index: rs.subrequirement_index || 0 + }) + ); + + // Sort by index if available + subrequirements.sort((a: any, b: any) => a.index - b.index); + + return { + name: requirementData.name || "", + description: requirementData.description || "", + courses_required_count: requirementData.courses_required_count || 0, + subreqs_required_count: requirementData.subreqs_required_count || 0, + checkbox: requirementData.checkbox || false, + subrequirements: subrequirements.map( + (item: any, i: number) => transformSubrequirement(item.data, item.index || i) + ), + index: index + }; +} + +export function transformConcentration(concentrationData: any): Concentration { + // Get requirements with their indices + const requirements = concentrationData.concentration_requirements.map( + (cr: any) => ({ + data: cr.requirement, + index: cr.requirement_index || 0 + }) + ); + + // Sort by index if available + requirements.sort((a: any, b: any) => a.index - b.index); + + return { + name: concentrationData.name || "", + description: concentrationData.description || "", + requirements: requirements.map( + (item: any, i: number) => transformRequirement(item.data, item.index || i) + ) + }; +} + +export function transformDegree(degreeData: any): Degree { + return { + type: degreeData.type || "", + concentrations: degreeData.concentrations.map(transformConcentration) + }; +} + +export function transformProgram(programData: any): Program { + return { + name: programData.name || "", + abbreviation: programData.abbreviation || "", + student_count: programData.student_count || 0, + website_link: programData.website_link || "", + catolog_link: programData.catalog_link || "", // Note: Fixed typo + degrees: programData.degrees.map(transformDegree) + }; +} + +export function createProgramDict(programs: Program[]): ProgramDict { + const programDict: ProgramDict = {}; + programs.forEach(program => { + programDict[program.abbreviation] = program; + }); + return programDict; +} 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..e7b7514 --- /dev/null +++ b/frontend/src/app/courses/CoursesUtils.tsx @@ -0,0 +1,28 @@ + +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(studentCourses => studentCourses.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..e60de5d 100644 --- a/frontend/src/app/courses/page.tsx +++ b/frontend/src/app/courses/page.tsx @@ -3,40 +3,57 @@ import React, { useState, useEffect } from "react"; import Style from "./Courses.module.css"; -import { useAuth } from "../providers"; -import { StudentSemester } from "@/types/type-user"; +import { useAuth } from "@/context/AuthProvider"; +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"; - -function Courses() -{ - const { user, setUser } = useAuth(); - const [renderedSemesters, setRenderedSemesters] = useState([]); - +import YearBox from "./years/YearBox"; + +function Courses(){ + const { user } = useAuth(); + 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(
- +
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.module.css b/frontend/src/app/courses/semester/add-course/AddCourseButton.module.css deleted file mode 100644 index b6dc39b..0000000 --- a/frontend/src/app/courses/semester/add-course/AddCourseButton.module.css +++ /dev/null @@ -1,156 +0,0 @@ - -.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/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/add-course/AddCourseUtils.ts b/frontend/src/app/courses/semester/add-course/AddCourseUtils.ts deleted file mode 100644 index ce7fd3e..0000000 --- a/frontend/src/app/courses/semester/add-course/AddCourseUtils.ts +++ /dev/null @@ -1,28 +0,0 @@ - -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); - // } - } -} 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..a8a48ed --- /dev/null +++ b/frontend/src/app/courses/years/YearBox.tsx @@ -0,0 +1,71 @@ + +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" +import { useAuth } from "@/context/AuthProvider"; + +function RenderSemesters(props: { + edit: boolean; + columns: boolean; + studentYear: StudentYear, + setStudentYears: 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, +}){ + const { user } = useAuth(); + const [renderedSemesters, setRenderedSemesters] = useState(null); + + useEffect(() => { + setRenderedSemesters( + + ); + }, [props.edit, props.columns, props.studentYear, 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..37e6e08 --- /dev/null +++ b/frontend/src/app/courses/years/semester/SemesterBox.tsx @@ -0,0 +1,60 @@ + +import React, {useState, useEffect} from "react"; +import Style from "./SemesterBox.module.css" + +import { StudentSemester, User } from "@/types/type-user"; +import { TransformTermNumber, IsTermActive } from "@/utils/course-display/CourseDisplay"; + +import CourseBox from "./course/CourseBox"; +import AddCourseButton from "./add-course/AddCourseButton"; +import { useAuth } from "@/context/AuthProvider"; + +function RenderCourses(props: { + edit: boolean, + studentSemester: StudentSemester +}){ + const renderedCourses = props.studentSemester.studentCourses.map((studentCourse, index) => ( + + )); + + return( +
+ {renderedCourses} +
+ ); + } + +function SemesterBox(props: { + edit: boolean, + studentSemester: StudentSemester, +}){ + const { user } = useAuth(); + const [renderedCourses, setRenderedCourses] = useState(null); + + useEffect(() => { + setRenderedCourses( + + ); + }, [props.edit, props.studentSemester, user]); + + return( +
+
+ {TransformTermNumber(props.studentSemester.term)} +
+
+ {renderedCourses} + {props.edit && } +
+
+ ); +} + +export default SemesterBox; diff --git a/frontend/src/app/courses/years/semester/add-course/AddCourseButton.module.css b/frontend/src/app/courses/years/semester/add-course/AddCourseButton.module.css new file mode 100644 index 0000000..1f311ce --- /dev/null +++ b/frontend/src/app/courses/years/semester/add-course/AddCourseButton.module.css @@ -0,0 +1,112 @@ + +.Row { + display: flex; + flex-direction: row; +} + +.CourseBox { + display: flex; + flex-direction: row; + + gap: 4px; + + justify-content: center; + align-items: center; + + background: #F5F5F5; + border: black; + + width: max-content; + min-width: 36px; + height: 36px; + + border-radius: 16px; + cursor: pointer; +} + +.DropBox { + display: flex; + flex-direction: row; + + align-items: center; + + border: none; + background-color: white; + + position: relative; + cursor: pointer; + + height: 22px; + width: 80px; + box-sizing: border-box; + + font-size: 12px; + color: black; +} + +.DropdownOptions { + 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; +} + +.DropdownOptions div { + padding: 8px; + cursor: pointer; + color: black; + background-color: white; +} + +.DropdownOptions div:hover { + background-color: #f0f0f0; +} + +.SelectedDropdownOption { + background-color: #93c7ff !important; + color: black; +} + +.InputBox { + background-color: white; + + border: none; + outline: none; + + height: 22px; + box-sizing: border-box; + + font-size: 12px; + font-weight: 500; + +} +.InputBox:focus { + border-color: #aaa; + outline: none; +} +.InputBox::placeholder { + color: grey; + font-style: italic; +} + +.FuncButton { + border: none; + margin: 0; + padding: 0; + border: none; + cursor: pointer; + + width: 20px; + height: 20px; + border-radius: 50%; +} 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..1a19b56 --- /dev/null +++ b/frontend/src/app/courses/years/semester/add-course/AddCourseButton.tsx @@ -0,0 +1,140 @@ + +import { useRef, useState, useEffect } from "react"; +import Style from "./AddCourseButton.module.css"; + +import { useAuth } from "@/context/AuthProvider"; +import { executeAddCourse } from "./AddCourseUtils"; + +export interface AddCourseDisplay { + active: boolean; + termDropVis: boolean; + resultDropVis: boolean; +} + +function AddCourseButton(props: { + term: number +}){ + const { user, setUser } = useAuth(); + + 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("GRADE"); + const resultOptions = ["GRADE", "CR", "D/F"]; + + const catalogTerms = [202501] + + 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})); + } + } + }; + + return( +
+ {!addDisplay.active ? ( +
setAddDisplay((prevState) => ({...prevState, active: true}))} + > + + +
+ ) : ( +
+
+ )} +
+ ); +} + +export default AddCourseButton; diff --git a/frontend/src/app/courses/years/semester/add-course/AddCourseUtils.ts b/frontend/src/app/courses/years/semester/add-course/AddCourseUtils.ts new file mode 100644 index 0000000..bf9cebb --- /dev/null +++ b/frontend/src/app/courses/years/semester/add-course/AddCourseUtils.ts @@ -0,0 +1,72 @@ + +import { User, StudentCourse } from "@/types/type-user"; +import { AddCourseDisplay } from "./AddCourseButton"; + +export function executeAddCourse( + inputRef: React.RefObject, + result: string, + + toTerm: number, + fromTerm: number, + + user: User, + setUser: Function, + + setAddDisplay: Function +) { + if (!inputRef.current) return; + + const code = inputRef.current.value.trim().toUpperCase(); + const strFromTerm = fromTerm.toString(); // Assuming term corresponds to season_code + + // Fetch course from API + fetch(`/api/courses/${strFromTerm}/${code}`) + .then((res) => res.json()) + .then((data) => { + if (!data || data.length === 0) return; // No course found + + const newCourse = data[0]; + + const status = fromTerm === toTerm ? "DA" : "MA"; + const newStudentCourse: StudentCourse = { + course: newCourse, + status, + term: toTerm, + result: result, + }; + const updatedCourses = [...user.FYP.studentCourses, newStudentCourse]; + + setUser({ ...user, FYP: { ...user.FYP, studentCourses: updatedCourses } }); + setAddDisplay((prevState: AddCourseDisplay) => ({ ...prevState, active: false })); + }) + .catch((error) => console.error("Error fetching course:", error)); +} + + +// 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); +// // } +// } +// } diff --git a/frontend/src/app/courses/years/semester/course/CourseBox.module.css b/frontend/src/app/courses/years/semester/course/CourseBox.module.css new file mode 100644 index 0000000..56b7b80 --- /dev/null +++ b/frontend/src/app/courses/years/semester/course/CourseBox.module.css @@ -0,0 +1,46 @@ + +.Row { + display: flex; + flex-direction: row; +} + +.Column { + display: flex; + flex-direction: column; +} + +.CourseBox { + display: flex; + flex-direction: row; + + justify-content: space-between; + align-items: center; + + width: 420px; + height: 36px; + + padding: 0 10px; + border-radius: 16px; +} + +.CourseCode { + font-size: 12px; + font-weight: 500; +} + +.CourseTitle { + font-size: 8px; + font-weight: 500; +} + +.FuncButton { + border: none; + margin: 0; + padding: 0; + border: none; + cursor: pointer; + + width: 20px; + height: 20px; + border-radius: 50%; +} 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..c7c188f --- /dev/null +++ b/frontend/src/app/courses/years/semester/course/CourseBox.tsx @@ -0,0 +1,66 @@ + +import Style from "./CourseBox.module.css"; +import { User, StudentCourse } from "@/types/type-user"; + +import { useAuth } from "@/context/AuthProvider"; + +import { RenderMark, SeasonIcon, GetCourseColor, IsTermActive } from "../../../../../utils/course-display/CourseDisplay"; +import DistributionCircle from "@/components/distribution-circle/DistributionsCircle"; + +function RemoveButton(props: { + studentCourse: StudentCourse; + user: User; + setUser: Function +}){ + const removeStudentCourse = () => { + 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, +}){ + const { user, setUser } = useAuth(); + + 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/graduation/Graduation.module.css b/frontend/src/app/graduation/Graduation.module.css new file mode 100644 index 0000000..85a846c --- /dev/null +++ b/frontend/src/app/graduation/Graduation.module.css @@ -0,0 +1,14 @@ + +.GradPage { + position: absolute; + top: 75px; + + display: flex; + flex-direction: row; + justify-content: center; + + width: 100%; + + padding-top: 50px; + padding-bottom: 200px; +} diff --git a/frontend/src/app/graduation/page.tsx b/frontend/src/app/graduation/page.tsx index 15de9bc..9dbb85b 100644 --- a/frontend/src/app/graduation/page.tsx +++ b/frontend/src/app/graduation/page.tsx @@ -1,10 +1,16 @@ -import NavBar from "@/components/navbar/NavBar" +"use client"; +import { useState, useEffect } from "react"; +import Style from "./Graduation.module.css"; +import NavBar from "@/components/navbar/NavBar"; -export default function Graduation(){ - return( +export default function Graduation() { + return (
- + +
+ +
- ) + ); } diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx index ec5a50e..0aa3c89 100644 --- a/frontend/src/app/layout.tsx +++ b/frontend/src/app/layout.tsx @@ -1,6 +1,7 @@ import "./globals.css"; -import { AuthProvider } from "./providers"; +import { AuthProvider } from "@/context/AuthProvider"; +import { ProgramProvider } from "@/context/ProgramProvider"; export const metadata = { title: "MajorAudit" @@ -11,9 +12,11 @@ export default function RootLayout({children}: {children: React.ReactNode}) return( - - {children} - + + + {children} + + ) 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..81263e4 --- /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 "@/context/AuthProvider"; +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..28296f4 100644 --- a/frontend/src/app/majors/Majors.module.css +++ b/frontend/src/app/majors/Majors.module.css @@ -1,11 +1,70 @@ .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); } + +.Divider { + border-right: 1px solid gray; + + margin-top: 50px; + margin-right: 25px; + margin-left: 25px; + + width: 1px; + height: calc(100% - 100px); +} + +.ListButton { + position: fixed; + cursor: pointer; + + top: 95px; + left: 20px; + + width: 30px; + height: 30px; + + color: white; + text-align: center; + line-height: 30px; + font-size: 20px; + + background-color: #61ADFE; + border: none; + border-radius: 50%; + box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.25); + + z-index: 2; +} + +.FilterButton { + position: fixed; + cursor: pointer; + + top: 140px; + left: 20px; + + width: 30px; + height: 30px; + + color: white; + text-align: center; + line-height: 30px; + font-size: 20px; + + background-color: #61fe73; + border: none; + border-radius: 50%; + box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.25); + + z-index: 2; +} \ No newline at end of file diff --git a/frontend/src/app/majors/MajorsUtils.ts b/frontend/src/app/majors/MajorsUtils.ts new file mode 100644 index 0000000..74cca18 --- /dev/null +++ b/frontend/src/app/majors/MajorsUtils.ts @@ -0,0 +1,36 @@ + +import { MajorsIndex } from "@/types/type-program"; + +export function initializeMajorsIndex( + storedIndex: string | null, + filteredProgKeys: string[] +): MajorsIndex { + let parsedIndex: MajorsIndex = storedIndex + ? JSON.parse(storedIndex) + : { conc: 0, deg: 0, prog: filteredProgKeys[0] }; + + if (!filteredProgKeys.includes(parsedIndex.prog)) { + parsedIndex = { ...parsedIndex, prog: filteredProgKeys[0] }; + } + + return parsedIndex; +} + +export function updateMajorsIndex( + prev: MajorsIndex | null, + newIndex: Partial, + filteredProgKeys: string[] +): MajorsIndex { + if (!prev) return { conc: 0, deg: 0, prog: filteredProgKeys[0] || "" }; + + return { + ...prev, + ...newIndex, + prog: newIndex.prog !== undefined + ? filteredProgKeys[ + (filteredProgKeys.indexOf(newIndex.prog) + filteredProgKeys.length) % filteredProgKeys.length + ] + : prev.prog, + conc: newIndex.conc ?? prev.conc, + }; +} diff --git a/frontend/src/app/majors/course-icon/MajorsCourseIcon.module.css b/frontend/src/app/majors/course-icon/MajorsCourseIcon.module.css new file mode 100644 index 0000000..0e534bf --- /dev/null +++ b/frontend/src/app/majors/course-icon/MajorsCourseIcon.module.css @@ -0,0 +1,57 @@ + +.Icon { + display: flex; + flex-direction: row; + + align-items: center; + justify-content: center; + + font-size: 14px; + font-weight: bold; + + padding: 2px 4px; + border-radius: 15px; + + width: max-content; + min-width: 14px; + height: 18px; + + background-color: #F5F5F5; + transition: filter 0.3s ease; +} + +.Icon:hover { + cursor: pointer; + filter: brightness(95%); +} + +.StudentCourseIcon { + background-color: #E1E9F8; +} + +.RemoveButton { + padding: 0; + border: none; + cursor: pointer; + + margin-right: 2px; + margin-left: 2px; + + width: 15px; + height: 15px; + + border-radius: 50%; +} + +.CodeSearch { + height: 14px; + width: 70px; + + font-size: 12px; + padding: 1px; + + border: none; + outline: none; + background-color: white; + color: black; +} diff --git a/frontend/src/app/majors/course-icon/MajorsCourseIcon.tsx b/frontend/src/app/majors/course-icon/MajorsCourseIcon.tsx new file mode 100644 index 0000000..c07761f --- /dev/null +++ b/frontend/src/app/majors/course-icon/MajorsCourseIcon.tsx @@ -0,0 +1,195 @@ + +import { useState, useRef, useEffect } from "react"; +import Style from "./MajorsCourseIcon.module.css" + +import Image from "next/image"; + +import { Subrequirement } from "@/types/type-program"; +import { StudentCourse, Course } from "@/types/type-user"; + +import DistributionCircle from "@/components/distribution-circle/DistributionsCircle"; + +function SeasonComp(props: { seasons: string[] }) +{ + const seasonImageMap: { [key: string]: string } = { + "Fall": "/fall.svg", + "Spring": "/spring.svg", + }; + + return ( +
+ {props.seasons.map((szn, index) => ( +
0 ? "-7.5px" : 0 }}> + {seasonImageMap[szn] && ( + {szn} + )} +
+ ))} +
+ ); +} + +function CourseIcon(props: { + edit: boolean; + course: Course; + subreq: Subrequirement; + onRemoveCourse: Function; +}){ + return ( +
+ {props.edit && ( + props.onRemoveCourse(props.course, props.subreq, false)} /> + )} + {/* */} + {props.course.codes[0]} +
+ +
+
+ ); +} + +function StudentCourseIcon(props: { + edit: boolean; + studentCourse: StudentCourse; + subreq: Subrequirement; + onRemoveCourse: Function; +}) { + return ( +
+ {/* ✅ Only show remove button in edit mode */} + {props.edit && ( + props.onRemoveCourse(props.studentCourse.course, props.subreq, true)} /> + )} + ✓ {props.studentCourse.course.codes[0]} +
+ ); +} + +function RemoveButton({ onClick }: { onClick: () => void }) { + return ( +
+ ); +} + +function EmptyIcon(props: { + edit: boolean, + onAddCourse: Function, +}){ + const [isAdding, setIsAdding] = useState(false); + const [courseCode, setCourseCode] = useState(""); + const popupRef = useRef(null); + const inputRef = useRef(null); + + useEffect(() => { + if (isAdding) { + inputRef.current?.focus(); + } + }, [isAdding]); + + useEffect(() => { + function handleClickOutside(event: MouseEvent) { + if (popupRef.current && !popupRef.current.contains(event.target as Node)) { + deactivate(); + } + } + + if (isAdding) { + document.addEventListener("mousedown", handleClickOutside); + } + + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, [isAdding]); + + function activate() { + setIsAdding(true); + } + + function deactivate() { + setIsAdding(false); + setCourseCode(""); + } + + function handleAddCourse() { + const success = props.onAddCourse(courseCode.trim().toUpperCase()); + if (success) { + deactivate(); + } + } + + if (!props.edit) { + return
; + } + + return ( +
+ {!isAdding ? ( +
+ + +
+ ) : ( +
+
+ )} +
+ ); +} + +export function MajorsIcon(props: { + edit: boolean; + contentCourse: Course | StudentCourse | null; + subreq: Subrequirement; + onRemoveCourse: Function; + onAddCourse: Function; +}) { + if (!props.contentCourse) { + return ; + } + + const isStudentCourse = "course" in props.contentCourse; + + return isStudentCourse ? ( + + ) : ( + + ); +} diff --git a/frontend/src/app/majors/metadata/Metadata.module.css b/frontend/src/app/majors/metadata/Metadata.module.css index adf4b8b..16658de 100644 --- a/frontend/src/app/majors/metadata/Metadata.module.css +++ b/frontend/src/app/majors/metadata/Metadata.module.css @@ -1,4 +1,22 @@ +.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,26 +70,73 @@ 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 { - font-size: 28px; - margin-right: 10px; - cursor: pointer; - transition: opacity 0.2s; + + + +.ScrollButton { + background-color: white; + margin: 0; + padding: 0; + border: none; + cursor: pointer; +} + +.PinButton { + cursor: pointer; + background-color: white; + border: none; + margin: 0; + padding: 0; + + + font-size: 30px; + margin-right: 6px; +} + + + +/* margin-left: 80px; */ + +.ToggleContainer { + display: flex; + flex-direction: row; + margin-bottom: 8px; } -.thumbtack:hover { - opacity: 0.7; /* Slightly lighter on hover */ +.ToggleOption { + cursor: pointer; + background-color: white; + border: 1px solid rgb(196, 196, 196); + border-radius: 5px; + color: black; + margin: 0; + padding: 4px 10px; + font-size: 14px; + transition: background-color 0.3s, color 0.3s; } -.thumbtack:active { - opacity: 0.5; /* Even lighter on click */ -} \ No newline at end of file +.ToggleOption.Active { + background-color: #598ff4; + color: white; +} + + + + + + +.ProgramOption { + color: gray; + font-size: 20px; + font-weight: bold; + margin-bottom: 4px; + cursor: pointer; +} diff --git a/frontend/src/app/majors/metadata/Metadata.tsx b/frontend/src/app/majors/metadata/Metadata.tsx index e5d4fc4..f09dbf7 100644 --- a/frontend/src/app/majors/metadata/Metadata.tsx +++ b/frontend/src/app/majors/metadata/Metadata.tsx @@ -1,161 +1,241 @@ import { useState, useEffect } from "react"; -import Link from 'next/link'; +import Style from "./Metadata.module.css"; -import { User } from "@/types/type-user"; -import { DegreeMetadata } from "@/types/type-program"; -import { pinProgram, addProgram } from "./MetadataUtils"; +import Link from 'next/link'; +import { Program, ProgramDict } from "@/types/type-program"; +import { MajorsIndex } from "@/types/type-user"; +import { usePrograms } from "@/context/ProgramProvider"; +import { useAuth } from "@/context/AuthProvider"; -import Style from "./Metadata.module.css"; +import { toggleConcentrationPin } from "./MetadataUtils"; function MetadataTopshelf(props: { - user: User, - setUser: Function, - programIndex: number, - degreeMetadata: DegreeMetadata -}) { - return ( -
-
pinProgram(props.programIndex, 0, props.user, props.setUser)}> - -
-
addProgram(props.programIndex, 0, props.user, props.setUser)}> - -
+ program: Program; + index: MajorsIndex; +}){ + const { setUser } = useAuth(); + const { progDict } = usePrograms(); + + function handlePinClick() { + toggleConcentrationPin(setUser, progDict, props.index); + } + + return( +
-
{props.degreeMetadata.name}
+ {/* */} +
+ {props.program.name} +
- {props.degreeMetadata.students} + {props.program.student_count}
MAJOR
- {props.degreeMetadata.abbr} + {props.program.abbreviation}
); } -function MetadataStats(props: { degreeMetadata: DegreeMetadata }){ - return( -
-
- STATS -
-
-
-
- COURSES -
-
- {props.degreeMetadata.stats.courses} -
-
-
-
- RATING -
-
{props.degreeMetadata.stats.rating}
-
-
-
- WORKLOAD -
-
{props.degreeMetadata.stats.workload}
-
-
-
- TYPE -
-
{props.degreeMetadata.stats.type}
-
-
-
- ); -} +// function MetadataStats(props: { concMetadata: ConcMetadata }){ +// return( +//
+//
+// STATS +//
+//
+//
+//
+// COURSES +//
+//
+// {props.concMetadata.stats.courses} +//
+//
+//
+//
+// RATING +//
+//
{props.concMetadata.stats.rating}
+//
+//
+//
+// WORKLOAD +//
+//
{props.concMetadata.stats.workload}
+//
+//
+//
+// TYPE +//
+//
{props.concMetadata.stats.type}
+//
+//
+//
+// ); +// } -function MetadataContent(props: { - user: User, - setUser: Function, - degreeMetadata: DegreeMetadata, - programIndex: number, +function MetadataToggle(props: { + program: Program, + index: MajorsIndex, + setIndex: Function }){ - return ( -
- -
- -
- ABOUT -
-
- {props.degreeMetadata.about} -
-
- DUS -
-
- {props.degreeMetadata.dus.name}; {props.degreeMetadata.dus.address} -
+ return( +
+
+ {props.program.degrees.map((deg, index) => ( + + ))} +
-
-
MAJOR CATALOG
-
MAJOR WEBSITE
+ {props.program.degrees[props.index.deg].concentrations.length > 1 && ( +
+ {props.program.degrees[props.index.deg].concentrations.map((conc, index) => ( + + ))}
+ )} +
+ ); +} + +function MetadataBody(props: { + program: Program, + index: MajorsIndex +}){ + return( + // style={{ marginLeft: "80px" }} +
+ {/* */} +
+ ABOUT
-
- ); +
+ {props.program.degrees[props.index.deg].concentrations[props.index.conc].description} +
+
+ DUS +
+
+ {/* {props.program.prog_data.prog_dus.dus_name}; {props.program.prog_data.prog_dus.dus_email} */} +
+
+
MAJOR CATALOG
+
MAJOR WEBSITE
+
+
+ ); } -function MetadataScrollButton(props: { - shiftProgramIndex: Function; - peekProgram: Function; - dir: number; -}) { +function MetadataScrollButton(props: { + programs: ProgramDict, + index: MajorsIndex, + setIndex: Function; + filteredProgKeys: string[]; + dir: number +}){ + const currentProgIndex = props.filteredProgKeys.indexOf(props.index.prog); + const nextProgIndex = (currentProgIndex + props.dir + props.filteredProgKeys.length) % props.filteredProgKeys.length; + const nextProg = props.filteredProgKeys[nextProgIndex]; + return ( -
props.shiftProgramIndex(props.dir)} - > +
+ ); } +function ProgramList(props: { + programs: ProgramDict, + setIndex: Function +}){ + return ( +
+ {Object.entries(props.programs).map(([progCode, program]) => ( +
props.setIndex({ conc: 0, deg: 0, prog: progCode })} + > + {program.name} ({program.abbreviation}) +
+ ))} +
+ ); +} function Metadata(props: { - user: User, - setUser: Function, - programMetadatas: DegreeMetadata[], - programIndex: number, - shiftProgramIndex: Function, - peekProgram: Function -}) { - return ( -
- - - + listView: boolean, + index: MajorsIndex, + setIndex: Function, + filteredProgKeys: string[], +}){ + const { progDict } = usePrograms(); + const currProgram = progDict[props.index.prog]; + + if (!currProgram) return null; + + return( +
+ {props.listView ? ( + + ) : ( +
+ +
+ + + +
+ +
+ ) + }
); } diff --git a/frontend/src/app/majors/metadata/MetadataUtils.ts b/frontend/src/app/majors/metadata/MetadataUtils.ts new file mode 100644 index 0000000..ed64437 --- /dev/null +++ b/frontend/src/app/majors/metadata/MetadataUtils.ts @@ -0,0 +1,53 @@ + +import { User, StudentConc } from "@/types/type-user"; +import { ProgramDict, MajorsIndex } from "@/types/type-program"; + +export function toggleConcentrationPin( + setUser: Function, + progDict: ProgramDict, + majorsIndex: MajorsIndex +) { + setUser((prevUser: User) => { + const existingConcIndex = prevUser.FYP.decl_list.findIndex( + (sc) => + sc.conc_majors_index.prog === majorsIndex.prog && + sc.conc_majors_index.deg === majorsIndex.deg && + sc.conc_majors_index.conc === majorsIndex.conc + ); + + if(existingConcIndex !== -1){ + return{ + ...prevUser, + FYP: { + ...prevUser.FYP, + decl_list: prevUser.FYP.decl_list.filter((_, idx) => idx !== existingConcIndex), + }, + }; + } + + const program = progDict[majorsIndex.prog]; + if (!program) return prevUser; + + const degree = program.prog_degs[majorsIndex.deg]; + if (!degree) return prevUser; + + const concentration = degree.deg_concs[majorsIndex.conc]; + if (!concentration) return prevUser; + + const newStudentConc: StudentConc = { + conc_majors_index: majorsIndex, + user_status: 1, + user_conc: { ...concentration }, + user_conc_name: concentration.conc_name, + selected_subreqs: {}, + }; + + return{ + ...prevUser, + FYP: { + ...prevUser.FYP, + decl_list: [...prevUser.FYP.decl_list, newStudentConc], + }, + }; + }); +} diff --git a/frontend/src/app/majors/metadata/MetadataUtils.tsx b/frontend/src/app/majors/metadata/MetadataUtils.tsx deleted file mode 100644 index f0ca59c..0000000 --- a/frontend/src/app/majors/metadata/MetadataUtils.tsx +++ /dev/null @@ -1,107 +0,0 @@ - -import { User } from "@/types/type-user"; -import { StudentDegree } from "@/types/type-program"; - -export function pinProgram( - currProgram: number, - currDegree: number, - user: User, - setUser: Function, -) { - // Find if the program is already in degreeDeclarations - const existingDegree = user.FYP.degreeDeclarations.find( - (degree) => degree.programIndex === currProgram - ); - - if (existingDegree) { - if (existingDegree.status === "PIN") { - // Unpin the program (remove it from degreeDeclarations) - const updatedUser: User = { - ...user, - FYP: { - ...user.FYP, - degreeDeclarations: user.FYP.degreeDeclarations.filter( - (degree) => degree.programIndex !== currProgram - ), - }, - }; - setUser(updatedUser); - } - } else { - // Pin the program if it's not already pinned or added - const newStudentDegree: StudentDegree = { - status: "PIN", - programIndex: currProgram, - degreeIndex: currDegree, - }; - - const updatedUser: User = { - ...user, - FYP: { - ...user.FYP, - degreeDeclarations: [...user.FYP.degreeDeclarations, newStudentDegree], - }, - }; - setUser(updatedUser); - } -} - - -// Utility function to add a program -export function addProgram( - currProgram: number, - currDegree: number, - user: User, - setUser: Function -) { - // Find if the program already exists in degreeDeclarations - const existingDegree = user.FYP.degreeDeclarations.find( - (degree) => degree.programIndex === currProgram - ); - - if (existingDegree) { - if (existingDegree.status === "ADD") { - // Unadd the program (remove it from degreeDeclarations) - const updatedUser: User = { - ...user, - FYP: { - ...user.FYP, - degreeDeclarations: user.FYP.degreeDeclarations.filter( - (degree) => degree.programIndex !== currProgram - ), - }, - }; - setUser(updatedUser); - } else if (existingDegree.status === "PIN") { - // Change status from "PIN" to "ADD" - const updatedUser: User = { - ...user, - FYP: { - ...user.FYP, - degreeDeclarations: user.FYP.degreeDeclarations.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: StudentDegree = { - status: "ADD", - programIndex: currProgram, - degreeIndex: currDegree, - }; - - const updatedUser: User = { - ...user, - FYP: { - ...user.FYP, - degreeDeclarations: [...user.FYP.degreeDeclarations, newStudentDegree], - }, - }; - setUser(updatedUser); - } -} diff --git a/frontend/src/app/majors/overhead/Overhead.tsx b/frontend/src/app/majors/overhead/Overhead.tsx index 8845215..2749dc5 100644 --- a/frontend/src/app/majors/overhead/Overhead.tsx +++ b/frontend/src/app/majors/overhead/Overhead.tsx @@ -2,17 +2,17 @@ import Style from "./Overhead.module.css"; import { User } from "@/types/type-user"; -import MajorSearchBar from "./major-search/MajorSearch"; +// import MajorSearchBar from "./major-search/MajorSearch"; import Pinned from "./pinned/Pinned"; -function Overhead(props: { user: User, setProgramIndex: Function }) { +function Overhead(props: { user: User, setIndex: Function }) { return (
- + {/* */}
PINNED
- +
); } diff --git a/frontend/src/app/majors/overhead/major-search/MajorSearch.tsx b/frontend/src/app/majors/overhead/major-search/MajorSearch.tsx index ff42cc7..24b6803 100644 --- a/frontend/src/app/majors/overhead/major-search/MajorSearch.tsx +++ b/frontend/src/app/majors/overhead/major-search/MajorSearch.tsx @@ -2,7 +2,7 @@ import { useState, useEffect, useRef } from "react"; import Style from "./MajorSearch.module.css"; import { User } from "@/types/type-user"; -import { ALL_PROGRAM_METADATAS } from "@/database/data-degree"; +import { ALL_PROGRAM_METADATAS } from "@/database/programs/metas/meta-econ"; function MajorSearchBar(props: { user: User; setProgramIndex: Function }) { const [searchTerm, setSearchTerm] = useState(""); diff --git a/frontend/src/app/majors/overhead/pinned/Pinned.tsx b/frontend/src/app/majors/overhead/pinned/Pinned.tsx index d5f7d1b..f5fd67b 100644 --- a/frontend/src/app/majors/overhead/pinned/Pinned.tsx +++ b/frontend/src/app/majors/overhead/pinned/Pinned.tsx @@ -1,45 +1,22 @@ import Style from "./Pinned.module.css"; -import { User } from "@/types/type-user"; -import { StudentDegree } from "@/types/type-program"; -import { ALL_PROGRAM_METADATAS } from "@/database/data-degree"; +import { User, StudentConc } from "@/types/type-user"; -function DegreeIcon(props: { studentDegree: StudentDegree, setProgramIndex: Function }) { - const mark = (status: string) => { - let mark = ""; - switch (status) { - case "DA": - mark = "✓"; - break; - case "ADD": - mark = "⚠"; - break; - case "PIN": - mark = "📌"; - break; - default: - mark = ""; - } - return ( -
- {mark} -
- ); - }; +function ConcIcon(props: { user: User, setIndex: Function, studentConc: StudentConc }) { return( -
props.setProgramIndex(props.studentDegree.programIndex)}> - {mark(props.studentDegree.status)}{ALL_PROGRAM_METADATAS[props.studentDegree.programIndex][0].abbr} +
props.setIndex(props.studentConc.conc_majors_index)}> + 📌{props.studentConc.user_conc_name}
); } -function Pinned(props: { user: User, setProgramIndex: Function }) { +function Pinned(props: { user: User, setIndex: Function }) { return (
- {props.user.FYP.degreeDeclarations.map((studentDegree, index) => ( - + {props.user.FYP.decl_list.map((studentConc, index) => ( + ))}
); diff --git a/frontend/src/app/majors/page.tsx b/frontend/src/app/majors/page.tsx index 4fb5166..e3e576c 100644 --- a/frontend/src/app/majors/page.tsx +++ b/frontend/src/app/majors/page.tsx @@ -1,12 +1,14 @@ "use client"; -import { useState } from "react"; -import { useAuth } from "../providers"; - -import { DegreeMetadata } from "@/types/type-program"; -import { ALL_PROGRAM_METADATAS } from "@/database/data-degree"; +import { useAuth } from "@/context/AuthProvider"; +import { usePrograms } from "@/context/ProgramProvider"; +import { useState, useEffect } from "react"; import Style from "./Majors.module.css"; + +import { MajorsIndex } from "@/types/type-user"; +import { initializeMajorsIndex, updateMajorsIndex } from "./MajorsUtils"; + import NavBar from "@/components/navbar/NavBar"; import Overhead from "./overhead/Overhead"; import Metadata from "./metadata/Metadata"; @@ -14,37 +16,52 @@ import Requirements from "./requirements/Requirements"; function Majors() { - const { user, setUser } = useAuth(); + const { user } = useAuth(); + const { progDict } = usePrograms(); + + const progKeys = Object.keys(progDict); + const [filteredProgKeys, setFilteredProgKeys] = useState(progKeys); + const [index, setIndex] = useState(null); + const [listView, setListView] = useState(false); - const [programIndex, setProgramIndex] = useState(0); + useEffect(() => { + if(progKeys.length > 0){ + setFilteredProgKeys(progKeys); + } + }, [progDict]); + + useEffect(() => { + if (typeof window !== "undefined" && filteredProgKeys.length > 0) { + const storedIndex = sessionStorage.getItem("majorsIndex"); + setIndex(initializeMajorsIndex(storedIndex, filteredProgKeys)); + } + }, [filteredProgKeys]); - const allProgramMetadatas: DegreeMetadata[][] = ALL_PROGRAM_METADATAS; + useEffect(() => { + if(typeof window !== "undefined" && index !== null){ + sessionStorage.setItem("majorsIndex", JSON.stringify(index)); + } + }, [index]); - const shiftProgramIndex = (dir: number) => { - setProgramIndex((programIndex + dir + allProgramMetadatas.length) % allProgramMetadatas.length); - }; + const updateIndex = (newIndex: Partial) => { + setListView(false); + setIndex((prev) => updateMajorsIndex(prev, newIndex, filteredProgKeys)); + }; - const peekProgram = (dir: number) => { - return allProgramMetadatas[(programIndex + dir + allProgramMetadatas.length) % allProgramMetadatas.length][0]; - }; + if (index === null || !filteredProgKeys.length) return null; - return( + return(
- }/> + }/>
- - +
setListView((prev) => !prev)}/> + +
+
); diff --git a/frontend/src/app/majors/requirements/Requirements.module.css b/frontend/src/app/majors/requirements/Requirements.module.css index 8d4817d..b1a3058 100644 --- a/frontend/src/app/majors/requirements/Requirements.module.css +++ b/frontend/src/app/majors/requirements/Requirements.module.css @@ -1,60 +1,146 @@ -.row{ +.Column { + display: flex; + flex-direction: column; +} + +.Row{ display: flex; flex-direction: row; } -.reqsList { - height: 430px; +.RequirementsContainer { + /* border: 1px solid green; */ + margin-top: 37px; + width: 600px; + height: 650px; +} + +.ReqsList { + /* border-bottom: 1px solid grey; */ + margin-left: 30px; + + /* height: 750px; scrollbar-color: rgb(131, 131, 131) transparent; scrollbar-width: thin; - overflow-y: scroll; - padding-right: 7px; - overflow-x: hidden; + overflow-y: scroll; */ +} + +.SubreqsList { + margin-left: 30px; } -.subsectionHeader { +.ReqHeader { color: grey; font-size: 18px; font-weight: 501; } -.reqsContainer { - padding: 20px; - border-radius: 25px; - width: 490px; - height: 490px; - min-width: 390px; - background-color: white; - box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.5); +.SubHeader { + color: grey; + font-size: 14px; + font-weight: 500; } +.SubDesc { + color: grey; + font-style: italic; + font-size: 12px; + font-weight: 500; + margin-bottom: 4px; +} + + + + + + + + + + + + + +.ToggleButton { + margin-top: 3px; + + cursor: pointer; + background: none; + border: none; + color: #65a8f0; + font-size: 14px; +} .ButtonRow { display: flex; - flex-direction: row; + gap: 6px; + margin-bottom: 2px; } -.resetButton { - margin-right: 10px; /* Add margin to the right of the reset button */ - opacity: 0; /* Make it invisible */ - pointer-events: none; /* Make it unclickable */ + + + +.SubreqSelectionRow { + display: flex; + gap: 5px; + margin-bottom: 8px; + flex-wrap: wrap; } -.editButton { - margin-right: 0; /* Remove margin for the last child */ +.SubreqOption { + cursor: pointer; + background-color: rgb(211, 211, 211); + color: white; + border: none; + border-radius: 6px; + margin: 0; + padding: 3px; + font-size: 10px; + transition: background-color 0.3s ease; } -.resetButton.visible { - opacity: 1; /* Make it visible */ - pointer-events: auto; /* Make it clickable */ +.SubreqOption.Selected { + background-color: rgb(100, 178, 238); } -.resetButton:hover, .editButton:hover { - color: #666; /* Slightly lighter color on hover */ - cursor: pointer; /* Show pointer cursor on hover */ + + + + + + + + + + + + + + + + + + +.RequirementsContainerHeader { + display: flex; + flex-direction: row; + justify-content: space-between; + margin-bottom: 10px; + font-size: 30px; + /* padding: 2px 8px; */ } -.resetButton:active, .editButton:active { - color: #333; /* Darker color on click */ +.GoldBackground { + /* background-color: rgba(255, 215, 0, 0.3); + border-radius: 2px; */ +} + +.EditButton { + background-color: white; + margin: 0; + padding: 0; + border: none; + cursor: pointer; + font-size: 30px; } diff --git a/frontend/src/app/majors/requirements/Requirements.tsx b/frontend/src/app/majors/requirements/Requirements.tsx index a8aa215..616dc3f 100644 --- a/frontend/src/app/majors/requirements/Requirements.tsx +++ b/frontend/src/app/majors/requirements/Requirements.tsx @@ -1,51 +1,218 @@ -import { useState } from "react"; +"use client"; +import { useAuth } from "@/context/AuthProvider"; +import { usePrograms } from "@/context/ProgramProvider"; + +import { useState, useEffect } from "react"; import Style from "./Requirements.module.css"; -import { User } from "@/types/type-user"; -import { DegreeConfiguration } from "@/types/type-program"; -// import AddableCourse from "./icons/AddableCourse"; -// import RemovableCourse from "./icons/RemovableCourse"; -// import { addCourseToSubsection, removeCourseFromSubsection, resetDegree } from "./RequirementsUtils"; +import { Course } from "@/types/type-user"; +import { Subrequirement, Requirement, Option } from "@/types/type-program"; +import { MajorsIndex } from "@/types/type-user"; + +import { getStudentConcentration, removeCourseInSubreq, addCourseInSubreq, toggleSubreqSelection } from "./RequirementsUtils"; +import { MajorsIcon } from "../course-icon/MajorsCourseIcon"; -function RequirementsContent(props: { +function RenderSubrequirementCourse(props: { + edit?: boolean; + option: Option; + subreq: Subrequirement; + onRemoveCourse: Function, + onAddCourse: Function +}){ + return ( +
+ +
+ ); +} + +function RenderSubrequirement(props: { edit: boolean, - degreeConfiguration: DegreeConfiguration, - user: User, - setUser: Function + majorsIndex: MajorsIndex, + reqIndex: number, + subreqIndex: number, + subreq: Subrequirement }){ + const { setUser } = useAuth(); + + function handleRemoveCourse(course: Course | null, isStudentCourse: boolean = false){ + removeCourseInSubreq(setUser, props.majorsIndex, props.reqIndex, props.subreqIndex, course, isStudentCourse); + } + + function handleAddCourse(courseCode: string){ + return addCourseInSubreq(setUser, props.majorsIndex, props.reqIndex, props.subreqIndex, courseCode); + } + + const satisfiedCount = props.subreq.options.filter(opt => opt.satisfier !== null).length; + const filteredCourses = satisfiedCount >= props.subreq.courses_required_count + ? props.subreq.options.filter(opt => opt.satisfier !== null) + : props.subreq.options; return( -
- +
+
+ {satisfiedCount}|{props.subreq.courses_required_count} {props.subreq.name} +
+
+ {props.subreq.description} +
+
+ {filteredCourses.map((option, option_index) => ( + + ))} +
); } -function Requirements(props: { - user: User, - setUser: Function, - degreeConfiguration: DegreeConfiguration +function RenderRequirement(props: { + edit: boolean, + majorsIndex: MajorsIndex, + reqIndex: number, + req: Requirement }){ - - const [edit, setEdit] = useState(false); - const updateEdit = () => { - setEdit(!edit); - }; + const { user, setUser } = useAuth(); + const userConc = getStudentConcentration(user, props.majorsIndex); + const selectedSubreqs = userConc?.selected_subreqs[props.reqIndex] ?? []; + + const visibleSubreqs = selectedSubreqs.length > 0 + ? props.req.subrequirements.filter((_, i) => selectedSubreqs.includes(i)) + : props.req.subrequirements; + + function handleToggleSubreq(subreqIndex: number) { + if(userConc && props.edit){ + toggleSubreqSelection( + setUser, + props.majorsIndex, + props.reqIndex, + subreqIndex, + props.req.subreqs_required_count ?? props.req.subrequirements.length + ); + } + } + + let dynamicRequiredCount: number | string = props.req.courses_required_count; + if(props.req.courses_required_count === -1){ + dynamicRequiredCount = selectedSubreqs.length > 0 + ? selectedSubreqs.reduce((sum, idx) => sum + props.req.subrequirements[idx].courses_required_count, 0) + : "~"; + } + + const dynamicSatisfiedCount = props.req.subrequirements.reduce( + (sum, subreq) => sum + subreq.options.filter(option => option.satisfier !== null).length, + 0 + ); return( -
-
-
- Requirements -
-
-
- ⚙ -
+
+
+
+ {props.req.name} +
+
+ {props.req.checkbox + ? dynamicSatisfiedCount === dynamicRequiredCount ? "✅" : "❌" + : `${dynamicSatisfiedCount}|${dynamicRequiredCount}` + } +
+
+
+ {props.req.description} +
+ + {(props.req.subreqs_required_count !== undefined && props.req.subreqs_required_count < props.req.subrequirements.length) && ( +
+ {props.req.subrequirements.map((subreq, i) => ( + + ))}
+ )} + +
+ {visibleSubreqs.map((subreq: Subrequirement, subreqIndex: number) => ( + + ))} +
+
+ ); +} + +function Requirements(props: { + majorsIndex: MajorsIndex | null +}){ + if(props.majorsIndex == null){ + return( +
+ ) + } + + const [edit, setEdit] = useState(false); + const { user } = useAuth(); + const { progDict } = usePrograms(); + + useEffect(() => { + setEdit(false); + }, [props.majorsIndex]); + + // const userConc = getStudentConcentration(user, props.majorsIndex); + const program = progDict[props.majorsIndex.prog]; + const conc = program?.degrees[props.majorsIndex.deg].concentrations[props.majorsIndex.conc]; + + // const conc = userConc + // ? userConc.user_conc + // : program?.prog_degs[props.majorsIndex.deg].deg_concs[props.majorsIndex.conc]; + + return( +
+
+
+ Requirements +
+ {/* {userConc && + + } */}
- +
+ {conc.requirements.map((req: Requirement, reqIndex: number) => ( + + ))} +
); } diff --git a/frontend/src/app/majors/requirements/RequirementsUtils.ts b/frontend/src/app/majors/requirements/RequirementsUtils.ts new file mode 100644 index 0000000..9794b41 --- /dev/null +++ b/frontend/src/app/majors/requirements/RequirementsUtils.ts @@ -0,0 +1,182 @@ + +import { MajorsIndex } from "@/types/type-program"; +import { Course, User, StudentConc } from "@/types/type-user"; +import { ConcentrationSubrequirement } from "@/types/type-program"; + +export function getStudentConcentration( + user: User, + majorsIndex: MajorsIndex +): StudentConc | undefined { + return user.FYP.decl_list.find( + (sc) => + sc.conc_majors_index.prog === majorsIndex.prog && + sc.conc_majors_index.deg === majorsIndex.deg && + sc.conc_majors_index.conc === majorsIndex.conc + ); +} + +function updateUserWithNewSubreq( + prevUser: User, + majorsIndex: MajorsIndex, + reqIndex: number, + subreqIndex: number, + updatedSubreq: ConcentrationSubrequirement +): User { + const matchingConc = getStudentConcentration(prevUser, majorsIndex); + if (!matchingConc) return prevUser; + + return { + ...prevUser, + FYP: { + ...prevUser.FYP, + decl_list: prevUser.FYP.decl_list.map((studentConc) => + studentConc === matchingConc + ? { + ...studentConc, + user_conc: { + ...studentConc.user_conc, + conc_reqs: studentConc.user_conc.conc_reqs.map((req, idx) => + idx === reqIndex + ? { + ...req, + subreqs_list: req.subreqs_list.map((subreq, sidx) => + sidx === subreqIndex ? updatedSubreq : subreq + ), + } + : req + ), + }, + } + : studentConc + ), + }, + }; +} + +export function addCourseInSubreq( + setUser: Function, + majorsIndex: MajorsIndex, + reqIndex: number, + subreqIndex: number, + courseCode: string +) { + setUser((prevUser: User) => { + const userConc = getStudentConcentration(prevUser, majorsIndex); + if (!userConc) return prevUser; + + const requirement = userConc.user_conc.conc_reqs[reqIndex]; + const subreq = requirement.subreqs_list[subreqIndex]; + + const matchingStudentCourse = prevUser.FYP.studentCourses.find((sc) => + sc.course.codes.includes(courseCode) + ); + + if (!matchingStudentCourse) return prevUser; + + // ✅ Find first empty slot in subreq_options + const updatedSubreqOptions = subreq.subreq_options.map((opt) => { + if (opt.o === null && opt.s === null) { + return { ...opt, o: matchingStudentCourse.course, s: matchingStudentCourse }; + } + return opt; + }); + + return updateUserWithNewSubreq(prevUser, majorsIndex, reqIndex, subreqIndex, { + ...subreq, + subreq_options: updatedSubreqOptions + }); + }); +} + +export function removeCourseInSubreq( + setUser: Function, + majorsIndex: MajorsIndex, + reqIndex: number, + subreqIndex: number, + course: Course | null, + isStudentCourse?: boolean +){ + setUser((prevUser: User) => { + const userConc = getStudentConcentration(prevUser, majorsIndex); + if (!userConc) return prevUser; + + const requirement = userConc.user_conc.conc_reqs[reqIndex]; + const subreq = requirement.subreqs_list[subreqIndex]; + + // ✅ Remove student course from `student_courses_satisfying` + const updatedStudentCourses = isStudentCourse + ? subreq.subreq_options.filter((opt) => opt.s?.course !== course).map(opt => opt.s) + : subreq.subreq_options.map(opt => opt.s); + + // ✅ Remove the course from `subreq_options` + let updatedSubreqOptions = subreq.subreq_options.map(opt => + opt.o === course ? { ...opt, o: null, s: null } : opt + ); + + // ✅ Ensure nulls don't exceed `subreq_courses_req_count` + const nullCount = updatedSubreqOptions.filter(opt => opt.o === null).length; + if (nullCount > subreq.subreq_courses_req_count) { + updatedSubreqOptions = updatedSubreqOptions + .filter(opt => opt.o !== null) + .concat(Array(subreq.subreq_courses_req_count).fill({ o: null, s: null })); + } + + return updateUserWithNewSubreq(prevUser, majorsIndex, reqIndex, subreqIndex, { + ...subreq, + subreq_options: updatedSubreqOptions, + }); + }); +} + + +export function toggleSubreqSelection( + setUser: Function, + majorsIndex: MajorsIndex, + reqIndex: number, + subreqIndex: number, + maxSelected: number +) { + setUser((prevUser: User) => { + // ✅ Find the matching concentration + const userConc = getStudentConcentration(prevUser, majorsIndex); + if (!userConc) return prevUser; // 🚨 Safety check + + // ✅ Create a fresh copy of selected_subreqs + const updatedSelectedSubreqs = { ...userConc.selected_subreqs }; + + // ✅ Get currently selected subreqs or default to an empty array + const currentSelected = updatedSelectedSubreqs[reqIndex] ?? []; + + let newSelected = [...currentSelected]; + + if (newSelected.includes(subreqIndex)) { + // ✅ Remove if already selected + newSelected = newSelected.filter((i) => i !== subreqIndex); + } else if (newSelected.length < maxSelected) { + // ✅ Add if under max limit + newSelected.push(subreqIndex); + } + + // ✅ Ensure `selected_subreqs` is always properly structured + updatedSelectedSubreqs[reqIndex] = newSelected.length > 0 ? newSelected : []; + + // ✅ Return a fully **new** user object with updates applied + return { + ...prevUser, + FYP: { + ...prevUser.FYP, + decl_list: prevUser.FYP.decl_list.map((studentConc) => + studentConc.conc_majors_index.prog === majorsIndex.prog && + studentConc.conc_majors_index.deg === majorsIndex.deg && + studentConc.conc_majors_index.conc === majorsIndex.conc + ? { + ...studentConc, + selected_subreqs: updatedSelectedSubreqs, // ✅ Fully replace with new object + } + : studentConc + ), + }, + }; + }); +} + diff --git a/frontend/src/app/not-found.tsx b/frontend/src/app/not-found.tsx new file mode 100644 index 0000000..f8da259 --- /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 "@/context/AuthProvider"; + +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..b58e4b4 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -2,20 +2,18 @@ "use client"; import { useEffect } from "react"; import { useRouter } from "next/navigation"; -import { useAuth } from "./providers"; +import { useAuth } from "@/context/AuthProvider"; -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 deleted file mode 100644 index 7b73a64..0000000 --- a/frontend/src/app/providers.tsx +++ /dev/null @@ -1,25 +0,0 @@ - -"use client"; -import { createContext, useContext, useState } from "react"; - -import { User } from "../types/type-user"; -import { Ryan } from "./../database/data-user"; - -const AuthContext = createContext(null); - -export function AuthProvider({children}: {children: React.ReactNode}) -{ - const [auth, setAuth] = useState({ loggedIn: true, onboard: true }); - const [user, setUser] = useState(Ryan); - - return( - - {children} - - ); -} - -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..a4dac89 100644 --- a/frontend/src/components/course-icon/CourseIcon.module.css +++ b/frontend/src/components/course-icon/CourseIcon.module.css @@ -9,47 +9,16 @@ width: max-content; padding: 2px 4px; + min-height: 18px; font-size: 14px; font-weight: bold; background-color: #F5F5F5; transition: filter 0.4s ease; - - margin-right: 2px; } .CourseIcon:hover { cursor: pointer; filter: brightness(95%); } - -.Mark { - padding-left: 1px; - padding-right: 1px; -} - - - - -.OrIconContainer { - display: flex; - align-items: center; - padding: 2px 4px; - border-radius: 15px; - background-color: #e3e3e3; - transition: filter 0.4s ease; - margin-right: 2px; -} - -.OrIconContainer:hover { - cursor: pointer; - filter: brightness(95%); -} - -.OrSeparator { - margin-bottom: 3px; - margin-right: 2px; - font-size: 14px; - font-weight: bold; -} \ No newline at end of file diff --git a/frontend/src/components/course-icon/CourseIcon.tsx b/frontend/src/components/course-icon/CourseIcon.tsx index cdaf3f3..fd16aae 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/course-display/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/distribution-circle/DistributionsCircle.tsx b/frontend/src/components/distribution-circle/DistributionsCircle.tsx index 3824bdc..b7db107 100644 --- a/frontend/src/components/distribution-circle/DistributionsCircle.tsx +++ b/frontend/src/components/distribution-circle/DistributionsCircle.tsx @@ -25,7 +25,12 @@ function getData(distributions: string[]) { const width = 12; const height = 12; -export default function DistributionCircle(props: { distributions: string[] }) { +export default function DistributionCircle(props: { distributions: string[] }) +{ + if(props.distributions.length == 0){ + return
; + } + const radius = Math.min(width, height) / 2; const data = useMemo(() => getData(props.distributions), [props.distributions]); @@ -48,7 +53,7 @@ export default function DistributionCircle(props: { distributions: string[] }) { return (
- + {arcs.map((arc: string | null, i: number) => arc ? : null 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/context/AuthProvider.tsx b/frontend/src/context/AuthProvider.tsx new file mode 100644 index 0000000..52ab836 --- /dev/null +++ b/frontend/src/context/AuthProvider.tsx @@ -0,0 +1,62 @@ + +"use client"; +import { createContext, useContext, useState, useEffect, useCallback } from "react"; +import { User } from "@/types/type-user"; +import { Ryan } from "@/database/mock/data-user"; +import { usePrograms } from "@/context/ProgramProvider"; +import { fill } from "@/utils/preprocessing/Fill"; + +// Define context type +interface AuthContextType { + auth: { loggedIn: boolean }; + setAuth: (auth: { loggedIn: boolean }) => void; + user: User; + setUser: (user: User) => void; +} + +const AuthContext = createContext(null); + +export function AuthProvider({ children }: { children: React.ReactNode }) { + // const { setProgDict, baseProgDict } = usePrograms(); + const [auth, setAuth] = useState({ loggedIn: false }); + const [user, setUser] = useState(Ryan); + + // Create a stable reference to the combined reset and fill function + // const resetAndFill = useCallback(() => { + // const studentCourses = user.FYP.studentCourses; + + // if (studentCourses.length > 0) { + // // Create a deep clone of baseProgDict + // const freshCopy = JSON.parse(JSON.stringify(baseProgDict)); + + // // Pass the fresh copy to fill - the fill function will handle the state update + // fill(studentCourses, freshCopy, setProgDict); + // } + // }, [user.FYP.studentCourses, baseProgDict, setProgDict]); + + // Set initial user data + useEffect(() => { + setUser(Ryan); + }, []); + + // Update program data when courses change + // useEffect(() => { + // if (user.FYP.studentCourses.length > 0) { + // resetAndFill(); + // } + // }, [user.FYP.studentCourses, resetAndFill]); + + return ( + + {children} + + ); +} + +export function useAuth(): AuthContextType { + const context = useContext(AuthContext); + if (!context) { + throw new Error('useAuth must be used within an AuthProvider'); + } + return context; +} diff --git a/frontend/src/context/ProgramProvider.tsx b/frontend/src/context/ProgramProvider.tsx new file mode 100644 index 0000000..20a6743 --- /dev/null +++ b/frontend/src/context/ProgramProvider.tsx @@ -0,0 +1,71 @@ + +"use client"; +import { createContext, useContext, useState, useEffect, useCallback } from "react"; +import { ProgramDict } from "@/types/type-program"; + +// Define Context Type +interface ProgramContextType { + progDict: ProgramDict; + setProgDict: (dict: ProgramDict) => void; + baseProgDict: ProgramDict; + isLoading: boolean; + error: string | null; + resetToBase: () => void; +} + +const ProgramContext = createContext(null); + +export function ProgramProvider({ children }: { children: React.ReactNode }) { + const [isLoading, setIsLoading] = useState(true); + const [baseProgDict, setBaseProgDict] = useState({}); + const [progDict, setProgDict] = useState({}); + const [error, setError] = useState(null); + + useEffect(() => { + async function fetchPrograms() { + setIsLoading(true); + try { + const response = await fetch('/api/programs'); + if (!response.ok) throw new Error('Failed to fetch programs.'); + + const fetchedData = await response.json(); + + // Store as separate objects to prevent reference issues. + setBaseProgDict(JSON.parse(JSON.stringify(fetchedData))); + setProgDict(JSON.parse(JSON.stringify(fetchedData))); + } catch (err) { + setError(err instanceof Error ? err.message : 'Unknown error'); + } finally { + setIsLoading(false); + } + } + + fetchPrograms(); + }, []); + + // Deep clone when resetting to base. + const resetToBase = useCallback(() => { + setProgDict(JSON.parse(JSON.stringify(baseProgDict))); + }, [baseProgDict]); + + return ( + + {children} + + ); +} + +export function usePrograms(): ProgramContextType { + const context = useContext(ProgramContext); + if (!context) { + throw new Error('usePrograms must be used within a ProgramProvider'); + } + return context; +} diff --git a/frontend/src/database/client.ts b/frontend/src/database/client.ts new file mode 100644 index 0000000..d98b570 --- /dev/null +++ b/frontend/src/database/client.ts @@ -0,0 +1,11 @@ + +import { PrismaClient } from '@prisma/client' + +// Prevent multiple instances during dev hot reloading +const globalForPrisma = global as unknown as { prisma: PrismaClient } + +export const prisma = globalForPrisma.prisma || new PrismaClient() + +if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma + +export default prisma diff --git a/frontend/src/database/data-catalog.ts b/frontend/src/database/data-catalog.ts deleted file mode 100644 index 212e244..0000000 --- a/frontend/src/database/data-catalog.ts +++ /dev/null @@ -1,21 +0,0 @@ - -import { Course } from "@/types/type-user"; - -interface Catalog { - number: number; - courses: Course[]; -} - -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: [] }] }, -] - -export const getCatalogCourse = (catalogNumber: number, courseCode: string): Course | null => { - - const catalog = Catalogs.find((cat) => cat.number === catalogNumber); - if (!catalog) return null; - - const course = catalog.courses.find((course) => course.codes.includes(courseCode)); - return course || null; -}; diff --git a/frontend/src/database/data-degree.ts b/frontend/src/database/data-degree.ts deleted file mode 100644 index 1536dd2..0000000 --- a/frontend/src/database/data-degree.ts +++ /dev/null @@ -1,49 +0,0 @@ - -import { DegreeMetadata } from "@/types/type-program"; - -export const ALL_PROGRAM_METADATAS: DegreeMetadata[][] = [ - [{ - name: "Computer Science", - abbr: "CPSC", - degreeType: "BACH_ART", - dus: { address: "AKW 208 432-6400", email: "cpsc.yale.edu", name: "Y. Richard Yang" }, - about: "The Department of Computer Science offers a B.A. degree program, as well as four combined major programs in cooperation with other departments: Electrical Engineering and Computer Science, Computer Science and Economics, Computer Science and Mathematics, and Computer Science and Psychology. Each program not only provides a solid technical education but also allows students either to take a broad range of courses in other disciplines or to complete the requirements of a second major.", - stats: { courses: 10, rating: 0, type: "QR", workload: 0 }, - students: 0, - wesbiteLink: "http://cpsc.yale.edu", - catologLink: "https://catalog.yale.edu/ycps/subjects-of-instruction/computer-science/", - }], - [{ - name: "History", - abbr: "HIST", - degreeType: "BACH_ART", - dus: { address: "AKW 208 432-6400", email: "cpsc.yale.edu", name: "Y. Richard Yang" }, - about: "The History major is for students who understand that shaping the future requires knowing the past. History courses explore many centuries of human experimentation and ingenuity, from the global to the individual scale. History majors learn to be effective storytellers and analysts, and to craft arguments that speak to broad audiences. They make extensive use of Yale’s vast library resources to create pioneering original research projects. Students of history learn to think about politics and government, sexuality, the economy, cultural and intellectual life, war and society, and other themes in broadly humanistic—rather than narrowly technocratic—ways.", - stats: { courses: 10, rating: 0, type: "QR", workload: 0 }, - students: 0, - wesbiteLink: "http://cpsc.yale.edu", - catologLink: "https://catalog.yale.edu/ycps/subjects-of-instruction/computer-science/", - }], - [{ - name: "Political Science", - abbr: "PLSC", - degreeType: "BACH_ART", - dus: { address: "AKW 208 432-6400", email: "cpsc.yale.edu", name: "Y. Richard Yang" }, - about: "Political science addresses how individuals and groups organize, allocate, and challenge the power to make collective decisions involving public issues. The goal of the major is to enable students to think critically and analytically about the agents, incentives, and institutions that shape political phenomena within human society. The subfields of political philosophy and analytical political theory (which includes the study of both qualitative and quantitative methodology) support the acquisition of the lenses through which such thought skills can be enriched.", - stats: { courses: 10, rating: 0, type: "QR", workload: 0 }, - students: 0, - wesbiteLink: "http://cpsc.yale.edu", - catologLink: "https://catalog.yale.edu/ycps/subjects-of-instruction/computer-science/", - }], - [{ - name: "Environmental Studies", - abbr: "EVST", - degreeType: "BACH_ART", - dus: { address: "AKW 208 432-6400", email: "cpsc.yale.edu", name: "Y. Richard Yang" }, - about: "Environmental Studies offers the opportunity to examine human relations with their environments from diverse perspectives. The major encourages interdisciplinary study in (1) social sciences, including anthropology, political science, law, economics, and ethics; (2) humanities, to include history, literature, religion, and the arts; and (3) natural sciences, such as biology, ecology, human health, geology, and chemistry. Students work with faculty advisers and the directors of undergraduate studies (DUS) to concentrate on some of the most pressing environmental and sustainability problems of our time.", - stats: { courses: 10, rating: 0, type: "QR", workload: 0 }, - students: 0, - wesbiteLink: "http://cpsc.yale.edu", - catologLink: "https://catalog.yale.edu/ycps/subjects-of-instruction/computer-science/", - }] -]; diff --git a/frontend/src/database/data-user.ts b/frontend/src/database/data-user.ts deleted file mode 100644 index dd7f056..0000000 --- a/frontend/src/database/data-user.ts +++ /dev/null @@ -1,26 +0,0 @@ - -import { User } from "./../types/type-user"; - -export const Ryan: User = { - name: "Ryan", - netID: "rgg32", - onboard: true, - 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: [] } }] }, - ], - languageRequirement: "", - degreeDeclarations: [], - degreeConfigurations: [ - [ - ], - [ - ], - [ - ], - [ - ] - ], - } -} diff --git a/frontend/src/database/mock/data-courses.ts b/frontend/src/database/mock/data-courses.ts new file mode 100644 index 0000000..60a119e --- /dev/null +++ b/frontend/src/database/mock/data-courses.ts @@ -0,0 +1,53 @@ + +// COURSES + +// // PLSC PROGRAM +// export const PLSC_474: Course = { codes: ["PSLC 474"], title: "", credit: 1, dist: ["So"], seasons: ["Spring"]} +// export const PLSC_490: Course = { codes: ["PSLC 490"], title: "", credit: 1, dist: ["So"], seasons: ["Fall", "Spring"]} +// export const PLSC_493: Course = { codes: ["PSLC 493"], title: "", credit: 1, dist: ["So"], 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: "", credit: 1, dist: ["QR"], seasons: ["Fall", "Spring"] } +// export const MATH_244: Course = { codes: ["MATH 244"], title: "", credit: 1, dist: ["QR"], seasons: ["Fall", "Spring"] } +// export const CPSC_223: Course = { codes: ["CPSC 223"], title: "", credit: 1, dist: ["QR"], seasons: ["Fall", "Spring"] } +// export const CPSC_323: Course = { codes: ["CPSC 323"], title: "", credit: 1, dist: ["QR"], seasons: ["Fall", "Spring"] } +// export const CPSC_365: Course = { codes: ["CPSC 365"], title: "", credit: 1, dist: ["QR"], seasons: ["Fall", "Spring"] } +// export const CPSC_366: Course = { codes: ["CPSC 366"], title: "", credit: 1, dist: ["QR"], seasons: ["Fall", "Spring"] } +// export const CPSC_381: Course = { codes: ["CPSC 381"], title: "", 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"] } + +// // ECON PROGRAM +// export const MATH_110: Course = { codes: ["MATH 110"], title: "", credit: 1, dist: ["QR"], seasons: ["Fall", "Spring"] } +// export const MATH_111: Course = { codes: ["MATH 111"], title: "", credit: 1, dist: ["QR"], seasons: ["Fall", "Spring"] } +// export const MATH_112: Course = { codes: ["MATH 112"], title: "", credit: 1, dist: ["QR"], seasons: ["Fall", "Spring"] } +// export const MATH_115: Course = { codes: ["MATH 115"], title: "", credit: 1, dist: ["QR"], seasons: ["Fall", "Spring"] } +// export const MATH_116: Course = { codes: ["MATH 116"], title: "", credit: 1, dist: ["QR"], seasons: ["Fall", "Spring"] } +// export const ENAS_151: Course = { codes: ["ENAS 151"], title: "", credit: 1, dist: ["QR"], seasons: ["Fall", "Spring"] } +// export const MATH_118: Course = { codes: ["MATH 118"], title: "", credit: 1, dist: ["QR"], seasons: ["Fall", "Spring"] } +// export const MATH_120: Course = { codes: ["MATH 120"], title: "", credit: 1, dist: ["QR"], seasons: ["Fall", "Spring"] } +// export const ECON_108: Course = { codes: ["ECON 108"], title: "", credit: 1, dist: ["QR"], seasons: ["Fall", "Spring"] } +// export const ECON_110: Course = { codes: ["ECON 110"], title: "", credit: 1, dist: ["QR"], seasons: ["Fall", "Spring"] } +// export const ECON_115: Course = { codes: ["ECON 115"], title: "", credit: 1, dist: ["QR"], seasons: ["Fall", "Spring"] } +// export const ECON_111: Course = { codes: ["ECON 111"], title: "", credit: 1, dist: ["QR"], seasons: ["Fall", "Spring"] } +// export const ECON_116: Course = { codes: ["ECON 116"], title: "", credit: 1, dist: ["QR"], seasons: ["Fall", "Spring"] } +// export const ECON_121: Course = { codes: ["ECON 121"], title: "", credit: 1, dist: ["QR"], seasons: ["Fall", "Spring"] } +// export const ECON_125: Course = { codes: ["ECON 125"], title: "", credit: 1, dist: ["QR"], seasons: ["Fall", "Spring"] } +// export const ECON_122: Course = { codes: ["ECON 122"], title: "", credit: 1, dist: ["QR"], seasons: ["Fall", "Spring"] } +// export const ECON_126: Course = { codes: ["ECON 126"], title: "", credit: 1, dist: ["QR"], seasons: ["Fall", "Spring"] } +// export const ECON_117: Course = { codes: ["ECON 117"], title: "", credit: 1, dist: ["QR"], seasons: ["Fall", "Spring"] } +// export const ECON_123: Course = { codes: ["ECON 123"], title: "", credit: 1, dist: ["QR"], seasons: ["Fall", "Spring"] } +// export const ECON_136: Course = { codes: ["ECON 136"], title: "", credit: 1, dist: ["QR"], seasons: ["Fall", "Spring"] } + +// // STUDENT COURSES + +// // CPSC 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: "DA", result: "GRADE_PASS", course: CPSC_323 } +// export const SC_CPSC_381: StudentCourse = { term: 202503, status: "DA", result: "GRADE_PASS", course: CPSC_381 } +// export const SC_CPSC_490: StudentCourse = { term: 202503, status: "DA", result: "GRADE_PASS", course: CPSC_490 } + +// // ECON COURSES +// export const SC_ECON_110: StudentCourse = { term: 202403, status: "DA", result: "GRADE_PASS", course: ECON_110 } diff --git a/frontend/src/database/mock/data-user.ts b/frontend/src/database/mock/data-user.ts new file mode 100644 index 0000000..6db59b5 --- /dev/null +++ b/frontend/src/database/mock/data-user.ts @@ -0,0 +1,19 @@ + +import { User } from "./../types/type-user"; + +export const Ryan: User = { + name: "Ryan", + netID: "rgg32", + onboard: false, + FYP: { + studentCourses: [], + studentTermArrangement: { + first_year: [0, 202403, 202501], + sophomore: [0, 202503, 202601], + junior: [0, 202603, 202701], + senior: [0, 202703, 202801], + }, + languagePlacement: { language: "Spanish", level: 5 }, + decl_list: [], + } +} diff --git a/frontend/src/database/mock/programs/concs/concs-cpsc.ts b/frontend/src/database/mock/programs/concs/concs-cpsc.ts new file mode 100644 index 0000000..74dd885 --- /dev/null +++ b/frontend/src/database/mock/programs/concs/concs-cpsc.ts @@ -0,0 +1,174 @@ + +// import { ConcentrationSubrequirement, ConcentrationRequirement, DegreeConcentration } from "@/types/type-program"; + +// // CORE + +// const CORE_1: ConcentrationSubrequirement = { +// subreq_name: "INTRO", +// subreq_desc: "", +// subreq_flex: false, +// subreq_courses_req_count: 1, +// subreq_options: [ +// { +// option: null, +// satisfier: null, +// } +// ] +// } + +// const CORE_2: ConcentrationSubrequirement = { +// subreq_name: "DISCRETE MATH", +// subreq_desc: "", +// subreq_flex: false, +// subreq_courses_req_count: 1, +// subreq_options: [ +// { +// o: null, +// s: null, +// }, +// { +// o: null, +// s: null, +// }, +// ] +// } + +// const CORE_3: ConcentrationSubrequirement = { +// subreq_name: "DATA STRUCTURES", +// subreq_desc: "", +// subreq_flex: false, +// subreq_courses_req_count: 1, +// subreq_options: [ +// { +// o: null, +// s: null, +// } +// ] +// } + +// const CORE_4: ConcentrationSubrequirement = { +// subreq_name: "SYSTEMS", +// subreq_desc: "", +// subreq_flex: false, +// subreq_courses_req_count: 1, +// subreq_options: [ +// { +// o: null, +// s: null, +// } +// ] +// } + +// const CORE_5: ConcentrationSubrequirement = { +// subreq_name: "ALGORITHMS", +// subreq_desc: "", +// subreq_flex: false, +// subreq_courses_req_count: 1, +// subreq_options: [ +// { +// o: null, +// s: null, +// }, +// { +// o: null, +// s: null, +// }, +// ] +// } + +// const CPSC_CORE: ConcentrationRequirement = { +// req_name: "CORE", +// req_desc: "", +// courses_required_count: 5, +// subreqs_list: [CORE_1, CORE_2, CORE_3, CORE_4, CORE_5] +// } + +// // ELECTIVE + +// const ELEC_MULT_BA: ConcentrationSubrequirement = { +// subreq_name: "", +// subreq_desc: "Intermediate or advanced CPSC courses, traditionally numbered 300+.", +// subreq_flex: false, +// subreq_courses_req_count: 3, +// subreq_options: [ +// { o: null, s: null, n: { e: { dept: "CPSC", min: 300, max: 999 } } }, +// { o: null, s: null, n: { e: { dept: "CPSC", min: 300, max: 999 } } }, +// { o: null, s: null, n: { e: { dept: "CPSC", min: 300, max: 999 } } }, +// ] +// } + +// const ELEC_MULT_BS: ConcentrationSubrequirement = { +// subreq_name: "", +// subreq_desc: "Intermediate or advanced CPSC courses, traditionally numbered 300+.", +// subreq_flex: false, +// subreq_courses_req_count: 5, +// subreq_options: [ +// { o: null, s: null, n: { e: { dept: "CPSC", min: 300, max: 999 } } }, +// { o: null, s: null, n: { e: { dept: "CPSC", min: 300, max: 999 } } }, +// { o: null, s: null, n: { e: { dept: "CPSC", min: 300, max: 999 } } }, +// { o: null, s: null, n: { e: { dept: "CPSC", min: 300, max: 999 } } }, +// { o: null, s: null, n: { e: { dept: "CPSC", min: 300, max: 999 } } }, +// ] +// } + +// const ELEC_SUB: ConcentrationSubrequirement = { +// subreq_name: "", +// subreq_desc: "Standard elective or DUS approved extra-department substitution.", +// subreq_flex: true, +// subreq_courses_req_count: 1, +// subreq_options: [ +// { o: null, s: null, n: { e: { dept: "CPSC", min: 300, max: 999 }, a: true } }, +// ] +// } + +// const CPSC_BA_ELEC: ConcentrationRequirement = { +// req_name: "ELECTIVE", +// req_desc: "", +// courses_required_count: 4, +// subreqs_list: [ELEC_SUB, ELEC_MULT_BA] +// } + +// const CPSC_BS_ELEC: ConcentrationRequirement = { +// req_name: "ELECTIVE", +// req_desc: "", +// courses_required_count: 6, +// subreqs_list: [ELEC_SUB, ELEC_MULT_BS] +// } + +// // SENIOR + +// const SEN_PROJ: ConcentrationSubrequirement = { +// subreq_name: "SENIOR PROJECT", +// subreq_desc: "", +// subreq_flex: false, +// subreq_courses_req_count: 1, +// subreq_options: [ +// { +// o: null, +// s: null, +// } +// ] +// } + +// const CPSC_SENIOR: ConcentrationRequirement = { +// req_name: "SENIOR", +// req_desc: "", +// courses_required_count: 1, +// subreqs_list: [SEN_PROJ] +// } + +// // EXPORT + +// export const CONC_CPSC_BA_I: DegreeConcentration = { +// user_status: 1, +// conc_name: "", +// conc_desc: "", +// conc_reqs: [CPSC_CORE, CPSC_BA_ELEC, CPSC_SENIOR] +// } + +// export const CONC_CPSC_BS_I: DegreeConcentration = { +// user_status: 0, +// conc_name: "", +// conc_desc: "", +// conc_reqs: [CPSC_CORE, CPSC_BS_ELEC, CPSC_SENIOR] +// } diff --git a/frontend/src/database/mock/programs/concs/concs-econ.ts b/frontend/src/database/mock/programs/concs/concs-econ.ts new file mode 100644 index 0000000..8e25edd --- /dev/null +++ b/frontend/src/database/mock/programs/concs/concs-econ.ts @@ -0,0 +1,203 @@ + +// import { ConcentrationSubrequirement, ConcentrationRequirement, DegreeConcentration } from "@/types/type-program"; +// import { ECON_108, ECON_110, ECON_111, ECON_115, ECON_116, ECON_117, ECON_121, ECON_122, ECON_123, ECON_125, ECON_126, ECON_136, MATH_110, MATH_111, MATH_112, MATH_115, MATH_116, MATH_118, MATH_120, ENAS_151, SC_ECON_110 } from "../../data-courses"; + +// // // INTRO + +// const INTRO_1: ConcentrationSubrequirement = { +// subreq_name: "MATH", +// subreq_desc: "118 or 120 recommended. Any MATH 200+ satisfies.", +// subreq_flex: true, +// subreq_courses_req_count: 1, +// subreq_options: [ +// { +// o: MATH_118, +// s: null, +// }, +// { +// o: MATH_120, +// s: null, +// }, +// ] +// } + +// const INTRO_2: ConcentrationSubrequirement = { +// subreq_name: "INTRO MICRO", +// subreq_desc: "", +// subreq_flex: true, +// subreq_courses_req_count: 1, +// subreq_options: [ +// { +// o: ECON_108, +// s: null, +// }, +// { +// o: ECON_110, +// s: null, +// }, +// { +// o: ECON_115, +// s: null, +// }, +// ] +// } + +// const INTRO_3: ConcentrationSubrequirement = { +// subreq_name: "INTRO MACRO", +// subreq_desc: "", +// subreq_flex: true, +// subreq_courses_req_count: 1, +// subreq_options: [ +// { +// o: ECON_111, +// s: null, +// }, +// { +// o: ECON_116, +// s: null, +// }, +// ] +// } + +// const ECON_INTRO: ConcentrationRequirement = { +// req_name: "INTRO", +// req_desc: "", +// courses_required_count: 3, +// subreqs_list: [INTRO_1, INTRO_2, INTRO_3] +// } + +// // // CORE + +// const CORE_MICRO: ConcentrationSubrequirement = { +// subreq_name: "INTERMEDIATE MICRO", +// subreq_desc: "", +// subreq_flex: false, +// subreq_courses_req_count: 1, +// subreq_options: [ +// { +// o: ECON_121, +// s: null, +// }, +// { +// o: ECON_125, +// s: null, +// }, +// ] +// } + +// const CORE_MACRO: ConcentrationSubrequirement = { +// subreq_name: "INTERMEDIATE MACRO", +// subreq_desc: "", +// subreq_flex: false, +// subreq_courses_req_count: 1, +// subreq_options: [ +// { +// o: ECON_122, +// s: null, +// }, +// { +// o: ECON_126, +// s: null, +// }, +// ] +// } + +// const CORE_METRICS: ConcentrationSubrequirement = { +// subreq_name: "ECONOMETRICS", +// subreq_desc: "", +// subreq_flex: false, +// subreq_courses_req_count: 1, +// subreq_options: [ +// { +// o: ECON_117, +// s: null, +// }, +// { +// o: ECON_123, +// s: null, +// }, +// { +// o: ECON_136, +// s: null, +// }, +// ] +// } + +// const ECON_CORE: ConcentrationRequirement = { +// req_name: "CORE", +// req_desc: "", +// courses_required_count: 3, +// subreqs_list: [CORE_MICRO, CORE_MACRO, CORE_METRICS] +// } + +// // // ELECTIVE + +// const ELEC_SUB: ConcentrationSubrequirement = { +// subreq_name: "", +// subreq_desc: "Standard elective or DUS approved extra-department substitution.", +// subreq_flex: true, +// subreq_courses_req_count: 1, +// subreq_options: [ +// { o: null, s: null, n: { e: { dept: "ECON", min: 123, max: 399 }, a: true } }, +// ] +// } + +// const ELEC_STAN: ConcentrationSubrequirement = { +// subreq_name: "", +// subreq_desc: "Intermediate or advanced ECON courses, traditionally numbered 123+.", +// subreq_flex: true, +// subreq_courses_req_count: 3, +// subreq_options: [ +// { o: null, s: null, n: { e: { dept: "ECON", min: 123, max: 399 } } }, +// { o: null, s: null, n: { e: { dept: "ECON", min: 123, max: 399 } } }, +// { o: null, s: null, n: { e: { dept: "ECON", min: 123, max: 399 } } }, +// ] +// } + +// const ECON_ELECTIVE: ConcentrationRequirement = { +// req_name: "ELECTIVE", +// req_desc: "", +// courses_required_count: 4, +// subreqs_list: [ELEC_STAN, ELEC_SUB] +// } + +// // SENIOR + +// const SEN_REQ: ConcentrationSubrequirement = { +// subreq_name: "SENIOR REQUIREMENT", +// subreq_desc: "", +// subreq_flex: true, +// subreq_courses_req_count: 2, +// subreq_options: [ +// { +// o: null, +// s: null, +// n: { +// e: { dept: "ECON", min: 400, max: 491 } +// } +// }, +// { +// o: null, +// s: null, +// n: { +// e: { dept: "ECON", min: 400, max: 491 } +// } +// }, +// ] +// } + +// const ECON_SEN: ConcentrationRequirement = { +// req_name: "SENIOR", +// req_desc: "", +// courses_required_count: 2, +// subreqs_list: [SEN_REQ] +// } + +// // // // FINAL + +// export const CONC_ECON_BA_I: DegreeConcentration = { +// user_status: 0, +// conc_name: "", +// conc_desc: "", +// conc_reqs: [ECON_INTRO, ECON_CORE, ECON_ELECTIVE, ECON_SEN] +// } diff --git a/frontend/src/database/mock/programs/concs/concs-hist.ts b/frontend/src/database/mock/programs/concs/concs-hist.ts new file mode 100644 index 0000000..5d4bbb6 --- /dev/null +++ b/frontend/src/database/mock/programs/concs/concs-hist.ts @@ -0,0 +1,235 @@ +// import { ConcentrationRequirement, ConcentrationSubrequirement, DegreeConcentration } from "@/types/type-program"; + +// // PRE + +// const PRE_REQ: ConcentrationSubrequirement = { +// subreq_name: "PRE 1800", +// subreq_desc: "", +// courses_required: 2, +// courses_options: [null, null], +// courses_elective_range: null, +// courses_any_bool: true, +// student_courses_satisfying: [], +// } + +// const HIST_PRE: ConcentrationRequirement = { +// req_name: "PREINDUSTRIAL", +// req_desc: "", +// courses_required_count: 2, +// courses_satisfied_count: 0, +// checkbox: true, +// subreqs_list: [PRE_REQ] +// } + +// // GLOB CORE + +// const GLOB_CORE_AFRICA: ConcentrationSubrequirement = { +// subreq_name: "AFRICA", +// subreq_desc: "", +// courses_required: 1, +// courses_options: [null], +// courses_elective_range: null, +// courses_any_bool: true, +// student_courses_satisfying: [], +// } + +// const GLOB_CORE_ASIA: ConcentrationSubrequirement = { +// subreq_name: "ASIA", +// subreq_desc: "", +// courses_required: 1, +// courses_options: [null], +// courses_elective_range: null, +// courses_any_bool: true, +// student_courses_satisfying: [], +// } + +// const GLOB_CORE_EURO: ConcentrationSubrequirement = { +// subreq_name: "EUROPE", +// subreq_desc: "", +// courses_required: 1, +// courses_options: [null], +// courses_elective_range: null, +// courses_any_bool: true, +// student_courses_satisfying: [], +// } + +// const GLOB_CORE_LA: ConcentrationSubrequirement = { +// subreq_name: "LATIN AMERICA", +// subreq_desc: "", +// courses_required: 1, +// courses_options: [null], +// courses_elective_range: null, +// courses_any_bool: true, +// student_courses_satisfying: [], +// } + +// const GLOB_CORE_ME: ConcentrationSubrequirement = { +// subreq_name: "MIDDLE EAST", +// subreq_desc: "", +// courses_required: 1, +// courses_options: [null], +// courses_elective_range: null, +// courses_any_bool: true, +// student_courses_satisfying: [], +// } + +// const GLOB_CORE_US: ConcentrationSubrequirement = { +// subreq_name: "U.S.", +// subreq_desc: "", +// courses_required: 1, +// courses_options: [null], +// courses_elective_range: null, +// courses_any_bool: true, +// student_courses_satisfying: [], +// } + +// const HIST_GLOB_CORE: ConcentrationRequirement = { +// req_name: "GLOBAL", +// req_desc: "", +// courses_required_count: 5, +// courses_satisfied_count: 0, +// subreqs_required_count: 5, +// subreqs_satisfied_count: 0, +// subreqs_list: [GLOB_CORE_AFRICA, GLOB_CORE_ASIA, GLOB_CORE_EURO, GLOB_CORE_LA, GLOB_CORE_ME, GLOB_CORE_US] +// } + +// // SPEC CORE + +// const SPEC_CORE_IN: ConcentrationSubrequirement = { +// subreq_name: "REGION OR PATHWAY", +// subreq_desc: "", +// courses_required: 5, +// courses_options: [null, null, null, null, null], +// courses_elective_range: null, +// courses_any_bool: false, +// student_courses_satisfying: [], +// } + +// const SPEC_CORE_OUT: ConcentrationSubrequirement = { +// subreq_name: "OUTSIDE", +// subreq_desc: "", +// courses_required: 2, +// courses_options: [null, null], +// courses_elective_range: null, +// courses_any_bool: false, +// student_courses_satisfying: [], +// } + +// const HIST_SPEC_CORE: ConcentrationRequirement = { +// req_name: "SPECIALIST", +// req_desc: "", +// courses_required_count: 7, +// courses_satisfied_count: 0, +// subreqs_required_count: 2, +// subreqs_satisfied_count: 0, +// subreqs_list: [SPEC_CORE_IN, SPEC_CORE_OUT] +// } + +// // SEMINAR + +// const SEM_REQ: ConcentrationSubrequirement = { +// subreq_name: "DEPARTMENTAL", +// subreq_desc: "", +// courses_required: 2, +// courses_options: [null, null], +// courses_elective_range: null, +// courses_any_bool: false, +// student_courses_satisfying: [] +// } + +// const HIST_SEM: ConcentrationRequirement = { +// req_name: "SEMINAR", +// req_desc: "", +// courses_required_count: 2, +// courses_satisfied_count: 0, +// checkbox: true, +// subreqs_list: [SEM_REQ] +// } + +// // GLOB ELEC + +// const GLOB_ELEC_REQ: ConcentrationSubrequirement = { +// subreq_name: "", +// subreq_desc: "", +// courses_required: 5, +// courses_options: [null, null, null, null, null], +// courses_elective_range: null, +// courses_any_bool: false, +// student_courses_satisfying: [] +// } + +// const HIST_GLOB_ELEC: ConcentrationRequirement = { +// req_name: "ELECTIVE", +// req_desc: "", +// courses_required_count: 5, +// courses_satisfied_count: 0, +// subreqs_list: [GLOB_ELEC_REQ] +// } + +// // SPEC ELEC + +// const SPEC_ELEC_REQ: ConcentrationSubrequirement = { +// subreq_name: "", +// subreq_desc: "", +// courses_required: 3, +// courses_options: [null, null, null], +// courses_elective_range: null, +// courses_any_bool: false, +// student_courses_satisfying: [] +// } + +// const HIST_SPEC_ELEC: ConcentrationRequirement = { +// req_name: "ELECTIVE", +// req_desc: "", +// courses_required_count: 3, +// courses_satisfied_count: 0, +// subreqs_list: [SPEC_ELEC_REQ] +// } + +// // SENIOR + +// const SEN_ONE: ConcentrationSubrequirement = { +// subreq_name: "ONE TERM", +// subreq_desc: "", +// courses_required: 1, +// courses_options: [null], +// courses_elective_range: null, +// courses_any_bool: false, +// student_courses_satisfying: [] +// } + +// const SEN_TWO: ConcentrationSubrequirement = { +// subreq_name: "TWO TERM", +// subreq_desc: "", +// courses_required: 2, +// courses_options: [null, null], +// courses_elective_range: null, +// courses_any_bool: false, +// student_courses_satisfying: [] +// } + +// const HIST_SEN: ConcentrationRequirement = { +// req_name: "SENIOR", +// req_desc: "", +// courses_required_count: -1, +// courses_satisfied_count: 0, +// subreqs_required_count: 1, +// subreqs_satisfied_count: 0, +// subreqs_list: [SEN_ONE, SEN_TWO] +// } + +// // EXPORT + +// export const CONC_HIST_BA_GLOB: DegreeConcentration = { +// user_status: 0, +// conc_name: "GLOBALIST", +// conc_desc: "", +// conc_reqs: [HIST_PRE, HIST_GLOB_CORE, HIST_SEM, HIST_GLOB_ELEC, HIST_SEN] +// } + +// export const CONC_HIST_BA_SPEC: DegreeConcentration = { +// user_status: 0, +// conc_name: "SPECIALIST", +// conc_desc: "", +// conc_reqs: [HIST_PRE, HIST_SPEC_CORE, HIST_SEM, HIST_SPEC_ELEC, HIST_SEN] +// } diff --git a/frontend/src/database/mock/programs/concs/concs-plsc.ts b/frontend/src/database/mock/programs/concs/concs-plsc.ts new file mode 100644 index 0000000..993068d --- /dev/null +++ b/frontend/src/database/mock/programs/concs/concs-plsc.ts @@ -0,0 +1,222 @@ + +// import { DegreeConcentration, ConcentrationRequirement, ConcentrationSubrequirement } from "@/types/type-program"; +// import { PLSC_474, PLSC_490, PLSC_493 } from "@/database/data-courses"; + +// // INTRO + +// const INTRO_REQ: ConcentrationSubrequirement = { +// subreq_name: "INTRO COURSES", +// subreq_desc: "", +// courses_required: 2, +// courses_options: [null, null], +// courses_elective_range: null, +// courses_any_bool: false, +// student_courses_satisfying: [], +// } + +// const PLSC_INTRO: ConcentrationRequirement = { +// req_name: "INTRO", +// req_desc: "", +// courses_required_count: 3, +// courses_satisfied_count: 1, +// subreqs_list: [INTRO_REQ] +// } + +// // CORE + +// const CORE_LECT: ConcentrationSubrequirement = { +// subreq_name: "CORE LECTURES", +// subreq_desc: "", +// courses_required: 2, +// courses_options: [null, null], +// courses_elective_range: null, +// courses_any_bool: false, +// student_courses_satisfying: [], +// } + +// const CORE_METH: ConcentrationSubrequirement = { +// subreq_name: "METHODS AND FORMAL THEORY", +// subreq_desc: "", +// courses_required: 1, +// courses_options: [null], +// courses_elective_range: null, +// courses_any_bool: false, +// student_courses_satisfying: [], +// } + +// const PLSC_CORE_STAN: ConcentrationRequirement = { +// req_name: "CORE", +// req_desc: "", +// courses_required_count: 3, +// courses_satisfied_count: 0, +// subreqs_list: [CORE_LECT, CORE_METH] +// } + +// const CORE_RESE: ConcentrationSubrequirement = { +// subreq_name: "RESEARCH", +// subreq_desc: "", +// courses_required: 1, +// courses_options: [PLSC_474], +// courses_elective_range: null, +// courses_any_bool: false, +// student_courses_satisfying: [], +// } + +// const PLSC_CORE_INTE: ConcentrationRequirement = { +// req_name: "CORE", +// req_desc: "", +// courses_required_count: 4, +// courses_satisfied_count: 0, +// subreqs_list: [CORE_LECT, CORE_METH, CORE_RESE] +// } + +// // ELECTIVE + +// const SUB_INTL: ConcentrationSubrequirement = { +// subreq_name: "INTERNATIONAL RELATIONS", +// subreq_desc: "", +// courses_required: 2, +// courses_options: [null, null], +// courses_elective_range: null, +// courses_any_bool: false, +// student_courses_satisfying: [] +// } + +// const SUB_US: ConcentrationSubrequirement = { +// subreq_name: "AMERICAN GOVERNMENT", +// subreq_desc: "", +// courses_required: 2, +// courses_options: [null, null], +// courses_elective_range: null, +// courses_any_bool: false, +// student_courses_satisfying: [] +// } + +// const SUB_PHIL: ConcentrationSubrequirement = { +// subreq_name: "POLITICAL PHILOSOPHY", +// subreq_desc: "", +// courses_required: 2, +// courses_options: [null, null], +// courses_elective_range: null, +// courses_any_bool: false, +// student_courses_satisfying: [] +// } + +// const SUB_COMP: ConcentrationSubrequirement = { +// subreq_name: "COMPARATIVE POLITICS", +// subreq_desc: "", +// courses_required: 2, +// courses_options: [null, null], +// courses_elective_range: null, +// courses_any_bool: false, +// student_courses_satisfying: [] +// } + +// const PLSC_SUB: ConcentrationRequirement = { +// req_name: "SUBFIELDS", +// req_desc: "", +// courses_required_count: 4, +// courses_satisfied_count: 0, +// subreqs_required_count: 2, +// subreqs_satisfied_count: 0, +// subreqs_list: [SUB_INTL, SUB_US, SUB_PHIL, SUB_COMP] +// } + +// // SEMINAR + +// const SEM_ANY: ConcentrationSubrequirement = { +// subreq_name: "YEAR ANY", +// subreq_desc: "", +// courses_required: 1, +// courses_options: [null], +// courses_elective_range: null, +// courses_any_bool: false, +// student_courses_satisfying: [] +// } + +// const SEM_SEN: ConcentrationSubrequirement = { +// subreq_name: "YEAR SENIOR", +// subreq_desc: "", +// courses_required: 1, +// courses_options: [null], +// courses_elective_range: null, +// courses_any_bool: false, +// student_courses_satisfying: [] +// } + +// const PLSC_SEMINAR: ConcentrationRequirement = { +// req_name: "SEMINAR", +// req_desc: "Seminar courses taught by PLSC faculty satisfy.", +// courses_required_count: 2, +// courses_satisfied_count: 0, +// checkbox: true, +// subreqs_list: [SEM_ANY, SEM_SEN] +// } + +// // SEN STANDARD + +// const SEN_STAN_ONE: ConcentrationSubrequirement = { +// subreq_name: "ONE TERM", +// subreq_desc: "", +// courses_required: 1, +// courses_options: [null], +// courses_elective_range: null, +// courses_any_bool: false, +// student_courses_satisfying: [] +// } + +// const SEN_STAN_TWO: ConcentrationSubrequirement = { +// subreq_name: "TWO TERM", +// subreq_desc: "", +// courses_required: 2, +// courses_options: [null, null], +// courses_elective_range: null, +// courses_any_bool: false, +// student_courses_satisfying: [] +// } + +// const PLSC_SEN_STAN: ConcentrationRequirement = { +// req_name: "SENIOR", +// req_desc: "", +// courses_required_count: -1, +// courses_satisfied_count: 0, +// subreqs_required_count: 1, +// subreqs_satisfied_count: 0, +// subreqs_list: [SEN_STAN_ONE, SEN_STAN_TWO] +// } + +// // SEN INTENSIVE + +// const SEN_INTE_REQ: ConcentrationSubrequirement = { +// subreq_name: "TWO TERM", +// subreq_desc: "", +// courses_required: 2, +// courses_options: [PLSC_490, PLSC_493], +// courses_elective_range: null, +// courses_any_bool: false, +// student_courses_satisfying: [] +// } + +// const PLSC_SEN_INTE: ConcentrationRequirement = { +// req_name: "SENIOR", +// req_desc: "", +// courses_required_count: 2, +// courses_satisfied_count: 0, +// subreqs_list: [SEN_INTE_REQ] +// } + +// // EXPORT + +// export const CONC_PLSC_BA_STAN: DegreeConcentration = { +// user_status: 0, +// conc_name: "STANDARD", +// conc_desc: "", +// conc_reqs: [PLSC_INTRO, PLSC_CORE_STAN, PLSC_SUB, PLSC_SEMINAR, PLSC_SEN_STAN] +// } + +// export const CONC_PLSC_BA_INTE: DegreeConcentration = { +// user_status: 0, +// conc_name: "INTENSIVE", +// conc_desc: "", +// conc_reqs: [PLSC_INTRO, PLSC_CORE_INTE, PLSC_SUB, PLSC_SEMINAR, PLSC_SEN_INTE] +// } diff --git a/frontend/src/database/mock/programs/data-program.ts b/frontend/src/database/mock/programs/data-program.ts new file mode 100644 index 0000000..2a93a60 --- /dev/null +++ b/frontend/src/database/mock/programs/data-program.ts @@ -0,0 +1,74 @@ + +import { Program, ProgramDict } from "@/types/type-program"; +// import { CONC_CPSC_BA_I, CONC_CPSC_BS_I } from "./concs/concs-cpsc"; +// import { CONC_ECON_BA_I } from "./concs/concs-econ"; +// // import { CONC_HIST_BA_GLOB, CONC_HIST_BA_SPEC } from "./concs/concs-hist"; +// // import { CONC_PLSC_BA_INTE, CONC_PLSC_BA_STAN } from "./concs/concs-plsc"; + +// const PROG_CPSC: Program = { +// prog_data: { +// prog_name: "Computer Science", +// prog_abbr: "CPSC", +// prog_stud_count: 0, +// prog_dus: { dus_name: "", dus_email: "", dus_address: "" }, +// prog_catolog: "", +// prog_website: "" +// }, +// prog_degs: [ +// { deg_type: "B.A.", deg_concs: [CONC_CPSC_BA_I] }, + +// ] +// } +// // { deg_type: "B.S.", deg_concs: [CONC_CPSC_BS_I] } + +// // const PROG_ECON: Program = { +// // prog_data: { +// // prog_name: "Economics", +// // prog_abbr: "ECON", +// // prog_stud_count: 0, +// // prog_dus: { dus_name: "", dus_email: "", dus_address: "" }, +// // prog_catolog: "", +// // prog_website: "" +// // }, +// // prog_degs: [ +// // { deg_type: "B.A.", deg_concs: [CONC_ECON_BA_I] } +// // ] +// // } + +// // const PROG_PLSC: Program = { +// // prog_data: { +// // prog_name: "Political Science", +// // prog_abbr: "PLSC", +// // prog_stud_count: 0, +// // prog_dus: { dus_name: "", dus_email: "", dus_address: "" }, +// // prog_catolog: "", +// // prog_website: "" +// // }, +// // prog_degs: [ +// // { deg_type: "B.A.", deg_concs: [CONC_PLSC_BA_STAN, CONC_PLSC_BA_INTE] } +// // ] +// // } + +// // const PROG_HIST: Program = { +// // prog_data: { +// // prog_name: "History", +// // prog_abbr: "HIST", +// // prog_stud_count: 0, +// // prog_dus: { dus_name: "", dus_email: "", dus_address: "" }, +// // prog_catolog: "", +// // prog_website: "" +// // }, +// // prog_degs: [ +// // { +// // deg_type: "B.A.", +// // deg_concs: [CONC_HIST_BA_GLOB, CONC_HIST_BA_SPEC] +// // } +// // ] +// // } + +export const PROG_DICT: ProgramDict = { + // "CPSC": PROG_CPSC, + // "ECON": PROG_ECON, + // "PLSC": PROG_PLSC, + // "HIST": PROG_HIST, +}; 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..e7b50f5 100644 --- a/frontend/src/types/type-program.ts +++ b/frontend/src/types/type-program.ts @@ -1,71 +1,50 @@ -import { StudentCourse } from "./type-user"; +import { Course, StudentCourse } from "./type-user"; -interface DUS { - name: string; - address: string; - email: string; +export interface Option { + option: Course | null; + satisfier: StudentCourse | null; + elective_range?: string; + flags?: string[]; + is_any_okay?: boolean; } -interface DegreeMetadataStats { - courses: number; - rating: number; - workload: number; - type: string; +export interface Subrequirement { + name: string; + description: string; + index: number; + courses_required_count: number; + options: Option[] } -export interface DegreeMetadata { +export interface Requirement { name: string; - abbr: string; - degreeType: string; - stats: DegreeMetadataStats; - students: number; - about: string; - dus: DUS; - catologLink: string; - wesbiteLink: string; -} - - - - - - - -// \BEGIN{MAJOR MAGIC} - -interface DegreeRequirementsSubsection { - name?: string; - description?: string; - flexible: boolean; - courses: StudentCourse[]; + description: string; + index: number; + courses_required_count: number; + subreqs_required_count: number; + checkbox?: boolean; + subrequirements: Subrequirement[]; } -interface DegreeRequirement { +export interface Concentration { name: string; - description?: string; - subsections: DegreeRequirementsSubsection[]; -} - -export interface DegreeConfiguration { - degreeRequirements: DegreeRequirement[]; + description: string; + requirements: Requirement[]; } export interface Degree { - metadata: DegreeMetadata; - configuration: DegreeConfiguration; + type: string; + concentrations: Concentration[]; } -// \END{MAJOR MAGIC} - - - - - - - -export interface StudentDegree { - status: string; // DA | ADD | PIN - programIndex: number; - degreeIndex: number; +export interface Program { + name: string; + abbreviation: string; + student_count: number; + catolog_link: string; + website_link: string; + degrees: Degree[]; } + +export type ProgramDict = Record; diff --git a/frontend/src/types/type-user.ts b/frontend/src/types/type-user.ts index 1fd1884..ff158a8 100644 --- a/frontend/src/types/type-user.ts +++ b/frontend/src/types/type-user.ts @@ -1,35 +1,74 @@ -import { DegreeConfiguration, StudentDegree } from "./type-program"; +// import { DegreeConcentration } from "./type-program"; export interface Course { - codes: string[]; // ["FREN 403", "HUMS 409"] - title: string; // "Proust Interpretations: Reading Remembrance of Things Past" - credit: number; // 1 - dist: string[]; // ["Hu"] - seasons: string[]; // ["Spring"] + id: string; + codes: string[]; + title: string; + description: string; + requirements: string; + professors: string[]; + distributions: string[]; + flags: string[]; + credits: number; + term: number; + is_colsem: boolean; + is_fysem: boolean; + is_sysem: boolean; } 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 MajorsIndex { + prog: string; // e.g. "CPSC" + deg: number; + conc: number; +} +export interface StudentConc { + name: string; + status: number; + // concentration: DegreeConcentration; + concentration_majors_index: MajorsIndex; + selected_subreqs: Record; +} + export interface FYP { - languageRequirement: string; - studentSemesters: StudentSemester[] - degreeConfigurations: DegreeConfiguration[][]; - degreeDeclarations: StudentDegree[]; + decl_list: StudentConc[]; + studentCourses: StudentCourse[]; + languagePlacement: string; + studentTermArrangement: StudentTermArrangement; } export interface User { name: string; netID: string; - onboard: boolean; FYP: FYP; } + +export interface MajorsIndex { + prog: string; + deg: number; + conc: number; +} diff --git a/frontend/src/app/courses/semester/course/CourseBox.module.css b/frontend/src/utils/course-display/CourseDisplay.module.css similarity index 98% rename from frontend/src/app/courses/semester/course/CourseBox.module.css rename to frontend/src/utils/course-display/CourseDisplay.module.css index 739f3ee..c44cf5f 100644 --- a/frontend/src/app/courses/semester/course/CourseBox.module.css +++ b/frontend/src/utils/course-display/CourseDisplay.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/utils/course-display/CourseDisplay.tsx b/frontend/src/utils/course-display/CourseDisplay.tsx new file mode 100644 index 0000000..b09f91d --- /dev/null +++ b/frontend/src/utils/course-display/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/src/utils/preprocessing/Fill.ts b/frontend/src/utils/preprocessing/Fill.ts new file mode 100644 index 0000000..dd52d91 --- /dev/null +++ b/frontend/src/utils/preprocessing/Fill.ts @@ -0,0 +1,243 @@ + +import { StudentCourse } from "@/types/type-user"; +import { + DegreeConcentration, + ProgramDict, + ConcentrationRequirement +} from "@/types/type-program"; + +/** + * Main function - updates entire `progDict` by filling student courses in each concentration. + */ +export function fill( + studentCourses: StudentCourse[], + progDict: ProgramDict, + setProgDict: Function +): void { + return; + + + // Create a true deep copy to avoid mutations to the original + const updatedProgDict: ProgramDict = JSON.parse(JSON.stringify(progDict)); + + // Process each program + Object.keys(updatedProgDict).forEach(progKey => { + const program = updatedProgDict[progKey]; + + program.prog_degs = program.prog_degs.map(deg => { + deg.deg_concs = deg.deg_concs.map(conc => { + return processConcentration(conc, studentCourses); + }); + return deg; + }); + }); + + // Update state with the completely new object + setProgDict(updatedProgDict); +} + +/** + * Process a single concentration by handling all its requirements + */ +function processConcentration( + concentration: DegreeConcentration, + studentCourses: StudentCourse[] +): DegreeConcentration { + const usedCourses = new Set(); + const requiredCourses = new Set(); // Track courses explicitly required by `o` + + // Clone to avoid mutations + const processedConc = JSON.parse(JSON.stringify(concentration)) as DegreeConcentration; + + // Split requirements by type + const checkboxReqs = processedConc.conc_reqs.filter(req => req.checkbox); + const nonCheckboxReqs = processedConc.conc_reqs.filter(req => !req.checkbox); + + // Process non-checkbox requirements in three passes + + // ** Pass 1: Direct matches for non-checkbox `o` (Claim required courses) ** + const updatedNonCheckboxReqs = nonCheckboxReqs.map(req => { + return processDirectMatches(req, studentCourses, usedCourses, requiredCourses); + }); + + // ** Pass 2: Non-Flex elective ranges for non-checkbox (Only assign courses NOT in requiredCourses) ** + const updatedNonCheckboxReqs2 = updatedNonCheckboxReqs.map(req => { + return processElectiveRanges(req, studentCourses, usedCourses, requiredCourses, false); + }); + + // ** Pass 3: Flex elective ranges for non-checkbox (Only assign courses NOT in requiredCourses) ** + const updatedNonCheckboxReqs3 = updatedNonCheckboxReqs2.map(req => { + return processElectiveRanges(req, studentCourses, usedCourses, requiredCourses, true); + }); + + // Process checkbox requirements + const updatedCheckboxReqs = checkboxReqs.map(req => { + return processCheckboxReq(req, studentCourses, usedCourses); + }); + + // Combine the results + processedConc.conc_reqs = [...updatedNonCheckboxReqs3, ...updatedCheckboxReqs]; + + return processedConc; +} + +/** + * **Pass 1: Fill `s` where `o` is non-null (Claim required courses first).** + */ +function processDirectMatches( + req: ConcentrationRequirement, + studentCourses: StudentCourse[], + usedCourses: Set, + requiredCourses: Set +): ConcentrationRequirement { + return { + ...req, + subreqs_list: req.subreqs_list.map(subreq => ({ + ...subreq, + subreq_options: subreq.subreq_options.map(option => { + if (!option.o || option.s) return option; // Skip null `o` or already filled `s` + + const courseCode = option.o.codes[0]; + const matchingStudentCourse = studentCourses.find(sc => + sc.course.codes.includes(courseCode) && !usedCourses.has(courseCode) + ); + + // ✅ Ensure required courses are claimed FIRST before electives + if (matchingStudentCourse) { + usedCourses.add(courseCode); // Track as used + requiredCourses.add(courseCode); // Mark as REQUIRED + return { ...option, s: matchingStudentCourse }; + } + + return option; + }), + })), + }; +} + +/** + * **Pass 2 & 3: Fill elective ranges (Non-Flex first, then Flex).** + * - **Ensures required courses (`requiredCourses`) are NOT used for electives.** + */ +function processElectiveRanges( + req: ConcentrationRequirement, + studentCourses: StudentCourse[], + usedCourses: Set, + requiredCourses: Set, // 🔥 Courses reserved by direct matches + flex: boolean +): ConcentrationRequirement { + return { + ...req, + subreqs_list: req.subreqs_list.map(subreq => { + if (subreq.subreq_flex !== flex) return subreq; // Skip subreqs that don't match the flex condition + + return { + ...subreq, + subreq_options: subreq.subreq_options.map(option => { + if (option.o !== null || !option.n?.e || option.s) return option; // Skip non-null `o` or already filled `s` + + const { dept, min, max } = option.n.e; + + // ✅ Filter student courses: + // - Ensure the course is not in `usedCourses` + // - Ensure the course is not already **reserved by a required subreq** + const availableCourses = studentCourses.filter(sc => + !usedCourses.has(sc.course.codes[0]) && + !requiredCourses.has(sc.course.codes[0]) && // 🔥 PROTECT REQUIRED COURSES + sc.course.codes.some(code => { + if (!code.startsWith(dept)) return false; + const courseNum = parseInt(code.replace(dept, ""), 10); + return courseNum >= min && courseNum <= max; + }) + ); + + const matchingStudentCourse = availableCourses.find(sc => true); // ✅ Find first valid course + + if (matchingStudentCourse) { + usedCourses.add(matchingStudentCourse.course.codes[0]); // ✅ Mark as used + return { ...option, s: matchingStudentCourse }; + } + + return option; + }), + }; + }), + }; +} + +/** + * Process a checkbox requirement - allowing reuse within global pool + * but preventing duplicates within the checkbox requirement + */ +function processCheckboxReq( + req: ConcentrationRequirement, + studentCourses: StudentCourse[], + globalUsedCourses: Set +): ConcentrationRequirement { + const usedWithinCheckboxReq = new Set(); // Track courses used within this checkbox req + + // ** Phase 1: Direct matches for checkbox requirements ** + let updatedReq = { + ...req, + subreqs_list: req.subreqs_list.map(subreq => ({ + ...subreq, + subreq_options: subreq.subreq_options.map(option => { + if (!option.o || option.s) return option; // Skip null `o` or already filled `s` + + const courseCode = option.o.codes[0]; + // For checkbox reqs, we only check if it's used within this same checkbox req + const matchingStudentCourse = studentCourses.find(sc => + sc.course.codes.includes(courseCode) && !usedWithinCheckboxReq.has(courseCode) + ); + + if (matchingStudentCourse) { + usedWithinCheckboxReq.add(courseCode); // Track within checkbox req + // Don't add to globalUsedCourses - intentionally allow reuse outside this req + return { ...option, s: matchingStudentCourse }; + } + + return option; + }), + })), + }; + + // ** Phase 2: Elective ranges and null options for checkbox requirements ** + updatedReq = { + ...updatedReq, + subreqs_list: updatedReq.subreqs_list.map(subreq => ({ + ...subreq, + subreq_options: subreq.subreq_options.map(option => { + if (option.s) return option; // Skip already filled slots + + let matchingStudentCourse: StudentCourse | undefined; + + if (option.n?.e) { + // Handle elective range + const { dept, min, max } = option.n.e; + matchingStudentCourse = studentCourses.find(sc => + !usedWithinCheckboxReq.has(sc.course.codes[0]) && // Only check within this checkbox + sc.course.codes.some(code => { + if (!code.startsWith(dept)) return false; + const courseNum = parseInt(code.replace(dept, ""), 10); + return courseNum >= min && courseNum <= max; + }) + ); + } else if (!option.o) { + // For null option.o, find any course not used within this checkbox req + matchingStudentCourse = studentCourses.find(sc => + !usedWithinCheckboxReq.has(sc.course.codes[0]) // Only check within this checkbox + ); + } + + if (matchingStudentCourse) { + usedWithinCheckboxReq.add(matchingStudentCourse.course.codes[0]); // Track within checkbox + return { ...option, s: matchingStudentCourse }; + } + + return option; + }), + })), + }; + + return updatedReq; +} diff --git a/frontend/src/utils/supabase.ts b/frontend/src/utils/supabase.ts new file mode 100644 index 0000000..87d5f28 --- /dev/null +++ b/frontend/src/utils/supabase.ts @@ -0,0 +1,24 @@ + +import { createClient } from '@supabase/supabase-js'; + +const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!; +const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!; +const supabaseServiceKey = process.env.SUPABASE_SERVICE_ROLE_KEY!; + +export const supabase = createClient(supabaseUrl, supabaseAnonKey); + +export const supabaseAdmin = createClient(supabaseUrl, supabaseServiceKey, { + auth: { + autoRefreshToken: false, + persistSession: false + } +}); + +export async function getCourses() { + const { data, error } = await supabase + .from('courses') + .select('*'); + + if (error) throw error; + return data; +} 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 index 5d0f8a1..0f1c751 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,31 +5,140 @@ "packages": { "": { "dependencies": { - "jquery": "^3.7.1" + "@supabase/supabase-js": "^2.49.1" + } + }, + "node_modules/@supabase/auth-js": { + "version": "2.68.0", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.68.0.tgz", + "integrity": "sha512-odG7nb7aOmZPUXk6SwL2JchSsn36Ppx11i2yWMIc/meUO2B2HK9YwZHPK06utD9Ql9ke7JKDbwGin/8prHKxxQ==", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/functions-js": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.4.4.tgz", + "integrity": "sha512-WL2p6r4AXNGwop7iwvul2BvOtuJ1YQy8EbOd0dhG1oN1q8el/BIRSFCFnWAMM/vJJlHWLi4ad22sKbKr9mvjoA==", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/node-fetch": { + "version": "2.6.15", + "resolved": "https://registry.npmjs.org/@supabase/node-fetch/-/node-fetch-2.6.15.tgz", + "integrity": "sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==", + "dependencies": { + "whatwg-url": "^5.0.0" }, - "devDependencies": { - "@types/jquery": "^3.5.29" + "engines": { + "node": "4.x || >=6.0.0" } }, - "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, + "node_modules/@supabase/postgrest-js": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-1.19.2.tgz", + "integrity": "sha512-MXRbk4wpwhWl9IN6rIY1mR8uZCCG4MZAEji942ve6nMwIqnBgBnZhZlON6zTTs6fgveMnoCILpZv1+K91jN+ow==", "dependencies": { - "@types/sizzle": "*" + "@supabase/node-fetch": "^2.6.14" } }, - "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/@supabase/realtime-js": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.11.2.tgz", + "integrity": "sha512-u/XeuL2Y0QEhXSoIPZZwR6wMXgB+RQbJzG9VErA3VghVt7uRfSVsjeqd7m5GhX3JR6dM/WRmLbVR8URpDWG4+w==", + "dependencies": { + "@supabase/node-fetch": "^2.6.14", + "@types/phoenix": "^1.5.4", + "@types/ws": "^8.5.10", + "ws": "^8.18.0" + } + }, + "node_modules/@supabase/storage-js": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.7.1.tgz", + "integrity": "sha512-asYHcyDR1fKqrMpytAS1zjyEfvxuOIp1CIXX7ji4lHHcJKqyk+sLl/Vxgm4sN6u8zvuUtae9e4kDxQP2qrwWBA==", + "dependencies": { + "@supabase/node-fetch": "^2.6.14" + } + }, + "node_modules/@supabase/supabase-js": { + "version": "2.49.1", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.49.1.tgz", + "integrity": "sha512-lKaptKQB5/juEF5+jzmBeZlz69MdHZuxf+0f50NwhL+IE//m4ZnOeWlsKRjjsM0fVayZiQKqLvYdBn0RLkhGiQ==", + "dependencies": { + "@supabase/auth-js": "2.68.0", + "@supabase/functions-js": "2.4.4", + "@supabase/node-fetch": "2.6.15", + "@supabase/postgrest-js": "1.19.2", + "@supabase/realtime-js": "2.11.2", + "@supabase/storage-js": "2.7.1" + } }, - "node_modules/jquery": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", - "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==" + "node_modules/@types/node": { + "version": "22.13.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.10.tgz", + "integrity": "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/@types/phoenix": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.6.tgz", + "integrity": "sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==" + }, + "node_modules/@types/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8svvI3hMyvN0kKCJMvTJP/x6Y/EoQbepff882wL+Sn5QsXb3etnamgrJq4isrBxSJj5L2AuXcI0+bgkoAXGUJw==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==" + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } } } } diff --git a/package.json b/package.json index 85d3341..9b6d9a4 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,5 @@ - { "dependencies": { - "jquery": "^3.7.1" - }, - "devDependencies": { - "@types/jquery": "^3.5.29" + "@supabase/supabase-js": "^2.49.1" } } diff --git a/scrapers/coursetableScraper/coursetable.py b/scrapers/coursetableScraper/coursetable.py new file mode 100644 index 0000000..1bde0dd --- /dev/null +++ b/scrapers/coursetableScraper/coursetable.py @@ -0,0 +1,117 @@ + +import requests +import json +import os +from supabase import create_client + + +def fetch_process_and_upload(terms: list[int]): + + supabase_url = "https://cqonuujfvpucligwwgtq.supabase.co" + supabase_key = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImNxb251dWpmdnB1Y2xpZ3d3Z3RxIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTczODUyMjg1MywiZXhwIjoyMDU0MDk4ODUzfQ.OtS4JpoFfW-T4YjksMW7SOeBZ1zSaf2EIBbevd09oaI" + + supabase = create_client(supabase_url, supabase_key) + + for term in terms: + term_str = str(term) + url = f"https://api.coursetable.com/api/catalog/public/{term_str}" + print(f"Fetching course data for term {term_str} from {url}...") + + try: + response = requests.get(url) + response.raise_for_status() + courses_data = response.json() + except requests.exceptions.RequestException as e: + print(f"✗ Failed to fetch data for term {term_str}: {e}") + continue + + print(f"Processing course data for term {term_str}...") + processed_courses = [transform_course(course) for course in courses_data[:25]] + + # Save locally (backup) + file_name = f"results_{term_str}.json" + with open(file_name, "w", encoding="utf-8") as file: + json.dump(processed_courses, file, indent=2) + print(f"✓ Saved {len(processed_courses)} courses to {file_name}") + + # Upload to Supabase + batch_size = 50 + total_batches = (len(processed_courses) + batch_size - 1) // batch_size + + print(f"Uploading {len(processed_courses)} courses for term {term_str} in {total_batches} batches...") + + for i in range(0, len(processed_courses), batch_size): + batch = processed_courses[i:i+batch_size] + batch_num = i // batch_size + 1 + + print(f"Uploading batch {batch_num}/{total_batches} for term {term_str}...") + + try: + result = supabase.table("courses").upsert(batch).execute() + print(f"✓ Batch {batch_num} for term {term_str} uploaded successfully") + except Exception as e: + print(f"✗ Error with batch {batch_num} for term {term_str}: {e}") + + print("✅ All terms processed successfully!") + + +def transform_course(course): + # Extract course flags + course_flags = [] + if "course_flags" in course and course["course_flags"]: + course_flags = [flag["flag"]["flag_text"] for flag in course["course_flags"]] + + # Extract professors + professors = [] + if "course_professors" in course and course["course_professors"]: + professors = [prof["professor"]["name"] for prof in course["course_professors"]] + + # Extract course codes + course_codes = [] + if "listings" in course and course["listings"]: + course_codes = [listing["course_code"] for listing in course["listings"]] + + # Combine areas and skills into distributions + distributions = [] + if "areas" in course and course["areas"]: + distributions.extend(course["areas"]) + if "skills" in course and course["skills"]: + distributions.extend(course["skills"]) + + # Handle credits - ensure we're preserving the decimal value if present + credits = course.get("credits") + # If credits is a string, convert it to a float (preserving decimal places) + if isinstance(credits, str): + try: + credits = float(credits) + except (ValueError, TypeError): + credits = None + + # Create transformed course object + return { + "course_id": course.get("course_id"), + "title": course.get("title"), + "description": course.get("description"), + "professors": professors, + "codes": course_codes, + "flags": course_flags, + "distributions": distributions, + "credits": credits, # This will now preserve decimal values + "requirements": course.get("requirements"), + "term": course.get("season_code"), + "colsem": course.get("colsem", False), + "fysem": course.get("fysem", False), + "sysem": course.get("sysem", False) + } + +if __name__ == "__main__": + try: + print("Course Data to Supabase Uploader") + print("--------------------------------") + terms = [202403, 202501] + fetch_process_and_upload(terms) + except KeyboardInterrupt: + print("\nProcess cancelled by user") + except Exception as e: + print(f"An error occurred: {e}") + \ No newline at end of file diff --git a/scrapers/coursetableScraper/coursetable_scraper.py b/scrapers/coursetableScraper/coursetable_scraper.py deleted file mode 100644 index 2e8c327..0000000 --- a/scrapers/coursetableScraper/coursetable_scraper.py +++ /dev/null @@ -1,38 +0,0 @@ -import requests -import json -import datetime -import time - -def scrape_courses(): - #TODO change these to env variables - cookies = { - 'session': 'enter session', - 'session.sig': 'etner session.sig', - } - - # response = requests.get('https://api.coursetable.com/api/static/catalogs/202301.json', cookies=cookies) - - course_dic = {} - for year in range(datetime.datetime.now().year-6, datetime.datetime.now().year + 6 + 1): - for season in range(1, 4): - if year not in course_dic: - course_dic[year]={} - - data_url = f'https://api.coursetable.com/api/static/catalogs/{year}0{season}.json' - response = requests.get(data_url, cookies=cookies) - - if response.status_code == 404: - print(f'unable to access {year} {season}') - continue - else: - print(f'scraping {year} {season}') - - course_dic[year][season] = json.loads(response.text) - time.sleep(1) - - with open('courses.json', 'w') as infile: - json.dump(course_dic, infile) - - -if __name__=='__main__': - scrape_courses() diff --git a/scrapers/coursetableScraper/display_courses.py b/scrapers/coursetableScraper/display_courses.py deleted file mode 100644 index ab6d8a3..0000000 --- a/scrapers/coursetableScraper/display_courses.py +++ /dev/null @@ -1,58 +0,0 @@ -import json -import argparse - - -def display_courses(): - with open('courses.json', 'r') as infile: - courses=json.load(infile) - new_courses={} - for year in courses: - if year not in new_courses: - new_courses[year] = {} - - for season in courses[year]: - if season not in new_courses[year]: - new_courses[year][season] = {} - - for course in courses[year][season]: - for code in course["all_course_codes"]: - dep=code.split(' ')[0] - num = str(code.split(' ')[1]) - if dep not in new_courses[year][season]: - new_courses[year][season][dep]={} - - new_courses[year][season][dep][num]=course - - courses=new_courses - - while True: - request=input('enter course:\n') - request=request.split(' ') - if len(request)==2: - year='2023' - season='3' - dep = str(request[0]).upper() - c_num = str(request[1]) - else: - year=str(request[0]) - season=str(request[1]) - dep=str(request[2]).upper() - c_num=str(request[3]) - - try: - course=courses[year][season][dep][c_num] - print(f'{course["title"]}:') - print(f'{course["description"]}\n') - print(f'rating: {course["average_rating"]}') - print(f'difficulty: {course["average_workload"]}\n') - - except: - print('invalid course') - - -if __name__=='__main__': - display_courses() - - - - diff --git a/scrapers/coursetableScraper/results.json b/scrapers/coursetableScraper/results.json new file mode 100644 index 0000000..96a277a --- /dev/null +++ b/scrapers/coursetableScraper/results.json @@ -0,0 +1,535 @@ +[ + { + "course_id": 250121861, + "title": "Social and Cultural Factors in Mental Health and Illness", + "description": "This course provides an introduction to mental health and illness with a focus on the complex interplay between risk and protective factors and social and cultural influences on mental health status. We examine the role of social and cultural factors in the etiology, course, and treatment of substance misuse; depressive, anxiety, and psychotic disorders; and some of the severe behavioral disorders of childhood. The social consequences of mental illness such as stigma, isolation, and barriers to care are explored, and their impact on access to care and recovery considered. The effectiveness of the current system of services and the role of public health and public health professionals in mental health promotion are discussed.", + "professors": [], + "codes": [ + "PSYC 576" + ], + "flags": [], + "distributions": [], + "credits": 1, + "requirements": "", + "season_code": "202501", + "colsem": false, + "fysem": false, + "sysem": false + }, + { + "course_id": 250125120, + "title": "Research", + "description": "Individual research for Ph.D. degree candidates in the Department of Chemistry, under the direct supervision of one or more faculty members.", + "professors": [ + "Tianyu Zhu" + ], + "codes": [ + "CHEM 990" + ], + "flags": [], + "distributions": [], + "credits": 1, + "requirements": "", + "season_code": "202501", + "colsem": false, + "fysem": false, + "sysem": false + }, + { + "course_id": 250121135, + "title": "Junior Seminar", + "description": "Ongoing visual projects addressed in relation to historical and contemporary issues. Readings, slide presentations, critiques by School of Art faculty, and gallery and museum visits. Critiques address all four areas of study in the Art major.", + "professors": [ + "Elle Perez" + ], + "codes": [ + "ART 395" + ], + "flags": [], + "distributions": [ + "Hu" + ], + "credits": 1, + "requirements": "Prerequisite: at least four courses in Art.", + "season_code": "202501", + "colsem": false, + "fysem": false, + "sysem": false + }, + { + "course_id": 250121016, + "title": "Topics: Events, Distributivity, Durational Modifiers", + "description": "This course bridges introductory courses (LING 263, LING 264) and advanced seminars in semantics. It explores selected topics in some detail, allowing students to appreciate the nuances of semantic argumentation while at the same time emphasizing the foundational issues involved.\u00a0The goal of this course is to allow students, within a structured format, to become comfortable engaging with open-ended problems and to gain confidence in proposing original solutions to such problems.\u00a0Topics vary across semesters.", + "professors": [ + "Veneeta Dayal", + "Simon Charlow" + ], + "codes": [ + "LING 291", + "LING 691" + ], + "flags": [ + "YC LING Depth Semntcs/Pragmat", + "YC LING Elective", + "YC LING Intermediate Courses" + ], + "distributions": [ + "So" + ], + "credits": 1, + "requirements": "Prerequisite: LING 263 / LING 663 or permission of Instructor", + "season_code": "202501", + "colsem": false, + "fysem": false, + "sysem": false + }, + { + "course_id": 250123229, + "title": "Yale Concert Band", + "description": "The Yale Concert Band, a group of 45-60 wind, brass, and percussion players, embraces the aesthetics of the traditional wind band and the contemporary experimental ensemble. Our repertoire consists of a panoply of wind band classics; premieres by and commissions of Yale students, faculty and established world-class composers; and the newest wind band literature that incorporates electro-acoustic sounds, folk/rock/hip hop music, soloists, and theatrical trappings. The Yale Concert Band regularly presents concerts to benefit causes and organizations, ranging from benefit concerts to support the work of New Haven\u2019s IRIS (Integrated Refugee and Immigrant Services (2017, 2018, 2019); to provide aid to the relief efforts after Hurricane Katrina (2005), floods in Myanmar (2007), tornadoes in the American midwest (2007), the earthquake in Haiti (2010), the tsunami in Japan (2011), and West African Ebola recovery efforts (2016).\u00a0 In 1959, the Yale Concert Band became the first university band to produce an international concert tour, and, since then, has appeared in concerts in Japan, South Africa, Swaziland, Mexico, Brazil, Bermuda, Russia, Finland, the Czech Republic, Austria, Ireland, England, France, Italy, Denmark, Germany, Holland, Belgium, Lithuania, Latvia, Estonia, Ghana, Haiti, Greece, Australia, and Spain. This course cannot be applied toward the 36-course-credit requirement for the Yale bachelor's degree.", + "professors": [ + "Thomas Duffy" + ], + "codes": [ + "MUSI 190" + ], + "flags": [], + "distributions": [], + "credits": 0, + "requirements": "By audition at the beginning of the academic year or by permission of instructor.", + "season_code": "202501", + "colsem": false, + "fysem": false, + "sysem": false + }, + { + "course_id": 250124916, + "title": "Exploring and Understanding White Collar Crime", + "description": "This course examines the many aspects of white collar crime; perjury, obstruction of justice, corporate crimes, Ponzi Schemes, insider trading, money laundering bribery and political corruption. The course explores how white collar crime, once virtually ignored by law enforcement has become a major focus of federal and state investigative agencies with massive resources allocated toward combatting it. The seminar examines the root causes of white collar crime as well as its pervasiveness in every day life.\u00a0 Specific cases of white collar defendants, both individuals and corporations that have profoundly impacted business, law, science, healthcare and other disciplines are examined.", + "professors": [ + "Bradley Simon" + ], + "codes": [ + "CSMY 220" + ], + "flags": [ + "YC College Seminar" + ], + "distributions": [], + "credits": 1, + "requirements": "Students may enroll in no more than 1 RCS for credit in a given term.", + "season_code": "202501", + "colsem": true, + "fysem": false, + "sysem": false + }, + { + "course_id": 250126581, + "title": "EL IntMedHematology2WK", + "description": "", + "professors": [], + "codes": [ + "MD 3090" + ], + "flags": [], + "distributions": [], + "credits": 0, + "requirements": "", + "season_code": "202501", + "colsem": false, + "fysem": false, + "sysem": false + }, + { + "course_id": 250126701, + "title": "Global Health Elective Ghana", + "description": "", + "professors": [], + "codes": [ + "MD 301" + ], + "flags": [], + "distributions": [], + "credits": 0, + "requirements": "", + "season_code": "202501", + "colsem": false, + "fysem": false, + "sysem": false + }, + { + "course_id": 250120660, + "title": "Methods in Gender and Sexuality Studies", + "description": "This seminar explores\u00a0the dynamics of power and knowledge, the ethics of representation and accountability, and the nexus between disciplinarity and interdisciplinarity. It is designed for graduate students developing research projects that engage feminist, queer, postcolonial, and critical race methodologies, among others. The course adopts an epistemological approach that centers \"encounter\" across geopolitical scales and multiple disciplinary fronts in the humanities and social sciences. It posits that research methods, regardless of their origin, can adopt feminist, queer, decolonial/postcolonial, and critical race perspectives and potentially serve counter-disciplinary purposes. Although we cover a broad spectrum of methods\u2014ranging from ethnographic, historiographic/archival, and geographic, to literary, media, and textual analysis, cultural studies, and political theory\u2014our work does not unfold as a practicum. Instead of experimenting with a predefined \"toolkit,\" students critically engage book-length works that demonstrate counter-disciplinary methodologies, reflecting hermeneutically on how method and theory relate in these texts by drawing on Foucault's framework of \"the archaeology of knowledge.\"", + "professors": [ + "Eda Pepi" + ], + "codes": [ + "AMST 798", + "WGSS 800" + ], + "flags": [ + "YC Ethnography Methods" + ], + "distributions": [], + "credits": 1, + "requirements": "", + "season_code": "202501", + "colsem": false, + "fysem": false, + "sysem": false + }, + { + "course_id": 250121028, + "title": "Special Investigations", + "description": "Directed research by arrangement with individual faculty members and approved by the DGS. Students are expected to propose and complete a term-long research project. The culmination of the project is a presentation that fulfills the departmental requirement for the research qualifying event.", + "professors": [ + "Daisuke Nagai", + "Rona Ramos" + ], + "codes": [ + "PHYS 990" + ], + "flags": [], + "distributions": [], + "credits": 1, + "requirements": "", + "season_code": "202501", + "colsem": false, + "fysem": false, + "sysem": false + }, + { + "course_id": 250121423, + "title": "Topics in Biomedical Informatics and Data Science", + "description": "The course focuses on providing an introduction to common unifying themes that serve as the foundation for different areas of biomedical informatics, including clinical, neuro-, and genome informatics. The course is designed for students with basic computer experience and course work who plan to build databases and computational tools for use in biomedical research. Emphasis is on understanding basic principles underlying informatics approaches to interoperation among biomedical databases and software tools, standardized biomedical vocabularies and ontologies, biomedical natural language processing, modeling of biological systems, high-performance computation in biomedicine, and other related topics.", + "professors": [ + "Samah Jarad" + ], + "codes": [ + "BIS 550", + "CB&B 750", + "HSCI 5500" + ], + "flags": [], + "distributions": [], + "credits": 0, + "requirements": "", + "season_code": "202501", + "colsem": false, + "fysem": false, + "sysem": false + }, + { + "course_id": 250121441, + "title": "Topics in Biomedical Informatics and Data Science", + "description": "The course focuses on providing an introduction to common unifying themes that serve as the foundation for different areas of biomedical informatics, including clinical, neuro-, and genome informatics. The course is designed for students with significant computer experience and course work who plan to build databases and computational tools for use in biomedical research. Emphasis is on understanding basic principles underlying informatics approaches to interoperation among biomedical databases and software tools, standardized biomedical vocabularies and ontologies, biomedical natural language processing, modeling of biological systems, high-performance computation in biomedicine, and other related topics.", + "professors": [ + "Samah Jarad", + "Kei-Hoi Cheung" + ], + "codes": [ + "BIS 543E" + ], + "flags": [], + "distributions": [], + "credits": 0, + "requirements": "Open only to students enrolled in the Executive Online M.P.H. Program. Not open to auditors.", + "season_code": "202501", + "colsem": false, + "fysem": false, + "sysem": false + }, + { + "course_id": 250122238, + "title": "Corporate Sustainability: Strategy and Management", + "description": "This survey course focuses on the policy and business logic for making environmental issues and sustainability a core focus of corporate strategy and management. Students are asked to analyze when and how sustainability leadership can translate into competitive advantage by helping to cut costs, reduce risk, drive growth, and promote brand identity and intangible value. The course seeks to provide students with an introduction to the range of sustainability issues and challenges that companies face in today\u2019s fast-changing marketplace. It introduces key corporate sustainability terms, concepts, tools, strategies, and frameworks based on the overarching theory that the traditional profit-maximizing mission of business (often called shareholder primacy) is giving way to a new vision of stakeholder responsibility that still seeks to provide good returns to the enterprise\u2019s owners but also acknowledges obligations to employees, suppliers, customers, communities, and society more broadly. The course combines lectures, case studies, and class discussions on management theory and tools, the legal and regulatory frameworks that shape the business-environment interface, and the evolving role of business in society. It explores how to deal with a world of diverse stakeholders, increasing transparency, and rising expectations related to corporate environmental, social, and governance (ESG) performance. Self-scheduled examination.", + "professors": [ + "Daniel Esty" + ], + "codes": [ + "ENV 807", + "MGT 688" + ], + "flags": [ + "YC ENRG Energy & Environment", + "YSE MEM B&E Core", + "YSE MEM IEGC Add'l Electives", + "YSE MEM IEGC Primary Electives" + ], + "distributions": [], + "credits": 0, + "requirements": "", + "season_code": "202501", + "colsem": false, + "fysem": false, + "sysem": false + }, + { + "course_id": 250122406, + "title": "What was Latinx Literature", + "description": "With the arrival of \"Latinx,\" the last decade was defined as a moment of rupture and break with traditional notions of latinidad. Artists and activists asserted refusal and historical reckoning as the mode of doing politics and aesthetics. Now, pessimistic about Latinx as a signifier of a unified political project, the generational tides have shifted to \"Latine.\" This seminar asks what is \"Latinx literature\" and why are the methods of \"Latinx studies\" considered revolutionary or disruptive? What ideas were rooted in prior generations of feminist and queer collectives that sustained life when the arrival of a decolonial future seemed forever deferred and withheld from reach? We examine contemporary artists alongside historical antecedents to reevaluate what literary and social forms can help us challenge a racialized, heteronormative conception of citizenship. One possibility is that Gloria Anzald\u00faa\u2014rightly critiqued for her relation to mestizaje \u2014might be helpful in this moment of growing nationalism and hostility towards migrants to think about other ways of organizing life aside borders and the nation. We read across a long and varied arc of creative expression to consider forms that endure amidst colonial duress. For example: the serial, montage, anthology, performance collective, and inter-linked storytelling. Artists up for discussion may include Natalie Diaz, John Rechy, and Jes\u00fas Col\u00f3n. Students will engage these works alongside theorists like Jos\u00e9 Esteban Mu\u00f1oz and Juana Mar\u00eda Rodr\u00edguez. Previously ENGL 331.", + "professors": [ + "Joseph Miranda" + ], + "codes": [ + "ENGL 4831", + "ER&M 268" + ], + "flags": [ + "YC ENGL 20th/21st Century", + "YC ENGL Senior Seminar" + ], + "distributions": [ + "Hu", + "WR" + ], + "credits": 1, + "requirements": "", + "season_code": "202501", + "colsem": false, + "fysem": false, + "sysem": false + }, + { + "course_id": 250122770, + "title": "Machine Learning for Economic Analysis", + "description": "Machine learning algorithms and their applications to economic analysis, specifically causal inference, learning, and game theory. Curse of dimensionality, model selection, and choice of tuning parameters from a computational and econometric perspective.", + "professors": [ + "Max Cytrynbaum" + ], + "codes": [ + "ECON 566" + ], + "flags": [], + "distributions": [], + "credits": 1, + "requirements": "", + "season_code": "202501", + "colsem": false, + "fysem": false, + "sysem": false + }, + { + "course_id": 250122954, + "title": "Machine Learning for Economic Analysis", + "description": "Machine learning algorithms and their applications to economic analysis, specifically causal inference, learning, and game theory. Curse of dimensionality, model selection, and choice of tuning parameters from a computational and econometric perspective.", + "professors": [ + "Max Cytrynbaum" + ], + "codes": [ + "ECON 428" + ], + "flags": [], + "distributions": [ + "So" + ], + "credits": 1, + "requirements": "Prerequisites: CPSC 100 or CPSC 112; and ECON 117 or ECON 136.", + "season_code": "202501", + "colsem": false, + "fysem": false, + "sysem": false + }, + { + "course_id": 250123384, + "title": "Secndry Instrmnt--: Secndry Instrmnt--PIANO", + "description": "2 credits per term. P/F. All students enrolled in secondary lessons can receive instruction in either voice or piano. In addition, YSM keyboard majors may take secondary organ or harpsichord, and YSM violinists may take secondary viola. Any other students who wish to take secondary lessons in any other instruments must petition the director of secondary lessons, Kyung Yu, by email (kyung.yu@yale.edu) no later than Aug. 30, 2021, for the fall term and Jan. 14, 2022, for the spring term. Students who are not conducting majors may take only one secondary instrument per term. YSM students who wish to take secondary lessons must register for the course and request a teacher using the online form for graduate students found at http://music.yale.edu/study/music-lessons; the availability of a secondary lessons teacher is not guaranteed until the form is received and a teacher assigned by the director of lessons. Secondary instruction in choral conducting and orchestral conducting is only available with permission of the instructor and requires as prerequisites MUS 565 for secondary instruction in choral conducting, and both MUS 529 and MUS 530 for secondary instruction in orchestral conducting. Students of the Yale Divinity School, School of Drama, and School of Art may also register as above for secondary lessons and will be charged $200 per term for these lessons. Questions may be emailed to the director, Kyung Yu (kyung.yu@yale.edu).", + "professors": [ + "Kyung Yu" + ], + "codes": [ + "MUS 541" + ], + "flags": [], + "distributions": [], + "credits": 0, + "requirements": "", + "season_code": "202501", + "colsem": false, + "fysem": false, + "sysem": false + }, + { + "course_id": 250123877, + "title": "The Senior Essay I", + "description": "Students wishing to undertake an independent senior essay in English must submit a proposal to the DUS in the previous term; deadlines and instructions are posted at https://english.yale.edu/undergraduate/courses/independent-study-courses. For one-term senior essays, the essay itself is due in the office of the director of undergraduate studies according to the following schedule: (1) end of the fourth week of classes: five to ten pages of writing and/or an annotated bibliography; (2) end of the ninth week of classes: a rough draft of the complete essay; (3) end of the last week of classes (fall term) or end of the next-to-last week of classes (spring term): the completed essay. Consult the director of undergraduate studies regarding the schedule for submission of the yearlong senior essay.", + "professors": [ + "Marcel Elias", + "Stefanie Markovits" + ], + "codes": [ + "ENGL 4100" + ], + "flags": [], + "distributions": [], + "credits": 1, + "requirements": "", + "season_code": "202501", + "colsem": false, + "fysem": false, + "sysem": false + }, + { + "course_id": 250124362, + "title": "Styles of Professional Prose: Writing about Legal Affairs", + "description": "A seminar and workshop in the conventions of good writing in a specific field. Each section focuses on one professional kind of writing and explores its distinctive features through a variety of written and oral assignments, in which students both analyze and practice writing in the field. Section topics, which change yearly, are listed at the beginning of each term on the English department website. This course may be repeated for credit in a section that treats a different genre or style of writing; may not be repeated for credit toward the major. ENGL 121 and ENGL 421 may not be taken for credit on the same topic.", + "professors": [ + "Lincoln Caplan" + ], + "codes": [ + "ENGL 1021" + ], + "flags": [], + "distributions": [ + "WR" + ], + "credits": 1, + "requirements": "Prerequisite: ENGL 114, 115, 120, or another writing-intensive course at Yale.", + "season_code": "202501", + "colsem": false, + "fysem": false, + "sysem": false + }, + { + "course_id": 250124828, + "title": "Abolition in the Americas", + "description": "This seminar examines histories of slavery's abolition in the Americas. It situates the end of slavery in the United States within hemispheric and transatlantic contexts, touching upon processes of abolition\u00a0in Antigua, Brazil, Colombia, Cuba, the Dominican Republic, Haiti, Jamaica, and more. The course approaches abolition as a historiographical problem, considering debates about its definition, its causes and consequences, its primary agents, its periodization, and its relation to other historical processes. Questions include: How have historians defined abolition? How have they periodized it? How have scholars variously characterized the forms it took and who was responsible for it? How have they differently understood the social, cultural, economic, legal, and political conditions that gave rise to abolition? How have historians agreed and disagreed upon its effects and its aftermath? How have they framed the relation between freedom and formal emancipation? How have the communicated the stakes of their accounts of abolition? The organization of the course is topical and loosely chronological. Readings address the origins of abolition in the Atlantic world, the Haitian Revolution, processes of gradual emancipation, the historical significance of Black abolitionists, the activism of women and children, the formation and contributions of antislavery movements, the practice of moral suasion, the question of violence and antislavery militantism, antislavery discourses of rights and sexual morals, the circulation of racial and climate science in abolitionist circles, enslaved people\u2019s practices of fugitivity, self-purchase, and revolt, the relation between capitalism and abolitionism, the Civil War, and the so-called \"last abolition\" of slavery in Brazil.", + "professors": [ + "Caleb Knapp" + ], + "codes": [ + "HIST 124J" + ], + "flags": [ + "YC HIST Cultural History", + "YC HIST Departmental Seminars", + "YC HIST Empires & Colonialism", + "YC HIST Ideas & Intellectuals", + "YC HIST Pltcs, Law & Govt", + "YC HIST Race Gender&Sexuality", + "YC HIST Soc Chng&Social Mvmnt", + "YC HIST United States", + "YC HIST War & Society", + "YC HSHM Colonial Know & Power", + "YC HSHM Gender, Reprod & Body" + ], + "distributions": [ + "Hu", + "WR" + ], + "credits": 1, + "requirements": "", + "season_code": "202501", + "colsem": false, + "fysem": false, + "sysem": false + }, + { + "course_id": 250125902, + "title": "Clinical Practice II for Global Health Track", + "description": "This clinical application course for students in the global health track provides opportunities to develop advanced nursing skills with a range of global populations within the students\u2019 areas of specialization. While in clinical settings, students develop skills in assessment and management of acute and chronic conditions using evidence-based patient management strategies in accordance with the cultural beliefs and practices of populations of immigrants, refugees, American Indians, and Alaskan native and rural residents. These experiences may take place in YSN-approved U.S. or international settings. Additional experiences with local resettlement organizations such as Integrated Refugee and Immigrant Services (IRIS) and Connecticut Institute for Refugees and Immigrants (CIRI) are also available. These experiences may include developing and presenting education programs to groups of refugees, immigrants, or asylum seekers; creating training materials for the resettlement agencies; or serving as a cultural companion or health navigator for newly arrived families. Required of all students pursuing the global health track during the fall term of their second specialty year. Thirty hours of face-to-face interactions either in a health care setting or in an alternative setting, and one hour per week of clinical conference. Taken after NURS 6230.", + "professors": [ + "Sandy Cayo" + ], + "codes": [ + "NURS 6240" + ], + "flags": [], + "distributions": [], + "credits": 0, + "requirements": "", + "season_code": "202501", + "colsem": false, + "fysem": false, + "sysem": false + }, + { + "course_id": 250126137, + "title": "Independent Course Work", + "description": "Program to be determined with a faculty adviser of the student\u2019s choice and submitted, with the endorsement of the study area coordinators, to the Rules Committee for confirmation of the student\u2019s eligibility under the rules. (See the School\u2019s Academic Rules and Regulations.)", + "professors": [ + "Brennan Buck" + ], + "codes": [ + "ARCH 2299" + ], + "flags": [], + "distributions": [], + "credits": 0, + "requirements": "", + "season_code": "202501", + "colsem": false, + "fysem": false, + "sysem": false + }, + { + "course_id": 250120297, + "title": "American Political Institutions", + "description": "The origins and development of American political institutions, especially in relation to constitutional choice and the agency of persons seeking freedom, equality, and self-governing capabilities as a driver of constitutional change. Key concepts include: American federalism, compound republic, citizenship, social movements, racial justice, and nonviolence.", + "professors": [ + "Michael Fotos" + ], + "codes": [ + "PLSC 256", + "AFAM 177", + "EP&E 248" + ], + "flags": [ + "YC EP&E Politics Core" + ], + "distributions": [ + "So", + "WR" + ], + "credits": 1, + "requirements": "", + "season_code": "202501", + "colsem": false, + "fysem": false, + "sysem": false + }, + { + "course_id": 250120530, + "title": "Elementary Modern Standard Arabic II", + "description": "Continuation of ARBC 110.", + "professors": [ + "Muhammad Aziz" + ], + "codes": [ + "ARBC 120", + "ARBC 501" + ], + "flags": [], + "distributions": [ + "L2" + ], + "credits": 1.5, + "requirements": "Prerequisite: ARBC 110 or\u00a0requisite score on a\u00a0placement test.", + "season_code": "202501", + "colsem": false, + "fysem": false, + "sysem": false + }, + { + "course_id": 250121598, + "title": "Chinese for Current Affairs", + "description": "Advanced language course with a focus on speaking and writing in formal styles. Current affairs are used as a vehicle to help students learn advanced vocabulary, idiomatic expressions, complex sentence structures, news writing styles and formal stylistic register. Materials include texts and videos selected from news media worldwide to improve students\u2019 language proficiency for sophisticated communications on a wide range of topics.", + "professors": [ + "Jingjing Ao" + ], + "codes": [ + "CHNS 167" + ], + "flags": [], + "distributions": [ + "L5" + ], + "credits": 1, + "requirements": "After CHNS 153, or 157, or 159,\u00a0 or equivalent.", + "season_code": "202501", + "colsem": false, + "fysem": false, + "sysem": false + } +] \ No newline at end of file diff --git a/scrapers/coursetableScraper/results_202403.json b/scrapers/coursetableScraper/results_202403.json new file mode 100644 index 0000000..d6c3751 --- /dev/null +++ b/scrapers/coursetableScraper/results_202403.json @@ -0,0 +1,544 @@ +[ + { + "course_id": 240316045, + "title": "Project Course", + "description": "Project Course", + "professors": [ + "Daniel Esty" + ], + "codes": [ + "ENV 1180" + ], + "flags": [], + "distributions": [], + "credits": 0, + "requirements": "", + "term": "202403", + "colsem": false, + "fysem": false, + "sysem": false + }, + { + "course_id": 240311041, + "title": "The Archaeology of Trade and Exchange", + "description": "This seminar will focus on archaeological approaches to exchange and trade. As background, we will review some of the principal theories of exchange from anthropology and sociology, such as those of Mauss, Malinowski and Polanyi. The role of trade and exchange in different kinds of societies will examined by contextualizing these transactions within specific cultural configurations and considering the nature of production and consumption as they relate to movement of these goods. We will consider methods and models that have been used to analyze regions of interaction at different spatial scales and the theoretical arguments about the social impact of inter-regional and intra-regional interactions involving the transfer of goods, including approaches such as world systems, unequal development and globalization. In addition, we will examine the ways that have been utilized in archaeology to identify different kinds of exchange systems, often through analogies to well documented ethnographic and historic cases. Finally, we will consider the range of techniques that have been employed in order to track the movement of goods across space. These sourcing techniques will be evaluated in terms of their advantages and disadvantages from an archaeological perspective, and how the best technical analyses may vary according to the nature of natural or cultural materials under consideration (ceramics, volcanic stone, metals, etc.). The theme for this year\u2019s seminar is obsidian so students should select some aspect of obsidian research for their final paper and presentation.", + "professors": [ + "Richard Burger" + ], + "codes": [ + "ARCG 353", + "ANTH 353", + "ANTH 756", + "ARCG 756" + ], + "flags": [], + "distributions": [], + "credits": 1, + "requirements": "", + "term": "202403", + "colsem": false, + "fysem": false, + "sysem": false + }, + { + "course_id": 240311722, + "title": "Principles of Chemical Engineering and Process\u00a0Modeling", + "description": "Analysis of the transport and reactions of chemical species as applied to problems in chemical, biochemical, and environmental systems. Emphasis on the interpretation of laboratory experiments, mathematical modeling, and dimensional analysis. Lectures include classroom demonstrations.", + "professors": [ + "Peijun Guo" + ], + "codes": [ + "CENG 210", + "ENVE 210" + ], + "flags": [], + "distributions": [ + "Sc", + "QR" + ], + "credits": 1, + "requirements": "Prerequisite: MATH 115 or permission of instructor.", + "term": "202403", + "colsem": false, + "fysem": false, + "sysem": false + }, + { + "course_id": 240315412, + "title": "Global Korean Cinema", + "description": "In recent times, world cinema has witnessed the rise of South Korean cinema as an alternative to Hollywood and includes many distinguished directors such as Park Chan-wook, Lee Chang-dong, Kim Ki-duk, and Bong Joon-ho. This course explores the Korean film history and aesthetics from its colonial days (1910-1945) to the hallyu era (2001-present), and also analyzes several key texts that are critical for understanding this field of study. How is Korean cinema shaped by (re)interpretations of history and society? How do we understand Korean cinema vis-\u00e0-vis the public memories of the Korean War, industrialization, social movements, economic development, and globalization? And how do aesthetics and storytelling in Korean cinema contribute to its popularity among local spectators and to its globality in shaping the contours of world cinema? By deeply inquiring into such questions, students learn how to critically view, think about, and write about film. Primary texts include literature and film. All films are screened with English subtitles.", + "professors": [ + "Tian Li" + ], + "codes": [ + "EALL 297", + "EAST 300", + "FILM 342" + ], + "flags": [], + "distributions": [ + "Hu" + ], + "credits": 1, + "requirements": "", + "term": "202403", + "colsem": false, + "fysem": false, + "sysem": false + }, + { + "course_id": 240313325, + "title": "Shakespeare and Popular Culture", + "description": "How and why did Shakespeare become \"popular\"? Why is he still part of popular culture today? In this transhistorical and interdisciplinary course, we chart the history of Shakespeare\u2019s celebrity, from the first publication of his works to their first adaptations in the Restoration, from Garrick\u2019s Shakespeare Jubilee to the preservation of the Shakespeare Birthplace that he put on the map, from the recreation of the Globe Theatre to the role of Shakespeare in our contemporary cultural imagination. We read\u00a0Romeo and Juliet,\u00a0Hamlet, and\u00a0Macbeth\u00a0alongside a wide range of adaptations and cultural objects they inspire, using television, film, graphic novels, short stories, advertising, toys and souvenirs, and even tumblr poetry to consider how Shakespeare\u2019s legacy evolves to meet the needs of changing eras. By the end of the course, we curate a collection of contemporary Shakespeariana to consider what Shakespeare means to our popular imagination.", + "professors": [ + "Nicole Sheriko" + ], + "codes": [ + "ENGL 216" + ], + "flags": [ + "YC ENGL Junior Seminar", + "YC ENGL Renaissance", + "YC THST Histories" + ], + "distributions": [ + "Hu", + "WR" + ], + "credits": 1, + "requirements": "Not open to students who took ENGL 012.", + "term": "202403", + "colsem": false, + "fysem": false, + "sysem": false + }, + { + "course_id": 240312962, + "title": "Approaches to International Security", + "description": "Introduction to major approaches and central topics in the field of international security, with primary focus on the principal man-made threats to human security: the use of violence among and within states, both by state and non-state actors.", + "professors": [], + "codes": [ + "GLBL 275" + ], + "flags": [], + "distributions": [ + "So" + ], + "credits": 0, + "requirements": "Priority to Global Affairs majors. Non-majors require permission of the instructor.", + "term": "202403", + "colsem": false, + "fysem": false, + "sysem": false + }, + { + "course_id": 240310136, + "title": "Database Systems", + "description": "Introduction to database systems. Data modeling. The relational model and the SQL query language. Relational database design, integrity constraints, functional dependencies, and normal forms. Object-oriented databases. Database data structures: files, B-trees, hash indexes.", + "professors": [ + "Avi Silberschatz" + ], + "codes": [ + "CPSC 437", + "CPSC 537" + ], + "flags": [], + "distributions": [ + "QR" + ], + "credits": 1, + "requirements": "After CPSC 223.", + "term": "202403", + "colsem": false, + "fysem": false, + "sysem": false + }, + { + "course_id": 240312331, + "title": "What Was Reading?", + "description": "This course takes a long and curious view of the history of reading, using primary sources, material objects, historical records, and contemporary debates to unsettle our assumptions about what reading is and does. How have ideas about the meaning and purpose of reading changed over time? What methods or goals have fallen out of favor, and which continue to shape our ideologies of reading today? What relation is there between the reading we do in a Yale English class, and the reading we do on the beach, or at synagogue, or online\u2015and where do those different sorts of reading come from? The syllabus focuses on early modern English literature, but it also engages ongoing debates about reading in the present, seeking both to link them to and distinguish them from earlier controversies. For instance, a unit on reading as religion raises questions about the morally improving (or morally destabilizing) effects of scriptural interpretation that then haunt later debates about the merits and limitations of anti-racist reading, as James Baldwin argues; similarly, early arguments about the effeminating influence of certain books--especially those aimed at women or young readers--give rise to assumptions about gender and genre that still shape our ambivalence toward reading for pleasure. As we explore these older efforts to shape, inform, regulate, or liberate reading, we\u2019ll also experiment with our own readerly practices, using forgotten or neglected forms like the commonplace book, the moral commentary, or the meditation as foils to the more usual modes of academic writing.", + "professors": [ + "Catherine Nicholson" + ], + "codes": [ + "ENGL 229" + ], + "flags": [ + "YC CPLT Theory", + "YC ENGL Junior Seminar", + "YC ENGL Renaissance" + ], + "distributions": [ + "Hu", + "WR" + ], + "credits": 1, + "requirements": "", + "term": "202403", + "colsem": false, + "fysem": false, + "sysem": false + }, + { + "course_id": 240314316, + "title": "Independent Project I", + "description": "By arrangement with faculty.", + "professors": [ + "Lin Zhong" + ], + "codes": [ + "CPSC 690" + ], + "flags": [], + "distributions": [], + "credits": 1, + "requirements": "", + "term": "202403", + "colsem": false, + "fysem": false, + "sysem": false + }, + { + "course_id": 240310263, + "title": "Sound/Image Practice", + "description": "We start from the assumption that sound is actually the \u2018secret-sauce\u2019 in the film/videomaking process. Often overlooked\u2013or at least neglected, sound is a potent tool to advance the logic of a film or video and even more, to enhance the emotional patina and immersive engagement of a film or video. Sound becomes an accessible portal to the perhaps overlooked not-quite-conscious realm of the film/video experience. While we certainly read some theory/history of sound, this is primarily a class of making. The first 7 weeks include videomaking exercises designed to highlight specific challenges in sound for picture. The core concern is with conceptual development in the myriad ways that sound and picture work together. There is no genre or mode preference in this class. Fiction, non-fiction, experimental, animation, game, tiktok, anything is okay. For the second half of the semester, each student (or collaborative small group\u2013with permission) design, shoot, edit, and mix a short (3-5min) video of their own design\u2013a video that demonstrates attention and developing sophistication in the use of sound with picture, as well as in how to design visual shots and temporal structures (editing) with sound in mind. The visual and auditory aspects of any video are entangled in such a way that contribute (when blended with the audience\u2019s imagination and memory) to the formation of the Sound/Image in the audience member\u2019s minds.", + "professors": [ + "Leighton Pierce" + ], + "codes": [ + "FILM 460" + ], + "flags": [ + "YC FILM Production" + ], + "distributions": [], + "credits": 1, + "requirements": "", + "term": "202403", + "colsem": false, + "fysem": false, + "sysem": false + }, + { + "course_id": 240310267, + "title": "The Senior Essay", + "description": "An independent writing and research project. A prospectus signed by the student's adviser must be submitted to the director of undergraduate studies by the end of the second week of the term in which the essay project is to commence. A rough draft must be submitted to the adviser and the director of undergraduate studies approximately one month before the final draft is due. Essays are normally thirty-five pages long (one term) or fifty pages (two terms).", + "professors": [ + "John Peters" + ], + "codes": [ + "FILM 491" + ], + "flags": [ + "YC FILM Critical Studies" + ], + "distributions": [], + "credits": 1, + "requirements": "", + "term": "202403", + "colsem": false, + "fysem": false, + "sysem": false + }, + { + "course_id": 240310726, + "title": "Socialist '80s: Aesthetics of Reform in China and the Soviet Union", + "description": "This course offers an interdisciplinary introduction to the study of the complex cultural and political paradigms of late socialism from a transnational perspective by focusing on the literature, cinema, and popular culture of the Soviet Union and China in 1980s. How were intellectual and everyday life in the Soviet Union and China distinct from and similar to that of the West of the same era? How do we parse \"the cultural logic of late socialism?\" What can today\u2019s America learn from it? Examining two major socialist cultures together in a global context, this course queries the ethnographic, ideological, and socio-economic constituents of late socialism. Students analyze cultural materials in the context of Soviet and Chinese history. Along the way, we explore themes of identity, nationalism, globalization, capitalism, and the Cold War.\u00a0Students with knowledge of Russian and Chinese are encouraged to read in original languages. All readings are\u00a0available in English.", + "professors": [ + "Jinyi Chu" + ], + "codes": [ + "LITR 303", + "RUSS 605", + "RSEE 316", + "CPLT 612", + "EALL 288", + "RUSS 316", + "RSEE 605", + "EALL 588", + "EAST 316", + "EAST 616" + ], + "flags": [ + "YC GLBL Elective" + ], + "distributions": [ + "Hu", + "WR" + ], + "credits": 1, + "requirements": "", + "term": "202403", + "colsem": false, + "fysem": false, + "sysem": false + }, + { + "course_id": 240310839, + "title": "Statistical Case Studies", + "description": "Statistical analysis of a variety of statistical problems using real data. Emphasis on methods of choosing data, acquiring data, assessing data quality, and the issues posed by extremely large data sets. Extensive computations using R.", + "professors": [ + "Brian Macdonald" + ], + "codes": [ + "S&DS 625" + ], + "flags": [], + "distributions": [], + "credits": 1, + "requirements": "Enrollment limited; requires permission of the instructor.", + "term": "202403", + "colsem": false, + "fysem": false, + "sysem": false + }, + { + "course_id": 240311133, + "title": "Jesus to Muhammad: Ancient Christianity to the Rise of Islam", + "description": "The history of Christianity and the development of Western culture from Jesus to the early Middle Ages. The creation of orthodoxy and heresy; Christian religious practice; philosophy and theology; politics and society; gender; Christian literature in its various forms, up to and including the early Islamic period.", + "professors": [ + "Stephen Davis" + ], + "codes": [ + "HUMS 129", + "HIST 159", + "NELC 158", + "RLST 158", + "CLCV 129", + "RLST 649" + ], + "flags": [], + "distributions": [ + "Hu" + ], + "credits": 1, + "requirements": "", + "term": "202403", + "colsem": false, + "fysem": false, + "sysem": false + }, + { + "course_id": 240311540, + "title": "Independent Study", + "description": "By arrangement with faculty and with approval of the DGS.", + "professors": [], + "codes": [ + "EAST 910" + ], + "flags": [], + "distributions": [], + "credits": 1, + "requirements": "", + "term": "202403", + "colsem": false, + "fysem": false, + "sysem": false + }, + { + "course_id": 240311660, + "title": "Readings in Anthropology", + "description": "For students who wish to investigate an area of anthropology not covered by regular departmental offerings. The project must terminate with at least a term paper or its equivalent. No student may take more than two terms for credit. To apply for admission, a student should present a prospectus and bibliography to the director of undergraduate studies no later than the third week of the term. Written approval from the faculty member who will direct the student's reading and writing must accompany the prospectus.", + "professors": [ + "William Honeychurch" + ], + "codes": [ + "ANTH 472" + ], + "flags": [ + "YC ANTH Archaeology", + "YC ANTH Biological", + "YC ANTH Sociocultural" + ], + "distributions": [], + "credits": 1, + "requirements": "", + "term": "202403", + "colsem": false, + "fysem": false, + "sysem": false + }, + { + "course_id": 240311827, + "title": "Modern Physical Measurement", + "description": "A\u00a0two-term sequence of experiments in classical and modern physics for students who plan to major in Physics. In the first term, the basic principles of mechanics, electricity, and magnetism are illustrated in experiments designed to make use of computer data handling and teach error analysis. In the second term, students plan and carry out experiments illustrating aspects of wave and quantum phenomena and of atomic, solid state, and nuclear physics using modern instrumentation.", + "professors": [ + "David Moore", + "Mehdi Ghiassi-Nejad", + "Reina Maruyama", + "Stephen Irons" + ], + "codes": [ + "PHYS 206L" + ], + "flags": [], + "distributions": [ + "Sc" + ], + "credits": 0.5, + "requirements": "", + "term": "202403", + "colsem": false, + "fysem": false, + "sysem": false + }, + { + "course_id": 240312047, + "title": "The Senior Essay", + "description": "All senior History majors should attend the mandatory senior essay meeting in early September at a time and location to be announced in the online Senior Essay Handbook.\nThe senior essay is a required one- or two-term independent research project conducted under the guidance of a faculty adviser. As a significant work of primary-source research, it serves as the capstone project of the History major. Students writing the one-term senior essay enroll in HIST 497 (see description), not HIST 495 and 496. The two-term essay takes the form of a substantial article, not longer than 12,500 words (approximately forty to fifty double-spaced typewritten pages). This is a maximum limit; there is no minimum requirement. Length will vary according to the topic and the historical techniques employed. Students writing the two-term senior essay who expect to graduate in May enroll in HIST 495 during the fall term and complete their essays in HIST 496 in the spring term. December graduates enroll in HIST 495 in the spring term and complete their essays in HIST 496 during the following fall term; students planning to begin their essay in the spring term should notify the senior essay director by early December. Each student majoring in History must present a completed Statement of Intention, signed by a department member who has agreed to serve as adviser, to the History Department Undergraduate Registrar by the dates indicated in the Senior Essay Handbook. Blank statement forms are available from the History Undergraduate Registrar and in the Senior Essay handbook. Students enrolled in HIST 495 submit to the administrator in 237 HGS a two-to-three-page analysis of a single primary source, a draft bibliographic essay, and at least ten pages of the essay by the deadlines listed in the Senior Essay Handbook. Those who meet these requirements receive a temporary grade of SAT for the fall term, which will be changed to the grade received by the essay upon its completion. Failure to meet any requirement may result in the student\u2019s being asked to withdraw from HIST 495. Students enrolled in HIST 496 must submit a completed essay to 211 HGS no later than 5 p.m. on the dates indicated in the Senior Essay Handbook. Essays submitted after 5 p.m. will be considered as having been turned in on the following day. If the essay is submitted late without an excuse from the student's residential college dean, the penalty is one letter grade for the first day and one-half letter grade for each of the next two days past the deadline. No essay that would otherwise pass will be failed because it is late, but late essays will not be considered for departmental or Yale College prizes. All senior departmental essays will be judged by members of the faculty other than the adviser. In order to graduate from Yale College, a student majoring in History must achieve a passing grade on the departmental essay.", + "professors": [ + "Anne Eller" + ], + "codes": [ + "HIST 495" + ], + "flags": [], + "distributions": [], + "credits": 1, + "requirements": "", + "term": "202403", + "colsem": false, + "fysem": false, + "sysem": false + }, + { + "course_id": 240312048, + "title": "The Senior Essay", + "description": "All senior History majors should attend the mandatory senior essay meeting in early September at a time and location to be announced in the online Senior Essay Handbook.\nThe senior essay is a required one- or two-term independent research project conducted under the guidance of a faculty adviser. As a significant work of primary-source research, it serves as the capstone project of the History major. Students writing the one-term senior essay enroll in HIST 497 (see description), not HIST 495 and 496. The two-term essay takes the form of a substantial article, not longer than 12,500 words (approximately forty to fifty double-spaced typewritten pages). This is a maximum limit; there is no minimum requirement. Length will vary according to the topic and the historical techniques employed. Students writing the two-term senior essay who expect to graduate in May enroll in HIST 495 during the fall term and complete their essays in HIST 496 in the spring term. December graduates enroll in HIST 495 in the spring term and complete their essays in HIST 496 during the following fall term; students planning to begin their essay in the spring term should notify the senior essay director by early December. Each student majoring in History must present a completed Statement of Intention, signed by a department member who has agreed to serve as adviser, to the History Department Undergraduate Registrar by the dates indicated in the Senior Essay Handbook. Blank statement forms are available from the History Undergraduate Registrar and in the Senior Essay handbook. Students enrolled in HIST 495 submit to the administrator in 237 HGS a two-to-three-page analysis of a single primary source, a draft bibliographic essay, and at least ten pages of the essay by the deadlines listed in the Senior Essay Handbook. Those who meet these requirements receive a temporary grade of SAT for the fall term, which will be changed to the grade received by the essay upon its completion. Failure to meet any requirement may result in the student\u2019s being asked to withdraw from HIST 495. Students enrolled in HIST 496 must submit a completed essay to 211 HGS no later than 5 p.m. on the dates indicated in the Senior Essay Handbook. Essays submitted after 5 p.m. will be considered as having been turned in on the following day. If the essay is submitted late without an excuse from the student's residential college dean, the penalty is one letter grade for the first day and one-half letter grade for each of the next two days past the deadline. No essay that would otherwise pass will be failed because it is late, but late essays will not be considered for departmental or Yale College prizes. All senior departmental essays will be judged by members of the faculty other than the adviser. In order to graduate from Yale College, a student majoring in History must achieve a passing grade on the departmental essay.", + "professors": [ + "Anne Eller" + ], + "codes": [ + "HIST 496" + ], + "flags": [], + "distributions": [], + "credits": 1, + "requirements": "", + "term": "202403", + "colsem": false, + "fysem": false, + "sysem": false + }, + { + "course_id": 240312083, + "title": "Research and Senior Thesis", + "description": "Two terms of independent library, laboratory, field, or modeling-based research under faculty supervision. To register for this course, each student must submit a written plan of study, approved by a faculty adviser, to the director of undergraduate studies by the start of the senior year.\u00a0The plan requires approval of the full EPS faculty.", + "professors": [ + "Pincelli Hull" + ], + "codes": [ + "EPS 490" + ], + "flags": [], + "distributions": [], + "credits": 1, + "requirements": "", + "term": "202403", + "colsem": false, + "fysem": false, + "sysem": false + }, + { + "course_id": 240312084, + "title": "Research and Senior Thesis", + "description": "Two terms of independent library, laboratory, field, or modeling-based research under faculty supervision. To register for this course, each student must submit a written plan of study, approved by a faculty adviser, to the director of undergraduate studies by the start of the senior year.\u00a0The plan requires approval of the full EPS faculty.", + "professors": [ + "Pincelli Hull" + ], + "codes": [ + "EPS 491" + ], + "flags": [], + "distributions": [], + "credits": 1, + "requirements": "", + "term": "202403", + "colsem": false, + "fysem": false, + "sysem": false + }, + { + "course_id": 240312214, + "title": "Frontiers of Public Health", + "description": "This course is designed to expose students to the breadth of public health and is required of M.S. and Ph.D. students who do not have prior degrees in public health. It explores the major public health achievements in the last century in order to provide students with a conceptual interdisciplinary framework by which effective interventions are developed and implemented. Case studies and discussions examine the advances across public health disciplines including epidemiology and biostatistics, environmental and behavioral sciences, and health policy and management services that led to these major public health achievements. The course examines global and national trends in the burden of disease and underlying determinants of disease, which pose new challenges; and it covers new approaches that are on the forefront of addressing current and future public health needs.", + "professors": [ + "Jeannette Ickovics" + ], + "codes": [ + "EPH 537E" + ], + "flags": [], + "distributions": [], + "credits": 0, + "requirements": "Open only to students enrolled in the Executive Online M.P.H. Program. Not open to auditors.", + "term": "202403", + "colsem": false, + "fysem": false, + "sysem": false + }, + { + "course_id": 240312531, + "title": "Statistical Case Studies", + "description": "Statistical analysis of a variety of statistical problems using real data. Emphasis on methods of choosing data, acquiring data, assessing data quality, and the issues posed by extremely large data sets. Extensive computations using R statistical software.", + "professors": [ + "Jay Emerson" + ], + "codes": [ + "S&DS 425", + "S&DS 625" + ], + "flags": [ + "YC GLBL Addtl Methods Course" + ], + "distributions": [ + "QR" + ], + "credits": 1, + "requirements": "Prerequisites: S&DS 361, and prior course work in probability, statistics, and data analysis (e.g. 363, 365, 220, 230, etc., equivalent courses, or equivalent research/internship experience).\u00a0 Enrollment limited; requires permission of the instructor.\u00a0", + "term": "202403", + "colsem": false, + "fysem": false, + "sysem": false + }, + { + "course_id": 240313588, + "title": "Senior Capstone Project: AI, Emerging Tech and the Midd", + "description": "Students work in small task-force groups and complete a one-term public policy project under the guidance of a faculty member. Clients for the projects are drawn from government agencies, nongovernmental organizations and nonprofit groups, and private sector organizations in the United States and abroad. Projects and clients vary from year to year.\nFulfills the capstone project requirement for the Global Affairs major.", + "professors": [ + "Roland McKay" + ], + "codes": [ + "GLBL 499" + ], + "flags": [ + "PH Glbl Hlth Devlpmt/PolEcon" + ], + "distributions": [], + "credits": 1, + "requirements": "", + "term": "202403", + "colsem": false, + "fysem": false, + "sysem": false + }, + { + "course_id": 240313622, + "title": "Structural and Functional Organization of the Human Nervous System", + "description": "An integrative overview of the structure and function of the human brain as it pertains to major neurological and psychiatric disorders. Neuroanatomy, neurophysiology, and clinical correlations are interrelated to provide essential background in the neurosciences. Lectures in neurocytology and neuroanatomy survey neuronal organization in the human brain, with emphasis on long fiber tracts related to clinical neurology. Lectures in neurophysiology cover various aspects of neural function at the cellular and systems levels, with a strong emphasis on the mammalian nervous system. Clinical correlations consist of sessions applying basic science principles to understanding pathophysiology in the context of patients. Seven three-hour laboratory sessions are coordinated with lectures throughout the course to provide an understanding of the structural basis of function and disease. Case-based conference sections provide an opportunity to integrate and apply the information learned about the structure and function of the nervous system in the rest of the course to solving a focused clinical problem in a journal club format. Variable class schedule; contact course instructors. This course is offered to graduate and M.D./Ph.D. students only and cannot be audited.", + "professors": [ + "Thomas Biederer" + ], + "codes": [ + "INP 510" + ], + "flags": [], + "distributions": [], + "credits": 1, + "requirements": "", + "term": "202403", + "colsem": false, + "fysem": false, + "sysem": false + } +] \ No newline at end of file diff --git a/scrapers/coursetableScraper/results_202501.json b/scrapers/coursetableScraper/results_202501.json new file mode 100644 index 0000000..1c87877 --- /dev/null +++ b/scrapers/coursetableScraper/results_202501.json @@ -0,0 +1,535 @@ +[ + { + "course_id": 250121861, + "title": "Social and Cultural Factors in Mental Health and Illness", + "description": "This course provides an introduction to mental health and illness with a focus on the complex interplay between risk and protective factors and social and cultural influences on mental health status. We examine the role of social and cultural factors in the etiology, course, and treatment of substance misuse; depressive, anxiety, and psychotic disorders; and some of the severe behavioral disorders of childhood. The social consequences of mental illness such as stigma, isolation, and barriers to care are explored, and their impact on access to care and recovery considered. The effectiveness of the current system of services and the role of public health and public health professionals in mental health promotion are discussed.", + "professors": [], + "codes": [ + "PSYC 576" + ], + "flags": [], + "distributions": [], + "credits": 1, + "requirements": "", + "term": "202501", + "colsem": false, + "fysem": false, + "sysem": false + }, + { + "course_id": 250125120, + "title": "Research", + "description": "Individual research for Ph.D. degree candidates in the Department of Chemistry, under the direct supervision of one or more faculty members.", + "professors": [ + "Tianyu Zhu" + ], + "codes": [ + "CHEM 990" + ], + "flags": [], + "distributions": [], + "credits": 1, + "requirements": "", + "term": "202501", + "colsem": false, + "fysem": false, + "sysem": false + }, + { + "course_id": 250121135, + "title": "Junior Seminar", + "description": "Ongoing visual projects addressed in relation to historical and contemporary issues. Readings, slide presentations, critiques by School of Art faculty, and gallery and museum visits. Critiques address all four areas of study in the Art major.", + "professors": [ + "Elle Perez" + ], + "codes": [ + "ART 395" + ], + "flags": [], + "distributions": [ + "Hu" + ], + "credits": 1, + "requirements": "Prerequisite: at least four courses in Art.", + "term": "202501", + "colsem": false, + "fysem": false, + "sysem": false + }, + { + "course_id": 250121016, + "title": "Topics: Events, Distributivity, Durational Modifiers", + "description": "This course bridges introductory courses (LING 263, LING 264) and advanced seminars in semantics. It explores selected topics in some detail, allowing students to appreciate the nuances of semantic argumentation while at the same time emphasizing the foundational issues involved.\u00a0The goal of this course is to allow students, within a structured format, to become comfortable engaging with open-ended problems and to gain confidence in proposing original solutions to such problems.\u00a0Topics vary across semesters.", + "professors": [ + "Veneeta Dayal", + "Simon Charlow" + ], + "codes": [ + "LING 291", + "LING 691" + ], + "flags": [ + "YC LING Depth Semntcs/Pragmat", + "YC LING Elective", + "YC LING Intermediate Courses" + ], + "distributions": [ + "So" + ], + "credits": 1, + "requirements": "Prerequisite: LING 263 / LING 663 or permission of Instructor", + "term": "202501", + "colsem": false, + "fysem": false, + "sysem": false + }, + { + "course_id": 250123229, + "title": "Yale Concert Band", + "description": "The Yale Concert Band, a group of 45-60 wind, brass, and percussion players, embraces the aesthetics of the traditional wind band and the contemporary experimental ensemble. Our repertoire consists of a panoply of wind band classics; premieres by and commissions of Yale students, faculty and established world-class composers; and the newest wind band literature that incorporates electro-acoustic sounds, folk/rock/hip hop music, soloists, and theatrical trappings. The Yale Concert Band regularly presents concerts to benefit causes and organizations, ranging from benefit concerts to support the work of New Haven\u2019s IRIS (Integrated Refugee and Immigrant Services (2017, 2018, 2019); to provide aid to the relief efforts after Hurricane Katrina (2005), floods in Myanmar (2007), tornadoes in the American midwest (2007), the earthquake in Haiti (2010), the tsunami in Japan (2011), and West African Ebola recovery efforts (2016).\u00a0 In 1959, the Yale Concert Band became the first university band to produce an international concert tour, and, since then, has appeared in concerts in Japan, South Africa, Swaziland, Mexico, Brazil, Bermuda, Russia, Finland, the Czech Republic, Austria, Ireland, England, France, Italy, Denmark, Germany, Holland, Belgium, Lithuania, Latvia, Estonia, Ghana, Haiti, Greece, Australia, and Spain. This course cannot be applied toward the 36-course-credit requirement for the Yale bachelor's degree.", + "professors": [ + "Thomas Duffy" + ], + "codes": [ + "MUSI 190" + ], + "flags": [], + "distributions": [], + "credits": 0, + "requirements": "By audition at the beginning of the academic year or by permission of instructor.", + "term": "202501", + "colsem": false, + "fysem": false, + "sysem": false + }, + { + "course_id": 250124916, + "title": "Exploring and Understanding White Collar Crime", + "description": "This course examines the many aspects of white collar crime; perjury, obstruction of justice, corporate crimes, Ponzi Schemes, insider trading, money laundering bribery and political corruption. The course explores how white collar crime, once virtually ignored by law enforcement has become a major focus of federal and state investigative agencies with massive resources allocated toward combatting it. The seminar examines the root causes of white collar crime as well as its pervasiveness in every day life.\u00a0 Specific cases of white collar defendants, both individuals and corporations that have profoundly impacted business, law, science, healthcare and other disciplines are examined.", + "professors": [ + "Bradley Simon" + ], + "codes": [ + "CSMY 220" + ], + "flags": [ + "YC College Seminar" + ], + "distributions": [], + "credits": 1, + "requirements": "Students may enroll in no more than 1 RCS for credit in a given term.", + "term": "202501", + "colsem": true, + "fysem": false, + "sysem": false + }, + { + "course_id": 250126581, + "title": "EL IntMedHematology2WK", + "description": "", + "professors": [], + "codes": [ + "MD 3090" + ], + "flags": [], + "distributions": [], + "credits": 0, + "requirements": "", + "term": "202501", + "colsem": false, + "fysem": false, + "sysem": false + }, + { + "course_id": 250126701, + "title": "Global Health Elective Ghana", + "description": "", + "professors": [], + "codes": [ + "MD 301" + ], + "flags": [], + "distributions": [], + "credits": 0, + "requirements": "", + "term": "202501", + "colsem": false, + "fysem": false, + "sysem": false + }, + { + "course_id": 250120660, + "title": "Methods in Gender and Sexuality Studies", + "description": "This seminar explores\u00a0the dynamics of power and knowledge, the ethics of representation and accountability, and the nexus between disciplinarity and interdisciplinarity. It is designed for graduate students developing research projects that engage feminist, queer, postcolonial, and critical race methodologies, among others. The course adopts an epistemological approach that centers \"encounter\" across geopolitical scales and multiple disciplinary fronts in the humanities and social sciences. It posits that research methods, regardless of their origin, can adopt feminist, queer, decolonial/postcolonial, and critical race perspectives and potentially serve counter-disciplinary purposes. Although we cover a broad spectrum of methods\u2014ranging from ethnographic, historiographic/archival, and geographic, to literary, media, and textual analysis, cultural studies, and political theory\u2014our work does not unfold as a practicum. Instead of experimenting with a predefined \"toolkit,\" students critically engage book-length works that demonstrate counter-disciplinary methodologies, reflecting hermeneutically on how method and theory relate in these texts by drawing on Foucault's framework of \"the archaeology of knowledge.\"", + "professors": [ + "Eda Pepi" + ], + "codes": [ + "AMST 798", + "WGSS 800" + ], + "flags": [ + "YC Ethnography Methods" + ], + "distributions": [], + "credits": 1, + "requirements": "", + "term": "202501", + "colsem": false, + "fysem": false, + "sysem": false + }, + { + "course_id": 250121028, + "title": "Special Investigations", + "description": "Directed research by arrangement with individual faculty members and approved by the DGS. Students are expected to propose and complete a term-long research project. The culmination of the project is a presentation that fulfills the departmental requirement for the research qualifying event.", + "professors": [ + "Daisuke Nagai", + "Rona Ramos" + ], + "codes": [ + "PHYS 990" + ], + "flags": [], + "distributions": [], + "credits": 1, + "requirements": "", + "term": "202501", + "colsem": false, + "fysem": false, + "sysem": false + }, + { + "course_id": 250121423, + "title": "Topics in Biomedical Informatics and Data Science", + "description": "The course focuses on providing an introduction to common unifying themes that serve as the foundation for different areas of biomedical informatics, including clinical, neuro-, and genome informatics. The course is designed for students with basic computer experience and course work who plan to build databases and computational tools for use in biomedical research. Emphasis is on understanding basic principles underlying informatics approaches to interoperation among biomedical databases and software tools, standardized biomedical vocabularies and ontologies, biomedical natural language processing, modeling of biological systems, high-performance computation in biomedicine, and other related topics.", + "professors": [ + "Samah Jarad" + ], + "codes": [ + "BIS 550", + "CB&B 750", + "HSCI 5500" + ], + "flags": [], + "distributions": [], + "credits": 0, + "requirements": "", + "term": "202501", + "colsem": false, + "fysem": false, + "sysem": false + }, + { + "course_id": 250121441, + "title": "Topics in Biomedical Informatics and Data Science", + "description": "The course focuses on providing an introduction to common unifying themes that serve as the foundation for different areas of biomedical informatics, including clinical, neuro-, and genome informatics. The course is designed for students with significant computer experience and course work who plan to build databases and computational tools for use in biomedical research. Emphasis is on understanding basic principles underlying informatics approaches to interoperation among biomedical databases and software tools, standardized biomedical vocabularies and ontologies, biomedical natural language processing, modeling of biological systems, high-performance computation in biomedicine, and other related topics.", + "professors": [ + "Samah Jarad", + "Kei-Hoi Cheung" + ], + "codes": [ + "BIS 543E" + ], + "flags": [], + "distributions": [], + "credits": 0, + "requirements": "Open only to students enrolled in the Executive Online M.P.H. Program. Not open to auditors.", + "term": "202501", + "colsem": false, + "fysem": false, + "sysem": false + }, + { + "course_id": 250122238, + "title": "Corporate Sustainability: Strategy and Management", + "description": "This survey course focuses on the policy and business logic for making environmental issues and sustainability a core focus of corporate strategy and management. Students are asked to analyze when and how sustainability leadership can translate into competitive advantage by helping to cut costs, reduce risk, drive growth, and promote brand identity and intangible value. The course seeks to provide students with an introduction to the range of sustainability issues and challenges that companies face in today\u2019s fast-changing marketplace. It introduces key corporate sustainability terms, concepts, tools, strategies, and frameworks based on the overarching theory that the traditional profit-maximizing mission of business (often called shareholder primacy) is giving way to a new vision of stakeholder responsibility that still seeks to provide good returns to the enterprise\u2019s owners but also acknowledges obligations to employees, suppliers, customers, communities, and society more broadly. The course combines lectures, case studies, and class discussions on management theory and tools, the legal and regulatory frameworks that shape the business-environment interface, and the evolving role of business in society. It explores how to deal with a world of diverse stakeholders, increasing transparency, and rising expectations related to corporate environmental, social, and governance (ESG) performance. Self-scheduled examination.", + "professors": [ + "Daniel Esty" + ], + "codes": [ + "ENV 807", + "MGT 688" + ], + "flags": [ + "YC ENRG Energy & Environment", + "YSE MEM B&E Core", + "YSE MEM IEGC Add'l Electives", + "YSE MEM IEGC Primary Electives" + ], + "distributions": [], + "credits": 0, + "requirements": "", + "term": "202501", + "colsem": false, + "fysem": false, + "sysem": false + }, + { + "course_id": 250122406, + "title": "What was Latinx Literature", + "description": "With the arrival of \"Latinx,\" the last decade was defined as a moment of rupture and break with traditional notions of latinidad. Artists and activists asserted refusal and historical reckoning as the mode of doing politics and aesthetics. Now, pessimistic about Latinx as a signifier of a unified political project, the generational tides have shifted to \"Latine.\" This seminar asks what is \"Latinx literature\" and why are the methods of \"Latinx studies\" considered revolutionary or disruptive? What ideas were rooted in prior generations of feminist and queer collectives that sustained life when the arrival of a decolonial future seemed forever deferred and withheld from reach? We examine contemporary artists alongside historical antecedents to reevaluate what literary and social forms can help us challenge a racialized, heteronormative conception of citizenship. One possibility is that Gloria Anzald\u00faa\u2014rightly critiqued for her relation to mestizaje \u2014might be helpful in this moment of growing nationalism and hostility towards migrants to think about other ways of organizing life aside borders and the nation. We read across a long and varied arc of creative expression to consider forms that endure amidst colonial duress. For example: the serial, montage, anthology, performance collective, and inter-linked storytelling. Artists up for discussion may include Natalie Diaz, John Rechy, and Jes\u00fas Col\u00f3n. Students will engage these works alongside theorists like Jos\u00e9 Esteban Mu\u00f1oz and Juana Mar\u00eda Rodr\u00edguez. Previously ENGL 331.", + "professors": [ + "Joseph Miranda" + ], + "codes": [ + "ENGL 4831", + "ER&M 268" + ], + "flags": [ + "YC ENGL 20th/21st Century", + "YC ENGL Senior Seminar" + ], + "distributions": [ + "Hu", + "WR" + ], + "credits": 1, + "requirements": "", + "term": "202501", + "colsem": false, + "fysem": false, + "sysem": false + }, + { + "course_id": 250122770, + "title": "Machine Learning for Economic Analysis", + "description": "Machine learning algorithms and their applications to economic analysis, specifically causal inference, learning, and game theory. Curse of dimensionality, model selection, and choice of tuning parameters from a computational and econometric perspective.", + "professors": [ + "Max Cytrynbaum" + ], + "codes": [ + "ECON 566" + ], + "flags": [], + "distributions": [], + "credits": 1, + "requirements": "", + "term": "202501", + "colsem": false, + "fysem": false, + "sysem": false + }, + { + "course_id": 250122954, + "title": "Machine Learning for Economic Analysis", + "description": "Machine learning algorithms and their applications to economic analysis, specifically causal inference, learning, and game theory. Curse of dimensionality, model selection, and choice of tuning parameters from a computational and econometric perspective.", + "professors": [ + "Max Cytrynbaum" + ], + "codes": [ + "ECON 428" + ], + "flags": [], + "distributions": [ + "So" + ], + "credits": 1, + "requirements": "Prerequisites: CPSC 100 or CPSC 112; and ECON 117 or ECON 136.", + "term": "202501", + "colsem": false, + "fysem": false, + "sysem": false + }, + { + "course_id": 250123384, + "title": "Secndry Instrmnt--: Secndry Instrmnt--PIANO", + "description": "2 credits per term. P/F. All students enrolled in secondary lessons can receive instruction in either voice or piano. In addition, YSM keyboard majors may take secondary organ or harpsichord, and YSM violinists may take secondary viola. Any other students who wish to take secondary lessons in any other instruments must petition the director of secondary lessons, Kyung Yu, by email (kyung.yu@yale.edu) no later than Aug. 30, 2021, for the fall term and Jan. 14, 2022, for the spring term. Students who are not conducting majors may take only one secondary instrument per term. YSM students who wish to take secondary lessons must register for the course and request a teacher using the online form for graduate students found at http://music.yale.edu/study/music-lessons; the availability of a secondary lessons teacher is not guaranteed until the form is received and a teacher assigned by the director of lessons. Secondary instruction in choral conducting and orchestral conducting is only available with permission of the instructor and requires as prerequisites MUS 565 for secondary instruction in choral conducting, and both MUS 529 and MUS 530 for secondary instruction in orchestral conducting. Students of the Yale Divinity School, School of Drama, and School of Art may also register as above for secondary lessons and will be charged $200 per term for these lessons. Questions may be emailed to the director, Kyung Yu (kyung.yu@yale.edu).", + "professors": [ + "Kyung Yu" + ], + "codes": [ + "MUS 541" + ], + "flags": [], + "distributions": [], + "credits": 0, + "requirements": "", + "term": "202501", + "colsem": false, + "fysem": false, + "sysem": false + }, + { + "course_id": 250123877, + "title": "The Senior Essay I", + "description": "Students wishing to undertake an independent senior essay in English must submit a proposal to the DUS in the previous term; deadlines and instructions are posted at https://english.yale.edu/undergraduate/courses/independent-study-courses. For one-term senior essays, the essay itself is due in the office of the director of undergraduate studies according to the following schedule: (1) end of the fourth week of classes: five to ten pages of writing and/or an annotated bibliography; (2) end of the ninth week of classes: a rough draft of the complete essay; (3) end of the last week of classes (fall term) or end of the next-to-last week of classes (spring term): the completed essay. Consult the director of undergraduate studies regarding the schedule for submission of the yearlong senior essay.", + "professors": [ + "Marcel Elias", + "Stefanie Markovits" + ], + "codes": [ + "ENGL 4100" + ], + "flags": [], + "distributions": [], + "credits": 1, + "requirements": "", + "term": "202501", + "colsem": false, + "fysem": false, + "sysem": false + }, + { + "course_id": 250124362, + "title": "Styles of Professional Prose: Writing about Legal Affairs", + "description": "A seminar and workshop in the conventions of good writing in a specific field. Each section focuses on one professional kind of writing and explores its distinctive features through a variety of written and oral assignments, in which students both analyze and practice writing in the field. Section topics, which change yearly, are listed at the beginning of each term on the English department website. This course may be repeated for credit in a section that treats a different genre or style of writing; may not be repeated for credit toward the major. ENGL 121 and ENGL 421 may not be taken for credit on the same topic.", + "professors": [ + "Lincoln Caplan" + ], + "codes": [ + "ENGL 1021" + ], + "flags": [], + "distributions": [ + "WR" + ], + "credits": 1, + "requirements": "Prerequisite: ENGL 114, 115, 120, or another writing-intensive course at Yale.", + "term": "202501", + "colsem": false, + "fysem": false, + "sysem": false + }, + { + "course_id": 250124828, + "title": "Abolition in the Americas", + "description": "This seminar examines histories of slavery's abolition in the Americas. It situates the end of slavery in the United States within hemispheric and transatlantic contexts, touching upon processes of abolition\u00a0in Antigua, Brazil, Colombia, Cuba, the Dominican Republic, Haiti, Jamaica, and more. The course approaches abolition as a historiographical problem, considering debates about its definition, its causes and consequences, its primary agents, its periodization, and its relation to other historical processes. Questions include: How have historians defined abolition? How have they periodized it? How have scholars variously characterized the forms it took and who was responsible for it? How have they differently understood the social, cultural, economic, legal, and political conditions that gave rise to abolition? How have historians agreed and disagreed upon its effects and its aftermath? How have they framed the relation between freedom and formal emancipation? How have the communicated the stakes of their accounts of abolition? The organization of the course is topical and loosely chronological. Readings address the origins of abolition in the Atlantic world, the Haitian Revolution, processes of gradual emancipation, the historical significance of Black abolitionists, the activism of women and children, the formation and contributions of antislavery movements, the practice of moral suasion, the question of violence and antislavery militantism, antislavery discourses of rights and sexual morals, the circulation of racial and climate science in abolitionist circles, enslaved people\u2019s practices of fugitivity, self-purchase, and revolt, the relation between capitalism and abolitionism, the Civil War, and the so-called \"last abolition\" of slavery in Brazil.", + "professors": [ + "Caleb Knapp" + ], + "codes": [ + "HIST 124J" + ], + "flags": [ + "YC HIST Cultural History", + "YC HIST Departmental Seminars", + "YC HIST Empires & Colonialism", + "YC HIST Ideas & Intellectuals", + "YC HIST Pltcs, Law & Govt", + "YC HIST Race Gender&Sexuality", + "YC HIST Soc Chng&Social Mvmnt", + "YC HIST United States", + "YC HIST War & Society", + "YC HSHM Colonial Know & Power", + "YC HSHM Gender, Reprod & Body" + ], + "distributions": [ + "Hu", + "WR" + ], + "credits": 1, + "requirements": "", + "term": "202501", + "colsem": false, + "fysem": false, + "sysem": false + }, + { + "course_id": 250125902, + "title": "Clinical Practice II for Global Health Track", + "description": "This clinical application course for students in the global health track provides opportunities to develop advanced nursing skills with a range of global populations within the students\u2019 areas of specialization. While in clinical settings, students develop skills in assessment and management of acute and chronic conditions using evidence-based patient management strategies in accordance with the cultural beliefs and practices of populations of immigrants, refugees, American Indians, and Alaskan native and rural residents. These experiences may take place in YSN-approved U.S. or international settings. Additional experiences with local resettlement organizations such as Integrated Refugee and Immigrant Services (IRIS) and Connecticut Institute for Refugees and Immigrants (CIRI) are also available. These experiences may include developing and presenting education programs to groups of refugees, immigrants, or asylum seekers; creating training materials for the resettlement agencies; or serving as a cultural companion or health navigator for newly arrived families. Required of all students pursuing the global health track during the fall term of their second specialty year. Thirty hours of face-to-face interactions either in a health care setting or in an alternative setting, and one hour per week of clinical conference. Taken after NURS 6230.", + "professors": [ + "Sandy Cayo" + ], + "codes": [ + "NURS 6240" + ], + "flags": [], + "distributions": [], + "credits": 0, + "requirements": "", + "term": "202501", + "colsem": false, + "fysem": false, + "sysem": false + }, + { + "course_id": 250126137, + "title": "Independent Course Work", + "description": "Program to be determined with a faculty adviser of the student\u2019s choice and submitted, with the endorsement of the study area coordinators, to the Rules Committee for confirmation of the student\u2019s eligibility under the rules. (See the School\u2019s Academic Rules and Regulations.)", + "professors": [ + "Brennan Buck" + ], + "codes": [ + "ARCH 2299" + ], + "flags": [], + "distributions": [], + "credits": 0, + "requirements": "", + "term": "202501", + "colsem": false, + "fysem": false, + "sysem": false + }, + { + "course_id": 250120297, + "title": "American Political Institutions", + "description": "The origins and development of American political institutions, especially in relation to constitutional choice and the agency of persons seeking freedom, equality, and self-governing capabilities as a driver of constitutional change. Key concepts include: American federalism, compound republic, citizenship, social movements, racial justice, and nonviolence.", + "professors": [ + "Michael Fotos" + ], + "codes": [ + "PLSC 256", + "AFAM 177", + "EP&E 248" + ], + "flags": [ + "YC EP&E Politics Core" + ], + "distributions": [ + "So", + "WR" + ], + "credits": 1, + "requirements": "", + "term": "202501", + "colsem": false, + "fysem": false, + "sysem": false + }, + { + "course_id": 250120530, + "title": "Elementary Modern Standard Arabic II", + "description": "Continuation of ARBC 110.", + "professors": [ + "Muhammad Aziz" + ], + "codes": [ + "ARBC 120", + "ARBC 501" + ], + "flags": [], + "distributions": [ + "L2" + ], + "credits": 1.5, + "requirements": "Prerequisite: ARBC 110 or\u00a0requisite score on a\u00a0placement test.", + "term": "202501", + "colsem": false, + "fysem": false, + "sysem": false + }, + { + "course_id": 250121598, + "title": "Chinese for Current Affairs", + "description": "Advanced language course with a focus on speaking and writing in formal styles. Current affairs are used as a vehicle to help students learn advanced vocabulary, idiomatic expressions, complex sentence structures, news writing styles and formal stylistic register. Materials include texts and videos selected from news media worldwide to improve students\u2019 language proficiency for sophisticated communications on a wide range of topics.", + "professors": [ + "Jingjing Ao" + ], + "codes": [ + "CHNS 167" + ], + "flags": [], + "distributions": [ + "L5" + ], + "credits": 1, + "requirements": "After CHNS 153, or 157, or 159,\u00a0 or equivalent.", + "term": "202501", + "colsem": false, + "fysem": false, + "sysem": false + } +] \ No newline at end of file