From 77d78b1fb5d8e8624cb227d0c5589189633f5980 Mon Sep 17 00:00:00 2001 From: Ericzhou-ez Date: Wed, 21 May 2025 09:46:08 -0700 Subject: [PATCH 1/4] feat: capstone import data front end and useCapstone context --- .eslintrc.js | 82 ----- package-lock.json | 193 ++++++++--- package.json | 2 + .../dashboard/capstone/configurations/page.js | 71 ++++ .../dashboard/{ => capstone}/rooms/page.js | 0 .../dashboard/{ => capstone}/students/page.js | 0 src/app/dashboard/import-data/file-drop.js | 127 ------- src/app/dashboard/import-data/file.css | 114 ------- src/app/dashboard/import-data/page.js | 87 ----- src/app/layout.js | 9 +- .../capstone/configurations/auditView.js | 200 +++++++++++ .../capstone/configurations/errorDisplay.js | 77 +++++ .../capstone/configurations/fileDropZone.js | 103 ++++++ .../capstone/configurations/importView.js | 229 +++++++++++++ .../capstone/configurations/roomDataTable.js | 215 ++++++++++++ .../configurations/studentDataTable.js | 316 ++++++++++++++++++ src/components/dashboard/layout/config.js | 185 +++++----- src/components/marketing/home/hero.js | 6 +- src/contexts/capstone-context-provider.tsx | 105 ++++++ src/contexts/capstone-context.ts | 42 +++ src/lib/parseCSV.ts | 169 ++++++++++ src/paths.js | 8 +- src/types/capstone-types.ts | 23 ++ 23 files changed, 1799 insertions(+), 564 deletions(-) delete mode 100644 .eslintrc.js create mode 100644 src/app/dashboard/capstone/configurations/page.js rename src/app/dashboard/{ => capstone}/rooms/page.js (100%) rename src/app/dashboard/{ => capstone}/students/page.js (100%) delete mode 100644 src/app/dashboard/import-data/file-drop.js delete mode 100644 src/app/dashboard/import-data/file.css delete mode 100644 src/app/dashboard/import-data/page.js create mode 100644 src/components/dashboard/capstone/configurations/auditView.js create mode 100644 src/components/dashboard/capstone/configurations/errorDisplay.js create mode 100644 src/components/dashboard/capstone/configurations/fileDropZone.js create mode 100644 src/components/dashboard/capstone/configurations/importView.js create mode 100644 src/components/dashboard/capstone/configurations/roomDataTable.js create mode 100644 src/components/dashboard/capstone/configurations/studentDataTable.js create mode 100644 src/contexts/capstone-context-provider.tsx create mode 100644 src/contexts/capstone-context.ts create mode 100644 src/lib/parseCSV.ts create mode 100644 src/types/capstone-types.ts diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index a8cce20..0000000 --- a/.eslintrc.js +++ /dev/null @@ -1,82 +0,0 @@ -const { resolve } = require('node:path'); - -const project = resolve(__dirname, 'jsconfig.json'); - -module.exports = { - root: true, - extends: [ - require.resolve('@vercel/style-guide/eslint/node'), - require.resolve('@vercel/style-guide/eslint/typescript'), - require.resolve('@vercel/style-guide/eslint/browser'), - require.resolve('@vercel/style-guide/eslint/react'), - require.resolve('@vercel/style-guide/eslint/next'), - ], - parser: '@typescript-eslint/parser', - parserOptions: { - project, - }, - settings: { - 'import/resolver': { - typescript: { - project, - }, - }, - }, - rules: { - '@typescript-eslint/restrict-template-expressions': [ - 'error', - { - allowNumber: true, - }, - ], - '@typescript-eslint/no-unused-vars': [ - 'error', - { - ignoreRestSiblings: true, - argsIgnorePattern: '^_', - varsIgnorePattern: '^_', - caughtErrorsIgnorePattern: '^_', - }, - ], - '@typescript-eslint/no-empty-interface': [ - 'error', - { - allowSingleExtends: true, - }, - ], - '@typescript-eslint/no-shadow': [ - 'error', - { - ignoreOnInitialization: true, - }, - ], - 'import/newline-after-import': 'error', - 'react/jsx-uses-react': 'error', - 'react/react-in-jsx-scope': 'error', - 'unicorn/filename-case': [ - 'error', - { - cases: { - kebabCase: true, // personal style - pascalCase: true, - }, - }, - ], - - // Deactivated - '@typescript-eslint/dot-notation': 'off', // paths are used with a dot notation - '@typescript-eslint/no-misused-promises': 'off', // onClick with async fails - '@typescript-eslint/no-non-null-assertion': 'off', // sometimes compiler is unable to detect - '@typescript-eslint/no-unnecessary-condition': 'off', // remove when no static data is used - '@typescript-eslint/prefer-nullish-coalescing': 'off', // sometimes we need to check for empty strings - '@typescript-eslint/require-await': 'off', // Server Actions require async flag always - 'import/no-default-export': 'off', // Next.js components must be exported as default - 'import/no-extraneous-dependencies': 'off', // conflict with sort-imports plugin - 'import/order': 'off', // using custom sort plugin - 'no-nested-ternary': 'off', // personal style - 'no-redeclare': 'off', // conflict with TypeScript function overloads - 'react/jsx-fragments': 'off', // personal style - 'react/prop-types': 'off', // TypeScript is used for type checking - '@next/next/no-img-element': 'off', // temporary disabled - }, -}; diff --git a/package-lock.json b/package-lock.json index 3112157..ebdc3ce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -53,6 +53,7 @@ "mapbox-gl": "3.6.0", "next": "14.2.5", "next-i18next": "15.3.1", + "papaparse": "^5.5.3", "react": "18.3.1", "react-dom": "18.3.1", "react-dropzone": "14.2.3", @@ -66,6 +67,7 @@ "sonner": "1.5.0", "stylis": "4.3.2", "stylis-plugin-rtl": "2.1.1", + "xlsx": "^0.18.5", "zod": "3.23.8" }, "devDependencies": { @@ -1414,14 +1416,13 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", - "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", - "license": "MIT", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", "dependencies": { - "@babel/helper-validator-identifier": "^7.25.9", + "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" + "picocolors": "^1.1.1" }, "engines": { "node": ">=6.9.0" @@ -1609,19 +1610,17 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", - "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", - "license": "MIT", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", - "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", - "license": "MIT", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", "engines": { "node": ">=6.9.0" } @@ -1637,26 +1636,24 @@ } }, "node_modules/@babel/helpers": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.9.tgz", - "integrity": "sha512-Mz/4+y8udxBKdmzt/UjPACs4G3j5SshJJEFFKxlCGPydG4JAHXxjWjAwjd09tf6oINvl1VfMJo+nB7H2YKQ0dA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.1.tgz", + "integrity": "sha512-FCvFTm0sWV8Fxhpp2McP5/W53GPllQ9QeQ7SiqGWjMf/LVG07lFa5+pgK05IRhVwtvafT22KF+ZSnM9I545CvQ==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/template": "^7.26.9", - "@babel/types": "^7.26.9" + "@babel/template": "^7.27.1", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.9.tgz", - "integrity": "sha512-81NWa1njQblgZbQHxWHpxxCzNsa3ZwvFqpUg7P+NNUU6f3UU2jBEg4OlF/J6rl8+PQGh1q6/zWScd001YwcA5A==", - "license": "MIT", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.2.tgz", + "integrity": "sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw==", "dependencies": { - "@babel/types": "^7.26.9" + "@babel/types": "^7.27.1" }, "bin": { "parser": "bin/babel-parser.js" @@ -1905,26 +1902,21 @@ } }, "node_modules/@babel/runtime": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.9.tgz", - "integrity": "sha512-aA63XwOkcl4xxQa3HjPMqOP6LiK0ZDv3mUPYEFXkpHbaFjtGggE1A61FjFzJnB+p7/oy2gA8E+rcBNl/zC1tMg==", - "license": "MIT", - "dependencies": { - "regenerator-runtime": "^0.14.0" - }, + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.1.tgz", + "integrity": "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/template": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz", - "integrity": "sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==", - "license": "MIT", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", "dependencies": { - "@babel/code-frame": "^7.26.2", - "@babel/parser": "^7.26.9", - "@babel/types": "^7.26.9" + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1949,13 +1941,12 @@ } }, "node_modules/@babel/types": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.9.tgz", - "integrity": "sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw==", - "license": "MIT", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.1.tgz", + "integrity": "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==", "dependencies": { - "@babel/helper-string-parser": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -7125,6 +7116,14 @@ "node": ">=0.4.0" } }, + "node_modules/adler-32": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz", + "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==", + "engines": { + "node": ">=0.8" + } + }, "node_modules/agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -7930,6 +7929,18 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/cfb": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", + "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==", + "dependencies": { + "adler-32": "~1.3.0", + "crc-32": "~1.2.0" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -8134,6 +8145,14 @@ "node": ">= 0.12.0" } }, + "node_modules/codepage": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz", + "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==", + "engines": { + "node": ">=0.8" + } + }, "node_modules/collect-v8-coverage": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", @@ -8267,6 +8286,17 @@ "node": ">=10" } }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/create-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", @@ -10901,6 +10931,14 @@ "node": ">=0.4.x" } }, + "node_modules/frac": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz", + "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==", + "engines": { + "node": ">=0.8" + } + }, "node_modules/framer-motion": { "version": "12.4.7", "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.4.7.tgz", @@ -15177,6 +15215,11 @@ "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", "license": "(MIT AND Zlib)" }, + "node_modules/papaparse": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.5.3.tgz", + "integrity": "sha512-5QvjGxYVjxO59MGU2lHVYpRWBBtKHnlIAcSe1uNFCkkptUh63NFRj0FJQm7nR67puEruUci/ZkjmEFrjCAyP4A==" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -15579,10 +15622,9 @@ "license": "MIT" }, "node_modules/prismjs": { - "version": "1.29.0", - "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz", - "integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==", - "license": "MIT", + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", + "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", "engines": { "node": ">=6" } @@ -16473,12 +16515,6 @@ "node": ">=6" } }, - "node_modules/regenerator-runtime": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", - "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", - "license": "MIT" - }, "node_modules/regexp-tree": { "version": "0.1.27", "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz", @@ -17366,6 +17402,17 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/ssf": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", + "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==", + "dependencies": { + "frac": "~1.1.2" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/stable-hash": { "version": "0.0.4", "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.4.tgz", @@ -18953,6 +19000,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/wmf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz", + "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/word": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz", + "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==", + "engines": { + "node": ">=0.8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -19113,6 +19176,26 @@ } } }, + "node_modules/xlsx": { + "version": "0.18.5", + "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", + "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==", + "dependencies": { + "adler-32": "~1.3.0", + "cfb": "~1.2.1", + "codepage": "~1.15.0", + "crc-32": "~1.2.1", + "ssf": "~0.11.2", + "wmf": "~1.0.1", + "word": "~0.3.0" + }, + "bin": { + "xlsx": "bin/xlsx.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/xml-name-validator": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", diff --git a/package.json b/package.json index 2f9dcf7..f4828a7 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ "mapbox-gl": "3.6.0", "next": "14.2.5", "next-i18next": "15.3.1", + "papaparse": "^5.5.3", "react": "18.3.1", "react-dom": "18.3.1", "react-dropzone": "14.2.3", @@ -73,6 +74,7 @@ "sonner": "1.5.0", "stylis": "4.3.2", "stylis-plugin-rtl": "2.1.1", + "xlsx": "^0.18.5", "zod": "3.23.8" }, "devDependencies": { diff --git a/src/app/dashboard/capstone/configurations/page.js b/src/app/dashboard/capstone/configurations/page.js new file mode 100644 index 0000000..fd0b926 --- /dev/null +++ b/src/app/dashboard/capstone/configurations/page.js @@ -0,0 +1,71 @@ +"use client"; + +import * as React from "react"; +import Box from "@mui/material/Box"; +import Button from "@mui/material/Button"; +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import { useCapstoneContext } from "@/contexts/capstone-context"; +import AuditView from "@/components/dashboard/capstone/configurations/auditView"; +import ImportView from "@/components/dashboard/capstone/configurations/importView"; +import { ArrowRight } from "@phosphor-icons/react"; + +export default function Page() { + const { isAuditView, setIsAuditView } = useCapstoneContext(); + + const handleBack = () => { + setIsAuditView(!isAuditView); + }; + + return ( + + + + + + Capstone Configurations + + + + + + +
+ +
+
+
+ ); +} + +const ConfigContent = () => { + const { isAuditView } = useCapstoneContext(); + + return ( +
+ {isAuditView ? : } +
+ ); +}; diff --git a/src/app/dashboard/rooms/page.js b/src/app/dashboard/capstone/rooms/page.js similarity index 100% rename from src/app/dashboard/rooms/page.js rename to src/app/dashboard/capstone/rooms/page.js diff --git a/src/app/dashboard/students/page.js b/src/app/dashboard/capstone/students/page.js similarity index 100% rename from src/app/dashboard/students/page.js rename to src/app/dashboard/capstone/students/page.js diff --git a/src/app/dashboard/import-data/file-drop.js b/src/app/dashboard/import-data/file-drop.js deleted file mode 100644 index 7c26073..0000000 --- a/src/app/dashboard/import-data/file-drop.js +++ /dev/null @@ -1,127 +0,0 @@ -"use client"; - -import "./file.css"; -import React, { useRef, useState } from "react"; -import Box from "@mui/material/Box"; -import PropTypes from "prop-types"; -import { UploadSimple as UploadSimpleIcon } from "@phosphor-icons/react/dist/ssr/UploadSimple"; -import { MicrosoftExcelLogo } from "@phosphor-icons/react/dist/ssr/MicrosoftExcelLogo"; -import { X } from "@phosphor-icons/react/dist/ssr/X"; -import Typography from "@mui/material/Typography"; -import { Modal9 } from "@/components/widgets/modals/modal-9"; -import Button from "@mui/material/Button"; - -export default function FileDrop({ title }) { - const onFileChange = (files) => { - // send to firebase - }; - - return ( -
- - {title} - - onFileChange(files)} /> -
- ); -} - -DropFileInput.propTypes = { - onFileChange: PropTypes.func, -}; - -function DropFileInput(props) { - const wrapperRef = useRef(null); - const [fileList, setFileList] = useState([]); - const [openModal, setOpenModal] = useState(false); - - const onDragEnter = () => wrapperRef.current.classList.add("dragover"); - const onDragLeave = () => wrapperRef.current.classList.remove("dragover"); - const onDrop = () => wrapperRef.current.classList.remove("dragover"); - - const isValidFileType = (file) => { - const validMimeTypes = [ - "text/csv", - "application/vnd.ms-excel", - "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", - ]; - return validMimeTypes.includes(file.type) || file.name.endsWith(".csv"); - }; - - const onFileDrop = (e) => { - const newFile = e.target.files[0]; - if (newFile) { - if (isValidFileType(newFile)) { - setFileList([newFile]); - props.onFileChange([newFile]); - } else { - setOpenModal(true); - } - } - }; - - const fileRemove = () => { - setFileList([]); - props.onFileChange([]); - }; - - const toggleModal = () => { - setOpenModal(!openModal); - }; - - return ( - <> -
-
- -

Drag & Drop your files here

-
- -
- - {fileList.length > 0 && ( -
-
-
- -
-

{fileList[0].name}

-
-
- -
-
- )} - - {openModal && } - {openModal && ( - - )} - - ); -} diff --git a/src/app/dashboard/import-data/file.css b/src/app/dashboard/import-data/file.css deleted file mode 100644 index b5ffc4f..0000000 --- a/src/app/dashboard/import-data/file.css +++ /dev/null @@ -1,114 +0,0 @@ -.box { - background-color: #ffffff; - padding: 30px; - border-radius: 20px; - - -webkit-box-shadow: rgba(0, 0, 0, 0.3) 0 2px 5px; - -moz-box-shadow: rgba(0, 0, 0, 0.3) 0 2px 5px; - box-shadow: rgba(0, 0, 0, 0.08) 0px 1px 8px; -} - -.header { - margin-bottom: 30px; - text-align: center; -} - -.drop-file-input { - position: relative; - width: 100%; - height: 200px; - border: 2px dashed rgb(116, 54, 232); - border-radius: 20px; - - display: flex; - align-items: center; - justify-content: center; - - background-color: #caceef; - opacity: 0.8; -} - -.drop-file-input input { - opacity: 0; - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - cursor: pointer; -} - -.drop-file-input:hover, -.drop-file-input.dragover { - opacity: 0.6; -} - -.drop-file-input__label { - text-align: center; -} - -.drop-file-preview { - margin-top: 30px; -} - -.icon-name { - display: flex; - justify-content: space-between; - align-items: center; - gap: 15px; -} - -.drop-file-preview p { - font-weight: 500; - font-size: 0.6rem; -} - -.drop-file-preview__title { - margin-bottom: 20px; -} - -.drop-file-preview__item { - position: relative; - display: flex; - margin-bottom: 5px; - background-color: #edefff; - padding: 15px; - border-radius: 12px; -} - -.drop-file-preview__item img { - width: 50px; - margin-right: 20px; -} - -.drop-file-preview__item__info { - display: flex; - flex-direction: column; - justify-content: space-between; -} - -.drop-file-preview__item__del { - background-color: #ffffff; - width: 20px; - height: 20px; - padding: 5px; - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - position: absolute; - right: 10px; - top: 50%; - transform: translateY(-50%); - cursor: pointer; - opacity: 0; - transition: opacity 0.3s ease; - - -webkit-box-shadow: rgba(0, 0, 0, 0.3) 0 2px 3px; - -moz-box-shadow: rgba(0, 0, 0, 0.3) 0 2px 3px; - box-shadow: rgba(0, 0, 0, 0.3) 0 2px 3px; -} - -.drop-file-preview__item:hover .drop-file-preview__item__del { - opacity: 1; -} diff --git a/src/app/dashboard/import-data/page.js b/src/app/dashboard/import-data/page.js deleted file mode 100644 index 31803d6..0000000 --- a/src/app/dashboard/import-data/page.js +++ /dev/null @@ -1,87 +0,0 @@ -"use client"; - -import * as React from "react"; -import Box from "@mui/material/Box"; -import Button from "@mui/material/Button"; -import Stack from "@mui/material/Stack"; -import Typography from "@mui/material/Typography"; -import Grid from "@mui/material/Unstable_Grid2"; -import { Users as UsersIcon } from "@phosphor-icons/react/dist/ssr/Users"; -import { Warning as WarningIcon } from "@phosphor-icons/react/dist/ssr/Warning"; -import Card from "@mui/material/Card"; - -import FileDrop from "./file-drop"; -import { Summary } from "@/components/dashboard/overview/summary"; -import { Modal8 } from "@/components/widgets/modals/modal-8"; -import { SummaryPending } from "@/components/dashboard/overview/summary-pending"; - -import Image from "next/image"; -const dataExampleDemo = "/assets/dataExampleDemo.png"; - -// export const metadata = { title: `Overview | Dashboard | ${config.site.name}` }; - -export default function Page() { - return ( - - - - - Import Data - -
- - - -
-
- - - - Data Example Demo - - - - Data Example Demo - - - - - - - - - - - - - - -
-
- ); -} diff --git a/src/app/layout.js b/src/app/layout.js index d8f8445..23e78e9 100644 --- a/src/app/layout.js +++ b/src/app/layout.js @@ -15,6 +15,7 @@ import { ThemeProvider } from "@/components/core/theme-provider/theme-provider"; import { Query, QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { Toaster } from "@/components/core/toaster"; import QueryProvider from "./query-provider"; +import { CapstoneProvider } from "@/contexts/capstone-context-provider"; export const metadata = { title: config.site.name, @@ -42,9 +43,11 @@ export default async function Layout({ children }) { - {children} - {/* */} - + + {children} + {/* */} + + diff --git a/src/components/dashboard/capstone/configurations/auditView.js b/src/components/dashboard/capstone/configurations/auditView.js new file mode 100644 index 0000000..9adca9b --- /dev/null +++ b/src/components/dashboard/capstone/configurations/auditView.js @@ -0,0 +1,200 @@ +"use client"; + +import React, { useState } from "react"; +import { + Box, + Button, + Card, + CardContent, + CardHeader, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + Grid, + IconButton, + Tab, + Tabs, + Tooltip, + Typography, + useMediaQuery, + useTheme, +} from "@mui/material"; +import { Question } from "@phosphor-icons/react"; + +import { useCapstoneContext } from "@/contexts/capstone-context"; +import RoomDataTable from "./roomDataTable"; +import StudentDataTable from "./studentDataTable"; +import { Stack } from "@mui/system"; + +export default function AuditView() { + const { importType, setImportType, roomData, studentData } = + useCapstoneContext(); + + const theme = useTheme(); + const isMonitor = useMediaQuery("(max-width:1600px)"); + const [helpOpen, setHelpOpen] = useState(null); + + const RoomCard = ( + + + setHelpOpen("room")}> + + + + } + /> + + + + + ); + + const StudentCard = ( + + + setHelpOpen("student")}> + + + + } + /> + + + + + ); + + /* ------------------------------ render ------------------------------ */ + return ( + + {isMonitor ? ( + <> + + setImportType(val)} + centered + sx={{ + mb: 3, + display: "flex", + justifySelf: { xs: "center", sm: "start" }, + }} + > + + Room Data  + {roomData.length ? `(${roomData.length})` : ""} + + } + value="room" + /> + + Student Data  + {studentData.length ? `(${studentData.length})` : ""} + + } + value="student" + /> + + + + + + {importType === "room" ? RoomCard : StudentCard} + + ) : ( + + + + + + + {RoomCard} + + + {StudentCard} + + + + )} + + {/* ---------------- Help dialogs ---------------- */} + setHelpOpen(null)} + maxWidth="sm" + fullWidth + > + Room Data Instructions + + + Use this table to manage room information: + +
    +
  • Teacher Name – full name of the teacher.
  • +
  • Room Number – e.g. A‑201 or 305.
  • +
  • Edit / delete rows as needed.
  • +
  • Click “Add Room” for a new entry.
  • +
