diff --git a/package-lock.json b/package-lock.json index e8c17ef2..c5b0a838 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "@radix-ui/react-dropdown-menu": "^2.1.7", "@radix-ui/react-label": "^2.1.7", "@radix-ui/react-popover": "^1.1.15", + "@radix-ui/react-progress": "^1.1.8", "@radix-ui/react-radio-group": "^1.3.7", "@radix-ui/react-scroll-area": "^1.2.9", "@radix-ui/react-select": "^2.2.5", @@ -32,6 +33,7 @@ "@supabase/supabase-js": "^2.49.1", "@tailwindcss/vite": "^4.0.14", "@tanstack/react-query": "^5.81.5", + "@tanstack/react-table": "^8.21.3", "axios": "^1.8.4", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -154,6 +156,7 @@ "integrity": "sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -702,6 +705,7 @@ "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", "license": "MIT", + "peer": true, "dependencies": { "@dnd-kit/accessibility": "^3.1.1", "@dnd-kit/utilities": "^3.2.2", @@ -1383,7 +1387,6 @@ "resolved": "https://registry.npmjs.org/@gerrit0/mini-shiki/-/mini-shiki-3.7.0.tgz", "integrity": "sha512-7iY9wg4FWXmeoFJpUL2u+tsmh0d0jcEJHAIzVxl3TG4KL493JNnisdLAILZ77zcD+z3J0keEXZ+lFzUgzQzPDg==", "license": "MIT", - "peer": true, "dependencies": { "@shikijs/engine-oniguruma": "^3.7.0", "@shikijs/langs": "^3.7.0", @@ -2014,8 +2017,7 @@ "version": "0.3.4", "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", @@ -2753,6 +2755,86 @@ } } }, + "node_modules/@radix-ui/react-progress": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.8.tgz", + "integrity": "sha512-+gISHcSPUJ7ktBy9RnTqbdKW78bcGke3t6taawyZ71pio1JewwGSJizycs7rLhGTvMJYCQB1DBK4KQsxs7U8dA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.3", + "@radix-ui/react-primitive": "2.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress/node_modules/@radix-ui/react-context": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.3.tgz", + "integrity": "sha512-ieIFACdMpYfMEjF0rEf5KLvfVyIkOz6PDGyNnP+u+4xQ6jny3VCgA4OgXOwNx2aUkxn8zx9fiVcM8CfFYv9Lxw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress/node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress/node_modules/@radix-ui/react-slot": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", + "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-radio-group": { "version": "1.3.7", "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.3.7.tgz", @@ -3512,7 +3594,6 @@ "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.7.0.tgz", "integrity": "sha512-5BxcD6LjVWsGu4xyaBC5bu8LdNgPCVBnAkWTtOCs/CZxcB22L8rcoWfv7Hh/3WooVjBZmFtyxhgvkQFedPGnFw==", "license": "MIT", - "peer": true, "dependencies": { "@shikijs/types": "3.7.0", "@shikijs/vscode-textmate": "^10.0.2" @@ -3523,7 +3604,6 @@ "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.7.0.tgz", "integrity": "sha512-1zYtdfXLr9xDKLTGy5kb7O0zDQsxXiIsw1iIBcNOO8Yi5/Y1qDbJ+0VsFoqTlzdmneO8Ij35g7QKF8kcLyznCQ==", "license": "MIT", - "peer": true, "dependencies": { "@shikijs/types": "3.7.0" } @@ -3533,7 +3613,6 @@ "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.7.0.tgz", "integrity": "sha512-VJx8497iZPy5zLiiCTSIaOChIcKQwR0FebwE9S3rcN0+J/GTWwQ1v/bqhTbpbY3zybPKeO8wdammqkpXc4NVjQ==", "license": "MIT", - "peer": true, "dependencies": { "@shikijs/types": "3.7.0" } @@ -3543,7 +3622,6 @@ "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.7.0.tgz", "integrity": "sha512-MGaLeaRlSWpnP0XSAum3kP3a8vtcTsITqoEPYdt3lQG3YCdQH4DnEhodkYcNMcU0uW0RffhoD1O3e0vG5eSBBg==", "license": "MIT", - "peer": true, "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" @@ -3553,8 +3631,7 @@ "version": "10.0.2", "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@sinclair/typebox": { "version": "0.27.8", @@ -3963,6 +4040,39 @@ "react": "^18 || ^19" } }, + "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" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "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" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@testing-library/dom": { "version": "10.4.0", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", @@ -4111,8 +4221,7 @@ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -4187,7 +4296,6 @@ "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", "license": "MIT", - "peer": true, "dependencies": { "@types/unist": "*" } @@ -4289,6 +4397,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.32.tgz", "integrity": "sha512-3jigKqgSjsH6gYZv2nEsqdXfZqIFGAV36XYYjf9KGZ3PSG+IhLecqPnI310RvjutyMwifE2hhhNEklOUrvx/wA==", "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -4315,6 +4424,7 @@ "integrity": "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -4325,6 +4435,7 @@ "integrity": "sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw==", "devOptional": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.0.0" } @@ -4347,8 +4458,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/ws": { "version": "8.18.1", @@ -4422,6 +4532,7 @@ "integrity": "sha512-6sMvZePQrnZH2/cJkwRpkT7DxoAWh+g6+GFRK6bV3YQo7ogi3SX5rgF6099r5Q53Ma5qeT7LGmOmuIutF4t3lA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.35.0", "@typescript-eslint/types": "8.35.0", @@ -4681,6 +4792,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "devOptional": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5081,6 +5193,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001718", "electron-to-chromium": "^1.5.160", @@ -5730,8 +5843,7 @@ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/domexception": { "version": "4.0.0", @@ -5801,7 +5913,8 @@ "version": "8.6.0", "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz", "integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/embla-carousel-react": { "version": "8.6.0", @@ -6016,6 +6129,7 @@ "integrity": "sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -6077,6 +6191,7 @@ "integrity": "sha512-zc1UmCpNltmVY34vuLRV61r1K27sWuX39E+uyUnY8xS2Bex88VV9cugG+UZbRSRGtGyFboj+D8JODyme1plMpw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -7394,6 +7509,7 @@ "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -8790,7 +8906,6 @@ "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", "license": "MIT", - "peer": true, "dependencies": { "uc.micro": "^2.0.0" } @@ -8863,8 +8978,7 @@ "version": "2.3.9", "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/lz-string": { "version": "1.5.0", @@ -8872,7 +8986,6 @@ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -8924,7 +9037,6 @@ "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", "license": "MIT", - "peer": true, "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", @@ -8941,15 +9053,13 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "license": "Python-2.0", - "peer": true + "license": "Python-2.0" }, "node_modules/markdown-it/node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", "license": "BSD-2-Clause", - "peer": true, "engines": { "node": ">=0.12" }, @@ -8970,8 +9080,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/merge-stream": { "version": "2.0.0", @@ -9524,6 +9633,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -9664,6 +9774,7 @@ "integrity": "sha512-ujSB9uXHJKzM/2GBuE0hBOUgC77CN3Bnpqa+g80bkv3T3A93wL/xlzDATHhnhkzifz/UE2SNOvmbTz5hSkDlHw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -9693,7 +9804,6 @@ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -9709,7 +9819,6 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10" }, @@ -9765,7 +9874,6 @@ "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", "license": "MIT", - "peer": true, "engines": { "node": ">=6" } @@ -9825,6 +9933,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -9876,6 +9985,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.26.0" }, @@ -9888,6 +9998,7 @@ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.58.1.tgz", "integrity": "sha512-Lml/KZYEEFfPhUVgE0RdCVpnC4yhW+PndRhbiTtdvSlQTL8IfVR+iQkBjLIvmmc6+GGoVeM11z37ktKFPAb0FA==", "license": "MIT", + "peer": true, "engines": { "node": ">=18.0.0" }, @@ -9913,8 +10024,7 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/react-refresh": { "version": "0.17.0", @@ -10664,6 +10774,7 @@ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", "license": "MIT", + "peer": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -10820,6 +10931,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -11005,6 +11117,7 @@ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -11161,7 +11274,6 @@ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "license": "MIT", - "peer": true, "dependencies": { "balanced-match": "^1.0.0" } @@ -11171,7 +11283,6 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "license": "ISC", - "peer": true, "dependencies": { "brace-expansion": "^2.0.1" }, @@ -11187,6 +11298,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -11222,8 +11334,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/undici-types": { "version": "6.21.0", @@ -11377,6 +11488,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -11484,6 +11596,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, diff --git a/package.json b/package.json index eeaf262c..3a92ea91 100755 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "@radix-ui/react-dropdown-menu": "^2.1.7", "@radix-ui/react-label": "^2.1.7", "@radix-ui/react-popover": "^1.1.15", + "@radix-ui/react-progress": "^1.1.8", "@radix-ui/react-radio-group": "^1.3.7", "@radix-ui/react-scroll-area": "^1.2.9", "@radix-ui/react-select": "^2.2.5", @@ -38,6 +39,7 @@ "@supabase/supabase-js": "^2.49.1", "@tailwindcss/vite": "^4.0.14", "@tanstack/react-query": "^5.81.5", + "@tanstack/react-table": "^8.21.3", "axios": "^1.8.4", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", diff --git a/src/.test/api/classes.test.ts b/src/.test/api/classes.test.ts index c25a0878..6ea1e843 100755 --- a/src/.test/api/classes.test.ts +++ b/src/.test/api/classes.test.ts @@ -15,7 +15,7 @@ jest.mock("../../api/endpoints", () => ({ LOG_ENDPOINT: "http://mock-api/logs", })); -jest.mock("../../supabaseClient", () => { +jest.mock("../../lib/supabaseClient.ts", () => { return { supabase: { from: jest.fn(() => ({ @@ -124,7 +124,7 @@ describe("class-api", () => { const result = await updateStudentEnrollmentStatus( "class-1", "student-1", - "ENROLLED" as EnrollmentStatus + "ENROLLED" as EnrollmentStatus, ); expect(result.success).toBe(true); @@ -222,7 +222,7 @@ describe("class-api", () => { const result = await getClassActivityByClassId("class-id"); expect(result.error).toBe( - "Invalid response: expected an array of activity logs" + "Invalid response: expected an array of activity logs", ); }); @@ -233,7 +233,7 @@ describe("class-api", () => { eqMock.mockReturnValueOnce(errorResult); // for .eq("student_id", ...) // eslint-disable-next-line @typescript-eslint/no-require-imports - require("../../supabaseClient").supabase.from = jest.fn(() => ({ + require("../../lib/supabaseClient.ts").supabase.from = jest.fn(() => ({ update: jest.fn(() => ({ eq: jest.fn().mockReturnValue({ eq: eqMock, @@ -244,7 +244,7 @@ describe("class-api", () => { const result = await updateStudentEnrollmentStatus( "c1", "s1", - EnrollmentStatus.ENROLLED + EnrollmentStatus.ENROLLED, ); expect(result.success).toBe(false); diff --git a/src/.test/hooks/useAuth.test.ts b/src/.test/hooks/useAuth.test.ts index 9d529a91..d8d8a54d 100755 --- a/src/.test/hooks/useAuth.test.ts +++ b/src/.test/hooks/useAuth.test.ts @@ -11,7 +11,7 @@ import { import { useLocation, useNavigate } from "react-router-dom"; // Mock Supabase client -jest.mock("../../supabaseClient", () => { +jest.mock("../../lib/supabaseClient.ts", () => { return { supabase: { auth: { @@ -27,7 +27,7 @@ jest.mock("react-router-dom", () => ({ useLocation: jest.fn(), })); -import { supabase } from "../../supabaseClient"; +import { supabase } from "../../lib/supabaseClient.ts"; const mockSupabaseAuth = supabase.auth as jest.Mocked; const mockUseNavigate = useNavigate as jest.Mock; const mockUseLocation = useLocation as jest.Mock; @@ -79,7 +79,7 @@ describe("useAuth", () => { subscription: mockSubscription, }, }; - } + }, ); }); @@ -116,7 +116,7 @@ describe("useAuth", () => { it("should handle auth state changes", async () => { let capturedCallback: ( event: AuthChangeEvent, - session: Session | null + session: Session | null, ) => void; mockSupabaseAuth.onAuthStateChange.mockImplementation((cb) => { @@ -163,7 +163,7 @@ describe("useAuth", () => { it("should handle null session from auth state change", async () => { let capturedCallback: ( event: AuthChangeEvent, - session: Session | null + session: Session | null, ) => void; mockSupabaseAuth.onAuthStateChange.mockImplementation((cb) => { @@ -189,7 +189,7 @@ describe("useAuth", () => { it("should handle unexpected getSession error gracefully", async () => { mockSupabaseAuth.getSession.mockRejectedValue( - new Error("Failed to fetch session") + new Error("Failed to fetch session"), ); mockSupabaseAuth.onAuthStateChange.mockImplementation(() => ({ diff --git a/src/.test/hooks/useInstructorClasses.test.ts b/src/.test/hooks/useInstructorClasses.test.ts index eb411526..5b8f37ec 100755 --- a/src/.test/hooks/useInstructorClasses.test.ts +++ b/src/.test/hooks/useInstructorClasses.test.ts @@ -24,7 +24,7 @@ const supabaseMocks: { }; // Supabase module mock -jest.mock("../../supabaseClient", () => ({ +jest.mock("../../lib/supabaseClient.ts", () => ({ supabase: { from: (...args: any[]) => supabaseMocks.mockFrom(...args), select: (...args: any[]) => supabaseMocks.mockSelect(...args), diff --git a/src/.test/hooks/useUserClasses.test.ts b/src/.test/hooks/useUserClasses.test.ts index 080ca0ee..702c3103 100755 --- a/src/.test/hooks/useUserClasses.test.ts +++ b/src/.test/hooks/useUserClasses.test.ts @@ -18,7 +18,7 @@ jest.mock("../../hooks/useAuth", () => ({ useAuth: jest.fn(), })); -jest.mock("../../supabaseClient", () => ({ +jest.mock("../../lib/supabaseClient.ts", () => ({ supabase: { from: jest.fn(), }, @@ -28,7 +28,8 @@ const mockUseAuth = require("../../hooks/useAuth").useAuth as jest.Mock; const mockGetUserClasses = getUserClasses as jest.MockedFunction< typeof getUserClasses >; -const mockFrom = require("../../supabaseClient").supabase.from as jest.Mock; +const mockFrom = require("../../lib/supabaseClient.ts").supabase + .from as jest.Mock; const mockClassInfo: UserClassInfo = { user_class: { @@ -97,7 +98,7 @@ describe("useUserClasses", () => { expect(result.current.selectedClassId).toBe("class-1"); expect(result.current.selectedClassType).toBe("class"); expect(result.current.selectedClass?.user_class.class_title).toBe( - "Physics" + "Physics", ); }); @@ -135,7 +136,7 @@ describe("useUserClassStatus", () => { }); const { result } = renderHook(() => - useUserClassStatus("student-1", "class-1") + useUserClassStatus("student-1", "class-1"), ); await waitFor(() => expect(result.current.loading).toBe(false)); @@ -164,7 +165,7 @@ describe("useUserClassStatus", () => { }); const { result } = renderHook(() => - useUserClassStatus("student-1", "class-1") + useUserClassStatus("student-1", "class-1"), ); await waitFor(() => expect(result.current.loading).toBe(false)); @@ -187,7 +188,7 @@ describe("useUserClassStatus", () => { }); const { result } = renderHook(() => - useUserClassStatus("student-1", "class-1") + useUserClassStatus("student-1", "class-1"), ); await waitFor(() => expect(result.current.loading).toBe(false)); @@ -220,7 +221,7 @@ describe("useUserClassStatus", () => { expect(result.current.selectedClassId).toBe("non-class"); expect(result.current.selectedClassType).toBe("non-class"); expect(result.current.selectedClass?.user_class.class_title).toBe( - "Non-class Activities" + "Non-class Activities", ); }); diff --git a/src/api/endpoints.ts b/src/api/endpoints.ts index 2652ef06..37243e90 100755 --- a/src/api/endpoints.ts +++ b/src/api/endpoints.ts @@ -1,6 +1,6 @@ /** Gets the endpoint from the .env if it is available otherwise it uses localhost */ export const BASE_ENDPOINT = - import.meta.env.VITE_API_URL || "http://127.0.0.1:8080/api/v1"; + import.meta.env.VITE_API_URL || "http://127.0.0.1:8080/api/v2"; /* Endpoint for creating new AI suggestions */ export const AI_SUGGESTION_ENDPOINT: string = `${BASE_ENDPOINT}/suggestion`; diff --git a/src/components/CustomSelect.tsx b/src/components/CustomSelect.tsx index 28ba760d..bedd176f 100644 --- a/src/components/CustomSelect.tsx +++ b/src/components/CustomSelect.tsx @@ -38,8 +38,8 @@ const CustomSelect = ({ > diff --git a/src/components/NavBar.tsx b/src/components/NavBar.tsx index 82473633..2c512b67 100755 --- a/src/components/NavBar.tsx +++ b/src/components/NavBar.tsx @@ -14,7 +14,7 @@ import { DoorOpen, Menu, User2 } from "lucide-react"; import { UserRole } from "@/types/user"; import { useUser } from "@/context/UserContext"; import UserAvatar from "./UserAvatar"; -import { supabase } from "@/supabaseClient"; +import { supabase } from "@/lib/supabaseClient.ts"; import CloverLogo from "./CloverLogo"; /** diff --git a/src/components/ResetPasswordButton.tsx b/src/components/ResetPasswordButton.tsx index 9ee4e8c3..28dd1f7e 100644 --- a/src/components/ResetPasswordButton.tsx +++ b/src/components/ResetPasswordButton.tsx @@ -1,7 +1,7 @@ import { useState } from "react"; import { useForm } from "react-hook-form"; import { updateUserPassword } from "../api/user"; -import { supabase } from "../supabaseClient"; +import { supabase } from "../lib/supabaseClient.ts"; import { Dialog, DialogContent, @@ -40,7 +40,7 @@ export const ResetPasswordButton = ({ userId }: { userId?: string }) => { if (userId) { const { error } = await updateUserPassword( userId, - form.getValues("newPassword") + form.getValues("newPassword"), ); if (error) { diff --git a/src/components/TypingLogs.tsx b/src/components/TypingLogs.tsx index ba956679..bca6c341 100644 --- a/src/components/TypingLogs.tsx +++ b/src/components/TypingLogs.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useState } from "react"; -import { supabase } from "@/supabaseClient"; +import { supabase } from "@/lib/supabaseClient.ts"; import { User } from "@/types/user"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import CustomSelect from "@/components/CustomSelect"; diff --git a/src/components/TypingLogsDownloadButton.tsx b/src/components/TypingLogsDownloadButton.tsx index ad8be481..3ac55bf7 100644 --- a/src/components/TypingLogsDownloadButton.tsx +++ b/src/components/TypingLogsDownloadButton.tsx @@ -1,5 +1,5 @@ import DownloadFormattedFile from "@/components/DownloadFormattedFile"; -import { supabase } from "@/supabaseClient"; +import { supabase } from "@/lib/supabaseClient.ts"; import { User } from "@/types/user"; import { useEffect, useState } from "react"; diff --git a/src/components/ui/progress.tsx b/src/components/ui/progress.tsx new file mode 100644 index 00000000..3fd47ada --- /dev/null +++ b/src/components/ui/progress.tsx @@ -0,0 +1,26 @@ +import * as React from "react" +import * as ProgressPrimitive from "@radix-ui/react-progress" + +import { cn } from "@/lib/utils" + +const Progress = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, value, ...props }, ref) => ( + + + +)) +Progress.displayName = ProgressPrimitive.Root.displayName + +export { Progress } diff --git a/src/components/ui/table.tsx b/src/components/ui/table.tsx index 6b83618c..3aa4e69a 100644 --- a/src/components/ui/table.tsx +++ b/src/components/ui/table.tsx @@ -44,7 +44,7 @@ const TableFooter = React.forwardRef< ref={ref} className={cn( "border-t bg-muted/50 font-medium [&>tr]:last:border-b-0", - className + className, )} {...props} /> @@ -59,7 +59,7 @@ const TableRow = React.forwardRef< ref={ref} className={cn( "border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted", - className + className, )} {...props} /> @@ -73,8 +73,8 @@ const TableHead = React.forwardRef< [role=checkbox]]:translate-y-[2px]", - className + "h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px] bg-gray-200 dark:bg-transparent", + className, )} {...props} /> @@ -89,7 +89,7 @@ const TableCell = React.forwardRef< ref={ref} className={cn( "p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]", - className + className, )} {...props} /> diff --git a/src/constants/sidebarConfigs.ts b/src/constants/sidebarConfigs.ts index 7bf5e99c..7d785cf5 100644 --- a/src/constants/sidebarConfigs.ts +++ b/src/constants/sidebarConfigs.ts @@ -10,6 +10,7 @@ import { BarChart3, ClipboardPenIcon, BugIcon, + DatabaseIcon, } from "lucide-react"; import { UserRole } from "../types/user"; import { ComponentType } from "react"; @@ -20,12 +21,13 @@ import StudentLogsView from "@/pages/dashboard/ui/views/student/StudentLogsView" import InstructorStudentListView from "@/pages/dashboard/ui/views/instructor/InstructorStudentListView"; import InstructorStatsView from "@/pages/dashboard/ui/views/instructor/InstructorStatsView"; import InstructorClassesView from "@/pages/dashboard/ui/views/instructor/InstructorClassesView"; -import UsersAdministrationView from "@/pages/dashboard/ui/views/admin/UsersAdministrationView"; -import ClassesAdministrationView from "@/pages/dashboard/ui/views/admin/ClassesAdministrationView"; +import AdminUserDataView from "@/pages/dashboard/ui/views/admin/AdminUserDataView.tsx"; +import AdminClassDataView from "@/pages/dashboard/ui/views/admin/AdminClassDataView.tsx"; import AppAnalyticsView from "@/pages/dashboard/ui/views/dev/AppAnalyticsView"; import StudentQuizView from "@/pages/dashboard/ui/views/student/StudentQuizView"; import ErrorAnalyticsView from "@/pages/dashboard/ui/views/dev/ErrorAnalyticsView"; -import ViewAllSurveys from "@/pages/dashboard/ui/views/admin/ViewAllSurveys"; +import AdminSurveyDataView from "@/pages/dashboard/ui/views/admin/AdminSurveyDataView.tsx"; +import AdminManageDataView from "@/pages/dashboard/ui/views/admin/AdminManageDataView.tsx"; export type SideBarItem = { id: string; @@ -105,75 +107,84 @@ export const sidebarItems: SideBarItem[] = [ roles: STUDENT, dashboardView: StudentLogsView, }, - { - id: "user-quiz", - icon: ClipboardPenIcon, - name: "Review", - title: "Quiz", - subheading: "My Dashboard", - roles: STUDENT, - dashboardView: StudentQuizView, - }, + // { + // id: "user-quiz", + // icon: ClipboardPenIcon, + // name: "Review", + // title: "Quiz", + // subheading: "My Dashboard", + // roles: STUDENT, + // dashboardView: StudentQuizView, + // }, - // Teaching - { - id: "instructor-stats", - icon: BarChart3, - name: "Class Statistics", - title: "Student Performance Analytics", - subheading: "Teaching", - description: "Track and analyze student performance and progress.", - roles: INSTRUCTOR, - dashboardView: InstructorStatsView, - }, - { - id: "instructor-students", - icon: Users, - name: "Students", - title: "Student Management", - subheading: "Teaching", - roles: INSTRUCTOR, - dashboardView: InstructorStudentListView, - }, - { - id: "instructor-classes", - icon: BookOpenText, - name: "Classes", - title: "Class Management", - description: - "Create new courses, monitor class progress, and manage your teaching portfolio.", - subheading: "Teaching", - roles: INSTRUCTOR, - dashboardView: InstructorClassesView, - }, + // Instructor + // { + // id: "instructor-stats", + // icon: BarChart3, + // name: "Class Statistics", + // title: "Student Performance Analytics", + // subheading: "Instructor", + // description: "Track and analyze student performance and progress.", + // roles: INSTRUCTOR, + // dashboardView: InstructorStatsView, + // }, + // { + // id: "instructor-students", + // icon: Users, + // name: "Students", + // title: "Student Management", + // subheading: "Instructor", + // roles: INSTRUCTOR, + // dashboardView: InstructorStudentListView, + // }, + // { + // id: "instructor-classes", + // icon: BookOpenText, + // name: "Classes", + // title: "Class Management", + // description: + // "Create new courses, monitor class progress, and manage your teaching portfolio.", + // subheading: "Instructor", + // roles: INSTRUCTOR, + // dashboardView: InstructorClassesView, + // }, - // Administration + // Admin { id: "admin-users", icon: Users, name: "Manage Users", - title: "User Administration", - subheading: "Administration", + title: "User Data Management", + subheading: "Admin", roles: ADMIN, - dashboardView: UsersAdministrationView, + dashboardView: AdminUserDataView, }, { id: "admin-classes", icon: Settings, name: "Manage Classes", - title: "Class Administration", - subheading: "Administration", + title: "Class Data Management", + subheading: "Admin", roles: ADMIN, - dashboardView: ClassesAdministrationView, + dashboardView: AdminClassDataView, }, { id: "admin-surveys", icon: FileText, - name: "Surveys", - title: "Survey Management", - subheading: "Administration", + name: "Manage Surveys", + title: "Survey Data Management", + subheading: "Admin", + roles: ADMIN, + dashboardView: AdminSurveyDataView, + }, + { + id: "admin-data", + icon: DatabaseIcon, + name: "Manage Data", + title: "Supabase Data Management", + subheading: "Admin", roles: ADMIN, - dashboardView: ViewAllSurveys, + dashboardView: AdminManageDataView, }, // Development diff --git a/src/context/UserProvider.tsx b/src/context/UserProvider.tsx index 0b65053b..eb9a3ee2 100644 --- a/src/context/UserProvider.tsx +++ b/src/context/UserProvider.tsx @@ -3,7 +3,7 @@ import { User } from "../types/user"; import { useAuth } from "../hooks/useAuth"; import { getUserData } from "../api/user"; import { UserContext } from "./UserContext"; -import { supabase } from "@/supabaseClient"; +import { supabase } from "@/lib/supabaseClient.ts"; export const UserProvider = ({ children }: { children: React.ReactNode }) => { const { isAuthenticated, user } = useAuth(); @@ -23,7 +23,7 @@ export const UserProvider = ({ children }: { children: React.ReactNode }) => { if (window.location.pathname !== "/reset-form") { // If they're not on reset page, sign them out console.log( - "Password recovery detected on wrong page, signing out..." + "Password recovery detected on wrong page, signing out...", ); await supabase.auth.signOut(); } @@ -32,7 +32,7 @@ export const UserProvider = ({ children }: { children: React.ReactNode }) => { } else if (event === "SIGNED_IN") { setIsPasswordRecovery(false); } - } + }, ); return () => authListener.subscription.unsubscribe(); diff --git a/src/hooks/useAllClasses.ts b/src/hooks/useAllClasses.ts index 040387c5..ceb1206a 100644 --- a/src/hooks/useAllClasses.ts +++ b/src/hooks/useAllClasses.ts @@ -57,79 +57,3 @@ export const useAllClasses = (options?: UseAllClassesOptions) => { totalClasses: data?.pagination?.total || 0, }; }; - -export const useAllClassesWithSearch = ( - initialSearch = "", - includeStudents = false -) => { - const [page, setPage] = useState(1); - const [search, setSearch] = useState(initialSearch); - const [limit] = useState(50); - const { userData } = useUser(); - - const { - classes, - pagination, - isLoading, - error, - refetch, - isFetching, - hasNextPage, - hasPreviousPage, - totalClasses, - } = useAllClasses({ - page, - limit, - search, - userId: userData?.id, - includeStudents, - }); - - const handleSearch = useCallback((newSearch: string) => { - setSearch(newSearch); - setPage(1); - }, []); - - const handlePageChange = useCallback((newPage: number) => { - setPage(newPage); - }, []); - - const nextPage = useCallback(() => { - if (hasNextPage) { - setPage((prev) => prev + 1); - } - }, [hasNextPage]); - - const previousPage = useCallback(() => { - if (hasPreviousPage) { - setPage((prev) => prev - 1); - } - }, [hasPreviousPage]); - - return { - // Data - classes, - pagination, - totalClasses, - - // Loading states - isLoading, - isFetching, - error, - - // Search - search, - handleSearch, - - // Pagination - page, - hasNextPage, - hasPreviousPage, - handlePageChange, - nextPage, - previousPage, - - // Actions - refetch, - }; -}; diff --git a/src/hooks/useAuth.ts b/src/hooks/useAuth.ts index e035e904..0bbc9a5f 100755 --- a/src/hooks/useAuth.ts +++ b/src/hooks/useAuth.ts @@ -1,6 +1,6 @@ import { useEffect, useState } from "react"; import { User } from "@supabase/supabase-js"; -import { supabase } from "@/supabaseClient"; +import { supabase } from "@/lib/supabaseClient.ts"; import { useLocation, useNavigate } from "react-router-dom"; /** @@ -32,7 +32,7 @@ export const useAuth = () => { (_event, session) => { setUser(session?.user || null); setLoading(false); - } + }, ); return () => listener?.subscription.unsubscribe(); diff --git a/src/supabaseClient.ts b/src/lib/supabaseClient.ts similarity index 100% rename from src/supabaseClient.ts rename to src/lib/supabaseClient.ts diff --git a/src/pages/Quiz.tsx b/src/pages/Quiz.tsx index ed406e05..8af60e14 100755 --- a/src/pages/Quiz.tsx +++ b/src/pages/Quiz.tsx @@ -1,6 +1,6 @@ import { useAuth } from "../hooks/useAuth"; import { useState, useEffect } from "react"; -import { supabase } from "../supabaseClient"; +import { supabase } from "../lib/supabaseClient.ts"; import { BASE_ENDPOINT } from "../api/endpoints"; import { useUserClasses } from "../hooks/useUserClasses"; import ClassesDropdownMenu from "./dashboard/ui/components/ClassesDropdownMenu"; @@ -243,7 +243,7 @@ export const QuizPage = () => { const navigate = useNavigate(); const { allClasses, handleClassSelect, selectedClassId } = useUserClasses( - user?.id || "" + user?.id || "", ); const [quiz, setQuiz] = useState([]); @@ -260,7 +260,7 @@ export const QuizPage = () => { quiz.length > 0 && quiz.every( (question) => - answers[question.id] !== null && answers[question.id] !== undefined + answers[question.id] !== null && answers[question.id] !== undefined, ); const resetQuizState = () => { @@ -302,10 +302,10 @@ export const QuizPage = () => { data.message?.includes("no sections that need review") ) { const selectedClass = allClasses.find( - (cls) => cls.id === selectedClassId + (cls) => cls.id === selectedClassId, ); toast.success( - `You're doing great! No sections need review${selectedClass ? ` in ${selectedClass.classTitle}` : ""}.` + `You're doing great! No sections need review${selectedClass ? ` in ${selectedClass.classTitle}` : ""}.`, ); } else { throw new Error(data?.message || "Failed to generate quiz"); @@ -327,7 +327,7 @@ export const QuizPage = () => { } catch (error) { console.error("Error generating quiz:", error); toast.error( - error instanceof Error ? error.message : "An unknown error occurred" + error instanceof Error ? error.message : "An unknown error occurred", ); } finally { setIsGenerating(false); @@ -343,7 +343,7 @@ export const QuizPage = () => { setSubmitting(true); const calculatedScore = quiz.reduce( (total, q) => (answers[q.id] === q.answerIndex ? total + 1 : total), - 0 + 0, ); const passThreshold = 0.8; const hasPassed = calculatedScore / quiz.length >= passThreshold; @@ -359,8 +359,8 @@ export const QuizPage = () => { supabase .from("user_section_questions") .update({ user_answer_index: answers[q.id] ?? null }) - .eq("id", q.id) - ) + .eq("id", q.id), + ), ); // 2. Upsert the quiz result @@ -373,7 +373,7 @@ export const QuizPage = () => { user_class_id: selectedClassId === "non-class" ? null : selectedClassId, }, - { onConflict: "quiz_id" } + { onConflict: "quiz_id" }, ); if (upsertError) throw upsertError; @@ -418,13 +418,13 @@ export const QuizPage = () => { acc[q.id] = q.user_answer_index; return acc; }, - {} as Record + {} as Record, ); const calculatedScore = loadedQuiz.reduce( (total, q) => loadedAnswers[q.id] === q.answerIndex ? total + 1 : total, - 0 + 0, ); setQuiz(loadedQuiz); @@ -443,7 +443,7 @@ export const QuizPage = () => { const updateUserProgress = async ( userId: string, sectionId: string | null, - classId: string | null + classId: string | null, ) => { try { // Mark the section as complete diff --git a/src/pages/SurveyView.tsx b/src/pages/SurveyView.tsx index befc35f4..4997c8ee 100644 --- a/src/pages/SurveyView.tsx +++ b/src/pages/SurveyView.tsx @@ -4,7 +4,7 @@ import SurveyPreview, { Participant, Survey, } from "./dashboard/ui/components/SurveyPreview"; -import { supabase } from "@/supabaseClient"; +import { supabase } from "@/lib/supabaseClient.ts"; import { AlertTriangle, CheckCircle } from "lucide-react"; const SurveyView = () => { @@ -30,7 +30,7 @@ const SurveyView = () => { try { setLoading(true); - // Load survey data + // Survey now has questions embedded as JSONB — single query const { data: surveyData, error: surveyError } = await supabase .from("surveys") .select("*") @@ -39,15 +39,6 @@ const SurveyView = () => { if (surveyError) throw surveyError; - // Load survey questions - const { data: questionsData, error: questionsError } = await supabase - .from("survey_questions") - .select("*") - .eq("survey_id", surveyId) - .order("question_number"); - - if (questionsError) throw questionsError; - // Load user data const { data: userData, error: userError } = await supabase .from("users") @@ -57,10 +48,11 @@ const SurveyView = () => { if (userError) throw userError; - setSurvey({ ...surveyData, questions: questionsData || [] }); + // questions are already embedded in surveyData.questions + setSurvey(surveyData); setUser(userData); - // Check if user has already submitted + // Check if user already submitted const { data: existingResponse } = await supabase .from("survey_responses") .select("id") @@ -127,34 +119,40 @@ const SurveyView = () => { ); } + if (!survey.is_active) { + return ( +
+ + + +

