diff --git a/.env.development b/.env.development index 91179686b9..19be702663 100644 --- a/.env.development +++ b/.env.development @@ -4,7 +4,7 @@ # VITE_API_URL=https://360Api.gordon.edu/ # @TRAIN - VITE_API_URL=https://360ApiTrain.gordon.edu/ + VITE_API_URL=https://360ApiSP.gordon.edu/ # @LOCALHOST # VITE_API_URL=http://localhost:51626/ diff --git a/.env.production b/.env.production index adf7e4f84f..b238b31c1d 100644 --- a/.env.production +++ b/.env.production @@ -5,4 +5,4 @@ VITE_ANALYTICS_ID=G-2FE78G0CBN # @TRAIN -VITE_API_URL=https://360ApiTrain.gordon.edu/ +VITE_API_URL=https://360ApiSP.gordon.edu/ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 955dd7afc1..53adc0a727 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,6 +8,7 @@ on: branches: - develop - master + - senior-project jobs: build: @@ -59,3 +60,10 @@ jobs: with: name: build-prod path: dist + + - name: Upload senior-project build artifacts for deployment + if: ${{ github.ref == 'refs/heads/senior-project' }} + uses: actions/upload-artifact@v3 + with: + name: build-senior-project + path: dist diff --git a/package-lock.json b/package-lock.json index 41f94bf8bd..8f2767f7b7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,6 @@ "@mui/material": "^5.14.10", "@mui/x-date-pickers": "^5.0.20", "add-to-calendar-button": "^2.2.9", - "caniuse-lite": "^1.0.30001283", "chart.js": "^2.9.4", "cropperjs": "^1.5.13", "date-fns": "^2.30.0", @@ -114,11 +113,12 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.5.tgz", - "integrity": "sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", + "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", "dependencies": { - "@babel/highlight": "^7.22.5" + "@babel/highlight": "^7.23.4", + "chalk": "^2.4.2" }, "engines": { "node": ">=6.9.0" @@ -189,11 +189,11 @@ } }, "node_modules/@babel/generator": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.5.tgz", - "integrity": "sha512-+lcUbnTRhd0jOewtFSedLyiPsD5tswKkbgcezOqqWFUVNEwoUTlpPOBmvhG7OXWLR4jMdv0czPGH5XbflnD1EA==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.5.tgz", + "integrity": "sha512-BPssCHrBD+0YrxviOa3QzpqwhNIXKEtOa2jQrm4FlmkC2apYgRnQcmPWiGZDlGxiNtltnUFolMe8497Esry+jA==", "dependencies": { - "@babel/types": "^7.22.5", + "@babel/types": "^7.23.5", "@jridgewell/gen-mapping": "^0.3.2", "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" @@ -301,20 +301,20 @@ } }, "node_modules/@babel/helper-environment-visitor": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz", - "integrity": "sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-function-name": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz", - "integrity": "sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", "dependencies": { - "@babel/template": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" }, "engines": { "node": ">=6.9.0" @@ -451,9 +451,9 @@ } }, "node_modules/@babel/helper-split-export-declaration": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.5.tgz", - "integrity": "sha512-thqK5QFghPKWLhAV321lxF95yCg2K3Ob5yw+M3VHWfdia0IkPXUtoLH8x/6Fh486QUvzhb8YOWHChTVen2/PoQ==", + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", "dependencies": { "@babel/types": "^7.22.5" }, @@ -462,17 +462,17 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", - "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", + "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz", - "integrity": "sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", "engines": { "node": ">=6.9.0" } @@ -514,12 +514,12 @@ } }, "node_modules/@babel/highlight": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.5.tgz", - "integrity": "sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", + "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", "dependencies": { - "@babel/helper-validator-identifier": "^7.22.5", - "chalk": "^2.0.0", + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", "js-tokens": "^4.0.0" }, "engines": { @@ -527,9 +527,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.5.tgz", - "integrity": "sha512-DFZMC9LJUG9PLOclRC32G63UXwzqS2koQC8dkx+PLdmt1xSePYpbT/NbsrJy8Q/muXz7o/h/d4A7Fuyixm559Q==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.5.tgz", + "integrity": "sha512-hOOqoiNXrmGdFbhgCzu6GiURxUgM27Xwd/aPuu8RfHEZPBzL1Z54okAHAQjXfcQNwvrlkAmAp4SlRTZ45vlthQ==", "bin": { "parser": "bin/babel-parser.js" }, @@ -2079,31 +2079,31 @@ } }, "node_modules/@babel/template": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.5.tgz", - "integrity": "sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", "dependencies": { - "@babel/code-frame": "^7.22.5", - "@babel/parser": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.5.tgz", - "integrity": "sha512-7DuIjPgERaNo6r+PZwItpjCZEa5vyw4eJGufeLxrPdBXBoLcCJCIasvK6pK/9DVNrLZTLFhUGqaC6X/PA007TQ==", - "dependencies": { - "@babel/code-frame": "^7.22.5", - "@babel/generator": "^7.22.5", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.5.tgz", + "integrity": "sha512-czx7Xy5a6sapWWRx61m1Ke1Ra4vczu1mCTtJam5zRTBOonfdJ+S/B6HYmGYu3fJtr8GGET3si6IhgWVBhJ/m8w==", + "dependencies": { + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.5", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.5", - "@babel/parser": "^7.22.5", - "@babel/types": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.5", + "@babel/types": "^7.23.5", "debug": "^4.1.0", "globals": "^11.1.0" }, @@ -2112,12 +2112,12 @@ } }, "node_modules/@babel/types": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.5.tgz", - "integrity": "sha512-zo3MIHGOkPOfoRXitsgHLjEXmlDaD/5KU1Uzuc9GNiZPhSqVxVRtxuPaSBZDsYZ9qV88AjtMtWW7ww98loJ9KA==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.5.tgz", + "integrity": "sha512-ON5kSOJwVO6xXVRTvOI0eOnWe7VdUcIpsovGo9U/Br4Ie4UVFQTboO2cYnDhAGU6Fp+UxSiT+pMft0SMHfuq6w==", "dependencies": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.5", + "@babel/helper-string-parser": "^7.23.4", + "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" }, "engines": { @@ -2425,10 +2425,250 @@ "node": ">=16" } }, + "node_modules/@esbuild/android-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/@esbuild/linux-x64": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.19.tgz", - "integrity": "sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", "cpu": [ "x64" ], @@ -2441,6 +2681,102 @@ "node": ">=12" } }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -4139,9 +4475,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001505", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001505.tgz", - "integrity": "sha512-jaAOR5zVtxHfL0NjZyflVTtXm3D3J9P15zSJ7HmQF8dSKGA6tqzQq+0ZI3xkjyQj46I4/M0K2GbMpcAFOcbr3A==", + "version": "1.0.30001566", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001566.tgz", + "integrity": "sha512-ggIhCsTxmITBAMmK8yZjEhCO5/47jKXPu6Dha/wuCS4JePVL+3uiDEBuhu2aIoT+bqTOR8L76Ip1ARL9xYsEJA==", "funding": [ { "type": "opencollective", @@ -4764,9 +5100,9 @@ } }, "node_modules/esbuild": { - "version": "0.17.19", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.19.tgz", - "integrity": "sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==", + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", "dev": true, "hasInstallScript": true, "bin": { @@ -4776,28 +5112,28 @@ "node": ">=12" }, "optionalDependencies": { - "@esbuild/android-arm": "0.17.19", - "@esbuild/android-arm64": "0.17.19", - "@esbuild/android-x64": "0.17.19", - "@esbuild/darwin-arm64": "0.17.19", - "@esbuild/darwin-x64": "0.17.19", - "@esbuild/freebsd-arm64": "0.17.19", - "@esbuild/freebsd-x64": "0.17.19", - "@esbuild/linux-arm": "0.17.19", - "@esbuild/linux-arm64": "0.17.19", - "@esbuild/linux-ia32": "0.17.19", - "@esbuild/linux-loong64": "0.17.19", - "@esbuild/linux-mips64el": "0.17.19", - "@esbuild/linux-ppc64": "0.17.19", - "@esbuild/linux-riscv64": "0.17.19", - "@esbuild/linux-s390x": "0.17.19", - "@esbuild/linux-x64": "0.17.19", - "@esbuild/netbsd-x64": "0.17.19", - "@esbuild/openbsd-x64": "0.17.19", - "@esbuild/sunos-x64": "0.17.19", - "@esbuild/win32-arm64": "0.17.19", - "@esbuild/win32-ia32": "0.17.19", - "@esbuild/win32-x64": "0.17.19" + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" } }, "node_modules/escalade": { @@ -8518,9 +8854,9 @@ } }, "node_modules/rollup": { - "version": "3.25.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.25.1.tgz", - "integrity": "sha512-tywOR+rwIt5m2ZAWSe5AIJcTat8vGlnPFAv15ycCrw33t6iFsXZ6mzHVFh2psSjxQPmI+xgzMZZizUAukBI4aQ==", + "version": "3.29.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz", + "integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==", "dev": true, "bin": { "rollup": "dist/bin/rollup" @@ -9644,14 +9980,14 @@ } }, "node_modules/vite": { - "version": "4.3.9", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.3.9.tgz", - "integrity": "sha512-qsTNZjO9NoJNW7KnOrgYwczm0WctJ8m/yqYAMAK9Lxt4SoySUfS5S8ia9K7JHpa3KEeMfyF8LoJ3c5NeBJy6pg==", + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.2.tgz", + "integrity": "sha512-tBCZBNSBbHQkaGyhGCDUGqeo2ph8Fstyp6FMSvTtsXeZSPpSMGlviAOav2hxVTqFcx8Hj/twtWKsMJXNY0xI8w==", "dev": true, "dependencies": { - "esbuild": "^0.17.5", - "postcss": "^8.4.23", - "rollup": "^3.21.0" + "esbuild": "^0.18.10", + "postcss": "^8.4.27", + "rollup": "^3.27.1" }, "bin": { "vite": "bin/vite.js" @@ -9659,12 +9995,16 @@ "engines": { "node": "^14.18.0 || >=16.0.0" }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, "optionalDependencies": { "fsevents": "~2.3.2" }, "peerDependencies": { "@types/node": ">= 14", "less": "*", + "lightningcss": "^1.21.0", "sass": "*", "stylus": "*", "sugarss": "*", @@ -9677,6 +10017,9 @@ "less": { "optional": true }, + "lightningcss": { + "optional": true + }, "sass": { "optional": true }, diff --git a/package.json b/package.json index cb7e4501e6..5a9a885901 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,6 @@ "@mui/material": "^5.14.10", "@mui/x-date-pickers": "^5.0.20", "add-to-calendar-button": "^2.2.9", - "caniuse-lite": "^1.0.30001283", "chart.js": "^2.9.4", "cropperjs": "^1.5.13", "date-fns": "^2.30.0", diff --git a/src/components/Nav/components/NavLinks/index.jsx b/src/components/Nav/components/NavLinks/index.jsx index 1dfd4c6479..68795774f6 100644 --- a/src/components/Nav/components/NavLinks/index.jsx +++ b/src/components/Nav/components/NavLinks/index.jsx @@ -148,6 +148,15 @@ const GordonNavLinks = ({ onLinkClick }) => { /> ); + const housingLotteryButton = ( + + ); + const helpButton = ( { {linksButton} {helpButton} {aboutButton} + {housingLotteryButton} {paletteOptionsButton} {feedbackButton} {adminButton} diff --git a/src/routes.jsx b/src/routes.jsx index 205a885a66..1f87f6ba3a 100644 --- a/src/routes.jsx +++ b/src/routes.jsx @@ -1,4 +1,5 @@ import About from './views/About'; +import HousingLottery from './views/HousingLottery'; import Admin from './views/Admin'; import ApartmentApp from './views/ApartmentApp'; import BannerSubmission from './views/BannerSubmission'; @@ -33,6 +34,16 @@ const routes = [ path: '/about', element: , }, + { + name: 'HousingLottery', + path: '/housingLottery', + element: , + }, + // { + // name: 'Wellness', + // path: '/wellness', + // element: , + // }, { name: 'Apartment Application', path: '/ApartApp', diff --git a/src/services/housing.ts b/src/services/housing.ts index 674c4b7547..daa1d40fbf 100644 --- a/src/services/housing.ts +++ b/src/services/housing.ts @@ -61,6 +61,16 @@ const getApartmentSelectionDate = async (): Promise => { }; const getApartmentHalls = (): Promise => http.get('housing/halls/apartments'); +const getTraditionalHalls = (): Promise => http.get('housing/halls/traditionals'); +const getAllPreference = (): Promise => + http.get('housing/housing_lottery/all_preference'); +const getAllPreferredHall = (): Promise => + http.get('housing/housing_lottery/all_preferred_hall'); +const getAllApplicant = (): Promise => + http.get('housing/housing_lottery/all_applicant'); +const getAllSchoolYear = (): Promise => + http.get('housing/housing_lottery/all_school_year'); +const getDueDate = (): Promise => http.get('housing/housing_lottery/get_due_date'); const getCurrentApplicationID = (username: string = ''): Promise => http.get(username ? `housing/apartment/${username}/` : 'housing/apartment/'); @@ -164,9 +174,26 @@ const getSubmittedApartmentApplications = async (): Promise => http.put(`housing/apartment/applications/${applicationID}/submit`); +const addApplicant = (applicantion_id: string, emailList: string[]) => + http.put(`housing/housing_lottery/roommate/${applicantion_id}`, emailList); + +const addHall = (applicantion_id: string, hallList: string[]) => + http.put(`housing/housing_lottery/hall/${applicantion_id}`, hallList); + +const addPreference = (applicantion_id: string, preferenceList: string[]) => + http.put(`housing/housing_lottery/preference/${applicantion_id}`, preferenceList); + +const addDueDate = (dueDate: string) => http.put(`housing/housing_lottery/due_date/`, dueDate); + const housingService = { getApartmentSelectionDate, getApartmentHalls, + getTraditionalHalls, + getAllPreference, + getAllPreferredHall, + getAllApplicant, + getAllSchoolYear, + getDueDate, getCurrentApplicationID, saveApartmentApplication, deleteApartmentApplication, @@ -174,6 +201,10 @@ const housingService = { getApartmentApplication, getSubmittedApartmentApplications, submitApplication, + addApplicant, + addHall, + addPreference, + addDueDate, }; export default housingService; diff --git a/src/views/HousingLottery/HousingLottery.module.scss b/src/views/HousingLottery/HousingLottery.module.scss new file mode 100644 index 0000000000..183e93cc0a --- /dev/null +++ b/src/views/HousingLottery/HousingLottery.module.scss @@ -0,0 +1,196 @@ +.submit_preference_button { + height: 45px; + margin-left: 10px; +} + +.add_applicant_button { + height: 45px; + margin-left: 10px; +} + +.addHall_button, +.submit_button { + height: 45px; + margin-left: 10px; +} + +.rankLabel { + text-align: left; + font-weight: bold; + margin-top: 10px; +} + +.hallSlots { + position: relative; + padding: 10px; +} + +.hallSlotWrapper { + margin-top: 10px; + margin-bottom: 10px; + margin-left: 10px; +} +.admin_card_header, +.preferences_card_header, +.applicants_card_header { + background-color: var(--mui-palette-primary-main); + color: var(--mui-palette-primary-contrastText); + + :global { + .MuiTypography-colorTextSecondary { + color: var(--mui-palette-primary-contrastText); + font-size: 1.25rem; + } + + .MuiCardHeader-subheader { + color: var(--mui-palette-primary-contrastText); + } + } +} + +//Imported from ApartmentApp.module.scss +.people_search_parent { + @extend %list_item; + + :global { + .gc360_quick_search_root { + border-color: var(--mui-palette-neutral-main); // override to black instead of white + } + + .gc360_quick_search_dropdown { + position: inherit; + } + } +} + +strong.over_emphasized { + text-transform: uppercase; + text-decoration: underline; +} + +.housing_card_header { + background-color: var(--mui-palette-primary-main); + color: var(--mui-palette-primary-contrastText); + + :global { + .MuiTypography-colorTextSecondary { + color: var(--mui-palette-primary-contrastText); + font-size: 1.25rem; + } + + .MuiCardHeader-subheader { + color: var(--mui-palette-primary-contrastText); + } + } +} + +.applicant_name { + display: flexbox; + width: 10em; + margin-left: 1em; +} + +.applicant_email { + display: flexbox; + width: 24em; + margin-left: 1em; +} + +.applicant_field { + padding-top: 1em; + padding-bottom: 1em; +} + +.housing_instructions { + & :global(.MuiTypography-root) { + padding: 0.25rem 0.75rem; + } + + & :global(.MuiTypography-paragraph) { + text-align: left; + } + + & :global(.MuiTableContainer-root) { + border: 1px solid var(--mui-palette-neutral-700); + border-radius: 4px; + } +} + +.list_item, +%list_item { + color: var(--mui-palette-neutral-700); +} + +.nested_list_item { + padding-left: 48px; + padding-right: 16px; +} + +.bordered_list_item { + @extend %list_item; + border: 1px solid var(--mui-palette-primary-main); +} + +.error { + color: var(--mui-palette-error-main); +} + +:global(.MuiGrid-item) .sticky_page_bottom_bar { + position: -webkit-sticky; + position: sticky; + bottom: 0px; + border-top: 8px; // The seems to be ignoring these, I will investigate this in the near future + margin-top: 8px; // The seems to be ignoring these + + :global(.MuiCard-root), + .sticky_page_bottom_bar { + box-shadow: 0px 0px 4px; + position: sticky; + } +} + +.housing_agreements_form_control { + text-align: left; + + &_label { + color: var(--mui-palette-neutral-700); + } + + // (CSSMODULES - fix mui overrides - disable no-descending-specificity) + /* stylelint-disable-next-line */ + & :global(.MuiDivider-root) { + margin-top: 0.5em; + margin-bottom: 0.5em; + } +} + +.email { + color: var(--mui-palette-primary-main); + display: flex; + + &:hover { + color: var(--mui-palette-secondary-main); + } + + &_icon { + margin-right: 0.75rem; + } +} + +.flexContainer { + display: flex; + flex-direction: column; + justify-content: flex-start; /* This will align children to the top */ +} + +.flexContainer > :first-child { + flex-shrink: 0; /* This ensures the 'Preferred Halls' doesn't shrink */ +} + +.flexContainer > :last-child { + flex-grow: 1; /* This allows the 'Preferences' to grow and fill available space */ +} + +.csvLink { + color: var(--mui-palette-primary-contrastText); +} diff --git a/src/views/HousingLottery/adminView/index.jsx b/src/views/HousingLottery/adminView/index.jsx new file mode 100644 index 0000000000..adeb094600 --- /dev/null +++ b/src/views/HousingLottery/adminView/index.jsx @@ -0,0 +1,255 @@ +import React, { useState, useEffect } from 'react'; +import { + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Button, + Grid, + Card, + CardHeader, + CardContent, + TextField, + Accordion, + AccordionSummary, + AccordionDetails, + Typography, + Link, +} from '@mui/material'; +import housingService from 'services/housing'; +import styles from '../HousingLottery.module.css'; +import { CSVLink } from 'react-csv'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import { setDate } from 'date-fns'; + +const AdminView = () => { + const [data, setData] = useState([]); + const [preference, setPreference] = useState([]); + const [preferredHall, setPreferredHall] = useState([]); + const [applicant, setApplicant] = useState([]); + const [schoolYear, setSchoolYear] = useState([]); + const [ApplicationID, setApplicationID] = useState([]); + const [dueDate, setDueDate] = useState(''); + + useEffect(() => { + housingService.getCurrentApplicationID().then(setApplicationID); + housingService.getAllPreference().then(setPreference); + housingService.getAllPreferredHall().then(setPreferredHall); + housingService.getAllApplicant().then(setApplicant); + housingService.getAllSchoolYear().then(setSchoolYear); + housingService.getDueDate().then(setDueDate); + }, []); + + const handleClick = async () => { + console.log(preference); + console.log(preferredHall); + console.log(applicant); + console.log(schoolYear); + }; + + const handleDateChange = (event) => { + let input = event.target.value.replace(/\D/g, ''); + + if (/^\d+$/.test(input)) { + if (input.length <= 2) { + setDueDate(input); + } else if (input.length <= 4) { + setDueDate(`${input.slice(0, 2)}/${input.slice(2)}`); + } else { + setDueDate(`${input.slice(0, 2)}/${input.slice(2, 4)}/${input.slice(4, 8)}`); + } + } + }; + + const submitDueDate = async () => { + await housingService.addDueDate(dueDate); + }; + + const combineData = (applicants, preferredHalls, preferences, schoolYears) => { + const normalizedData = {}; + + applicants.forEach(item => { + if (!normalizedData[item.ApplicationID]) { + normalizedData[item.ApplicationID] = { + applicants: [], + preferredHalls: [], + preferences: [], + year: null + }; + } + normalizedData[item.ApplicationID].applicants.push(item.Applicant1); + }); + + preferredHalls.forEach(item => { + if (normalizedData[item.ApplicationID]) { + normalizedData[item.ApplicationID].preferredHalls[item.Rank - 1] = item.HallName; + } + }); + + preferences.forEach(item => { + if (normalizedData[item.ApplicationID]) { + normalizedData[item.ApplicationID].preferences.push(item.Preference1); + } + }); + + schoolYears.forEach(item => { + if (normalizedData[item.ApplicationID]) { + normalizedData[item.ApplicationID].year = item.Year1; + } + }); + + return normalizedData; + }; + + const combinedData = combineData(applicant,preferredHall,preference,schoolYear); + + const csvData = Object.keys(combinedData).map((applicationId) => { + const appData = combinedData[applicationId]; + return { + 'Lottery Number': applicationId, + //Jan 28: Now we still treat ApplicationID is used as Lottery Number + 'Applicant 1`s Email': appData.applicants[0] || '', + 'Applicant 2`s Email': appData.applicants[1] || '', + 'Applicant 3`s Email': appData.applicants[2] || '', + 'Applicant 4`s Email': appData.applicants[3] || '', + 'Preferred Hall 1': appData.preferredHalls[0] || '', + 'Preferred Hall 2': appData.preferredHalls[1] || '', + 'Preferred Hall 3': appData.preferredHalls[2] || '', + 'Preferred Hall 4': appData.preferredHalls[3] || '', + 'Preferred Hall 5': appData.preferredHalls[4] || '', + 'Preferred Hall 6': appData.preferredHalls[5] || '', + 'Preference 1': appData.preferences[0] || '', + 'Preference 2': appData.preferences[1] || '', + 'Class Standing': appData.year || '', + }; + }); + + const csvHeaders = [ + {label:'Lottery Number',key:'Lottery Number'}, + {label:'Applicant 1`s Email',key:'Applicant 1`s Email'}, + {label:'Applicant 2`s Email',key:'Applicant 2`s Email'}, + {label:'Applicant 3`s Email',key:'Applicant 3`s Email'}, + {label:'Applicant 4`s Email',key:'Applicant 4`s Email'}, + {label:'Preferred Hall 1',key:'Preferred Hall 1'}, + {label:'Preferred Hall 2',key:'Preferred Hall 2'}, + {label:'Preferred Hall 3',key:'Preferred Hall 3'}, + {label:'Preferred Hall 4',key:'Preferred Hall 4'}, + {label:'Preferred Hall 5',key:'Preferred Hall 5'}, + {label:'Preferred Hall 6',key:'Preferred Hall 6'}, + {label:'Preference 1',key:'Preference 1'}, + {label:'Preference 2',key:'Preference 2'}, + {label:'Class Standing',key:'Class Standing'}, + ]; + + + return ( + + + + + + + + + + + + + + + Lottery Number + {Array.from({ length: 4 }, (_, i) => ( + + Applicant {i + 1}'s Email + + ))} + {Array.from({ length: 6 }, (_, i) => ( + Preferred Hall {i + 1} + ))} + {Array.from({ length: 2 }, (_, i) => ( + Preference {i + 1} + ))} + Class Standing + + + + {Object.keys(combinedData).map((ApplicationID, index) => { + const appData = combinedData[ApplicationID]; + return ( + + {ApplicationID} + + + } + aria-controls={`panel1a-content-${index}`} + id={`panel1a-header-${index}`} + > + View Applicants + + + {appData.applicants.map((email, idx) => ( + + {email} + + ))} + + + + {Array.from({ length: 6 }, (_, i) => ( + + {appData.preferredHalls && appData.preferredHalls.length > i ? appData.preferredHalls[i] : ''} + + ))} + {Array.from({ length: 2 }, (_, i) => ( + + {appData.preferences && appData.preferences.length > i ? appData.preferences[i] : ''} + + ))} + {appData.year || ''} + + ); + })} + + +
+
+ +
+
+ +
+
+ ); +}; + +export default AdminView; diff --git a/src/views/HousingLottery/index.jsx b/src/views/HousingLottery/index.jsx new file mode 100644 index 0000000000..05d16a5229 --- /dev/null +++ b/src/views/HousingLottery/index.jsx @@ -0,0 +1,77 @@ +import GordonLimitedAvailability from 'components/GordonLimitedAvailability'; +import GordonOffline from 'components/GordonOffline'; +import GordonUnauthenticated from 'components/GordonUnauthenticated'; +import GordonLoader from 'components/Loader'; +import { useAuthGroups, useUser } from 'hooks'; +import useNetworkStatus from 'hooks/useNetworkStatus'; +import StudentView from './studentView'; +import AdminView from './adminView'; + +//Imports for application period closed view +import { Card, CardContent, Grid, Button, Link } from '@mui/material'; +// eslint-disable-next-line no-unused-vars +import { useEffect, useState } from 'react'; // eslint disabled because it doesn't recognise type imports that ARE used in JSDoc comments +import { AuthGroup } from 'services/auth'; +import styles from './HousingLottery.module.css'; + +const HousingLottery = () => { + const [loading, setLoading] = useState(true); + const { profile, loading: loadingProfile } = useUser(); + const [isUserStudent, setIsUserStudent] = useState(false); + const isHousingAdmin = useAuthGroups(AuthGroup.HousingAdmin); + const isOnline = useNetworkStatus(); + + useEffect(() => { + const loadPage = async () => { + setLoading(true); + try { + setIsUserStudent(profile.PersonType.includes('stu')); + } catch { + setIsUserStudent(false); + } finally { + setLoading(false); + } + }; + + if (profile) { + loadPage(); + } else { + // Clear out component's person-specific state when authenticated becomes false + // (i.e. user logs out) so that it isn't preserved falsely for the next user + setIsUserStudent(false); + setLoading(false); + } + }, [profile]); + + if (loading || loadingProfile) { + return ; + } else if (!profile) { + // The user is not logged in + return ; + } else if (isOnline) { + if (isHousingAdmin) { + return ( +
+ +
+ ); + } else if (isUserStudent) { + return ( +
+ +
+ ); + } else { + return ( + + ); + } + } else { + return ; + } +}; + +export default HousingLottery; diff --git a/src/views/HousingLottery/studentView/Agreements/index.jsx b/src/views/HousingLottery/studentView/Agreements/index.jsx new file mode 100644 index 0000000000..fc58770803 --- /dev/null +++ b/src/views/HousingLottery/studentView/Agreements/index.jsx @@ -0,0 +1,154 @@ +import { + Card, + CardContent, + CardHeader, + Checkbox, + Divider, + FormControl, + FormControlLabel, + FormGroup, + FormHelperText, + FormLabel, +} from '@mui/material/'; +import { Fragment, useEffect, useState } from 'react'; +import housing from 'services/housing'; +// @TODO CSSMODULES - outside directory +import styles from '../../HousingLottery.module.css'; + +/** + * Renders a card displaying the housing application instructions + * + * @param {Object} props The React component props + * @param {boolean | string} props.deleting Status of delete operation + * @param {Function} props.onChange Callback for change of the checkbox state + * @returns {JSX.Element} JSX Element for the instructions card + */ +const Agreements = ({ deleting, onChange }) => { + const [checkboxes, setCheckboxes] = useState([]); + + const loadAgreements = async () => { + const currentYear = new Date().getFullYear(); + const selectionDate = await housing.getApartmentSelectionDate(); + + const newCheckboxes = [ + { + checked: false, + label: 'Each individual on the application has agreed to be on the application', + }, + { + checked: false, + label: + 'We understand that if someone on this application has not agreed to be on the application, our application will be disqualified', + }, + { + checked: false, + label: 'Each individual on this application appears ONLY on this application', + }, + { + checked: false, + label: + "We understand that if an individual on this application also appears on another group's application, our application could be disqualified", + }, + { + checked: false, + label: `Any individual on this application who has been on disciplinary probation at any point during the ${ + currentYear - 1 + }-${currentYear} academic year has been approved to apply by the Dean of Student Life or Assistant Dean of Student Life`, + }, + { + checked: false, + label: `Each individual on this application intends to register as a full-time student by ${selectionDate}`, + }, + { + checked: false, + label: `We understand that if any member of our application fails to register as a full-time student by ${selectionDate}, our application could be disqualified`, + }, + { + checked: false, + label: + 'We have read and understand all of the information and guidelines listed in the Instructions section', + }, + { + checked: false, + label: + 'We certify that all information provided on this application is accurate, to the best of our knowledge', + }, + { + checked: false, + label: + 'We agree to host other students in our apartment during the winter break recess, in accordance with the policy outlined in the student handbook', + }, + ]; + + setCheckboxes(newCheckboxes); + }; + + useEffect(() => loadAgreements(), []); + + useEffect(() => deleting === 'success' && loadAgreements(), [deleting]); + + const handleChange = (event, index) => { + setCheckboxes((prevCheckboxes) => { + let newCheckboxes = prevCheckboxes.map((prevCheckbox, j) => + j === index ? { ...prevCheckbox, checked: event.target.checked } : prevCheckbox, + ); + onChange(newCheckboxes.every((checkbox) => checkbox.checked)); + return newCheckboxes; + }); + }; + + const AgreementChecklistItem = ({ checked, index, label, onChange }) => ( + + onChange(event, index)} + name={'agreement-' + 1} + /> + } + label={label} + key={index} + /> + + + ); + + const error = checkboxes.some((checkbox) => !checkbox.checked); + + return ( + + + + + {error && ( + + Use the checkboxes next to each statement to indicate your group's understanding + and/or affirmative answer. Failure to complete this section will result in the + disqualification of the application. + + )} + + + {checkboxes.map((checkbox, index) => ( + handleChange(event, index)} + /> + ))} + + + You must read and complete this section before you will be allowed to submit this + application + + + + + ); +}; + +export default Agreements; diff --git a/src/views/HousingLottery/studentView/Instructions/index.jsx b/src/views/HousingLottery/studentView/Instructions/index.jsx new file mode 100644 index 0000000000..b9b96d17d1 --- /dev/null +++ b/src/views/HousingLottery/studentView/Instructions/index.jsx @@ -0,0 +1,126 @@ +import React from 'react'; +import { + Typography, + Accordion, + AccordionSummary, + AccordionDetails, + Link, +} from '@mui/material/'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import styles from '../../HousingLottery.module.css'; + +const Instructions = () => { + return ( + +
+ + Instructions (As easy as 1, 2, 3!) + + + 1. Review FAQs + + + 2. Access questionnaire + + + 3. Complete questionnaire by providing name(s), email(s), and housing preferences by Friday, Apr. 21 at noon. + + + + } + aria-controls="housing-lottery-faq-content" + id="housing-lottery-faq-header" + > + Housing Lottery FAQ + + + + 1. Who should complete the questionnaire?
+ Anyone who has not yet secured housing for the Fall 2022 semester should participate in the lottery by completing the questionnaire. +
+ + 2. What happens if I do not complete the questionnaire by the deadline?
+ The questionnaire will remain open and can be completed after the deadline. + All late submissions will be placed on a housing waitlist and given a placement in July, as fall semester availability is much clearer by that time. +
+ + 3. How can I learn more about my housing options?
+ Please visit our residence halls + for information about our residence halls. Room cost information can be obtained by visiting + room costs . +
+ + 4. How will I learn my room assignment?
+ You will receive your room assignment via email no later than Friday, Apr.28. +
+ + 5. Do I need to be registered for classes to complete the questionnaire?
+ No, you should complete the questionnaire even if you are not yet registered for classes. + However, registration will be taken into consideration during the assigning process. See question #13. +
+ + 6.What if I complete the questionnaire, and then want to change my preferences?
+ Simply complete the questionnaire again. You can complete the questionnaire as many times as needed prior to the deadline. + Only your most recent submission will be taken into consideration. +
+ + 7. Do I need a roommate to complete the questionnaire?
+ No, you do not need a roommate to complete the questionnaire and receive a housing assignment. If you participate in the housing lottery without a roommate, you will be assigned a roommate. That assignment will be communicated via email. +
+ + 8. I have a roommate (or two or three). Should we all complete separate questionnaires?
+ No. If you are applying as a group of two, three, or four, you should only complete one questionnaire. Each group will receive one lottery number, regardless of group size. The person listed first on the questionnaire will serve as the contact person for your group. +
+ + 9. Can I request to room with an incoming student?
+ Yes, just include “(incoming)” after listing their name on the questionnaire. +
+ + 10. What about suite mates?
+ There is a space to indicate suite mate requests on the questionnaire. +
+ + 11. Are single rooms available during the lottery?
+ No, single rooms were assigned during the Special Accommodations process. +
+ + 12. Can I request a specific room during the lottery?
+ Yes, there is a space on the form to indicate a specific room request. We will do our best to accommodate requests, but there is no guarantee. +
+ + 13. How will lottery order be determined?
+ Lottery order will be determined class-by-class (based on current academic standing), using a randomizer. + For example, if 40 current seniors register for the lottery, the names of those 40 seniors will be inputted, and assigning will begin with the lowest number. + Current juniors, current sophomores, and current freshman will follow, in that order. + Important note: the aforementioned process assumes that a student is registered for Fall 2023 classes and is not on chapel probation. + All students, regardless of class, who are not registered for classes by the beginning of the assignment process will be grouped together, given lottery numbers, and assigned after the current freshman class. + If one member of a group is not registered for classes, the entire group will be considered as unregistered. Students on chapel probation will be included with the unregistered group. +
+ + 14. I am a sophomore, and my roommate is a senior. With which class will our questionnaire be reviewed?
+ In situations where group members have different academic standings, the group will be considered to have the standing of the member with the highest standing. + To use the example from the question, a sophomore + senior group would be considered to have senior standing. +
+ + 15. Where can I learn my class standing?
+ You can visit 360.gordon.edu to learn your class standing. +
+ + 16. What if I want housing other than what I receive?
+ It is our goal to accommodate every preference, but we are unfortunately unable to do so. + If you want to request a different location after receiving your assignment, you can email Housing on or after July 1 to request that. +
+ + 17. How can I receive confirmation that my questionnaire has been received?
+ If you would like confirmation that your questionnaire has been received, you will need to select the “Send me an email receipt of my responses” box at the bottom of the questionnaire. + No other confirmation will be provided. +
+
+
+
+ ); +}; + +export default Instructions; diff --git a/src/views/HousingLottery/studentView/PreferenceBox/index.jsx b/src/views/HousingLottery/studentView/PreferenceBox/index.jsx new file mode 100644 index 0000000000..93bd1fe9ea --- /dev/null +++ b/src/views/HousingLottery/studentView/PreferenceBox/index.jsx @@ -0,0 +1,115 @@ +import { useEffect, useState } from 'react'; +import { + Table, + TableBody, + TableContainer, + Typography, + FormControl, + FormControlLabel, + Input, + Button, + RadioGroup, + Card, + CardContent, + CardHeader, + Radio, + Grid, +} from '@mui/material'; +import housingService from 'services/housing'; +import housing from 'services/housing'; +import styles from '../../HousingLottery.module.css'; + +const Preference = ({ onPreferenceChange }) => { + const [preferences, setPreferences] = useState(['', '']); // Store preferences as an array + const [morningOrNight, setMorningOrNight] = useState(''); // Store the selected morning or night + const [loudOrQuiet, setLoudOrQuiet] = useState(''); // Store the selected loud or quiet + + useEffect(() => { + // Check for stored preferences in localStorage + const storedPreferences = localStorage.getItem('userPreferences'); + if (storedPreferences) { + const { morningOrNight, loudOrQuiet } = JSON.parse(storedPreferences); + setMorningOrNight(morningOrNight || ''); + setLoudOrQuiet(loudOrQuiet || ''); + } + }, []); + + const handleMorningOrNightChange = (event) => { + const newMorningOrNight = event.target.value; + let newList = [...preferences]; + newList[0] = newMorningOrNight; + setMorningOrNight(newMorningOrNight); + setPreferences(newList); + onPreferenceChange(newList); + }; + + const handleLoudOrQuietChange = (event) => { + const newLoudOrQuiet = event.target.value; + let newList = [...preferences]; + newList[1] = newLoudOrQuiet; + setLoudOrQuiet(newLoudOrQuiet); + setPreferences(newList); + onPreferenceChange(newList); + }; + + useEffect(() => { + // Save preferences to local storage + const storedPreferences = JSON.stringify({ morningOrNight, loudOrQuiet }); + localStorage.setItem('userPreferences', storedPreferences); + }, [morningOrNight, loudOrQuiet]); + + useEffect(() => { + // Check if both morningOrNight and loudOrQuiet are empty, clear localStorage + if (!morningOrNight && !loudOrQuiet) { + localStorage.removeItem('userPreferences'); + } + }, [morningOrNight, loudOrQuiet]); + + useEffect(() => { + // Clear selected radio buttons when the page is refreshed + if (!localStorage.getItem('userPreferences')) { + setMorningOrNight(''); + setLoudOrQuiet(''); + } + }, []); + + return ( + + + + + +
+ + + } label="Night Owl" /> + } label="Morning Bird" /> + +
+ +
+ + + } label="Quiet" /> + } label="Loud" /> + +
+
+ +
+
+
+ ); +}; + +export default Preference; diff --git a/src/views/HousingLottery/studentView/PreferredHall/HallSlotComponent/index.jsx b/src/views/HousingLottery/studentView/PreferredHall/HallSlotComponent/index.jsx new file mode 100644 index 0000000000..b179432839 --- /dev/null +++ b/src/views/HousingLottery/studentView/PreferredHall/HallSlotComponent/index.jsx @@ -0,0 +1,52 @@ +import { Grid } from '@mui/material'; +import { useState } from 'react'; +import { HallSearchField } from 'views/PeopleSearch/components/SearchFieldList/components/SearchField'; +import GordonSnackbar from 'components/Snackbar'; + +/** + * + * @param {number} rank + * @returns + */ +const HallSlot = ({ rank, hallList, preferredHallList, updatePreferredHallList }) => { + const [hall, setHall] = useState(''); + const [snackbar, setSnackbar] = useState({ message: '', severity: null, open: false }); + + const selectPreferredHall = (event) => { + if (preferredHallList.length > 0) { + for (let i = 0; i < preferredHallList.length; i++) { + if (event.target.value == preferredHallList[i] && event.target.value != '') { + setSnackbar({ + message: 'You have already selected this hall.', + severity: 'error', + open: true, + }); + return; + } + } + } + setHall(event.target.value); + updatePreferredHallList(rank, event.target.value); + }; + + return ( + + selectPreferredHall(event)} + options={hallList} + select + size={200} + /> + setSnackbar((s) => ({ ...s, open: false }))} + /> + + ); +}; + +export default HallSlot; diff --git a/src/views/HousingLottery/studentView/PreferredHall/index.jsx b/src/views/HousingLottery/studentView/PreferredHall/index.jsx new file mode 100644 index 0000000000..2c217ade1b --- /dev/null +++ b/src/views/HousingLottery/studentView/PreferredHall/index.jsx @@ -0,0 +1,125 @@ +import { Button, Card, CardContent, CardHeader, Grid } from '@mui/material'; +import { useState, useEffect } from 'react'; +import HallSlot from './HallSlotComponent'; +import housingService from 'services/housing'; +import styles from '../../HousingLottery.module.css'; +import GordonSnackbar from 'components/Snackbar'; + +const PreferredHallsCard = ({ setPreferredHallResult }) => { + const [hallList, setHallList] = useState([]); + const [preferredHallList, setPreferredHallList] = useState(['', '', '', '', '', '']); + const [snackbar, setSnackbar] = useState({ message: '', severity: null, open: false }); + const searchHallTitle =
Preferred Halls
; + + useEffect(() => { + housingService.getTraditionalHalls().then(setHallList); + }, []); + + function updatePreferredHallList(rank, hall) { + let newList = [...preferredHallList]; + newList[rank - 1] = hall; + setPreferredHallList(newList); + setPreferredHallResult(newList); + } + + return ( + + + + + +
Rank
+ + + + 1 + + + + + + + + 2 + + + + + + + + 3 + + + + + + + + 4 + + + + + + + + 5 + + + + + + + + 6 + + + + + + +
+
+
+ setSnackbar((s) => ({ ...s, open: false }))} + /> +
+ ); +}; + +export default PreferredHallsCard; diff --git a/src/views/HousingLottery/studentView/StudentApplicants/ApplicantFields/index.jsx b/src/views/HousingLottery/studentView/StudentApplicants/ApplicantFields/index.jsx new file mode 100644 index 0000000000..cfcfb9dc99 --- /dev/null +++ b/src/views/HousingLottery/studentView/StudentApplicants/ApplicantFields/index.jsx @@ -0,0 +1,52 @@ +import { TextField } from '@mui/material'; +import styles from '../../../HousingLottery.module.css'; + +const ApplicantFields = ({ applicant, onApplicantChange, index }) => { + const handleFieldChange = (field, value) => { + onApplicantChange(index, { ...applicant, [field]: value }); + }; + + return ( + <> + handleFieldChange('firstName', e.target.value)} + value={applicant.firstName} + fullWidth + required + className={styles.applicant_name} + helperText={'*Required'} + /> + handleFieldChange('lastName', e.target.value)} + value={applicant.lastName} + fullWidth + required + className={styles.applicant_name} + helperText={'*Required'} + /> + handleFieldChange('email', e.target.value)} + value={applicant.email} + required + error={applicant.email !== '' && !applicant.email.endsWith('@gordon.edu')} + helperText={applicant.email === '' || applicant.email.endsWith('@gordon.edu') ? '*Required' : 'Not a Valid Gordon Email'} + className={styles.applicant_email} + /> + + ); +}; + +export default ApplicantFields; + diff --git a/src/views/HousingLottery/studentView/StudentApplicants/index.jsx b/src/views/HousingLottery/studentView/StudentApplicants/index.jsx new file mode 100644 index 0000000000..5acd5783c5 --- /dev/null +++ b/src/views/HousingLottery/studentView/StudentApplicants/index.jsx @@ -0,0 +1,65 @@ +import { Button, Card, CardContent, CardHeader, Divider, Grid, TextField } from '@mui/material'; +import { useState } from 'react'; +import ApplicantFields from './ApplicantFields'; +import styles from '../../HousingLottery.module.css'; +import { useEffect } from 'react'; +import user from '../../../../services/user'; + +const StudentApplicants = ({ setStudentApplicantResult }) => { + const [applicants, setApplicants] = useState([]); + const [emails, setEmails] = useState([]); + + useEffect(() => { + const loadCurrentUser = async () => { + try { + const profile = await user.getProfileInfo(); + const initialApplicants = [ + { firstName: profile.FirstName, lastName: profile.LastName, email: profile.Email }, + { firstName: '', lastName: '', email: '' }, + { firstName: '', lastName: '', email: '' }, + { firstName: '', lastName: '', email: '' }, + ]; + setApplicants(initialApplicants); + setEmails(initialApplicants.map((a) => a.email)); + } catch (error) { + console.error('Error fetching user data', error); + } + }; + loadCurrentUser(); + }, []); + + const handleApplicantChange = (index, updatedApplicant) => { + const newApplicants = [...applicants]; + newApplicants[index] = updatedApplicant; + setApplicants(newApplicants); + + const newEmails = [...emails]; + newEmails[index] = updatedApplicant.email; + setEmails(newEmails); + setStudentApplicantResult(newEmails); + }; + + return ( + + + + + {applicants.map((applicant, index) => ( + +
+ +
+ {index < applicants.length - 1 && } +
+ ))} +
+
+
+ ); +}; + +export default StudentApplicants; diff --git a/src/views/HousingLottery/studentView/index.jsx b/src/views/HousingLottery/studentView/index.jsx new file mode 100644 index 0000000000..59622c4bf1 --- /dev/null +++ b/src/views/HousingLottery/studentView/index.jsx @@ -0,0 +1,107 @@ +import { useEffect, useState } from 'react'; +import { Button, Grid } from '@mui/material'; +import PreferredHall from './PreferredHall'; +import StudentApplicants from './StudentApplicants/index.jsx'; +import Preference from './PreferenceBox'; +import Agreements from './Agreements'; +import Instructions from './Instructions'; +import housingService from 'services/housing'; +import styles from '../HousingLottery.module.css'; +import { nanoid } from 'nanoid'; +import GordonSnackbar from 'components/Snackbar'; +import user from '../../../services/user'; + +const StudentView = () => { + const [email, setEmail] = useState(''); + const [studentApplicantResult, setStudentApplicantResult] = useState([]); + useEffect(async () => { + const profile = await user.getProfileInfo(); + setEmail(profile.Email); + setStudentApplicantResult([email]); + }, [email]); + + const [preferredHallResult, setPreferredHallResult] = useState([]); + const [preferenceResult, setPreferenceResult] = useState([]); + const [snackbar, setSnackbar] = useState({ message: '', severity: null, open: false }); + const [areAllAgreementsChecked, setAreAllAgreementsChecked] = useState(false); + console.log('Preferred Hall Result:', preferredHallResult); + console.log('Student Applicant Result:', studentApplicantResult); + console.log('Preference Result:', preferenceResult); + + const [dueDate, setDueDate] = useState(''); + useEffect(() => { + housingService.getDueDate().then(setDueDate); + }, []); + + const handleAgreementsChange = (allChecked) => { + const agreementData = [allChecked]; + console.log('Agreement Data:', agreementData); + setAreAllAgreementsChecked(allChecked); + }; + + const handlePreferenceChange = (newPreferences) => { + setPreferenceResult(newPreferences); + console.log('Preference Data:', newPreferences); + }; + + const handleClick = async () => { + try { + let application_id = nanoid(8), + timeTarget = new Date(dueDate + ' 11:59:59 PM').getTime(), + timeNow = new Date().getTime(); + if (timeNow > timeTarget) { + application_id = 'zzz' + timeNow; + } + console.log('application_id ' + application_id); + await housingService.addApplicant(application_id, studentApplicantResult); + await housingService.addHall(application_id, preferredHallResult); + await housingService.addPreference(application_id, preferenceResult); + } catch { + setSnackbar({ + message: 'Application fail to submit. Please check your information or contact CTS.', + severity: 'error', + open: true, + }); + } + }; + + return ( + + + + + + + + + + + + + + + + + + + + + + setSnackbar((s) => ({ ...s, open: false }))} + /> + + ); +}; + +export default StudentView; diff --git a/src/views/PeopleSearch/components/SearchFieldList/components/SearchField/index.tsx b/src/views/PeopleSearch/components/SearchFieldList/components/SearchField/index.tsx index 3d059f4ddf..e385168dd8 100644 --- a/src/views/PeopleSearch/components/SearchFieldList/components/SearchField/index.tsx +++ b/src/views/PeopleSearch/components/SearchFieldList/components/SearchField/index.tsx @@ -37,6 +37,12 @@ const defaultMenuItem = ( ); +const HalldefaultMenuItem = ( + + None + +); + const mapOptionsToMenuItems = (options: string[] | SelectOption[]) => options.map((option) => typeof option === 'string' ? ( @@ -88,4 +94,42 @@ const SearchField = ({ ); }; +export const HallSearchField = ({ + name, + value, + updateValue, + Icon, + disabled = false, + select = false, + options = undefined, +}: SearchFieldProps) => { + const isLargeScreen = useMediaQuery('(min-width: 600px)'); + + return ( + + {isLargeScreen && Icon && ( + + + + )} + + + {select && options && [HalldefaultMenuItem, mapOptionsToMenuItems(options)]} + + + + ); +}; + export default SearchField; diff --git a/src/views/RecIM/views/Home/index.jsx b/src/views/RecIM/views/Home/index.jsx index 1b71b941a8..0f5c73e205 100644 --- a/src/views/RecIM/views/Home/index.jsx +++ b/src/views/RecIM/views/Home/index.jsx @@ -334,8 +334,8 @@ const Home = () => { ); - let headerAlert = ( - participant?.Status === 'Pending' &&