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/(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/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 (
+
+ );
+};
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 && (
-
- )}
-
- {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
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-}
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..dcc58d0
--- /dev/null
+++ b/src/components/dashboard/capstone/configurations/auditView.js
@@ -0,0 +1,271 @@
+"use client";
+
+import React, { useEffect, useState } from "react";
+import {
+ Box,
+ Button,
+ Card,
+ CardContent,
+ CardHeader,
+ Dialog,
+ DialogActions,
+ DialogContent,
+ DialogTitle,
+ Grid,
+ IconButton,
+ Tab,
+ Tabs,
+ Tooltip,
+ 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";
+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")}>
+
+
+
+ }
+ />
+
+
+
+
+ );
+
+ 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 ---------------- */}
+
+
+
+
+ );
+}
+
+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 (
+ <>
+
+
+
+ >
+ );
+}
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
+
+ }
+ onClick={() => setNewRowOpen(true)}
+ >
+ Add room
+
+
+ );
+ }
+
+ 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 && (
+
+ }
+ onClick={() => setNewRowOpen(true)}
+ variant="contained"
+ color="info"
+ >
+ Add room
+
+
+ )}
+
+ );
+}
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
+
+ }
+ onClick={() => setAdding(true)}
+ >
+ Add student
+
+
+ );
+ }
+
+ 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 && (
+
+ }
+ onClick={() => setAdding(true)}
+ color="info"
+ >
+ Add student
+
+
+ )}
+
+ );
+}
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..cde4be9 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";
@@ -53,8 +51,8 @@ export function Hero() {
tl.to(
demoRef.current,
{
- scale: 1.2,
- ease: "none",
+ scale: 1.1,
+ ease: "power1",
},
0,
);
@@ -116,15 +114,15 @@ export function Hero() {
"+=0.2",
);
- return () => tl.kill();
+ return () => {
+ tl.kill();
+ };
}, []);
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 f07b42d..2357a36 100644
--- a/src/components/marketing/layout/mobile-nav.js
+++ b/src/components/marketing/layout/mobile-nav.js
@@ -1,53 +1,62 @@
-'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: "capstone configurations",
+ title: "Capstone configurations",
+ href: paths.dashboard.capstone.configurations,
+ },
],
},
{
- 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
],
},
];
@@ -60,42 +69,77 @@ 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-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: "center",
+ alignItems: "center",
+ },
+ "& .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 +154,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 +186,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}
);
}
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;
+}