Survey Unavailable

+

+ This survey is not currently open for responses. +

+
+
+
+ ); + } + if (showSuccess || alreadySubmitted) { return ( -
+
-

Survey Submitted

+

+ {alreadySubmitted && !showSuccess + ? "Already Submitted" + : "Survey Submitted"} +

- Thank you for completing the survey. Your response has been - recorded. + {alreadySubmitted && !showSuccess + ? "You have already submitted a response for this survey." + : "Thank you for completing the survey. Your response has been recorded."}

- {survey.type === "POST_SURVEY" && ( -
-
-

- Would you like to do a one on one session? -

-

Schedule Here

- -
-
- )}
); } @@ -164,7 +162,7 @@ const SurveyView = () => {
diff --git a/src/pages/auth/ui/components/LogInForm.tsx b/src/pages/auth/ui/components/LogInForm.tsx index a84ba7e3..54bd188b 100755 --- a/src/pages/auth/ui/components/LogInForm.tsx +++ b/src/pages/auth/ui/components/LogInForm.tsx @@ -1,7 +1,7 @@ import React, { useState } from "react"; import { useNavigate } from "react-router-dom"; import { Eye, EyeOff, VenetianMask } from "lucide-react"; -import { supabase } from "@/supabaseClient"; +import { supabase } from "@/lib/supabaseClient.ts"; import { AUTH_ENDPOINT } from "@/api/endpoints"; import { Card } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; diff --git a/src/pages/auth/ui/components/PasswordResetForm.tsx b/src/pages/auth/ui/components/PasswordResetForm.tsx index c2817dbc..22a6d940 100644 --- a/src/pages/auth/ui/components/PasswordResetForm.tsx +++ b/src/pages/auth/ui/components/PasswordResetForm.tsx @@ -1,4 +1,4 @@ -import { supabase } from "@/supabaseClient"; +import { supabase } from "@/lib/supabaseClient.ts"; import React, { useState, useEffect } from "react"; import { Card, @@ -50,7 +50,7 @@ export const PasswordResetForm = () => { if (error) { console.error("Password reset error:", error); setError( - error.message || "Failed to send reset email. Please try again." + error.message || "Failed to send reset email. Please try again.", ); return; } @@ -180,7 +180,7 @@ export const PasswordResetCallback = () => { if (event === "PASSWORD_RECOVERY") { setCanReset(true); } - } + }, ); return () => { diff --git a/src/pages/auth/ui/components/SignUpForm.tsx b/src/pages/auth/ui/components/SignUpForm.tsx index d494d9b3..f4f38b01 100755 --- a/src/pages/auth/ui/components/SignUpForm.tsx +++ b/src/pages/auth/ui/components/SignUpForm.tsx @@ -1,7 +1,7 @@ import React, { useState } from "react"; import { Eye, EyeOff } from "lucide-react"; import { useNavigate } from "react-router-dom"; -import { supabase } from "@/supabaseClient"; +import { supabase } from "@/lib/supabaseClient.ts"; import { registerUser } from "@/api/auth"; import { Card } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; @@ -39,7 +39,7 @@ const SignUpForm = () => { lastName, email, password, - isConsent + isConsent, ); if (error) { diff --git a/src/pages/auth/ui/views/AnonymousLoginView.tsx b/src/pages/auth/ui/views/AnonymousLoginView.tsx index c1b92f4b..db6baad4 100644 --- a/src/pages/auth/ui/views/AnonymousLoginView.tsx +++ b/src/pages/auth/ui/views/AnonymousLoginView.tsx @@ -3,7 +3,7 @@ import { Button } from "@/components/ui/button"; import { Card } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { supabase } from "@/supabaseClient"; +import { supabase } from "@/lib/supabaseClient.ts"; import React, { useState } from "react"; import { useNavigate } from "react-router-dom"; import { toast } from "sonner"; @@ -37,7 +37,7 @@ const AnonymousLoginView = () => { }; const checkUsernameExists = async ( - usernameToCheck: string + usernameToCheck: string, ): Promise => { try { const { data, error } = await supabase @@ -76,7 +76,7 @@ const AnonymousLoginView = () => { if (error) { setError( - "No account found with this username. Try creating a new one." + "No account found with this username. Try creating a new one.", ); return; } @@ -107,7 +107,7 @@ const AnonymousLoginView = () => { if (usernameExists) { setError( - "This username is already taken. Please choose a different one or generate a new one." + "This username is already taken. Please choose a different one or generate a new one.", ); setLoading(false); return; @@ -120,7 +120,7 @@ const AnonymousLoginView = () => { "", email, ANONYMOUS_PASSWORD, - isConsent + isConsent, ); if (registerResult.error) { diff --git a/src/pages/classes/ui/components/ClassTypingLogsDownloadButton.tsx b/src/pages/classes/ui/components/ClassTypingLogsDownloadButton.tsx index 60217bd5..62744397 100644 --- a/src/pages/classes/ui/components/ClassTypingLogsDownloadButton.tsx +++ b/src/pages/classes/ui/components/ClassTypingLogsDownloadButton.tsx @@ -1,5 +1,5 @@ import DownloadFormattedFile from "@/components/DownloadFormattedFile"; -import { supabase } from "@/supabaseClient"; +import { supabase } from "@/lib/supabaseClient.ts"; import { User } from "@/types/user"; import { useEffect, useState } from "react"; diff --git a/src/pages/classes/ui/components/ClassesDataTable.tsx b/src/pages/classes/ui/components/ClassesDataTable.tsx deleted file mode 100644 index 073bc947..00000000 --- a/src/pages/classes/ui/components/ClassesDataTable.tsx +++ /dev/null @@ -1,358 +0,0 @@ -import { useAllClassesWithSearch } from "@/hooks/useAllClasses"; -import { useMemo, useState } from "react"; -import { Card, CardContent } from "@/components/ui/card"; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table"; -import PaginatedTable from "@/components/PaginatedTable"; -import { Avatar, AvatarFallback } from "@/components/ui/avatar"; -import { Users } from "lucide-react"; -import { User } from "@/types/user"; -import UserDataSearchFilters from "../../../dashboard/ui/components/UserDataSearchFilter"; -import { formatActivityTimestamp } from "@/utils/timeConverter"; -import { useNavigate } from "react-router-dom"; - -interface ClassData { - id: string; - classTitle: string; - classCode: string; - classHexColor?: string; - classDescription?: string; - classImageCover?: string; - instructorId?: string; - instructorName?: string; - studentCount: number; - students?: User[]; - createdAt: string; -} - -const ClassesDataTable = () => { - const [nameFilter, setNameFilter] = useState(""); - - const { classes, isLoading, error } = useAllClassesWithSearch("", true); - - const navigate = useNavigate(); - - const filteredClasses = useMemo(() => { - return classes.filter((classItem) => { - const matchName = - classItem.classTitle.toLowerCase().includes(nameFilter.toLowerCase()) || - classItem.classCode?.toLowerCase().includes(nameFilter.toLowerCase()) || - (classItem.instructorName && - classItem.instructorName - .toLowerCase() - .includes(nameFilter.toLowerCase())) || - (classItem.classDescription && - classItem.classDescription - .toLowerCase() - .includes(nameFilter.toLowerCase())); - - return matchName; - }); - }, [classes, nameFilter]); - - const handleRowClick = (instructorId: string, classId: string) => { - navigate(`${instructorId}/${classId}`); - }; - - if (error) { - return ( -
-
- Error loading classes: {error} -
-
- ); - } - - if (isLoading) { - return ( -
- - {/* Loading skeleton for mobile cards */} -
- {Array.from({ length: 5 }).map((_, index) => ( - - -
-
-
-
-
-
-
-
-
-
- ))} -
- {/* Loading skeleton for desktop table */} -
- - - - No. - Class - Code - Students - Created - - - - {Array.from({ length: 5 }).map((_, index) => ( - {}} - /> - ))} - -
-
-
- ); - } - - return ( - <> -
- -
- - {/* Mobile Card View */} -
- ( -
- {currentItems.map((classItem, index) => ( - - handleRowClick( - classItem.instructorId as string, - classItem.id as string - ) - } - /> - ))} -
- )} - /> -
- - {/* Desktop Table View */} -
- ( -
- - - - No. - Class - Code - - Students - - Created - - - - {currentItems.map((classItem, index) => ( - - handleRowClick( - classItem.instructorId as string, - classItem.id as string - ) - } - /> - ))} - -
-
- )} - /> -
- - ); -}; - -export default ClassesDataTable; - -interface ClassCardProps { - classData: ClassData; - index: number; - onClick: () => void; -} - -const ClassCard = ({ classData, index, onClick }: ClassCardProps) => { - return ( - - -
-
-
- - #{index + 1} - -
- - {classData.classTitle || "Untitled Class"} - -
- {classData.classCode} -
-
-
- -
- Created at {formatActivityTimestamp(classData.createdAt)} -
-
- -
-
- {classData.studentCount} -
-
- by {classData.instructorName || "Unknown Instructor"} -
-
-
-
-
- ); -}; - -// Desktop Row Component -interface ClassRowProps { - classData: ClassData; - index: number; - isLoading?: boolean; - onClick: () => void; -} - -const ClassRow = ({ - classData, - index, - isLoading = false, - onClick, -}: ClassRowProps) => { - if (isLoading) { - return ( - - -
-
- -
-
-
-
-
-
-
-
- -
-
- -
-
- -
-
-
- ); - } - - return ( - - {index + 1} - - -
- - {classData.classImageCover ? ( - {classData.classTitle} - ) : ( - - {classData.classTitle?.charAt(0).toUpperCase()} - - )} - -
-
{classData.classTitle}
- {classData.classDescription && ( -
- {classData.classDescription} -
- )} -
-
-
- - -
{classData.classCode}
-
- - -
{classData.studentCount}
-
- - -
- {formatActivityTimestamp(classData.createdAt)} -
-
-
- ); -}; diff --git a/src/pages/dashboard/hooks/useAllUsersActivity.ts b/src/pages/dashboard/hooks/useAllUsersActivity.ts deleted file mode 100644 index 3864fdc2..00000000 --- a/src/pages/dashboard/hooks/useAllUsersActivity.ts +++ /dev/null @@ -1,277 +0,0 @@ -import { useState, useCallback, useMemo } from "react"; -import { useQuery, useQueries } from "@tanstack/react-query"; -import { UserMode, UserWithActivity } from "@/types/user"; -import { getAllUsers, getUserActivity } from "@/api/user"; -import { UserActivityLogItem } from "@/types/suggestion"; -import { - calculateProgress, - getEmptyProgressData, -} from "@/utils/calculateProgress"; -import QUERY_INTERVALS from "@/constants/queryIntervals"; -import { UseAllUsersOptions } from "@/types/data"; -import { isOnline } from "@/utils/timeConverter"; - -export const useAllUsersWithActivity = (options?: UseAllUsersOptions) => { - const { search = "", enabled = true } = options || {}; - - const { - data: usersData, - isLoading: usersLoading, - error: usersError, - } = useQuery({ - queryKey: ["allUsers", { search }], - queryFn: async () => { - const { data, error } = await getAllUsers({ - page: 1, - limit: 500, // Fetch more users - search, - }); - if (error) throw new Error(error); - return data!; - }, - enabled, - staleTime: 1000 * 60 * 5, - gcTime: 1000 * 60 * 15, - retry: 2, - }); - - const users = useMemo(() => usersData?.users || [], [usersData]); - - const activityQueries = useQueries({ - queries: users.map((user) => ({ - queryKey: ["userActivity", user.id, "all"], - queryFn: async () => { - if (!user.settings?.mode) { - return { - userId: user.id, - activities: [], - progressData: getEmptyProgressData(), - }; - } - - try { - const { logs, error } = await getUserActivity( - user.id, - user.settings.mode as UserMode, - ); - - if (error || !logs) { - return { - userId: user.id, - activities: [], - progressData: getEmptyProgressData(), - }; - } - - const logArray = logs as UserActivityLogItem[]; - - const progressData = - logArray.length > 0 - ? calculateProgress(logArray) - : getEmptyProgressData(); - - return { - userId: user.id, - activities: logArray, - progressData, - }; - } catch (err) { - console.warn(`Failed to fetch activity for user ${user.id}:`, err); - return { - userId: user.id, - activities: [], - progressData: getEmptyProgressData(), - }; - } - }, - enabled: enabled && !!user.id && !!user.settings?.mode, - staleTime: QUERY_INTERVALS.staleTime, - retry: 1, - })), - }); - - const usersWithActivity = useMemo(() => { - return users.map((user, index): UserWithActivity => { - const activityQuery = activityQueries[index]; - const activityData = activityQuery?.data; - - if (!activityData) { - return { - ...user, - totalAccepted: 0, - totalRejected: 0, - totalInteractions: 0, - correctSuggestions: 0, - correctAccepts: 0, - correctRejects: 0, - accuracyPercentage: 0, - lastActivity: null, - activityMode: (user.settings?.mode as UserMode) || null, - }; - } - - // Get the last activity timestamp - const lastActivity = - activityData.activities.length > 0 - ? activityData.activities.reduce( - (latest, activity) => - new Date(activity.createdAt) > new Date(latest) - ? activity.createdAt - : latest, - activityData.activities[0].createdAt, - ) - : null; - - return { - ...user, - totalAccepted: activityData.progressData.totalAccepted, - totalRejected: activityData.progressData.totalRejected, - totalInteractions: activityData.progressData.totalInteractions, - correctSuggestions: activityData.progressData.correctSuggestions, - correctAccepts: activityData.progressData.correctAccepts, - correctRejects: activityData.progressData.correctRejects, - accuracyPercentage: activityData.progressData.accuracyPercentage, - lastActivity, - activityMode: (user.settings?.mode as UserMode) || null, - }; - }); - }, [users, activityQueries]); - - const isLoading = - usersLoading || activityQueries.some((query) => query.isLoading); - const isFetching = activityQueries.some((query) => query.isFetching); - - const error = - usersError?.message || - activityQueries.find((query) => query.error)?.error?.message || - null; - - return { - users: usersWithActivity, - isLoading, - isFetching, - error, - totalUsers: usersWithActivity.length, - }; -}; - -export const useAllUsersWithActivityAndSearch = ( - initialSearch = "", - initialLimit = 10, -) => { - const [page, setPage] = useState(1); - const [search, setSearch] = useState(initialSearch); - const [limit, setLimit] = useState(initialLimit); - - // Fetch ALL users (no server-side pagination) - const { - users: allUsers, - isLoading, - isFetching, - error, - totalUsers: allUsersCount, - } = useAllUsersWithActivity({ - search, - }); - - // Prioritize online users first, then by recent activity - const sortedUsers = useMemo(() => { - return [...allUsers].sort((a, b) => { - const aOnline = isOnline(a.lastActivity); - const bOnline = isOnline(b.lastActivity); - - // First priority: online status (online users first) - if (aOnline && !bOnline) return -1; - if (!aOnline && bOnline) return 1; - - // Second priority: recent activity (most recent first) - const aTime = a.lastActivity ? new Date(a.lastActivity).getTime() : 0; - const bTime = b.lastActivity ? new Date(b.lastActivity).getTime() : 0; - - return bTime - aTime; - }); - }, [allUsers]); - - // Client-side pagination - const paginatedUsers = useMemo(() => { - const startIndex = (page - 1) * limit; - const endIndex = startIndex + limit; - return sortedUsers.slice(startIndex, endIndex); - }, [sortedUsers, page, limit]); - - // Calculate pagination info - const totalPages = Math.ceil(sortedUsers.length / limit); - const hasNextPage = page < totalPages; - const hasPreviousPage = page > 1; - - const pagination = { - page, - limit, - total: sortedUsers.length, - totalPages, - }; - - const handleSearch = useCallback((newSearch: string) => { - setSearch(newSearch); - setPage(1); - }, []); - - const handlePageChange = useCallback((newPage: number) => { - setPage(newPage); - }, []); - - const nextPage = useCallback(() => { - if (hasNextPage) { - setPage((prev) => prev + 1); - } - }, [hasNextPage]); - - const previousPage = useCallback(() => { - if (hasPreviousPage) { - setPage((prev) => prev - 1); - } - }, [hasPreviousPage]); - - const firstPage = useCallback(() => { - setPage(1); - }, []); - - const lastPage = useCallback(() => { - setPage(totalPages); - }, [totalPages]); - - const handleLimitChange = useCallback((newLimit: number) => { - setLimit(newLimit); - setPage(1); - }, []); - - return { - // Sorted users - users: paginatedUsers, - pagination, - totalUsers: sortedUsers.length, - limit, - - // Loading states - isLoading, - isFetching, - error, - - // Search - search, - handleSearch, - - // Pagination - page, - hasNextPage, - hasPreviousPage, - handlePageChange, - nextPage, - previousPage, - firstPage, - lastPage, - handleLimitChange, - }; -}; - -export type { UserWithActivity }; diff --git a/src/pages/dashboard/hooks/useUserActivity.ts b/src/pages/dashboard/hooks/useUserActivity.ts index e64cc70d..95646fc1 100755 --- a/src/pages/dashboard/hooks/useUserActivity.ts +++ b/src/pages/dashboard/hooks/useUserActivity.ts @@ -8,14 +8,14 @@ import { UserMode } from "@/types/user"; import { useQuery } from "@tanstack/react-query"; import QUERY_INTERVALS from "@/constants/queryIntervals"; import { useEffect, useMemo } from "react"; -import { supabase } from "@/supabaseClient"; +import { supabase } from "@/lib/supabaseClient.ts"; import { RealtimePostgresChangesFilter } from "@supabase/supabase-js"; export const useUserActivity = ( userId?: string | null, mode?: UserMode | null, selectedClassId: string | null = null, - isRealtimeEnabled?: boolean + isRealtimeEnabled?: boolean, ) => { useEffect(() => { if (!isRealtimeEnabled) return; @@ -84,7 +84,7 @@ export const useUserActivity = ( filtered = filtered.filter((activity) => !activity.classId); } else if (selectedClassId && selectedClassId !== "all") { filtered = filtered.filter( - (activity) => activity.classId === selectedClassId + (activity) => activity.classId === selectedClassId, ); } diff --git a/src/pages/dashboard/ui/components/ActivityStatsSection.tsx b/src/pages/dashboard/ui/components/ActivityStatsSection.tsx index 7ccdb667..9753b172 100644 --- a/src/pages/dashboard/ui/components/ActivityStatsSection.tsx +++ b/src/pages/dashboard/ui/components/ActivityStatsSection.tsx @@ -80,6 +80,8 @@ const ActivityStatsSection = ({ }; } | null>(null); + console.log("Data", JSON.stringify(userActivity, null, 2)); + const handleRealtimeToggle = (enabled: boolean) => { setIsRealtimeEnabled(enabled); onRealtimeToggle?.(enabled); diff --git a/src/pages/dashboard/ui/components/CompletedSurveyView.tsx b/src/pages/dashboard/ui/components/CompletedSurveyView.tsx index de51d7d7..afa27025 100644 --- a/src/pages/dashboard/ui/components/CompletedSurveyView.tsx +++ b/src/pages/dashboard/ui/components/CompletedSurveyView.tsx @@ -1,12 +1,8 @@ import { useEffect, useState } from "react"; -import { supabase } from "@/supabaseClient"; +import { supabase } from "@/lib/supabaseClient.ts"; import { Card, CardContent } from "@/components/ui/card"; import { Loader2 } from "lucide-react"; -import SurveyPreview, { - Participant, - Survey, - SurveyAnswers, -} from "./SurveyPreview"; +import SurveyPreview, { Participant, Survey } from "./SurveyPreview"; const CompletedSurveyView = () => { const urlParams = new URLSearchParams(window.location.search); @@ -15,7 +11,7 @@ const CompletedSurveyView = () => { const [survey, setSurvey] = useState(null); const [user, setUser] = useState(null); - const [answers, setAnswers] = useState({}); + const [answers, setAnswers] = useState>({}); const [loading, setLoading] = useState(true); useEffect(() => { @@ -25,29 +21,10 @@ const CompletedSurveyView = () => { try { setLoading(true); - // 1. Load survey with questions + // 1. Load survey — questions are embedded JSONB const { data: surveyData, error: surveyError } = await supabase .from("surveys") - .select( - ` - id, - title, - description, - context, - type, - created_at, - questions:survey_questions( - id, - survey_id, - question_type, - question_number, - question_text, - question_options, - is_required, - created_at - ) - ` - ) + .select("id, title, description, is_active, created_at, questions") .eq("id", surveyId) .single(); @@ -74,7 +51,15 @@ const CompletedSurveyView = () => { setSurvey(surveyData as Survey); setUser(userData as Participant); - setAnswers(responseData?.answers || {}); + + // Convert [{ questionId, value }] array to Record + const answersArray: { questionId: string; value: string }[] = + responseData?.answers ?? []; + const answersMap: Record = {}; + answersArray.forEach(({ questionId, value }) => { + answersMap[questionId] = value; + }); + setAnswers(answersMap); } catch (err) { console.error("Error loading completed survey:", err); } finally { diff --git a/src/pages/dashboard/ui/components/NavUser.tsx b/src/pages/dashboard/ui/components/NavUser.tsx index bc1209d2..d1024c18 100644 --- a/src/pages/dashboard/ui/components/NavUser.tsx +++ b/src/pages/dashboard/ui/components/NavUser.tsx @@ -10,7 +10,7 @@ import { } from "@/components/ui/dropdown-menu"; import { SidebarMenuButton } from "@/components/ui/sidebar"; import { useUser } from "@/context/UserContext"; -import { supabase } from "@/supabaseClient"; +import { supabase } from "@/lib/supabaseClient.ts"; import UserInfoItem from "@/components/UserInfoItem"; import { LogOutIcon, User2Icon } from "lucide-react"; diff --git a/src/pages/dashboard/ui/components/NewSurveyDialog.tsx b/src/pages/dashboard/ui/components/NewSurveyDialog.tsx index c36db2f6..77598ad6 100644 --- a/src/pages/dashboard/ui/components/NewSurveyDialog.tsx +++ b/src/pages/dashboard/ui/components/NewSurveyDialog.tsx @@ -1,5 +1,5 @@ import React, { useState } from "react"; -import { supabase } from "@/supabaseClient"; +import { supabase } from "@/lib/supabaseClient.ts"; import { Dialog, DialogContent, @@ -7,19 +7,11 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { Button } from "@/components/ui/button"; import { Label } from "@/components/ui/label"; - -type SURVEY_TYPES = "PRE_SURVEY" | "POST_SURVEY" | "POST_STUDY"; +import { Switch } from "@/components/ui/switch"; interface Props { open: boolean; @@ -32,8 +24,7 @@ const NewSurveyDialog = ({ open, onOpenChange, onSurveyCreated }: Props) => { const [formData, setFormData] = useState({ title: "", description: "", - context: "", - type: "PRE_SURVEY" as SURVEY_TYPES, + is_active: false, }); const [errors, setErrors] = useState>({}); @@ -50,10 +41,6 @@ const NewSurveyDialog = ({ open, onOpenChange, onSurveyCreated }: Props) => { newErrors.description = "Description too long (max 1000 characters)"; } - if (formData.context && formData.context.length > 100) { - newErrors.context = "Context too long (max 100 characters)"; - } - setErrors(newErrors); return Object.keys(newErrors).length === 0; }; @@ -66,7 +53,6 @@ const NewSurveyDialog = ({ open, onOpenChange, onSurveyCreated }: Props) => { try { setIsCreating(true); - // Get current user for RLS const { data: { user }, error: userError, @@ -76,35 +62,24 @@ const NewSurveyDialog = ({ open, onOpenChange, onSurveyCreated }: Props) => { return; } - const newSurveyData = { - title: formData.title, - description: formData.description || "", - context: formData.context || "", - type: formData.type, - }; - const { data: surveyData, error } = await supabase .from("surveys") - .insert([newSurveyData]) + .insert([ + { + title: formData.title.trim(), + description: formData.description.trim() || null, + is_active: formData.is_active, + questions: [], + }, + ]) .select() .single(); if (error) throw error; - console.log("Survey created successfully!"); - - // Close dialog and notify parent onOpenChange(false); onSurveyCreated?.(surveyData.id); - - // Reset form - setFormData({ - title: "", - description: "", - context: "", - type: "PRE_SURVEY", - }); - setErrors({}); + resetForm(); } catch (error: any) { console.error("Error creating survey:", error); setErrors({ submit: error.message || "Failed to create survey" }); @@ -113,21 +88,22 @@ const NewSurveyDialog = ({ open, onOpenChange, onSurveyCreated }: Props) => { } }; - const handleCancel = () => { - setFormData({ - title: "", - description: "", - context: "", - type: "PRE_SURVEY", - }); + const resetForm = () => { + setFormData({ title: "", description: "", is_active: false }); setErrors({}); + }; + + const handleCancel = () => { + resetForm(); onOpenChange(false); }; - const handleInputChange = (field: keyof typeof formData, value: string) => { + const handleInputChange = ( + field: keyof typeof formData, + value: string | boolean, + ) => { setFormData((prev) => ({ ...prev, [field]: value })); - // Clear error for this field when user starts typing - if (errors[field]) { + if (errors[field as string]) { setErrors((prev) => ({ ...prev, [field]: "" })); } }; @@ -172,38 +148,20 @@ const NewSurveyDialog = ({ open, onOpenChange, onSurveyCreated }: Props) => { )} -
- - handleInputChange("context", e.target.value)} - placeholder="e.g., study_phase_1, baseline" - disabled={isCreating} - /> - {errors.context && ( -

{errors.context}

- )} -
- -
- - + />
{errors.submit && ( diff --git a/src/pages/dashboard/ui/components/SurveyPreview.tsx b/src/pages/dashboard/ui/components/SurveyPreview.tsx index 695cad66..18894161 100644 --- a/src/pages/dashboard/ui/components/SurveyPreview.tsx +++ b/src/pages/dashboard/ui/components/SurveyPreview.tsx @@ -6,34 +6,40 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { Checkbox } from "@/components/ui/checkbox"; import { Textarea } from "@/components/ui/textarea"; +import { Slider } from "@/components/ui/slider"; +import { Separator } from "@/components/ui/separator"; +import { Progress } from "@/components/ui/progress"; import { CheckCircle } from "lucide-react"; -import { supabase } from "@/supabaseClient"; +import { supabase } from "@/lib/supabaseClient.ts"; import { toast } from "sonner"; +// ── Types ───────────────────────────────────────────────────────────────────── + export interface SurveyQuestion { id: string; - survey_id: string; - question_type: + type: | "text" - | "multiple_choice" - | "multiple_select" + | "long_text" | "likert" - | "section_title" - | "slider" + | "rating" + | "multiple_choice" + | "checkbox" | "nasa_tlx"; - question_number: number; - question_text: string; - question_options?: string[]; - is_required: boolean; - created_at: string; + prompt: string; + required: boolean; + options?: string[]; + points?: number; + labels?: string[]; + min?: number; + max?: number; + step?: number; } export interface Survey { id: string; title: string; - description: string; - context: string; - type: string; + description: string | null; + is_active: boolean; created_at: string; questions: SurveyQuestion[]; } @@ -44,10 +50,6 @@ export interface Participant { pid: string; } -export interface SurveyAnswers { - [questionId: string]: string | string[] | Record; -} - interface SurveyPreviewProps { survey: Survey; user?: Participant; @@ -55,9 +57,253 @@ interface SurveyPreviewProps { className?: string; onSuccess?: () => void; readOnly?: boolean; - initialAnswers?: SurveyAnswers; + initialAnswers?: Record; +} + +// ── Question field ──────────────────────────────────────────────────────────── + +function QuestionField({ + question, + value, + onChange, + disabled, + error, +}: { + question: SurveyQuestion; + value: string; + onChange: (v: string) => void; + disabled: boolean; + error: boolean; +}) { + if (question.type === "text") { + return ( + onChange(e.target.value)} + placeholder="Your answer" + disabled={disabled} + className={`border-0 border-b-2 rounded-none px-0 focus:border-ring bg-transparent ${ + error ? "border-red-500" : "border-border" + }`} + /> + ); + } + + if (question.type === "long_text") { + return ( +