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 ( +
+
+
+
+

Loading results...

+
+
+
+ ); + } + + 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 ( +
+
+
+
+

Loading results...

+
+
+
+ ); + } + + // 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); + } + }} >