From b0b7196518a07c6daf00eef2b7b9fd33b4c5ac3e Mon Sep 17 00:00:00 2001 From: Timofey Gulenok Date: Wed, 12 Aug 2026 19:30:17 +0300 Subject: [PATCH] upd --- .DS_Store | Bin 0 -> 6148 bytes develop-ctf-platform-frontend3.zip | Bin 757656 -> 0 bytes index.html | 23 + package-lock.json | 4907 +++++++++++++++++ package.json | 55 + src/App.tsx | 52 + src/api/axios.ts | 209 + src/api/endpoints/admin.api.ts | 251 + src/api/endpoints/analytics.api.ts | 259 + src/api/endpoints/auth.api.ts | 160 + src/api/endpoints/flags.api.ts | 178 + src/api/endpoints/index.ts | 19 + src/api/endpoints/logs.api.ts | 220 + src/api/endpoints/matches.api.ts | 193 + src/api/endpoints/notifications.api.ts | 125 + src/api/endpoints/rounds.api.ts | 148 + src/api/endpoints/scoreboard.api.ts | 182 + src/api/endpoints/seasons.api.ts | 159 + src/api/endpoints/services.api.ts | 230 + src/api/endpoints/teams.api.ts | 194 + src/api/endpoints/training.api.ts | 244 + src/api/endpoints/users.api.ts | 173 + src/api/types/admin.types.ts | 312 ++ src/api/types/analytics.types.ts | 483 ++ src/api/types/auth.types.ts | 135 + src/api/types/common.types.ts | 71 + src/api/types/flag.types.ts | 127 + src/api/types/index.ts | 38 + src/api/types/log.types.ts | 208 + src/api/types/match.types.ts | 196 + src/api/types/notification.types.ts | 133 + src/api/types/round.types.ts | 136 + src/api/types/scoreboard.types.ts | 138 + src/api/types/season.types.ts | 186 + src/api/types/service.types.ts | 240 + src/api/types/team.types.ts | 168 + src/api/types/training.types.ts | 326 ++ src/api/websocket.ts | 396 ++ .../analytics/AIRecommendationCard.tsx | 119 + src/components/analytics/AnalyticsCard.tsx | 57 + src/components/analytics/AttackHeatmap.tsx | 97 + src/components/analytics/PerformanceTrend.tsx | 112 + src/components/analytics/SLAChart.tsx | 160 + .../analytics/TeamComparisonChart.tsx | 98 + src/components/analytics/index.ts | 7 + src/components/auth/LoginForm.tsx | 139 + src/components/auth/ProtectedRoute.tsx | 39 + src/components/auth/RegisterForm.tsx | 251 + src/components/auth/RoleGuard.tsx | 79 + src/components/auth/index.ts | 5 + src/components/common/Breadcrumbs.tsx | 115 + src/components/common/ConfirmDialog.tsx | 150 + src/components/common/CopyButton.tsx | 85 + src/components/common/Countdown.tsx | 133 + src/components/common/DataTable.tsx | 267 + src/components/common/EmptyState.tsx | 42 + src/components/common/ErrorBoundary.tsx | 69 + src/components/common/LoadingSkeleton.tsx | 120 + src/components/common/LoadingSpinner.tsx | 44 + src/components/common/MarkdownRenderer.tsx | 126 + src/components/common/SearchInput.tsx | 119 + src/components/common/StatusBadge.tsx | 152 + src/components/common/ThemeToggle.tsx | 43 + src/components/common/index.ts | 23 + src/components/layout/AppLayout.tsx | 59 + src/components/layout/Footer.tsx | 175 + src/components/layout/Header.tsx | 110 + src/components/layout/MobileNav.tsx | 109 + src/components/layout/Sidebar.tsx | 199 + src/components/layout/index.ts | 6 + src/components/logs/EventTimeline.tsx | 75 + src/components/logs/LogEntry.tsx | 69 + src/components/logs/LogFilter.tsx | 151 + src/components/logs/LogStream.tsx | 77 + src/components/logs/ReplayControls.tsx | 123 + src/components/logs/index.ts | 6 + src/components/match/FlagSubmitForm.tsx | 157 + src/components/match/MatchCard.tsx | 109 + src/components/match/MatchConfigForm.tsx | 329 ++ src/components/match/MatchStatusBadge.tsx | 28 + src/components/match/MatchTimer.tsx | 105 + src/components/match/RoundIndicator.tsx | 73 + src/components/match/ServiceStatusGrid.tsx | 94 + src/components/match/index.ts | 8 + src/components/profile/AchievementBadge.tsx | 77 + src/components/profile/ActivityFeed.tsx | 48 + src/components/profile/ProfileStats.tsx | 76 + src/components/profile/index.ts | 4 + src/components/scoreboard/ScoreBreakdown.tsx | 109 + src/components/scoreboard/ScoreChart.tsx | 150 + src/components/scoreboard/ScoreboardTable.tsx | 169 + src/components/scoreboard/TeamScoreCard.tsx | 124 + src/components/scoreboard/index.ts | 5 + src/components/services/ServiceCard.tsx | 105 + src/components/services/ServiceFilter.tsx | 204 + .../services/ServiceStatusIndicator.tsx | 47 + src/components/services/index.ts | 4 + src/components/team/TeamInviteForm.tsx | 182 + src/components/team/TeamMatchHistory.tsx | 77 + src/components/team/TeamMemberCard.tsx | 91 + src/components/team/TeamRoleSelector.tsx | 69 + src/components/team/index.ts | 5 + src/components/training/ExerciseCard.tsx | 111 + src/components/training/HintAccordion.tsx | 112 + src/components/training/ProgressBar.tsx | 57 + src/components/training/RoleBadge.tsx | 35 + src/components/training/SkillRadarChart.tsx | 174 + src/components/training/TrackCard.tsx | 110 + src/components/training/index.ts | 7 + src/hooks/index.ts | 18 + src/hooks/useAuth.ts | 137 + src/hooks/useDebounce.ts | 105 + src/hooks/useLogs.ts | 169 + src/hooks/useMatchTimer.ts | 240 + src/hooks/useNotifications.ts | 171 + src/hooks/usePagination.ts | 155 + src/hooks/useReplay.ts | 183 + src/hooks/useScoreboard.ts | 164 + src/hooks/useWebSocket.ts | 170 + src/main.tsx | 10 + src/pages/NotFoundPage.tsx | 45 + src/pages/auth/ForgotPasswordPage.tsx | 137 + src/pages/auth/LoginPage.tsx | 184 + src/pages/auth/RegisterPage.tsx | 54 + src/pages/dashboard/DashboardPage.tsx | 268 + src/pages/matches/MatchDetailPage.tsx | 150 + src/pages/matches/MatchListPage.tsx | 100 + src/pages/matches/MatchLivePage.tsx | 159 + src/pages/matches/index.ts | 4 + src/pages/scoreboard/ScoreboardPage.tsx | 150 + src/pages/scoreboard/index.ts | 2 + src/router/index.tsx | 245 + src/store/authStore.ts | 170 + src/store/index.ts | 10 + src/store/logStore.ts | 265 + src/store/matchStore.ts | 208 + src/store/notificationStore.ts | 155 + src/store/scoreboardStore.ts | 194 + src/store/serviceStore.ts | 218 + src/store/teamStore.ts | 240 + src/store/trainingStore.ts | 245 + src/store/uiStore.ts | 177 + src/styles/globals.css | 382 ++ src/utils/colors.ts | 175 + src/utils/constants.ts | 260 + src/utils/formatters.ts | 200 + src/utils/helpers.ts | 269 + src/utils/validators.ts | 192 + src/vite-env.d.ts | 10 + tailwind.config.js | 78 + tsconfig.json | 31 + vite.config.ts | 19 + 152 files changed, 24657 insertions(+) create mode 100644 .DS_Store delete mode 100644 develop-ctf-platform-frontend3.zip create mode 100644 index.html create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/App.tsx create mode 100644 src/api/axios.ts create mode 100644 src/api/endpoints/admin.api.ts create mode 100644 src/api/endpoints/analytics.api.ts create mode 100644 src/api/endpoints/auth.api.ts create mode 100644 src/api/endpoints/flags.api.ts create mode 100644 src/api/endpoints/index.ts create mode 100644 src/api/endpoints/logs.api.ts create mode 100644 src/api/endpoints/matches.api.ts create mode 100644 src/api/endpoints/notifications.api.ts create mode 100644 src/api/endpoints/rounds.api.ts create mode 100644 src/api/endpoints/scoreboard.api.ts create mode 100644 src/api/endpoints/seasons.api.ts create mode 100644 src/api/endpoints/services.api.ts create mode 100644 src/api/endpoints/teams.api.ts create mode 100644 src/api/endpoints/training.api.ts create mode 100644 src/api/endpoints/users.api.ts create mode 100644 src/api/types/admin.types.ts create mode 100644 src/api/types/analytics.types.ts create mode 100644 src/api/types/auth.types.ts create mode 100644 src/api/types/common.types.ts create mode 100644 src/api/types/flag.types.ts create mode 100644 src/api/types/index.ts create mode 100644 src/api/types/log.types.ts create mode 100644 src/api/types/match.types.ts create mode 100644 src/api/types/notification.types.ts create mode 100644 src/api/types/round.types.ts create mode 100644 src/api/types/scoreboard.types.ts create mode 100644 src/api/types/season.types.ts create mode 100644 src/api/types/service.types.ts create mode 100644 src/api/types/team.types.ts create mode 100644 src/api/types/training.types.ts create mode 100644 src/api/websocket.ts create mode 100644 src/components/analytics/AIRecommendationCard.tsx create mode 100644 src/components/analytics/AnalyticsCard.tsx create mode 100644 src/components/analytics/AttackHeatmap.tsx create mode 100644 src/components/analytics/PerformanceTrend.tsx create mode 100644 src/components/analytics/SLAChart.tsx create mode 100644 src/components/analytics/TeamComparisonChart.tsx create mode 100644 src/components/analytics/index.ts create mode 100644 src/components/auth/LoginForm.tsx create mode 100644 src/components/auth/ProtectedRoute.tsx create mode 100644 src/components/auth/RegisterForm.tsx create mode 100644 src/components/auth/RoleGuard.tsx create mode 100644 src/components/auth/index.ts create mode 100644 src/components/common/Breadcrumbs.tsx create mode 100644 src/components/common/ConfirmDialog.tsx create mode 100644 src/components/common/CopyButton.tsx create mode 100644 src/components/common/Countdown.tsx create mode 100644 src/components/common/DataTable.tsx create mode 100644 src/components/common/EmptyState.tsx create mode 100644 src/components/common/ErrorBoundary.tsx create mode 100644 src/components/common/LoadingSkeleton.tsx create mode 100644 src/components/common/LoadingSpinner.tsx create mode 100644 src/components/common/MarkdownRenderer.tsx create mode 100644 src/components/common/SearchInput.tsx create mode 100644 src/components/common/StatusBadge.tsx create mode 100644 src/components/common/ThemeToggle.tsx create mode 100644 src/components/common/index.ts create mode 100644 src/components/layout/AppLayout.tsx create mode 100644 src/components/layout/Footer.tsx create mode 100644 src/components/layout/Header.tsx create mode 100644 src/components/layout/MobileNav.tsx create mode 100644 src/components/layout/Sidebar.tsx create mode 100644 src/components/layout/index.ts create mode 100644 src/components/logs/EventTimeline.tsx create mode 100644 src/components/logs/LogEntry.tsx create mode 100644 src/components/logs/LogFilter.tsx create mode 100644 src/components/logs/LogStream.tsx create mode 100644 src/components/logs/ReplayControls.tsx create mode 100644 src/components/logs/index.ts create mode 100644 src/components/match/FlagSubmitForm.tsx create mode 100644 src/components/match/MatchCard.tsx create mode 100644 src/components/match/MatchConfigForm.tsx create mode 100644 src/components/match/MatchStatusBadge.tsx create mode 100644 src/components/match/MatchTimer.tsx create mode 100644 src/components/match/RoundIndicator.tsx create mode 100644 src/components/match/ServiceStatusGrid.tsx create mode 100644 src/components/match/index.ts create mode 100644 src/components/profile/AchievementBadge.tsx create mode 100644 src/components/profile/ActivityFeed.tsx create mode 100644 src/components/profile/ProfileStats.tsx create mode 100644 src/components/profile/index.ts create mode 100644 src/components/scoreboard/ScoreBreakdown.tsx create mode 100644 src/components/scoreboard/ScoreChart.tsx create mode 100644 src/components/scoreboard/ScoreboardTable.tsx create mode 100644 src/components/scoreboard/TeamScoreCard.tsx create mode 100644 src/components/scoreboard/index.ts create mode 100644 src/components/services/ServiceCard.tsx create mode 100644 src/components/services/ServiceFilter.tsx create mode 100644 src/components/services/ServiceStatusIndicator.tsx create mode 100644 src/components/services/index.ts create mode 100644 src/components/team/TeamInviteForm.tsx create mode 100644 src/components/team/TeamMatchHistory.tsx create mode 100644 src/components/team/TeamMemberCard.tsx create mode 100644 src/components/team/TeamRoleSelector.tsx create mode 100644 src/components/team/index.ts create mode 100644 src/components/training/ExerciseCard.tsx create mode 100644 src/components/training/HintAccordion.tsx create mode 100644 src/components/training/ProgressBar.tsx create mode 100644 src/components/training/RoleBadge.tsx create mode 100644 src/components/training/SkillRadarChart.tsx create mode 100644 src/components/training/TrackCard.tsx create mode 100644 src/components/training/index.ts create mode 100644 src/hooks/index.ts create mode 100644 src/hooks/useAuth.ts create mode 100644 src/hooks/useDebounce.ts create mode 100644 src/hooks/useLogs.ts create mode 100644 src/hooks/useMatchTimer.ts create mode 100644 src/hooks/useNotifications.ts create mode 100644 src/hooks/usePagination.ts create mode 100644 src/hooks/useReplay.ts create mode 100644 src/hooks/useScoreboard.ts create mode 100644 src/hooks/useWebSocket.ts create mode 100644 src/main.tsx create mode 100644 src/pages/NotFoundPage.tsx create mode 100644 src/pages/auth/ForgotPasswordPage.tsx create mode 100644 src/pages/auth/LoginPage.tsx create mode 100644 src/pages/auth/RegisterPage.tsx create mode 100644 src/pages/dashboard/DashboardPage.tsx create mode 100644 src/pages/matches/MatchDetailPage.tsx create mode 100644 src/pages/matches/MatchListPage.tsx create mode 100644 src/pages/matches/MatchLivePage.tsx create mode 100644 src/pages/matches/index.ts create mode 100644 src/pages/scoreboard/ScoreboardPage.tsx create mode 100644 src/pages/scoreboard/index.ts create mode 100644 src/router/index.tsx create mode 100644 src/store/authStore.ts create mode 100644 src/store/index.ts create mode 100644 src/store/logStore.ts create mode 100644 src/store/matchStore.ts create mode 100644 src/store/notificationStore.ts create mode 100644 src/store/scoreboardStore.ts create mode 100644 src/store/serviceStore.ts create mode 100644 src/store/teamStore.ts create mode 100644 src/store/trainingStore.ts create mode 100644 src/store/uiStore.ts create mode 100644 src/styles/globals.css create mode 100644 src/utils/colors.ts create mode 100644 src/utils/constants.ts create mode 100644 src/utils/formatters.ts create mode 100644 src/utils/helpers.ts create mode 100644 src/utils/validators.ts create mode 100644 src/vite-env.d.ts create mode 100644 tailwind.config.js create mode 100644 tsconfig.json create mode 100644 vite.config.ts diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..5008ddfcf53c02e82d7eee2e57c38e5672ef89f6 GIT binary patch literal 6148 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
    QD?DaanW?ckLH=+mN;F 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 r$Pd$QyJx6oRWESaSy#;PDA&0rr? + + + + + + + + + + Врата ADA - CTF Attack-Defence Platform + + + +

    + + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..c016c89 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,4907 @@ +{ + "name": "react-vite-tailwind", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "react-vite-tailwind", + "version": "0.0.0", + "dependencies": { + "@hookform/resolvers": "^5.7.1", + "@radix-ui/react-accordion": "^1.2.20", + "@radix-ui/react-avatar": "^1.2.6", + "@radix-ui/react-checkbox": "^1.3.11", + "@radix-ui/react-dialog": "^1.1.23", + "@radix-ui/react-dropdown-menu": "^2.1.24", + "@radix-ui/react-label": "^2.1.15", + "@radix-ui/react-popover": "^1.1.23", + "@radix-ui/react-progress": "^1.1.16", + "@radix-ui/react-scroll-area": "^1.2.18", + "@radix-ui/react-select": "^2.3.7", + "@radix-ui/react-separator": "^1.1.15", + "@radix-ui/react-slot": "^1.3.3", + "@radix-ui/react-switch": "^1.3.7", + "@radix-ui/react-tabs": "^1.1.21", + "@radix-ui/react-tooltip": "^1.2.16", + "@tanstack/react-table": "^9.1.2", + "axios": "^1.19.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^1.31.0", + "react": "19.2.6", + "react-dom": "19.2.6", + "react-hook-form": "^7.85.0", + "react-hot-toast": "^2.6.0", + "react-router-dom": "^7.18.2", + "recharts": "^3.10.1", + "socket.io-client": "^4.8.3", + "tailwind-merge": "^3.4.0", + "zod": "^4.4.3", + "zustand": "^5.0.14" + }, + "devDependencies": { + "@tailwindcss/vite": "4.1.17", + "@types/node": "22.19.17", + "@types/react": "19.2.7", + "@types/react-dom": "19.2.3", + "@vitejs/plugin-react": "5.1.1", + "tailwindcss": "4.1.17", + "typescript": "5.9.3", + "vite": "7.3.2", + "vite-plugin-singlefile": "2.3.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.8.0.tgz", + "integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.8.0.tgz", + "integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.8.0", + "@floating-ui/utils": "^0.2.12" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.9.tgz", + "integrity": "sha512-JDjEFGCpImxDCA7JJKviA0M9+RtmJdj0m/NVU5IMgBK+AmZouAQQ7/+2GLH0GXXY0YMw9oXPB8hKdbPYg5QLYg==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.8.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz", + "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==", + "license": "MIT" + }, + "node_modules/@hookform/resolvers": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.7.1.tgz", + "integrity": "sha512-8wS/P4UDr5sQDe4nFaV51TVyfDPrWgNIXweqG0Bs9Z5LSuzKLb+RQNPvkN2oHM5SRrJyWrVH/F+LOUcFjUyvwQ==", + "license": "MIT", + "dependencies": { + "@standard-schema/utils": "^0.3.0" + }, + "peerDependencies": { + "@sinclair/typebox": ">=0.25.24", + "@standard-schema/spec": "^1.0.0", + "@typeschema/main": ">=0.13.7", + "@vinejs/vine": "^2.0.0 || ^3.0.0 || ^4.0.0", + "ajv": "^8.12.0", + "ajv-errors": "^3.0.0", + "ajv-formats": "^2.1.1", + "arktype": "^2.0.0", + "ata-validator": "^1.2.0", + "class-transformer": ">=0.4.0", + "class-validator": ">=0.12.0", + "computed-types": "^1.0.0", + "effect": "^3.10.3", + "fluentvalidation-ts": "^3.0.0", + "fp-ts": "^2.7.0", + "io-ts": "^2.0.0", + "joi": "^17.0.0", + "nope-validator": ">=0.12.0", + "react-hook-form": "^7.55.0", + "superstruct": ">=0.12.0", + "typanion": "^3.3.2", + "valibot": ">=0.31.0 || ^1.0.0-beta.4 || ^1.0.0-rc", + "vest": ">=3.0.0", + "yup": "^1.0.0", + "zod": "^3.25.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "@sinclair/typebox": { + "optional": true + }, + "@standard-schema/spec": { + "optional": true + }, + "@typeschema/main": { + "optional": true + }, + "@vinejs/vine": { + "optional": true + }, + "ajv": { + "optional": true + }, + "ajv-errors": { + "optional": true + }, + "ajv-formats": { + "optional": true + }, + "arktype": { + "optional": true + }, + "ata-validator": { + "optional": true + }, + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + }, + "computed-types": { + "optional": true + }, + "effect": { + "optional": true + }, + "fluentvalidation-ts": { + "optional": true + }, + "fp-ts": { + "optional": true + }, + "io-ts": { + "optional": true + }, + "joi": { + "optional": true + }, + "nope-validator": { + "optional": true + }, + "superstruct": { + "optional": true + }, + "typanion": { + "optional": true + }, + "valibot": { + "optional": true + }, + "vest": { + "optional": true + }, + "yup": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@radix-ui/number": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.3.tgz", + "integrity": "sha512-Road2bidD0uu/1BGDOWNdPI06g0lIRy6IF9GZcIrDK2KGItfor8IQwQa+yM2ERgHM1MmHxaxpTzk0/Jp42lNfA==", + "license": "MIT" + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.7.tgz", + "integrity": "sha512-rqWnm76nYT8HoNNqEjpgJ7Pw/DrBj5iBTrmEPo6HTX5+VJyBNOqTdv4g89G63HuR5g0AaENoAcH7Is5fF2kZ8Q==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-accordion": { + "version": "1.2.20", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.20.tgz", + "integrity": "sha512-jDhG9FvAEnlhnjrsINbNXcUa4G+L1KqSkJSunkbKEzFRcAb52jvM0PjPxPRvhe1HNc5F5yc0yzzWeeqlH4yBIg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-collapsible": "1.1.20", + "@radix-ui/react-collection": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-controllable-state": "1.2.6" + }, + "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-arrow": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.15.tgz", + "integrity": "sha512-v4zggRcjadnI+ClKDuijlQEW4tw3NoaeHc/PwpKnLoLLKNUG4InLegkstooLcRIUWCs+8L22dGURCVuFfOKfnA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.10" + }, + "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-avatar": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.2.6.tgz", + "integrity": "sha512-4ULOTJ/mqy2hT9GlWa/MFHxHSvH3nJzHnZM1waNsc5Bonv7i70aNenghXmD97S6OJ81ekXONGGt4nT1r0PfEdA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-is-hydrated": "0.1.3", + "@radix-ui/react-use-layout-effect": "1.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-checkbox": { + "version": "1.3.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.11.tgz", + "integrity": "sha512-Gnptr9pDDQxD3hgq2dtPbtrp/c2qH1mBwIzw3X/ivrMb2e1t0jMTi606fVEqFPaQR1ggXIVQWKj3P2WW9v7zGQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-size": "1.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-collapsible": { + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.20.tgz", + "integrity": "sha512-mcGesGplBnzN2sbvJETzpCNfSMyPnb29q1GRLU+Ib7bJrpIG2ywmRoh2V5VbA2uNvKikKUlVbAPks7JDjz4A8Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-layout-effect": "1.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-collection": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.15.tgz", + "integrity": "sha512-9W+B9NPF0NaaPh/1NJd3+KqsnlLqU9H7T2rvww+fp+T/evVXdNAyYcnfRQZFOjkR1ajQp3yORlqnI8soawLvNA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-slot": "1.3.3" + }, + "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-compose-refs": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.5.tgz", + "integrity": "sha512-+48PbAAbq3didjJxa+OaWY2ZwgAKsNiRGyeHKszblZMQ+kcpd9pAaT11cMkGEie0vsOi3QdeTE6d5Fe3Gn61kA==", + "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-context": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.2.2.tgz", + "integrity": "sha512-RHCUGwKHDr0hDGg4X7ma4JG4/+12qxw8rkh5QKdDldlCvtja6nUx1Ef/8HVrJze81lEsgLQlqjzjGNHantgnQA==", + "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-dialog": { + "version": "1.1.23", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.23.tgz", + "integrity": "sha512-Ksw4WeROkO4rC9k/onilX/Ao2Cr1ku1unMNH+XSCcP4jSXYu7HDsg9n4ojMjVb22XpYjAQ9qfrFlVbru1vXDUA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-dismissable-layer": "1.1.19", + "@radix-ui/react-focus-guards": "1.1.6", + "@radix-ui/react-focus-scope": "1.1.16", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-portal": "1.1.17", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-slot": "1.3.3", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-layout-effect": "1.1.4", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.7.2" + }, + "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-direction": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.4.tgz", + "integrity": "sha512-5pzg4FGQNpExhnhT2zlrP1wZFaYCd1K0nYWoFAdcYoYK868IEigqMX3B3f8yIoRlAhAeDWciLI6ZdCKHF9P4Vg==", + "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-dismissable-layer": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.19.tgz", + "integrity": "sha512-8g4pfOL9HoKKLWGiypT+dphVqjFfmcXO5GBnhsG6zI+lxAx/8feQpr+1LSN8Re3hiZ+XkLNS4O9ztK11/LzQ6w==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-effect-event": "0.0.5" + }, + "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-dropdown-menu": { + "version": "2.1.24", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.24.tgz", + "integrity": "sha512-geq8l2rJkxvkXsT9RMgtUE3P8pITFpTsvYpbySi1IH4fZEABD/Gp85myayFgxk0ktljGMJnCbeFkyTusvSvv7g==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-menu": "2.1.24", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-controllable-state": "1.2.6" + }, + "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-focus-guards": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.6.tgz", + "integrity": "sha512-RNOJjfZMTyBM6xYmV3IVGXkPjIhcBAuv48POevAXwrGJhkWZ9p1rFoIS1JFooPuT193AZmRsCPhpoVJxx6OPoQ==", + "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-focus-scope": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.16.tgz", + "integrity": "sha512-wmRZ2WWLvmt6KHy2rNPOdPUjwq5xOHY02+m+udwJTn0aNIox/rkskAvJTyTLGhPK6KgrUjlJUJpgmx/+wFiFIQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.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-id": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.4.tgz", + "integrity": "sha512-TMQp2llA+RYn7JcjnrMnz7wN4pcVttPZnRZo52PLQsoLVKzNlVwUeHmfePgTgRluXFvlD3GD5g5MOVVTJCO0qA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "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-label": { + "version": "2.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.15.tgz", + "integrity": "sha512-o/rdYEwZTTo5tjknnPeyQFU45kUC4i/XyeDPP+HGyi6XqpOP6Zf5Ya5vh/Yfe9Id5JiuWnnAx2XqIeD3UYZt0g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.10" + }, + "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-menu": { + "version": "2.1.24", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.24.tgz", + "integrity": "sha512-uW7RVuU6Lp/ZtfeY4b3kL32zccgEWvPv1+cf17ubYzHa9cL8AHokmk36cG/XEiH/smbQvumnieXX9j/e9RqJWA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-collection": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-dismissable-layer": "1.1.19", + "@radix-ui/react-focus-guards": "1.1.6", + "@radix-ui/react-focus-scope": "1.1.16", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-popper": "1.3.7", + "@radix-ui/react-portal": "1.1.17", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-roving-focus": "1.1.19", + "@radix-ui/react-slot": "1.3.3", + "@radix-ui/react-use-callback-ref": "1.1.4", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.7.2" + }, + "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-popover": { + "version": "1.1.23", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.23.tgz", + "integrity": "sha512-mw58MrBlyHWFisTOYignD0vf/3gdcgAR+9of1s9G/38CbFiUwH1nCDkc0AUM9IrXFgN5Ue8n45j9WCgyM1sbiQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-dismissable-layer": "1.1.19", + "@radix-ui/react-focus-guards": "1.1.6", + "@radix-ui/react-focus-scope": "1.1.16", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-popper": "1.3.7", + "@radix-ui/react-portal": "1.1.17", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-slot": "1.3.3", + "@radix-ui/react-use-controllable-state": "1.2.6", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.7.2" + }, + "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-popper": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.3.7.tgz", + "integrity": "sha512-UsJrrd7w4wuKKTdvd/DNERVlwSlUcyXzjhyDwBk+3aPOsCjOY6ZSbxuw8E6lZTjjfP8Cpd0J8VVkrYUWyGYXyg==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-layout-effect": "1.1.4", + "@radix-ui/react-use-rect": "1.1.4", + "@radix-ui/react-use-size": "1.1.4", + "@radix-ui/rect": "1.1.3" + }, + "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-portal": { + "version": "1.1.17", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.17.tgz", + "integrity": "sha512-vKQLcWypUnwZVvfV7UkGahH2g6ySe8M8R+zYBwPrv5byZ9QAW6cQVvNKo7GgmD+p8aYb6D9JBuvy8/WhOno2wQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-layout-effect": "1.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-presence": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.10.tgz", + "integrity": "sha512-3wyzCQ6+ubRA+D4uv9m95JYLXxmOHp05qjrkjeA7uKHHtjpPggQzc6DAb0URl7j67oR0K2foO4ip27TiX037Bw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.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-primitive": { + "version": "2.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.10.tgz", + "integrity": "sha512-MucOnzh6hR5mid6VpkbglRAMYMjKLqRnGBbjXkzjK52fuQDd1qbkx78a5P40mkcnVXJdEVxm26E9OPAiUq7nBg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.3.3" + }, + "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": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.16.tgz", + "integrity": "sha512-5XnomAsoZZCY+KNTxbIghpGqPruZvKFNlvcAljVAOdDRDsH4/OZQxhtwo5wdtoDM5R6MhJBb2sPnDuRFep3lzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-primitive": "2.1.10" + }, + "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-roving-focus": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.19.tgz", + "integrity": "sha512-V9jI6hDjT7l3jsCQD9bLNvDLM3tH/gdbOTp7Tefp3hbbgCGQoK7tUvrWiRlcoBHIZ809ElXwNQwVo0B98LuTXQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-collection": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-is-hydrated": "0.1.3", + "@radix-ui/react-use-layout-effect": "1.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-scroll-area": { + "version": "1.2.18", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.18.tgz", + "integrity": "sha512-Zn5Cd171wxsO3Dfg8HaW6RifTb9CYTKQJHs/G4+LN1GfmJpaQMZQyQxMprVPHpaz7QY4l9BxK2JwQuzHsXC8nA==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.3", + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-layout-effect": "1.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-select": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.3.7.tgz", + "integrity": "sha512-WFGImkmbzcfxeIwq/+4HvRN0pizBwbwQUED4I13ezQsDdfl38ZntN6TmR8XaSzPBqoCToe8rF75j6NPNDSzhbg==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.3", + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-collection": "1.1.15", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-dismissable-layer": "1.1.19", + "@radix-ui/react-focus-guards": "1.1.6", + "@radix-ui/react-focus-scope": "1.1.16", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-popper": "1.3.7", + "@radix-ui/react-portal": "1.1.17", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-slot": "1.3.3", + "@radix-ui/react-use-callback-ref": "1.1.4", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-layout-effect": "1.1.4", + "@radix-ui/react-use-previous": "1.1.4", + "@radix-ui/react-visually-hidden": "1.2.11", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.7.2" + }, + "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-separator": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.15.tgz", + "integrity": "sha512-jOLO4lssEzWpoDu7G+Ze4VjwMRUBt291pnZD0gmalREZipnTX3wadQo7Fy48GCTfe14/YRN6rw/rOJqrE85Wxw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.10" + }, + "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-slot": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.3.3.tgz", + "integrity": "sha512-qx7oqnYbxnK9kYI9m317qmFmEgo6ywqWvbTogdj7cL9p3/yx4M48p7Rnw5z3H890cL/ow/EeWJsuTykeZVXP5Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.5" + }, + "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-switch": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.3.7.tgz", + "integrity": "sha512-48tB/4dn2UVLBCYhTu9AuR63IHl73l/qLbLgxd86noTUor4/K4LFDAcYjK+isP5313qxaFpjPVogE7+Y0/V3Kw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-size": "1.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-tabs": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.21.tgz", + "integrity": "sha512-UKxJlZid7FVtsk/WTxj4i4uSEgj2Au+KBbS7SQyTlzMhhn+86Cz3tISZdTa87bfEfcuvZezf2ZsxD4xuEKtkog==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-direction": "1.1.4", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-roving-focus": "1.1.19", + "@radix-ui/react-use-controllable-state": "1.2.6" + }, + "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-tooltip": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.16.tgz", + "integrity": "sha512-6EamKFRRnlpdadndbZ6LMwycfwkwPte1B42hs6QA0gYhjaOKqW4PZ4pjaW9UrlDX5eVt/OjncE7BFTPL5nmZhg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.5", + "@radix-ui/react-context": "1.2.2", + "@radix-ui/react-dismissable-layer": "1.1.19", + "@radix-ui/react-id": "1.1.4", + "@radix-ui/react-popper": "1.3.7", + "@radix-ui/react-portal": "1.1.17", + "@radix-ui/react-presence": "1.1.10", + "@radix-ui/react-primitive": "2.1.10", + "@radix-ui/react-slot": "1.3.3", + "@radix-ui/react-use-controllable-state": "1.2.6", + "@radix-ui/react-use-layout-effect": "1.1.4", + "@radix-ui/react-visually-hidden": "1.2.11" + }, + "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-use-callback-ref": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.4.tgz", + "integrity": "sha512-R6OUY2e2fA6Yn6s+VSx5KBV6Nx8LQEhu+cz7LCej18rQ1HLyg9PSC9jP/ZNx0o6FAIK9c0F1kHylzSxKsdlkrQ==", + "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-use-controllable-state": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.6.tgz", + "integrity": "sha512-uEQJGT97ZA/TgP/Hydw47lHu+/vQj6z/0jA+WeTbK1o9Rx45GImjpD0tc3W5ad3D6XTSR6e1yEO0FvGq6WQfVQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.7", + "@radix-ui/react-use-effect-event": "0.0.5", + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "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-use-effect-event": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.5.tgz", + "integrity": "sha512-7cshFL8HGS/7HEiHH+9kL9HBwp2sa9yX18Knwek6KYWmXwM7pegMgta2AXMQKI+rq3JnfSj9x8wYqFMTdG1Jgg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "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-use-is-hydrated": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.3.tgz", + "integrity": "sha512-umO/aJ+82CpOnhDZUTbILCQf7kU/g0iv+oGs/Q8jw7IkhWBzaEP4sA268PhFAJTFetbwp3ICc6ktpI4TqtxcIw==", + "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-use-layout-effect": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.4.tgz", + "integrity": "sha512-K20DkRkUwDnxEYMBPcg3Y6voLkEy5p5QQmszZgLngKKiC7dzBR/aEuK3w1qlx2JWDUNH6FluahYdgR3BP+QbYw==", + "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-use-previous": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.4.tgz", + "integrity": "sha512-XoSLhbRbqxFtgJoi2fNHA3C6pDlY34x508vUpUGoFZfvePfHXHbE1lC4FYFMnJWgiCRroSTw6fOsXQoVS9RwZg==", + "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-use-rect": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.4.tgz", + "integrity": "sha512-cSOCh6JlkmfjLyNcLiu2nB4v+nm+dkZ+Q5KHWk/soo4U7ZLiEQFKHK9/YmtBHjfCEaU43IBKQOc4/uJmCaiCTQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.3" + }, + "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-use-size": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.4.tgz", + "integrity": "sha512-D3anSY15EJoxrihpsXI6SMrmmonnQtR2ni7arO+Lfdg3O95b9hNXxONk8jA5C8ANdF/h5HMAxejgs8PWJ6rlhw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.4" + }, + "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-visually-hidden": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.11.tgz", + "integrity": "sha512-NFS86RYYZb4/exihaESBGOpMJFz8MGLAfu3mOBSGByVnVPC9JPASfYubxd/8KbkQK0sYAv8lVQDEQukDX/qXvQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.10" + }, + "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/rect": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.3.tgz", + "integrity": "sha512-JtyZR+mqgBibTo8xea3B6ZRmzZiM/YeVBtUkas6zMuXjAlfIFIW2FgqeM9eLyvEaYX66vr6DJMK+4U6LV0KhNw==", + "license": "MIT" + }, + "node_modules/@reduxjs/toolkit": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.12.0.tgz", + "integrity": "sha512-KiT+RzZbp6mQET+Mg+h2c97+9j1sNflUxQkIHI7Yuzf6Peu+OYpmkn6nbHWmLLWj+1ZODUJFwGZ7gx3L9R9EOw==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^11.0.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.47", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.47.tgz", + "integrity": "sha512-8QagwMH3kNCuzD8EWL8R2YPW5e4OrHNSAHRFDdmFqEwEaD/KcNKjVoumo+gP2vW5eKB2UPbM6vTYiGZX0ixLnw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.4.tgz", + "integrity": "sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.4.tgz", + "integrity": "sha512-JKuJc+pnpks2pjy7L/N3v/cAkZxYlnmuZoD840ldbMI5KDbC4iO9NKwPKYdjYFCMAIIlBzYSFHxIJVYzRo2/8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.4.tgz", + "integrity": "sha512-krw5uS2STmvJ02x0uTXHbqQNuz+9eZ1iw+qXk9dmW2gvV4jV7O2hEoOnuhFrpOPiel1mBFtqbxYZZtC46hXLOw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.4.tgz", + "integrity": "sha512-wsTxtgApb4PrOsNJIm0FZ1h3WvCC+k9uxLJ4ad75hgoS4NiRes2SoJFlDAyMwiUY8IssDqGcHbXuN0sx1tfF1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.4.tgz", + "integrity": "sha512-GUOnQlyZe3yAXhWOtOMsn5Qkrv5E5mZXa0thbARWi5Ei2szlVXJFQhddZ4HbAzh8q92w5twp+CQvs/eFanz9YQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.4.tgz", + "integrity": "sha512-/Y7f3QuxjzPKsjA/rfEDa3+0vXqyjmJ50Ln8dPpCmWkKTrUoWHG1cWhTqaAMLob2m2nESWuC7yGrREz019Ztqg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.4.tgz", + "integrity": "sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.4.tgz", + "integrity": "sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.4.tgz", + "integrity": "sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.4.tgz", + "integrity": "sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.4.tgz", + "integrity": "sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.4.tgz", + "integrity": "sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.4.tgz", + "integrity": "sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.4.tgz", + "integrity": "sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.4.tgz", + "integrity": "sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.4.tgz", + "integrity": "sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.4.tgz", + "integrity": "sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.4.tgz", + "integrity": "sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.4.tgz", + "integrity": "sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.4.tgz", + "integrity": "sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.4.tgz", + "integrity": "sha512-s62SQ/vgsRSvMwDkOEfTqfgASF0f26ZNaQuTA6Aok5lrikf89yI2W0gFHvZb2Jpgc6N8JnOKZgCK2iciO3CsxQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.4.tgz", + "integrity": "sha512-J6wGf8TVGbXJq+HH+ttTvrcfNKPbuZecV6KT1B8I18BC5IURUh5kl4Yl5OEP5eFIUoI5BWxCsyYMhFsDx8kekw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.4.tgz", + "integrity": "sha512-zmfrQd/0wu6oJs8Vq8KwY/YtsKSsLtKe/HwAP4Wqy8LhWjeT55fHRAkOhYQ12wI3ayS4Tt12d5CDRD7N96SAYQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.4.tgz", + "integrity": "sha512-qPzHqdj9rfUD+w79dtE07zi/kFwKyCJqplp5K5ygeLTp7jLpAoc16OAH39HSmRC9UpozaecsleI8uAdEj6v2yw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.4.tgz", + "integrity": "sha512-zD6NdeWEByGE9QF9vCrlJ5YQB4oq9q91kPZS37Jwj5hOkvR1lTBSpsKhKDw4IJtbQ35LsTS1HD9DZYGKIshU1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, + "node_modules/@tailwindcss/node": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.17.tgz", + "integrity": "sha512-csIkHIgLb3JisEFQ0vxr2Y57GUNYh447C8xzwj89U/8fdW8LhProdxvnVH6U8M2Y73QKiTIH+LWbK3V2BBZsAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "enhanced-resolve": "^5.18.3", + "jiti": "^2.6.1", + "lightningcss": "1.30.2", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.17" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.17.tgz", + "integrity": "sha512-F0F7d01fmkQhsTjXezGBLdrl1KresJTcI3DB8EkScCldyKp3Msz4hub4uyYaVnk88BAS1g5DQjjF6F5qczheLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.17", + "@tailwindcss/oxide-darwin-arm64": "4.1.17", + "@tailwindcss/oxide-darwin-x64": "4.1.17", + "@tailwindcss/oxide-freebsd-x64": "4.1.17", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.17", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.17", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.17", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.17", + "@tailwindcss/oxide-linux-x64-musl": "4.1.17", + "@tailwindcss/oxide-wasm32-wasi": "4.1.17", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.17", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.17" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.17.tgz", + "integrity": "sha512-BMqpkJHgOZ5z78qqiGE6ZIRExyaHyuxjgrJ6eBO5+hfrfGkuya0lYfw8fRHG77gdTjWkNWEEm+qeG2cDMxArLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.17.tgz", + "integrity": "sha512-EquyumkQweUBNk1zGEU/wfZo2qkp/nQKRZM8bUYO0J+Lums5+wl2CcG1f9BgAjn/u9pJzdYddHWBiFXJTcxmOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.17.tgz", + "integrity": "sha512-gdhEPLzke2Pog8s12oADwYu0IAw04Y2tlmgVzIN0+046ytcgx8uZmCzEg4VcQh+AHKiS7xaL8kGo/QTiNEGRog==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.17.tgz", + "integrity": "sha512-hxGS81KskMxML9DXsaXT1H0DyA+ZBIbyG/sSAjWNe2EDl7TkPOBI42GBV3u38itzGUOmFfCzk1iAjDXds8Oh0g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.17.tgz", + "integrity": "sha512-k7jWk5E3ldAdw0cNglhjSgv501u7yrMf8oeZ0cElhxU6Y2o7f8yqelOp3fhf7evjIS6ujTI3U8pKUXV2I4iXHQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.17.tgz", + "integrity": "sha512-HVDOm/mxK6+TbARwdW17WrgDYEGzmoYayrCgmLEw7FxTPLcp/glBisuyWkFz/jb7ZfiAXAXUACfyItn+nTgsdQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.17.tgz", + "integrity": "sha512-HvZLfGr42i5anKtIeQzxdkw/wPqIbpeZqe7vd3V9vI3RQxe3xU1fLjss0TjyhxWcBaipk7NYwSrwTwK1hJARMg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.17.tgz", + "integrity": "sha512-M3XZuORCGB7VPOEDH+nzpJ21XPvK5PyjlkSFkFziNHGLc5d6g3di2McAAblmaSUNl8IOmzYwLx9NsE7bplNkwQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.17.tgz", + "integrity": "sha512-k7f+pf9eXLEey4pBlw+8dgfJHY4PZ5qOUFDyNf7SI6lHjQ9Zt7+NcscjpwdCEbYi6FI5c2KDTDWyf2iHcCSyyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.17.tgz", + "integrity": "sha512-cEytGqSSoy7zK4JRWiTCx43FsKP/zGr0CsuMawhH67ONlH+T79VteQeJQRO/X7L0juEUA8ZyuYikcRBf0vsxhg==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.6.0", + "@emnapi/runtime": "^1.6.0", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.0.7", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.17.tgz", + "integrity": "sha512-JU5AHr7gKbZlOGvMdb4722/0aYbU+tN6lv1kONx0JK2cGsh7g148zVWLM0IKR3NeKLv+L90chBVYcJ8uJWbC9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.17.tgz", + "integrity": "sha512-SKWM4waLuqx0IH+FMDUw6R66Hu4OuTALFgnleKbqhgGU30DY20NORZMZUKgLRjQXNN2TLzKvh48QXTig4h4bGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.17.tgz", + "integrity": "sha512-4+9w8ZHOiGnpcGI6z1TVVfWaX/koK7fKeSYF3qlYg2xpBtbteP2ddBxiarL+HVgfSJGeK5RIxRQmKm4rTJJAwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.1.17", + "@tailwindcss/oxide": "4.1.17", + "tailwindcss": "4.1.17" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7" + } + }, + "node_modules/@tanstack/react-table": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-9.1.2.tgz", + "integrity": "sha512-YQPZFJ1nIi/bjjwsPZVouABgahDcl7Gdm33CdTStUJBn0DjEVJ2uhSTVmIoWt9MVKdQziXGAsXipSzy949Hygg==", + "license": "MIT", + "dependencies": { + "@tanstack/react-store": "^0.11.0", + "@tanstack/table-core": "9.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": ">=18" + } + }, + "node_modules/@tanstack/react-table/node_modules/@tanstack/react-store": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/@tanstack/react-store/-/react-store-0.11.1.tgz", + "integrity": "sha512-HaIGKI3YLmjBYIvy5DFDY23oNaYZIsTZfngey07Uh5iLVJgM3bIGCnZeOFOqzjFld9JHWcaHJnasD/bKoGKwJQ==", + "license": "MIT", + "dependencies": { + "@tanstack/store": "0.11.1", + "use-sync-external-store": "^1.6.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@tanstack/store": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/@tanstack/store/-/store-0.11.1.tgz", + "integrity": "sha512-mzTOBhypOuDJAy/D8n2MfUZ1HFkXnmSETviRyhqEC8LUE7/IZQExOTxMANj3KjTofYTkFNpBY67qaVrT41YccA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/table-core": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-9.1.2.tgz", + "integrity": "sha512-ONpWQeass1sfg80CWF1NSwQ8r3GiqxA2lT/EdqIcrDEPZ0Z+0mM94eQoFYLPN0Kztzj8TQVb2+PrSZSItqA61g==", + "license": "MIT", + "dependencies": { + "@tanstack/store": "^0.11.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz", + "integrity": "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", + "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.1.tgz", + "integrity": "sha512-WQfkSw0QbQ5aJ2CHYw23ZGkqnRwqKHD/KYsMeTkZzPT4Jcf0DcBxBtwMJxnu6E7oxw5+JC6ZAiePgh28uJ1HBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.5", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.47", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.19.0.tgz", + "integrity": "sha512-ht/iuYZXEjFxLH/Hkezgd7m6JKlHHXEUSneaDz8uZe1Gj5QZtCnpyDsckvAiEnT89OEbCLmnte4R4sn7P0EKFw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.6", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.13", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.13.tgz", + "integrity": "sha512-k9HNuUVMlqVjQ9UHzfPjIqiDbWw7WqT1AoT7GL8VwvF3r0ZfArtgiSPAlmupyNquNgOJHTuH4CKYf8ttMTWBTQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.8", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz", + "integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.11.12", + "caniuse-lite": "^1.0.30001809", + "electron-to-chromium": "^1.5.402", + "node-releases": "^2.0.53", + "update-browserslist-db": "^1.3.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001809", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001809.tgz", + "integrity": "sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.405", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.405.tgz", + "integrity": "sha512-bNglH7lPH5l+yHOes7Zr4VqxhOy4BQ9ZBUX4VdoFgxMpzJk7W1ZoO3Vgd9Pxa9PyjQ76sfm2aKH/nzEcCNRlew==", + "dev": true, + "license": "ISC" + }, + "node_modules/engine.io-client": { + "version": "6.6.6", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.6.tgz", + "integrity": "sha512-iY6QdftLQ9pyiPoX082bpf/u1UewnOaJrtJIF9T0++QB34lZrj0uP+Q/bj8AlUsAxqhnkTV2BS8SBZSxOmoV5Q==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.21.0", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.24.5", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.5.tgz", + "integrity": "sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-toolkit": { + "version": "1.50.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.50.0.tgz", + "integrity": "sha512-OyZKhUVvEep9ITEiwHn8GKnMRQIVqoSIX7WnRbkWgJkllCujilqP2rD0u979tkl8wqyc8ICwlc1UBVv/Sl1G6w==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks", + "tests/types" + ] + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/goober": { + "version": "2.1.19", + "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.19.tgz", + "integrity": "sha512-U7veizMqxyKlM58+Z5j2ngJBH/r9siDmxpvNxSw0PylF6WQvrASJEZrxh1hidRBJc2jqoBVSyOban5u8m+6Rxg==", + "license": "MIT", + "peerDependencies": { + "csstype": "^3.0.10" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/immer": { + "version": "11.1.16", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.16.tgz", + "integrity": "sha512-Xs7H9rBc+kti1J6RueUvbEBkmOz7jqj11XYgf+YMXAYzu8EeE7hwZ9poLXdVfVnGmJu7QAf41T7H2KuF6QoK6Q==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lightningcss": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", + "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.30.2", + "lightningcss-darwin-arm64": "1.30.2", + "lightningcss-darwin-x64": "1.30.2", + "lightningcss-freebsd-x64": "1.30.2", + "lightningcss-linux-arm-gnueabihf": "1.30.2", + "lightningcss-linux-arm64-gnu": "1.30.2", + "lightningcss-linux-arm64-musl": "1.30.2", + "lightningcss-linux-x64-gnu": "1.30.2", + "lightningcss-linux-x64-musl": "1.30.2", + "lightningcss-win32-arm64-msvc": "1.30.2", + "lightningcss-win32-x64-msvc": "1.30.2" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz", + "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz", + "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz", + "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", + "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz", + "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz", + "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz", + "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", + "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz", + "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz", + "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz", + "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "1.31.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.31.0.tgz", + "integrity": "sha512-G8u2eEtoHUnUa9f8lbvqDhCiORMnYLdUEo06EEG9MQvHQrInKcX3Pa2TH39MM5qyzRcWETxB0+aOwAPI1g1kEg==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.53", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.53.tgz", + "integrity": "sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/react": { + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz", + "integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz", + "integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.6" + } + }, + "node_modules/react-hook-form": { + "version": "7.85.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.85.0.tgz", + "integrity": "sha512-U2MTriFXnclmV4rOE20p2DcRFv5WEg3FIcBFOKcOLFHDVvGIMPvLTkTWefUsonmlaVy23khVDxDWym6uJVGOzw==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, + "node_modules/react-hot-toast": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.6.0.tgz", + "integrity": "sha512-bH+2EBMZ4sdyou/DPrfgIouFpcRLCJ+HoCA32UoAYHn6T3Ur5yfcDCeSr5mwldl6pFOsiocmrXMuoCJ1vV8bWg==", + "license": "MIT", + "dependencies": { + "csstype": "^3.1.3", + "goober": "^2.1.16" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">=16", + "react-dom": ">=16" + } + }, + "node_modules/react-is": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.8.tgz", + "integrity": "sha512-s5un28nYxKJw5gvUHyW5PCC28CvBqLu9r3cWgzHT4Vo/5fqqkFcdRYsGcKf50WMPpjjFZS5d76fn3YCo2njKwQ==", + "license": "MIT", + "peer": true + }, + "node_modules/react-redux": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.3.0.tgz", + "integrity": "sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-remove-scroll": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", + "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-router": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.2.tgz", + "integrity": "sha512-aUVMjFm3GAPTTZL7oYr5E7ETiqfQCHRLH+B+5afnICvf0r7kkK4eR6SMuwbSTJw/7t+12khT/Kahij49fqOCIg==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.2.tgz", + "integrity": "sha512-AIKJ/jgGlFb3EbfCXk5Gzshiwt+l3mqbCrNjmEWMMjqQxNJ3svBa6bgzFyCC2Sw3RA0VWF1kg3uQf2OFhxb8hw==", + "license": "MIT", + "dependencies": { + "react-router": "7.18.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/recharts": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.10.1.tgz", + "integrity": "sha512-QXFrvt6IVcw7eeZCoyXTwkIJAX3Dv1nyVhMicXJ47GsGDDpcN8z6o644DibE9XjpBTThtsomLKnTV6lc+cVFUA==", + "license": "MIT", + "workspaces": [ + "www" + ], + "dependencies": { + "@reduxjs/toolkit": "^1.9.0 || 2.x.x", + "clsx": "^2.1.1", + "decimal.js-light": "^2.5.1", + "es-toolkit": "^1.39.3", + "eventemitter3": "^5.0.1", + "immer": "^11.1.8", + "react-redux": "8.x.x || 9.x.x", + "reselect": "5.2.0", + "tiny-invariant": "^1.3.3", + "use-sync-external-store": "^1.2.2", + "victory-vendor": "^37.0.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, + "node_modules/reselect": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.2.0.tgz", + "integrity": "sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw==", + "license": "MIT" + }, + "node_modules/rollup": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.4.tgz", + "integrity": "sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.62.4", + "@rollup/rollup-android-arm64": "4.62.4", + "@rollup/rollup-darwin-arm64": "4.62.4", + "@rollup/rollup-darwin-x64": "4.62.4", + "@rollup/rollup-freebsd-arm64": "4.62.4", + "@rollup/rollup-freebsd-x64": "4.62.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.4", + "@rollup/rollup-linux-arm-musleabihf": "4.62.4", + "@rollup/rollup-linux-arm64-gnu": "4.62.4", + "@rollup/rollup-linux-arm64-musl": "4.62.4", + "@rollup/rollup-linux-loong64-gnu": "4.62.4", + "@rollup/rollup-linux-loong64-musl": "4.62.4", + "@rollup/rollup-linux-ppc64-gnu": "4.62.4", + "@rollup/rollup-linux-ppc64-musl": "4.62.4", + "@rollup/rollup-linux-riscv64-gnu": "4.62.4", + "@rollup/rollup-linux-riscv64-musl": "4.62.4", + "@rollup/rollup-linux-s390x-gnu": "4.62.4", + "@rollup/rollup-linux-x64-gnu": "4.62.4", + "@rollup/rollup-linux-x64-musl": "4.62.4", + "@rollup/rollup-openbsd-x64": "4.62.4", + "@rollup/rollup-openharmony-arm64": "4.62.4", + "@rollup/rollup-win32-arm64-msvc": "4.62.4", + "@rollup/rollup-win32-ia32-msvc": "4.62.4", + "@rollup/rollup-win32-x64-gnu": "4.62.4", + "@rollup/rollup-win32-x64-msvc": "4.62.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/socket.io-client": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.3.tgz", + "integrity": "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.7.tgz", + "integrity": "sha512-IH/iSeO9T6gz1KkFleGDWkG9N3dl4jXVYUtMhIqH10Md0ttMer8nUNWiP1DKuNrybD2xBrixLJdCC9J6ECoYkg==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tailwind-merge": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.4.0.tgz", + "integrity": "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.17.tgz", + "integrity": "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.1.tgz", + "integrity": "sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/victory-vendor": { + "version": "37.3.6", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", + "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, + "node_modules/vite": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz", + "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-plugin-singlefile": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/vite-plugin-singlefile/-/vite-plugin-singlefile-2.3.0.tgz", + "integrity": "sha512-DAcHzYypM0CasNLSz/WG0VdKOCxGHErfrjOoyIPiNxTPTGmO6rRD/te93n1YL/s+miXq66ipF1brMBikf99c6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">18.0.0" + }, + "peerDependencies": { + "rollup": "^4.44.1", + "vite": "^5.4.11 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/ws": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", + "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zustand": { + "version": "5.0.14", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.14.tgz", + "integrity": "sha512-/8tAspM5LMPr28b3fwLYrtdj77ECpfZviaP75CMTnwO8ISyaE4GDIG/9rDDYq/cH9D2Xw2A2RXglLInmVBQB/g==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..c4c4ca0 --- /dev/null +++ b/package.json @@ -0,0 +1,55 @@ +{ + "name": "react-vite-tailwind", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "@hookform/resolvers": "^5.7.1", + "@radix-ui/react-accordion": "^1.2.20", + "@radix-ui/react-avatar": "^1.2.6", + "@radix-ui/react-checkbox": "^1.3.11", + "@radix-ui/react-dialog": "^1.1.23", + "@radix-ui/react-dropdown-menu": "^2.1.24", + "@radix-ui/react-label": "^2.1.15", + "@radix-ui/react-popover": "^1.1.23", + "@radix-ui/react-progress": "^1.1.16", + "@radix-ui/react-scroll-area": "^1.2.18", + "@radix-ui/react-select": "^2.3.7", + "@radix-ui/react-separator": "^1.1.15", + "@radix-ui/react-slot": "^1.3.3", + "@radix-ui/react-switch": "^1.3.7", + "@radix-ui/react-tabs": "^1.1.21", + "@radix-ui/react-tooltip": "^1.2.16", + "@tanstack/react-table": "^9.1.2", + "axios": "^1.19.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^1.31.0", + "react": "19.2.6", + "react-dom": "19.2.6", + "react-hook-form": "^7.85.0", + "react-hot-toast": "^2.6.0", + "react-router-dom": "^7.18.2", + "recharts": "^3.10.1", + "socket.io-client": "^4.8.3", + "tailwind-merge": "^3.4.0", + "zod": "^4.4.3", + "zustand": "^5.0.14" + }, + "devDependencies": { + "@tailwindcss/vite": "4.1.17", + "@types/node": "22.19.17", + "@types/react": "19.2.7", + "@types/react-dom": "19.2.3", + "@vitejs/plugin-react": "5.1.1", + "tailwindcss": "4.1.17", + "typescript": "5.9.3", + "vite": "7.3.2", + "vite-plugin-singlefile": "2.3.0" + } +} diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..efc213b --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,52 @@ +import { useEffect } from 'react'; +import { RouterProvider } from 'react-router-dom'; +import { Toaster } from 'react-hot-toast'; +import { router } from './router'; +import { useAuthStore, useUIStore } from './store'; + +function App() { + const { initialize } = useAuthStore(); + const { resolvedTheme } = useUIStore(); + + // Initialize auth on mount + useEffect(() => { + initialize(); + }, [initialize]); + + // Apply theme class to document + useEffect(() => { + document.documentElement.classList.remove('light', 'dark'); + document.documentElement.classList.add(resolvedTheme); + }, [resolvedTheme]); + + return ( + <> + + + + ); +} + +export default App; diff --git a/src/api/axios.ts b/src/api/axios.ts new file mode 100644 index 0000000..e3363f2 --- /dev/null +++ b/src/api/axios.ts @@ -0,0 +1,209 @@ +import axios, { AxiosInstance, AxiosError, InternalAxiosRequestConfig } from 'axios'; +import toast from 'react-hot-toast'; +import { ApiError, TokenPair } from './types'; + +// Create axios instance with default config +const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:3001/api/v1'; + +export const apiClient: AxiosInstance = axios.create({ + baseURL: API_BASE_URL, + timeout: 30000, + headers: { + 'Content-Type': 'application/json', + }, +}); + +// Token storage utilities +const TOKEN_KEY = 'ada_access_token'; +const REFRESH_TOKEN_KEY = 'ada_refresh_token'; + +export const tokenStorage = { + getAccessToken: (): string | null => { + return localStorage.getItem(TOKEN_KEY); + }, + + getRefreshToken: (): string | null => { + return localStorage.getItem(REFRESH_TOKEN_KEY); + }, + + setTokens: (tokens: TokenPair): void => { + localStorage.setItem(TOKEN_KEY, tokens.accessToken); + localStorage.setItem(REFRESH_TOKEN_KEY, tokens.refreshToken); + }, + + clearTokens: (): void => { + localStorage.removeItem(TOKEN_KEY); + localStorage.removeItem(REFRESH_TOKEN_KEY); + }, + + hasTokens: (): boolean => { + return !!localStorage.getItem(TOKEN_KEY); + }, +}; + +// Flag to prevent multiple refresh requests +let isRefreshing = false; +let failedQueue: Array<{ + resolve: (value: unknown) => void; + reject: (reason?: unknown) => void; +}> = []; + +const processQueue = (error: Error | null, token: string | null = null): void => { + failedQueue.forEach((prom) => { + if (error) { + prom.reject(error); + } else { + prom.resolve(token); + } + }); + failedQueue = []; +}; + +// Request interceptor - add auth token +apiClient.interceptors.request.use( + (config: InternalAxiosRequestConfig) => { + const token = tokenStorage.getAccessToken(); + + if (token && config.headers) { + config.headers.Authorization = `Bearer ${token}`; + } + + return config; + }, + (error: AxiosError) => { + return Promise.reject(error); + } +); + +// Response interceptor - handle errors and token refresh +apiClient.interceptors.response.use( + (response) => response, + async (error: AxiosError) => { + const originalRequest = error.config as InternalAxiosRequestConfig & { _retry?: boolean }; + + // Handle 401 Unauthorized - attempt token refresh + if (error.response?.status === 401 && !originalRequest._retry) { + if (isRefreshing) { + // Wait for the refresh to complete + return new Promise((resolve, reject) => { + failedQueue.push({ resolve, reject }); + }) + .then((token) => { + if (originalRequest.headers) { + originalRequest.headers.Authorization = `Bearer ${token}`; + } + return apiClient(originalRequest); + }) + .catch((err) => { + return Promise.reject(err); + }); + } + + originalRequest._retry = true; + isRefreshing = true; + + const refreshToken = tokenStorage.getRefreshToken(); + + if (!refreshToken) { + tokenStorage.clearTokens(); + window.location.href = '/login'; + return Promise.reject(error); + } + + try { + const response = await axios.post( + `${API_BASE_URL}/auth/refresh`, + { refreshToken }, + { headers: { 'Content-Type': 'application/json' } } + ); + + const newTokens = response.data; + tokenStorage.setTokens(newTokens); + + processQueue(null, newTokens.accessToken); + + if (originalRequest.headers) { + originalRequest.headers.Authorization = `Bearer ${newTokens.accessToken}`; + } + + return apiClient(originalRequest); + } catch (refreshError) { + processQueue(refreshError as Error, null); + tokenStorage.clearTokens(); + + // Redirect to login + toast.error('Сессия истекла. Пожалуйста, войдите снова.'); + window.location.href = '/login'; + + return Promise.reject(refreshError); + } finally { + isRefreshing = false; + } + } + + // Handle other errors + const errorMessage = getErrorMessage(error); + + // Show toast for client errors (except 401 which is handled above) + if (error.response?.status && error.response.status >= 400 && error.response.status !== 401) { + toast.error(errorMessage); + } + + // Handle network errors + if (!error.response) { + toast.error('Ошибка сети. Проверьте подключение к интернету.'); + } + + return Promise.reject(error); + } +); + +// Helper function to extract error message +function getErrorMessage(error: AxiosError): string { + if (error.response?.data?.message) { + return error.response.data.message; + } + + switch (error.response?.status) { + case 400: + return 'Неверный запрос'; + case 403: + return 'Доступ запрещён'; + case 404: + return 'Ресурс не найден'; + case 409: + return 'Конфликт данных'; + case 422: + return 'Ошибка валидации'; + case 429: + return 'Слишком много запросов. Попробуйте позже.'; + case 500: + return 'Внутренняя ошибка сервера'; + case 502: + return 'Сервис временно недоступен'; + case 503: + return 'Сервис на обслуживании'; + default: + return 'Произошла ошибка'; + } +} + +// Export typed request helpers +export const api = { + get: (url: string, config?: object) => + apiClient.get(url, config).then(res => res.data), + + post: (url: string, data?: object, config?: object) => + apiClient.post(url, data, config).then(res => res.data), + + put: (url: string, data?: object, config?: object) => + apiClient.put(url, data, config).then(res => res.data), + + patch: (url: string, data?: object, config?: object) => + apiClient.patch(url, data, config).then(res => res.data), + + delete: (url: string, config?: object) => + apiClient.delete(url, config).then(res => res.data), +}; + +export default apiClient; diff --git a/src/api/endpoints/admin.api.ts b/src/api/endpoints/admin.api.ts new file mode 100644 index 0000000..51071ef --- /dev/null +++ b/src/api/endpoints/admin.api.ts @@ -0,0 +1,251 @@ +import { api } from '../axios'; +import { + User, + Match, + Service, + AdminDashboard, + AdminActivityLog, + SystemAlert, + SystemConfig, + SystemHealthCheck, + AdminUserListParams, + AdminUpdateUserRequest, + AdminCreateMatchRequest, + AdminMatchControl, + AdminServiceListParams, + AdminUpdateServiceRequest, + AdminServiceReview, + MaintenanceRequest, + UpdateSystemConfigRequest, + PaginatedResponse, + PaginationParams, +} from '../types'; + +export const adminApi = { + // Dashboard + getDashboard: (): Promise => { + return api.get('/admin/dashboard'); + }, + + // User management + getUsers: ( + params?: AdminUserListParams + ): Promise> => { + return api.get>('/admin/users', { params }); + }, + + getUser: (userId: string): Promise => { + return api.get(`/admin/users/${userId}`); + }, + + updateUser: (userId: string, data: AdminUpdateUserRequest): Promise => { + return api.patch(`/admin/users/${userId}`, data); + }, + + deleteUser: (userId: string): Promise => { + return api.delete(`/admin/users/${userId}`); + }, + + banUser: ( + userId: string, + reason: string, + expiresAt?: string + ): Promise => { + return api.post(`/admin/users/${userId}/ban`, { reason, expiresAt }); + }, + + unbanUser: (userId: string): Promise => { + return api.post(`/admin/users/${userId}/unban`); + }, + + resetUserPassword: (userId: string): Promise<{ temporaryPassword: string }> => { + return api.post(`/admin/users/${userId}/reset-password`); + }, + + impersonateUser: (userId: string): Promise<{ token: string; expiresAt: string }> => { + return api.post(`/admin/users/${userId}/impersonate`); + }, + + // Match management + getMatches: ( + params?: PaginationParams & { status?: string; mode?: string } + ): Promise> => { + return api.get>('/admin/matches', { params }); + }, + + createMatch: (data: AdminCreateMatchRequest): Promise => { + return api.post('/admin/matches', data); + }, + + controlMatch: (data: AdminMatchControl): Promise => { + const { matchId, ...body } = data; + return api.post(`/admin/matches/${matchId}/control`, body); + }, + + forceEndMatch: (matchId: string, reason: string): Promise => { + return api.post(`/admin/matches/${matchId}/force-end`, { reason }); + }, + + getMatchHealth: (matchId: string): Promise<{ + status: string; + checkers: Array<{ serviceId: string; status: string; lastRun: string }>; + scorers: Array<{ status: string; queueLength: number }>; + network: { status: string; latency: number }; + }> => { + return api.get(`/admin/matches/${matchId}/health`); + }, + + // Service management + getServices: ( + params?: AdminServiceListParams + ): Promise> => { + return api.get>('/admin/services', { params }); + }, + + getPendingServices: (): Promise => { + return api.get('/admin/services/pending'); + }, + + reviewService: (data: AdminServiceReview): Promise => { + const { serviceId, ...body } = data; + return api.post(`/admin/services/${serviceId}/review`, body); + }, + + updateService: ( + serviceId: string, + data: AdminUpdateServiceRequest + ): Promise => { + return api.patch(`/admin/services/${serviceId}`, data); + }, + + deleteService: (serviceId: string): Promise => { + return api.delete(`/admin/services/${serviceId}`); + }, + + // System management + getSystemConfig: (): Promise => { + return api.get('/admin/system/config'); + }, + + updateSystemConfig: (data: UpdateSystemConfigRequest): Promise => { + return api.patch('/admin/system/config', data); + }, + + getSystemHealth: (): Promise => { + return api.get('/admin/system/health'); + }, + + setMaintenanceMode: (data: MaintenanceRequest): Promise<{ success: boolean }> => { + return api.post('/admin/system/maintenance', data); + }, + + restartService: ( + serviceName: string + ): Promise<{ success: boolean; message: string }> => { + return api.post(`/admin/system/restart/${serviceName}`); + }, + + clearCache: ( + cacheType?: 'all' | 'scoreboard' | 'sessions' | 'api' + ): Promise<{ cleared: string[] }> => { + return api.post('/admin/system/clear-cache', { cacheType }); + }, + + // Activity logs + getActivityLogs: ( + params?: PaginationParams & { + adminId?: string; + action?: string; + targetType?: string; + from?: string; + to?: string; + } + ): Promise> => { + return api.get>('/admin/activity-logs', { + params, + }); + }, + + // Alerts + getAlerts: ( + params?: { + severity?: string; + isResolved?: boolean; + limit?: number; + } + ): Promise => { + return api.get('/admin/alerts', { params }); + }, + + resolveAlert: (alertId: string): Promise => { + return api.post(`/admin/alerts/${alertId}/resolve`); + }, + + dismissAlert: (alertId: string): Promise => { + return api.delete(`/admin/alerts/${alertId}`); + }, + + // Statistics + getSystemStats: (): Promise<{ + users: { total: number; active: number; new24h: number }; + teams: { total: number; active: number }; + matches: { total: number; active: number; today: number }; + services: { total: number; pending: number }; + exercises: { total: number; completions24h: number }; + }> => { + return api.get('/admin/stats'); + }, + + getUsageStats: (period: 'day' | 'week' | 'month'): Promise<{ + apiRequests: Array<{ timestamp: string; count: number }>; + activeUsers: Array<{ timestamp: string; count: number }>; + matchesCreated: Array<{ timestamp: string; count: number }>; + flagsSubmitted: Array<{ timestamp: string; count: number }>; + }> => { + return api.get('/admin/stats/usage', { params: { period } }); + }, + + // Broadcasts + sendBroadcast: (data: { + title: string; + message: string; + type: 'info' | 'warning' | 'maintenance'; + targetRoles?: string[]; + expiresAt?: string; + }): Promise<{ sent: number }> => { + return api.post('/admin/broadcast', data); + }, + + // Backups + createBackup: ( + type: 'full' | 'database' | 'files' + ): Promise<{ + backupId: string; + status: string; + estimatedTime: number; + }> => { + return api.post('/admin/backups', { type }); + }, + + getBackups: (): Promise> => { + return api.get('/admin/backups'); + }, + + downloadBackup: (backupId: string): Promise => { + return api.get(`/admin/backups/${backupId}/download`, { + responseType: 'blob', + }); + }, + + restoreBackup: (backupId: string): Promise<{ status: string }> => { + return api.post(`/admin/backups/${backupId}/restore`); + }, +}; + +export default adminApi; diff --git a/src/api/endpoints/analytics.api.ts b/src/api/endpoints/analytics.api.ts new file mode 100644 index 0000000..9ce3fff --- /dev/null +++ b/src/api/endpoints/analytics.api.ts @@ -0,0 +1,259 @@ +import { api } from '../axios'; +import { + TeamAnalytics, + PlayerAnalytics, + MatchAnalytics, + AIRecommendation, + TeamAnalyticsParams, + PlayerAnalyticsParams, + MatchAnalyticsParams, + AnalyticsPeriod, + HeatmapData, +} from '../types'; + +export const analyticsApi = { + // Get team analytics + getTeamAnalytics: (params: TeamAnalyticsParams): Promise => { + const { teamId, ...queryParams } = params; + return api.get(`/analytics/teams/${teamId}`, { + params: queryParams, + }); + }, + + // Get player analytics + getPlayerAnalytics: (params: PlayerAnalyticsParams): Promise => { + const { userId, ...queryParams } = params; + return api.get(`/analytics/players/${userId}`, { + params: queryParams, + }); + }, + + // Get match analytics + getMatchAnalytics: (params: MatchAnalyticsParams): Promise => { + const { matchId, ...queryParams } = params; + return api.get(`/analytics/matches/${matchId}`, { + params: queryParams, + }); + }, + + // Get AI recommendations for team + getTeamRecommendations: ( + teamId: string, + limit?: number + ): Promise => { + return api.get( + `/analytics/teams/${teamId}/recommendations`, + { params: { limit } } + ); + }, + + // Get AI recommendations for player + getPlayerRecommendations: ( + userId: string, + limit?: number + ): Promise => { + return api.get( + `/analytics/players/${userId}/recommendations`, + { params: { limit } } + ); + }, + + // Get match AI insights + getMatchInsights: (matchId: string): Promise => { + return api.get(`/analytics/matches/${matchId}/insights`); + }, + + // Get attack heatmap + getAttackHeatmap: ( + matchId: string, + params?: { + teamId?: string; + round?: number; + } + ): Promise => { + return api.get(`/analytics/matches/${matchId}/heatmap/attacks`, { + params, + }); + }, + + // Get SLA heatmap + getSLAHeatmap: ( + matchId: string, + params?: { + teamId?: string; + } + ): Promise => { + return api.get(`/analytics/matches/${matchId}/heatmap/sla`, { + params, + }); + }, + + // Get team performance trends + getTeamTrends: ( + teamId: string, + period: AnalyticsPeriod + ): Promise; + trend: 'up' | 'down' | 'stable'; + changePercent: number; + }>> => { + return api.get(`/analytics/teams/${teamId}/trends`, { + params: { ...period }, + }); + }, + + // Get player skill progression + getSkillProgression: ( + userId: string, + skillId?: string + ): Promise; + }>> => { + return api.get(`/analytics/players/${userId}/skill-progression`, { + params: { skillId }, + }); + }, + + // Compare teams + compareTeams: ( + teamIds: string[], + period?: AnalyticsPeriod + ): Promise<{ + teams: Array<{ + teamId: string; + teamName: string; + }>; + metrics: Array<{ + name: string; + values: Record; + }>; + }> => { + return api.get('/analytics/compare/teams', { + params: { teamIds: teamIds.join(','), ...period }, + }); + }, + + // Compare players + comparePlayers: ( + userIds: string[], + period?: AnalyticsPeriod + ): Promise<{ + players: Array<{ + userId: string; + username: string; + }>; + metrics: Array<{ + name: string; + values: Record; + }>; + }> => { + return api.get('/analytics/compare/players', { + params: { userIds: userIds.join(','), ...period }, + }); + }, + + // Get service analytics for match + getServiceAnalytics: ( + matchId: string, + serviceId: string + ): Promise<{ + serviceId: string; + serviceName: string; + totalExploits: number; + uniqueExploiters: number; + avgExploitTime: number; + exploitsByRound: Array<{ round: number; count: number }>; + exploitsByTeam: Array<{ teamId: string; teamName: string; count: number }>; + slaByTeam: Array<{ teamId: string; teamName: string; sla: number }>; + firstBlood: { + teamId: string; + teamName: string; + round: number; + timestamp: string; + } | null; + }> => { + return api.get(`/analytics/matches/${matchId}/services/${serviceId}`); + }, + + // Get round-by-round analysis + getRoundAnalysis: ( + matchId: string, + roundNumber: number + ): Promise<{ + round: number; + summary: string; + keyEvents: Array<{ + type: string; + description: string; + impact: number; + }>; + teamPerformance: Array<{ + teamId: string; + teamName: string; + score: number; + attackSuccess: number; + defenseSuccess: number; + sla: number; + }>; + mvp: { + teamId: string; + teamName: string; + reason: string; + } | null; + }> => { + return api.get(`/analytics/matches/${matchId}/rounds/${roundNumber}/analysis`); + }, + + // Export analytics report + exportAnalytics: ( + type: 'team' | 'player' | 'match', + id: string, + format: 'pdf' | 'json' | 'csv' + ): Promise => { + return api.get(`/analytics/${type}s/${id}/export`, { + params: { format }, + responseType: 'blob', + }); + }, + + // Get benchmark data (how team/player compares to others) + getBenchmark: ( + type: 'team' | 'player', + id: string + ): Promise<{ + percentiles: Record; + comparisons: Array<{ + metric: string; + value: number; + average: number; + top10Percent: number; + percentile: number; + }>; + }> => { + return api.get(`/analytics/benchmark/${type}s/${id}`); + }, + + // Get improvement suggestions + getImprovementSuggestions: ( + type: 'team' | 'player', + id: string + ): Promise; + }>> => { + return api.get(`/analytics/${type}s/${id}/suggestions`); + }, +}; + +export default analyticsApi; diff --git a/src/api/endpoints/auth.api.ts b/src/api/endpoints/auth.api.ts new file mode 100644 index 0000000..e33bd7b --- /dev/null +++ b/src/api/endpoints/auth.api.ts @@ -0,0 +1,160 @@ +import { api, tokenStorage } from '../axios'; +import { + User, + LoginRequest, + RegisterRequest, + AuthResponse, + TokenPair, + ForgotPasswordRequest, + ResetPasswordRequest, + ChangePasswordRequest, + UpdateProfileRequest, + ApiKey, + CreateApiKeyRequest, + CreateApiKeyResponse, +} from '../types'; + +export const authApi = { + // Login user + login: async (data: LoginRequest): Promise => { + const response = await api.post('/auth/login', data); + tokenStorage.setTokens(response.tokens); + return response; + }, + + // Register new user + register: async (data: RegisterRequest): Promise => { + const response = await api.post('/auth/register', data); + tokenStorage.setTokens(response.tokens); + return response; + }, + + // Logout user + logout: async (): Promise => { + try { + await api.post('/auth/logout'); + } finally { + tokenStorage.clearTokens(); + } + }, + + // Get current user + getMe: (): Promise => { + return api.get('/auth/me'); + }, + + // Refresh access token + refreshToken: async (refreshToken: string): Promise => { + const response = await api.post('/auth/refresh', { refreshToken }); + tokenStorage.setTokens(response); + return response; + }, + + // Send forgot password email + forgotPassword: (data: ForgotPasswordRequest): Promise<{ message: string }> => { + return api.post<{ message: string }>('/auth/forgot-password', data); + }, + + // Reset password with token + resetPassword: (data: ResetPasswordRequest): Promise<{ message: string }> => { + return api.post<{ message: string }>('/auth/reset-password', data); + }, + + // Change password (authenticated) + changePassword: (data: ChangePasswordRequest): Promise<{ message: string }> => { + return api.post<{ message: string }>('/auth/change-password', data); + }, + + // Update profile + updateProfile: (data: UpdateProfileRequest): Promise => { + return api.patch('/auth/profile', data); + }, + + // Upload avatar + uploadAvatar: async (file: File): Promise<{ avatarUrl: string }> => { + const formData = new FormData(); + formData.append('avatar', file); + return api.post<{ avatarUrl: string }>('/auth/avatar', formData, { + headers: { 'Content-Type': 'multipart/form-data' }, + }); + }, + + // Delete avatar + deleteAvatar: (): Promise => { + return api.delete('/auth/avatar'); + }, + + // Verify email + verifyEmail: (token: string): Promise<{ message: string }> => { + return api.post<{ message: string }>('/auth/verify-email', { token }); + }, + + // Resend verification email + resendVerification: (): Promise<{ message: string }> => { + return api.post<{ message: string }>('/auth/resend-verification'); + }, + + // Enable two-factor authentication + enableTwoFactor: (): Promise<{ qrCode: string; secret: string }> => { + return api.post<{ qrCode: string; secret: string }>('/auth/2fa/enable'); + }, + + // Verify and complete 2FA setup + verifyTwoFactor: (code: string): Promise<{ backupCodes: string[] }> => { + return api.post<{ backupCodes: string[] }>('/auth/2fa/verify', { code }); + }, + + // Disable two-factor authentication + disableTwoFactor: (password: string): Promise<{ message: string }> => { + return api.post<{ message: string }>('/auth/2fa/disable', { password }); + }, + + // Get API keys + getApiKeys: (): Promise => { + return api.get('/auth/api-keys'); + }, + + // Create API key + createApiKey: (data: CreateApiKeyRequest): Promise => { + return api.post('/auth/api-keys', data); + }, + + // Delete API key + deleteApiKey: (keyId: string): Promise => { + return api.delete(`/auth/api-keys/${keyId}`); + }, + + // Check if username is available + checkUsername: (username: string): Promise<{ available: boolean }> => { + return api.get<{ available: boolean }>(`/auth/check-username/${username}`); + }, + + // Check if email is available + checkEmail: (email: string): Promise<{ available: boolean }> => { + return api.get<{ available: boolean }>(`/auth/check-email/${encodeURIComponent(email)}`); + }, + + // Get active sessions + getSessions: (): Promise> => { + return api.get('/auth/sessions'); + }, + + // Revoke session + revokeSession: (sessionId: string): Promise => { + return api.delete(`/auth/sessions/${sessionId}`); + }, + + // Revoke all other sessions + revokeAllSessions: (): Promise => { + return api.post('/auth/sessions/revoke-all'); + }, +}; + +export default authApi; diff --git a/src/api/endpoints/flags.api.ts b/src/api/endpoints/flags.api.ts new file mode 100644 index 0000000..3cd1686 --- /dev/null +++ b/src/api/endpoints/flags.api.ts @@ -0,0 +1,178 @@ +import { api } from '../axios'; +import { + Flag, + FlagSubmission, + FlagStats, + FlagSubmissionStats, + SubmitFlagRequest, + SubmitFlagResponse, + BulkSubmitFlagsRequest, + BulkSubmitFlagsResponse, + FlagHistoryParams, + PaginatedResponse, +} from '../types'; + +export const flagsApi = { + // Submit a flag + submitFlag: (data: SubmitFlagRequest): Promise => { + return api.post('/flags/submit', data); + }, + + // Bulk submit flags + bulkSubmitFlags: (data: BulkSubmitFlagsRequest): Promise => { + return api.post('/flags/submit/bulk', data); + }, + + // Get flag submission history for current team + getFlagHistory: ( + params: FlagHistoryParams + ): Promise> => { + const { matchId, ...queryParams } = params; + return api.get>( + `/matches/${matchId}/flags/history`, + { params: queryParams } + ); + }, + + // Get flag stats for team + getFlagStats: (matchId: string, teamId: string): Promise => { + return api.get(`/matches/${matchId}/teams/${teamId}/flag-stats`); + }, + + // Get submission stats + getSubmissionStats: (matchId: string, teamId: string): Promise => { + return api.get( + `/matches/${matchId}/teams/${teamId}/submission-stats` + ); + }, + + // Get captured flags (attacks made by team) + getCapturedFlags: ( + matchId: string, + teamId: string, + params?: { + serviceId?: string; + round?: number; + limit?: number; + offset?: number; + } + ): Promise> => { + return api.get>( + `/matches/${matchId}/teams/${teamId}/flags/captured`, + { params } + ); + }, + + // Get lost flags (flags stolen from team) + getLostFlags: ( + matchId: string, + teamId: string, + params?: { + serviceId?: string; + round?: number; + limit?: number; + offset?: number; + } + ): Promise> => { + return api.get>( + `/matches/${matchId}/teams/${teamId}/flags/lost`, + { params } + ); + }, + + // Get recent submissions (live feed) + getRecentSubmissions: ( + matchId: string, + limit?: number + ): Promise => { + return api.get(`/matches/${matchId}/flags/recent`, { + params: { limit }, + }); + }, + + // Get flag distribution by service + getFlagDistributionByService: ( + matchId: string + ): Promise> => { + return api.get(`/matches/${matchId}/flags/distribution/by-service`); + }, + + // Get flag distribution by team + getFlagDistributionByTeam: ( + matchId: string + ): Promise> => { + return api.get(`/matches/${matchId}/flags/distribution/by-team`); + }, + + // Get flag timeline (for visualization) + getFlagTimeline: ( + matchId: string, + params?: { + teamId?: string; + serviceId?: string; + fromRound?: number; + toRound?: number; + } + ): Promise> => { + return api.get(`/matches/${matchId}/flags/timeline`, { params }); + }, + + // Get first blood info + getFirstBlood: (matchId: string): Promise<{ + serviceId: string; + serviceName: string; + attackerTeamId: string; + attackerTeamName: string; + defenderTeamId: string; + defenderTeamName: string; + round: number; + timestamp: string; + timeFromStart: number; // seconds + } | null> => { + return api.get(`/matches/${matchId}/flags/first-blood`); + }, + + // Validate flag format (client-side check before submission) + validateFlagFormat: (matchId: string, flag: string): Promise<{ + valid: boolean; + message: string; + }> => { + return api.post(`/matches/${matchId}/flags/validate-format`, { flag }); + }, + + // Get submission rate limits + getRateLimits: (matchId: string): Promise<{ + perSecond: number; + perMinute: number; + currentUsage: { + lastSecond: number; + lastMinute: number; + }; + resetAt: string; + }> => { + return api.get(`/matches/${matchId}/flags/rate-limits`); + }, +}; + +export default flagsApi; diff --git a/src/api/endpoints/index.ts b/src/api/endpoints/index.ts new file mode 100644 index 0000000..813df39 --- /dev/null +++ b/src/api/endpoints/index.ts @@ -0,0 +1,19 @@ +// Export all API modules +export { authApi } from './auth.api'; +export { usersApi } from './users.api'; +export { teamsApi } from './teams.api'; +export { matchesApi } from './matches.api'; +export { roundsApi } from './rounds.api'; +export { servicesApi } from './services.api'; +export { scoreboardApi } from './scoreboard.api'; +export { flagsApi } from './flags.api'; +export { logsApi } from './logs.api'; +export { trainingApi } from './training.api'; +export { analyticsApi } from './analytics.api'; +export { adminApi } from './admin.api'; +export { notificationsApi } from './notifications.api'; +export { seasonsApi } from './seasons.api'; + +// Re-export types for convenience +export type { UserProfile, UserActivity, LeaderboardUser } from './users.api'; +export type { MatchSummary } from './matches.api'; diff --git a/src/api/endpoints/logs.api.ts b/src/api/endpoints/logs.api.ts new file mode 100644 index 0000000..07e5efc --- /dev/null +++ b/src/api/endpoints/logs.api.ts @@ -0,0 +1,220 @@ +import { api } from '../axios'; +import { + LogEntry, + LogFilterParams, + ReplayData, + ReplayParams, + EventTimelineParams, + PaginatedResponse, + EventType, + LogLevel, +} from '../types'; +import type { LogTimelineEvent } from '../types'; + +export const logsApi = { + // Get logs with filtering + getLogs: (params: LogFilterParams): Promise> => { + const { matchId, ...queryParams } = params; + return api.get>(`/matches/${matchId}/logs`, { + params: queryParams, + }); + }, + + // Get event timeline + getEventTimeline: ( + params: EventTimelineParams + ): Promise => { + const { matchId, ...queryParams } = params; + return api.get(`/matches/${matchId}/timeline`, { + params: queryParams, + }); + }, + + // Get replay data + getReplay: (params: ReplayParams): Promise => { + const { matchId, ...queryParams } = params; + return api.get(`/matches/${matchId}/replay`, { + params: queryParams, + }); + }, + + // Get replay frame at specific time + getReplayFrame: ( + matchId: string, + timestamp: number + ): Promise<{ + scoreboard: Array<{ + teamId: string; + teamName: string; + position: number; + score: number; + }>; + events: LogTimelineEvent[]; + serviceStatuses: Array<{ + teamId: string; + serviceId: string; + status: string; + }>; + }> => { + return api.get(`/matches/${matchId}/replay/frame`, { + params: { timestamp }, + }); + }, + + // Get key moments + getKeyMoments: (matchId: string): Promise> => { + return api.get(`/matches/${matchId}/replay/key-moments`); + }, + + // Stream logs (SSE) + streamLogs: ( + matchId: string, + filters: { + levels?: LogLevel[]; + types?: EventType[]; + teamId?: string; + serviceId?: string; + }, + onMessage: (log: LogEntry) => void, + onError?: (error: Event) => void + ): EventSource => { + const baseUrl = import.meta.env.VITE_API_URL || 'http://localhost:3001/api/v1'; + const params = new URLSearchParams(); + + if (filters.levels) params.append('levels', filters.levels.join(',')); + if (filters.types) params.append('types', filters.types.join(',')); + if (filters.teamId) params.append('teamId', filters.teamId); + if (filters.serviceId) params.append('serviceId', filters.serviceId); + + const url = `${baseUrl}/matches/${matchId}/logs/stream?${params.toString()}`; + const eventSource = new EventSource(url, { withCredentials: true }); + + eventSource.onmessage = (event) => { + try { + const data = JSON.parse(event.data) as LogEntry; + onMessage(data); + } catch (error) { + console.error('Error parsing log entry:', error); + } + }; + + if (onError) { + eventSource.onerror = onError; + } + + return eventSource; + }, + + // Get log statistics + getLogStats: (matchId: string): Promise<{ + total: number; + byLevel: Record; + byType: Record; + byTeam: Record; + byService: Record; + perRound: Array<{ round: number; count: number }>; + }> => { + return api.get(`/matches/${matchId}/logs/stats`); + }, + + // Search logs + searchLogs: ( + matchId: string, + query: string, + params?: { + limit?: number; + offset?: number; + } + ): Promise> => { + return api.get>(`/matches/${matchId}/logs/search`, { + params: { query, ...params }, + }); + }, + + // Export logs + exportLogs: ( + matchId: string, + format: 'json' | 'csv' | 'txt', + filters?: Partial + ): Promise => { + return api.get(`/matches/${matchId}/logs/export`, { + params: { format, ...filters }, + responseType: 'blob', + }); + }, + + // Get attack log (filtered for attack events) + getAttackLog: ( + matchId: string, + params?: { + attackerTeamId?: string; + defenderTeamId?: string; + serviceId?: string; + round?: number; + limit?: number; + offset?: number; + } + ): Promise> => { + return api.get>(`/matches/${matchId}/logs/attacks`, { + params, + }); + }, + + // Get service check log + getServiceCheckLog: ( + matchId: string, + params?: { + teamId?: string; + serviceId?: string; + result?: string; + round?: number; + limit?: number; + offset?: number; + } + ): Promise> => { + return api.get>( + `/matches/${matchId}/logs/service-checks`, + { params } + ); + }, + + // Get score change log + getScoreChangeLog: ( + matchId: string, + params?: { + teamId?: string; + round?: number; + limit?: number; + offset?: number; + } + ): Promise> => { + return api.get>( + `/matches/${matchId}/logs/score-changes`, + { params } + ); + }, + + // Bookmark log entry + bookmarkLogEntry: (logId: string, note?: string): Promise => { + return api.post(`/logs/${logId}/bookmark`, { note }); + }, + + // Get bookmarked logs + getBookmarkedLogs: (matchId: string): Promise => { + return api.get(`/matches/${matchId}/logs/bookmarked`); + }, + + // Remove bookmark + removeBookmark: (logId: string): Promise => { + return api.delete(`/logs/${logId}/bookmark`); + }, +}; + +export default logsApi; diff --git a/src/api/endpoints/matches.api.ts b/src/api/endpoints/matches.api.ts new file mode 100644 index 0000000..d4bdefa --- /dev/null +++ b/src/api/endpoints/matches.api.ts @@ -0,0 +1,193 @@ +import { api } from '../axios'; +import { + Match, + MatchResult, + MatchTeam, + CreateMatchRequest, + UpdateMatchRequest, + MatchListParams, + JoinMatchRequest, + PaginatedResponse, + PaginationParams, +} from '../types'; + +export interface MatchSummary { + id: string; + title: string; + mode: string; + status: string; + teamsCount: number; + maxTeams: number; + currentRound: number; + totalRounds: number; + scheduledAt: string | null; + startedAt: string | null; + isRanked: boolean; +} + +export const matchesApi = { + // Create a new match + createMatch: (data: CreateMatchRequest): Promise => { + return api.post('/matches', data); + }, + + // Get match by ID + getMatch: (matchId: string): Promise => { + return api.get(`/matches/${matchId}`); + }, + + // Update match + updateMatch: (matchId: string, data: UpdateMatchRequest): Promise => { + return api.patch(`/matches/${matchId}`, data); + }, + + // Delete match + deleteMatch: (matchId: string): Promise => { + return api.delete(`/matches/${matchId}`); + }, + + // List matches + listMatches: ( + params?: MatchListParams & PaginationParams + ): Promise> => { + return api.get>('/matches', { params }); + }, + + // Get active matches + getActiveMatches: (): Promise => { + return api.get('/matches/active'); + }, + + // Get upcoming matches + getUpcomingMatches: ( + params?: PaginationParams + ): Promise> => { + return api.get>('/matches/upcoming', { + params, + }); + }, + + // Get past matches + getPastMatches: ( + params?: PaginationParams + ): Promise> => { + return api.get>('/matches/past', { + params, + }); + }, + + // Join match with team + joinMatch: (matchId: string, data: JoinMatchRequest): Promise => { + return api.post(`/matches/${matchId}/join`, data); + }, + + // Leave match + leaveMatch: (matchId: string, teamId: string): Promise => { + return api.post(`/matches/${matchId}/leave`, { teamId }); + }, + + // Set team ready status + setTeamReady: ( + matchId: string, + teamId: string, + isReady: boolean + ): Promise => { + return api.post(`/matches/${matchId}/teams/${teamId}/ready`, { + isReady, + }); + }, + + // Get match teams + getMatchTeams: (matchId: string): Promise => { + return api.get(`/matches/${matchId}/teams`); + }, + + // Start match (organizer/admin) + startMatch: (matchId: string): Promise => { + return api.post(`/matches/${matchId}/start`); + }, + + // Stop match (organizer/admin) + stopMatch: (matchId: string, reason?: string): Promise => { + return api.post(`/matches/${matchId}/stop`, { reason }); + }, + + // Pause match + pauseMatch: (matchId: string): Promise => { + return api.post(`/matches/${matchId}/pause`); + }, + + // Resume match + resumeMatch: (matchId: string): Promise => { + return api.post(`/matches/${matchId}/resume`); + }, + + // Get match results + getMatchResults: (matchId: string): Promise => { + return api.get(`/matches/${matchId}/results`); + }, + + // Get match network config (for teams in match) + getNetworkConfig: (matchId: string, teamId: string): Promise<{ + ip: string; + subnet: string; + vpnConfig: string | null; + instructions: string; + }> => { + return api.get(`/matches/${matchId}/teams/${teamId}/network`); + }, + + // Download VPN config + downloadVpnConfig: (matchId: string, teamId: string): Promise => { + return api.get(`/matches/${matchId}/teams/${teamId}/vpn-config`, { + responseType: 'blob', + }); + }, + + // Get match services + getMatchServices: (matchId: string): Promise> => { + return api.get(`/matches/${matchId}/services`); + }, + + // Get featured matches + getFeaturedMatches: (): Promise => { + return api.get('/matches/featured'); + }, + + // Clone match (create new from existing config) + cloneMatch: (matchId: string): Promise => { + return api.post(`/matches/${matchId}/clone`); + }, + + // Get match statistics overview + getMatchStats: (matchId: string): Promise<{ + totalFlags: number; + totalAttacks: number; + successfulAttacks: number; + avgSLA: number; + duration: number; + roundsCompleted: number; + }> => { + return api.get(`/matches/${matchId}/stats`); + }, + + // Export match data + exportMatch: ( + matchId: string, + format: 'json' | 'csv' + ): Promise => { + return api.get(`/matches/${matchId}/export`, { + params: { format }, + responseType: 'blob', + }); + }, +}; + +export default matchesApi; diff --git a/src/api/endpoints/notifications.api.ts b/src/api/endpoints/notifications.api.ts new file mode 100644 index 0000000..4a1e22b --- /dev/null +++ b/src/api/endpoints/notifications.api.ts @@ -0,0 +1,125 @@ +import { api } from '../axios'; +import { + Notification, + NotificationGroup, + NotificationStats, + NotificationPreferences, + GetNotificationsParams, + MarkNotificationsRequest, + PaginatedResponse, +} from '../types'; + +export const notificationsApi = { + // Get notifications + getNotifications: ( + params?: GetNotificationsParams + ): Promise> => { + return api.get>('/notifications', { params }); + }, + + // Get notifications grouped by date + getNotificationsGrouped: ( + params?: GetNotificationsParams + ): Promise => { + return api.get('/notifications/grouped', { params }); + }, + + // Get single notification + getNotification: (notificationId: string): Promise => { + return api.get(`/notifications/${notificationId}`); + }, + + // Get unread count + getUnreadCount: (): Promise<{ count: number }> => { + return api.get<{ count: number }>('/notifications/unread-count'); + }, + + // Get notification stats + getStats: (): Promise => { + return api.get('/notifications/stats'); + }, + + // Mark notification as read + markAsRead: (notificationId: string): Promise => { + return api.post(`/notifications/${notificationId}/read`); + }, + + // Mark notification as unread + markAsUnread: (notificationId: string): Promise => { + return api.post(`/notifications/${notificationId}/unread`); + }, + + // Mark multiple notifications + markMultiple: (data: MarkNotificationsRequest): Promise<{ updated: number }> => { + return api.post<{ updated: number }>('/notifications/mark', data); + }, + + // Mark all as read + markAllAsRead: (): Promise<{ updated: number }> => { + return api.post<{ updated: number }>('/notifications/mark-all-read'); + }, + + // Archive notification + archiveNotification: (notificationId: string): Promise => { + return api.post(`/notifications/${notificationId}/archive`); + }, + + // Unarchive notification + unarchiveNotification: (notificationId: string): Promise => { + return api.post(`/notifications/${notificationId}/unarchive`); + }, + + // Delete notification + deleteNotification: (notificationId: string): Promise => { + return api.delete(`/notifications/${notificationId}`); + }, + + // Delete multiple notifications + deleteMultiple: (notificationIds: string[]): Promise<{ deleted: number }> => { + return api.post<{ deleted: number }>('/notifications/delete', { + notificationIds, + }); + }, + + // Clear all notifications + clearAll: (options?: { archived?: boolean }): Promise<{ deleted: number }> => { + return api.post<{ deleted: number }>('/notifications/clear', options); + }, + + // Get notification preferences + getPreferences: (): Promise => { + return api.get('/notifications/preferences'); + }, + + // Update notification preferences + updatePreferences: ( + data: Partial + ): Promise => { + return api.patch('/notifications/preferences', data); + }, + + // Subscribe to push notifications + subscribePush: (subscription: PushSubscriptionJSON): Promise<{ success: boolean }> => { + return api.post('/notifications/push/subscribe', { subscription }); + }, + + // Unsubscribe from push notifications + unsubscribePush: (): Promise => { + return api.post('/notifications/push/unsubscribe'); + }, + + // Test notification + sendTestNotification: (): Promise<{ success: boolean }> => { + return api.post('/notifications/test'); + }, + + // Get notification actions + performAction: ( + notificationId: string, + action: string + ): Promise<{ success: boolean; redirectUrl?: string }> => { + return api.post(`/notifications/${notificationId}/action`, { action }); + }, +}; + +export default notificationsApi; diff --git a/src/api/endpoints/rounds.api.ts b/src/api/endpoints/rounds.api.ts new file mode 100644 index 0000000..edcbaf9 --- /dev/null +++ b/src/api/endpoints/rounds.api.ts @@ -0,0 +1,148 @@ +import { api } from '../axios'; +import { + Round, + RoundEvent, + RoundSummary, + RoundTimeline, + ServiceCheck, + RoundEventsParams, + PaginatedResponse, +} from '../types'; + +export const roundsApi = { + // Get round by ID + getRound: (roundId: string): Promise => { + return api.get(`/rounds/${roundId}`); + }, + + // Get round by match and number + getRoundByNumber: (matchId: string, roundNumber: number): Promise => { + return api.get(`/matches/${matchId}/rounds/${roundNumber}`); + }, + + // List rounds for a match + listRounds: (matchId: string): Promise => { + return api.get(`/matches/${matchId}/rounds`); + }, + + // Get current round for a match + getCurrentRound: (matchId: string): Promise => { + return api.get(`/matches/${matchId}/rounds/current`); + }, + + // Get round events + getRoundEvents: ( + params: RoundEventsParams + ): Promise> => { + const { roundId, ...queryParams } = params; + return api.get>( + `/rounds/${roundId}/events`, + { params: queryParams } + ); + }, + + // Get round summary + getRoundSummary: (matchId: string, roundNumber: number): Promise => { + return api.get(`/matches/${matchId}/rounds/${roundNumber}/summary`); + }, + + // Get round timeline + getRoundTimeline: (matchId: string, roundNumber: number): Promise => { + return api.get( + `/matches/${matchId}/rounds/${roundNumber}/timeline` + ); + }, + + // Get service checks for a round + getServiceChecks: ( + roundId: string, + params?: { + teamId?: string; + serviceId?: string; + result?: string; + } + ): Promise => { + return api.get(`/rounds/${roundId}/checks`, { params }); + }, + + // Get round statistics + getRoundStats: (roundId: string): Promise<{ + flagsGenerated: number; + flagsCaptured: number; + flagsExpired: number; + avgSLA: number; + totalChecks: number; + successfulChecks: number; + failedChecks: number; + topAttacker: { teamId: string; teamName: string; count: number } | null; + topDefender: { teamId: string; teamName: string; count: number } | null; + mostAttackedService: { serviceId: string; serviceName: string; count: number } | null; + }> => { + return api.get(`/rounds/${roundId}/stats`); + }, + + // Get rounds comparison (for analytics) + getRoundsComparison: ( + matchId: string, + rounds: number[] + ): Promise> => { + return api.get(`/matches/${matchId}/rounds/compare`, { + params: { rounds: rounds.join(',') }, + }); + }, + + // Get round events stream (for live view) + streamRoundEvents: ( + matchId: string, + callback: (event: RoundEvent) => void + ): EventSource => { + const baseUrl = import.meta.env.VITE_API_URL || 'http://localhost:3001/api/v1'; + const eventSource = new EventSource( + `${baseUrl}/matches/${matchId}/rounds/stream`, + { withCredentials: true } + ); + + eventSource.onmessage = (event) => { + try { + const data = JSON.parse(event.data) as RoundEvent; + callback(data); + } catch (error) { + console.error('Error parsing round event:', error); + } + }; + + eventSource.onerror = (error) => { + console.error('EventSource error:', error); + }; + + return eventSource; + }, + + // Get flag distribution for round + getFlagDistribution: (roundId: string): Promise<{ + byService: Array<{ + serviceId: string; + serviceName: string; + generated: number; + captured: number; + expired: number; + }>; + byTeam: Array<{ + teamId: string; + teamName: string; + captured: number; + lost: number; + }>; + }> => { + return api.get(`/rounds/${roundId}/flag-distribution`); + }, +}; + +export default roundsApi; diff --git a/src/api/endpoints/scoreboard.api.ts b/src/api/endpoints/scoreboard.api.ts new file mode 100644 index 0000000..a748a7d --- /dev/null +++ b/src/api/endpoints/scoreboard.api.ts @@ -0,0 +1,182 @@ +import { api } from '../axios'; +import { + ScoreboardData, + ScoreboardEntry, + ScoreHistory, + TeamScore, + ScoreBreakdown, + ScoreboardParams, + ScoreHistoryParams, + LeaderboardEntry, + SeasonLeaderboard, + PaginatedResponse, +} from '../types'; + +export const scoreboardApi = { + // Get full scoreboard for a match + getScoreboard: (params: ScoreboardParams): Promise => { + const { matchId, ...queryParams } = params; + return api.get(`/matches/${matchId}/scoreboard`, { + params: queryParams, + }); + }, + + // Get scoreboard entry for specific team + getTeamScore: (matchId: string, teamId: string): Promise => { + return api.get(`/matches/${matchId}/scoreboard/teams/${teamId}`); + }, + + // Get score history for match + getScoreHistory: (params: ScoreHistoryParams): Promise => { + const { matchId, ...queryParams } = params; + return api.get(`/matches/${matchId}/scoreboard/history`, { + params: queryParams, + }); + }, + + // Get score history for specific team + getTeamScoreHistory: ( + matchId: string, + teamId: string, + params?: { + fromRound?: number; + toRound?: number; + } + ): Promise => { + return api.get( + `/matches/${matchId}/scoreboard/teams/${teamId}/history`, + { params } + ); + }, + + // Get score breakdown for team + getScoreBreakdown: (matchId: string, teamId: string): Promise => { + return api.get( + `/matches/${matchId}/scoreboard/teams/${teamId}/breakdown` + ); + }, + + // Get scoreboard for specific round + getRoundScoreboard: ( + matchId: string, + roundNumber: number + ): Promise => { + return api.get( + `/matches/${matchId}/rounds/${roundNumber}/scoreboard` + ); + }, + + // Get frozen scoreboard (last public state before freeze) + getFrozenScoreboard: (matchId: string): Promise => { + return api.get(`/matches/${matchId}/scoreboard/frozen`); + }, + + // Get final scoreboard (unfrozen, after match end) + getFinalScoreboard: (matchId: string): Promise => { + return api.get(`/matches/${matchId}/scoreboard/final`); + }, + + // Get live scoreboard updates (polling fallback) + pollScoreboard: ( + matchId: string, + lastUpdate?: string + ): Promise<{ + hasChanges: boolean; + scoreboard: ScoreboardData | null; + timestamp: string; + }> => { + return api.get(`/matches/${matchId}/scoreboard/poll`, { + params: { lastUpdate }, + }); + }, + + // Get position changes + getPositionChanges: (matchId: string): Promise; + }>> => { + return api.get(`/matches/${matchId}/scoreboard/position-changes`); + }, + + // Get score comparison between teams + getTeamsComparison: ( + matchId: string, + teamIds: string[] + ): Promise; + }>> => { + return api.get(`/matches/${matchId}/scoreboard/compare`, { + params: { teamIds: teamIds.join(',') }, + }); + }, + + // Global leaderboard + getGlobalLeaderboard: (params?: { + period?: 'week' | 'month' | 'season' | 'all_time'; + page?: number; + pageSize?: number; + }): Promise> => { + return api.get>('/leaderboard', { + params, + }); + }, + + // Season leaderboard + getSeasonLeaderboard: (seasonId: string): Promise => { + return api.get(`/seasons/${seasonId}/leaderboard`); + }, + + // Get top teams + getTopTeams: (limit?: number): Promise => { + return api.get('/leaderboard/top', { + params: { limit }, + }); + }, + + // Get team ranking + getTeamRanking: (teamId: string): Promise<{ + globalRank: number; + seasonRank: number | null; + rating: number; + percentile: number; + }> => { + return api.get(`/teams/${teamId}/ranking`); + }, + + // Export scoreboard + exportScoreboard: ( + matchId: string, + format: 'json' | 'csv' | 'pdf' + ): Promise => { + return api.get(`/matches/${matchId}/scoreboard/export`, { + params: { format }, + responseType: 'blob', + }); + }, + + // Get service status matrix + getServiceStatusMatrix: (matchId: string): Promise<{ + services: Array<{ id: string; name: string }>; + teams: Array<{ id: string; name: string }>; + matrix: Record>; + }> => { + return api.get(`/matches/${matchId}/scoreboard/service-matrix`); + }, +}; + +export default scoreboardApi; diff --git a/src/api/endpoints/seasons.api.ts b/src/api/endpoints/seasons.api.ts new file mode 100644 index 0000000..364cb9c --- /dev/null +++ b/src/api/endpoints/seasons.api.ts @@ -0,0 +1,159 @@ +import { api } from '../axios'; +import { + Season, + League, + SeasonSchedule, + SeasonStanding, + SeasonRegistration, + RegisterForSeasonRequest, + SeasonListParams, + LeagueListParams, + PaginatedResponse, + PaginationParams, +} from '../types'; + +export const seasonsApi = { + // Leagues + getLeagues: (params?: LeagueListParams): Promise => { + return api.get('/leagues', { params }); + }, + + getLeague: (leagueId: string): Promise => { + return api.get(`/leagues/${leagueId}`); + }, + + getLeagueBySlug: (slug: string): Promise => { + return api.get(`/leagues/slug/${slug}`); + }, + + // Seasons + getSeasons: ( + params?: SeasonListParams & PaginationParams + ): Promise> => { + return api.get>('/seasons', { params }); + }, + + getSeason: (seasonId: string): Promise => { + return api.get(`/seasons/${seasonId}`); + }, + + getSeasonBySlug: (slug: string): Promise => { + return api.get(`/seasons/slug/${slug}`); + }, + + getCurrentSeason: (leagueId?: string): Promise => { + return api.get('/seasons/current', { + params: { leagueId }, + }); + }, + + // Season schedule + getSeasonSchedule: (seasonId: string): Promise => { + return api.get(`/seasons/${seasonId}/schedule`); + }, + + // Standings + getSeasonStandings: (seasonId: string): Promise => { + return api.get(`/seasons/${seasonId}/standings`); + }, + + getTeamStanding: (seasonId: string, teamId: string): Promise => { + return api.get(`/seasons/${seasonId}/standings/${teamId}`); + }, + + // Registration + registerForSeason: (data: RegisterForSeasonRequest): Promise => { + return api.post('/seasons/register', data); + }, + + withdrawFromSeason: (seasonId: string, teamId: string): Promise => { + return api.post(`/seasons/${seasonId}/withdraw`, { teamId }); + }, + + getMyRegistrations: (): Promise => { + return api.get('/seasons/my-registrations'); + }, + + getSeasonRegistrations: (seasonId: string): Promise => { + return api.get(`/seasons/${seasonId}/registrations`); + }, + + // Season matches + getSeasonMatches: ( + seasonId: string, + params?: { week?: number; teamId?: string } + ): Promise> => { + return api.get(`/seasons/${seasonId}/matches`, { params }); + }, + + // Season stats + getSeasonStats: (seasonId: string): Promise<{ + teamsCount: number; + matchesPlayed: number; + matchesRemaining: number; + totalFlags: number; + avgScore: number; + topScorer: { teamId: string; teamName: string; score: number }; + bestDefense: { teamId: string; teamName: string; sla: number }; + }> => { + return api.get(`/seasons/${seasonId}/stats`); + }, + + // Season history for team + getTeamSeasonHistory: ( + teamId: string + ): Promise> => { + return api.get(`/teams/${teamId}/season-history`); + }, + + // Upcoming season events + getUpcomingEvents: ( + seasonId: string + ): Promise; + }>> => { + return api.get(`/seasons/${seasonId}/events`); + }, + + // Season rules + getSeasonRules: (seasonId: string): Promise => { + return api.get(`/seasons/${seasonId}/rules`); + }, + + // Prizes + getSeasonPrizes: (seasonId: string): Promise> => { + return api.get(`/seasons/${seasonId}/prizes`); + }, +}; + +export default seasonsApi; diff --git a/src/api/endpoints/services.api.ts b/src/api/endpoints/services.api.ts new file mode 100644 index 0000000..08726d5 --- /dev/null +++ b/src/api/endpoints/services.api.ts @@ -0,0 +1,230 @@ +import { api } from '../axios'; +import { + Service, + ServiceReview, + ServiceVersion, + ServiceValidation, + ServiceSearchParams, + CreateReviewRequest, + UploadServiceRequest, + UpdateServiceRequest, + PaginatedResponse, + PaginationParams, + ServiceCategory, + ServiceDifficulty, +} from '../types'; + +export const servicesApi = { + // List services + listServices: ( + params?: ServiceSearchParams & PaginationParams + ): Promise> => { + return api.get>('/services', { params }); + }, + + // Get service by ID + getService: (serviceId: string): Promise => { + return api.get(`/services/${serviceId}`); + }, + + // Get service by slug + getServiceBySlug: (slug: string): Promise => { + return api.get(`/services/slug/${slug}`); + }, + + // Upload new service + uploadService: (data: UploadServiceRequest): Promise => { + return api.post('/services', data); + }, + + // Update service + updateService: (serviceId: string, data: UpdateServiceRequest): Promise => { + return api.patch(`/services/${serviceId}`, data); + }, + + // Delete service + deleteService: (serviceId: string): Promise => { + return api.delete(`/services/${serviceId}`); + }, + + // Upload service files (Docker images, checker, etc.) + uploadServiceFiles: async ( + serviceId: string, + files: { + serviceImage?: File; + checkerImage?: File; + additionalFiles?: File[]; + } + ): Promise<{ status: string; urls: Record }> => { + const formData = new FormData(); + + if (files.serviceImage) { + formData.append('serviceImage', files.serviceImage); + } + if (files.checkerImage) { + formData.append('checkerImage', files.checkerImage); + } + if (files.additionalFiles) { + files.additionalFiles.forEach((file, index) => { + formData.append(`additionalFile_${index}`, file); + }); + } + + return api.post(`/services/${serviceId}/files`, formData, { + headers: { 'Content-Type': 'multipart/form-data' }, + }); + }, + + // Validate service + validateService: (serviceId: string): Promise => { + return api.post(`/services/${serviceId}/validate`); + }, + + // Get validation status + getValidationStatus: (serviceId: string): Promise => { + return api.get(`/services/${serviceId}/validation`); + }, + + // Get validation history + getValidationHistory: (serviceId: string): Promise => { + return api.get(`/services/${serviceId}/validation/history`); + }, + + // Submit service for review + submitForReview: (serviceId: string): Promise => { + return api.post(`/services/${serviceId}/submit-review`); + }, + + // Publish service + publishService: (serviceId: string): Promise => { + return api.post(`/services/${serviceId}/publish`); + }, + + // Deprecate service + deprecateService: (serviceId: string, reason: string): Promise => { + return api.post(`/services/${serviceId}/deprecate`, { reason }); + }, + + // Get service versions + getServiceVersions: (serviceId: string): Promise => { + return api.get(`/services/${serviceId}/versions`); + }, + + // Create new version + createVersion: ( + serviceId: string, + data: { version: string; changelog: string } + ): Promise => { + return api.post(`/services/${serviceId}/versions`, data); + }, + + // Get service reviews + getServiceReviews: ( + serviceId: string, + params?: PaginationParams + ): Promise> => { + return api.get>( + `/services/${serviceId}/reviews`, + { params } + ); + }, + + // Create review + createReview: (serviceId: string, data: CreateReviewRequest): Promise => { + return api.post(`/services/${serviceId}/reviews`, data); + }, + + // Update review + updateReview: ( + serviceId: string, + reviewId: string, + data: Partial + ): Promise => { + return api.patch( + `/services/${serviceId}/reviews/${reviewId}`, + data + ); + }, + + // Delete review + deleteReview: (serviceId: string, reviewId: string): Promise => { + return api.delete(`/services/${serviceId}/reviews/${reviewId}`); + }, + + // Rate service (quick rating without full review) + rateService: (serviceId: string, rating: number): Promise<{ averageRating: number }> => { + return api.post<{ averageRating: number }>(`/services/${serviceId}/rate`, { + rating, + }); + }, + + // Mark review as helpful + markReviewHelpful: (serviceId: string, reviewId: string): Promise => { + return api.post(`/services/${serviceId}/reviews/${reviewId}/helpful`); + }, + + // Get featured services + getFeaturedServices: (): Promise => { + return api.get('/services/featured'); + }, + + // Get popular services + getPopularServices: (limit?: number): Promise => { + return api.get('/services/popular', { params: { limit } }); + }, + + // Get services by category + getServicesByCategory: ( + category: ServiceCategory, + params?: PaginationParams + ): Promise> => { + return api.get>(`/services/category/${category}`, { + params, + }); + }, + + // Get services by difficulty + getServicesByDifficulty: ( + difficulty: ServiceDifficulty, + params?: PaginationParams + ): Promise> => { + return api.get>(`/services/difficulty/${difficulty}`, { + params, + }); + }, + + // Get service categories with counts + getCategories: (): Promise> => { + return api.get('/services/categories'); + }, + + // Get service tags with counts + getTags: (): Promise> => { + return api.get('/services/tags'); + }, + + // Get service tech stacks with counts + getStacks: (): Promise> => { + return api.get('/services/stacks'); + }, + + // Download service source (if allowed) + downloadSource: (serviceId: string, version?: string): Promise => { + return api.get(`/services/${serviceId}/download`, { + params: { version }, + responseType: 'blob', + }); + }, + + // Get my services (created by current user) + getMyServices: ( + params?: PaginationParams + ): Promise> => { + return api.get>('/services/my', { params }); + }, +}; + +export default servicesApi; diff --git a/src/api/endpoints/teams.api.ts b/src/api/endpoints/teams.api.ts new file mode 100644 index 0000000..c57bcf6 --- /dev/null +++ b/src/api/endpoints/teams.api.ts @@ -0,0 +1,194 @@ +import { api } from '../axios'; +import { + Team, + TeamMember, + TeamInvite, + TeamJoinRequest, + TeamMatchHistory, + TeamStats, + CreateTeamRequest, + UpdateTeamRequest, + InviteToTeamRequest, + UpdateMemberRoleRequest, + TeamSearchParams, + PaginatedResponse, + PaginationParams, +} from '../types'; + +export const teamsApi = { + // Create a new team + createTeam: (data: CreateTeamRequest): Promise => { + return api.post('/teams', data); + }, + + // Get team by ID + getTeam: (teamId: string): Promise => { + return api.get(`/teams/${teamId}`); + }, + + // Get team by tag + getTeamByTag: (tag: string): Promise => { + return api.get(`/teams/tag/${tag}`); + }, + + // Update team + updateTeam: (teamId: string, data: UpdateTeamRequest): Promise => { + return api.patch(`/teams/${teamId}`, data); + }, + + // Delete team + deleteTeam: (teamId: string): Promise => { + return api.delete(`/teams/${teamId}`); + }, + + // Get team members + getTeamMembers: (teamId: string): Promise => { + return api.get(`/teams/${teamId}/members`); + }, + + // Get team stats + getTeamStats: (teamId: string): Promise => { + return api.get(`/teams/${teamId}/stats`); + }, + + // Get team match history + getTeamMatchHistory: ( + teamId: string, + params?: PaginationParams + ): Promise> => { + return api.get>( + `/teams/${teamId}/matches`, + { params } + ); + }, + + // Search teams + searchTeams: ( + params: TeamSearchParams & PaginationParams + ): Promise> => { + return api.get>('/teams/search', { params }); + }, + + // Join team (public team) + joinTeam: (teamId: string, message?: string): Promise => { + return api.post(`/teams/${teamId}/join`, { message }); + }, + + // Leave team + leaveTeam: (teamId: string): Promise => { + return api.post(`/teams/${teamId}/leave`); + }, + + // Invite user to team + inviteToTeam: (teamId: string, data: InviteToTeamRequest): Promise => { + return api.post(`/teams/${teamId}/invites`, data); + }, + + // Get team invites (sent by team) + getTeamInvites: (teamId: string): Promise => { + return api.get(`/teams/${teamId}/invites`); + }, + + // Cancel team invite + cancelInvite: (teamId: string, inviteId: string): Promise => { + return api.delete(`/teams/${teamId}/invites/${inviteId}`); + }, + + // Get pending join requests (for team admins) + getJoinRequests: (teamId: string): Promise => { + return api.get(`/teams/${teamId}/join-requests`); + }, + + // Approve join request + approveJoinRequest: (teamId: string, requestId: string): Promise => { + return api.post(`/teams/${teamId}/join-requests/${requestId}/approve`); + }, + + // Reject join request + rejectJoinRequest: (teamId: string, requestId: string): Promise => { + return api.post(`/teams/${teamId}/join-requests/${requestId}/reject`); + }, + + // Get my invites (received by current user) + getMyInvites: (): Promise => { + return api.get('/teams/invites/me'); + }, + + // Accept invite + acceptInvite: (inviteId: string): Promise => { + return api.post(`/teams/invites/${inviteId}/accept`); + }, + + // Decline invite + declineInvite: (inviteId: string): Promise => { + return api.post(`/teams/invites/${inviteId}/decline`); + }, + + // Get my join requests (sent by current user) + getMyJoinRequests: (): Promise => { + return api.get('/teams/join-requests/me'); + }, + + // Cancel my join request + cancelJoinRequest: (requestId: string): Promise => { + return api.delete(`/teams/join-requests/${requestId}`); + }, + + // Update member role + updateMemberRole: ( + teamId: string, + memberId: string, + data: UpdateMemberRoleRequest + ): Promise => { + return api.patch(`/teams/${teamId}/members/${memberId}`, data); + }, + + // Remove member from team + removeMember: (teamId: string, memberId: string): Promise => { + return api.delete(`/teams/${teamId}/members/${memberId}`); + }, + + // Transfer captaincy + transferCaptain: (teamId: string, newCaptainId: string): Promise => { + return api.post(`/teams/${teamId}/transfer-captain`, { + newCaptainId, + }); + }, + + // Upload team avatar + uploadAvatar: async (teamId: string, file: File): Promise<{ avatarUrl: string }> => { + const formData = new FormData(); + formData.append('avatar', file); + return api.post<{ avatarUrl: string }>(`/teams/${teamId}/avatar`, formData, { + headers: { 'Content-Type': 'multipart/form-data' }, + }); + }, + + // Upload team banner + uploadBanner: async (teamId: string, file: File): Promise<{ bannerUrl: string }> => { + const formData = new FormData(); + formData.append('banner', file); + return api.post<{ bannerUrl: string }>(`/teams/${teamId}/banner`, formData, { + headers: { 'Content-Type': 'multipart/form-data' }, + }); + }, + + // Get featured teams + getFeaturedTeams: (): Promise => { + return api.get('/teams/featured'); + }, + + // Get recruiting teams + getRecruitingTeams: ( + params?: PaginationParams + ): Promise> => { + return api.get>('/teams/recruiting', { params }); + }, + + // Check if team tag is available + checkTag: (tag: string): Promise<{ available: boolean }> => { + return api.get<{ available: boolean }>(`/teams/check-tag/${tag}`); + }, +}; + +export default teamsApi; diff --git a/src/api/endpoints/training.api.ts b/src/api/endpoints/training.api.ts new file mode 100644 index 0000000..011a1e6 --- /dev/null +++ b/src/api/endpoints/training.api.ts @@ -0,0 +1,244 @@ +import { api } from '../axios'; +import { + Track, + Exercise, + ExerciseResult, + Progress, + SkillMapData, + TrackProgress, + StartExerciseRequest, + StartExerciseResponse, + SubmitExerciseRequest, + SubmitExerciseResponse, + UseHintRequest, + UseHintResponse, + TrackSearchParams, + PaginatedResponse, + PaginationParams, +} from '../types'; + +export const trainingApi = { + // Get all tracks + getTracks: ( + params?: TrackSearchParams & PaginationParams + ): Promise> => { + return api.get>('/training/tracks', { params }); + }, + + // Get track by ID + getTrack: (trackId: string): Promise => { + return api.get(`/training/tracks/${trackId}`); + }, + + // Get track by slug + getTrackBySlug: (slug: string): Promise => { + return api.get(`/training/tracks/slug/${slug}`); + }, + + // Enroll in track + enrollInTrack: (trackId: string): Promise => { + return api.post(`/training/tracks/${trackId}/enroll`); + }, + + // Unenroll from track + unenrollFromTrack: (trackId: string): Promise => { + return api.delete(`/training/tracks/${trackId}/enroll`); + }, + + // Get track progress + getTrackProgress: (trackId: string): Promise => { + return api.get(`/training/tracks/${trackId}/progress`); + }, + + // Get exercise by ID + getExercise: (exerciseId: string): Promise => { + return api.get(`/training/exercises/${exerciseId}`); + }, + + // Get exercise by slug + getExerciseBySlug: (trackSlug: string, exerciseSlug: string): Promise => { + return api.get( + `/training/tracks/slug/${trackSlug}/exercises/slug/${exerciseSlug}` + ); + }, + + // Start exercise + startExercise: (data: StartExerciseRequest): Promise => { + return api.post('/training/exercises/start', data); + }, + + // Submit exercise + submitExercise: (data: SubmitExerciseRequest): Promise => { + return api.post('/training/exercises/submit', data); + }, + + // Get exercise result + getExerciseResult: (exerciseId: string): Promise => { + return api.get( + `/training/exercises/${exerciseId}/result` + ); + }, + + // Get available hints + getAvailableHints: (exerciseId: string): Promise<{ + hints: Array<{ + id: string; + order: number; + title: string; + isUnlocked: boolean; + costPercentage: number; + }>; + usedHints: number; + totalHints: number; + }> => { + return api.get(`/training/exercises/${exerciseId}/hints`); + }, + + // Use hint + useHint: (data: UseHintRequest): Promise => { + return api.post('/training/exercises/use-hint', data); + }, + + // Get overall progress + getProgress: (): Promise => { + return api.get('/training/progress'); + }, + + // Get skill map + getSkillMap: (): Promise => { + return api.get('/training/skill-map'); + }, + + // Get recommended tracks + getRecommendedTracks: (limit?: number): Promise => { + return api.get('/training/recommendations/tracks', { + params: { limit }, + }); + }, + + // Get recommended exercises + getRecommendedExercises: (limit?: number): Promise => { + return api.get('/training/recommendations/exercises', { + params: { limit }, + }); + }, + + // Get featured tracks + getFeaturedTracks: (): Promise => { + return api.get('/training/tracks/featured'); + }, + + // Get popular tracks + getPopularTracks: (limit?: number): Promise => { + return api.get('/training/tracks/popular', { + params: { limit }, + }); + }, + + // Get enrolled tracks + getEnrolledTracks: (): Promise => { + return api.get('/training/my-tracks'); + }, + + // Get completed tracks + getCompletedTracks: (): Promise => { + return api.get('/training/completed-tracks'); + }, + + // Get exercise history (all attempts) + getExerciseHistory: ( + exerciseId: string + ): Promise> => { + return api.get(`/training/exercises/${exerciseId}/history`); + }, + + // Reset exercise progress + resetExerciseProgress: (exerciseId: string): Promise => { + return api.post(`/training/exercises/${exerciseId}/reset`); + }, + + // Get daily challenge + getDailyChallenge: (): Promise => { + return api.get('/training/daily-challenge'); + }, + + // Get streak info + getStreakInfo: (): Promise<{ + currentStreak: number; + longestStreak: number; + lastActivityDate: string; + streakFreezeAvailable: boolean; + }> => { + return api.get('/training/streak'); + }, + + // Use streak freeze + useStreakFreeze: (): Promise<{ success: boolean; freezesRemaining: number }> => { + return api.post('/training/streak/freeze'); + }, + + // Get certificates + getCertificates: (): Promise> => { + return api.get('/training/certificates'); + }, + + // Generate certificate for completed track + generateCertificate: (trackId: string): Promise<{ + certificateUrl: string; + verificationCode: string; + }> => { + return api.post(`/training/tracks/${trackId}/certificate`); + }, + + // Get leaderboard for track + getTrackLeaderboard: ( + trackId: string, + params?: PaginationParams + ): Promise> => { + return api.get(`/training/tracks/${trackId}/leaderboard`, { params }); + }, + + // Rate track + rateTrack: ( + trackId: string, + rating: number, + feedback?: string + ): Promise<{ averageRating: number }> => { + return api.post(`/training/tracks/${trackId}/rate`, { rating, feedback }); + }, + + // Report issue with exercise + reportExerciseIssue: ( + exerciseId: string, + type: 'bug' | 'unclear' | 'incorrect' | 'other', + description: string + ): Promise => { + return api.post(`/training/exercises/${exerciseId}/report`, { + type, + description, + }); + }, +}; + +export default trainingApi; diff --git a/src/api/endpoints/users.api.ts b/src/api/endpoints/users.api.ts new file mode 100644 index 0000000..da5e03f --- /dev/null +++ b/src/api/endpoints/users.api.ts @@ -0,0 +1,173 @@ +import { api } from '../axios'; +import { + User, + UserStats, + Achievement, + PaginatedResponse, + PaginationParams, +} from '../types'; + +export interface UserProfile extends User { + bio: string; + socialLinks: { + github?: string; + twitter?: string; + linkedin?: string; + website?: string; + ctftime?: string; + }; + isFollowing?: boolean; + followersCount: number; + followingCount: number; +} + +export interface UserActivity { + id: string; + type: 'match_played' | 'achievement_unlocked' | 'exercise_completed' | 'team_joined' | 'rank_change'; + title: string; + description: string; + data: Record; + timestamp: string; +} + +export interface LeaderboardUser { + position: number; + userId: string; + username: string; + displayName: string; + avatar: string | null; + rating: number; + ratingChange: number; + matchesPlayed: number; + winRate: number; + teamId: string | null; + teamName: string | null; +} + +export const usersApi = { + // Get user by ID + getUser: (userId: string): Promise => { + return api.get(`/users/${userId}`); + }, + + // Get user by username + getUserByUsername: (username: string): Promise => { + return api.get(`/users/username/${username}`); + }, + + // Get user stats + getUserStats: (userId: string): Promise => { + return api.get(`/users/${userId}/stats`); + }, + + // Get user achievements + getUserAchievements: (userId: string): Promise => { + return api.get(`/users/${userId}/achievements`); + }, + + // Get user activity feed + getUserActivity: ( + userId: string, + params?: PaginationParams + ): Promise> => { + return api.get>(`/users/${userId}/activity`, { + params, + }); + }, + + // Search users + searchUsers: ( + query: string, + params?: PaginationParams + ): Promise> => { + return api.get>('/users/search', { + params: { query, ...params }, + }); + }, + + // Get global leaderboard + getLeaderboard: (params?: { + period?: 'week' | 'month' | 'season' | 'all_time'; + page?: number; + pageSize?: number; + }): Promise> => { + return api.get>('/users/leaderboard', { + params, + }); + }, + + // Follow user + followUser: (userId: string): Promise => { + return api.post(`/users/${userId}/follow`); + }, + + // Unfollow user + unfollowUser: (userId: string): Promise => { + return api.delete(`/users/${userId}/follow`); + }, + + // Get followers + getFollowers: ( + userId: string, + params?: PaginationParams + ): Promise> => { + return api.get>(`/users/${userId}/followers`, { + params, + }); + }, + + // Get following + getFollowing: ( + userId: string, + params?: PaginationParams + ): Promise> => { + return api.get>(`/users/${userId}/following`, { + params, + }); + }, + + // Get user match history + getUserMatches: ( + userId: string, + params?: PaginationParams & { status?: string; mode?: string } + ): Promise> => { + return api.get(`/users/${userId}/matches`, { params }); + }, + + // Get online users count + getOnlineCount: (): Promise<{ count: number }> => { + return api.get<{ count: number }>('/users/online-count'); + }, + + // Report user + reportUser: (userId: string, reason: string, details?: string): Promise => { + return api.post(`/users/${userId}/report`, { reason, details }); + }, + + // Block user + blockUser: (userId: string): Promise => { + return api.post(`/users/${userId}/block`); + }, + + // Unblock user + unblockUser: (userId: string): Promise => { + return api.delete(`/users/${userId}/block`); + }, + + // Get blocked users + getBlockedUsers: (): Promise => { + return api.get('/users/blocked'); + }, +}; + +export default usersApi; diff --git a/src/api/types/admin.types.ts b/src/api/types/admin.types.ts new file mode 100644 index 0000000..3ec9328 --- /dev/null +++ b/src/api/types/admin.types.ts @@ -0,0 +1,312 @@ +// Admin types + +import { UserRole, UserStatus } from './auth.types'; +import { MatchStatus } from './match.types'; +import { ServiceStatus } from './service.types'; + +export interface AdminDashboard { + systemStats: SystemStats; + activeMatches: ActiveMatchInfo[]; + recentActivity: AdminActivityLog[]; + alerts: SystemAlert[]; + queueStatus: QueueStatus; +} + +export interface SystemStats { + totalUsers: number; + activeUsers: number; + newUsersToday: number; + newUsersWeek: number; + totalTeams: number; + activeTeams: number; + totalMatches: number; + activeMatches: number; + matchesToday: number; + totalServices: number; + pendingServices: number; + totalExercises: number; + systemUptime: number; + serverLoad: ServerLoad; + storageUsage: StorageUsage; +} + +export interface ServerLoad { + cpu: number; + memory: number; + disk: number; + network: number; +} + +export interface StorageUsage { + total: number; + used: number; + available: number; + breakdown: StorageBreakdown; +} + +export interface StorageBreakdown { + services: number; + replays: number; + logs: number; + backups: number; + other: number; +} + +export interface ActiveMatchInfo { + matchId: string; + matchTitle: string; + mode: string; + status: MatchStatus; + teamsCount: number; + currentRound: number; + totalRounds: number; + startedAt: string; + healthStatus: HealthStatus; +} + +export type HealthStatus = 'healthy' | 'degraded' | 'critical' | 'unknown'; + +export interface AdminActivityLog { + id: string; + adminId: string; + adminName: string; + action: AdminAction; + targetType: AdminTargetType; + targetId: string; + targetName: string; + details: Record; + timestamp: string; + ip: string; +} + +export type AdminAction = + | 'create' + | 'update' + | 'delete' + | 'ban' + | 'unban' + | 'approve' + | 'reject' + | 'start' + | 'stop' + | 'pause' + | 'resume' + | 'config_change' + | 'role_change' + | 'system_restart'; + +export type AdminTargetType = + | 'user' + | 'team' + | 'match' + | 'service' + | 'exercise' + | 'system' + | 'config'; + +export interface SystemAlert { + id: string; + severity: AlertSeverity; + type: AlertType; + title: string; + message: string; + source: string; + isResolved: boolean; + createdAt: string; + resolvedAt: string | null; + resolvedBy: string | null; +} + +export type AlertSeverity = 'info' | 'warning' | 'error' | 'critical'; + +export type AlertType = + | 'high_load' + | 'service_down' + | 'security_threat' + | 'disk_space' + | 'memory_usage' + | 'match_error' + | 'checker_timeout' + | 'database_issue' + | 'network_issue'; + +export interface QueueStatus { + checkerQueue: QueueInfo; + scorerQueue: QueueInfo; + validationQueue: QueueInfo; + notificationQueue: QueueInfo; +} + +export interface QueueInfo { + pending: number; + processing: number; + completed: number; + failed: number; + avgProcessingTime: number; + isHealthy: boolean; +} + +export interface AdminUserListParams { + search?: string; + role?: UserRole; + status?: UserStatus; + teamId?: string; + createdFrom?: string; + createdTo?: string; + lastLoginFrom?: string; + lastLoginTo?: string; + sortBy?: string; + sortOrder?: 'asc' | 'desc'; + page?: number; + pageSize?: number; +} + +export interface AdminUpdateUserRequest { + role?: UserRole; + status?: UserStatus; + displayName?: string; + email?: string; + emailVerified?: boolean; + banReason?: string; + banExpires?: string; +} + +export interface AdminCreateMatchRequest { + title: string; + description?: string; + mode: string; + config: Record; + scheduledAt?: string; + teamIds?: string[]; + serviceIds: string[]; + isRanked?: boolean; + seasonId?: string; +} + +export interface AdminMatchControl { + matchId: string; + action: 'start' | 'stop' | 'pause' | 'resume' | 'cancel' | 'restart'; + reason?: string; +} + +export interface AdminServiceListParams { + search?: string; + status?: ServiceStatus; + category?: string; + authorId?: string; + isFeatured?: boolean; + createdFrom?: string; + createdTo?: string; + sortBy?: string; + sortOrder?: 'asc' | 'desc'; + page?: number; + pageSize?: number; +} + +export interface AdminUpdateServiceRequest { + status?: ServiceStatus; + isFeatured?: boolean; + isPublic?: boolean; + rejectionReason?: string; +} + +export interface AdminServiceReview { + serviceId: string; + decision: 'approve' | 'reject' | 'needs_revision'; + feedback: string; + suggestedChanges?: string[]; +} + +export interface SystemConfig { + general: GeneralConfig; + matches: MatchesConfig; + security: SecurityConfig; + email: EmailConfig; + storage: StorageConfig; + integrations: IntegrationsConfig; +} + +export interface GeneralConfig { + siteName: string; + siteUrl: string; + maintenanceMode: boolean; + maintenanceMessage: string; + registrationEnabled: boolean; + inviteOnly: boolean; + defaultUserRole: UserRole; + maxTeamSize: number; +} + +export interface MatchesConfig { + maxConcurrentMatches: number; + defaultRoundDuration: number; + defaultTotalRounds: number; + checkerTimeout: number; + checkerInterval: number; + flagLifetime: number; + scoringPolicy: string; + replayRetention: number; +} + +export interface SecurityConfig { + sessionTimeout: number; + maxLoginAttempts: number; + lockoutDuration: number; + passwordMinLength: number; + requireEmailVerification: boolean; + twoFactorEnabled: boolean; + apiRateLimit: number; + flagSubmitRateLimit: number; +} + +export interface EmailConfig { + provider: string; + fromAddress: string; + fromName: string; + smtpHost?: string; + smtpPort?: number; + smtpSecure?: boolean; +} + +export interface StorageConfig { + provider: string; + bucket: string; + region: string; + maxFileSize: number; + allowedFileTypes: string[]; +} + +export interface IntegrationsConfig { + discordWebhook?: string; + slackWebhook?: string; + ctftimeTeamId?: string; + googleAnalyticsId?: string; + sentryDsn?: string; +} + +export interface UpdateSystemConfigRequest { + section: keyof SystemConfig; + config: Partial; +} + +export interface SystemHealthCheck { + overall: HealthStatus; + services: ServiceHealthCheck[]; + lastCheck: string; + nextCheck: string; +} + +export interface ServiceHealthCheck { + name: string; + status: HealthStatus; + latency: number; + message: string | null; + lastCheck: string; +} + +export interface MaintenanceRequest { + enabled: boolean; + message: string; + estimatedDuration?: number; + allowAdminAccess?: boolean; +} diff --git a/src/api/types/analytics.types.ts b/src/api/types/analytics.types.ts new file mode 100644 index 0000000..e55d944 --- /dev/null +++ b/src/api/types/analytics.types.ts @@ -0,0 +1,483 @@ +// Analytics types + +import { ServiceCategory, ServiceDifficulty } from './service.types'; +import { TrackRole, SkillCategory } from './training.types'; + +export interface TeamAnalytics { + teamId: string; + teamName: string; + period: AnalyticsPeriod; + overview: TeamOverview; + performance: TeamPerformance; + strengths: StrengthWeakness[]; + weaknesses: StrengthWeakness[]; + trends: TeamTrend[]; + comparison: TeamComparison | null; + recommendations: AIRecommendation[]; + memberAnalytics: MemberAnalytics[]; + serviceAnalytics: TeamServiceAnalytics[]; + generatedAt: string; +} + +export interface AnalyticsPeriod { + from: string; + to: string; + type: 'week' | 'month' | 'season' | 'all_time' | 'custom'; +} + +export interface TeamOverview { + matchesPlayed: number; + matchesWon: number; + winRate: number; + avgPosition: number; + bestPosition: number; + worstPosition: number; + totalScore: number; + avgScore: number; + totalFlagsCaptured: number; + totalFlagsLost: number; + avgSLA: number; + rating: number; + ratingChange: number; + rank: number; + rankChange: number; +} + +export interface TeamPerformance { + attackEfficiency: number; + defenseEfficiency: number; + slaConsistency: number; + responseTime: number; + exploitSpeed: number; + patchSpeed: number; + coordination: number; + adaptability: number; +} + +export interface StrengthWeakness { + type: 'attack' | 'defense' | 'sla' | 'service' | 'skill'; + name: string; + description: string; + score: number; + trend: 'improving' | 'stable' | 'declining'; + relatedServices?: string[]; + relatedSkills?: string[]; +} + +export interface TeamTrend { + metric: string; + values: TrendPoint[]; + trend: 'up' | 'down' | 'stable'; + changePercent: number; +} + +export interface TrendPoint { + timestamp: string; + value: number; + label: string; +} + +export interface TeamComparison { + compareToTeamId: string; + compareToTeamName: string; + metrics: ComparisonMetric[]; +} + +export interface ComparisonMetric { + name: string; + ourValue: number; + theirValue: number; + difference: number; + differencePercent: number; + winner: 'us' | 'them' | 'tie'; +} + +export interface MemberAnalytics { + userId: string; + userName: string; + role: TrackRole; + contribution: number; + attackContribution: number; + defenseContribution: number; + activityLevel: number; + skillGrowth: number; + strengths: string[]; + areasToImprove: string[]; +} + +export interface TeamServiceAnalytics { + serviceId: string; + serviceName: string; + category: ServiceCategory; + attackSuccess: number; + defenseSuccess: number; + avgSLA: number; + flagsCaptured: number; + flagsLost: number; + exploitTime: number; + patchTime: number; + recommendation: string; +} + +export interface PlayerAnalytics { + userId: string; + userName: string; + period: AnalyticsPeriod; + overview: PlayerOverview; + skillAnalysis: SkillAnalysis; + roleAnalysis: RoleAnalysis; + learningPath: LearningPath; + achievements: AchievementAnalysis; + recommendations: AIRecommendation[]; + activityHeatmap: ActivityHeatmap; + generatedAt: string; +} + +export interface PlayerOverview { + matchesPlayed: number; + teamsPlayed: number; + totalScore: number; + avgScore: number; + flagsCaptured: number; + flagsDefended: number; + trainingProgress: number; + exercisesCompleted: number; + hoursSpent: number; + currentStreak: number; + longestStreak: number; + rating: number; + percentile: number; +} + +export interface SkillAnalysis { + overallLevel: number; + skillLevels: SkillLevelAnalysis[]; + skillGrowth: SkillGrowthData[]; + strongestSkills: string[]; + weakestSkills: string[]; + recentlyImproved: string[]; +} + +export interface SkillLevelAnalysis { + skillId: string; + skillName: string; + category: SkillCategory; + level: number; + maxLevel: number; + percentile: number; + trend: 'up' | 'down' | 'stable'; +} + +export interface SkillGrowthData { + skillId: string; + skillName: string; + history: TrendPoint[]; +} + +export interface RoleAnalysis { + primaryRole: TrackRole; + secondaryRole: TrackRole | null; + roleScores: RoleScore[]; + roleRecommendation: string; + roleComparison: RoleComparison[]; +} + +export interface RoleScore { + role: TrackRole; + score: number; + percentile: number; + matchingSkills: string[]; +} + +export interface RoleComparison { + role: TrackRole; + avgScore: number; + yourScore: number; + gap: number; +} + +export interface LearningPath { + currentTrack: string | null; + completedTracks: string[]; + suggestedTracks: SuggestedTrack[]; + nextMilestone: Milestone; + longTermGoals: Goal[]; +} + +export interface SuggestedTrack { + trackId: string; + trackName: string; + reason: string; + matchScore: number; + estimatedTime: number; + priority: number; +} + +export interface Milestone { + type: string; + title: string; + description: string; + progress: number; + target: number; + estimatedCompletion: string; +} + +export interface Goal { + id: string; + title: string; + description: string; + progress: number; + target: number; + deadline: string | null; +} + +export interface AchievementAnalysis { + totalUnlocked: number; + totalAvailable: number; + recentUnlocks: string[]; + nearUnlocks: NearAchievement[]; + rareAchievements: string[]; +} + +export interface NearAchievement { + achievementId: string; + name: string; + progress: number; + target: number; + hint: string; +} + +export interface ActivityHeatmap { + data: HeatmapCell[]; + mostActiveDay: string; + mostActiveHour: number; + totalActiveDays: number; +} + +export interface HeatmapCell { + date: string; + hour: number; + value: number; + activities: string[]; +} + +export interface MatchAnalytics { + matchId: string; + matchTitle: string; + overview: MatchAnalyticsOverview; + timeline: MatchTimeline; + teamAnalytics: MatchTeamAnalytics[]; + serviceAnalytics: MatchServiceAnalytics[]; + attackPatterns: AttackPattern[]; + keyMoments: KeyMomentAnalysis[]; + aiInsights: AIRecommendation[]; + heatmaps: MatchHeatmaps; + generatedAt: string; +} + +export interface MatchAnalyticsOverview { + duration: number; + totalRounds: number; + totalFlags: number; + totalAttacks: number; + successfulAttacks: number; + avgSLA: number; + competitiveness: number; + volatility: number; + dominantTeam: string | null; +} + +export interface MatchTimeline { + events: TimelineEventAnalysis[]; + phases: MatchPhase[]; + turningPoints: TurningPoint[]; +} + +export interface TimelineEventAnalysis { + timestamp: string; + round: number; + type: string; + significance: number; + teams: string[]; + description: string; +} + +export interface MatchPhase { + name: string; + fromRound: number; + toRound: number; + characteristics: string[]; + dominantTeam: string | null; +} + +export interface TurningPoint { + round: number; + timestamp: string; + description: string; + impact: number; + beforeLeader: string; + afterLeader: string; +} + +export interface MatchTeamAnalytics { + teamId: string; + teamName: string; + finalPosition: number; + scoreBreakdown: ScoreBreakdownAnalysis; + performance: PerformanceMetrics; + strategy: StrategyAnalysis; +} + +export interface ScoreBreakdownAnalysis { + attack: number; + defense: number; + sla: number; + penalties: number; + bonuses: number; + byRound: RoundScore[]; +} + +export interface RoundScore { + round: number; + total: number; + attack: number; + defense: number; + sla: number; +} + +export interface PerformanceMetrics { + attackEfficiency: number; + defenseEfficiency: number; + exploitSpeed: number; + patchSpeed: number; + slaUptime: number; + consistency: number; +} + +export interface StrategyAnalysis { + type: 'aggressive' | 'defensive' | 'balanced' | 'adaptive'; + focusedServices: string[]; + attackTargets: string[]; + timeline: StrategyPhase[]; +} + +export interface StrategyPhase { + fromRound: number; + toRound: number; + strategy: string; + effectiveness: number; +} + +export interface MatchServiceAnalytics { + serviceId: string; + serviceName: string; + category: ServiceCategory; + difficulty: ServiceDifficulty; + totalExploits: number; + uniqueExploiters: number; + firstBlood: FirstBloodAnalysis | null; + avgExploitTime: number; + patchRate: number; + avgSLA: number; + vulnerabilitiesExploited: string[]; +} + +export interface FirstBloodAnalysis { + teamId: string; + teamName: string; + round: number; + timestamp: string; + timeFromStart: number; +} + +export interface AttackPattern { + id: string; + type: string; + frequency: number; + successRate: number; + targetServices: string[]; + attackerTeams: string[]; + peakRounds: number[]; + description: string; +} + +export interface KeyMomentAnalysis { + round: number; + timestamp: string; + type: string; + title: string; + description: string; + impact: number; + involvedTeams: string[]; + scoreChanges: Record; +} + +export interface MatchHeatmaps { + attackHeatmap: HeatmapData; + scoreHeatmap: HeatmapData; + slaHeatmap: HeatmapData; +} + +export interface HeatmapData { + xLabels: string[]; + yLabels: string[]; + values: number[][]; + min: number; + max: number; +} + +export interface AIRecommendation { + id: string; + type: RecommendationType; + priority: 'low' | 'medium' | 'high' | 'critical'; + title: string; + description: string; + rationale: string; + actionItems: ActionItem[]; + relatedSkills: string[]; + relatedServices: string[]; + estimatedImpact: number; + estimatedEffort: number; + confidence: number; + generatedAt: string; +} + +export type RecommendationType = + | 'skill_improvement' + | 'strategy_change' + | 'service_focus' + | 'team_coordination' + | 'training_suggestion' + | 'role_adjustment' + | 'tool_recommendation' + | 'resource_allocation'; + +export interface ActionItem { + id: string; + action: string; + priority: number; + estimatedTime: number; + resources: string[]; +} + +export interface TeamAnalyticsParams { + teamId: string; + period?: AnalyticsPeriod; + compareToTeamId?: string; + includeMembers?: boolean; + includeRecommendations?: boolean; +} + +export interface PlayerAnalyticsParams { + userId: string; + period?: AnalyticsPeriod; + includeSkillAnalysis?: boolean; + includeRecommendations?: boolean; +} + +export interface MatchAnalyticsParams { + matchId: string; + includeHeatmaps?: boolean; + includeAIInsights?: boolean; + teamId?: string; +} diff --git a/src/api/types/auth.types.ts b/src/api/types/auth.types.ts new file mode 100644 index 0000000..7c32056 --- /dev/null +++ b/src/api/types/auth.types.ts @@ -0,0 +1,135 @@ +// Authentication types + +export interface User { + id: string; + username: string; + email: string; + displayName: string; + avatar: string | null; + role: UserRole; + teamId: string | null; + status: UserStatus; + stats: UserStats; + achievements: Achievement[]; + createdAt: string; + lastLoginAt: string | null; + emailVerified: boolean; + twoFactorEnabled: boolean; +} + +export type UserRole = 'user' | 'organizer' | 'admin' | 'superadmin'; + +export type UserStatus = 'active' | 'inactive' | 'banned' | 'pending'; + +export interface UserStats { + matchesPlayed: number; + matchesWon: number; + flagsCaptured: number; + flagsDefended: number; + totalScore: number; + rank: number; + rating: number; + trainingProgress: number; + hoursPlayed: number; +} + +export interface Achievement { + id: string; + name: string; + description: string; + icon: string; + rarity: 'common' | 'uncommon' | 'rare' | 'epic' | 'legendary'; + unlockedAt: string; + progress?: number; + maxProgress?: number; +} + +export interface LoginRequest { + email: string; + password: string; + rememberMe?: boolean; + twoFactorCode?: string; +} + +export interface RegisterRequest { + username: string; + email: string; + password: string; + confirmPassword: string; + displayName?: string; + acceptTerms: boolean; +} + +export interface AuthResponse { + user: User; + tokens: TokenPair; +} + +export interface TokenPair { + accessToken: string; + refreshToken: string; + expiresIn: number; + refreshExpiresIn: number; +} + +export interface ForgotPasswordRequest { + email: string; +} + +export interface ResetPasswordRequest { + token: string; + password: string; + confirmPassword: string; +} + +export interface ChangePasswordRequest { + currentPassword: string; + newPassword: string; + confirmPassword: string; +} + +export interface UpdateProfileRequest { + displayName?: string; + avatar?: string; + bio?: string; + socialLinks?: SocialLinks; + notificationSettings?: NotificationSettings; +} + +export interface SocialLinks { + github?: string; + twitter?: string; + linkedin?: string; + website?: string; + ctftime?: string; +} + +export interface NotificationSettings { + emailNotifications: boolean; + matchReminders: boolean; + teamInvites: boolean; + achievementUnlocks: boolean; + systemAnnouncements: boolean; + weeklyDigest: boolean; +} + +export interface ApiKey { + id: string; + name: string; + prefix: string; + createdAt: string; + lastUsedAt: string | null; + expiresAt: string | null; + permissions: string[]; +} + +export interface CreateApiKeyRequest { + name: string; + permissions: string[]; + expiresIn?: number; +} + +export interface CreateApiKeyResponse { + apiKey: ApiKey; + secret: string; // Only shown once +} diff --git a/src/api/types/common.types.ts b/src/api/types/common.types.ts new file mode 100644 index 0000000..d35ef17 --- /dev/null +++ b/src/api/types/common.types.ts @@ -0,0 +1,71 @@ +// Common types used across the application + +export interface PaginatedResponse { + data: T[]; + total: number; + page: number; + pageSize: number; + totalPages: number; + hasNext: boolean; + hasPrev: boolean; +} + +export interface ApiError { + code: string; + message: string; + details?: Record; + timestamp: string; + path: string; +} + +export type SortOrder = 'asc' | 'desc'; + +export interface SortParams { + field: string; + order: SortOrder; +} + +export interface FilterParams { + [key: string]: string | number | boolean | string[] | undefined; +} + +export interface PaginationParams { + page?: number; + pageSize?: number; + sort?: SortParams; + filters?: FilterParams; +} + +export interface ApiResponse { + success: boolean; + data: T; + message?: string; +} + +export interface SelectOption { + value: string; + label: string; +} + +export interface DateRange { + from: string; + to: string; +} + +export interface Coordinates { + x: number; + y: number; +} + +export interface TimeRange { + start: number; + end: number; +} + +export type LoadingState = 'idle' | 'loading' | 'success' | 'error'; + +export interface AsyncState { + data: T | null; + loading: boolean; + error: string | null; +} diff --git a/src/api/types/flag.types.ts b/src/api/types/flag.types.ts new file mode 100644 index 0000000..772feef --- /dev/null +++ b/src/api/types/flag.types.ts @@ -0,0 +1,127 @@ +// Flag types + +export interface Flag { + id: string; + matchId: string; + roundNumber: number; + serviceId: string; + serviceName: string; + teamId: string; + teamName: string; + value: string; + status: FlagStatus; + generatedAt: string; + expiresAt: string; + capturedAt: string | null; + capturedBy: string | null; + capturedByTeam: string | null; + points: number; +} + +export type FlagStatus = + | 'active' + | 'captured' + | 'expired' + | 'stolen' + | 'invalid'; + +export interface FlagSubmission { + id: string; + matchId: string; + teamId: string; + teamName: string; + flagValue: string; + result: FlagSubmissionResult; + message: string; + points: number; + targetTeamId: string | null; + targetTeamName: string | null; + serviceId: string | null; + serviceName: string | null; + roundNumber: number; + submittedAt: string; + latency: number; // ms +} + +export type FlagSubmissionResult = + | 'accepted' + | 'duplicate' + | 'expired' + | 'own_flag' + | 'invalid' + | 'not_found' + | 'rate_limited' + | 'match_not_active'; + +export interface SubmitFlagRequest { + matchId: string; + flag: string; +} + +export interface SubmitFlagResponse { + success: boolean; + result: FlagSubmissionResult; + message: string; + points: number; + targetTeam: string | null; + service: string | null; +} + +export interface FlagHistoryParams { + matchId: string; + teamId?: string; + serviceId?: string; + status?: FlagStatus; + round?: number; + limit?: number; + offset?: number; +} + +export interface FlagStats { + matchId: string; + teamId: string; + totalCaptured: number; + totalLost: number; + totalPoints: number; + byService: FlagStatsByService[]; + byRound: FlagStatsByRound[]; + captureRate: number; + avgCaptureTime: number; +} + +export interface FlagStatsByService { + serviceId: string; + serviceName: string; + captured: number; + lost: number; + points: number; +} + +export interface FlagStatsByRound { + round: number; + captured: number; + lost: number; + points: number; +} + +export interface FlagSubmissionStats { + total: number; + accepted: number; + duplicate: number; + expired: number; + ownFlag: number; + invalid: number; + successRate: number; +} + +export interface BulkSubmitFlagsRequest { + matchId: string; + flags: string[]; +} + +export interface BulkSubmitFlagsResponse { + results: SubmitFlagResponse[]; + accepted: number; + rejected: number; + totalPoints: number; +} diff --git a/src/api/types/index.ts b/src/api/types/index.ts new file mode 100644 index 0000000..c6f93c8 --- /dev/null +++ b/src/api/types/index.ts @@ -0,0 +1,38 @@ +// Export all types from a single entry point + +export * from './common.types'; +export * from './auth.types'; +export * from './team.types'; +export * from './match.types'; +export * from './round.types'; +export * from './service.types'; +export * from './scoreboard.types'; +export * from './flag.types'; +export { + type LogEntry, + type LogLevel, + type EventType, + type LogSource, + type LogMetadata, + type TimelineEvent as LogTimelineEvent, + type TimelineTeamInfo, + type TimelineServiceInfo, + type EventImportance, + type ReplayFrame, + type ReplayScoreboardState, + type ReplayTeamScore, + type ReplayServiceStatus, + type ReplayData, + type ReplayTeamInfo, + type ReplayServiceInfo, + type KeyMoment, + type LogFilterParams, + type LogStreamConfig, + type EventTimelineParams, + type ReplayParams +} from './log.types'; +export * from './training.types'; +export * from './analytics.types'; +export * from './notification.types'; +export * from './admin.types'; +export * from './season.types'; diff --git a/src/api/types/log.types.ts b/src/api/types/log.types.ts new file mode 100644 index 0000000..01d1c5a --- /dev/null +++ b/src/api/types/log.types.ts @@ -0,0 +1,208 @@ +// Log types + +export interface LogEntry { + id: string; + matchId: string; + roundNumber: number; + timestamp: string; + level: LogLevel; + type: EventType; + source: LogSource; + teamId: string | null; + teamName: string | null; + targetTeamId: string | null; + targetTeamName: string | null; + serviceId: string | null; + serviceName: string | null; + message: string; + details: Record; + metadata: LogMetadata; +} + +export type LogLevel = 'debug' | 'info' | 'warning' | 'error' | 'critical'; + +export type EventType = + | 'match_start' + | 'match_end' + | 'match_pause' + | 'match_resume' + | 'round_start' + | 'round_end' + | 'flag_generated' + | 'flag_captured' + | 'flag_expired' + | 'flag_submitted' + | 'service_check' + | 'service_up' + | 'service_down' + | 'service_corrupt' + | 'attack_detected' + | 'exploit_attempt' + | 'score_update' + | 'penalty' + | 'bonus' + | 'team_join' + | 'team_leave' + | 'system_info' + | 'system_error' + | 'admin_action' + | 'network_event'; + +export type LogSource = + | 'system' + | 'checker' + | 'scorer' + | 'network' + | 'team' + | 'admin' + | 'api'; + +export interface LogMetadata { + ip?: string; + userAgent?: string; + requestId?: string; + traceId?: string; + spanId?: string; + duration?: number; + statusCode?: number; +} + +export interface TimelineEvent { + id: string; + type: EventType; + timestamp: string; + relativeTime: number; + round: number; + title: string; + description: string; + icon: string; + color: string; + teams: TimelineTeamInfo[]; + service: TimelineServiceInfo | null; + score: number | null; + importance: EventImportance; + linkedEvents: string[]; +} + +export interface TimelineTeamInfo { + teamId: string; + teamName: string; + teamTag: string; + role: 'attacker' | 'defender' | 'neutral'; +} + +export interface TimelineServiceInfo { + serviceId: string; + serviceName: string; +} + +export type EventImportance = 'low' | 'medium' | 'high' | 'critical'; + +export interface ReplayFrame { + timestamp: string; + relativeTime: number; + round: number; + scoreboard: ReplayScoreboardState; + events: TimelineEvent[]; + serviceStatuses: ReplayServiceStatus[]; +} + +export interface ReplayScoreboardState { + entries: ReplayTeamScore[]; + timestamp: string; +} + +export interface ReplayTeamScore { + teamId: string; + teamName: string; + position: number; + score: number; + attackScore: number; + defenseScore: number; + slaScore: number; +} + +export interface ReplayServiceStatus { + teamId: string; + serviceId: string; + status: string; + lastCheck: string; +} + +export interface ReplayData { + matchId: string; + matchTitle: string; + duration: number; + totalRounds: number; + teams: ReplayTeamInfo[]; + services: ReplayServiceInfo[]; + frames: ReplayFrame[]; + keyMoments: KeyMoment[]; +} + +export interface ReplayTeamInfo { + id: string; + name: string; + tag: string; + avatar: string | null; + color: string; +} + +export interface ReplayServiceInfo { + id: string; + name: string; + category: string; +} + +export interface KeyMoment { + timestamp: string; + relativeTime: number; + round: number; + type: string; + title: string; + description: string; +} + +export interface LogFilterParams { + matchId: string; + levels?: LogLevel[]; + types?: EventType[]; + sources?: LogSource[]; + teamId?: string; + serviceId?: string; + fromTimestamp?: string; + toTimestamp?: string; + fromRound?: number; + toRound?: number; + search?: string; + limit?: number; + offset?: number; +} + +export interface LogStreamConfig { + matchId: string; + filters?: { + levels?: LogLevel[]; + types?: EventType[]; + teamId?: string; + serviceId?: string; + }; + bufferSize?: number; +} + +export interface EventTimelineParams { + matchId: string; + fromRound?: number; + toRound?: number; + teamIds?: string[]; + serviceIds?: string[]; + importance?: EventImportance; + types?: EventType[]; +} + +export interface ReplayParams { + matchId: string; + fromTime?: number; + toTime?: number; + resolution?: 'low' | 'medium' | 'high'; +} diff --git a/src/api/types/match.types.ts b/src/api/types/match.types.ts new file mode 100644 index 0000000..dd620dc --- /dev/null +++ b/src/api/types/match.types.ts @@ -0,0 +1,196 @@ +// Match types + +import { Team } from './team.types'; +import { Service } from './service.types'; +import { Round } from './round.types'; + +export interface Match { + id: string; + title: string; + description: string; + mode: MatchMode; + status: MatchStatus; + config: MatchConfig; + teams: MatchTeam[]; + services: Service[]; + rounds: Round[]; + currentRound: number; + totalRounds: number; + roundDuration: number; // seconds + startedAt: string | null; + finishedAt: string | null; + scheduledAt: string | null; + createdBy: string; + createdAt: string; + updatedAt: string; + seasonId: string | null; + isRanked: boolean; + visibility: MatchVisibility; +} + +export type MatchMode = 'training' | 'tournament' | 'scrim' | 'practice'; + +export type MatchStatus = + | 'draft' + | 'lobby' + | 'starting' + | 'running' + | 'paused' + | 'finished' + | 'cancelled' + | 'archived'; + +export type MatchVisibility = 'public' | 'private' | 'unlisted'; + +export interface MatchConfig { + maxTeams: number; + minTeams: number; + scoringPolicy: ScoringPolicy; + flagLifetime: number; // seconds + checkerTimeout: number; // seconds + checkerInterval: number; // seconds + enableHints: boolean; + enableReplay: boolean; + freezeTime: number; // minutes before end to freeze scoreboard + allowedServiceIds: string[]; + networkConfig: NetworkConfig; + penaltyConfig: PenaltyConfig; + flagFormat: string; // regex pattern + autoStart: boolean; + warmupDuration: number; // seconds +} + +export type ScoringPolicy = 'classic' | 'linear' | 'exponential' | 'dynamic'; + +export interface NetworkConfig { + teamSubnet: string; + servicePort: number; + vpnEnabled: boolean; + bandwidthLimit: number | null; +} + +export interface PenaltyConfig { + slaDownPenalty: number; + slaCorruptPenalty: number; + flagMissPenalty: number; + maxPenaltyPerRound: number; +} + +export interface MatchTeam { + id: string; + matchId: string; + team: Team; + slot: number; + status: MatchTeamStatus; + isReady: boolean; + joinedAt: string; + networkInfo: TeamNetworkInfo; +} + +export type MatchTeamStatus = 'registered' | 'ready' | 'playing' | 'finished' | 'disqualified'; + +export interface TeamNetworkInfo { + ip: string; + subnet: string; + vpnConfig: string | null; +} + +export interface MatchResult { + matchId: string; + match: Match; + rankings: MatchRanking[]; + duration: number; // seconds + totalRounds: number; + totalFlags: number; + stats: MatchStats; + highlights: MatchHighlight[]; + completedAt: string; +} + +export interface MatchRanking { + position: number; + team: Team; + score: number; + attackScore: number; + defenseScore: number; + slaScore: number; + flagsCaptured: number; + flagsLost: number; + avgSLA: number; + servicesUp: number; + servicesTotal: number; + trend: number; // position change from previous +} + +export interface MatchStats { + totalFlags: number; + totalAttacks: number; + successfulAttacks: number; + avgSLA: number; + mostActiveTeam: string; + mostAttackedService: string; + longestDowntime: number; + firstBlood: FirstBloodInfo | null; +} + +export interface FirstBloodInfo { + teamId: string; + teamName: string; + serviceId: string; + serviceName: string; + timestamp: string; + round: number; +} + +export interface MatchHighlight { + id: string; + type: HighlightType; + title: string; + description: string; + teamId: string; + round: number; + timestamp: string; + score: number; +} + +export type HighlightType = + | 'first_blood' + | 'comeback' + | 'dominant_round' + | 'perfect_defense' + | 'massive_attack' + | 'sla_recovery'; + +export interface CreateMatchRequest { + title: string; + description?: string; + mode: MatchMode; + config: Partial; + scheduledAt?: string; + visibility?: MatchVisibility; + isRanked?: boolean; + seasonId?: string; +} + +export interface UpdateMatchRequest { + title?: string; + description?: string; + config?: Partial; + scheduledAt?: string; + visibility?: MatchVisibility; +} + +export interface MatchListParams { + mode?: MatchMode; + status?: MatchStatus; + teamId?: string; + seasonId?: string; + isRanked?: boolean; + from?: string; + to?: string; +} + +export interface JoinMatchRequest { + teamId: string; + slot?: number; +} diff --git a/src/api/types/notification.types.ts b/src/api/types/notification.types.ts new file mode 100644 index 0000000..122dcd5 --- /dev/null +++ b/src/api/types/notification.types.ts @@ -0,0 +1,133 @@ +// Notification types + +export interface Notification { + id: string; + userId: string; + type: NotificationType; + category: NotificationCategory; + title: string; + message: string; + data: NotificationData; + isRead: boolean; + isArchived: boolean; + priority: NotificationPriority; + actionUrl: string | null; + actionText: string | null; + createdAt: string; + readAt: string | null; + expiresAt: string | null; +} + +export type NotificationType = + | 'match_invite' + | 'match_starting' + | 'match_ended' + | 'match_result' + | 'team_invite' + | 'team_join_request' + | 'team_member_joined' + | 'team_member_left' + | 'team_role_changed' + | 'flag_captured' + | 'service_down' + | 'achievement_unlocked' + | 'training_completed' + | 'new_exercise' + | 'streak_reminder' + | 'weekly_digest' + | 'system_announcement' + | 'maintenance' + | 'security_alert' + | 'rating_change' + | 'comment_reply' + | 'mention'; + +export type NotificationCategory = + | 'match' + | 'team' + | 'training' + | 'achievement' + | 'social' + | 'system' + | 'security'; + +export type NotificationPriority = 'low' | 'normal' | 'high' | 'urgent'; + +export interface NotificationData { + matchId?: string; + matchTitle?: string; + teamId?: string; + teamName?: string; + userId?: string; + userName?: string; + serviceId?: string; + serviceName?: string; + achievementId?: string; + achievementName?: string; + trackId?: string; + trackName?: string; + exerciseId?: string; + exerciseName?: string; + score?: number; + position?: number; + rating?: number; + ratingChange?: number; + [key: string]: unknown; +} + +export interface NotificationPreferences { + email: NotificationChannelPrefs; + push: NotificationChannelPrefs; + inApp: NotificationChannelPrefs; +} + +export interface NotificationChannelPrefs { + enabled: boolean; + categories: { + [K in NotificationCategory]: boolean; + }; + quietHours: QuietHours | null; +} + +export interface QuietHours { + enabled: boolean; + from: string; // HH:mm + to: string; // HH:mm + timezone: string; +} + +export interface NotificationGroup { + date: string; + notifications: Notification[]; +} + +export interface NotificationStats { + total: number; + unread: number; + byCategory: Record; + byType: Record; +} + +export interface GetNotificationsParams { + category?: NotificationCategory; + type?: NotificationType; + isRead?: boolean; + isArchived?: boolean; + limit?: number; + offset?: number; + from?: string; + to?: string; +} + +export interface MarkNotificationsRequest { + notificationIds: string[]; + isRead?: boolean; + isArchived?: boolean; +} + +export interface NotificationEvent { + type: 'new' | 'read' | 'deleted'; + notification?: Notification; + notificationId?: string; + unreadCount: number; +} diff --git a/src/api/types/round.types.ts b/src/api/types/round.types.ts new file mode 100644 index 0000000..14f9dce --- /dev/null +++ b/src/api/types/round.types.ts @@ -0,0 +1,136 @@ +// Round types + +export interface Round { + id: string; + matchId: string; + number: number; + status: RoundStatus; + startedAt: string | null; + finishedAt: string | null; + duration: number; // actual duration in seconds + events: RoundEvent[]; + serviceChecks: ServiceCheck[]; + flagsGenerated: number; + flagsCaptured: number; + avgSLA: number; +} + +export type RoundStatus = 'pending' | 'active' | 'finished' | 'skipped'; + +export interface RoundEvent { + id: string; + roundId: string; + type: RoundEventType; + teamId: string | null; + targetTeamId: string | null; + serviceId: string | null; + data: RoundEventData; + timestamp: string; + round: number; +} + +export type RoundEventType = + | 'round_start' + | 'round_end' + | 'flag_generated' + | 'flag_captured' + | 'flag_expired' + | 'service_check' + | 'service_up' + | 'service_down' + | 'service_corrupt' + | 'attack_detected' + | 'exploit_blocked' + | 'score_update' + | 'penalty_applied' + | 'first_blood'; + +export interface RoundEventData { + flagId?: string; + flagValue?: string; + score?: number; + attackerTeamId?: string; + defenderTeamId?: string; + serviceId?: string; + serviceName?: string; + checkResult?: CheckerResult; + penaltyReason?: string; + penaltyAmount?: number; + message?: string; + details?: Record; +} + +export interface ServiceCheck { + id: string; + roundId: string; + serviceId: string; + teamId: string; + result: CheckerResult; + message: string | null; + latency: number; // ms + timestamp: string; +} + +export type CheckerResult = + | 'ok' + | 'corrupt' + | 'mumble' + | 'down' + | 'error' + | 'timeout'; + +export interface RoundSummary { + roundNumber: number; + teamScores: TeamRoundScore[]; + topAttacker: string | null; + topDefender: string | null; + flagsCaptured: number; + avgSLA: number; + duration: number; +} + +export interface TeamRoundScore { + teamId: string; + teamName: string; + score: number; + attackScore: number; + defenseScore: number; + slaScore: number; + flagsCaptured: number; + flagsLost: number; + servicesUp: number; + totalServices: number; +} + +export interface RoundTimeline { + roundNumber: number; + events: TimelineEvent[]; +} + +export interface TimelineEvent { + id: string; + type: RoundEventType; + timestamp: string; + relativeTime: number; // seconds from round start + teams: string[]; + service: string | null; + score: number | null; + icon: string; + color: string; + title: string; + description: string; +} + +export interface RoundParams { + matchId: string; + roundNumber?: number; +} + +export interface RoundEventsParams { + roundId: string; + types?: RoundEventType[]; + teamId?: string; + serviceId?: string; + limit?: number; + offset?: number; +} diff --git a/src/api/types/scoreboard.types.ts b/src/api/types/scoreboard.types.ts new file mode 100644 index 0000000..267f346 --- /dev/null +++ b/src/api/types/scoreboard.types.ts @@ -0,0 +1,138 @@ +// Scoreboard types + +import { ServiceStatusInfo } from './service.types'; + +export interface ScoreboardEntry { + position: number; + previousPosition: number; + positionChange: number; + teamId: string; + teamName: string; + teamTag: string; + teamAvatar: string | null; + score: number; + attackScore: number; + defenseScore: number; + slaScore: number; + breakdown: ScoreBreakdown; + serviceStatuses: ServiceStatusInfo[]; + flagsCaptured: number; + flagsLost: number; + avgSLA: number; + isCurrentTeam: boolean; + trend: ScoreTrend; +} + +export interface ScoreBreakdown { + services: ServiceScore[]; + totalAttack: number; + totalDefense: number; + totalSLA: number; + penalties: number; + bonuses: number; +} + +export interface ServiceScore { + serviceId: string; + serviceName: string; + attackScore: number; + defenseScore: number; + slaScore: number; + flagsCaptured: number; + flagsLost: number; + slaPercentage: number; + status: string; +} + +export type ScoreTrend = 'up' | 'down' | 'stable'; + +export interface ScoreHistory { + teamId: string; + teamName: string; + history: ScorePoint[]; +} + +export interface ScorePoint { + round: number; + timestamp: string; + score: number; + attackScore: number; + defenseScore: number; + slaScore: number; + position: number; +} + +export interface TeamScore { + teamId: string; + teamName: string; + score: number; + position: number; + breakdown: ScoreBreakdown; + history: ScorePoint[]; + lastUpdate: string; +} + +export interface ScoreboardData { + matchId: string; + matchTitle: string; + currentRound: number; + totalRounds: number; + roundEndsAt: string | null; + isFrozen: boolean; + frozenAt: string | null; + entries: ScoreboardEntry[]; + lastUpdate: string; +} + +export interface ScoreboardUpdate { + type: 'full' | 'partial'; + matchId: string; + round: number; + timestamp: string; + entries?: ScoreboardEntry[]; + updates?: PartialScoreUpdate[]; +} + +export interface PartialScoreUpdate { + teamId: string; + field: string; + value: number; + delta: number; +} + +export interface ScoreboardParams { + matchId: string; + round?: number; + includeHistory?: boolean; + includeBreakdown?: boolean; +} + +export interface ScoreHistoryParams { + matchId: string; + teamIds?: string[]; + fromRound?: number; + toRound?: number; + granularity?: 'round' | 'minute' | 'second'; +} + +export interface LeaderboardEntry { + position: number; + teamId: string; + teamName: string; + teamTag: string; + teamAvatar: string | null; + rating: number; + ratingChange: number; + matchesPlayed: number; + matchesWon: number; + winRate: number; + avgScore: number; + country: string | null; +} + +export interface SeasonLeaderboard { + seasonId: string; + seasonName: string; + entries: LeaderboardEntry[]; + lastUpdate: string; +} diff --git a/src/api/types/season.types.ts b/src/api/types/season.types.ts new file mode 100644 index 0000000..7466c7f --- /dev/null +++ b/src/api/types/season.types.ts @@ -0,0 +1,186 @@ +// Season and league types + +import { Team } from './team.types'; + +export interface Season { + id: string; + name: string; + slug: string; + description: string; + league: League; + status: SeasonStatus; + startDate: string; + endDate: string; + registrationStart: string; + registrationEnd: string; + config: SeasonConfig; + matches: SeasonMatch[]; + standings: SeasonStanding[]; + prizes: Prize[]; + rules: string; + isActive: boolean; + createdAt: string; + updatedAt: string; +} + +export type SeasonStatus = + | 'draft' + | 'registration' + | 'active' + | 'playoffs' + | 'finished' + | 'archived'; + +export interface League { + id: string; + name: string; + slug: string; + description: string; + tier: LeagueTier; + icon: string; + color: string; + seasons: Season[]; + currentSeason: Season | null; + requirements: LeagueRequirements; +} + +export type LeagueTier = 'open' | 'amateur' | 'semi_pro' | 'professional' | 'elite'; + +export interface LeagueRequirements { + minRating: number; + minMatches: number; + minTeamSize: number; + maxTeamSize: number; + minTrainingProgress: number; +} + +export interface SeasonConfig { + format: SeasonFormat; + matchesPerWeek: number; + pointsForWin: number; + pointsForDraw: number; + pointsForLoss: number; + tiebreakers: string[]; + playoffTeams: number; + playoffFormat: PlayoffFormat; + rankingAlgorithm: RankingAlgorithm; +} + +export type SeasonFormat = 'round_robin' | 'swiss' | 'elimination' | 'double_elimination' | 'hybrid'; + +export type PlayoffFormat = 'single_elimination' | 'double_elimination' | 'best_of_3' | 'best_of_5'; + +export type RankingAlgorithm = 'elo' | 'glicko2' | 'trueskill' | 'points'; + +export interface SeasonMatch { + id: string; + seasonId: string; + matchId: string; + week: number; + round: string; + homeTeamId: string; + homeTeam: Team; + awayTeamId: string; + awayTeam: Team; + scheduledAt: string; + status: string; + result: SeasonMatchResult | null; +} + +export interface SeasonMatchResult { + homeScore: number; + awayScore: number; + homePoints: number; + awayPoints: number; + winnerId: string | null; + isDraw: boolean; +} + +export interface SeasonStanding { + position: number; + team: Team; + played: number; + won: number; + drawn: number; + lost: number; + pointsFor: number; + pointsAgainst: number; + pointsDiff: number; + seasonPoints: number; + form: string[]; // Last 5 results: W, L, D + streak: number; + streakType: 'win' | 'loss' | 'draw' | null; + qualifiedForPlayoffs: boolean; + eliminated: boolean; + rating: number; + ratingChange: number; +} + +export interface Prize { + position: number; + title: string; + description: string; + type: PrizeType; + value: number | null; + icon: string; +} + +export type PrizeType = 'cash' | 'points' | 'badge' | 'qualification' | 'merchandise' | 'other'; + +export interface SeasonSchedule { + seasonId: string; + weeks: ScheduleWeek[]; + playoffs: PlayoffBracket | null; +} + +export interface ScheduleWeek { + week: number; + startDate: string; + endDate: string; + matches: SeasonMatch[]; + isCurrentWeek: boolean; + isCompleted: boolean; +} + +export interface PlayoffBracket { + rounds: PlayoffRound[]; + final: SeasonMatch | null; + thirdPlace: SeasonMatch | null; +} + +export interface PlayoffRound { + name: string; + matches: SeasonMatch[]; +} + +export interface SeasonRegistration { + id: string; + seasonId: string; + teamId: string; + team: Team; + status: RegistrationStatus; + registeredAt: string; + processedAt: string | null; + processedBy: string | null; + rejectionReason: string | null; +} + +export type RegistrationStatus = 'pending' | 'approved' | 'rejected' | 'waitlisted' | 'withdrawn'; + +export interface RegisterForSeasonRequest { + seasonId: string; + teamId: string; + message?: string; +} + +export interface SeasonListParams { + leagueId?: string; + status?: SeasonStatus; + isActive?: boolean; + year?: number; +} + +export interface LeagueListParams { + tier?: LeagueTier; + search?: string; +} diff --git a/src/api/types/service.types.ts b/src/api/types/service.types.ts new file mode 100644 index 0000000..e288859 --- /dev/null +++ b/src/api/types/service.types.ts @@ -0,0 +1,240 @@ +// Service types + +import { User } from './auth.types'; +import { CheckerResult } from './round.types'; + +export interface Service { + id: string; + name: string; + slug: string; + description: string; + fullDescription: string; + category: ServiceCategory; + difficulty: ServiceDifficulty; + stack: string[]; + tags: string[]; + version: string; + author: User; + maintainers: User[]; + status: ServiceStatus; + rating: ServiceRating; + stats: ServiceStats; + config: ServiceConfig; + vulnerabilities: VulnerabilityInfo[]; + screenshots: string[]; + repository: string | null; + documentation: string | null; + createdAt: string; + updatedAt: string; + publishedAt: string | null; + isPublic: boolean; + isFeatured: boolean; +} + +export type ServiceCategory = + | 'web' + | 'crypto' + | 'pwn' + | 'reverse' + | 'forensics' + | 'network' + | 'misc' + | 'blockchain' + | 'hardware'; + +export type ServiceDifficulty = 'beginner' | 'easy' | 'medium' | 'hard' | 'expert' | 'insane'; + +export type ServiceStatus = + | 'draft' + | 'pending_review' + | 'under_review' + | 'needs_revision' + | 'approved' + | 'published' + | 'deprecated' + | 'archived'; + +export interface ServiceRating { + average: number; + count: number; + distribution: RatingDistribution; +} + +export interface RatingDistribution { + 1: number; + 2: number; + 3: number; + 4: number; + 5: number; +} + +export interface ServiceStats { + timesUsed: number; + uniqueTeams: number; + flagsCaptured: number; + avgExploitTime: number; // seconds + successRate: number; + avgSLA: number; + downloadsCount: number; +} + +export interface ServiceConfig { + port: number; + checkerImage: string; + serviceImage: string; + flagRegex: string; + flagPlacementMethods: string[]; + healthCheckEndpoint: string | null; + requiredResources: ResourceRequirements; + environment: Record; + volumes: VolumeMount[]; + networks: string[]; +} + +export interface ResourceRequirements { + cpu: string; + memory: string; + disk: string; +} + +export interface VolumeMount { + name: string; + mountPath: string; + readOnly: boolean; +} + +export interface VulnerabilityInfo { + id: string; + name: string; + type: VulnerabilityType; + difficulty: ServiceDifficulty; + description: string; + hint: string | null; + points: number; + isRequired: boolean; +} + +export type VulnerabilityType = + | 'sqli' + | 'xss' + | 'rce' + | 'lfi' + | 'rfi' + | 'ssrf' + | 'xxe' + | 'deserialization' + | 'buffer_overflow' + | 'format_string' + | 'race_condition' + | 'logic_flaw' + | 'crypto_weakness' + | 'auth_bypass' + | 'other'; + +export interface ServiceReview { + id: string; + serviceId: string; + userId: string; + user: User; + rating: number; + title: string; + content: string; + pros: string[]; + cons: string[]; + isVerified: boolean; // User actually played with this service + helpfulCount: number; + createdAt: string; + updatedAt: string; +} + +export interface ServiceVersion { + id: string; + serviceId: string; + version: string; + changelog: string; + isLatest: boolean; + isBroken: boolean; + publishedAt: string; + publishedBy: User; +} + +export interface ServiceValidation { + id: string; + serviceId: string; + status: ValidationStatus; + checks: ValidationCheck[]; + startedAt: string; + completedAt: string | null; + logs: string[]; + errorMessage: string | null; +} + +export type ValidationStatus = + | 'queued' + | 'running' + | 'passed' + | 'failed' + | 'cancelled'; + +export interface ValidationCheck { + name: string; + status: 'pending' | 'running' | 'passed' | 'failed' | 'skipped'; + message: string | null; + duration: number | null; +} + +export interface UploadServiceRequest { + name: string; + description: string; + fullDescription?: string; + category: ServiceCategory; + difficulty: ServiceDifficulty; + stack: string[]; + tags?: string[]; + config: Partial; + isPublic?: boolean; + repository?: string; +} + +export interface UpdateServiceRequest { + name?: string; + description?: string; + fullDescription?: string; + category?: ServiceCategory; + difficulty?: ServiceDifficulty; + stack?: string[]; + tags?: string[]; + config?: Partial; + isPublic?: boolean; + repository?: string; +} + +export interface ServiceSearchParams { + query?: string; + category?: ServiceCategory; + difficulty?: ServiceDifficulty; + stack?: string[]; + tags?: string[]; + status?: ServiceStatus; + minRating?: number; + authorId?: string; + isFeatured?: boolean; +} + +export interface CreateReviewRequest { + rating: number; + title: string; + content: string; + pros?: string[]; + cons?: string[]; +} + +export interface ServiceStatusInfo { + serviceId: string; + teamId: string; + status: CheckerResult; + lastCheck: string; + uptime: number; // percentage + consecutiveFailures: number; + message: string | null; +} diff --git a/src/api/types/team.types.ts b/src/api/types/team.types.ts new file mode 100644 index 0000000..5779be5 --- /dev/null +++ b/src/api/types/team.types.ts @@ -0,0 +1,168 @@ +// Team types + +import { User } from './auth.types'; + +export interface Team { + id: string; + name: string; + tag: string; // Short team tag (3-5 chars) + description: string; + avatar: string | null; + banner: string | null; + captain: TeamMember; + members: TeamMember[]; + maxMembers: number; + isPublic: boolean; + isRecruiting: boolean; + stats: TeamStats; + rank: number; + rating: number; + country: string | null; + socialLinks: TeamSocialLinks; + createdAt: string; + updatedAt: string; +} + +export interface TeamMember { + id: string; + userId: string; + user: User; + role: TeamRole; + specialization: TeamSpecialization | null; + joinedAt: string; + contribution: TeamContribution; + isActive: boolean; +} + +export type TeamRole = 'captain' | 'co-captain' | 'member' | 'substitute' | 'coach'; + +export type TeamSpecialization = + | 'attacker' + | 'defender' + | 'analyst' + | 'reverse_engineer' + | 'exploit_developer' + | 'infrastructure' + | 'generalist'; + +export interface TeamContribution { + matchesPlayed: number; + flagsCaptured: number; + flagsDefended: number; + scoreContributed: number; +} + +export interface TeamStats { + matchesPlayed: number; + matchesWon: number; + matchesLost: number; + winRate: number; + totalScore: number; + avgScore: number; + flagsCaptured: number; + flagsDefended: number; + avgSLA: number; + bestRank: number; + currentStreak: number; + longestStreak: number; +} + +export interface TeamSocialLinks { + website?: string; + discord?: string; + twitter?: string; + ctftime?: string; + github?: string; +} + +export interface TeamInvite { + id: string; + teamId: string; + team: Team; + invitedUserId: string; + invitedUser: User; + invitedByUserId: string; + invitedBy: User; + role: TeamRole; + message: string | null; + status: InviteStatus; + createdAt: string; + expiresAt: string; +} + +export type InviteStatus = 'pending' | 'accepted' | 'declined' | 'expired' | 'cancelled'; + +export interface TeamJoinRequest { + id: string; + teamId: string; + team: Team; + userId: string; + user: User; + message: string | null; + status: JoinRequestStatus; + createdAt: string; + processedAt: string | null; + processedBy: User | null; +} + +export type JoinRequestStatus = 'pending' | 'approved' | 'rejected' | 'cancelled'; + +export interface CreateTeamRequest { + name: string; + tag: string; + description?: string; + isPublic?: boolean; + isRecruiting?: boolean; + country?: string; +} + +export interface UpdateTeamRequest { + name?: string; + description?: string; + avatar?: string; + banner?: string; + isPublic?: boolean; + isRecruiting?: boolean; + maxMembers?: number; + socialLinks?: TeamSocialLinks; +} + +export interface InviteToTeamRequest { + userId: string; + role?: TeamRole; + message?: string; +} + +export interface JoinTeamRequest { + teamId: string; + message?: string; +} + +export interface UpdateMemberRoleRequest { + memberId: string; + role: TeamRole; + specialization?: TeamSpecialization; +} + +export interface TeamSearchParams { + query?: string; + isRecruiting?: boolean; + country?: string; + minRating?: number; + maxRating?: number; + minMembers?: number; + maxMembers?: number; +} + +export interface TeamMatchHistory { + matchId: string; + matchTitle: string; + mode: string; + position: number; + totalTeams: number; + score: number; + flagsCaptured: number; + flagsDefended: number; + sla: number; + playedAt: string; +} diff --git a/src/api/types/training.types.ts b/src/api/types/training.types.ts new file mode 100644 index 0000000..173dd7d --- /dev/null +++ b/src/api/types/training.types.ts @@ -0,0 +1,326 @@ +// Training types + +import { User } from './auth.types'; +import { ServiceCategory, ServiceDifficulty } from './service.types'; + +export interface Track { + id: string; + name: string; + slug: string; + description: string; + fullDescription: string; + role: TrackRole; + difficulty: ServiceDifficulty; + icon: string; + color: string; + skills: Skill[]; + exercises: Exercise[]; + totalExercises: number; + estimatedHours: number; + prerequisites: string[]; + learningOutcomes: string[]; + author: User; + rating: TrackRating; + enrolledCount: number; + completedCount: number; + tags: string[]; + isPublished: boolean; + isFeatured: boolean; + createdAt: string; + updatedAt: string; +} + +export type TrackRole = + | 'attacker' + | 'defender' + | 'analyst' + | 'reverse_engineer' + | 'exploit_developer' + | 'infrastructure' + | 'generalist' + | 'team_lead'; + +export interface TrackRating { + average: number; + count: number; +} + +export interface Skill { + id: string; + name: string; + slug: string; + description: string; + category: SkillCategory; + icon: string; + color: string; + maxLevel: number; + relatedSkills: string[]; +} + +export type SkillCategory = + | 'offensive' + | 'defensive' + | 'analysis' + | 'reverse_engineering' + | 'cryptography' + | 'networking' + | 'programming' + | 'infrastructure' + | 'soft_skills'; + +export interface Exercise { + id: string; + trackId: string; + title: string; + slug: string; + description: string; + content: string; // Markdown + type: ExerciseType; + difficulty: ServiceDifficulty; + category: ServiceCategory; + skills: Skill[]; + order: number; + estimatedMinutes: number; + points: number; + hints: Hint[]; + resources: Resource[]; + prerequisites: string[]; + successCriteria: SuccessCriteria; + environment: ExerciseEnvironment | null; + isLocked: boolean; + createdAt: string; + updatedAt: string; +} + +export type ExerciseType = + | 'theory' + | 'quiz' + | 'practical' + | 'challenge' + | 'ctf' + | 'project' + | 'assessment'; + +export interface Hint { + id: string; + order: number; + title: string; + content: string; + costPercentage: number; // Score reduction for using hint +} + +export interface Resource { + id: string; + type: ResourceType; + title: string; + url: string; + description: string; + isRequired: boolean; +} + +export type ResourceType = + | 'article' + | 'video' + | 'documentation' + | 'tool' + | 'writeup' + | 'book' + | 'course'; + +export interface SuccessCriteria { + type: 'flag' | 'quiz' | 'code' | 'manual' | 'auto'; + flagFormat?: string; + quizQuestions?: QuizQuestion[]; + codeTests?: CodeTest[]; + passingScore?: number; +} + +export interface QuizQuestion { + id: string; + question: string; + options: string[]; + correctIndex: number; + explanation: string; +} + +export interface CodeTest { + id: string; + name: string; + input: string; + expectedOutput: string; + timeLimit: number; + memoryLimit: number; +} + +export interface ExerciseEnvironment { + type: 'docker' | 'vm' | 'browser' | 'ssh'; + image: string; + ports: number[]; + credentials?: { + username: string; + password: string; + }; + files: EnvironmentFile[]; + timeLimit: number; // seconds +} + +export interface EnvironmentFile { + name: string; + path: string; + content: string; + isEditable: boolean; +} + +export interface ExerciseResult { + id: string; + exerciseId: string; + userId: string; + status: ExerciseStatus; + score: number; + maxScore: number; + hintsUsed: number; + totalHints: number; + attempts: number; + timeSpent: number; // seconds + startedAt: string; + completedAt: string | null; + submission: ExerciseSubmission | null; +} + +export type ExerciseStatus = + | 'not_started' + | 'in_progress' + | 'completed' + | 'failed' + | 'skipped'; + +export interface ExerciseSubmission { + id: string; + type: 'flag' | 'code' | 'quiz' | 'file'; + content: string; + isCorrect: boolean; + feedback: string; + submittedAt: string; +} + +export interface Progress { + userId: string; + trackId: string | null; + overallProgress: number; + totalPoints: number; + totalExercises: number; + completedExercises: number; + currentStreak: number; + longestStreak: number; + lastActivityAt: string; + trackProgress: TrackProgress[]; + skillLevels: SkillLevel[]; + achievements: string[]; + weeklyActivity: WeeklyActivity[]; +} + +export interface TrackProgress { + trackId: string; + trackName: string; + progress: number; + completedExercises: number; + totalExercises: number; + pointsEarned: number; + maxPoints: number; + startedAt: string; + lastActivityAt: string; + isCompleted: boolean; + certificate: string | null; +} + +export interface SkillLevel { + skillId: string; + skillName: string; + category: SkillCategory; + level: number; + maxLevel: number; + experience: number; + nextLevelExperience: number; +} + +export interface WeeklyActivity { + date: string; + exercisesCompleted: number; + pointsEarned: number; + minutesSpent: number; +} + +export interface StartExerciseRequest { + exerciseId: string; +} + +export interface StartExerciseResponse { + exerciseId: string; + sessionId: string; + environment: ExerciseEnvironment | null; + expiresAt: string; +} + +export interface SubmitExerciseRequest { + exerciseId: string; + type: 'flag' | 'code' | 'quiz' | 'file'; + content: string; +} + +export interface SubmitExerciseResponse { + success: boolean; + isCorrect: boolean; + score: number; + maxScore: number; + feedback: string; + nextExerciseId: string | null; +} + +export interface UseHintRequest { + exerciseId: string; + hintId: string; +} + +export interface UseHintResponse { + hint: Hint; + scorePenalty: number; + remainingHints: number; +} + +export interface TrackSearchParams { + query?: string; + role?: TrackRole; + difficulty?: ServiceDifficulty; + skills?: string[]; + tags?: string[]; + isFeatured?: boolean; + minRating?: number; +} + +export interface SkillMapData { + userId: string; + skills: SkillMapNode[]; + connections: SkillConnection[]; + recommendations: SkillRecommendation[]; +} + +export interface SkillMapNode { + skill: Skill; + level: number; + experience: number; + isUnlocked: boolean; + position: { x: number; y: number }; +} + +export interface SkillConnection { + fromSkillId: string; + toSkillId: string; + isUnlocked: boolean; +} + +export interface SkillRecommendation { + skillId: string; + reason: string; + priority: number; + suggestedExercises: string[]; +} diff --git a/src/api/websocket.ts b/src/api/websocket.ts new file mode 100644 index 0000000..ac266f8 --- /dev/null +++ b/src/api/websocket.ts @@ -0,0 +1,396 @@ +import { io, Socket } from 'socket.io-client'; +import { tokenStorage } from './axios'; +import { + ScoreboardUpdate, + RoundEvent, + LogEntry, + NotificationEvent +} from './types'; + +const WS_URL = import.meta.env.VITE_WS_URL || 'http://localhost:3001'; + +// Channel types +export type ChannelType = + | 'scoreboard' + | 'rounds' + | 'logs' + | 'notifications' + | 'match_status'; + +// Event handlers type +export interface WebSocketEvents { + // Scoreboard events + 'scoreboard:update': (data: ScoreboardUpdate) => void; + 'scoreboard:freeze': (data: { matchId: string; frozenAt: string }) => void; + 'scoreboard:unfreeze': (data: { matchId: string }) => void; + + // Round events + 'round:start': (data: RoundEvent) => void; + 'round:end': (data: RoundEvent) => void; + 'round:event': (data: RoundEvent) => void; + + // Log events + 'log:entry': (data: LogEntry) => void; + 'log:batch': (data: LogEntry[]) => void; + + // Match events + 'match:status': (data: { matchId: string; status: string; message?: string }) => void; + 'match:start': (data: { matchId: string; startedAt: string }) => void; + 'match:end': (data: { matchId: string; finishedAt: string }) => void; + 'match:pause': (data: { matchId: string }) => void; + 'match:resume': (data: { matchId: string }) => void; + + // Service events + 'service:status': (data: { matchId: string; teamId: string; serviceId: string; status: string }) => void; + 'service:check': (data: { matchId: string; teamId: string; serviceId: string; result: string }) => void; + + // Flag events + 'flag:captured': (data: { matchId: string; attackerId: string; defenderId: string; serviceId: string; points: number }) => void; + 'flag:generated': (data: { matchId: string; round: number; count: number }) => void; + + // Notification events + 'notification:new': (data: NotificationEvent) => void; + 'notification:read': (data: { notificationId: string }) => void; + + // Connection events + 'connect': () => void; + 'disconnect': (reason: string) => void; + 'connect_error': (error: Error) => void; + 'reconnect': (attemptNumber: number) => void; + 'reconnect_attempt': (attemptNumber: number) => void; + 'reconnect_error': (error: Error) => void; + 'reconnect_failed': () => void; +} + +type EventName = keyof WebSocketEvents; +type EventHandler = WebSocketEvents[E]; + +class WebSocketClient { + private socket: Socket | null = null; + private subscriptions: Map> = new Map(); + private eventHandlers: Map> = new Map(); + private reconnectAttempts = 0; + private maxReconnectAttempts = 10; + private reconnectDelay = 1000; + private isConnecting = false; + + // Connect to WebSocket server + connect(): Promise { + return new Promise((resolve, reject) => { + if (this.socket?.connected) { + resolve(); + return; + } + + if (this.isConnecting) { + // Wait for existing connection attempt + const checkConnection = setInterval(() => { + if (this.socket?.connected) { + clearInterval(checkConnection); + resolve(); + } + }, 100); + return; + } + + this.isConnecting = true; + const token = tokenStorage.getAccessToken(); + + this.socket = io(WS_URL, { + auth: { token }, + transports: ['websocket', 'polling'], + reconnection: true, + reconnectionAttempts: this.maxReconnectAttempts, + reconnectionDelay: this.reconnectDelay, + reconnectionDelayMax: 5000, + timeout: 20000, + }); + + this.socket.on('connect', () => { + console.log('[WS] Connected'); + this.isConnecting = false; + this.reconnectAttempts = 0; + this.resubscribeAll(); + this.emit('connect'); + resolve(); + }); + + this.socket.on('disconnect', (reason) => { + console.log('[WS] Disconnected:', reason); + this.emit('disconnect', reason); + }); + + this.socket.on('connect_error', (error) => { + console.error('[WS] Connection error:', error); + this.isConnecting = false; + this.emit('connect_error', error); + if (this.reconnectAttempts === 0) { + reject(error); + } + }); + + this.socket.on('reconnect', (attemptNumber) => { + console.log('[WS] Reconnected after', attemptNumber, 'attempts'); + this.emit('reconnect', attemptNumber); + }); + + this.socket.on('reconnect_attempt', (attemptNumber) => { + this.reconnectAttempts = attemptNumber; + this.emit('reconnect_attempt', attemptNumber); + }); + + this.socket.on('reconnect_error', (error) => { + console.error('[WS] Reconnect error:', error); + this.emit('reconnect_error', error); + }); + + this.socket.on('reconnect_failed', () => { + console.error('[WS] Reconnection failed'); + this.emit('reconnect_failed'); + }); + + // Setup all event listeners + this.setupEventListeners(); + }); + } + + // Disconnect from WebSocket server + disconnect(): void { + if (this.socket) { + this.socket.disconnect(); + this.socket = null; + } + this.subscriptions.clear(); + this.isConnecting = false; + } + + // Check if connected + isConnected(): boolean { + return this.socket?.connected ?? false; + } + + // Subscribe to a channel + subscribe(channel: ChannelType, id: string): void { + const channelKey = `${channel}:${id}`; + + if (!this.subscriptions.has(channel)) { + this.subscriptions.set(channel, new Set()); + } + this.subscriptions.get(channel)!.add(id); + + if (this.socket?.connected) { + this.socket.emit('subscribe', { channel, id }); + console.log('[WS] Subscribed to', channelKey); + } + } + + // Unsubscribe from a channel + unsubscribe(channel: ChannelType, id: string): void { + const channelKey = `${channel}:${id}`; + + this.subscriptions.get(channel)?.delete(id); + + if (this.socket?.connected) { + this.socket.emit('unsubscribe', { channel, id }); + console.log('[WS] Unsubscribed from', channelKey); + } + } + + // Resubscribe to all channels after reconnection + private resubscribeAll(): void { + this.subscriptions.forEach((ids, channel) => { + ids.forEach((id) => { + if (this.socket?.connected) { + this.socket.emit('subscribe', { channel, id }); + console.log('[WS] Resubscribed to', `${channel}:${id}`); + } + }); + }); + } + + // Add event handler + on(event: E, handler: EventHandler): void { + if (!this.eventHandlers.has(event)) { + this.eventHandlers.set(event, new Set()); + } + this.eventHandlers.get(event)!.add(handler); + } + + // Remove event handler + off(event: E, handler: EventHandler): void { + this.eventHandlers.get(event)?.delete(handler); + } + + // Emit to handlers + private emit(event: string, ...args: unknown[]): void { + this.eventHandlers.get(event)?.forEach((handler) => { + try { + handler(...args); + } catch (error) { + console.error(`[WS] Error in handler for ${event}:`, error); + } + }); + } + + // Setup all socket event listeners + private setupEventListeners(): void { + if (!this.socket) return; + + // Scoreboard events + this.socket.on('scoreboard:update', (data: ScoreboardUpdate) => { + this.emit('scoreboard:update', data); + }); + + this.socket.on('scoreboard:freeze', (data) => { + this.emit('scoreboard:freeze', data); + }); + + this.socket.on('scoreboard:unfreeze', (data) => { + this.emit('scoreboard:unfreeze', data); + }); + + // Round events + this.socket.on('round:start', (data: RoundEvent) => { + this.emit('round:start', data); + }); + + this.socket.on('round:end', (data: RoundEvent) => { + this.emit('round:end', data); + }); + + this.socket.on('round:event', (data: RoundEvent) => { + this.emit('round:event', data); + }); + + // Log events + this.socket.on('log:entry', (data: LogEntry) => { + this.emit('log:entry', data); + }); + + this.socket.on('log:batch', (data: LogEntry[]) => { + this.emit('log:batch', data); + }); + + // Match events + this.socket.on('match:status', (data) => { + this.emit('match:status', data); + }); + + this.socket.on('match:start', (data) => { + this.emit('match:start', data); + }); + + this.socket.on('match:end', (data) => { + this.emit('match:end', data); + }); + + this.socket.on('match:pause', (data) => { + this.emit('match:pause', data); + }); + + this.socket.on('match:resume', (data) => { + this.emit('match:resume', data); + }); + + // Service events + this.socket.on('service:status', (data) => { + this.emit('service:status', data); + }); + + this.socket.on('service:check', (data) => { + this.emit('service:check', data); + }); + + // Flag events + this.socket.on('flag:captured', (data) => { + this.emit('flag:captured', data); + }); + + this.socket.on('flag:generated', (data) => { + this.emit('flag:generated', data); + }); + + // Notification events + this.socket.on('notification:new', (data: NotificationEvent) => { + this.emit('notification:new', data); + }); + + this.socket.on('notification:read', (data) => { + this.emit('notification:read', data); + }); + } + + // Send message through socket + send(event: string, data: unknown): void { + if (this.socket?.connected) { + this.socket.emit(event, data); + } else { + console.warn('[WS] Cannot send message, not connected'); + } + } + + // Get socket instance (for advanced usage) + getSocket(): Socket | null { + return this.socket; + } +} + +// Export singleton instance +export const wsClient = new WebSocketClient(); + +// Export helper functions for common operations +export const wsHelpers = { + // Subscribe to match scoreboard updates + subscribeToScoreboard: (matchId: string) => { + wsClient.subscribe('scoreboard', matchId); + }, + + unsubscribeFromScoreboard: (matchId: string) => { + wsClient.unsubscribe('scoreboard', matchId); + }, + + // Subscribe to match round events + subscribeToRounds: (matchId: string) => { + wsClient.subscribe('rounds', matchId); + }, + + unsubscribeFromRounds: (matchId: string) => { + wsClient.unsubscribe('rounds', matchId); + }, + + // Subscribe to match logs + subscribeToLogs: (matchId: string) => { + wsClient.subscribe('logs', matchId); + }, + + unsubscribeFromLogs: (matchId: string) => { + wsClient.unsubscribe('logs', matchId); + }, + + // Subscribe to user notifications + subscribeToNotifications: (userId: string) => { + wsClient.subscribe('notifications', userId); + }, + + unsubscribeFromNotifications: (userId: string) => { + wsClient.unsubscribe('notifications', userId); + }, + + // Subscribe to match status changes + subscribeToMatchStatus: (matchId: string) => { + wsClient.subscribe('match_status', matchId); + }, + + unsubscribeFromMatchStatus: (matchId: string) => { + wsClient.unsubscribe('match_status', matchId); + }, + + // Connect and authenticate + connectWithAuth: async () => { + if (tokenStorage.hasTokens()) { + await wsClient.connect(); + } + }, +}; + +export default wsClient; diff --git a/src/components/analytics/AIRecommendationCard.tsx b/src/components/analytics/AIRecommendationCard.tsx new file mode 100644 index 0000000..42984b5 --- /dev/null +++ b/src/components/analytics/AIRecommendationCard.tsx @@ -0,0 +1,119 @@ +import { Lightbulb, TrendingUp, Target, BookOpen } from 'lucide-react'; +import { AIRecommendation } from '../../api/types'; +import { cn } from '../../utils/helpers'; + +interface AIRecommendationCardProps { + recommendation: AIRecommendation; + className?: string; + onDismiss?: () => void; +} + +const priorityColors = { + low: 'bg-blue-500/20 text-blue-400', + medium: 'bg-yellow-500/20 text-yellow-400', + high: 'bg-orange-500/20 text-orange-400', + critical: 'bg-red-500/20 text-red-400', +}; + +const typeIcons: Record> = { + skill_improvement: BookOpen, + strategy_change: TrendingUp, + service_focus: Target, + team_coordination: Lightbulb, + training_suggestion: BookOpen, + role_adjustment: Target, + tool_recommendation: Lightbulb, + resource_allocation: TrendingUp, +}; + +export function AIRecommendationCard({ + recommendation, + className, + onDismiss, +}: AIRecommendationCardProps) { + const Icon = typeIcons[recommendation.type] || Lightbulb; + const priorityColor = priorityColors[recommendation.priority]; + + return ( +
    + {/* Header */} +
    +
    +
    + +
    +
    +

    {recommendation.title}

    +

    + {recommendation.description} +

    +
    +
    + {onDismiss && ( + + )} +
    + + {/* Rationale */} + {recommendation.rationale && ( +

    + {recommendation.rationale} +

    + )} + + {/* Action items */} + {recommendation.actionItems.length > 0 && ( +
    +
    Действия:
    + {recommendation.actionItems.map((item) => ( +
    +
    + {item.action} +
    + ))} +
    + )} + + {/* Footer */} +
    +
    +
    + Влияние:{' '} + {(recommendation.estimatedImpact * 100).toFixed(0)}% +
    +
    + Усилия:{' '} + {(recommendation.estimatedEffort * 100).toFixed(0)}% +
    +
    + Уверенность:{' '} + {(recommendation.confidence * 100).toFixed(0)}% +
    +
    + + {recommendation.priority === 'low' && 'Низкий'} + {recommendation.priority === 'medium' && 'Средний'} + {recommendation.priority === 'high' && 'Высокий'} + {recommendation.priority === 'critical' && 'Критичный'} + +
    +
    + ); +} + +export default AIRecommendationCard; diff --git a/src/components/analytics/AnalyticsCard.tsx b/src/components/analytics/AnalyticsCard.tsx new file mode 100644 index 0000000..a19e2e2 --- /dev/null +++ b/src/components/analytics/AnalyticsCard.tsx @@ -0,0 +1,57 @@ +import { LucideIcon } from 'lucide-react'; +import { cn } from '../../utils/helpers'; + +interface AnalyticsCardProps { + title: string; + value: string | number; + change?: number; + icon?: LucideIcon; + color?: string; + description?: string; + className?: string; +} + +export function AnalyticsCard({ + title, + value, + change, + icon: Icon, + color = 'text-primary', + description, + className, +}: AnalyticsCardProps) { + const isPositive = change && change > 0; + const isNegative = change && change < 0; + + return ( +
    +
    +
    +

    {title}

    +

    {value}

    + {change !== undefined && ( +

    + {isPositive ? '+' : ''}{change.toFixed(1)}% +

    + )} + {description && ( +

    {description}

    + )} +
    + {Icon && ( +
    + +
    + )} +
    +
    + ); +} + +export default AnalyticsCard; diff --git a/src/components/analytics/AttackHeatmap.tsx b/src/components/analytics/AttackHeatmap.tsx new file mode 100644 index 0000000..39db5b8 --- /dev/null +++ b/src/components/analytics/AttackHeatmap.tsx @@ -0,0 +1,97 @@ +import { cn } from '../../utils/helpers'; + +interface HeatmapCell { + round: number; + serviceId: string; + attacks: number; + successRate: number; +} + +interface AttackHeatmapProps { + data: HeatmapCell[]; + services: { id: string; name: string }[]; + rounds: number[]; + className?: string; +} + +export function AttackHeatmap({ + data, + services, + rounds, + className, +}: AttackHeatmapProps) { + const maxValue = Math.max(...data.map((d) => d.attacks), 1); + + const getColor = (attacks: number, successRate: number) => { + const intensity = attacks / maxValue; + const hue = 142 - (intensity * 100); // Green to Red + const saturation = 70 + (successRate * 30); + const lightness = 30 + (intensity * 30); + return `hsl(${hue}, ${saturation}%, ${lightness}%)`; + }; + + const getCellValue = (round: number, serviceId: string) => { + return data.find((d) => d.round === round && d.serviceId === serviceId); + }; + + return ( +
    +
    + {/* Header */} +
    +
    Сервис
    + {rounds.map((round) => ( +
    + R{round} +
    + ))} +
    + + {/* Rows */} + {services.map((service) => ( +
    +
    {service.name}
    + {rounds.map((round) => { + const cell = getCellValue(round, service.id); + return ( +
    + {cell ? cell.attacks : '-'} +
    + ); + })} +
    + ))} +
    + + {/* Legend */} +
    + Меньше +
    + Больше +
    +
    + ); +} + +export default AttackHeatmap; diff --git a/src/components/analytics/PerformanceTrend.tsx b/src/components/analytics/PerformanceTrend.tsx new file mode 100644 index 0000000..184557a --- /dev/null +++ b/src/components/analytics/PerformanceTrend.tsx @@ -0,0 +1,112 @@ +import { useMemo } from 'react'; +import { TrendingUp, TrendingDown, Minus } from 'lucide-react'; +import { cn } from '../../utils/helpers'; + +interface TrendPoint { + timestamp: string; + value: number; + label?: string; +} + +interface PerformanceTrendProps { + data: TrendPoint[]; + title?: string; + value?: number; + change?: number; + height?: number; + color?: string; + className?: string; +} + +export function PerformanceTrend({ + data, + title, + value, + change, + height = 60, + color = 'hsl(var(--primary))', + className, +}: PerformanceTrendProps) { + const trend = useMemo(() => { + if (data.length < 2) return 'stable'; + const first = data[0].value; + const last = data[data.length - 1].value; + if (last > first) return 'up'; + if (last < first) return 'down'; + return 'stable'; + }, [data]); + + const points = useMemo(() => { + if (data.length === 0) return ''; + const maxValue = Math.max(...data.map((d) => d.value)); + const minValue = Math.min(...data.map((d) => d.value)); + const range = maxValue - minValue || 1; + + return data + .map((d, i) => { + const x = (i / (data.length - 1 || 1)) * 100; + const y = 100 - ((d.value - minValue) / range) * 100; + return `${x},${y}`; + }) + .join(' '); + }, [data]); + + const areaPath = points ? `M 0,100 L ${points} L 100,100 Z` : ''; + + return ( +
    + {(title || value !== undefined) && ( +
    + {title && {title}} + {value !== undefined && ( +
    + {value} + {change !== undefined && ( + 0 && 'text-green-400', + change < 0 && 'text-red-400', + change === 0 && 'text-muted-foreground' + )} + > + {change > 0 ? ( + + ) : change < 0 ? ( + + ) : ( + + )} + {change > 0 && '+'} + {change.toFixed(1)}% + + )} +
    + )} +
    + )} + + {/* Sparkline */} +
    + + + + +
    +
    + ); +} + +export default PerformanceTrend; diff --git a/src/components/analytics/SLAChart.tsx b/src/components/analytics/SLAChart.tsx new file mode 100644 index 0000000..e11cc8d --- /dev/null +++ b/src/components/analytics/SLAChart.tsx @@ -0,0 +1,160 @@ +import { useMemo } from 'react'; +import { cn } from '../../utils/helpers'; + +interface SLADataPoint { + round: number; + sla: number; + serviceName?: string; +} + +interface SLAChartProps { + data: SLADataPoint[]; + height?: number; + showGrid?: boolean; + showLabels?: boolean; + className?: string; + targetSLA?: number; +} + +export function SLAChart({ + data, + height = 200, + showGrid = true, + showLabels = true, + className, + targetSLA = 1.0, +}: SLAChartProps) { + const rounds = useMemo(() => data.map((d) => d.round), [data]); + const maxSLA = Math.max(...data.map((d) => d.sla), 1); + + const points = data + .map((d, i) => { + const x = (i / (data.length - 1 || 1)) * 100; + const y = 100 - (d.sla / maxSLA) * 100; + return `${x},${y}`; + }) + .join(' '); + + const areaPath = `M 0,100 L ${points} L 100,100 Z`; + + // Target line + const targetY = 100 - (targetSLA / maxSLA) * 100; + + return ( +
    + {/* Chart */} +
    + + {/* Grid */} + {showGrid && ( + <> + {[0, 25, 50, 75, 100].map((y) => ( + + ))} + + )} + + {/* Target line */} + + + {/* Area */} + + + {/* Line */} + + + {/* Points */} + {data.map((d, i) => { + const x = (i / (data.length - 1 || 1)) * 100; + const y = 100 - (d.sla / maxSLA) * 100; + const isBelowTarget = d.sla < targetSLA; + + return ( + + ); + })} + + + {/* Y-axis labels */} + {showLabels && ( +
    + 100% + 50% + 0% +
    + )} + + {/* X-axis labels */} + {showLabels && ( +
    + {rounds.slice(0, 5).map((round) => ( + R{round} + ))} + {rounds.length > 5 && ...} + {rounds.length > 1 && R{rounds[rounds.length - 1]}} +
    + )} +
    + + {/* Stats */} +
    +
    +
    Средний SLA
    +
    + {((data.reduce((acc, d) => acc + d.sla, 0) / data.length) * 100).toFixed(1)}% +
    +
    +
    +
    Минимум
    +
    + {(Math.min(...data.map((d) => d.sla)) * 100).toFixed(1)}% +
    +
    +
    +
    Максимум
    +
    + {(Math.max(...data.map((d) => d.sla)) * 100).toFixed(1)}% +
    +
    +
    +
    + ); +} + +export default SLAChart; diff --git a/src/components/analytics/TeamComparisonChart.tsx b/src/components/analytics/TeamComparisonChart.tsx new file mode 100644 index 0000000..5a5a600 --- /dev/null +++ b/src/components/analytics/TeamComparisonChart.tsx @@ -0,0 +1,98 @@ +import { useMemo } from 'react'; +import { cn } from '../../utils/helpers'; + +interface ComparisonMetric { + name: string; + ourValue: number; + theirValue: number; + difference: number; + differencePercent: number; + winner: 'us' | 'them' | 'tie'; +} + +interface TeamComparisonChartProps { + metrics: ComparisonMetric[]; + ourTeamName?: string; + theirTeamName?: string; + className?: string; +} + +export function TeamComparisonChart({ + metrics, + ourTeamName = 'Наша команда', + theirTeamName = 'Соперник', + className, +}: TeamComparisonChartProps) { + const maxValue = useMemo(() => { + return Math.max( + ...metrics.map((m) => Math.max(m.ourValue, m.theirValue)) + ); + }, [metrics]); + + return ( +
    + {/* Header */} +
    +
    {ourTeamName}
    +
    Метрика
    +
    {theirTeamName}
    +
    + + {/* Metrics */} + {metrics.map((metric) => { + const ourPercent = (metric.ourValue / maxValue) * 100; + const theirPercent = (metric.theirValue / maxValue) * 100; + + return ( +
    +
    {metric.name}
    +
    + {/* Our bar */} +
    + + {metric.ourValue} + +
    +
    +
    +
    + + {/* Metric name */} +
    + + {metric.differencePercent > 0 && '+'} + {metric.differencePercent.toFixed(1)}% + +
    + + {/* Their bar */} +
    +
    +
    +
    + + {metric.theirValue} + +
    +
    +
    + ); + })} +
    + ); +} + +export default TeamComparisonChart; diff --git a/src/components/analytics/index.ts b/src/components/analytics/index.ts new file mode 100644 index 0000000..897d154 --- /dev/null +++ b/src/components/analytics/index.ts @@ -0,0 +1,7 @@ +// Export all analytics components +export { AnalyticsCard } from './AnalyticsCard'; +export { TeamComparisonChart } from './TeamComparisonChart'; +export { AttackHeatmap } from './AttackHeatmap'; +export { SLAChart } from './SLAChart'; +export { AIRecommendationCard } from './AIRecommendationCard'; +export { PerformanceTrend } from './PerformanceTrend'; diff --git a/src/components/auth/LoginForm.tsx b/src/components/auth/LoginForm.tsx new file mode 100644 index 0000000..68efe99 --- /dev/null +++ b/src/components/auth/LoginForm.tsx @@ -0,0 +1,139 @@ +import { useState } from 'react'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { Eye, EyeOff, Loader2 } from 'lucide-react'; +import { useAuthStore } from '../../store'; +import { loginSchema, LoginFormData } from '../../utils/validators'; +import { cn } from '../../utils/helpers'; + +interface LoginFormProps { + onSuccess?: () => void; + className?: string; +} + +export function LoginForm({ onSuccess, className }: LoginFormProps) { + const { login, isLoading, error, clearError } = useAuthStore(); + const [showPassword, setShowPassword] = useState(false); + + const { + register, + handleSubmit, + formState: { errors }, + } = useForm({ + resolver: zodResolver(loginSchema), + defaultValues: { + email: '', + password: '', + rememberMe: false, + }, + }); + + const onSubmit = async (data: LoginFormData) => { + try { + clearError(); + await login(data); + onSuccess?.(); + } catch { + // Error handled by store + } + }; + + return ( +
    + {/* Error message */} + {error && ( +
    + {error} +
    + )} + + {/* Email */} +
    + + + {errors.email && ( +

    {errors.email.message}

    + )} +
    + + {/* Password */} +
    + +
    + + +
    + {errors.password && ( +

    {errors.password.message}

    + )} +
    + + {/* Remember me */} +
    + + +
    + + {/* Submit */} + +
    + ); +} + +export default LoginForm; diff --git a/src/components/auth/ProtectedRoute.tsx b/src/components/auth/ProtectedRoute.tsx new file mode 100644 index 0000000..d58af9d --- /dev/null +++ b/src/components/auth/ProtectedRoute.tsx @@ -0,0 +1,39 @@ +import { Navigate, useLocation } from 'react-router-dom'; +import { useAuthStore } from '../../store'; +import { LoadingSpinner } from '../common/LoadingSpinner'; + +interface ProtectedRouteProps { + children: React.ReactNode; + requiredRoles?: string[]; +} + +export function ProtectedRoute({ children, requiredRoles }: ProtectedRouteProps) { + const location = useLocation(); + const { isAuthenticated, isInitialized, isLoading, user } = useAuthStore(); + + // Show loading while checking auth + if (!isInitialized || isLoading) { + return ( +
    + +
    + ); + } + + // Redirect to login if not authenticated + if (!isAuthenticated) { + return ; + } + + // Check role requirements + if (requiredRoles && user) { + const hasRequiredRole = requiredRoles.includes(user.role); + if (!hasRequiredRole) { + return ; + } + } + + return <>{children}; +} + +export default ProtectedRoute; diff --git a/src/components/auth/RegisterForm.tsx b/src/components/auth/RegisterForm.tsx new file mode 100644 index 0000000..85e0292 --- /dev/null +++ b/src/components/auth/RegisterForm.tsx @@ -0,0 +1,251 @@ +import { useState } from 'react'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { Eye, EyeOff, Loader2, Check, X } from 'lucide-react'; +import { useAuthStore } from '../../store'; +import { registerSchema, RegisterFormData } from '../../utils/validators'; +import { cn } from '../../utils/helpers'; + +interface RegisterFormProps { + onSuccess?: () => void; + className?: string; +} + +export function RegisterForm({ onSuccess, className }: RegisterFormProps) { + const { register: registerUser, isLoading, error, clearError } = useAuthStore(); + const [showPassword, setShowPassword] = useState(false); + const [showConfirmPassword, setShowConfirmPassword] = useState(false); + + const { + register, + handleSubmit, + watch, + formState: { errors }, + } = useForm({ + resolver: zodResolver(registerSchema), + defaultValues: { + username: '', + email: '', + password: '', + confirmPassword: '', + displayName: '', + acceptTerms: false, + }, + }); + + const password = watch('password'); + + const passwordRequirements = [ + { label: 'Минимум 8 символов', met: password?.length >= 8 }, + { label: 'Заглавная буква', met: /[A-Z]/.test(password || '') }, + { label: 'Строчная буква', met: /[a-z]/.test(password || '') }, + { label: 'Цифра', met: /[0-9]/.test(password || '') }, + { label: 'Специальный символ', met: /[^A-Za-z0-9]/.test(password || '') }, + ]; + + const onSubmit = async (data: RegisterFormData) => { + try { + clearError(); + await registerUser(data); + onSuccess?.(); + } catch { + // Error handled by store + } + }; + + return ( +
    + {/* Error message */} + {error && ( +
    + {error} +
    + )} + + {/* Username */} +
    + + + {errors.username && ( +

    {errors.username.message}

    + )} +
    + + {/* Email */} +
    + + + {errors.email && ( +

    {errors.email.message}

    + )} +
    + + {/* Display Name (optional) */} +
    + + + {errors.displayName && ( +

    {errors.displayName.message}

    + )} +
    + + {/* Password */} +
    + +
    + + +
    + {/* Password requirements */} + {password && ( +
    + {passwordRequirements.map((req) => ( +
    + {req.met ? : } + {req.label} +
    + ))} +
    + )} + {errors.password && ( +

    {errors.password.message}

    + )} +
    + + {/* Confirm Password */} +
    + +
    + + +
    + {errors.confirmPassword && ( +

    {errors.confirmPassword.message}

    + )} +
    + + {/* Accept Terms */} + + {errors.acceptTerms && ( +

    {errors.acceptTerms.message}

    + )} + + {/* Submit */} + +
    + ); +} + +export default RegisterForm; diff --git a/src/components/auth/RoleGuard.tsx b/src/components/auth/RoleGuard.tsx new file mode 100644 index 0000000..2719c54 --- /dev/null +++ b/src/components/auth/RoleGuard.tsx @@ -0,0 +1,79 @@ +import { Navigate } from 'react-router-dom'; +import { useAuthStore } from '../../store'; +import { UserRole } from '../../api/types'; +import { LoadingSpinner } from '../common/LoadingSpinner'; + +interface RoleGuardProps { + children: React.ReactNode; + allowedRoles: UserRole[]; + fallback?: React.ReactNode; + redirectTo?: string; +} + +export function RoleGuard({ + children, + allowedRoles, + fallback, + redirectTo = '/dashboard', +}: RoleGuardProps) { + const { user, isLoading, isInitialized } = useAuthStore(); + + // Show loading while checking auth + if (!isInitialized || isLoading) { + return ( +
    + +
    + ); + } + + // Check if user has required role + if (!user || !allowedRoles.includes(user.role)) { + if (fallback) { + return <>{fallback}; + } + return ; + } + + return <>{children}; +} + +/** + * Admin-only guard + */ +export function AdminGuard({ + children, + fallback, + redirectTo, +}: Omit) { + return ( + + {children} + + ); +} + +/** + * Organizer or admin guard + */ +export function OrganizerGuard({ + children, + fallback, + redirectTo, +}: Omit) { + return ( + + {children} + + ); +} + +export default RoleGuard; diff --git a/src/components/auth/index.ts b/src/components/auth/index.ts new file mode 100644 index 0000000..2946568 --- /dev/null +++ b/src/components/auth/index.ts @@ -0,0 +1,5 @@ +// Export all auth components +export { ProtectedRoute } from './ProtectedRoute'; +export { LoginForm } from './LoginForm'; +export { RegisterForm } from './RegisterForm'; +export { RoleGuard, AdminGuard, OrganizerGuard } from './RoleGuard'; diff --git a/src/components/common/Breadcrumbs.tsx b/src/components/common/Breadcrumbs.tsx new file mode 100644 index 0000000..82b32b2 --- /dev/null +++ b/src/components/common/Breadcrumbs.tsx @@ -0,0 +1,115 @@ +import { Link, useLocation } from 'react-router-dom'; +import { ChevronRight, Home } from 'lucide-react'; +import { cn } from '../../utils/helpers'; + +interface BreadcrumbItem { + label: string; + href?: string; +} + +interface BreadcrumbsProps { + items?: BreadcrumbItem[]; + showHome?: boolean; + className?: string; + separator?: React.ReactNode; +} + +// Default route labels +const routeLabels: Record = { + dashboard: 'Обзор', + matches: 'Матчи', + scoreboard: 'Рейтинг', + services: 'Сервисы', + teams: 'Команды', + training: 'Обучение', + analytics: 'Аналитика', + logs: 'Логи', + profile: 'Профиль', + settings: 'Настройки', + admin: 'Администрирование', + seasons: 'Сезоны', + live: 'Прямой эфир', + lobby: 'Лобби', + results: 'Результаты', + exercises: 'Упражнения', + team: 'Команда', + player: 'Игрок', + users: 'Пользователи', + system: 'Система', +}; + +export function Breadcrumbs({ + items, + showHome = true, + className, + separator, +}: BreadcrumbsProps) { + const location = useLocation(); + + // Generate breadcrumbs from current path if items not provided + const breadcrumbItems: BreadcrumbItem[] = items || (() => { + const pathSegments = location.pathname.split('/').filter(Boolean); + return pathSegments.map((segment, index) => { + const href = '/' + pathSegments.slice(0, index + 1).join('/'); + const label = routeLabels[segment] || segment.charAt(0).toUpperCase() + segment.slice(1); + return { label, href }; + }); + })(); + + const defaultSeparator = ( + + ); + + return ( + + ); +} + +export default Breadcrumbs; diff --git a/src/components/common/ConfirmDialog.tsx b/src/components/common/ConfirmDialog.tsx new file mode 100644 index 0000000..9315095 --- /dev/null +++ b/src/components/common/ConfirmDialog.tsx @@ -0,0 +1,150 @@ +import { useState, useCallback, createContext, useContext, ReactNode } from 'react'; +import { AlertTriangle, X } from 'lucide-react'; +import { cn } from '../../utils/helpers'; + +interface ConfirmDialogProps { + open: boolean; + onClose: () => void; + onConfirm: () => void | Promise; + title: string; + description?: string; + confirmText?: string; + cancelText?: string; + variant?: 'default' | 'destructive'; + loading?: boolean; +} + +export function ConfirmDialog({ + open, + onClose, + onConfirm, + title, + description, + confirmText = 'Подтвердить', + cancelText = 'Отмена', + variant = 'default', + loading = false, +}: ConfirmDialogProps) { + const [isLoading, setIsLoading] = useState(false); + + const handleConfirm = useCallback(async () => { + setIsLoading(true); + try { + await onConfirm(); + onClose(); + } finally { + setIsLoading(false); + } + }, [onConfirm, onClose]); + + if (!open) return null; + + return ( +
    + {/* Backdrop */} +
    + + {/* Dialog */} +
    + {/* Close button */} + + + {/* Icon */} + {variant === 'destructive' && ( +
    + +
    + )} + + {/* Content */} +

    {title}

    + {description && ( +

    {description}

    + )} + + {/* Actions */} +
    + + +
    +
    +
    + ); +} + +// Context for confirm dialogs +interface ConfirmContextType { + confirm: (options: Omit & { + onConfirm: () => void | Promise; + }) => Promise; +} + +const ConfirmContext = createContext(null); + +export function ConfirmProvider({ children }: { children: ReactNode }) { + const [dialogProps, setDialogProps] = useState(null); + + const confirm = useCallback(( + options: Omit & { + onConfirm: () => void | Promise; + } + ): Promise => { + return new Promise((resolve) => { + setDialogProps({ + ...options, + open: true, + onClose: () => { + setDialogProps(null); + resolve(false); + }, + onConfirm: async () => { + await options.onConfirm(); + setDialogProps(null); + resolve(true); + }, + }); + }); + }, []); + + return ( + + {children} + {dialogProps && } + + ); +} + +export function useConfirm() { + const context = useContext(ConfirmContext); + if (!context) { + throw new Error('useConfirm must be used within ConfirmProvider'); + } + return context.confirm; +} + +export default ConfirmDialog; diff --git a/src/components/common/CopyButton.tsx b/src/components/common/CopyButton.tsx new file mode 100644 index 0000000..aae0498 --- /dev/null +++ b/src/components/common/CopyButton.tsx @@ -0,0 +1,85 @@ +import { useState, useCallback } from 'react'; +import { Copy, Check } from 'lucide-react'; +import { cn } from '../../utils/helpers'; +import { copyToClipboard } from '../../utils/helpers'; +import toast from 'react-hot-toast'; + +interface CopyButtonProps { + value: string; + className?: string; + successMessage?: string; + showToast?: boolean; + size?: 'sm' | 'md' | 'lg'; + variant?: 'default' | 'ghost' | 'outline'; + children?: React.ReactNode; +} + +const sizeClasses = { + sm: 'h-7 w-7', + md: 'h-8 w-8', + lg: 'h-10 w-10', +}; + +const iconSizes = { + sm: 'h-3 w-3', + md: 'h-4 w-4', + lg: 'h-5 w-5', +}; + +const variantClasses = { + default: 'bg-primary text-primary-foreground hover:bg-primary/90', + ghost: 'hover:bg-accent hover:text-accent-foreground', + outline: 'border border-border bg-background hover:bg-accent hover:text-accent-foreground', +}; + +export function CopyButton({ + value, + className, + successMessage = 'Скопировано!', + showToast = true, + size = 'md', + variant = 'ghost', + children, +}: CopyButtonProps) { + const [copied, setCopied] = useState(false); + + const handleCopy = useCallback(async () => { + const success = await copyToClipboard(value); + + if (success) { + setCopied(true); + if (showToast) { + toast.success(successMessage); + } + setTimeout(() => setCopied(false), 2000); + } else { + if (showToast) { + toast.error('Не удалось скопировать'); + } + } + }, [value, successMessage, showToast]); + + return ( + + ); +} + +export default CopyButton; diff --git a/src/components/common/Countdown.tsx b/src/components/common/Countdown.tsx new file mode 100644 index 0000000..157258a --- /dev/null +++ b/src/components/common/Countdown.tsx @@ -0,0 +1,133 @@ +import { useMatchTimer } from '../../hooks/useMatchTimer'; +import { cn } from '../../utils/helpers'; + +interface CountdownProps { + /** Duration in seconds */ + duration: number; + /** Auto-start the countdown */ + autoStart?: boolean; + /** Callback when countdown reaches zero */ + onComplete?: () => void; + /** Show hours even if zero */ + showHours?: boolean; + /** Size variant */ + size?: 'sm' | 'md' | 'lg' | 'xl'; + /** Style variant */ + variant?: 'default' | 'danger' | 'warning' | 'success'; + /** Additional className */ + className?: string; + /** Show progress bar */ + showProgress?: boolean; + /** Separator between time parts */ + separator?: string; +} + +const sizeClasses = { + sm: 'text-lg', + md: 'text-2xl', + lg: 'text-4xl', + xl: 'text-6xl', +}; + +const variantClasses = { + default: 'text-foreground', + danger: 'text-destructive', + warning: 'text-yellow-500', + success: 'text-green-500', +}; + +export function Countdown({ + duration, + autoStart = true, + onComplete, + showHours = false, + size = 'md', + variant = 'default', + className, + showProgress = false, + separator = ':', +}: CountdownProps) { + const { remaining, progress, parts, isCompleted } = useMatchTimer({ + duration, + autoStart, + onComplete, + }); + + // Determine variant based on remaining time + const dynamicVariant = (() => { + if (variant !== 'default') return variant; + if (remaining <= 10) return 'danger'; + if (remaining <= 60) return 'warning'; + return 'default'; + })(); + + const formatPart = (value: number) => value.toString().padStart(2, '0'); + + return ( +
    +
    + {(showHours || parts.hours > 0) && ( + <> + {formatPart(parts.hours)} + {separator} + + )} + {formatPart(parts.minutes)} + {separator} + {formatPart(parts.seconds)} +
    + + {showProgress && ( +
    +
    +
    + )} +
    + ); +} + +/** + * Inline countdown for text + */ +export function InlineCountdown({ + duration, + autoStart = true, + className +}: { + duration: number; + autoStart?: boolean; + className?: string; +}) { + const { formatted, isCompleted } = useMatchTimer({ + duration, + autoStart, + }); + + return ( + + {formatted} + + ); +} + +export default Countdown; diff --git a/src/components/common/DataTable.tsx b/src/components/common/DataTable.tsx new file mode 100644 index 0000000..923bff3 --- /dev/null +++ b/src/components/common/DataTable.tsx @@ -0,0 +1,267 @@ +import { useState } from 'react'; +import { ChevronUp, ChevronDown, ChevronsUpDown, ChevronLeft, ChevronRight } from 'lucide-react'; +import { cn } from '../../utils/helpers'; +import { Skeleton } from './LoadingSkeleton'; +import { EmptyState } from './EmptyState'; + +// Simplified column definition +export interface ColumnDef { + id: string; + header: string; + accessorKey?: keyof T; + accessorFn?: (row: T) => unknown; + cell?: (info: { row: T; getValue: () => unknown }) => React.ReactNode; + sortable?: boolean; + width?: string; +} + +interface DataTableProps { + columns: ColumnDef[]; + data: T[]; + isLoading?: boolean; + emptyMessage?: string; + emptyDescription?: string; + pageSize?: number; + showPagination?: boolean; + enableSorting?: boolean; + className?: string; + striped?: boolean; + compact?: boolean; + onRowClick?: (row: T) => void; + getRowId?: (row: T) => string; +} + +type SortDirection = 'asc' | 'desc' | null; + +interface SortState { + columnId: string | null; + direction: SortDirection; +} + +export function DataTable({ + columns, + data, + isLoading = false, + emptyMessage = 'Нет данных', + emptyDescription, + pageSize = 10, + showPagination = true, + enableSorting = true, + className, + striped = false, + compact = false, + onRowClick, + getRowId, +}: DataTableProps) { + const [sort, setSort] = useState({ columnId: null, direction: null }); + const [currentPage, setCurrentPage] = useState(0); + + // Get value from row + const getValue = (row: T, column: ColumnDef): unknown => { + if (column.accessorFn) return column.accessorFn(row); + if (column.accessorKey) return row[column.accessorKey]; + return null; + }; + + // Sort data + const sortedData = [...data].sort((a, b) => { + if (!sort.columnId || !sort.direction) return 0; + + const column = columns.find((c) => c.id === sort.columnId); + if (!column) return 0; + + const aVal = getValue(a, column); + const bVal = getValue(b, column); + + if (aVal === bVal) return 0; + if (aVal === null || aVal === undefined) return 1; + if (bVal === null || bVal === undefined) return -1; + + const comparison = aVal < bVal ? -1 : 1; + return sort.direction === 'asc' ? comparison : -comparison; + }); + + // Paginate data + const totalPages = Math.ceil(sortedData.length / pageSize); + const paginatedData = showPagination + ? sortedData.slice(currentPage * pageSize, (currentPage + 1) * pageSize) + : sortedData; + + // Toggle sort + const toggleSort = (columnId: string) => { + if (!enableSorting) return; + + setSort((prev) => { + if (prev.columnId !== columnId) { + return { columnId, direction: 'asc' }; + } + if (prev.direction === 'asc') { + return { columnId, direction: 'desc' }; + } + return { columnId: null, direction: null }; + }); + }; + + if (isLoading) { + return ( +
    + + + + {columns.map((col) => ( + + ))} + + + + {Array.from({ length: 5 }).map((_, i) => ( + + {columns.map((col) => ( + + ))} + + ))} + +
    + +
    + +
    +
    + ); + } + + if (data.length === 0) { + return ( + + ); + } + + return ( +
    +
    + + + + {columns.map((column) => ( + + ))} + + + + {paginatedData.map((row, index) => ( + onRowClick?.(row)} + > + {columns.map((column) => { + const value = getValue(row, column); + return ( + + ); + })} + + ))} + +
    +
    column.sortable !== false && toggleSort(column.id)} + > + {column.header} + {column.sortable !== false && enableSorting && ( + + {sort.columnId === column.id && sort.direction === 'asc' ? ( + + ) : sort.columnId === column.id && sort.direction === 'desc' ? ( + + ) : ( + + )} + + )} +
    +
    + {column.cell + ? column.cell({ row, getValue: () => value }) + : String(value ?? '')} +
    +
    + + {showPagination && totalPages > 1 && ( +
    +
    + Страница {currentPage + 1} из {totalPages} +
    +
    + + {Array.from({ length: Math.min(5, totalPages) }, (_, i) => { + let pageNum: number; + if (totalPages <= 5) { + pageNum = i; + } else if (currentPage < 3) { + pageNum = i; + } else if (currentPage > totalPages - 4) { + pageNum = totalPages - 5 + i; + } else { + pageNum = currentPage - 2 + i; + } + return ( + + ); + })} + +
    +
    + )} +
    + ); +} + +export default DataTable; diff --git a/src/components/common/EmptyState.tsx b/src/components/common/EmptyState.tsx new file mode 100644 index 0000000..07928d8 --- /dev/null +++ b/src/components/common/EmptyState.tsx @@ -0,0 +1,42 @@ +import { cn } from '../../utils/helpers'; +import { LucideIcon } from 'lucide-react'; + +interface EmptyStateProps { + icon?: LucideIcon; + title: string; + description?: string; + action?: React.ReactNode; + className?: string; +} + +export function EmptyState({ + icon: Icon, + title, + description, + action, + className, +}: EmptyStateProps) { + return ( +
    + {Icon && ( +
    + +
    + )} +

    {title}

    + {description && ( +

    + {description} +

    + )} + {action &&
    {action}
    } +
    + ); +} + +export default EmptyState; diff --git a/src/components/common/ErrorBoundary.tsx b/src/components/common/ErrorBoundary.tsx new file mode 100644 index 0000000..309383a --- /dev/null +++ b/src/components/common/ErrorBoundary.tsx @@ -0,0 +1,69 @@ +import { Component, ErrorInfo, ReactNode } from 'react'; +import { AlertTriangle, RefreshCw } from 'lucide-react'; + +interface Props { + children: ReactNode; + fallback?: ReactNode; + onError?: (error: Error, errorInfo: ErrorInfo) => void; +} + +interface State { + hasError: boolean; + error: Error | null; +} + +export class ErrorBoundary extends Component { + public state: State = { + hasError: false, + error: null, + }; + + public static getDerivedStateFromError(error: Error): State { + return { hasError: true, error }; + } + + public componentDidCatch(error: Error, errorInfo: ErrorInfo) { + console.error('Error caught by boundary:', error, errorInfo); + this.props.onError?.(error, errorInfo); + } + + private handleReset = () => { + this.setState({ hasError: false, error: null }); + }; + + public render() { + if (this.state.hasError) { + if (this.props.fallback) { + return this.props.fallback; + } + + return ( +
    +
    + +
    +

    Что-то пошло не так

    +

    + Произошла непредвиденная ошибка. Попробуйте обновить страницу. +

    + {this.state.error && ( +
    +              {this.state.error.message}
    +            
    + )} + +
    + ); + } + + return this.props.children; + } +} + +export default ErrorBoundary; diff --git a/src/components/common/LoadingSkeleton.tsx b/src/components/common/LoadingSkeleton.tsx new file mode 100644 index 0000000..82dd38e --- /dev/null +++ b/src/components/common/LoadingSkeleton.tsx @@ -0,0 +1,120 @@ +import { cn } from '../../utils/helpers'; + +interface SkeletonProps { + className?: string; +} + +export function Skeleton({ className }: SkeletonProps) { + return ( +
    + ); +} + +// Common skeleton patterns +export function CardSkeleton() { + return ( +
    + + + + +
    + ); +} + +export function TableRowSkeleton({ columns = 5 }: { columns?: number }) { + return ( + + {Array.from({ length: columns }).map((_, i) => ( + + + + ))} + + ); +} + +export function TableSkeleton({ rows = 5, columns = 5 }: { rows?: number; columns?: number }) { + return ( +
    + + + + {Array.from({ length: columns }).map((_, i) => ( + + ))} + + + + {Array.from({ length: rows }).map((_, i) => ( + + ))} + +
    + +
    +
    + ); +} + +export function ListSkeleton({ items = 5 }: { items?: number }) { + return ( +
    + {Array.from({ length: items }).map((_, i) => ( +
    + +
    + + +
    + +
    + ))} +
    + ); +} + +export function ProfileSkeleton() { + return ( +
    +
    + +
    + + +
    + + +
    +
    +
    +
    + + + +
    +
    + ); +} + +export function StatsSkeleton() { + return ( +
    + {Array.from({ length: 4 }).map((_, i) => ( +
    + + + +
    + ))} +
    + ); +} + +export { Skeleton as LoadingSkeleton }; +export default Skeleton; diff --git a/src/components/common/LoadingSpinner.tsx b/src/components/common/LoadingSpinner.tsx new file mode 100644 index 0000000..0f791be --- /dev/null +++ b/src/components/common/LoadingSpinner.tsx @@ -0,0 +1,44 @@ +import { cn } from '../../utils/helpers'; + +interface LoadingSpinnerProps { + size?: 'sm' | 'md' | 'lg' | 'xl'; + className?: string; +} + +const sizeClasses = { + sm: 'h-4 w-4 border-2', + md: 'h-8 w-8 border-2', + lg: 'h-12 w-12 border-3', + xl: 'h-16 w-16 border-4', +}; + +export function LoadingSpinner({ size = 'md', className }: LoadingSpinnerProps) { + return ( +
    + ); +} + +interface LoadingOverlayProps { + message?: string; +} + +export function LoadingOverlay({ message }: LoadingOverlayProps) { + return ( +
    +
    + + {message && ( +

    {message}

    + )} +
    +
    + ); +} + +export default LoadingSpinner; diff --git a/src/components/common/MarkdownRenderer.tsx b/src/components/common/MarkdownRenderer.tsx new file mode 100644 index 0000000..8c9e653 --- /dev/null +++ b/src/components/common/MarkdownRenderer.tsx @@ -0,0 +1,126 @@ +import { useMemo } from 'react'; +import { cn } from '../../utils/helpers'; + +interface MarkdownRendererProps { + content: string; + className?: string; +} + +// Simple markdown parser (for basic formatting) +// For production, consider using react-markdown or similar library +function parseMarkdown(text: string): string { + let html = text; + + // Escape HTML + html = html + .replace(/&/g, '&') + .replace(//g, '>'); + + // Headers + html = html.replace(/^### (.*$)/gim, '

    $1

    '); + html = html.replace(/^## (.*$)/gim, '

    $1

    '); + html = html.replace(/^# (.*$)/gim, '

    $1

    '); + + // Bold and italic + html = html.replace(/\*\*\*(.*?)\*\*\*/g, '$1'); + html = html.replace(/\*\*(.*?)\*\*/g, '$1'); + html = html.replace(/\*(.*?)\*/g, '$1'); + html = html.replace(/___(.*?)___/g, '$1'); + html = html.replace(/__(.*?)__/g, '$1'); + html = html.replace(/_(.*?)_/g, '$1'); + + // Inline code + html = html.replace(/`([^`]+)`/g, '$1'); + + // Code blocks + html = html.replace( + /```(\w+)?\n([\s\S]*?)```/g, + '
    $2
    ' + ); + + // Links + html = html.replace( + /\[([^\]]+)\]\(([^)]+)\)/g, + '$1' + ); + + // Images + html = html.replace( + /!\[([^\]]*)\]\(([^)]+)\)/g, + '$1' + ); + + // Unordered lists + html = html.replace(/^\s*[-*+]\s+(.*)$/gim, '
  1. $1
  2. '); + html = html.replace(/(\n?)+/g, '
      $&
    '); + + // Ordered lists + html = html.replace(/^\s*\d+\.\s+(.*)$/gim, '
  3. $1
  4. '); + + // Blockquotes + html = html.replace( + /^>\s*(.*)$/gim, + '
    $1
    ' + ); + + // Horizontal rules + html = html.replace(/^---+$/gim, '
    '); + html = html.replace(/^\*\*\*+$/gim, '
    '); + + // Line breaks (two spaces at end of line) + html = html.replace(/ $/gim, '
    '); + + // Paragraphs + html = html.replace(/\n\n/g, '

    '); + html = '

    ' + html + '

    '; + + // Clean up empty paragraphs + html = html.replace(/

    \s*<\/p>/g, ''); + html = html.replace(/

    ()<\/p>/g, '$1'); + html = html.replace(/

    ()<\/p>/g, '$1'); + html = html.replace(/

    ()<\/p>/g, '$1'); + html = html.replace(/

    ()<\/p>/g, '$1'); + html = html.replace(/

    ( parseMarkdown(content), [content]); + + return ( +

    + ); +} + +// Simple text renderer (no HTML, just clean text) +export function MarkdownToText({ content }: { content: string }): string { + return content + .replace(/\*\*\*(.*?)\*\*\*/g, '$1') + .replace(/\*\*(.*?)\*\*/g, '$1') + .replace(/\*(.*?)\*/g, '$1') + .replace(/___(.*?)___/g, '$1') + .replace(/__(.*?)__/g, '$1') + .replace(/_(.*?)_/g, '$1') + .replace(/`([^`]+)`/g, '$1') + .replace(/```[\s\S]*?```/g, '') + .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') + .replace(/!\[([^\]]*)\]\([^)]+\)/g, '') + .replace(/^#+\s*/gm, '') + .replace(/^[-*+]\s+/gm, '• ') + .replace(/^\d+\.\s+/gm, '') + .replace(/^>\s*/gm, '') + .replace(/---+/g, '') + .replace(/\*\*\*+/g, '') + .trim(); +} + +export default MarkdownRenderer; diff --git a/src/components/common/SearchInput.tsx b/src/components/common/SearchInput.tsx new file mode 100644 index 0000000..568d546 --- /dev/null +++ b/src/components/common/SearchInput.tsx @@ -0,0 +1,119 @@ +import { useState, useEffect, useCallback } from 'react'; +import { Search, X } from 'lucide-react'; +import { cn } from '../../utils/helpers'; +import { useDebounce } from '../../hooks/useDebounce'; + +interface SearchInputProps { + value?: string; + onChange?: (value: string) => void; + onSearch?: (value: string) => void; + placeholder?: string; + debounceMs?: number; + className?: string; + autoFocus?: boolean; + showClearButton?: boolean; + size?: 'sm' | 'md' | 'lg'; +} + +const sizeClasses = { + sm: 'h-8 text-sm pl-8 pr-8', + md: 'h-10 text-sm pl-10 pr-10', + lg: 'h-12 text-base pl-12 pr-12', +}; + +const iconSizes = { + sm: 'h-4 w-4', + md: 'h-4 w-4', + lg: 'h-5 w-5', +}; + +const iconPositions = { + sm: 'left-2', + md: 'left-3', + lg: 'left-4', +}; + +export function SearchInput({ + value: externalValue, + onChange, + onSearch, + placeholder = 'Поиск...', + debounceMs = 300, + className, + autoFocus = false, + showClearButton = true, + size = 'md', +}: SearchInputProps) { + const [internalValue, setInternalValue] = useState(externalValue || ''); + const debouncedValue = useDebounce(internalValue, debounceMs); + + // Sync with external value + useEffect(() => { + if (externalValue !== undefined) { + setInternalValue(externalValue); + } + }, [externalValue]); + + // Call onSearch when debounced value changes + useEffect(() => { + onSearch?.(debouncedValue); + }, [debouncedValue, onSearch]); + + const handleChange = useCallback((e: React.ChangeEvent) => { + const newValue = e.target.value; + setInternalValue(newValue); + onChange?.(newValue); + }, [onChange]); + + const handleClear = useCallback(() => { + setInternalValue(''); + onChange?.(''); + onSearch?.(''); + }, [onChange, onSearch]); + + const handleKeyDown = useCallback((e: React.KeyboardEvent) => { + if (e.key === 'Escape') { + handleClear(); + } + }, [handleClear]); + + return ( +
    + + + {showClearButton && internalValue && ( + + )} +
    + ); +} + +export default SearchInput; diff --git a/src/components/common/StatusBadge.tsx b/src/components/common/StatusBadge.tsx new file mode 100644 index 0000000..a2849c2 --- /dev/null +++ b/src/components/common/StatusBadge.tsx @@ -0,0 +1,152 @@ +import { cn } from '../../utils/helpers'; +import { + MATCH_STATUS_LABELS, + CHECKER_RESULT_LABELS, + DIFFICULTY_LABELS, +} from '../../utils/constants'; +// Colors are defined inline to avoid unused imports + +type BadgeVariant = 'default' | 'success' | 'warning' | 'error' | 'info' | 'neutral'; + +interface StatusBadgeProps { + status: string; + variant?: BadgeVariant; + type?: 'match' | 'checker' | 'difficulty' | 'custom'; + size?: 'sm' | 'md' | 'lg'; + pulse?: boolean; + className?: string; +} + +const variantClasses: Record = { + default: 'bg-primary/20 text-primary border-primary/30', + success: 'bg-green-500/20 text-green-400 border-green-500/30', + warning: 'bg-yellow-500/20 text-yellow-400 border-yellow-500/30', + error: 'bg-red-500/20 text-red-400 border-red-500/30', + info: 'bg-blue-500/20 text-blue-400 border-blue-500/30', + neutral: 'bg-gray-500/20 text-gray-400 border-gray-500/30', +}; + +const sizeClasses = { + sm: 'px-1.5 py-0.5 text-xs', + md: 'px-2 py-1 text-xs', + lg: 'px-3 py-1.5 text-sm', +}; + +const getVariantFromMatchStatus = (status: string): BadgeVariant => { + switch (status) { + case 'running': + return 'success'; + case 'lobby': + case 'starting': + return 'info'; + case 'paused': + return 'warning'; + case 'finished': + return 'neutral'; + case 'cancelled': + return 'error'; + default: + return 'default'; + } +}; + +const getVariantFromCheckerResult = (result: string): BadgeVariant => { + switch (result) { + case 'ok': + return 'success'; + case 'corrupt': + case 'mumble': + return 'warning'; + case 'down': + case 'error': + case 'timeout': + return 'error'; + default: + return 'neutral'; + } +}; + +const getVariantFromDifficulty = (difficulty: string): BadgeVariant => { + switch (difficulty) { + case 'beginner': + case 'easy': + return 'success'; + case 'medium': + return 'warning'; + case 'hard': + case 'expert': + return 'error'; + case 'insane': + return 'error'; + default: + return 'default'; + } +}; + +export function StatusBadge({ + status, + variant, + type = 'custom', + size = 'md', + pulse = false, + className, +}: StatusBadgeProps) { + let label = status; + let computedVariant = variant || 'default'; + + if (type === 'match') { + label = MATCH_STATUS_LABELS[status] || status; + if (!variant) { + computedVariant = getVariantFromMatchStatus(status); + } + } else if (type === 'checker') { + label = CHECKER_RESULT_LABELS[status] || status; + if (!variant) { + computedVariant = getVariantFromCheckerResult(status); + } + } else if (type === 'difficulty') { + label = DIFFICULTY_LABELS[status] || status; + if (!variant) { + computedVariant = getVariantFromDifficulty(status); + } + } + + return ( + + {pulse && ( + + + + + )} + {label} + + ); +} + +export default StatusBadge; diff --git a/src/components/common/ThemeToggle.tsx b/src/components/common/ThemeToggle.tsx new file mode 100644 index 0000000..c35b39c --- /dev/null +++ b/src/components/common/ThemeToggle.tsx @@ -0,0 +1,43 @@ +import { Moon, Sun, Monitor } from 'lucide-react'; +import { useUIStore } from '../../store'; +import { cn } from '../../utils/helpers'; + +interface ThemeToggleProps { + className?: string; + showLabel?: boolean; +} + +export function ThemeToggle({ className, showLabel = false }: ThemeToggleProps) { + const { theme, setTheme } = useUIStore(); + + const themes = [ + { value: 'light', icon: Sun, label: 'Светлая' }, + { value: 'dark', icon: Moon, label: 'Тёмная' }, + { value: 'system', icon: Monitor, label: 'Системная' }, + ] as const; + + const cycleTheme = () => { + const currentIndex = themes.findIndex((t) => t.value === theme); + const nextIndex = (currentIndex + 1) % themes.length; + setTheme(themes[nextIndex].value); + }; + + const currentTheme = themes.find((t) => t.value === theme)!; + const Icon = currentTheme.icon; + + return ( + + ); +} + +export default ThemeToggle; diff --git a/src/components/common/index.ts b/src/components/common/index.ts new file mode 100644 index 0000000..cd41798 --- /dev/null +++ b/src/components/common/index.ts @@ -0,0 +1,23 @@ +// Export all common components +export { LoadingSpinner, LoadingOverlay } from './LoadingSpinner'; +export { StatusBadge } from './StatusBadge'; +export { ThemeToggle } from './ThemeToggle'; +export { EmptyState } from './EmptyState'; +export { ErrorBoundary } from './ErrorBoundary'; +export { + Skeleton, + LoadingSkeleton, + CardSkeleton, + TableSkeleton, + TableRowSkeleton, + ListSkeleton, + ProfileSkeleton, + StatsSkeleton, +} from './LoadingSkeleton'; +export { SearchInput } from './SearchInput'; +export { CopyButton } from './CopyButton'; +export { ConfirmDialog, ConfirmProvider, useConfirm } from './ConfirmDialog'; +export { Breadcrumbs } from './Breadcrumbs'; +export { Countdown, InlineCountdown } from './Countdown'; +export { DataTable } from './DataTable'; +export { MarkdownRenderer, MarkdownToText } from './MarkdownRenderer'; diff --git a/src/components/layout/AppLayout.tsx b/src/components/layout/AppLayout.tsx new file mode 100644 index 0000000..d8c7d92 --- /dev/null +++ b/src/components/layout/AppLayout.tsx @@ -0,0 +1,59 @@ +import { Outlet } from 'react-router-dom'; +import { Sidebar } from './Sidebar'; +import { Header } from './Header'; +import { useUIStore } from '../../store'; +import { cn } from '../../utils/helpers'; + +export function AppLayout() { + const { sidebarCollapsed, sidebarMobileOpen, setSidebarMobileOpen } = useUIStore(); + + return ( +
    + {/* Mobile overlay */} + {sidebarMobileOpen && ( +
    setSidebarMobileOpen(false)} + /> + )} + + {/* Sidebar */} +
    + +
    + + {/* Main content */} +
    +
    + +
    + +
    + + {/* Footer */} + +
    +
    + ); +} + +export default AppLayout; diff --git a/src/components/layout/Footer.tsx b/src/components/layout/Footer.tsx new file mode 100644 index 0000000..602c205 --- /dev/null +++ b/src/components/layout/Footer.tsx @@ -0,0 +1,175 @@ +import { Link } from 'react-router-dom'; +import { MessageCircle, Mail, Globe } from 'lucide-react'; +import { cn } from '../../utils/helpers'; + +interface FooterProps { + className?: string; + minimal?: boolean; +} + +export function Footer({ className, minimal = false }: FooterProps) { + const currentYear = new Date().getFullYear(); + + if (minimal) { + return ( +
    +
    +

    © {currentYear} Врата ADA. Все права защищены.

    +
    + Документация + Поддержка + + GitHub + +
    +
    +
    + ); + } + + return ( +
    +
    +
    + {/* Brand */} +
    +

    Врата ADA

    +

    + Платформа для тренировок и обучения CTF в формате Attack-Defence. + Развивайте навыки кибербезопасности вместе с нами. +

    + +
    + + {/* Platform */} +
    +

    Платформа

    +
      +
    • + + Матчи + +
    • +
    • + + Обучение + +
    • +
    • + + Сервисы + +
    • +
    • + + Рейтинг + +
    • +
    • + + Сезоны + +
    • +
    +
    + + {/* Resources */} +
    +

    Ресурсы

    +
      +
    • + + Документация + +
    • +
    • + + API + +
    • +
    • + + Блог + +
    • +
    • + + GitHub + +
    • +
    +
    + + {/* Legal */} +
    +

    Правовая информация

    +
      +
    • + + Условия использования + +
    • +
    • + + Политика конфиденциальности + +
    • +
    • + + Правила платформы + +
    • +
    • + + Поддержка + +
    • +
    +
    +
    + +
    +
    +

    © {currentYear} Врата ADA. Все права защищены.

    +

    + Сделано с ❤️ для CTF-сообщества +

    +
    +
    +
    +
    + ); +} + +export default Footer; diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx new file mode 100644 index 0000000..2d839ae --- /dev/null +++ b/src/components/layout/Header.tsx @@ -0,0 +1,110 @@ +import { Link } from 'react-router-dom'; +import { + Bell, + Search, + Menu, + Timer, +} from 'lucide-react'; +import { cn } from '../../utils/helpers'; +import { useUIStore, useAuthStore, useNotificationStore, useMatchStore } from '../../store'; +import { ThemeToggle } from '../common/ThemeToggle'; +import { formatCountdown } from '../../utils/formatters'; + +export function Header() { + const { setSidebarMobileOpen, sidebarCollapsed, toggleCommandPalette } = useUIStore(); + const { user } = useAuthStore(); + const { unreadCount } = useNotificationStore(); + const { currentMatch, roundTimeRemaining } = useMatchStore(); + + return ( +
    + {/* Left side */} +
    + {/* Mobile menu button */} + + + {/* Search */} + +
    + + {/* Center - Match timer */} + {currentMatch && currentMatch.status === 'running' && ( +
    + +
    +

    + Раунд {currentMatch.currentRound}/{currentMatch.totalRounds} +

    +

    + {formatCountdown(roundTimeRemaining)} +

    +
    + + Смотреть + +
    + )} + + {/* Right side */} +
    + {/* Theme toggle */} + + + {/* Notifications */} + + + {unreadCount > 0 && ( + + {unreadCount > 99 ? '99+' : unreadCount} + + )} + + + {/* User menu */} + {user && ( + +
    + {user.displayName?.[0]?.toUpperCase() || user.username[0].toUpperCase()} +
    +
    +

    {user.displayName || user.username}

    +

    + {user.teamId ? 'В команде' : 'Без команды'} +

    +
    + + )} +
    +
    + ); +} + +export default Header; diff --git a/src/components/layout/MobileNav.tsx b/src/components/layout/MobileNav.tsx new file mode 100644 index 0000000..e3de726 --- /dev/null +++ b/src/components/layout/MobileNav.tsx @@ -0,0 +1,109 @@ +import { NavLink, useLocation } from 'react-router-dom'; +import { X, LayoutDashboard, Swords, Trophy, Box, Users, GraduationCap, BarChart3, Settings, Shield } from 'lucide-react'; +import { cn } from '../../utils/helpers'; +import { useUIStore, useAuthStore } from '../../store'; + +interface NavItem { + path: string; + label: string; + icon: React.ComponentType<{ className?: string }>; + adminOnly?: boolean; +} + +const navItems: NavItem[] = [ + { path: '/dashboard', label: 'Обзор', icon: LayoutDashboard }, + { path: '/matches', label: 'Матчи', icon: Swords }, + { path: '/scoreboard', label: 'Рейтинг', icon: Trophy }, + { path: '/services', label: 'Сервисы', icon: Box }, + { path: '/teams', label: 'Команды', icon: Users }, + { path: '/training', label: 'Обучение', icon: GraduationCap }, + { path: '/analytics/team', label: 'Аналитика', icon: BarChart3 }, + { path: '/settings', label: 'Настройки', icon: Settings }, + { path: '/admin', label: 'Админ-панель', icon: Shield, adminOnly: true }, +]; + +export function MobileNav() { + const location = useLocation(); + const { sidebarMobileOpen, setSidebarMobileOpen } = useUIStore(); + const { user } = useAuthStore(); + + const isAdmin = user?.role === 'admin' || user?.role === 'superadmin'; + + if (!sidebarMobileOpen) return null; + + return ( + <> + {/* Backdrop */} +
    setSidebarMobileOpen(false)} + /> + + {/* Drawer */} +
    + {/* Header */} +
    +
    +
    + +
    + Врата ADA +
    + +
    + + {/* Navigation */} + + + {/* User info */} + {user && ( +
    +
    +
    + {user.displayName?.[0]?.toUpperCase() || user.username[0].toUpperCase()} +
    +
    +

    {user.displayName || user.username}

    +

    {user.email}

    +
    +
    +
    + )} +
    + + ); +} + +export default MobileNav; diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx new file mode 100644 index 0000000..cff15ec --- /dev/null +++ b/src/components/layout/Sidebar.tsx @@ -0,0 +1,199 @@ +import { NavLink, useLocation } from 'react-router-dom'; +import { + LayoutDashboard, + Swords, + Trophy, + Box, + Users, + GraduationCap, + BarChart3, + ScrollText, + Settings, + Shield, + ChevronLeft, + ChevronRight, + Calendar, + User, + LogOut, +} from 'lucide-react'; +import { cn } from '../../utils/helpers'; +import { useUIStore, useAuthStore } from '../../store'; + +interface NavItem { + path: string; + label: string; + icon: React.ComponentType<{ className?: string }>; + badge?: number; + adminOnly?: boolean; + organizerOnly?: boolean; +} + +const mainNavItems: NavItem[] = [ + { path: '/dashboard', label: 'Обзор', icon: LayoutDashboard }, + { path: '/matches', label: 'Матчи', icon: Swords }, + { path: '/scoreboard', label: 'Рейтинг', icon: Trophy }, + { path: '/services', label: 'Сервисы', icon: Box }, + { path: '/teams', label: 'Команды', icon: Users }, + { path: '/training', label: 'Обучение', icon: GraduationCap }, + { path: '/seasons', label: 'Сезоны', icon: Calendar }, +]; + +const analyticsNavItems: NavItem[] = [ + { path: '/analytics/team', label: 'Аналитика', icon: BarChart3 }, + { path: '/logs', label: 'Логи', icon: ScrollText }, +]; + +const adminNavItems: NavItem[] = [ + { path: '/admin', label: 'Админ-панель', icon: Shield, adminOnly: true }, +]; + +export function Sidebar() { + const location = useLocation(); + const { sidebarCollapsed, toggleSidebar, setSidebarMobileOpen } = useUIStore(); + const { user, logout } = useAuthStore(); + + const isAdmin = user?.role === 'admin' || user?.role === 'superadmin'; + const isOrganizer = user?.role === 'organizer' || isAdmin; + + const renderNavItem = (item: NavItem) => { + if (item.adminOnly && !isAdmin) return null; + if (item.organizerOnly && !isOrganizer) return null; + + const Icon = item.icon; + const isActive = location.pathname.startsWith(item.path); + + return ( + setSidebarMobileOpen(false)} + className={cn( + 'flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-all', + 'hover:bg-accent hover:text-accent-foreground', + isActive + ? 'bg-primary/10 text-primary' + : 'text-muted-foreground' + )} + > + + {!sidebarCollapsed && ( + <> + {item.label} + {item.badge && item.badge > 0 && ( + + {item.badge} + + )} + + )} + + ); + }; + + return ( + + ); +} + +export default Sidebar; diff --git a/src/components/layout/index.ts b/src/components/layout/index.ts new file mode 100644 index 0000000..eb6937c --- /dev/null +++ b/src/components/layout/index.ts @@ -0,0 +1,6 @@ +// Export all layout components +export { AppLayout } from './AppLayout'; +export { Sidebar } from './Sidebar'; +export { Header } from './Header'; +export { Footer } from './Footer'; +export { MobileNav } from './MobileNav'; diff --git a/src/components/logs/EventTimeline.tsx b/src/components/logs/EventTimeline.tsx new file mode 100644 index 0000000..d9fe86c --- /dev/null +++ b/src/components/logs/EventTimeline.tsx @@ -0,0 +1,75 @@ +import { LogTimelineEvent } from '../../api/types'; +import { cn } from '../../utils/helpers'; +import { formatDateTime } from '../../utils/formatters'; + +interface EventTimelineProps { + events: LogTimelineEvent[]; + className?: string; +} + +const eventColors: Record = { + round_start: 'bg-blue-500', + round_end: 'bg-green-500', + flag_captured: 'bg-red-500', + flag_generated: 'bg-yellow-500', + service_down: 'bg-orange-500', + service_up: 'bg-green-500', + attack_detected: 'bg-purple-500', + score_update: 'bg-blue-500', +}; + +export function EventTimeline({ events, className }: EventTimelineProps) { + return ( +
    + {/* Timeline line */} +
    + + {/* Events */} + {events.map((event, index) => { + const color = eventColors[event.type] || 'bg-muted-foreground'; + + return ( +
    + {/* Dot */} +
    + + {/* Content */} +
    +
    + {event.title} + + {formatDateTime(event.timestamp)} + + {event.importance === 'high' || event.importance === 'critical' && ( + + Важно + + )} +
    +

    {event.description}

    + {event.teams.length > 0 && ( +
    + {event.teams.map((team) => ( + + {team.teamName} + + ))} +
    + )} +
    +
    + ); + })} +
    + ); +} + +export default EventTimeline; diff --git a/src/components/logs/LogEntry.tsx b/src/components/logs/LogEntry.tsx new file mode 100644 index 0000000..132fa11 --- /dev/null +++ b/src/components/logs/LogEntry.tsx @@ -0,0 +1,69 @@ +import { formatDateTime } from '../../utils/formatters'; +import { LOG_LEVEL_COLORS, LOG_LEVEL_LABELS } from '../../utils/constants'; +import { LogEntry as LogEntryType } from '../../api/types'; +import { cn } from '../../utils/helpers'; + +interface LogEntryProps { + log: LogEntryType; + compact?: boolean; + className?: string; +} + +const iconMap: Record = { + match_start: '🚀', + match_end: '🏁', + round_start: '🔄', + round_end: '✅', + flag_captured: '🚩', + flag_generated: '🎯', + service_down: '⚠️', + service_up: '✅', + attack_detected: '⚔️', + score_update: '📊', +}; + +export function LogEntry({ log, compact = false, className }: LogEntryProps) { + const color = LOG_LEVEL_COLORS[log.level]; + const icon = iconMap[log.type] || '📝'; + + return ( +
    + {/* Icon */} + {icon} + + {/* Content */} +
    +
    + + {LOG_LEVEL_LABELS[log.level]} + + + {formatDateTime(log.timestamp)} + + {log.teamName && ( + {log.teamName} + )} +
    +

    {log.message}

    +
    + + {/* Round badge */} + {log.roundNumber && ( + + R{log.roundNumber} + + )} +
    + ); +} + +export default LogEntry; diff --git a/src/components/logs/LogFilter.tsx b/src/components/logs/LogFilter.tsx new file mode 100644 index 0000000..071c81b --- /dev/null +++ b/src/components/logs/LogFilter.tsx @@ -0,0 +1,151 @@ +import { Filter, X } from 'lucide-react'; +import { LogLevel, EventType } from '../../api/types'; +import { cn } from '../../utils/helpers'; +import { LOG_LEVEL_LABELS } from '../../utils/constants'; + +interface LogFilterProps { + selectedLevels?: LogLevel[]; + selectedTypes?: EventType[]; + selectedTeamId?: string | null; + selectedServiceId?: string | null; + search?: string; + onLevelsChange?: (levels: LogLevel[]) => void; + onTypesChange?: (types: EventType[]) => void; + onTeamChange?: (teamId: string | null) => void; + onServiceChange?: (serviceId: string | null) => void; + onSearchChange?: (search: string) => void; + onClear?: () => void; + teams?: { id: string; name: string }[]; + services?: { id: string; name: string }[]; + className?: string; +} + +const logLevels: LogLevel[] = ['debug', 'info', 'warning', 'error', 'critical']; + +export function LogFilter({ + selectedLevels = [], + selectedTypes = [], + selectedTeamId, + selectedServiceId, + search = '', + onLevelsChange, + onTypesChange, + onTeamChange, + onServiceChange, + onSearchChange, + onClear, + teams = [], + services = [], + className, +}: LogFilterProps) { + const hasActiveFilters = + selectedLevels.length > 0 || + selectedTypes.length > 0 || + selectedTeamId || + selectedServiceId || + search; + + return ( +
    + {/* Header */} +
    +
    + + Фильтры логов +
    + {hasActiveFilters && ( + + )} +
    + + {/* Search */} +
    + + onSearchChange?.(e.target.value)} + placeholder="Поиск по тексту..." + className="w-full rounded-lg border border-border bg-background px-4 py-2 text-sm outline-none focus:border-primary focus:ring-1 focus:ring-primary" + /> +
    + + {/* Levels */} +
    + +
    + {logLevels.map((level) => { + const isSelected = selectedLevels.includes(level); + const color = LOG_LEVEL_COLORS[level]; + + return ( + + ); + })} +
    +
    + + {/* Teams */} + {teams.length > 0 && ( +
    + + +
    + )} + + {/* Services */} + {services.length > 0 && ( +
    + + +
    + )} +
    + ); +} + +export default LogFilter; diff --git a/src/components/logs/LogStream.tsx b/src/components/logs/LogStream.tsx new file mode 100644 index 0000000..eeab1fe --- /dev/null +++ b/src/components/logs/LogStream.tsx @@ -0,0 +1,77 @@ +import { useEffect, useRef, useCallback } from 'react'; +import { LogEntry } from '../../api/types'; +import { LogEntry as LogEntryComponent } from './LogEntry'; +import { cn } from '../../utils/helpers'; + +interface LogStreamProps { + logs: LogEntry[]; + isLoading?: boolean; + isLive?: boolean; + autoScroll?: boolean; + className?: string; + maxHeight?: string; +} + +export function LogStream({ + logs, + isLoading = false, + isLive = false, + autoScroll = true, + className, + maxHeight = '500px', +}: LogStreamProps) { + const containerRef = useRef(null); + const prevLogsLength = useRef(logs.length); + + const scrollToBottom = useCallback(() => { + if (containerRef.current) { + containerRef.current.scrollTop = containerRef.current.scrollHeight; + } + }, []); + + useEffect(() => { + if (autoScroll && logs.length > prevLogsLength.current) { + scrollToBottom(); + } + prevLogsLength.current = logs.length; + }, [logs.length, autoScroll, scrollToBottom]); + + return ( +
    + {isLoading && logs.length === 0 ? ( +
    + Загрузка логов... +
    + ) : logs.length === 0 ? ( +
    + Нет логов для отображения +
    + ) : ( +
    + {logs.map((log) => ( + + ))} +
    + )} + + {isLive && ( +
    + + + + + Прямой эфир +
    + )} +
    + ); +} + +export default LogStream; diff --git a/src/components/logs/ReplayControls.tsx b/src/components/logs/ReplayControls.tsx new file mode 100644 index 0000000..32cf488 --- /dev/null +++ b/src/components/logs/ReplayControls.tsx @@ -0,0 +1,123 @@ +import { Play, Pause, SkipBack, SkipForward, FastForward, Rewind } from 'lucide-react'; +import { cn } from '../../utils/helpers'; +import { formatCountdown } from '../../utils/formatters'; + +interface ReplayControlsProps { + isPlaying: boolean; + currentTime: number; + duration: number; + speed: number; + onPlay: () => void; + onPause: () => void; + onSeek: (time: number) => void; + onSpeedChange: (speed: number) => void; + onSkipBack?: () => void; + onSkipForward?: () => void; + className?: string; +} + +const speedOptions = [0.25, 0.5, 1, 1.5, 2, 4, 8]; + +export function ReplayControls({ + isPlaying, + currentTime, + duration, + speed, + onPlay, + onPause, + onSeek, + onSpeedChange, + onSkipBack, + onSkipForward, + className, +}: ReplayControlsProps) { + const progress = duration > 0 ? (currentTime / duration) * 100 : 0; + + const handleSeek = (e: React.ChangeEvent) => { + const newTime = (parseFloat(e.target.value) / 100) * duration; + onSeek(newTime); + }; + + const cycleSpeed = () => { + const currentIndex = speedOptions.indexOf(speed); + const nextIndex = (currentIndex + 1) % speedOptions.length; + onSpeedChange(speedOptions[nextIndex]); + }; + + return ( +
    + {/* Progress bar */} +
    + + {formatCountdown(currentTime)} + + + + {formatCountdown(duration)} + +
    + + {/* Controls */} +
    + {onSkipBack && ( + + )} + + + + {onSkipForward && ( + + )} + +
    +
    + ); +} + +export default ReplayControls; diff --git a/src/components/logs/index.ts b/src/components/logs/index.ts new file mode 100644 index 0000000..02f289e --- /dev/null +++ b/src/components/logs/index.ts @@ -0,0 +1,6 @@ +// Export all logs components +export { LogStream } from './LogStream'; +export { LogEntry } from './LogEntry'; +export { LogFilter } from './LogFilter'; +export { EventTimeline } from './EventTimeline'; +export { ReplayControls } from './ReplayControls'; diff --git a/src/components/match/FlagSubmitForm.tsx b/src/components/match/FlagSubmitForm.tsx new file mode 100644 index 0000000..5e9d3a7 --- /dev/null +++ b/src/components/match/FlagSubmitForm.tsx @@ -0,0 +1,157 @@ +import { useState, useCallback } from 'react'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { z } from 'zod'; +import { Flag, Loader2, Check, X } from 'lucide-react'; +import { cn } from '../../utils/helpers'; +import toast from 'react-hot-toast'; +import { flagsApi } from '../../api/endpoints'; + +const flagSubmitSchema = z.object({ + flag: z.string().min(1, 'Флаг обязателен'), +}); + +type FlagSubmitFormData = z.infer; + +interface FlagSubmitFormProps { + matchId: string; + onSuccess?: (result: any) => void; + className?: string; + disabled?: boolean; +} + +export function FlagSubmitForm({ + matchId, + onSuccess, + className, + disabled = false, +}: FlagSubmitFormProps) { + const [isSubmitting, setIsSubmitting] = useState(false); + const [lastResult, setLastResult] = useState<{ + success: boolean; + message: string; + points?: number; + } | null>(null); + + const { + register, + handleSubmit, + reset, + formState: { errors }, + } = useForm({ + resolver: zodResolver(flagSubmitSchema), + defaultValues: { + flag: '', + }, + }); + + const onSubmit = useCallback( + async (data: FlagSubmitFormData) => { + setIsSubmitting(true); + setLastResult(null); + + try { + const result = await flagsApi.submitFlag({ + matchId, + flag: data.flag, + }); + + setLastResult({ + success: result.success, + message: result.message, + points: result.points, + }); + + if (result.success) { + toast.success(`Флаг принят! +${result.points} очков`); + onSuccess?.(result); + reset(); + } else { + toast.error(result.message); + } + } catch (error) { + setLastResult({ + success: false, + message: error instanceof Error ? error.message : 'Ошибка отправки', + }); + toast.error('Не удалось отправить флаг'); + } finally { + setIsSubmitting(false); + } + }, + [matchId, onSuccess, reset] + ); + + return ( +
    + {/* Flag input */} +
    + +
    + + +
    + {errors.flag && ( +

    {errors.flag.message}

    + )} +
    + + {/* Last result */} + {lastResult && ( +
    + {lastResult.success ? ( + + ) : ( + + )} + {lastResult.message} + {lastResult.points && ( + +{lastResult.points} + )} +
    + )} + + {/* Submit button */} + +
    + ); +} + +export default FlagSubmitForm; diff --git a/src/components/match/MatchCard.tsx b/src/components/match/MatchCard.tsx new file mode 100644 index 0000000..09768f8 --- /dev/null +++ b/src/components/match/MatchCard.tsx @@ -0,0 +1,109 @@ +import { Link } from 'react-router-dom'; +import { Swords, Calendar, Users, Clock } from 'lucide-react'; +import { Match } from '../../api/types'; +import { cn } from '../../utils/helpers'; +import { formatDateTime, formatRelativeTime } from '../../utils/formatters'; +import { StatusBadge } from '../common/StatusBadge'; + +interface MatchCardProps { + match: Match; + className?: string; + showDescription?: boolean; +} + +const modeLabels: Record = { + training: 'Тренировка', + tournament: 'Турнир', + scrim: 'Скрим', + practice: 'Практика', +}; + +const modeColors: Record = { + training: 'text-blue-400', + tournament: 'text-yellow-400', + scrim: 'text-purple-400', + practice: 'text-green-400', +}; + +export function MatchCard({ match, className, showDescription = true }: MatchCardProps) { + const isRunning = match.status === 'running'; + const isStarting = match.status === 'starting' || match.status === 'lobby'; + + return ( + + {/* Header */} +
    +
    +
    + + {modeLabels[match.mode]} + + {match.isRanked && ( + + Рейтинг + + )} +
    +

    + {match.title} +

    +
    + +
    + + {/* Description */} + {showDescription && match.description && ( +

    + {match.description} +

    + )} + + {/* Info grid */} +
    +
    + + {match.config.maxTeams} команд +
    +
    + + {match.totalRounds} раундов +
    +
    + + {Math.floor(match.roundDuration / 60)} мин раунд +
    +
    + + {match.scheduledAt ? formatDateTime(match.scheduledAt) : 'TBD'} +
    +
    + + {/* Footer */} +
    +
    + {match.startedAt ? ( + <>Начался {formatRelativeTime(match.startedAt)} + ) : match.scheduledAt ? ( + <>Начнётся {formatRelativeTime(match.scheduledAt)} + ) : ( + <>Создан {formatRelativeTime(match.createdAt)} + )} +
    + {(isRunning || isStarting) && ( +
    + {isRunning ? 'Смотреть' : 'Войти'} + +
    + )} +
    + + ); +} + +export default MatchCard; diff --git a/src/components/match/MatchConfigForm.tsx b/src/components/match/MatchConfigForm.tsx new file mode 100644 index 0000000..59845d7 --- /dev/null +++ b/src/components/match/MatchConfigForm.tsx @@ -0,0 +1,329 @@ +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { z } from 'zod'; +import { Plus, Loader2 } from 'lucide-react'; +import { cn } from '../../utils/helpers'; +import { Service } from '../../api/types'; + +const matchConfigSchema = z.object({ + title: z.string().min(3, 'Минимум 3 символа').max(100, 'Максимум 100 символов'), + description: z.string().max(1000, 'Максимум 1000 символов').optional(), + mode: z.enum(['training', 'tournament', 'scrim', 'practice']), + maxTeams: z.number().int().min(2).max(100), + minTeams: z.number().int().min(2), + roundDuration: z.number().int().min(30).max(3600), + totalRounds: z.number().int().min(1).max(500), + flagLifetime: z.number().int().min(10).max(600), + scoringPolicy: z.enum(['classic', 'linear', 'exponential', 'dynamic']), + serviceIds: z.array(z.string()).min(1, 'Выберите хотя бы один сервис'), + scheduledAt: z.string().optional(), + visibility: z.enum(['public', 'private', 'unlisted']), + isRanked: z.boolean(), +}); + +type MatchConfigFormData = z.infer; + +interface MatchConfigFormProps { + services: Service[]; + onSubmit: (data: MatchConfigFormData) => Promise; + isLoading?: boolean; + className?: string; +} + +const modeLabels: Record = { + training: 'Тренировка', + tournament: 'Турнир', + scrim: 'Скрим', + practice: 'Практика', +}; + +const scoringLabels: Record = { + classic: 'Классическое', + linear: 'Линейное', + exponential: 'Экспоненциальное', + dynamic: 'Динамическое', +}; + +const visibilityLabels: Record = { + public: 'Публичный', + private: 'Приватный', + unlisted: 'Скрытый', +}; + +export function MatchConfigForm({ + services, + onSubmit, + isLoading = false, + className, +}: MatchConfigFormProps) { + const { + register, + handleSubmit, + watch, + setValue, + formState: { errors }, + } = useForm({ + resolver: zodResolver(matchConfigSchema), + defaultValues: { + title: '', + description: '', + mode: 'training', + maxTeams: 10, + minTeams: 2, + roundDuration: 300, + totalRounds: 20, + flagLifetime: 120, + scoringPolicy: 'classic', + serviceIds: [], + visibility: 'public', + isRanked: false, + }, + }); + + const selectedServiceIds = watch('serviceIds'); + + const toggleService = (serviceId: string) => { + const current = selectedServiceIds || []; + if (current.includes(serviceId)) { + setValue( + 'serviceIds', + current.filter((id) => id !== serviceId) + ); + } else { + setValue('serviceIds', [...current, serviceId]); + } + }; + + const handleSubmitForm = async (data: any) => { + try { + await onSubmit(data as MatchConfigFormData); + } catch (error) { + // Error handled by parent + } + }; + + return ( +
    + {/* Basic info */} +
    +
    + + + {errors.title && ( +

    {errors.title.message}

    + )} +
    + +
    + +