diff --git a/package-lock.json b/package-lock.json
index 33c87b3..baa8926 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -44,6 +44,7 @@
"@tiptap/starter-kit": "^2.12.0",
"@types/jsonwebtoken": "^9.0.9",
"axios": "^1.9.0",
+ "chart.js": "^4.4.9",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
@@ -60,11 +61,13 @@
"next-auth": "^5.0.0-beta.28",
"next-themes": "^0.4.6",
"react": "^19.0.0",
+ "react-chartjs-2": "^5.3.0",
"react-day-picker": "^9.7.0",
"react-dom": "^19.0.0",
"react-hook-form": "^7.57.0",
"react-katex": "^3.1.0",
"react-resizable-panels": "^3.0.2",
+ "recharts": "^2.15.3",
"sonner": "^2.0.3",
"tailwind-merge": "^3.2.0",
"uuid": "^11.1.0",
@@ -169,6 +172,15 @@
"url": "https://github.com/sponsors/panva"
}
},
+ "node_modules/@babel/runtime": {
+ "version": "7.27.6",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz",
+ "integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
"node_modules/@csstools/color-helpers": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.0.2.tgz",
@@ -552,8 +564,6 @@
},
"node_modules/@hookform/resolvers": {
"version": "5.1.1",
- "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.1.1.tgz",
- "integrity": "sha512-J/NVING3LMAEvexJkyTLjruSm7aOFx7QX21pzkiJfMoNG0wl5aFEjLTl7ay7IQb9EWY6AkrBy7tHL2Alijpdcg==",
"license": "MIT",
"dependencies": {
"@standard-schema/utils": "^0.3.0"
@@ -1090,6 +1100,12 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
+ "node_modules/@kurkle/color": {
+ "version": "0.3.4",
+ "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
+ "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
+ "license": "MIT"
+ },
"node_modules/@monaco-editor/loader": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.5.0.tgz",
@@ -2440,8 +2456,6 @@
},
"node_modules/@standard-schema/utils": {
"version": "0.3.0",
- "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
- "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
"license": "MIT"
},
"node_modules/@swc/counter": {
@@ -2735,6 +2749,33 @@
"tailwindcss": "4.1.8"
}
},
+ "node_modules/@tailwindcss/postcss/node_modules/@tailwindcss/oxide": {
+ "version": "4.1.8",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "dependencies": {
+ "detect-libc": "^2.0.4",
+ "tar": "^7.4.3"
+ },
+ "engines": {
+ "node": ">= 10"
+ },
+ "optionalDependencies": {
+ "@tailwindcss/oxide-android-arm64": "4.1.8",
+ "@tailwindcss/oxide-darwin-arm64": "4.1.8",
+ "@tailwindcss/oxide-darwin-x64": "4.1.8",
+ "@tailwindcss/oxide-freebsd-x64": "4.1.8",
+ "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.8",
+ "@tailwindcss/oxide-linux-arm64-gnu": "4.1.8",
+ "@tailwindcss/oxide-linux-arm64-musl": "4.1.8",
+ "@tailwindcss/oxide-linux-x64-gnu": "4.1.8",
+ "@tailwindcss/oxide-linux-x64-musl": "4.1.8",
+ "@tailwindcss/oxide-wasm32-wasi": "4.1.8",
+ "@tailwindcss/oxide-win32-arm64-msvc": "4.1.8",
+ "@tailwindcss/oxide-win32-x64-msvc": "4.1.8"
+ }
+ },
"node_modules/@tanstack/query-core": {
"version": "5.80.6",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.80.6.tgz",
@@ -2790,8 +2831,6 @@
},
"node_modules/@tanstack/react-table": {
"version": "8.21.3",
- "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.21.3.tgz",
- "integrity": "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==",
"license": "MIT",
"dependencies": {
"@tanstack/table-core": "8.21.3"
@@ -2810,8 +2849,6 @@
},
"node_modules/@tanstack/react-virtual": {
"version": "3.13.10",
- "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.10.tgz",
- "integrity": "sha512-nvrzk4E9mWB4124YdJ7/yzwou7IfHxlSef6ugCFcBfRmsnsma3heciiiV97sBNxyc3VuwtZvmwXd0aB5BpucVw==",
"license": "MIT",
"dependencies": {
"@tanstack/virtual-core": "3.13.10"
@@ -2827,8 +2864,6 @@
},
"node_modules/@tanstack/table-core": {
"version": "8.21.3",
- "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.3.tgz",
- "integrity": "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==",
"license": "MIT",
"engines": {
"node": ">=12"
@@ -2840,8 +2875,6 @@
},
"node_modules/@tanstack/virtual-core": {
"version": "3.13.10",
- "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.10.tgz",
- "integrity": "sha512-sPEDhXREou5HyZYqSWIqdU580rsF6FGeN7vpzijmP3KTiOGjOMZASz4Y6+QKjiFQwhWrR58OP8izYaNGVxvViA==",
"license": "MIT",
"funding": {
"type": "github",
@@ -3411,8 +3444,6 @@
},
"node_modules/@types/use-sync-external-store": {
"version": "0.0.6",
- "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
- "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
"license": "MIT"
},
"node_modules/@types/zen-observable": {
@@ -3975,8 +4006,6 @@
},
"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==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
@@ -4503,8 +4532,6 @@
},
"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==",
"license": "Apache-2.0",
"dependencies": {
"adler-32": "~1.3.0",
@@ -4531,6 +4558,18 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
+ "node_modules/chart.js": {
+ "version": "4.4.9",
+ "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.9.tgz",
+ "integrity": "sha512-EyZ9wWKgpAU0fLJ43YAEIF8sr5F2W3LqbS40ZJyHIner2lY14ufqv2VMp69MAiZ2rpwxEUxEhIH/0U3xyRynxg==",
+ "license": "MIT",
+ "dependencies": {
+ "@kurkle/color": "^0.3.0"
+ },
+ "engines": {
+ "pnpm": ">=8"
+ }
+ },
"node_modules/chownr": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
@@ -4850,8 +4889,6 @@
},
"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==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
@@ -4939,8 +4976,6 @@
},
"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==",
"license": "Apache-2.0",
"bin": {
"crc32": "bin/crc32.njs"
@@ -4985,11 +5020,129 @@
},
"node_modules/csstype": {
"version": "3.1.3",
- "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
- "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
- "devOptional": true,
"license": "MIT"
},
+ "node_modules/d3-array": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
+ "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
+ "license": "ISC",
+ "dependencies": {
+ "internmap": "1 - 2"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-color": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
+ "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-ease": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
+ "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-format": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz",
+ "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-interpolate": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
+ "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-color": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-path": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
+ "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-scale": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
+ "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "2.10.0 - 3",
+ "d3-format": "1 - 3",
+ "d3-interpolate": "1.2.0 - 3",
+ "d3-time": "2.1.1 - 3",
+ "d3-time-format": "2 - 4"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-shape": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
+ "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-path": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-time": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
+ "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "2 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-time-format": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
+ "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-time": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-timer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
+ "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/damerau-levenshtein": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
@@ -5082,8 +5235,6 @@
},
"node_modules/date-fns-tz": {
"version": "3.2.0",
- "resolved": "https://registry.npmjs.org/date-fns-tz/-/date-fns-tz-3.2.0.tgz",
- "integrity": "sha512-sg8HqoTEulcbbbVXeg84u5UnlsQa8GS5QXMqjjYIhS4abEVVKIUwe0/l/UhrZdKaL/W5eWZNlbTeEIiOXTcsBQ==",
"license": "MIT",
"peerDependencies": {
"date-fns": "^3.0.0 || ^4.0.0"
@@ -5200,6 +5351,16 @@
"node": ">=0.10.0"
}
},
+ "node_modules/dom-helpers": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
+ "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.8.7",
+ "csstype": "^3.0.2"
+ }
+ },
"node_modules/dompurify": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.6.tgz",
@@ -5938,6 +6099,15 @@
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"license": "MIT"
},
+ "node_modules/fast-equals": {
+ "version": "5.2.2",
+ "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.2.2.tgz",
+ "integrity": "sha512-V7/RktU11J3I36Nwq2JnZEM7tNm17eBJz+u25qdxBZeCKiX6BkVSZQjwWIr+IobgnZy+ag73tTZgZi7tr0LrBw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
"node_modules/fast-glob": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz",
@@ -6110,8 +6280,6 @@
},
"node_modules/frac": {
"version": "1.1.2",
- "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",
- "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
@@ -6123,7 +6291,7 @@
"integrity": "sha512-xryrmD4jSBQrS2IkMdcTmiS4aSKckbS7kLDCuhUn9110SQKG1w3zlq1RTqCblewg+ZYe+m3sdtzQA6cRwo5g8Q==",
"license": "MIT",
"dependencies": {
- "motion-dom": "^12.16.0",
+ "motion-dom": "^12.17.0",
"motion-utils": "^12.12.1",
"tslib": "^2.4.0"
},
@@ -6641,6 +6809,15 @@
"node": ">= 0.4"
}
},
+ "node_modules/internmap": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
+ "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/is-array-buffer": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
@@ -7142,8 +7319,6 @@
},
"node_modules/js-tokens": {
"version": "4.0.0",
- "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
- "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"license": "MIT"
},
"node_modules/js-yaml": {
@@ -7697,6 +7872,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/lodash": {
+ "version": "4.17.21",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
+ "license": "MIT"
+ },
"node_modules/lodash.includes": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
@@ -7814,8 +7995,6 @@
},
"node_modules/loose-envify": {
"version": "1.4.0",
- "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
- "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"license": "MIT",
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
@@ -8004,13 +8183,10 @@
"dev": true,
"license": "MIT",
"bin": {
- "mkdirp": "dist/cjs/src/bin.js"
+ "mkdirp": "bin/cmd.js"
},
"engines": {
"node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/monaco-editor": {
@@ -8242,8 +8418,6 @@
},
"node_modules/object-assign": {
"version": "4.1.1",
- "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
- "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@@ -8659,8 +8833,6 @@
},
"node_modules/prop-types": {
"version": "15.8.1",
- "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
- "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
"license": "MIT",
"dependencies": {
"loose-envify": "^1.4.0",
@@ -8917,6 +9089,16 @@
"node": ">=0.10.0"
}
},
+ "node_modules/react-chartjs-2": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.3.0.tgz",
+ "integrity": "sha512-UfZZFnDsERI3c3CZGxzvNJd02SHjaSJ8kgW1djn65H1KK8rehwTjyrRKOG3VTMG8wtHZ5rgAO5oTHtHi9GCCmw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "chart.js": "^4.1.1",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/react-day-picker": {
"version": "9.7.0",
"resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-9.7.0.tgz",
@@ -8962,8 +9144,6 @@
},
"node_modules/react-hook-form": {
"version": "7.57.0",
- "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.57.0.tgz",
- "integrity": "sha512-RbEks3+cbvTP84l/VXGUZ+JMrKOS8ykQCRYdm5aYsxnDquL0vspsyNhGRO7pcH6hsZqWlPOjLye7rJqdtdAmlg==",
"license": "MIT",
"engines": {
"node": ">=18.0.0"
@@ -8978,8 +9158,6 @@
},
"node_modules/react-is": {
"version": "16.13.1",
- "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
- "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"license": "MIT"
},
"node_modules/react-katex": {
@@ -9052,6 +9230,21 @@
"react-dom": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
}
},
+ "node_modules/react-smooth": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz",
+ "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==",
+ "license": "MIT",
+ "dependencies": {
+ "fast-equals": "^5.0.1",
+ "prop-types": "^15.8.1",
+ "react-transition-group": "^4.4.5"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/react-style-singleton": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
@@ -9643,8 +9836,6 @@
},
"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==",
"license": "Apache-2.0",
"dependencies": {
"frac": "~1.1.2"
@@ -10512,6 +10703,28 @@
"uuid": "dist/esm/bin/uuid"
}
},
+ "node_modules/victory-vendor": {
+ "version": "36.9.2",
+ "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz",
+ "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==",
+ "license": "MIT AND ISC",
+ "dependencies": {
+ "@types/d3-array": "^3.0.3",
+ "@types/d3-ease": "^3.0.0",
+ "@types/d3-interpolate": "^3.0.1",
+ "@types/d3-scale": "^4.0.2",
+ "@types/d3-shape": "^3.1.0",
+ "@types/d3-time": "^3.0.0",
+ "@types/d3-timer": "^3.0.0",
+ "d3-array": "^3.1.6",
+ "d3-ease": "^3.0.1",
+ "d3-interpolate": "^3.0.1",
+ "d3-scale": "^4.0.2",
+ "d3-shape": "^3.1.0",
+ "d3-time": "^3.0.0",
+ "d3-timer": "^3.0.1"
+ }
+ },
"node_modules/w3c-keyname": {
"version": "2.2.8",
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
@@ -10680,8 +10893,6 @@
},
"node_modules/wmf": {
"version": "1.0.2",
- "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz",
- "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
@@ -10689,8 +10900,6 @@
},
"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==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
@@ -10767,8 +10976,6 @@
},
"node_modules/xlsx": {
"version": "0.18.5",
- "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz",
- "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==",
"license": "Apache-2.0",
"dependencies": {
"adler-32": "~1.3.0",
diff --git a/package.json b/package.json
index 784c4a8..0f42840 100644
--- a/package.json
+++ b/package.json
@@ -47,6 +47,7 @@
"@tiptap/starter-kit": "^2.12.0",
"@types/jsonwebtoken": "^9.0.9",
"axios": "^1.9.0",
+ "chart.js": "^4.4.9",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
@@ -63,11 +64,13 @@
"next-auth": "^5.0.0-beta.28",
"next-themes": "^0.4.6",
"react": "^19.0.0",
+ "react-chartjs-2": "^5.3.0",
"react-day-picker": "^9.7.0",
"react-dom": "^19.0.0",
"react-hook-form": "^7.57.0",
"react-katex": "^3.1.0",
"react-resizable-panels": "^3.0.2",
+ "recharts": "^2.15.3",
"sonner": "^2.0.3",
"tailwind-merge": "^3.2.0",
"uuid": "^11.1.0",
diff --git a/src/app/(evalify)/(academics)/result/page.tsx b/src/app/(evalify)/(academics)/result/page.tsx
deleted file mode 100644
index a14bae3..0000000
--- a/src/app/(evalify)/(academics)/result/page.tsx
+++ /dev/null
@@ -1,9 +0,0 @@
-import React from "react";
-
-export default function page() {
- return (
-
-
Result Page
-
- );
-}
diff --git a/src/app/(test)/results-data/page.tsx b/src/app/(test)/results-data/page.tsx
new file mode 100644
index 0000000..c661c3d
--- /dev/null
+++ b/src/app/(test)/results-data/page.tsx
@@ -0,0 +1,10 @@
+"use client";
+
+// This file is a redirect. The student results functionality has been moved to:
+// src/app/(test)/student-results/page.tsx
+
+import { redirect } from "next/navigation";
+
+export default function ResultsDataPage() {
+ redirect("/student-results");
+}
diff --git a/src/app/(test)/results/page.tsx b/src/app/(test)/results/page.tsx
new file mode 100644
index 0000000..6e051f7
--- /dev/null
+++ b/src/app/(test)/results/page.tsx
@@ -0,0 +1,10 @@
+"use client";
+
+// This file is a redirect. The student results functionality has been moved to:
+// src/app/(test)/student-results/page.tsx
+
+import { redirect } from "next/navigation";
+
+export default function ResultsPage() {
+ redirect("/student-results");
+}
diff --git a/src/app/(test)/student-results/page.tsx b/src/app/(test)/student-results/page.tsx
new file mode 100644
index 0000000..1eb4d6d
--- /dev/null
+++ b/src/app/(test)/student-results/page.tsx
@@ -0,0 +1,208 @@
+"use client";
+
+import React, { useState, useEffect } from "react";
+import { useRouter, useSearchParams } from "next/navigation";
+import TopBar from "@/components/question_creation/top-bar";
+import {
+ StudentRecentTestsCard,
+ CourseResultsGrid,
+ TestSummaries,
+ StudentOverallResult,
+ CourseResult,
+ TestSummary,
+} from "@/components/results";
+import { MockResultsAPI } from "@/lib/results-api";
+
+export default function StudentResultsPage() {
+ const router = useRouter();
+ const searchParams = useSearchParams();
+ const [currentView, setCurrentView] = useState<
+ "overview" | "course" | "tests"
+ >("overview");
+ const [selectedCourseId, setSelectedCourseId] = useState(null);
+ const [studentData, setStudentData] = useState(
+ null,
+ );
+ const [courseResults, setCourseResults] = useState([]);
+ const [testSummaries, setTestSummaries] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ // Define functions before useEffect hooks
+ const loadInitialData = React.useCallback(async () => {
+ // Use a mock student ID since we're not requiring login
+ const mockStudentId = "student-1";
+
+ setLoading(true);
+ setError(null);
+
+ try {
+ // Load both student overview and course results in parallel
+ const [overview, courses] = await Promise.all([
+ MockResultsAPI.getStudentOverview(mockStudentId),
+ MockResultsAPI.getCourseResults(mockStudentId),
+ ]);
+
+ setStudentData(overview);
+ setCourseResults(courses);
+ } catch (err) {
+ console.error("Failed to load results data:", err);
+ setError("Failed to load results. Please try again.");
+ } finally {
+ setLoading(false);
+ }
+ }, []);
+
+ const loadTestSummaries = React.useCallback(async (courseId: string) => {
+ // Use a mock student ID since we're not requiring login
+ const mockStudentId = "student-1";
+
+ setLoading(true);
+ try {
+ const tests = await MockResultsAPI.getTestSummaries(
+ mockStudentId,
+ courseId,
+ );
+ setTestSummaries(tests);
+ } catch (err) {
+ console.error("Failed to load test summaries:", err);
+ setError("Failed to load test summaries. Please try again.");
+ } finally {
+ setLoading(false);
+ }
+ }, []);
+
+ // Handle URL parameters for navigation
+ useEffect(() => {
+ const view = searchParams.get("view");
+ const courseId = searchParams.get("courseId");
+
+ if (view === "course" && courseId) {
+ setCurrentView("course");
+ setSelectedCourseId(courseId);
+ loadTestSummaries(courseId);
+ } else if (view === "tests" && courseId) {
+ setCurrentView("tests");
+ setSelectedCourseId(courseId);
+ loadTestSummaries(courseId);
+ } else {
+ setCurrentView("overview");
+ setSelectedCourseId(null);
+ }
+ }, [searchParams, loadTestSummaries]);
+
+ // Load initial data
+ useEffect(() => {
+ // Load data immediately without waiting for session
+ loadInitialData();
+ }, [loadInitialData]);
+ const handleViewCourse = (courseId: string) => {
+ router.push(`/student-results?view=course&courseId=${courseId}`);
+ };
+
+ const handleViewTest = (testId: string) => {
+ router.push(`/student-results/test/${testId}`);
+ };
+ const handleBack = () => {
+ if (currentView === "course") {
+ router.push("/student-results");
+ }
+ };
+
+ const selectedCourse = courseResults.find(
+ (course) => course.courseId === selectedCourseId,
+ );
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ if (error) {
+ return (
+
+
+
+
+ Error Loading Results
+
+
{error}
+
+
+
+
+ );
+ }
+
+ if (!studentData) {
+ return (
+
+
+
+
No results data available
+
+
+
+ );
+ }
+ return (
+
+
+
+ {currentView === "overview" && (
+ <>
+ {/* Student Overview */}
+
+
My Results
+
+ Track your academic performance and progress
+
+ {/* Overview cards removed as requested */}
+
{" "}
+ {/* Recent Tests */}
+
+
Recent Tests
+
+
+ {/* Course Results */}
+
+
Course Results
+
+
+ >
+ )}
+ {currentView === "course" && selectedCourse && (
+
+ )}
+
+
+ );
+}
diff --git a/src/app/(test)/student-results/test/[testId]/page.tsx b/src/app/(test)/student-results/test/[testId]/page.tsx
new file mode 100644
index 0000000..19d1f24
--- /dev/null
+++ b/src/app/(test)/student-results/test/[testId]/page.tsx
@@ -0,0 +1,98 @@
+"use client";
+
+import React, { useState, useEffect } from "react";
+import { useRouter, useParams } from "next/navigation";
+import TopBar from "@/components/question_creation/top-bar";
+import {
+ DetailedTestResultView,
+ DetailedTestResult,
+} from "@/components/results";
+import { MockResultsAPI } from "@/lib/results-api";
+
+export default function TestResultPage() {
+ const router = useRouter();
+ const params = useParams();
+ const [result, setResult] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ const loadTestResult = async () => {
+ // Use a mock student ID since we're not requiring login
+ const mockStudentId = "student-1";
+
+ try {
+ setLoading(true);
+ setError(null);
+ const testId = params.testId as string;
+
+ const data = await MockResultsAPI.getTestResult(mockStudentId, testId);
+ setResult(data);
+ } catch (err) {
+ console.error("Failed to load test result:", err);
+ setError("Failed to load test result. Please try again.");
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ if (params.testId) {
+ loadTestResult();
+ }
+ }, [params.testId]);
+
+ const handleBack = () => {
+ router.back();
+ };
+
+ if (loading) {
+ return (
+
+
+
+
+
+
+
Loading test result...
+
+
+
+
+ );
+ }
+
+ if (error || !result) {
+ return (
+
+
+
+
+
+
+ Error Loading Test Result
+
+
+ {error || "Test result not found"}
+
+
+
+
+
+
+ );
+ }
+
+ return (
+
+ );
+}
diff --git a/src/app/(test)/teacher-results/page.tsx b/src/app/(test)/teacher-results/page.tsx
new file mode 100644
index 0000000..fb3be6b
--- /dev/null
+++ b/src/app/(test)/teacher-results/page.tsx
@@ -0,0 +1,573 @@
+"use client";
+
+import React, { useState, useEffect } from "react";
+import { useRouter, useSearchParams } from "next/navigation";
+import TopBar from "@/components/question_creation/top-bar";
+import {
+ TeacherCourseOverview,
+ TestOverview,
+ DetailedTestStatistics,
+} from "@/components/results/teacher/types";
+import { MockTeacherResultsAPI } from "@/lib/results-api";
+import {
+ CoursesGrid,
+ RecentTestsCard,
+ CourseTestsTable,
+ PerformanceDistributionChart,
+ StudentResultsTable,
+ QuestionStatsTable,
+ SortOption,
+ SortDropdown,
+} from "@/components/results/teacher";
+import { DetailedTestResultView } from "@/components/results/common/detailed-test-result";
+import { Button } from "@/components/ui/button";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import {
+ ChevronLeft,
+ Users,
+ Award,
+ BookOpen,
+ Clock,
+ FileBarChart,
+} from "lucide-react";
+import { Separator } from "@/components/ui/separator";
+import { toast } from "sonner";
+
+export default function TeacherResultsPage() {
+ const router = useRouter();
+ const searchParams = useSearchParams();
+ const [currentView, setCurrentView] = useState<
+ "overview" | "course" | "test" | "student-detail"
+ >("overview");
+ const [selectedCourseId, setSelectedCourseId] = useState(null);
+ const [selectedTestId, setSelectedTestId] = useState(null);
+ const [selectedStudentId, setSelectedStudentId] = useState(
+ null,
+ );
+
+ const [coursesList, setCoursesList] = useState([]);
+ const [recentTests, setRecentTests] = useState([]);
+ const [courseTests, setCourseTests] = useState([]);
+ const [testStatistics, setTestStatistics] =
+ useState(null);
+ const [courseSortOption, setCourseSortOption] =
+ useState("latest");
+
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ // Load initial data (courses and recent tests)
+ const loadInitialData = React.useCallback(async () => {
+ const mockTeacherId = "teacher-1"; // In a real app, get from auth context
+
+ setLoading(true);
+ setError(null);
+
+ try {
+ // Load both teacher courses and recent tests in parallel
+ const [courses, tests] = await Promise.all([
+ MockTeacherResultsAPI.getTeacherCourses(mockTeacherId),
+ MockTeacherResultsAPI.getRecentTests(mockTeacherId, 3),
+ ]);
+
+ setCoursesList(courses);
+ setRecentTests(tests);
+ } catch (err) {
+ console.error("Failed to load teacher results data:", err);
+ setError("Failed to load results. Please try again.");
+ toast.error("Failed to load results data", {
+ description: "Failed to load results. Please try again.",
+ });
+ } finally {
+ setLoading(false);
+ }
+ }, []);
+
+ // Load course tests when a course is selected
+ const loadCourseTests = React.useCallback(async (courseId: string) => {
+ setLoading(true);
+
+ try {
+ const tests = await MockTeacherResultsAPI.getCourseTests(courseId);
+ setCourseTests(tests);
+ } catch (err) {
+ console.error("Failed to load course tests:", err);
+ setError("Failed to load course tests. Please try again.");
+ toast.error("Failed to load course tests", {
+ description: "Please try again",
+ });
+ } finally {
+ setLoading(false);
+ }
+ }, []);
+
+ // Load test statistics when a test is selected
+ const loadTestStatistics = React.useCallback(async (testId: string) => {
+ setLoading(true);
+
+ try {
+ const stats = await MockTeacherResultsAPI.getTestStatistics(testId);
+ setTestStatistics(stats);
+ } catch (err) {
+ console.error("Failed to load test statistics:", err);
+ setError("Failed to load test statistics. Please try again.");
+ toast.error("Failed to load test statistics", {
+ description: "Please try again",
+ });
+ } finally {
+ setLoading(false);
+ }
+ }, []);
+
+ // Handle navigation from URL parameters
+ useEffect(() => {
+ const view = searchParams.get("view");
+ const courseId = searchParams.get("courseId");
+ const testId = searchParams.get("testId");
+
+ if (view === "test" && testId) {
+ setCurrentView("test");
+ setSelectedTestId(testId);
+ loadTestStatistics(testId);
+ } else if (view === "course" && courseId) {
+ setCurrentView("course");
+ setSelectedCourseId(courseId);
+ loadCourseTests(courseId);
+ } else {
+ setCurrentView("overview");
+ setSelectedCourseId(null);
+ setSelectedTestId(null);
+ }
+ }, [searchParams, loadCourseTests, loadTestStatistics]);
+
+ // Load initial data
+ useEffect(() => {
+ loadInitialData();
+ }, [loadInitialData]);
+
+ // Navigation handlers
+ const handleViewCourse = (courseId: string) => {
+ router.push(`/teacher-results?view=course&courseId=${courseId}`);
+ };
+
+ const handleViewTest = (testId: string) => {
+ router.push(`/teacher-results?view=test&testId=${testId}`);
+ };
+ const handleBack = () => {
+ if (currentView === "student-detail") {
+ setCurrentView("test");
+ setSelectedStudentId(null);
+ } else if (currentView === "test" && selectedCourseId) {
+ router.push(`/teacher-results?view=course&courseId=${selectedCourseId}`);
+ } else {
+ router.push("/teacher-results");
+ }
+ };
+ const handleViewStudentResult = (studentId: string) => {
+ setSelectedStudentId(studentId);
+ setCurrentView("student-detail");
+ };
+
+ // Find selected course
+ const selectedCourse = coursesList.find(
+ (course) => course.courseId === selectedCourseId,
+ );
+ // Use the selected test ID in a meaningful way - creating document title
+ // and displaying test ID for debugging purposes when in test view
+ useEffect(() => {
+ // Update document title based on the current view
+ if (currentView === "test" && selectedTestId && testStatistics) {
+ document.title = `Test: ${testStatistics.testName} | Teacher Results`;
+ console.log(`Viewing test with ID: ${selectedTestId}`);
+ } else if (currentView === "course" && selectedCourse) {
+ document.title = `Course: ${selectedCourse.courseName} | Teacher Results`;
+ } else {
+ document.title = "Teacher Results Dashboard";
+ }
+
+ return () => {
+ document.title = "Evalify";
+ };
+ }, [currentView, selectedTestId, selectedCourse, testStatistics]);
+
+ // Loading state
+ if (loading) {
+ return (
+
+ );
+ }
+
+ // Error state
+ if (error) {
+ return (
+
+
+
+
+ Error Loading Results
+
+
{error}
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+ {/* Overview Page */}
+ {currentView === "overview" && (
+ <>
+
+
+
+
+
+
Test Results & Analytics
+
+
+ Monitor student performance and test statistics across all your
+ courses
+
+
+
+ {/* Stats Summary Cards */}
+
+
+
+
+
+
+
+
+ Total Students
+
+
+ {coursesList.reduce(
+ (sum, course) => sum + course.totalStudents,
+ 0,
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+ Active Courses
+
+
+ {coursesList.length}
+
+
+
+
+
+
+
+
+
+
+ Avg Course Score
+
+
+ {coursesList.length > 0
+ ? (
+ coursesList.reduce(
+ (sum, course) => sum + course.averageScore,
+ 0,
+ ) / coursesList.length
+ ).toFixed(1)
+ : 0}
+ %
+
+
+
+
+
+
+
+
+
+
+
+
+ Recent Tests
+
+
+ {recentTests.length}
+
+
+
+
+
+ {/* Recent Tests */}
+
+
+
+
Recent Tests
+
+
+ Recently administered tests across all courses
+
+
+
{" "}
+ {/* All Courses */}
+
+
+
+
+
Your Courses
+ {" "}
+
+
+ Sort by:
+
+ setCourseSortOption(value)}
+ />
+
+
+
+ Performance analytics for all your courses
+
+
+
+ >
+ )}
+
+ {/* Course Page */}
+ {currentView === "course" && selectedCourse && (
+ <>
+
+
+
+
+ {selectedCourse.courseName}
+
+
+ {selectedCourse.courseCode} • Performance Analytics
+
+
+
+
+ {/* Course Summary Card */}
+
+
+ Course Summary
+
+
+
+
+
+
+
Students
+
+ {selectedCourse.totalStudents}
+
+
+
+
+
+
+
Tests
+
+ {selectedCourse.totalTests}
+
+
+
+
+
+
+
+ Average Score
+
+
+ {selectedCourse.averageScore.toFixed(1)}%
+
+
+
+
+
+
+
+ Completion Rate
+
+
+ {selectedCourse.completionRate}%
+
+
+
+
+
+ {" "}
+ {/* Course Tests */}
+
+
+
+
Test History
+
+ All tests administered for this course
+
+
+
+
+
+
+
+ >
+ )}
+
+ {/* Test Details Page */}
+ {currentView === "test" && testStatistics && (
+ <>
+
+
+
+
+ {testStatistics.testName}
+
+
+ {testStatistics.courseName} ({testStatistics.courseCode}) •
+ Test Analytics
+
+
+
+
+ {/* Test Summary */}
+
+
+ Test Overview
+
+
+
+
+
+
+
+ Average Score
+
+
+ {testStatistics.averageScore.toFixed(1)}%
+
+
+
+
+
+
+
+ Submissions
+
+
+ {testStatistics.totalSubmissions}
+
+
+
+
+
+
+
+ High / Low
+
+
+ {testStatistics.highestScore}% /{" "}
+ {testStatistics.lowestScore}%
+
+
+
+
+
+
+
+ Median Score
+
+
+ {testStatistics.medianScore}%
+
+
+
+
+
+ {" "}
+ {/* Performance Distribution Chart */}
+
+
Score Distribution
+
+ Student performance across different score ranges
+
+
+
+
+ {/* Student Results Table */}
+
+
Student Performance
+
+ Individual student scores and statistics
+
+
{" "}
+
+
+ {/* Question Stats Table */}
+
+
Question Analysis
+
+ Performance metrics for each question on the test
+
+
{" "}
+
+ >
+ )}
+
+ {currentView === "student-detail" && selectedStudentId && (
+
+ )}
+
+
+ );
+}
diff --git a/src/components/navigation/side-navbar/side-navbar.tsx b/src/components/navigation/side-navbar/side-navbar.tsx
index 112024c..0835de9 100644
--- a/src/components/navigation/side-navbar/side-navbar.tsx
+++ b/src/components/navigation/side-navbar/side-navbar.tsx
@@ -61,7 +61,7 @@ const academicsItems = [
},
{
title: "Results",
- url: "/result",
+ url: "/student-results",
icon: Trophy,
},
];
@@ -72,6 +72,11 @@ const administrationItems = [
url: "/user",
icon: Users,
},
+ {
+ title: "Teacher Results",
+ url: "/teacher-results",
+ icon: BarChart3,
+ },
{
title: "Batches",
url: "/batch",
diff --git a/src/components/question_creation/question-editor.tsx b/src/components/question_creation/question-editor.tsx
index 64d7fb4..f3ef169 100644
--- a/src/components/question_creation/question-editor.tsx
+++ b/src/components/question_creation/question-editor.tsx
@@ -141,7 +141,6 @@ const QuestionEditor: React.FC = ({
type: "true-false",
correctAnswer: null,
};
-
case "fillup":
return {
...baseData,
@@ -279,7 +278,6 @@ const QuestionEditor: React.FC = ({
);
}
break;
-
case "fillup":
if (questionData.type === "fillup") {
return (
@@ -288,6 +286,8 @@ const QuestionEditor: React.FC = ({
blanks={questionData.blanks}
explanation={questionData.explanation}
showExplanation={questionData.showExplanation}
+ strictMatch={questionData.strictMatch}
+ useHybridEvaluation={questionData.useHybridEvaluation}
onQuestionChange={(question) => updateData({ question })}
onBlanksChange={(blanks) => updateData({ blanks })}
onExplanationChange={(explanation) => updateData({ explanation })}
@@ -298,8 +298,6 @@ const QuestionEditor: React.FC = ({
onUseHybridEvaluationChange={(useHybridEvaluation) =>
updateData({ useHybridEvaluation })
}
- strictMatch={questionData.strictMatch}
- useHybridEvaluation={questionData.useHybridEvaluation}
/>
);
}
diff --git a/src/components/question_creation/question-settings.tsx b/src/components/question_creation/question-settings.tsx
index 36575ed..8f3e576 100644
--- a/src/components/question_creation/question-settings.tsx
+++ b/src/components/question_creation/question-settings.tsx
@@ -133,8 +133,7 @@ const QuestionSettings = ({
/>
{availableTopics.length > 0 && (
-
-
+
@@ -157,8 +156,7 @@ const QuestionSettings = ({
allowMultiple={true}
/>
- )}
-
+ )}
diff --git a/src/components/question_creation/question-types/fillup-question.tsx b/src/components/question_creation/question-types/fillup-question.tsx
index e2626af..3647bb5 100644
--- a/src/components/question_creation/question-types/fillup-question.tsx
+++ b/src/components/question_creation/question-types/fillup-question.tsx
@@ -151,7 +151,6 @@ const FillupQuestion: React.FC = ({
error("Editor is not ready. Please try again.");
return;
}
-
const editor = editorRef.current.editor;
if (!editor) {
console.error(
@@ -161,9 +160,7 @@ const FillupQuestion: React.FC = ({
"Editor is not initialized. Please refresh the page and try again.",
);
return;
- }
-
- // Check if editor is destroyed or not ready
+ } // Check if editor is destroyed or not ready
if (editor.isDestroyed) {
console.error("Cannot insert blank - editor has been destroyed");
error("Editor is no longer available. Please refresh the page.");
@@ -200,9 +197,7 @@ const FillupQuestion: React.FC = ({
console.error("Editor state:", {
isDestroyed: editor.isDestroyed,
isFocused: editor.isFocused,
- });
-
- // Show user-friendly error message
+ }); // Show user-friendly error message
error("Failed to insert blank. Please try again or refresh the page.");
}
};
@@ -234,7 +229,6 @@ const FillupQuestion: React.FC = ({
isUpdating.current = false;
}, 100);
};
-
const removeAnswerFromBlank = (blankId: string, answerToRemove: string) => {
// Set updating flag to prevent blank detection during answer removal
isUpdating.current = true;
@@ -349,7 +343,7 @@ const FillupQuestion: React.FC = ({
-
+ {" "}
= ({
onCheckedChange={onShowExplanationChange}
/>
-
+ {" "}
{showExplanation && (
= ({
-
+
= ({
: "border-border hover:border-primary/30"
}`}
onClick={() => onCorrectAnswerChange(true)}
+ onKeyDown={(e) => {
+ if (e.key === "Enter" || e.key === " ") {
+ e.preventDefault();
+ onCorrectAnswerChange(true);
+ }
+ }}
>
@@ -76,6 +86,12 @@ const TrueFalseQuestion: React.FC
= ({
: "border-border hover:border-primary/30"
}`}
onClick={() => onCorrectAnswerChange(false)}
+ onKeyDown={(e) => {
+ if (e.key === "Enter" || e.key === " ") {
+ e.preventDefault();
+ onCorrectAnswerChange(false);
+ }
+ }}
>