+
+ + + +
+ + setHelpOpen(null)} + maxWidth="sm" + fullWidth + > + Student Data Instructions + + + Use this table to manage student information: + +
    +
  • Student Name / Email – both required.
  • +
  • Role – Presenter or Viewer.
  • +
  • Topic Description – only for Presenters.
  • +
  • Edit / delete rows as needed.
  • +
  • Click “Add Student” for a new entry.
  • +
+
+ + + +
+
+ ); +} + +const SendLinkButton = () => { + return ( + + ); +}; diff --git a/src/components/dashboard/capstone/configurations/errorDisplay.js b/src/components/dashboard/capstone/configurations/errorDisplay.js new file mode 100644 index 0000000..760c250 --- /dev/null +++ b/src/components/dashboard/capstone/configurations/errorDisplay.js @@ -0,0 +1,77 @@ +"use client"; + +import React from "react"; +import { + Alert, + AlertTitle, + Box, + Paper, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Typography, + useTheme, +} from "@mui/material"; +import { WarningCircle } from "@phosphor-icons/react"; // ❱❱ any icon you like + +/** + * @typedef {{ row?: number|string, column: string, message: string }} ValidationError + * @param {{ errors: ValidationError[] }} props + */ +export default function ErrorDisplay({ errors = [] }) { + if (!errors.length) return null; + + const theme = useTheme(); + + return ( + + {/* summary banner */} + }> + Import Errors + Please fix the following issues in your file … + + + {/* table of individual errors */} + + + + + Row + Column + Error + + + + {errors.map((err, idx) => ( + + {err.row ?? "—"} + {err.column} + + {err.message} + + + ))} + +
+
+
+ ); +} diff --git a/src/components/dashboard/capstone/configurations/fileDropZone.js b/src/components/dashboard/capstone/configurations/fileDropZone.js new file mode 100644 index 0000000..40ab0b7 --- /dev/null +++ b/src/components/dashboard/capstone/configurations/fileDropZone.js @@ -0,0 +1,103 @@ +"use client"; + +import React, { useCallback } from "react"; +import { useDropzone } from "react-dropzone"; +import { Box, Button, Typography, useTheme } from "@mui/material"; +import { UploadSimple } from "@phosphor-icons/react"; + +/** + * @param {{ onFileAccepted: (f:File)=>void, importType:'room'|'student' }} props + */ +export default function FileDropzone({ onFileAccepted, importType }) { + const theme = useTheme(); + + const onDrop = useCallback( + (files) => { + if (files?.length) onFileAccepted(files[0]); + }, + [onFileAccepted], + ); + + const { + getRootProps, + getInputProps, + isDragActive, + isDragAccept, + isDragReject, + } = useDropzone({ + onDrop, + accept: { + "text/csv": [".csv"], + "application/vnd.ms-excel": [".xls"], + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": [ + ".xlsx", + ], + }, + maxFiles: 1, + }); + + /* dynamic border colour */ + const borderColor = isDragReject + ? theme.palette.error.main + : isDragAccept + ? "rgba(0, 113, 227, 0.5)" + : isDragActive + ? theme.palette.primary.main + : theme.palette.divider; + + return ( + + + + + + + {importType === "room" ? "Upload Room Data" : "Upload Student Data"} + + + + Drag & drop your  + {importType === "room" ? "room" : "student"} data file here or click to + browse. + + + + Accepts CSV, XLS, XLSX formats + + + + + ); +} diff --git a/src/components/dashboard/capstone/configurations/importView.js b/src/components/dashboard/capstone/configurations/importView.js new file mode 100644 index 0000000..7f9dfa9 --- /dev/null +++ b/src/components/dashboard/capstone/configurations/importView.js @@ -0,0 +1,229 @@ +"use client"; + +import React, { useState } from "react"; +import { + Box, + Button, + Card, + CardContent, + CardHeader, + CircularProgress, + Tab, + Tabs, + Typography, +} from "@mui/material"; +import { toast } from "sonner"; + +import { useCapstoneContext } from "@/contexts/capstone-context"; +import { parseFile } from "@/lib/parseCSV"; +import FileDropzone from "./fileDropZone"; +import ErrorDisplay from "./errorDisplay"; + +export default function ImportView() { + const { + importType, // "room" | "student" + setImportType, + setRoomData, + setStudentData, + setIsAuditView, + roomData, + studentData, + } = useCapstoneContext(); + + const [loading, setLoading] = useState(false); + const [lastFileName, setLastFileName] = useState({ + room: "", + student: "", + }); + + const [tabErrors, setTabErrors] = useState({ + room: [], + student: [], + }); + + /* ───────────────────────────── upload ───────────────────────────── */ + + const handleFileAccepted = async (file) => { + setLoading(true); + + // store filename **and** clear old errors for this tab immediately + setLastFileName((p) => ({ ...p, [importType]: file.name })); + setTabErrors((p) => ({ ...p, [importType]: [] })); + + try { + const res = await parseFile(file, importType); // calls XLSX / Papa etc. + processResult(res, importType); + } catch (err) { + console.error(err); + toast.error("Failed to process the file"); + } finally { + setLoading(false); + } + }; + + const processResult = (res, type) => { + if (res.errors.length) { + setTabErrors((p) => ({ ...p, [type]: res.errors })); + toast.error(`Found ${res.errors.length} errors in your file`); + return; + } + + setTabErrors((p) => ({ ...p, [type]: [] })); + + if (res.data.length === 0) { + toast.error("The file contains no valid data"); + return; + } + + if (type === "room") { + setRoomData(res.data); + toast.success(`Imported ${res.data.length} room records`); + } else { + setStudentData(res.data); + toast.success(`Imported ${res.data.length} student records`); + } + + const hasRoom = type === "room" ? res.data.length > 0 : roomData.length > 0; + const hasStudent = + type === "student" ? res.data.length > 0 : studentData.length > 0; + if (hasRoom && hasStudent) setIsAuditView(true); + }; + + /* ──────────────────────────── helpers ──────────────────────────── */ + + const columnsHelp = { + room: "teacherName, roomNumber", + student: + "studentName, studentEmail, studentRole, topicDescription (optional for presenters)", + }; + + const recordCount = + importType === "room" ? roomData.length : studentData.length; + + /* ─────────────────────────────── UI ─────────────────────────────── */ + + return ( + + setImportType(v)} + sx={{ mb: 3 }} + > + + + + + + + {importType === "room" ? "Room Import" : "Student Import"} + + } + /> + + + {/* expected columns */} + + Your file should include the following columns: + + + + + {columnsHelp[importType]} + + + + + + {/* drop zone */} + + + {/* last uploaded filename */} + {lastFileName[importType] && ( + + Last uploaded: {lastFileName[importType]} + + )} + + {/* spinner */} + {loading && ( + + + + )} + + + + {tabErrors[importType].length > 0 && ( + + + + )} + + ); +} + +/* ───────────────────────── template download helpers ───────────────────── */ + +export function downloadRoomCsvTemplate() { + const header = ["teacherName", "roomNumber"]; + const csv = "\uFEFF" + header.join(",") + "\n"; + triggerDownload(csv, "room-data-template.csv"); +} + +export function downloadStudentCsvTemplate() { + const header = [ + "studentName", + "studentEmail", + "studentRole", + "topicDescription", + ]; + const csv = "\uFEFF" + header.join(",") + "\n"; + triggerDownload(csv, "student-data-template.csv"); +} + +function triggerDownload(csv, filename) { + const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" }); + const url = URL.createObjectURL(blob); + const a = Object.assign(document.createElement("a"), { + href: url, + download: filename, + }); + document.body.appendChild(a); + a.click(); + a.remove(); + URL.revokeObjectURL(url); +} diff --git a/src/components/dashboard/capstone/configurations/roomDataTable.js b/src/components/dashboard/capstone/configurations/roomDataTable.js new file mode 100644 index 0000000..fb691c1 --- /dev/null +++ b/src/components/dashboard/capstone/configurations/roomDataTable.js @@ -0,0 +1,215 @@ +"use client"; + +import React, { useState } from "react"; +import { + Box, + Button, + IconButton, + Paper, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + TextField, + Typography, +} from "@mui/material"; +import { useCapstoneContext } from "@/contexts/capstone-context"; +import { + PencilSimple as EditIcon, + Trash as TrashIcon, + X as XIcon, + Check as CheckIcon, + Plus as PlusIcon, +} from "@phosphor-icons/react"; + +export default function RoomDataTable() { + const { roomData, addRoomData, updateRoomData, deleteRoomData } = + useCapstoneContext(); + + const [editingId, setEditingId] = useState(null); + const [editData, setEditData] = useState(null); + + const [newRowOpen, setNewRowOpen] = useState(false); + const [newRoom, setNewRoom] = useState({ teacherName: "", roomNumber: "" }); + + const startEdit = (r) => { + setEditingId(r.id); + setEditData({ ...r }); + }; + + const cancelEdit = () => { + setEditingId(null); + setEditData(null); + }; + + const saveEdit = () => { + updateRoomData(editingId, editData); + cancelEdit(); + }; + + const handleField = (e, setter) => + setter((prev) => ({ ...prev, [e.target.name]: e.target.value })); + + const addRow = () => { + if (newRoom.teacherName && newRoom.roomNumber) { + addRoomData(newRoom); + setNewRoom({ teacherName: "", roomNumber: "" }); + setNewRowOpen(false); + } + }; + + if (roomData.length === 0 && !newRowOpen) { + return ( + + + No room data available + + + + ); + } + + return ( + + + + + + Teacher name + Room # + + Actions + + + + + + {roomData.map((row) => + editingId === row.id ? ( + + + handleField(e, setEditData)} + fullWidth + /> + + + handleField(e, setEditData)} + fullWidth + /> + + + + + + + + + + + ) : ( + + {row.teacherName} + {row.roomNumber} + + startEdit(row)} + sx={{ mr: 1 }} + > + + + + window.confirm("Delete this room?") && + deleteRoomData(row.id) + } + > + + + + + ), + )} + + {/* add‑new row -------------------------------------------------- */} + {newRowOpen && ( + + + handleField(e, setNewRoom)} + fullWidth + /> + + + handleField(e, setNewRoom)} + fullWidth + /> + + + + + + setNewRowOpen(false)}> + + + + + )} + +
+
+ + {/* footer add‑button -------------------------------------------------- */} + {!newRowOpen && ( + + + + )} +
+ ); +} diff --git a/src/components/dashboard/capstone/configurations/studentDataTable.js b/src/components/dashboard/capstone/configurations/studentDataTable.js new file mode 100644 index 0000000..40a8f02 --- /dev/null +++ b/src/components/dashboard/capstone/configurations/studentDataTable.js @@ -0,0 +1,316 @@ +"use client"; + +import React, { useState } from "react"; +import { + Box, + Button, + Chip, + IconButton, + MenuItem, + Paper, + Select, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + TextField, + Typography, +} from "@mui/material"; +import { + PencilSimple as EditIcon, + Trash as TrashIcon, + X as XIcon, + Check as CheckIcon, + Plus as PlusIcon, +} from "@phosphor-icons/react"; +import { useCapstoneContext } from "@/contexts/capstone-context"; + +export default function StudentDataTable() { + const { studentData, addStudentData, updateStudentData, deleteStudentData } = + useCapstoneContext(); + + const [editingId, setEditingId] = useState(null); + const [editRow, setEditRow] = useState(null); + + const [adding, setAdding] = useState(false); + const [newRow, setNewRow] = useState({ + studentName: "", + studentEmail: "", + studentRole: "viewer", + topicDescription: "", + }); + + const emailOK = (e) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(e); + + const startEdit = (r) => { + setEditingId(r.id); + setEditRow({ ...r }); + }; + const cancelEdit = () => { + setEditingId(null); + setEditRow(null); + }; + const saveEdit = () => { + if (!emailOK(editRow.studentEmail)) { + window.alert("Enter a valid email"); + return; + } + updateStudentData(editingId, editRow); + cancelEdit(); + }; + + const addRow = () => { + if (!newRow.studentName || !emailOK(newRow.studentEmail)) { + window.alert("Fill all required fields with valid values"); + return; + } + addStudentData(newRow); + setNewRow({ + studentName: "", + studentEmail: "", + studentRole: "viewer", + topicDescription: "", + }); + setAdding(false); + }; + + const field = (e, setter) => + setter((prev) => ({ ...prev, [e.target.name]: e.target.value })); + + /* ------------------------- render ------------------------- */ + if (studentData.length === 0 && !adding) { + return ( + + + No student data available + + + + ); + } + + return ( + + + + + + Student name + Email + Role + Topic + + Actions + + + + + + {/* existing rows */} + {studentData.map((s) => + editingId === s.id ? ( + + + field(e, setEditRow)} + fullWidth + /> + + + field(e, setEditRow)} + fullWidth + /> + + + + + + field(e, setEditRow)} + disabled={editRow.studentRole === "viewer"} + fullWidth + /> + + + + + + + + + + + ) : ( + + {s.studentName} + {s.studentEmail} + + + + + {s.studentRole === "presenter" ? ( + s.topicDescription || ( + + No topic + + ) + ) : ( + N/A + )} + + + startEdit(s)} + sx={{ mr: 1 }} + > + + + + window.confirm("Delete this student?") && + deleteStudentData(s.id) + } + > + + + + + ), + )} + + {/* add‑new row */} + {adding && ( + + + field(e, setNewRow)} + placeholder="Student name" + fullWidth + /> + + + field(e, setNewRow)} + placeholder="student@example.com" + fullWidth + /> + + + + + + field(e, setNewRow)} + disabled={newRow.studentRole === "viewer"} + placeholder={ + newRow.studentRole === "presenter" + ? "Topic description" + : "N/A for viewers" + } + fullWidth + /> + + + + + + setAdding(false)}> + + + + + )} + +
+
+ + {/* footer add button */} + {!adding && ( + + + + )} +
+ ); +} diff --git a/src/components/dashboard/layout/config.js b/src/components/dashboard/layout/config.js index a3837d1..09d7e2a 100644 --- a/src/components/dashboard/layout/config.js +++ b/src/components/dashboard/layout/config.js @@ -1,110 +1,115 @@ -import { paths } from '@/paths'; +import { paths } from "@/paths"; export const layoutConfig = { navItems: [ { - key: 'dashboards', - title: 'Dashboards', + key: "dashboards", + title: "Dashboards", items: [ - { key: 'overview', title: 'Overview', href: paths.dashboard.overview, icon: 'house' } + { + key: "overview", + title: "Overview", + href: paths.dashboard.overview, + icon: "house", + }, ], }, { - key: 'capstone', - title: 'Capstone', + key: "capstone", + title: "Capstone", items: [ { - key: 'capstone:importData', - title: 'Import Data', - href: paths.dashboard.importData, + key: "capstone:importData", + title: "Configurations", + href: paths.dashboard.capstone.configurations, }, { - key: 'capstone:rooms', - title: 'Rooms', - href: paths.dashboard.rooms, + key: "capstone:students", + title: "Students", + href: paths.dashboard.capstone.students, }, { - key: 'capstone:students', - title: 'Students', - href: paths.dashboard.students, + key: "capstone:rooms", + title: "Rooms", + href: paths.dashboard.capstone.rooms, }, ], //href: paths.dashboard.settings.account, - icon: 'read-cv-logo', + icon: "read-cv-logo", //matcher: { type: 'startsWith', href: '/dashboard/settings' }, }, - ] - // { - // key: 'courseplanning', - // title: 'Course Planning', - // items: [ - // { - // key: 'teachers_and_courses', - // title: 'Teachers & Courses', - // icon: 'chalkboard', - // items: [ - // { - // key: 'teachers_and_courses:overview', - // title: 'Section Overview', - // }, - // { - // key: 'teachers_and_courses:courses', - // title: 'Courses', - // }, - // { - // key: 'teachers_and_courses:teachers', - // title: 'Teachers', - // }, - // ], - // }, - // { - // key: 'students', - // title: 'Students', - // icon: 'student', - // items: [ - // { - // key: 'students:overview', - // title: 'Section Overview', - // }, - // { - // key: 'students:manage', - // title: 'View & Manage Students', - // href: paths.dashboard.students.list - // }, - // ], - // }, - // { - // key: 'current_schedule', - // title: 'Current Schedule', - // icon: 'calendardots', - // items: [ - // { - // key: 'current_schedule:overview', - // title: 'Schedule Overview', - // }, - // { - // key: 'current_schedule:classes', - // title: 'Classes', - // }, - // { - // key: 'current_schedule:view_individual', - // title: 'View as Individual', - // }, - // ], - // }, - // { key: 'courseplan_settings', title: 'Settings', href: paths.pricing, icon: 'gear' }, - // { key: 'build_course', title: 'Checkout', href: paths.checkout, icon: 'sign-out' }, - // { key: 'contact', title: 'Contact', href: paths.contact, icon: 'address-book' }, - // { - // key: 'error', - // title: 'Error', - // icon: 'file-x', - // items: [ - // { key: 'error:not-authorized', title: 'Not authorized', href: paths.notAuthorized }, - // { key: 'error:not-found', title: 'Not found', href: paths.notFound }, - // { key: 'error:internal-server-error', title: 'Internal server error', href: paths.internalServerError }, - // ], - // }, - // ], - // } + ], + // { + // key: 'courseplanning', + // title: 'Course Planning', + // items: [ + // { + // key: 'teachers_and_courses', + // title: 'Teachers & Courses', + // icon: 'chalkboard', + // items: [ + // { + // key: 'teachers_and_courses:overview', + // title: 'Section Overview', + // }, + // { + // key: 'teachers_and_courses:courses', + // title: 'Courses', + // }, + // { + // key: 'teachers_and_courses:teachers', + // title: 'Teachers', + // }, + // ], + // }, + // { + // key: 'students', + // title: 'Students', + // icon: 'student', + // items: [ + // { + // key: 'students:overview', + // title: 'Section Overview', + // }, + // { + // key: 'students:manage', + // title: 'View & Manage Students', + // href: paths.dashboard.students.list + // }, + // ], + // }, + // { + // key: 'current_schedule', + // title: 'Current Schedule', + // icon: 'calendardots', + // items: [ + // { + // key: 'current_schedule:overview', + // title: 'Schedule Overview', + // }, + // { + // key: 'current_schedule:classes', + // title: 'Classes', + // }, + // { + // key: 'current_schedule:view_individual', + // title: 'View as Individual', + // }, + // ], + // }, + // { key: 'courseplan_settings', title: 'Settings', href: paths.pricing, icon: 'gear' }, + // { key: 'build_course', title: 'Checkout', href: paths.checkout, icon: 'sign-out' }, + // { key: 'contact', title: 'Contact', href: paths.contact, icon: 'address-book' }, + // { + // key: 'error', + // title: 'Error', + // icon: 'file-x', + // items: [ + // { key: 'error:not-authorized', title: 'Not authorized', href: paths.notAuthorized }, + // { key: 'error:not-found', title: 'Not found', href: paths.notFound }, + // { key: 'error:internal-server-error', title: 'Internal server error', href: paths.internalServerError }, + // ], + // }, + // ], + // } }; diff --git a/src/components/marketing/home/hero.js b/src/components/marketing/home/hero.js index ac08b62..5e6a695 100644 --- a/src/components/marketing/home/hero.js +++ b/src/components/marketing/home/hero.js @@ -1,13 +1,11 @@ "use client"; import * as React from "react"; -import RouterLink from "next/link"; import Box from "@mui/material/Box"; import Button from "@mui/material/Button"; import Container from "@mui/material/Container"; import Stack from "@mui/material/Stack"; import Typography from "@mui/material/Typography"; -import { paths } from "@/paths"; import { useRef } from "react"; import gsap from "gsap"; import { ScrollTrigger } from "gsap/ScrollTrigger"; @@ -116,7 +114,9 @@ export function Hero() { "+=0.2", ); - return () => tl.kill(); + return () => { + tl.kill(); + }; }, []); return ( diff --git a/src/contexts/capstone-context-provider.tsx b/src/contexts/capstone-context-provider.tsx new file mode 100644 index 0000000..ae46154 --- /dev/null +++ b/src/contexts/capstone-context-provider.tsx @@ -0,0 +1,105 @@ +"use client"; + +import React, { useState } from "react"; +import { CapstoneContext } from "./capstone-context"; +import { toast } from "sonner"; +import type { CapstoneContextType } from "./capstone-context"; +import type { + ImportType, + RoomData, + StudentData, + ValidationError, +} from "@/types/capstone-types"; + +export function CapstoneProvider({ children }: { children: React.ReactNode }) { + const [importType, setImportType] = useState("room"); + const [roomData, setRoomData] = useState([]); + const [studentData, setStudentData] = useState([]); + const [validationErrors, setValidationErrors] = useState( + [], + ); + const [isAuditView, setIsAuditView] = useState(false); + + const addRoomData = (data: RoomData) => { + const newRoom = { ...data, id: data.id || `room-${Date.now()}` }; + setRoomData((prev) => [...prev, newRoom]); + + toast.success("Room Added", { + description: `Added room ${newRoom.roomNumber}`, + }); + }; + + const addStudentData = (data: StudentData) => { + const newStudent = { ...data, id: data.id || `student-${Date.now()}` }; + setStudentData((prev) => [...prev, newStudent]); + + toast.success("Student Added", { + description: `Added ${newStudent.studentName}`, + }); + }; + + const updateRoomData = (id: string, data: RoomData) => { + setRoomData((prev) => + prev.map((room) => (room.id === id ? { ...data, id } : room)), + ); + toast.success("Room Updated", { + description: `Updated room ${data.roomNumber}`, + }); + }; + + const updateStudentData = (id: string, data: StudentData) => { + setStudentData((prev) => + prev.map((student) => (student.id === id ? { ...data, id } : student)), + ); + toast.success("Student Updated", { + description: `Updated ${data.studentName}`, + }); + }; + + const deleteRoomData = (id: string) => { + const room = roomData.find((r) => r.id === id); + setRoomData((prev) => prev.filter((r) => r.id !== id)); + + if (room) { + toast.success("Room Deleted", { + description: `Deleted room ${room.roomNumber}`, + }); + } + }; + + const deleteStudentData = (id: string) => { + const student = studentData.find((s) => s.id === id); + setStudentData((prev) => prev.filter((s) => s.id !== id)); + + if (student) { + toast.success("Student Deleted", { + description: `Deleted ${student.studentName}`, + }); + } + }; + + const contextValue: CapstoneContextType = { + importType, + setImportType, + roomData, + studentData, + validationErrors, + setValidationErrors, + setRoomData, + setStudentData, + addRoomData, + addStudentData, + updateRoomData, + updateStudentData, + deleteRoomData, + deleteStudentData, + isAuditView, + setIsAuditView, + }; + + return ( + + {children} + + ); +} diff --git a/src/contexts/capstone-context.ts b/src/contexts/capstone-context.ts new file mode 100644 index 0000000..2a31b53 --- /dev/null +++ b/src/contexts/capstone-context.ts @@ -0,0 +1,42 @@ +import { createContext, useContext } from "react"; +import type { + ImportType, + RoomData, + StudentData, + ValidationError, +} from "@/types/capstone-types"; + +export interface CapstoneContextType { + importType: ImportType; + setImportType: (type: ImportType) => void; + roomData: RoomData[]; + studentData: StudentData[]; + validationErrors: ValidationError[]; + setValidationErrors: (errors: ValidationError[]) => void; + setRoomData: (data: RoomData[]) => void; + setStudentData: (data: StudentData[]) => void; + addRoomData: (data: RoomData) => void; + addStudentData: (data: StudentData) => void; + updateRoomData: (id: string, data: RoomData) => void; + updateStudentData: (id: string, data: StudentData) => void; + deleteRoomData: (id: string) => void; + deleteStudentData: (id: string) => void; + isAuditView: boolean; + setIsAuditView: (isAudit: boolean) => void; +} + +export const CapstoneContext = createContext( + undefined, +); + +export const useCapstoneContext = () => { + const context = useContext(CapstoneContext); + + if (!context) { + throw new Error( + "useCapstoneContext must be used within a CapstoneProvider", + ); + } + + return context; +}; diff --git a/src/lib/parseCSV.ts b/src/lib/parseCSV.ts new file mode 100644 index 0000000..5fe2dbd --- /dev/null +++ b/src/lib/parseCSV.ts @@ -0,0 +1,169 @@ +import Papa from "papaparse"; +import * as XLSX from "xlsx"; +import { ImportType, ValidationError } from "@/types/capstone-types"; + +interface ParseResult { + data: T[]; + errors: ValidationError[]; +} + +export const parseFile = async ( + file: File, + importType: ImportType, +): Promise> => { + const fileExtension = file.name.split(".").pop()?.toLowerCase(); + + try { + let rawData: any[] = []; + + if (fileExtension === "csv") { + // Parse CSV using PapaParse + const result = await new Promise>( + (resolve, reject) => { + Papa.parse(file, { + header: true, + skipEmptyLines: true, + complete: resolve, + error: reject, + }); + }, + ); + + rawData = result.data; + } else if (["xls", "xlsx"].includes(fileExtension || "")) { + // Parse Excel files using XLSX + const data = await file.arrayBuffer(); + const workbook = XLSX.read(data); + const worksheet = workbook.Sheets[workbook.SheetNames[0]]; + rawData = XLSX.utils.sheet_to_json(worksheet); + } else { + throw new Error(`Unsupported file format: ${fileExtension}`); + } + + return validateData(rawData, importType); + } catch (error) { + console.error("Error parsing file:", error); + return { + data: [], + errors: [ + { + row: 0, + column: "file", + message: `Error parsing file: ${error instanceof Error ? error.message : "Unknown error"}`, + }, + ], + } as ParseResult; + } +}; + +const validateData = ( + data: any[], + importType: ImportType, +): ParseResult => { + const errors: ValidationError[] = []; + const validData: any[] = []; + + data.forEach((row, index) => { + const rowNumber = index + 2; // +2 because we're 0-indexed and row 1 is the header + const rowErrors: ValidationError[] = []; + + if (importType === "room") { + // Validate room data + if (!row.teacherName || typeof row.teacherName !== "string") { + rowErrors.push({ + row: rowNumber, + column: "teacherName", + message: "Teacher name is required and must be text", + }); + } + + if ( + !row.roomNumber || + (typeof row.roomNumber !== "string" && + typeof row.roomNumber !== "number") + ) { + rowErrors.push({ + row: rowNumber, + column: "roomNumber", + message: "Room number is required", + }); + } + + // If no errors, add to valid data with string roomNumber + if (rowErrors.length === 0) { + validData.push({ + ...row, + id: `room-${rowNumber}-${Date.now()}`, + roomNumber: row.roomNumber.toString(), + }); + } + } else if (importType === "student") { + // Validate student data + if (!row.studentName || typeof row.studentName !== "string") { + rowErrors.push({ + row: rowNumber, + column: "studentName", + message: "Student name is required and must be text", + }); + } + + if ( + !row.studentEmail || + typeof row.studentEmail !== "string" || + !isValidEmail(row.studentEmail) + ) { + rowErrors.push({ + row: rowNumber, + column: "studentEmail", + message: "Valid student email is required", + }); + } + + if ( + !row.studentRole || + !["presenter", "viewer"].includes(row.studentRole.toLowerCase()) + ) { + rowErrors.push({ + row: rowNumber, + column: "studentRole", + message: 'Student role must be either "presenter" or "viewer"', + }); + } else { + // Convert to lowercase for consistency + row.studentRole = row.studentRole.toLowerCase(); + } + + // Topic description is only required for presenters + if ( + row.studentRole?.toLowerCase() === "presenter" && + !row.topicDescription + ) { + // This is not a hard error, but we'll flag it + console.warn( + `Warning: Presenter at row ${rowNumber} has no topic description`, + ); + } + + // If no errors, add to valid data + if (rowErrors.length === 0) { + validData.push({ + ...row, + id: `student-${rowNumber}-${Date.now()}`, + studentRole: row.studentRole.toLowerCase(), + }); + } + } + + errors.push(...rowErrors); + }); + + return { + data: validData as T[], + errors, + }; +}; + +const isValidEmail = (email: string): boolean => { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(email); +}; diff --git a/src/paths.js b/src/paths.js index d296557..7f73063 100644 --- a/src/paths.js +++ b/src/paths.js @@ -54,6 +54,11 @@ export const paths = { }, dashboard: { overview: "/dashboard", + capstone: { + rooms: "/dashboard/capstone/rooms", + students: "/dashboard/capstone/students", + configurations: "/dashboard/capstone/configurations", + }, settings: { account: "/dashboard/settings/account", billing: "/dashboard/settings/billing", @@ -80,10 +85,7 @@ export const paths = { thread: (threadType, threadId) => `/dashboard/chat/${threadType}/${threadId}`, }, - rooms: "/dashboard/rooms", crypto: "/dashboard/crypto", - students: "/dashboard/students", - importData: "/dashboard/import-data", eCommerce: "/dashboard/e-commerce", fileStorage: "/dashboard/file-storage", i18n: "/dashboard/i18n", diff --git a/src/types/capstone-types.ts b/src/types/capstone-types.ts new file mode 100644 index 0000000..1b858fc --- /dev/null +++ b/src/types/capstone-types.ts @@ -0,0 +1,23 @@ +export type ImportType = "room" | "student"; + +export interface RoomData { + id?: string; + teacherName: string; + roomNumber: string; +} + +export interface StudentData { + id?: string; + studentName: string; + studentEmail: string; + studentRole: "presenter" | "viewer"; + topicDescription?: string; +} + +export type DataItem = RoomData | StudentData; + +export interface ValidationError { + row: number; + column: string; + message: string; +} From ce242d14ec746f1cf25e855639b2da3abf3b0469 Mon Sep 17 00:00:00 2001 From: Ericzhou-ez Date: Wed, 21 May 2025 12:56:01 -0700 Subject: [PATCH 2/4] feat: confirmation dialogue and frontend room and student guard --- .../capstone/configurations/auditView.js | 85 +++++++++++++++++-- 1 file changed, 78 insertions(+), 7 deletions(-) diff --git a/src/components/dashboard/capstone/configurations/auditView.js b/src/components/dashboard/capstone/configurations/auditView.js index 9adca9b..dcc58d0 100644 --- a/src/components/dashboard/capstone/configurations/auditView.js +++ b/src/components/dashboard/capstone/configurations/auditView.js @@ -1,6 +1,6 @@ "use client"; -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; import { Box, Button, @@ -19,8 +19,10 @@ import { Typography, useMediaQuery, useTheme, + DialogContentText, } from "@mui/material"; import { Question } from "@phosphor-icons/react"; +import { toast } from "sonner"; import { useCapstoneContext } from "@/contexts/capstone-context"; import RoomDataTable from "./roomDataTable"; @@ -73,7 +75,6 @@ export default function AuditView() { ); - /* ------------------------------ render ------------------------------ */ return ( {isMonitor ? ( @@ -191,10 +192,80 @@ export default function AuditView() { ); } -const SendLinkButton = () => { +async function sendRegistrationLinks() { + // Simulated latency + await new Promise((r) => setTimeout(r, 1200)); +} + +export function SendLinkButton() { + const { studentData, roomData } = useCapstoneContext(); + + const [open, setOpen] = useState(false); + const [sending, setSending] = useState(false); + const disabled = studentData.length === 0 || roomData.length === 0; + + const handleSend = async () => { + setSending(true); + try { + await sendRegistrationLinks(); + toast.success("Registration links sent!"); + setOpen(false); + } catch (err) { + console.error(err); + toast.error("Failed to send registration links"); + } finally { + setSending(false); + } + }; + return ( - + <> + + + !sending && setOpen(false)} + PaperProps={{ + sx: { p: 2 }, + }} + > + + Confirm Send + + + + + You're about to send registration links to + {studentData.length} + students for + {roomData.length} + room{roomData.length !== 1 && "s"}. +
+ Once sent, student emails are dispatched immediately. +
+
+ + + + + +
+ ); -}; +} From 9bdd2f924e35a9325d248bcc54ffacbcb89ecf3e Mon Sep 17 00:00:00 2001 From: Ericzhou-ez Date: Wed, 21 May 2025 13:06:11 -0700 Subject: [PATCH 3/4] fix: mobile nav groups --- src/components/marketing/layout/mobile-nav.js | 291 ++++++++++++------ 1 file changed, 195 insertions(+), 96 deletions(-) diff --git a/src/components/marketing/layout/mobile-nav.js b/src/components/marketing/layout/mobile-nav.js index f07b42d..5b7f028 100644 --- a/src/components/marketing/layout/mobile-nav.js +++ b/src/components/marketing/layout/mobile-nav.js @@ -1,53 +1,73 @@ -'use client'; - -import * as React from 'react'; -import RouterLink from 'next/link'; -import { usePathname } from 'next/navigation'; -import Box from '@mui/material/Box'; -import Dialog from '@mui/material/Dialog'; -import DialogContent from '@mui/material/DialogContent'; -import IconButton from '@mui/material/IconButton'; -import Stack from '@mui/material/Stack'; -import Typography from '@mui/material/Typography'; -import { CaretDown as CaretDownIcon } from '@phosphor-icons/react/dist/ssr/CaretDown'; -import { CaretRight as CaretRightIcon } from '@phosphor-icons/react/dist/ssr/CaretRight'; -import { X as XIcon } from '@phosphor-icons/react/dist/ssr/X'; - -import { paths } from '@/paths'; -import { isNavItemActive } from '@/lib/is-nav-item-active'; -import { DynamicLogo } from '@/components/core/logo'; +"use client"; + +import * as React from "react"; +import RouterLink from "next/link"; +import { usePathname } from "next/navigation"; +import Box from "@mui/material/Box"; +import Dialog from "@mui/material/Dialog"; +import DialogContent from "@mui/material/DialogContent"; +import IconButton from "@mui/material/IconButton"; +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import { CaretDown as CaretDownIcon } from "@phosphor-icons/react/dist/ssr/CaretDown"; +import { CaretRight as CaretRightIcon } from "@phosphor-icons/react/dist/ssr/CaretRight"; +import { X as XIcon } from "@phosphor-icons/react/dist/ssr/X"; + +import { paths } from "@/paths"; +import { isNavItemActive } from "@/lib/is-nav-item-active"; +import { DynamicLogo } from "@/components/core/logo"; // NOTE: First level elements are groups. const navItems = [ { - key: 'group-0', + key: "group-0", items: [ - { key: 'home', title: 'Home', href: paths.home }, - { key: 'components', title: 'Components', href: paths.components.index }, + { key: "home", title: "Home", href: paths.home }, { - key: 'dashboard', - title: 'Dashboard', + key: "dashboard", + title: "Dashboard", items: [ - { key: 'overview', title: 'Overview', href: paths.dashboard.overview }, - { key: 'analytics', title: 'Customers', href: paths.dashboard.students.list }, - { key: 'logistics', title: 'Logistics', href: paths.dashboard.logistics.metrics }, - { key: 'settings', title: 'Settings', href: paths.dashboard.settings.account }, - { key: 'file-storage', title: 'File storage', href: paths.dashboard.fileStorage }, + { + key: "overview", + title: "Overview", + href: paths.dashboard.overview, + }, + // { key: 'analytics', title: 'Customers', href: paths.dashboard.students.list }, + { + key: "logistics", + title: "Logistics", + href: paths.dashboard.logistics.metrics, + }, + { + key: "settings", + title: "Settings", + href: paths.dashboard.settings.account, + }, + { + key: "file-storage", + title: "File storage", + href: paths.dashboard.fileStorage, + }, ], }, { - key: 'marketing', - title: 'Marketing', + key: "services", + title: "Features", items: [ - { key: 'blog', title: 'Blog', href: paths.dashboard.blog.list }, - { key: 'pricing', title: 'Pricing', href: paths.pricing }, - { key: 'contact', title: 'Contact', href: paths.contact }, - { key: 'checkout', title: 'Checkout', href: paths.checkout }, - { key: 'error', title: 'Error', href: paths.notFound }, + { + key: "timetabling", + title: "Course timetabling", + href: paths.checkout, + }, // to do implement landing + { + key: "capstone", + title: "Capstone scheduling", + href: paths.notFound, + }, // to do implement landing ], }, - { key: 'docs', title: 'Docs', href: paths.docs, external: true }, + { key: "docs", title: "Docs", href: paths.docs, external: true }, // to do write docs ], }, ]; @@ -61,41 +81,60 @@ export function MobileNav({ onClose, open = false }) { onClose={onClose} open={open} sx={{ - '& .MuiDialog-container': { justifyContent: 'flex-end' }, - '& .MuiDialog-paper': { - '--MobileNav-background': 'var(--mui-palette-background-paper)', - '--MobileNav-color': 'var(--mui-palette-text-primary)', - '--NavGroup-title-color': 'var(--mui-palette-neutral-400)', - '--NavItem-color': 'var(--mui-palette-text-secondary)', - '--NavItem-hover-background': 'var(--mui-palette-action-hover)', - '--NavItem-active-background': 'var(--mui-palette-action-selected)', - '--NavItem-active-color': 'var(--mui-palette-text-primary)', - '--NavItem-disabled-color': 'var(--mui-palette-text-disabled)', - '--NavItem-icon-color': 'var(--mui-palette-neutral-500)', - '--NavItem-icon-active-color': 'var(--mui-palette-primary-main)', - '--NavItem-icon-disabled-color': 'var(--mui-palette-neutral-600)', - '--NavItem-expand-color': 'var(--mui-palette-neutral-400)', - bgcolor: 'var(--MobileNav-background)', - color: 'var(--MobileNav-color)', - display: 'flex', - flexDirection: 'column', - height: '100%', - width: '100%', - zIndex: 'var(--MobileNav-zIndex)', + "& .MuiDialog-container": { justifyContent: "flex-end" }, + "& .MuiDialog-paper": { + "--MobileNav-background": "var(--mui-palette-background-paper)", + "--MobileNav-color": "var(--mui-palette-text-primary)", + "--NavGroup-title-color": "var(--mui-palette-neutral-400)", + "--NavItem-color": "var(--mui-palette-text-secondary)", + "--NavItem-hover-background": "var(--mui-palette-action-hover)", + "--NavItem-active-background": "var(--mui-palette-action-selected)", + "--NavItem-active-color": "var(--mui-palette-text-primary)", + "--NavItem-disabled-color": "var(--mui-palette-text-disabled)", + "--NavItem-icon-color": "var(--mui-palette-neutral-500)", + "--NavItem-icon-active-color": "var(--mui-palette-primary-main)", + "--NavItem-icon-disabled-color": "var(--mui-palette-neutral-600)", + "--NavItem-expand-color": "var(--mui-palette-neutral-400)", + bgcolor: "var(--MobileNav-background)", + color: "var(--MobileNav-color)", + display: "flex", + flexDirection: "column", + height: "100%", + width: "100%", + zIndex: "var(--MobileNav-zIndex)", }, }} > - - - - + + + + - + {renderNavGroups({ items: navItems, onClose, pathname })} @@ -110,20 +149,28 @@ function renderNavGroups({ items, onClose, pathname }) { {curr.title ? (
- + {curr.title}
) : null} -
{renderNavItems({ depth: 0, items: curr.items, onClose, pathname })}
-
+
+ {renderNavItems({ depth: 0, items: curr.items, onClose, pathname })} +
+
, ); return acc; }, []); return ( - + {children} ); @@ -134,34 +181,75 @@ function renderNavItems({ depth = 0, items = [], onClose, pathname }) { const { items: childItems, key, ...item } = curr; const forceOpen = childItems - ? Boolean(childItems.find((childItem) => childItem.href && pathname.startsWith(childItem.href))) + ? Boolean( + childItems.find( + (childItem) => + childItem.href && pathname.startsWith(childItem.href), + ), + ) : false; acc.push( - - {childItems ? renderNavItems({ depth: depth + 1, items: childItems, onClose, pathname }) : null} - + + {childItems + ? renderNavItems({ + depth: depth + 1, + items: childItems, + onClose, + pathname, + }) + : null} + , ); return acc; }, []); return ( - + {children} ); } -function NavItem({ children, depth, disabled, external, forceOpen = false, href, matcher, onClose, pathname, title }) { +function NavItem({ + children, + depth, + disabled, + external, + forceOpen = false, + href, + matcher, + onClose, + pathname, + title, +}) { const [open, setOpen] = React.useState(forceOpen); - const active = isNavItemActive({ disabled, external, href, matcher, pathname }); + const active = isNavItemActive({ + disabled, + external, + href, + matcher, + pathname, + }); const ExpandIcon = open ? CaretDownIcon : CaretRightIcon; const isBranch = children && !href; const showChildren = Boolean(children && open); return ( - + { - if (event.key === 'Enter' || event.key === ' ') { + if (event.key === "Enter" || event.key === " ") { setOpen(!open); } }, - role: 'button', + role: "button", } : { ...(href ? { - component: external ? 'a' : RouterLink, + component: external ? "a" : RouterLink, href, - target: external ? '_blank' : undefined, - rel: external ? 'noreferrer' : undefined, + target: external ? "_blank" : undefined, + rel: external ? "noreferrer" : undefined, onClick: () => { onClose?.(); }, } - : { role: 'button' }), + : { role: "button" }), })} sx={{ - alignItems: 'center', + alignItems: "center", borderRadius: 1, - color: 'var(--NavItem-color)', - cursor: 'pointer', - display: 'flex', - p: '12px', - textDecoration: 'none', + color: "var(--NavItem-color)", + cursor: "pointer", + display: "flex", + p: "12px", + textDecoration: "none", ...(disabled && { - bgcolor: 'var(--NavItem-disabled-background)', - color: 'var(--NavItem-disabled-color)', - cursor: 'not-allowed', + bgcolor: "var(--NavItem-disabled-background)", + color: "var(--NavItem-disabled-color)", + cursor: "not-allowed", + }), + ...(active && { + bgcolor: "var(--NavItem-active-background)", + color: "var(--NavItem-active-color)", }), - ...(active && { bgcolor: 'var(--NavItem-active-background)', color: 'var(--NavItem-active-color)' }), - ...(open && { color: 'var(--NavItem-open-color)' }), - '&:hover': { + ...(open && { color: "var(--NavItem-open-color)" }), + "&:hover": { ...(!disabled && - !active && { bgcolor: 'var(--NavItem-hover-background)', color: 'var(--NavItem-hover-color)' }), + !active && { + bgcolor: "var(--NavItem-hover-background)", + color: "var(--NavItem-hover-color)", + }), }, }} tabIndex={0} > - + {title} {isBranch ? : null} - {showChildren ? {children} : null} + {showChildren ? {children} : null} ); } From a5add7baa62126ace8dd8f5ae6755ef5025c4b96 Mon Sep 17 00:00:00 2001 From: Ericzhou-ez Date: Wed, 21 May 2025 13:27:00 -0700 Subject: [PATCH 4/4] mobile nav group update + hero zIndex fix --- src/app/(marketing)/page.js | 9 ----- src/components/marketing/home/hero.js | 6 +-- src/components/marketing/layout/footer.js | 17 ++++---- src/components/marketing/layout/main-nav.js | 6 +-- src/components/marketing/layout/mobile-nav.js | 39 +++++++++++-------- 5 files changed, 35 insertions(+), 42 deletions(-) diff --git a/src/app/(marketing)/page.js b/src/app/(marketing)/page.js index 511700b..fe2d7c8 100644 --- a/src/app/(marketing)/page.js +++ b/src/app/(marketing)/page.js @@ -18,15 +18,6 @@ export default function Page() { return (
- diff --git a/src/components/marketing/home/hero.js b/src/components/marketing/home/hero.js index 5e6a695..cde4be9 100644 --- a/src/components/marketing/home/hero.js +++ b/src/components/marketing/home/hero.js @@ -51,8 +51,8 @@ export function Hero() { tl.to( demoRef.current, { - scale: 1.2, - ease: "none", + scale: 1.1, + ease: "power1", }, 0, ); @@ -122,9 +122,7 @@ export function Hero() { return ( - © 2024 EduCourse.ca + © {new Date().getFullYear()} EduCourse.ca diff --git a/src/components/marketing/layout/main-nav.js b/src/components/marketing/layout/main-nav.js index 9f39028..3f7cc36 100644 --- a/src/components/marketing/layout/main-nav.js +++ b/src/components/marketing/layout/main-nav.js @@ -60,10 +60,10 @@ export function MainNav() { sx={{ bgcolor: scrolledPastHero ? "black" : "transparent", color: scrolledPastHero ? "var(--mui-palette-common-white)" : "black", - ml: "16px", - width: "calc(100% - 32px)", + ml: "6px", + width: "calc(100% - 12px)", position: "fixed", - top: "16px", + top: "6px", zIndex: "var(--MainNav-zIndex)", transition: "background-color 0.3s ease, color 0.3s ease", borderRadius: "25px", diff --git a/src/components/marketing/layout/mobile-nav.js b/src/components/marketing/layout/mobile-nav.js index 5b7f028..2357a36 100644 --- a/src/components/marketing/layout/mobile-nav.js +++ b/src/components/marketing/layout/mobile-nav.js @@ -33,21 +33,10 @@ const navItems = [ title: "Overview", href: paths.dashboard.overview, }, - // { key: 'analytics', title: 'Customers', href: paths.dashboard.students.list }, { - key: "logistics", - title: "Logistics", - href: paths.dashboard.logistics.metrics, - }, - { - key: "settings", - title: "Settings", - href: paths.dashboard.settings.account, - }, - { - key: "file-storage", - title: "File storage", - href: paths.dashboard.fileStorage, + key: "capstone configurations", + title: "Capstone configurations", + href: paths.dashboard.capstone.configurations, }, ], }, @@ -80,8 +69,24 @@ export function MobileNav({ onClose, open = false }) { maxWidth="sm" onClose={onClose} open={open} + PaperProps={{ + sx: { + width: 280, + maxWidth: "80vw", + height: "90vh", + maxHeight: 700, + borderRadius: 2, + overflow: "hidden", + display: "flex", + flexDirection: "column", + bgcolor: "background.paper", + }, + }} sx={{ - "& .MuiDialog-container": { justifyContent: "flex-end" }, + "& .MuiDialog-container": { + justifyContent: "center", + alignItems: "center", + }, "& .MuiDialog-paper": { "--MobileNav-background": "var(--mui-palette-background-paper)", "--MobileNav-color": "var(--mui-palette-text-primary)", @@ -121,8 +126,8 @@ export function MobileNav({ onClose, open = false }) {