From 3b3971e65391f46d9445b73d4a38b053b1260999 Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Wed, 7 Jan 2026 14:16:02 +0100 Subject: [PATCH] streamline components --- E2E_TESTING_IMPROVEMENTS.md | 253 ------------------ E2E_TEST_DEBUGGING_SUMMARY.md | 144 ---------- MIDDLEWARE_FIX_SUMMARY.md | 48 ---- apps/website/app/races/[id]/results/page.tsx | 2 +- .../leagues/LeagueScoringSection.tsx | 56 ++-- .../components/races/FileProtestModal.tsx | 100 +++---- .../components/races/ImportResultsForm.tsx | 116 +------- .../components/shared/CapabilityGate.tsx | 28 +- .../services/leagues/LeagueSettingsService.ts | 178 ++++++++++++ .../lib/services/policy/PolicyService.ts | 65 +++++ .../lib/services/protests/ProtestService.ts | 59 ++++ .../lib/services/races/RaceResultsService.ts | 127 +++++++++ .../LeagueScoringSectionViewModel.ts | 125 +++++++++ .../RaceResultsDataTransformer.ts | 0 .../ScoringConfigurationViewModel.ts | 50 ++++ apps/website/lib/view-models/index.ts | 1 + 16 files changed, 685 insertions(+), 667 deletions(-) delete mode 100644 E2E_TESTING_IMPROVEMENTS.md delete mode 100644 E2E_TEST_DEBUGGING_SUMMARY.md delete mode 100644 MIDDLEWARE_FIX_SUMMARY.md create mode 100644 apps/website/lib/view-models/LeagueScoringSectionViewModel.ts rename apps/website/lib/{transformers => view-models}/RaceResultsDataTransformer.ts (100%) create mode 100644 apps/website/lib/view-models/ScoringConfigurationViewModel.ts diff --git a/E2E_TESTING_IMPROVEMENTS.md b/E2E_TESTING_IMPROVEMENTS.md deleted file mode 100644 index 9e354f4fd..000000000 --- a/E2E_TESTING_IMPROVEMENTS.md +++ /dev/null @@ -1,253 +0,0 @@ -# E2E Test Environment Improvements - -## Problem Summary - -Your original e2e test environment had several critical issues: - -1. **Hybrid Architecture**: Website ran locally via Playwright's `webServer` while API/DB ran in Docker -2. **SWC Compilation Issues**: Next.js SWC had problems in Docker containers -3. **CI Incompatibility**: The hybrid approach wouldn't work reliably in CI environments -4. **Complex Setup**: Multiple scripts and port configurations needed -5. **Port Conflicts**: Multiple services competing for ports (3000, 3001, 3101, 5432, 5433) - -## Root Cause Analysis - -### SWC Compilation Issues in Docker -The SWC (Speedy Web Compiler) issues were caused by: -- Missing native build tools (Python, make, g++) -- File system performance issues with volume mounts -- Insufficient memory/CPU allocation -- Missing dependencies in Alpine Linux - -### CI Incompatibility -The hybrid approach failed in CI because: -- CI environments don't have local Node.js processes -- Port management becomes complex -- Environment consistency is harder to maintain -- Debugging is more difficult - -## Solution: Unified Docker Architecture - -### Architecture Overview - -``` -┌─────────────────────────────────────────────────────────────┐ -│ Docker Network: gridpilot-e2e-network │ -│ │ -│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ -│ │ Playwright │───▶│ Website │───▶│ API │ │ -│ │ Runner │ │ (Next.js) │ │ (NestJS) │ │ -│ │ │ │ Port: 3000 │ │ Port: 3000 │ │ -│ └──────────────┘ └──────────────┘ └──────────────┘ │ -│ │ │ │ │ -│ └───────────────────┴───────────────────┴───────────┘ -│ │ -│ ┌───────▼────────┐ -│ │ PostgreSQL │ -│ │ Port: 5432 │ -│ └────────────────┘ -└─────────────────────────────────────────────────────────────┘ -``` - -### Key Components - -#### 1. Optimized Next.js Dockerfile (`apps/website/Dockerfile.e2e`) -```dockerfile -# Includes all SWC build dependencies -RUN apk add --no-cache python3 make g++ git curl - -# Multi-stage build for optimization -# Production stage only includes runtime dependencies -``` - -#### 2. Unified Docker Compose (`docker-compose.e2e.yml`) -- **Database**: PostgreSQL on port 5434 -- **API**: NestJS on port 3101 -- **Website**: Next.js on port 3100 -- **Playwright**: Test runner in container -- All services on isolated network - -#### 3. Updated Playwright Config -- No `webServer` - everything runs in Docker -- Base URL: `http://website:3000` (container network) -- No local dependencies - -#### 4. Simplified Package.json Scripts -```bash -# Single command for complete e2e testing -npm run test:e2e:website - -# Individual control commands -npm run docker:e2e:up -npm run docker:e2e:down -npm run docker:e2e:logs -npm run docker:e2e:clean -``` - -## Benefits - -### ✅ Reliability -- **No SWC issues**: Optimized Dockerfile with all build tools -- **No port conflicts**: Isolated network and unique ports -- **No local dependencies**: Everything in containers - -### ✅ CI Compatibility -- **Identical environments**: Local and CI run the same setup -- **Single command**: Easy to integrate in CI pipelines -- **Deterministic**: No "works on my machine" issues - -### ✅ Developer Experience -- **Simplicity**: One command vs multiple steps -- **Debugging**: Easy log access and service management -- **Speed**: No local server startup overhead - -### ✅ Maintainability -- **Single source of truth**: One docker-compose file -- **Clear documentation**: Updated README with migration guide -- **Future-proof**: Easy to extend and modify - -## Migration Guide - -### From Legacy to Unified - -1. **Stop existing services**: - ```bash - npm run docker:test:down - npm run docker:dev:down - ``` - -2. **Clean up**: - ```bash - npm run docker:test:clean - ``` - -3. **Use new approach**: - ```bash - npm run test:e2e:website - ``` - -### What Changes - -| Before | After | -|--------|-------| -| `npm run test:docker:website` | `npm run test:e2e:website` | -| Website: Local (3000) | Website: Docker (3100) | -| API: Docker (3101) | API: Docker (3101) | -| DB: Docker (5433) | DB: Docker (5434) | -| Playwright: Local | Playwright: Docker | -| 5+ commands | 1 command | - -## Testing the New Setup - -### Quick Test -```bash -# Run complete e2e test suite -npm run test:e2e:website -``` - -### Manual Verification -```bash -# Start services -npm run docker:e2e:up - -# Check status -npm run docker:e2e:ps - -# View logs -npm run docker:e2e:logs - -# Test website manually -curl http://localhost:3100 - -# Test API manually -curl http://localhost:3101/health - -# Stop services -npm run docker:e2e:down -``` - -## Files Created/Modified - -### New Files -- `apps/website/Dockerfile.e2e` - Optimized Next.js image -- `docker-compose.e2e.yml` - Unified test environment -- `E2E_TESTING_IMPROVEMENTS.md` - This document - -### Modified Files -- `playwright.website.config.ts` - Containerized setup -- `package.json` - New scripts -- `README.docker.md` - Updated documentation - -## Troubleshooting - -### Common Issues - -**Issue**: "Website not building" -- **Solution**: Ensure Docker has 4GB+ memory - -**Issue**: "Port already in use" -- **Solution**: `npm run docker:e2e:clean && npm run docker:e2e:up` - -**Issue**: "Module not found" -- **Solution**: `npm run docker:e2e:clean` to rebuild - -**Issue**: "Playwright timeout" -- **Solution**: Increase timeout in `playwright.website.config.ts` - -### Debug Commands -```bash -# View all logs -npm run docker:e2e:logs - -# Check specific service -docker-compose -f docker-compose.e2e.yml logs -f website - -# Shell into container -docker-compose -f docker-compose.e2e.yml exec website sh - -# Rebuild everything -npm run docker:e2e:clean && npm run docker:e2e:up -``` - -## Performance Comparison - -### Before (Legacy Hybrid) -- **Startup time**: ~45-60 seconds -- **Reliability**: ~70% (SWC issues) -- **CI compatibility**: ❌ No -- **Commands needed**: 5+ - -### After (Unified Docker) -- **Startup time**: ~30-45 seconds -- **Reliability**: ~95%+ -- **CI compatibility**: ✅ Yes -- **Commands needed**: 1 - -## Future Enhancements - -### Potential Improvements -1. **Test Parallelization**: Run multiple test suites simultaneously -2. **Database Seeding**: Pre-seeded test data -3. **API Mocking**: Optional mock mode for faster tests -4. **Visual Testing**: Screenshot comparison tests -5. **Performance Testing**: Load testing integration - -### CI Integration Example -```yaml -# .github/workflows/e2e.yml -name: E2E Tests -on: [push, pull_request] -jobs: - test: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - name: Run E2E Tests - run: npm run test:e2e:website -``` - -## Conclusion - -This improvement transforms your e2e testing from a fragile, complex setup into a robust, CI-ready solution. The unified Docker approach eliminates SWC issues, simplifies the workflow, and ensures consistent behavior across all environments. - -**Key Takeaway**: One command, one environment, zero headaches. \ No newline at end of file diff --git a/E2E_TEST_DEBUGGING_SUMMARY.md b/E2E_TEST_DEBUGGING_SUMMARY.md deleted file mode 100644 index b57385495..000000000 --- a/E2E_TEST_DEBUGGING_SUMMARY.md +++ /dev/null @@ -1,144 +0,0 @@ -# E2E Test Debugging Summary - -## Problem -6 out of 12 e2e tests failing due to authentication/cookie issues. - -## Root Cause Analysis - -### Key Finding: Cookies Not Being Sent to Middleware - -Through comprehensive debug logging, we discovered that: - -1. **Cookies ARE being set correctly in Playwright**: - ``` - [WebsiteAuthManager] Cookies after setting: [ - { - name: 'gp_session', - value: 'gp_bdecd1f8-6783-4946-8d2c-c817b0adfc71', - domain: 'website', - path: '/', - httpOnly: true, - sameSite: 'Lax' - } - ] - ``` - -2. **Cookies were NOT being received by the middleware** (INITIAL ISSUE): - ``` - [MIDDLEWARE] Request details | { - "pathname":"/sponsor/dashboard", - "cookieHeaderLength":0, // <-- NO COOKIES! - "cookiePreview":"" - } - ``` - -### The Issue - -The problem was with how Playwright handles cookies when using `context.addCookies()` after context creation. - -Playwright requires cookies to be set via the `storageState` option during context creation for reliable cookie handling in Docker environments. - -### Solution Applied - -✅ **Fixed cookie setting in WebsiteAuthManager**: -- Changed from `context.addCookies()` after context creation -- To using `storageState` option during `browser.newContext()` -- This ensures cookies are properly associated with the context from the start - -```typescript -const contextWithCookies = await browser.newContext({ - baseURL, - storageState: { - cookies: [{ - name: 'gp_session', - value: token, - domain: new URL(baseURL).hostname, - path: '/', - expires: -1, - httpOnly: true, - secure: false, - sameSite: 'Lax', - }], - origins: [], - }, -}); -``` - -### Verification - -After the fix, cookies ARE now being sent: -``` -[MIDDLEWARE] Request details | { - "cookieHeaderLength":50, - "cookiePreview":"gp_session=gp_21c3a2d0-2810-44ba-8a4c-0a3bd359ff3d" -} -``` - -### Other Fixes Applied - -1. ✅ **Added comprehensive debug logging** - Full transparency into auth flow -2. ✅ **Fixed ConsoleLogger to log in all environments** - Logs now visible in production/test -3. ✅ **Fixed cookie setting mechanism** - Using storageState instead of addCookies - -## Next Steps - -The real solution is likely one of: - -1. **Use `localhost` instead of `website` hostname** in Docker - - Change PLAYWRIGHT_BASE_URL to use localhost with port mapping - - This would make cookie handling more standard - -2. **Navigate to a page before setting cookies** - - Playwright needs an active page context to set cookies properly - - Set cookies after `page.goto()` instead of on the context - -3. **Use Playwright's storage state feature** - - Save authenticated state and reuse it - - More reliable than manual cookie management - -4. **Set cookies without domain parameter** - - Let Playwright infer the domain from the current page - -## Debug Logs Added - -We added permanent debug logs to: -- [`apps/website/middleware.ts`](apps/website/middleware.ts) - Request/session/auth flow logging -- [`apps/website/lib/auth/AuthFlowRouter.ts`](apps/website/lib/auth/AuthFlowRouter.ts) - Auth decision logging -- [`apps/website/lib/routing/RouteConfig.ts`](apps/website/lib/routing/RouteConfig.ts) - Route classification logging -- [`apps/website/lib/gateways/SessionGateway.ts`](apps/website/lib/gateways/SessionGateway.ts) - Session fetching logging -- [`apps/website/lib/infrastructure/logging/ConsoleLogger.ts`](apps/website/lib/infrastructure/logging/ConsoleLogger.ts) - Now logs in all environments -- [`tests/shared/website/WebsiteAuthManager.ts`](tests/shared/website/WebsiteAuthManager.ts) - Cookie setting verification - -These logs provide full transparency into the authentication flow and will help with future debugging. - -## Test Results - -Current status: **6 failed, 6 passed** (improved from initial 6 failures) - -Failing tests: -1. public routes are accessible without authentication -2. admin routes require admin role -3. sponsor routes require sponsor role -4. parameterized routes handle edge cases -5. no console or page errors on critical routes -6. TypeORM session persistence across routes - -### Analysis of Remaining Failures - -The cookie issue is **FIXED** - cookies are now being sent correctly. The remaining 6 failures are due to other issues: - -1. **Test expectations may be incorrect** - Some tests expect specific behavior that doesn't match the actual implementation -2. **Route configuration issues** - Some routes may not be properly classified as public/protected -3. **Test data issues** - Edge case tests may be using invalid data -4. **Console error expectations** - The "no console errors" test may be too strict - -The middleware and authentication flow are working correctly. The remaining failures need individual investigation and are NOT related to the cookie/session issue that was the focus of this debugging session. - -## Conclusion - -✅ **Cookie issue RESOLVED** - Cookies are now being sent from Playwright to middleware -✅ **Debug logging in place** - Full transparency for future debugging -✅ **ConsoleLogger fixed** - Logs in all environments -✅ **Documentation complete** - This summary provides full context - -The remaining test failures are separate issues that need individual attention, but the core authentication/cookie mechanism is now working correctly. diff --git a/MIDDLEWARE_FIX_SUMMARY.md b/MIDDLEWARE_FIX_SUMMARY.md deleted file mode 100644 index 1dc12dfba..000000000 --- a/MIDDLEWARE_FIX_SUMMARY.md +++ /dev/null @@ -1,48 +0,0 @@ -# Middleware Authentication Fix Summary - -## Problem -6 out of 12 e2e tests failing due to middleware not properly protecting routes. - -## Root Cause Analysis - -### Issue 1: Cookie Loss in Redirect Chain -When navigating to `/sponsor`, the page component does a server-side `redirect('/sponsor/dashboard')` which loses cookies in the redirect chain. This causes the second request to `/sponsor/dashboard` to have no cookies. - -**Evidence:** -``` -/sponsor - cookie header length: 50 ✓ -/sponsor/dashboard - cookie header length: 0 ✗ -``` - -**Fix:** Handle `/sponsor` → `/sponsor/dashboard` redirect in middleware to preserve cookies. - -### Issue 2: Auth Page Redirect Loop -When an authenticated user with insufficient permissions is redirected to `/auth/login?returnTo=/sponsor/dashboard`, the middleware immediately redirects them away from the login page because they're authenticated. This creates a conflict. - -**Fix:** Allow authenticated users to access login pages if they have a `returnTo` parameter (indicating they were sent there due to insufficient permissions). - -### Issue 3: SessionGateway Cookie Handling -The `SessionGateway.getSession()` method was checking `if (cookieHeader)` which evaluates to `false` for empty strings, causing it to fall through to server component context even when called from middleware with an empty cookie header. - -**Fix:** Check `if (cookieHeader !== undefined)` instead. - -## Changes Made - -1. **apps/website/lib/gateways/SessionGateway.ts** - - Fixed cookie header check to use `!== undefined` instead of truthy check - -2. **apps/website/middleware.ts** - - Added redirect from `/sponsor` to `/sponsor/dashboard` in middleware - - Added check for `returnTo` parameter in auth page logic - - Added comprehensive logging - -3. **apps/website/app/sponsor/dashboard/page.tsx** - - Added `export const dynamic = 'force-dynamic'` (reverted - doesn't work with client components) - -## Test Results -Still failing - need to investigate further. - -## Next Steps -1. Check if cookies are being set with correct domain -2. Verify Playwright cookie handling in Docker environment -3. Consider if the test expectations are correct diff --git a/apps/website/app/races/[id]/results/page.tsx b/apps/website/app/races/[id]/results/page.tsx index 3046e02a1..2c64cf9e4 100644 --- a/apps/website/app/races/[id]/results/page.tsx +++ b/apps/website/app/races/[id]/results/page.tsx @@ -3,7 +3,7 @@ import { StatefulPageWrapper } from '@/components/shared/state/StatefulPageWrapper'; import { RaceResultsTemplate } from '@/templates/RaceResultsTemplate'; import { useRaceResultsPageData } from '@/hooks/race/useRaceResultsPageData'; -import { RaceResultsDataTransformer } from '@/lib/transformers/RaceResultsDataTransformer'; +import { RaceResultsDataTransformer } from '@/lib/view-models/RaceResultsDataTransformer'; import { useLeagueMemberships } from '@/hooks/league/useLeagueMemberships'; import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId'; import { useState } from 'react'; diff --git a/apps/website/components/leagues/LeagueScoringSection.tsx b/apps/website/components/leagues/LeagueScoringSection.tsx index d5540b7c6..2dda85bfd 100644 --- a/apps/website/components/leagues/LeagueScoringSection.tsx +++ b/apps/website/components/leagues/LeagueScoringSection.tsx @@ -5,6 +5,9 @@ import { Trophy, Award, Check, Zap, Settings, Globe, Medal, Plus, Minus, RotateC import { createPortal } from 'react-dom'; import type { LeagueScoringPresetViewModel } from '@/lib/view-models/LeagueScoringPresetViewModel'; import type { LeagueConfigFormModel } from '@/lib/types/LeagueConfigFormModel'; +import { useInject } from '@/lib/di/hooks/useInject'; +import { LEAGUE_SETTINGS_SERVICE_TOKEN } from '@/lib/di/tokens'; +import type { CustomPointsConfig } from '@/lib/view-models/ScoringConfigurationViewModel'; // ============================================================================ // INFO FLYOUT COMPONENT @@ -304,14 +307,6 @@ interface ScoringPatternSectionProps { onUpdateCustomPoints?: (points: CustomPointsConfig) => void; } -// Custom points configuration for inline editor -export interface CustomPointsConfig { - racePoints: number[]; - poleBonusPoints: number; - fastestLapPoints: number; - leaderLapPoints: number; -} - const DEFAULT_CUSTOM_POINTS: CustomPointsConfig = { racePoints: [25, 18, 15, 12, 10, 8, 6, 4, 2, 1], poleBonusPoints: 1, @@ -333,29 +328,19 @@ export function LeagueScoringSection({ patternOnly, championshipsOnly, }: LeagueScoringSectionProps) { + const leagueSettingsService = useInject(LEAGUE_SETTINGS_SERVICE_TOKEN); const disabled = readOnly || !onChange; const handleSelectPreset = (presetId: string) => { if (disabled || !onChange) return; - onChange({ - ...form, - scoring: { - ...form.scoring, - patternId: presetId, - customScoringEnabled: false, - }, - }); + const updatedForm = leagueSettingsService.selectScoringPreset(form, presetId); + onChange(updatedForm); }; const handleToggleCustomScoring = () => { if (disabled || !onChange) return; - onChange({ - ...form, - scoring: { - ...form.scoring, - customScoringEnabled: !form.scoring.customScoringEnabled, - }, - }); + const updatedForm = leagueSettingsService.toggleCustomScoring(form); + onChange(updatedForm); }; const patternProps: ScoringPatternSectionProps = { @@ -465,6 +450,7 @@ export function ScoringPatternSection({ onToggleCustomScoring, onUpdateCustomPoints, }: ScoringPatternSectionProps) { + const leagueSettingsService = useInject(LEAGUE_SETTINGS_SERVICE_TOKEN); const disabled = readOnly || !onChangePatternId; const currentPreset = presets.find((p) => p.id === scoring.patternId) ?? null; const isCustom = scoring.customScoringEnabled; @@ -514,19 +500,11 @@ export function ScoringPatternSection({ }; const getPresetEmoji = (preset: LeagueScoringPresetViewModel) => { - const name = preset.name.toLowerCase(); - if (name.includes('sprint') || name.includes('double')) return '⚡'; - if (name.includes('endurance') || name.includes('long')) return '🏆'; - if (name.includes('club') || name.includes('casual')) return '🏅'; - return '🏁'; + return leagueSettingsService.getPresetEmoji(preset); }; const getPresetDescription = (preset: LeagueScoringPresetViewModel) => { - const name = preset.name.toLowerCase(); - if (name.includes('sprint')) return 'Sprint + Feature race'; - if (name.includes('endurance')) return 'Long-form endurance'; - if (name.includes('club')) return 'Casual league format'; - return preset.sessionSummary; + return leagueSettingsService.getPresetDescription(preset); }; // Flyout state @@ -939,6 +917,7 @@ export function ChampionshipsSection({ onChange, readOnly, }: ChampionshipsSectionProps) { + const leagueSettingsService = useInject(LEAGUE_SETTINGS_SERVICE_TOKEN); const disabled = readOnly || !onChange; const isTeamsMode = form.structure.mode === 'fixedTeams'; const [showChampFlyout, setShowChampFlyout] = useState(false); @@ -948,13 +927,8 @@ export function ChampionshipsSection({ const updateChampionship = (key: keyof LeagueConfigFormModel['championships'], value: boolean) => { if (!onChange) return; - onChange({ - ...form, - championships: { - ...form.championships, - [key]: value, - }, - }); + const updatedForm = leagueSettingsService.updateChampionship(form, key, value); + onChange(updatedForm); }; const championships = [ @@ -1157,4 +1131,4 @@ export function ChampionshipsSection({ ); -} +} \ No newline at end of file diff --git a/apps/website/components/races/FileProtestModal.tsx b/apps/website/components/races/FileProtestModal.tsx index 757d0530a..1bd02f93e 100644 --- a/apps/website/components/races/FileProtestModal.tsx +++ b/apps/website/components/races/FileProtestModal.tsx @@ -4,7 +4,6 @@ import { useState } from 'react'; import Modal from '@/components/ui/Modal'; import Button from '@/components/ui/Button'; import type { FileProtestCommandDTO } from '@/lib/types/generated/FileProtestCommandDTO'; -import type { ProtestIncidentDTO } from '@/lib/types/generated/ProtestIncidentDTO'; import { useFileProtest } from '@/hooks/race/useFileProtest'; import { AlertTriangle, @@ -16,11 +15,9 @@ import { FileText, CheckCircle2, } from 'lucide-react'; - -type ProtestParticipant = { - id: string; - name: string; -}; +import { useInject } from '@/lib/di/hooks/useInject'; +import { PROTEST_SERVICE_TOKEN } from '@/lib/di/tokens'; +import type { ProtestParticipant } from '@/lib/services/protests/ProtestService'; interface FileProtestModalProps { isOpen: boolean; @@ -41,6 +38,7 @@ export default function FileProtestModal({ }: FileProtestModalProps) { const fileProtestMutation = useFileProtest(); const [errorMessage, setErrorMessage] = useState(null); + const protestService = useInject(PROTEST_SERVICE_TOKEN); // Form state const [accusedDriverId, setAccusedDriverId] = useState(''); @@ -53,51 +51,41 @@ export default function FileProtestModal({ const otherParticipants = participants.filter(p => p.id !== protestingDriverId); const handleSubmit = async () => { - // Validation - if (!accusedDriverId) { - setErrorMessage('Please select the driver you are protesting against.'); - return; + try { + // Use ProtestService for validation and command construction + const command = protestService.constructFileProtestCommand({ + raceId, + leagueId, + protestingDriverId, + accusedDriverId, + lap, + timeInRace, + description, + comment, + proofVideoUrl, + }); + + setErrorMessage(null); + + // Use existing hook for the actual API call + fileProtestMutation.mutate(command, { + onSuccess: () => { + // Reset form state on success + setAccusedDriverId(''); + setLap(''); + setTimeInRace(''); + setDescription(''); + setComment(''); + setProofVideoUrl(''); + }, + onError: (error) => { + setErrorMessage(error.message || 'Failed to file protest'); + }, + }); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Failed to validate protest input'; + setErrorMessage(errorMessage); } - if (!lap || parseInt(lap, 10) < 0) { - setErrorMessage('Please enter a valid lap number.'); - return; - } - if (!description.trim()) { - setErrorMessage('Please describe what happened.'); - return; - } - - setErrorMessage(null); - - const incident: ProtestIncidentDTO = { - lap: parseInt(lap, 10), - description: description.trim(), - ...(timeInRace ? { timeInRace: parseInt(timeInRace, 10) } : {}), - }; - - const command = { - raceId, - protestingDriverId, - accusedDriverId, - incident, - ...(comment.trim() ? { comment: comment.trim() } : {}), - ...(proofVideoUrl.trim() ? { proofVideoUrl: proofVideoUrl.trim() } : {}), - } satisfies FileProtestCommandDTO; - - fileProtestMutation.mutate(command, { - onSuccess: () => { - // Reset form state on success - setAccusedDriverId(''); - setLap(''); - setTimeInRace(''); - setDescription(''); - setComment(''); - setProofVideoUrl(''); - }, - onError: (error) => { - setErrorMessage(error.message || 'Failed to file protest'); - }, - }); }; const handleClose = () => { @@ -207,7 +195,7 @@ export default function FileProtestModal({ /> - + {/* Incident Description */}
- + {/* Additional Comment */}
- + {/* Video Proof */}
- + {/* Info Box */}

@@ -267,7 +255,7 @@ export default function FileProtestModal({ to grid penalties for future races, depending on the severity.

- + {/* Actions */}