From 9c7e33499a0034129500451e69e9bccb243cfd6e Mon Sep 17 00:00:00 2001 From: debudebuye Date: Thu, 26 Feb 2026 11:18:40 +0300 Subject: [PATCH] chore: Update dependencies, refactor ESLint config, and enhance test infrastructure --- .gitignore | 1 + eslint.config.js | 33 +- index.html | 4 +- package-lock.json | 384 +++++++++++++----- package.json | 3 +- public/admin-icon.svg | 7 + public/vite.svg | 1 - src/components/DebugLogin.tsx | 27 -- .../__tests__/ErrorBoundary.test.tsx | 92 +++++ src/components/ui/badge-variants.ts | 21 + src/components/ui/badge.tsx | 25 +- src/components/ui/button-variants.ts | 31 ++ src/components/ui/button.tsx | 35 +- src/layouts/app-shell.tsx | 15 +- src/lib/__tests__/utils.test.ts | 59 ++- src/pages/activity-log/index.tsx | 8 +- src/pages/admin/analytics/api.tsx | 9 +- src/pages/admin/analytics/storage.tsx | 20 +- src/pages/admin/announcements/index.tsx | 32 +- src/pages/admin/audit/index.tsx | 8 +- src/pages/admin/maintenance/index.tsx | 11 +- src/pages/admin/security/api-keys.tsx | 18 +- src/pages/admin/security/failed-logins.tsx | 14 +- src/pages/admin/security/rate-limits.tsx | 3 +- src/pages/admin/security/sessions.tsx | 4 +- src/pages/admin/security/suspicious.tsx | 5 +- src/pages/admin/settings/index.tsx | 17 +- src/pages/admin/users/[id]/activity.tsx | 12 +- src/pages/admin/users/index.tsx | 37 +- src/pages/login/__tests__/index.test.tsx | 10 - src/pages/login/index.tsx | 12 +- src/services/__tests__/auth.service.test.ts | 200 +++++++++ src/services/analytics.service.ts | 17 +- src/services/api/__tests__/client.test.ts | 23 ++ src/services/api/client.ts | 8 +- src/services/audit.service.ts | 2 +- src/services/dashboard.service.ts | 13 +- src/services/index.ts | 4 +- src/services/security.service.ts | 16 +- src/services/user.service.ts | 5 +- src/test/setup.ts | 29 +- src/test/test-utils.tsx | 15 +- src/test/vitest.d.ts | 9 + src/types/activity.types.ts | 18 + src/types/analytics.types.ts | 46 +++ src/types/common.types.ts | 63 +++ src/types/error.types.ts | 15 + src/types/security.types.ts | 66 +++ vitest.config.ts | 6 + 49 files changed, 1159 insertions(+), 354 deletions(-) create mode 100644 public/admin-icon.svg delete mode 100644 public/vite.svg delete mode 100644 src/components/DebugLogin.tsx create mode 100644 src/components/__tests__/ErrorBoundary.test.tsx create mode 100644 src/components/ui/badge-variants.ts create mode 100644 src/components/ui/button-variants.ts create mode 100644 src/services/__tests__/auth.service.test.ts create mode 100644 src/services/api/__tests__/client.test.ts create mode 100644 src/test/vitest.d.ts create mode 100644 src/types/activity.types.ts create mode 100644 src/types/analytics.types.ts create mode 100644 src/types/common.types.ts create mode 100644 src/types/error.types.ts create mode 100644 src/types/security.types.ts diff --git a/.gitignore b/.gitignore index f2e3ced..f66b568 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ lerna-debug.log* node_modules dist dist-ssr +coverage *.local # Environment variables diff --git a/eslint.config.js b/eslint.config.js index 5e6b472..4d7c104 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -3,21 +3,34 @@ import globals from 'globals' import reactHooks from 'eslint-plugin-react-hooks' import reactRefresh from 'eslint-plugin-react-refresh' import tseslint from 'typescript-eslint' -import { defineConfig, globalIgnores } from 'eslint/config' -export default defineConfig([ - globalIgnores(['dist']), +export default tseslint.config( + { ignores: ['dist'] }, { + extends: [js.configs.recommended, ...tseslint.configs.recommended], files: ['**/*.{ts,tsx}'], - extends: [ - js.configs.recommended, - tseslint.configs.recommended, - reactHooks.configs.flat.recommended, - reactRefresh.configs.vite, - ], languageOptions: { ecmaVersion: 2020, globals: globals.browser, }, + plugins: { + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, + }, + rules: { + ...reactHooks.configs.recommended.rules, + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + '@typescript-eslint/no-explicit-any': 'warn', // Change from error to warning + }, }, -]) + { + files: ['src/test/**/*.{ts,tsx}'], + rules: { + 'react-refresh/only-export-components': 'off', + '@typescript-eslint/no-explicit-any': 'off', // Allow any in test files + }, + }, +) diff --git a/index.html b/index.html index c1b6187..9fe4b63 100644 --- a/index.html +++ b/index.html @@ -2,7 +2,7 @@ - + @@ -10,7 +10,7 @@ href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap" rel="stylesheet" /> - yaltopia-ticket-admin + Yaltopia Ticket Admin
diff --git a/package-lock.json b/package-lock.json index 0dff5d1..907e7b2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +21,7 @@ "@radix-ui/react-toast": "^1.2.15", "@sentry/react": "^10.39.0", "@tanstack/react-query": "^5.90.12", - "axios": "^1.13.2", + "axios": "^1.13.5", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "date-fns": "^4.1.0", @@ -42,6 +42,7 @@ "@types/react": "^19.2.5", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^5.1.1", + "@vitest/coverage-v8": "^4.0.18", "@vitest/ui": "^4.0.18", "autoprefixer": "^10.4.23", "eslint": "^9.39.1", @@ -435,6 +436,16 @@ "node": ">=6.9.0" } }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@bramus/specificity": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", @@ -1106,20 +1117,20 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", - "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.4.tgz", + "integrity": "sha512-4h4MVF8pmBsncB60r0wSJiIeUKTSD4m7FmTFThG8RHlsg9ajqckLm9OraguFGZE4vVdpiI1Q4+hFnisopmG6gQ==", "dev": true, "license": "MIT", "dependencies": { - "ajv": "^6.12.4", + "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", - "minimatch": "^3.1.2", + "minimatch": "^3.1.3", "strip-json-comments": "^3.1.1" }, "engines": { @@ -1143,9 +1154,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.39.2", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", - "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", + "version": "9.39.3", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.3.tgz", + "integrity": "sha512-1B1VkCq6FuUNlQvlBYb+1jDu/gV297TIs/OeiaSR9l1H27SVW55ONE1e1Vp16NqP683+xEGzxYtv4XCiDPaQiw==", "dev": true, "license": "MIT", "engines": { @@ -3871,17 +3882,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.54.0.tgz", - "integrity": "sha512-hAAP5io/7csFStuOmR782YmTthKBJ9ND3WVL60hcOjvtGFb+HJxH4O5huAcmcZ9v9G8P+JETiZ/G1B8MALnWZQ==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.1.tgz", + "integrity": "sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.54.0", - "@typescript-eslint/type-utils": "8.54.0", - "@typescript-eslint/utils": "8.54.0", - "@typescript-eslint/visitor-keys": "8.54.0", + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/type-utils": "8.56.1", + "@typescript-eslint/utils": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" @@ -3894,8 +3905,8 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.54.0", - "eslint": "^8.57.0 || ^9.0.0", + "@typescript-eslint/parser": "^8.56.1", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, @@ -3910,16 +3921,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.54.0.tgz", - "integrity": "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.1.tgz", + "integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.54.0", - "@typescript-eslint/types": "8.54.0", - "@typescript-eslint/typescript-estree": "8.54.0", - "@typescript-eslint/visitor-keys": "8.54.0", + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", "debug": "^4.4.3" }, "engines": { @@ -3930,19 +3941,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.54.0.tgz", - "integrity": "sha512-YPf+rvJ1s7MyiWM4uTRhE4DvBXrEV+d8oC3P9Y2eT7S+HBS0clybdMIPnhiATi9vZOYDc7OQ1L/i6ga6NFYK/g==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.56.1.tgz", + "integrity": "sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.54.0", - "@typescript-eslint/types": "^8.54.0", + "@typescript-eslint/tsconfig-utils": "^8.56.1", + "@typescript-eslint/types": "^8.56.1", "debug": "^4.4.3" }, "engines": { @@ -3957,14 +3968,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.54.0.tgz", - "integrity": "sha512-27rYVQku26j/PbHYcVfRPonmOlVI6gihHtXFbTdB5sb6qA0wdAQAbyXFVarQ5t4HRojIz64IV90YtsjQSSGlQg==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.56.1.tgz", + "integrity": "sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.54.0", - "@typescript-eslint/visitor-keys": "8.54.0" + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3975,9 +3986,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.54.0.tgz", - "integrity": "sha512-dRgOyT2hPk/JwxNMZDsIXDgyl9axdJI3ogZ2XWhBPsnZUv+hPesa5iuhdYt2gzwA9t8RE5ytOJ6xB0moV0Ujvw==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.1.tgz", + "integrity": "sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==", "dev": true, "license": "MIT", "engines": { @@ -3992,15 +4003,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.54.0.tgz", - "integrity": "sha512-hiLguxJWHjjwL6xMBwD903ciAwd7DmK30Y9Axs/etOkftC3ZNN9K44IuRD/EB08amu+Zw6W37x9RecLkOo3pMA==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.56.1.tgz", + "integrity": "sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.54.0", - "@typescript-eslint/typescript-estree": "8.54.0", - "@typescript-eslint/utils": "8.54.0", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1", + "@typescript-eslint/utils": "8.56.1", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, @@ -4012,14 +4023,14 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.54.0.tgz", - "integrity": "sha512-PDUI9R1BVjqu7AUDsRBbKMtwmjWcn4J3le+5LpcFgWULN3LvHC5rkc9gCVxbrsrGmO1jfPybN5s6h4Jy+OnkAA==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.1.tgz", + "integrity": "sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==", "dev": true, "license": "MIT", "engines": { @@ -4031,18 +4042,18 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.54.0.tgz", - "integrity": "sha512-BUwcskRaPvTk6fzVWgDPdUndLjB87KYDrN5EYGetnktoeAvPtO4ONHlAZDnj5VFnUANg0Sjm7j4usBlnoVMHwA==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.1.tgz", + "integrity": "sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.54.0", - "@typescript-eslint/tsconfig-utils": "8.54.0", - "@typescript-eslint/types": "8.54.0", - "@typescript-eslint/visitor-keys": "8.54.0", + "@typescript-eslint/project-service": "8.56.1", + "@typescript-eslint/tsconfig-utils": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", "debug": "^4.4.3", - "minimatch": "^9.0.5", + "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" @@ -4058,36 +4069,49 @@ "typescript": ">=4.8.4 <6.0.0" } }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz", + "integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.2.tgz", + "integrity": "sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^5.0.2" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "dev": true, "license": "ISC", "bin": { @@ -4098,16 +4122,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.54.0.tgz", - "integrity": "sha512-9Cnda8GS57AQakvRyG0PTejJNlA2xhvyNtEVIMlDWOOeEyBkYWhGPnfrIAnqxLMTSTo6q8g12XVjjev5l1NvMA==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.1.tgz", + "integrity": "sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.54.0", - "@typescript-eslint/types": "8.54.0", - "@typescript-eslint/typescript-estree": "8.54.0" + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4117,19 +4141,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.54.0.tgz", - "integrity": "sha512-VFlhGSl4opC0bprJiItPQ1RfUhGDIBokcPwaFH4yiBCaNPeld/9VeXbiPO1cLyorQi1G1vL+ecBk1x8o1axORA==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.1.tgz", + "integrity": "sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.54.0", - "eslint-visitor-keys": "^4.2.1" + "@typescript-eslint/types": "8.56.1", + "eslint-visitor-keys": "^5.0.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4139,6 +4163,19 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@vitejs/plugin-react": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.2.tgz", @@ -4160,6 +4197,37 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, + "node_modules/@vitest/coverage-v8": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.0.18.tgz", + "integrity": "sha512-7i+N2i0+ME+2JFZhfuz7Tg/FqKtilHjGyGvoHYQ6iLV0zahbsJ9sljC9OcFcPDbhYKCet+sG8SsVqlyGvPflZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^1.0.2", + "@vitest/utils": "4.0.18", + "ast-v8-to-istanbul": "^0.3.10", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.2.0", + "magicast": "^0.5.1", + "obug": "^2.1.1", + "std-env": "^3.10.0", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "4.0.18", + "vitest": "4.0.18" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, "node_modules/@vitest/expect": { "version": "4.0.18", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz", @@ -4327,9 +4395,9 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", "dependencies": { @@ -4437,6 +4505,25 @@ "node": ">=12" } }, + "node_modules/ast-v8-to-istanbul": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.11.tgz", + "integrity": "sha512-Qya9fkoofMjCBNVdWINMjB5KZvkYfaO9/anwkWnjxibpWUxo5iHl2sOdP7/uAqaRuUYuoo8rDwnbaaKVFxoUvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -4481,13 +4568,13 @@ } }, "node_modules/axios": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.4.tgz", - "integrity": "sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg==", + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz", + "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.4", + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", "proxy-from-env": "^1.1.0" } }, @@ -5272,9 +5359,9 @@ } }, "node_modules/eslint": { - "version": "9.39.2", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", - "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", + "version": "9.39.3", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.3.tgz", + "integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==", "dev": true, "license": "MIT", "dependencies": { @@ -5284,7 +5371,7 @@ "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.39.2", + "@eslint/js": "9.39.3", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", @@ -5860,6 +5947,13 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, "node_modules/http-proxy-agent": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", @@ -6030,6 +6124,45 @@ "dev": true, "license": "ISC" }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/jiti": { "version": "1.21.7", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", @@ -6255,6 +6388,47 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/magicast": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.2.tgz", + "integrity": "sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "source-map-js": "^1.2.1" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -6327,9 +6501,9 @@ } }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz", + "integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==", "dev": true, "license": "ISC", "dependencies": { @@ -7698,16 +7872,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.54.0.tgz", - "integrity": "sha512-CKsJ+g53QpsNPqbzUsfKVgd3Lny4yKZ1pP4qN3jdMOg/sisIDLGyDMezycquXLE5JsEU0wp3dGNdzig0/fmSVQ==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.56.1.tgz", + "integrity": "sha512-U4lM6pjmBX7J5wk4szltF7I1cGBHXZopnAXCMXb3+fZ3B/0Z3hq3wS/CCUB2NZBNAExK92mCU2tEohWuwVMsDQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.54.0", - "@typescript-eslint/parser": "8.54.0", - "@typescript-eslint/typescript-estree": "8.54.0", - "@typescript-eslint/utils": "8.54.0" + "@typescript-eslint/eslint-plugin": "8.56.1", + "@typescript-eslint/parser": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1", + "@typescript-eslint/utils": "8.56.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -7717,7 +7891,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, diff --git a/package.json b/package.json index d057be6..815d0ba 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "@radix-ui/react-toast": "^1.2.15", "@sentry/react": "^10.39.0", "@tanstack/react-query": "^5.90.12", - "axios": "^1.13.2", + "axios": "^1.13.5", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "date-fns": "^4.1.0", @@ -51,6 +51,7 @@ "@types/react": "^19.2.5", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^5.1.1", + "@vitest/coverage-v8": "^4.0.18", "@vitest/ui": "^4.0.18", "autoprefixer": "^10.4.23", "eslint": "^9.39.1", diff --git a/public/admin-icon.svg b/public/admin-icon.svg new file mode 100644 index 0000000..ee6fe69 --- /dev/null +++ b/public/admin-icon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/vite.svg b/public/vite.svg deleted file mode 100644 index e7b8dfb..0000000 --- a/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/components/DebugLogin.tsx b/src/components/DebugLogin.tsx deleted file mode 100644 index c676022..0000000 --- a/src/components/DebugLogin.tsx +++ /dev/null @@ -1,27 +0,0 @@ -// Temporary debug component - Remove after fixing login issue -import { useEffect } from 'react' - -export function DebugLogin() { - useEffect(() => { - console.log('=== LOGIN DEBUG INFO ===') - console.log('Current path:', window.location.pathname) - console.log('Access token:', localStorage.getItem('access_token')) - console.log('User:', localStorage.getItem('user')) - console.log('Token exists:', !!localStorage.getItem('access_token')) - - // Parse user if exists - const userStr = localStorage.getItem('user') - if (userStr) { - try { - const user = JSON.parse(userStr) - console.log('User role:', user.role) - console.log('Is admin:', user.role === 'ADMIN') - } catch (e) { - console.error('Failed to parse user:', e) - } - } - console.log('========================') - }, []) - - return null -} diff --git a/src/components/__tests__/ErrorBoundary.test.tsx b/src/components/__tests__/ErrorBoundary.test.tsx new file mode 100644 index 0000000..242e615 --- /dev/null +++ b/src/components/__tests__/ErrorBoundary.test.tsx @@ -0,0 +1,92 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { render, screen } from '@/test/test-utils' +import { ErrorBoundary } from '../ErrorBoundary' +import { Sentry } from '@/lib/sentry' + +// Mock Sentry +vi.mock('@/lib/sentry', () => ({ + Sentry: { + captureException: vi.fn(), + }, +})) + +// Component that throws an error +const ThrowError = ({ shouldThrow }: { shouldThrow: boolean }) => { + if (shouldThrow) { + throw new Error('Test error') + } + return
No error
+} + +describe('ErrorBoundary', () => { + beforeEach(() => { + vi.clearAllMocks() + // Suppress console.error for cleaner test output + vi.spyOn(console, 'error').mockImplementation(() => {}) + }) + + it('should render children when there is no error', () => { + render( + +
Test Content
+
+ ) + + expect(screen.getByText('Test Content')).toBeInTheDocument() + }) + + it('should render error UI when an error is thrown', () => { + render( + + + + ) + + expect(screen.getByText('Something went wrong')).toBeInTheDocument() + expect(screen.getByText(/An unexpected error occurred/i)).toBeInTheDocument() + expect(screen.getByRole('button', { name: /Go to Home/i })).toBeInTheDocument() + expect(screen.getByRole('button', { name: /Refresh Page/i })).toBeInTheDocument() + }) + + it('should log error to Sentry', () => { + render( + + + + ) + + expect(Sentry.captureException).toHaveBeenCalled() + }) + + it('should show error message in development mode', () => { + const originalEnv = import.meta.env.DEV + import.meta.env.DEV = true + + render( + + + + ) + + expect(screen.getByText('Test error')).toBeInTheDocument() + + import.meta.env.DEV = originalEnv + }) + + it('should handle refresh button click', () => { + render( + + + + ) + + const refreshButton = screen.getByRole('button', { name: /Refresh Page/i }) + + // Verify button exists and is clickable + expect(refreshButton).toBeInTheDocument() + expect(refreshButton).toBeEnabled() + + // Note: We can't easily test window.location.reload() in jsdom + // The button's onClick handler calls window.location.reload() which is tested manually + }) +}) diff --git a/src/components/ui/badge-variants.ts b/src/components/ui/badge-variants.ts new file mode 100644 index 0000000..1bd9724 --- /dev/null +++ b/src/components/ui/badge-variants.ts @@ -0,0 +1,21 @@ +import { cva } from "class-variance-authority" + +export const badgeVariants = cva( + "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80", + secondary: + "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: + "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80", + outline: "text-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) diff --git a/src/components/ui/badge.tsx b/src/components/ui/badge.tsx index e87d62b..0abefc2 100644 --- a/src/components/ui/badge.tsx +++ b/src/components/ui/badge.tsx @@ -1,27 +1,8 @@ import * as React from "react" -import { cva, type VariantProps } from "class-variance-authority" +import { type VariantProps } from "class-variance-authority" import { cn } from "@/lib/utils" - -const badgeVariants = cva( - "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", - { - variants: { - variant: { - default: - "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80", - secondary: - "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", - destructive: - "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80", - outline: "text-foreground", - }, - }, - defaultVariants: { - variant: "default", - }, - } -) +import { badgeVariants } from "./badge-variants" export interface BadgeProps extends React.HTMLAttributes, @@ -33,4 +14,4 @@ function Badge({ className, variant, ...props }: BadgeProps) { ) } -export { Badge, badgeVariants } +export { Badge } diff --git a/src/components/ui/button-variants.ts b/src/components/ui/button-variants.ts new file mode 100644 index 0000000..7f96851 --- /dev/null +++ b/src/components/ui/button-variants.ts @@ -0,0 +1,31 @@ +import { cva } from "class-variance-authority" + +export const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", + { + variants: { + variant: { + default: + "bg-primary text-primary-foreground shadow hover:bg-primary/90", + destructive: + "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", + outline: + "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", + secondary: + "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2", + sm: "h-8 rounded-md px-3 text-xs", + lg: "h-10 rounded-md px-8", + icon: "h-9 w-9", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index 65d4fcd..25fa2bd 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -1,38 +1,9 @@ import * as React from "react" import { Slot } from "@radix-ui/react-slot" -import { cva, type VariantProps } from "class-variance-authority" +import { type VariantProps } from "class-variance-authority" import { cn } from "@/lib/utils" - -const buttonVariants = cva( - "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", - { - variants: { - variant: { - default: - "bg-primary text-primary-foreground shadow hover:bg-primary/90", - destructive: - "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", - outline: - "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", - secondary: - "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", - ghost: "hover:bg-accent hover:text-accent-foreground", - link: "text-primary underline-offset-4 hover:underline", - }, - size: { - default: "h-9 px-4 py-2", - sm: "h-8 rounded-md px-3 text-xs", - lg: "h-10 rounded-md px-8", - icon: "h-9 w-9", - }, - }, - defaultVariants: { - variant: "default", - size: "default", - }, - } -) +import { buttonVariants } from "./button-variants" export interface ButtonProps extends React.ButtonHTMLAttributes, @@ -54,4 +25,4 @@ const Button = React.forwardRef( ) Button.displayName = "Button" -export { Button, buttonVariants } +export { Button } diff --git a/src/layouts/app-shell.tsx b/src/layouts/app-shell.tsx index bac8caf..e264b7f 100644 --- a/src/layouts/app-shell.tsx +++ b/src/layouts/app-shell.tsx @@ -1,5 +1,5 @@ import { Outlet, Link, useLocation, useNavigate } from "react-router-dom" -import { useEffect, useState } from "react" +import { useState } from "react" import { LayoutDashboard, Users, @@ -53,19 +53,21 @@ const adminNavigationItems = [ export function AppShell() { const location = useLocation() const navigate = useNavigate() - const [user, setUser] = useState(null) const [searchQuery, setSearchQuery] = useState("") - - useEffect(() => { + + // Initialize user from localStorage + const [user] = useState(() => { const userStr = localStorage.getItem('user') if (userStr) { try { - setUser(JSON.parse(userStr)) + return JSON.parse(userStr) } catch (error) { console.error('Failed to parse user data:', error) + return null } } - }, []) + return null + }) const isActive = (path: string) => { return location.pathname.startsWith(path) @@ -98,7 +100,6 @@ export function AppShell() { } const handleNotificationClick = () => { - console.log('Notification button clicked') navigate('/notifications') } diff --git a/src/lib/__tests__/utils.test.ts b/src/lib/__tests__/utils.test.ts index 4e3bb8f..dbc545b 100644 --- a/src/lib/__tests__/utils.test.ts +++ b/src/lib/__tests__/utils.test.ts @@ -1,25 +1,50 @@ import { describe, it, expect } from 'vitest' import { cn } from '../utils' -describe('utils', () => { - describe('cn', () => { - it('should merge class names correctly', () => { - const result = cn('text-red-500', 'bg-blue-500') - expect(result).toContain('text-red-500') - expect(result).toContain('bg-blue-500') - }) +describe('cn utility', () => { + it('should merge class names', () => { + const result = cn('class1', 'class2') + expect(result).toBe('class1 class2') + }) - it('should handle conditional classes', () => { - const result = cn('base-class', false && 'hidden', true && 'visible') - expect(result).toContain('base-class') - expect(result).toContain('visible') - expect(result).not.toContain('hidden') - }) + it('should handle conditional classes', () => { + const isConditional = true + const isHidden = false + const result = cn('base', isConditional && 'conditional', isHidden && 'hidden') + expect(result).toBe('base conditional') + }) - it('should merge conflicting Tailwind classes', () => { - const result = cn('p-4', 'p-8') - // tailwind-merge should keep only p-8 - expect(result).toBe('p-8') + it('should merge Tailwind classes correctly', () => { + const result = cn('px-2 py-1', 'px-4') + expect(result).toBe('py-1 px-4') + }) + + it('should handle arrays of classes', () => { + const result = cn(['class1', 'class2'], 'class3') + expect(result).toBe('class1 class2 class3') + }) + + it('should handle objects with boolean values', () => { + const result = cn({ + 'class1': true, + 'class2': false, + 'class3': true, }) + expect(result).toBe('class1 class3') + }) + + it('should handle undefined and null values', () => { + const result = cn('class1', undefined, null, 'class2') + expect(result).toBe('class1 class2') + }) + + it('should handle empty input', () => { + const result = cn() + expect(result).toBe('') + }) + + it('should merge conflicting Tailwind classes', () => { + const result = cn('text-red-500', 'text-blue-500') + expect(result).toBe('text-blue-500') }) }) diff --git a/src/pages/activity-log/index.tsx b/src/pages/activity-log/index.tsx index 2b0137f..9c83c47 100644 --- a/src/pages/activity-log/index.tsx +++ b/src/pages/activity-log/index.tsx @@ -13,7 +13,7 @@ import { TableRow, } from "@/components/ui/table" import { Search, Download, Eye, ChevronLeft, ChevronRight } from "lucide-react" -import { auditService } from "@/services" +import { auditService, type AuditLog } from "@/services" import { format } from "date-fns" export default function ActivityLogPage() { @@ -26,7 +26,7 @@ export default function ActivityLogPage() { const { data: auditData, isLoading } = useQuery({ queryKey: ['activity-log', page, limit, search, actionFilter, resourceTypeFilter], queryFn: async () => { - const params: any = { page, limit } + const params: Record = { page, limit } if (search) params.search = search if (actionFilter) params.action = actionFilter if (resourceTypeFilter) params.resourceType = resourceTypeFilter @@ -130,7 +130,7 @@ export default function ActivityLogPage() { - {auditData?.data?.map((log: any) => ( + {auditData?.data?.map((log: AuditLog) => ( {log.id} {log.userId || 'N/A'} @@ -143,7 +143,7 @@ export default function ActivityLogPage() { {log.resourceId} {log.ipAddress || 'N/A'} - {format(new Date(log.timestamp || log.createdAt), 'MMM dd, yyyy HH:mm:ss')} + {format(new Date(log.timestamp), 'MMM dd, yyyy HH:mm:ss')}