diff --git a/.env.example b/.env.example index 02cde089..a2a581ef 100644 --- a/.env.example +++ b/.env.example @@ -7,5 +7,8 @@ VITE_APP_SEMESTER= VITE_APP_SITE_TITLE= VITE_APP_BACKEND_URL= +VITE_APP_SENTRY_DSN=https://01f0882e6aa029a125426e4ad32e6c18@o553498.ingest.us.sentry.io/4507775325437952 +VITE_APP_SENTRY_TRACING=0 + # To upload source maps to Sentry (Optional) -SENTRY_AUTH_TOKEN= +APP_SENTRY_AUTH_TOKEN= diff --git a/.github/workflows/release_version.yaml b/.github/workflows/release_version.yaml new file mode 100644 index 00000000..28713564 --- /dev/null +++ b/.github/workflows/release_version.yaml @@ -0,0 +1,50 @@ +name: Check Package Version + +on: + pull_request: + branches: + - main + +jobs: + check_version: + runs-on: ubuntu-latest + + steps: + - name: Checkout PR branch code + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + node-version: '20' + + - name: Get PR package version + id: pr_version + run: | + PR_VERSION=$(jq -r .version < package.json) + echo "::set-output name=pr_version::$PR_VERSION" + + - name: Fetch and checkout the main branch + run: | + git fetch origin main + git checkout origin/main + + - name: Compare PR versions + run: | + PR_VERSION=${{ steps.pr_version.outputs.pr_version }} + MAIN_VERSION=$(jq -r .version < package.json) + echo "Main branch version: $MAIN_VERSION" + echo "PR version: $PR_VERSION" + + version_greater() { + printf '%s\n%s' "$1" "$2" | sort -V | head -n1 | grep -q "$2" + } + + if version_greater "$PR_VERSION" "$MAIN_VERSION"; then + echo "Version is correct!" + else + echo "Error: PR version ($PR_VERSION) is not greater than main version ($MAIN_VERSION)." + exit 1 + fi diff --git a/.github/workflows/sentry.yaml b/.github/workflows/sentry.yaml new file mode 100644 index 00000000..e4d4f409 --- /dev/null +++ b/.github/workflows/sentry.yaml @@ -0,0 +1,53 @@ +name: Sentry Release + +on: + push: + branches: + - main + - develop + +jobs: + release: + runs-on: ubuntu-latest + environment: + name: sentry + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + node-version: '20' + + - name: Install dependencies + run: yarn install + + - name: Build project + run: yarn build + + - name: Determine release version (default to commit hash) + id: get_version + run: | + VERSION=$(git rev-parse --short HEAD) + + # Override version if on main branch to use package.json version + if [ "${{ github.ref }}" == "refs/heads/main" ]; then + VERSION=$(jq -r .version < package.json) + fi + + echo "Version to be released: $VERSION" + echo "::set-output name=release_version::$VERSION" + + - name: Create and Finalize Sentry Release + uses: getsentry/action-release@v1 + with: + environment: ${{ github.ref == 'refs/heads/main' && 'production' || 'staging' }} + version: ${{ steps.get_version.outputs.release_version }} + sourcemaps: ./build + finalize: true + env: + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + SENTRY_ORG: ${{ secrets.SENTRY_ORG }} + SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }} diff --git a/package-lock.json b/package-lock.json index 2b51fc13..764ae29a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -54,6 +54,7 @@ "react-sortablejs": "^6.1.4", "react-spinners": "^0.14.1", "react-toastify": "^9.1.1", + "socket.io-client": "^4.8.0", "sortablejs": "^1.15.2", "swr": "^2.2.5", "tailwind-merge": "^2.2.0", @@ -1890,10 +1891,166 @@ "node": ">= 8.0.0" } }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.25.0.tgz", + "integrity": "sha512-CC/ZqFZwlAIbU1wUPisHyV/XRc5RydFrNLtgl3dGYskdwPZdt4HERtKm50a/+DtTlKeCq9IXFEWR+P6blwjqBA==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.25.0.tgz", + "integrity": "sha512-/Y76tmLGUJqVBXXCfVS8Q8FJqYGhgH4wl4qTA24E9v/IJM0XvJCGQVSW1QZ4J+VURO9h8YCa28sTFacZXwK7Rg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.25.0.tgz", + "integrity": "sha512-YVT6L3UrKTlC0FpCZd0MGA7NVdp7YNaEqkENbWQ7AOVOqd/7VzyHpgIpc1mIaxRAo1ZsJRH45fq8j4N63I/vvg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.25.0.tgz", + "integrity": "sha512-ZRL+gexs3+ZmmWmGKEU43Bdn67kWnMeWXLFhcVv5Un8FQcx38yulHBA7XR2+KQdYIOtD0yZDWBCudmfj6lQJoA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.25.0.tgz", + "integrity": "sha512-xpEIXhiP27EAylEpreCozozsxWQ2TJbOLSivGfXhU4G1TBVEYtUPi2pOZBnvGXHyOdLAUUhPnJzH3ah5cqF01g==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.25.0.tgz", + "integrity": "sha512-sC5FsmZGlJv5dOcURrsnIK7ngc3Kirnx3as2XU9uER+zjfyqIjdcMVgzy4cOawhsssqzoAX19qmxgJ8a14Qrqw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.25.0.tgz", + "integrity": "sha512-uD/dbLSs1BEPzg564TpRAQ/YvTnCds2XxyOndAO8nJhaQcqQGFgv/DAVko/ZHap3boCvxnzYMa3mTkV/B/3SWA==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.25.0.tgz", + "integrity": "sha512-ZVt/XkrDlQWegDWrwyC3l0OfAF7yeJUF4fq5RMS07YM72BlSfn2fQQ6lPyBNjt+YbczMguPiJoCfaQC2dnflpQ==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.25.0.tgz", + "integrity": "sha512-qboZ+T0gHAW2kkSDPHxu7quaFaaBlynODXpBVnPxUgvWYaE84xgCKAPEYE+fSMd3Zv5PyFZR+L0tCdYCMAtG0A==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.25.0.tgz", + "integrity": "sha512-ndWTSEmAaKr88dBuogGH2NZaxe7u2rDoArsejNslugHZ+r44NfWiwjzizVS1nUOHo+n1Z6qV3X60rqE/HlISgw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.25.0.tgz", + "integrity": "sha512-BVSQvVa2v5hKwJSy6X7W1fjDex6yZnNKy3Kx1JGimccHft6HV0THTwNtC2zawtNXKUu+S5CjXslilYdKBAadzA==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.25.0.tgz", + "integrity": "sha512-G4hTREQrIdeV0PE2JruzI+vXdRnaK1pg64hemHq2v5fhv8C7WjVaeXc9P5i4Q5UC06d/L+zA0mszYIKl+wY8oA==", + "cpu": [ + "riscv64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.25.0.tgz", + "integrity": "sha512-9T/w0kQ+upxdkFL9zPVB6zy9vWW1deA3g8IauJxojN4bnz5FwSsUAD034KpXIVX5j5p/rn6XqumBMxfRkcHapQ==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ] + }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.20.0.tgz", - "integrity": "sha512-y+eoL2I3iphUg9tN9GB6ku1FA8kOfmF4oUEWhztDJ4KXJy1agk/9+pejOuZkNFhRwHAOxMsBPLbXPd6mJiCwew==", + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.25.0.tgz", + "integrity": "sha512-ThcnU0EcMDn+J4B9LD++OgBYxZusuA7iemIIiz5yzEcFg04VZFzdFjuwPdlURmYPZw+fgVrFzj4CA64jSTG4Ig==", "cpu": [ "x64" ], @@ -1903,9 +2060,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.20.0.tgz", - "integrity": "sha512-hM3nhW40kBNYUkZb/r9k2FKK+/MnKglX7UYd4ZUy5DJs8/sMsIbqWK2piZtVGE3kcXVNj3B2IrUYROJMMCikNg==", + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.25.0.tgz", + "integrity": "sha512-zx71aY2oQxGxAT1JShfhNG79PnjYhMC6voAjzpu/xmMjDnKNf6Nl/xv7YaB/9SIa9jDYf8RBPWEnjcdlhlv1rQ==", "cpu": [ "x64" ], @@ -1914,6 +2071,42 @@ "linux" ] }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.25.0.tgz", + "integrity": "sha512-JT8tcjNocMs4CylWY/CxVLnv8e1lE7ff1fi6kbGocWwxDq9pj30IJ28Peb+Y8yiPNSF28oad42ApJB8oUkwGww==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.25.0.tgz", + "integrity": "sha512-dRLjLsO3dNOfSN6tjyVlG+Msm4IiZnGkuZ7G5NmpzwF9oOc582FZG05+UdfTbz5Jd4buK/wMb6UeHFhG18+OEg==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.25.0.tgz", + "integrity": "sha512-/RqrIFtLB926frMhZD0a5oDa4eFIbyNEwLLloMTEjmqfwZWXywwVVOVmwTsuyhC9HKkVEZcOOi+KV4U9wmOdlg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@sentry-internal/browser-utils": { "version": "8.33.1", "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-8.33.1.tgz", @@ -2174,6 +2367,12 @@ "node": ">= 14" } }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, "node_modules/@tailwindcss/forms": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.7.tgz", @@ -2374,9 +2573,9 @@ } }, "node_modules/@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==" + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==" }, "node_modules/@types/jest": { "version": "27.5.2", @@ -3745,6 +3944,28 @@ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" }, + "node_modules/engine.io-client": { + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.1.tgz", + "integrity": "sha512-aYuoak7I+R83M/BBPIOs2to51BmFIpC1wZe6zZzMrT2llVsHy5cvcmdsJgP2Qz6smHu+sD9oexiSUAVd8OfBPw==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/es-abstract": { "version": "1.23.3", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz", @@ -6526,11 +6747,11 @@ } }, "node_modules/rollup": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.20.0.tgz", - "integrity": "sha512-6rbWBChcnSGzIlXeIdNIZTopKYad8ZG8ajhl78lGRLsI2rX8IkaotQhVas2Ma+GPxJav19wrSzvRvuiv0YKzWw==", + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.25.0.tgz", + "integrity": "sha512-uVbClXmR6wvx5R1M3Od4utyLUxrmOcEm3pAtMphn73Apq19PDtHpgZoEvqH2YnnaNUuvKmg2DgRd2Sqv+odyqg==", "dependencies": { - "@types/estree": "1.0.5" + "@types/estree": "1.0.6" }, "bin": { "rollup": "dist/bin/rollup" @@ -6540,22 +6761,24 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.20.0", - "@rollup/rollup-android-arm64": "4.20.0", - "@rollup/rollup-darwin-arm64": "4.20.0", - "@rollup/rollup-darwin-x64": "4.20.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.20.0", - "@rollup/rollup-linux-arm-musleabihf": "4.20.0", - "@rollup/rollup-linux-arm64-gnu": "4.20.0", - "@rollup/rollup-linux-arm64-musl": "4.20.0", - "@rollup/rollup-linux-powerpc64le-gnu": "4.20.0", - "@rollup/rollup-linux-riscv64-gnu": "4.20.0", - "@rollup/rollup-linux-s390x-gnu": "4.20.0", - "@rollup/rollup-linux-x64-gnu": "4.20.0", - "@rollup/rollup-linux-x64-musl": "4.20.0", - "@rollup/rollup-win32-arm64-msvc": "4.20.0", - "@rollup/rollup-win32-ia32-msvc": "4.20.0", - "@rollup/rollup-win32-x64-msvc": "4.20.0", + "@rollup/rollup-android-arm-eabi": "4.25.0", + "@rollup/rollup-android-arm64": "4.25.0", + "@rollup/rollup-darwin-arm64": "4.25.0", + "@rollup/rollup-darwin-x64": "4.25.0", + "@rollup/rollup-freebsd-arm64": "4.25.0", + "@rollup/rollup-freebsd-x64": "4.25.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.25.0", + "@rollup/rollup-linux-arm-musleabihf": "4.25.0", + "@rollup/rollup-linux-arm64-gnu": "4.25.0", + "@rollup/rollup-linux-arm64-musl": "4.25.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.25.0", + "@rollup/rollup-linux-riscv64-gnu": "4.25.0", + "@rollup/rollup-linux-s390x-gnu": "4.25.0", + "@rollup/rollup-linux-x64-gnu": "4.25.0", + "@rollup/rollup-linux-x64-musl": "4.25.0", + "@rollup/rollup-win32-arm64-msvc": "4.25.0", + "@rollup/rollup-win32-ia32-msvc": "4.25.0", + "@rollup/rollup-win32-x64-msvc": "4.25.0", "fsevents": "~2.3.2" } }, @@ -6709,6 +6932,34 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/socket.io-client": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.0.tgz", + "integrity": "sha512-C0jdhD5yQahMws9alf/yvtsMGTaIDBnZ8Rb5HU56svyq0l5LIrGzIDZZD5pHQlmzxLuU91Gz+VpQMKgCTNYtkw==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.2", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/sortablejs": { "version": "1.15.2", "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.2.tgz", @@ -7734,6 +7985,35 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "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/xmlhttprequest-ssl": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.1.tgz", + "integrity": "sha512-ptjR8YSJIXoA3Mbv5po7RtSYHO6mZr8s7i5VGmEk7QY2pQWyT1o0N+W1gKbOyJPUCGXGnuw0wqe8f0L6Y0ny7g==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/package.json b/package.json index ceb6a0ac..9e018e20 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "react-sortablejs": "^6.1.4", "react-spinners": "^0.14.1", "react-toastify": "^9.1.1", + "socket.io-client": "^4.8.0", "sortablejs": "^1.15.2", "swr": "^2.2.5", "tailwind-merge": "^2.2.0", diff --git a/src/App.tsx b/src/App.tsx index d6eb828f..21d26eb7 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -44,17 +44,17 @@ const App = () => { // Enable Error Tracking, Performance Monitoring and Session Replay Sentry.init({ environment: Number(import.meta.env.VITE_APP_PROD) ? "production" : "development", - dsn: "https://01f0882e6aa029a125426e4ad32e6c18@o553498.ingest.us.sentry.io/4507775325437952", + dsn: import.meta.env.VITE_APP_SENTRY_DSN, integrations: [ - Sentry.browserTracingIntegration(), - Sentry.replayIntegration(), - Sentry.reactRouterV6BrowserTracingIntegration({ + import.meta.env.VITE_APP_SENTRY_TRACING ? Sentry.browserTracingIntegration() : null, + import.meta.env.VITE_APP_SENTRY_TRACING ? Sentry.replayIntegration() : null, + import.meta.env.VITE_APP_SENTRY_TRACING ? Sentry.reactRouterV6BrowserTracingIntegration({ useEffect: React.useEffect, useLocation, useNavigationType, createRoutesFromChildren, matchRoutes, - }), + }) : null, ], // Performance monitoring @@ -79,6 +79,7 @@ const App = () => {
+
} diff --git a/src/api/socket.ts b/src/api/socket.ts new file mode 100644 index 00000000..d516de0d --- /dev/null +++ b/src/api/socket.ts @@ -0,0 +1,70 @@ +import { io, Socket } from "socket.io-client"; +import backendApi from "./backend"; + +const SOCKET_URL = (import.meta.env.VITE_APP_PROD == 0 ? 'ws://' : 'wss://') + backendApi.BACKEND_URL.split('//')[1]; + +class OptionalSocket { + private socket: Socket | null; + + constructor() { + this.socket = null; + } + + set(socket: Socket) { + this.socket = socket; + } + + unset() { + this.socket = null; + } + + use(callback: (socket: Socket) => T): T { + if (!this.socket) { + throw new Error('Socket is not connected'); + } + return callback(this.socket); + } +} + +class SessionsSocket { + private url: string; + private socket: OptionalSocket; + + constructor(url: string) { + this.url = url; + this.socket = new OptionalSocket(); + } + + connect() { + const newSocket = io(this.url, { + auth: { + token: 'dummy', // TODO: Replace with actual federated authentication token + } + }); + this.socket.set(newSocket); + } + + disconnect() { + this.socket.use(socket => socket.disconnect()); + this.socket.unset(); + } + + on(event: string, callback: (...args: any[]) => void) { + this.socket.use(socket => socket.on(event, callback)); + } + + off(event: string, callback?: (...args: any[]) => void) { + this.socket.use(socket => socket.off(event, callback)); + } + + emit(event: string, ...args: any[]) { + this.socket.use(socket => socket.emit(event, args)); + } +} + +const sessionsSocket = new SessionsSocket(SOCKET_URL); + +export { + sessionsSocket, + SOCKET_URL, +}; diff --git a/src/components/planner/sidebar/CoursesController.tsx b/src/components/planner/sidebar/CoursesController.tsx index 5f3c87f5..6b9322a0 100644 --- a/src/components/planner/sidebar/CoursesController.tsx +++ b/src/components/planner/sidebar/CoursesController.tsx @@ -1,14 +1,36 @@ import { useContext } from 'react' import ClassSelector from './CoursesController/ClassSelector' import CourseContext from '../../../contexts/CourseContext' +import MultipleOptionsContext from '../../../contexts/MultipleOptionsContext' +import { TrashIcon } from "@heroicons/react/24/outline" import { NoMajorSelectedSVG } from '../../svgs' import { Button } from '../../ui/button' const CoursesController = () => { const { pickedCourses, setUcsModalOpen } = useContext(CourseContext); + const { multipleOptions, selectedOption, setMultipleOptions } = useContext(MultipleOptionsContext); const noCoursesPicked = pickedCourses.length === 0; + + const eraseClasses = () => { + const currentOption = multipleOptions[selectedOption]; + + const updatedCourseOptions = currentOption.course_options.map(courseOption => ({ + ...courseOption, + picked_class_id: null, + locked: false, + })); + + const updatedMultipleOptions = [...multipleOptions]; + updatedMultipleOptions[selectedOption] = { + ...currentOption, + course_options: updatedCourseOptions, + }; + + setMultipleOptions(updatedMultipleOptions); + }; + return (
{noCoursesPicked ? ( @@ -28,6 +50,20 @@ const CoursesController = () => { )) )} + + {!noCoursesPicked && ( +
+ +
+ )} +
) } diff --git a/src/components/planner/sidebar/CoursesController/ClassItem.tsx b/src/components/planner/sidebar/CoursesController/ClassItem.tsx index 13769b51..b8bc0332 100644 --- a/src/components/planner/sidebar/CoursesController/ClassItem.tsx +++ b/src/components/planner/sidebar/CoursesController/ClassItem.tsx @@ -1,8 +1,8 @@ -import { useContext, useMemo } from 'react' +import { useContext } from 'react' import { ClassInfo } from '../../../../@types/index' import { DropdownMenuCheckboxItem } from '../../../ui/dropdown-menu' import { ExclamationTriangleIcon } from '@heroicons/react/20/solid' -import { conflictsSeverity, schedulesConflict } from '../../../../utils' +import { classesConflictSeverity } from '../../../../utils' import MultipleOptionsContext from '../../../../contexts/MultipleOptionsContext' import CourseContext from '../../../../contexts/CourseContext' @@ -10,7 +10,6 @@ import CourseContext from '../../../../contexts/CourseContext' type Props = { course_id: number, classInfo: ClassInfo - conflict?: boolean onSelect?: () => void onMouseEnter?: () => void onMouseLeave?: () => void @@ -29,26 +28,26 @@ const ClassItem = ({ course_id, classInfo, onSelect, onMouseEnter, onMouseLeave onSelect(); } - const conflict: number = useMemo(() => { - const classes: ClassInfo[] = [] + const conflictSeverity = () => { + const chosenCourses = multipleOptions[selectedOption].course_options.filter( + (option) => option.course_id !== course_id + ); - for (const course_option of multipleOptions[selectedOption].course_options) { - if (course_option.picked_class_id && course_option.course_id !== course_id) { - const pickedCourse = pickedCourses.find(co => co.id === course_option.course_id); - // retrieve class with the picked class id of the course option - const pickedClass = pickedCourse.classes.find(c => c.id === course_option.picked_class_id); + const otherClasses = []; + chosenCourses.forEach((option) => { + const courseInfo = pickedCourses.find((course) => course.id === option.course_id); + const pickedClass = courseInfo.classes.find((classInfo) => classInfo.id === option.picked_class_id); - classes.push(pickedClass); - } + if (pickedClass) otherClasses.push(pickedClass); + }); + + let maxSeverity = 0; + for (const otherClass of otherClasses) { + maxSeverity = Math.max(maxSeverity, classesConflictSeverity(classInfo, otherClass)); } - for (const pickedClass of classes) - for (const slot1 of pickedClass.slots) - for (const slot2 of classInfo.slots) - if (schedulesConflict(slot1, slot2)) { - return conflictsSeverity(slot1, slot2); - } - }, []); + return maxSeverity; + } return ( (
{slot.lesson_type} - {/* {convertWeekday(slot.day)} */} - {/* {getLessonBoxTime(slot)} */} {slot.location} {slot.professors.map((professor) => professor.acronym).join(', ')} @@ -72,7 +69,7 @@ const ClassItem = ({ course_id, classInfo, onSelect, onMouseEnter, onMouseLeave ))}
-
) } diff --git a/src/components/planner/sidebar/CoursesController/ClassSelectorDropdownController.tsx b/src/components/planner/sidebar/CoursesController/ClassSelectorDropdownController.tsx index a5d42203..22893f8e 100644 --- a/src/components/planner/sidebar/CoursesController/ClassSelectorDropdownController.tsx +++ b/src/components/planner/sidebar/CoursesController/ClassSelectorDropdownController.tsx @@ -4,7 +4,7 @@ import { ClassInfo, CourseInfo, CourseOption, ProfessorInfo } from "../../../../ import StorageAPI from "../../../../api/storage"; import CourseContext from "../../../../contexts/CourseContext"; import MultipleOptionsContext from "../../../../contexts/MultipleOptionsContext"; -import { getAllPickedSlots, schedulesConflict, teacherIdsFromCourseInfo, uniqueTeachersFromCourseInfo } from "../../../../utils"; +import { teacherIdsFromCourseInfo, uniqueTeachersFromCourseInfo } from "../../../../utils"; import { Desert } from "../../../svgs"; import { DropdownMenuGroup, DropdownMenuItem, DropdownMenuPortal, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger } from "../../../ui/dropdown-menu"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "../../../ui/tabs"; @@ -157,13 +157,6 @@ const ClassSelectorDropdownController = ({ setMultipleOptions(newMultipleOptions) } - // Checks if any of the selected classes have time conflicts with the classInfo - // This is used to display a warning icon in each class of the dropdown in case of conflicts - const timesCollideWithSelected = (classInfo: ClassInfo) => { - const pickedSlots = getAllPickedSlots(pickedCourses, multipleOptions[selectedOption]) - return pickedSlots.some((slot) => classInfo.slots.some((currentSlot) => schedulesConflict(slot, currentSlot))) - } - return <>
{course.classes === null ? ( @@ -217,16 +210,17 @@ const ClassSelectorDropdownController = ({ {!course.classes || course.classes.length === 0 ? : <> + {selectedClassId && ( deleteOption()}> Remover Seleção + )} {course.classes && getOptions().map((classInfo) => ( { setSelectedClassId(classInfo.id) setPreview(null) @@ -260,7 +254,6 @@ const ClassSelectorDropdownController = ({ key={`schedule-${classInfo.name}`} course_id={course.id} classInfo={classInfo} - conflict={timesCollideWithSelected(classInfo)} onSelect={() => { setSelectedClassId(classInfo.id) setPreview(null) diff --git a/src/components/planner/sidebar/sessionController/course-picker/MajorSearchCombobox.tsx b/src/components/planner/sidebar/sessionController/course-picker/MajorSearchCombobox.tsx index 16cf8dd0..8bf0473f 100644 --- a/src/components/planner/sidebar/sessionController/course-picker/MajorSearchCombobox.tsx +++ b/src/components/planner/sidebar/sessionController/course-picker/MajorSearchCombobox.tsx @@ -71,9 +71,11 @@ const MajorSearchCombobox = ({ selectedMajor, setSelectedMajor }: Props) => { // handle that event and actually be scrollable with the mouse wheel onWheel={(e) => e.stopPropagation()} > + {selectedMajor && ( setSelectedMajor(null)}> Remover Seleção + )} {majors && majors.map((major) => ( { return (isMandatory(first) && isMandatory(second)) ? 2 : 1; } +const classesConflictSeverity = (first: ClassInfo, second: ClassInfo): number => { + let maxSeverity = 0; + + for (const slot of first.slots) { + for (const otherSlot of second.slots) { + if (schedulesConflict(slot, otherSlot)) { + maxSeverity = Math.max(maxSeverity, conflictsSeverity(slot, otherSlot)); + } + } + } + + return maxSeverity; +} + const schedulesConflict = (first: SlotInfo, second: SlotInfo) => { if (first.day !== second.day) return false @@ -395,5 +409,6 @@ export { uniqueTeachersFromCourseInfo, teacherIdsFromCourseInfo, scrollToTop, + classesConflictSeverity, plausible }