From a7aee424092f210a21eb326c99aa58d266bf56ac Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Wed, 24 Dec 2025 13:04:18 +0100 Subject: [PATCH] website cleanup --- .eslintrc.json | 77 ++ apps/api/openapi.json | 1052 ++++++++--------- .../domain/league/dtos/LeagueStandingDTO.ts | 14 +- .../app/auth/iracing/callback/route.ts | 5 +- apps/website/app/auth/iracing/page.tsx | 1 - apps/website/app/drivers/page.tsx | 29 +- apps/website/app/leagues/[id]/layout.tsx | 4 +- .../app/leagues/[id]/schedule/page.tsx | 4 +- .../app/leagues/[id]/settings/page.tsx | 6 +- .../app/leagues/[id]/sponsorships/page.tsx | 4 +- .../app/leagues/[id]/standings/page.tsx | 15 +- apps/website/app/profile/leagues/page.tsx | 2 +- apps/website/app/races/[id]/page.tsx | 1 - apps/website/app/races/[id]/results/page.tsx | 2 +- apps/website/app/sponsor/billing/page.tsx | 12 - .../website/app/sponsor/leagues/[id]/page.tsx | 7 +- .../alpha/CompanionInstructions.tsx | 4 +- .../website/components/drivers/DriverCard.tsx | 12 +- .../components/drivers/DriverIdentity.tsx | 9 +- .../components/drivers/DriverProfile.tsx | 72 +- .../components/drivers/HeroSection.tsx | 4 +- .../components/drivers/ProfileRaceHistory.tsx | 4 +- .../components/drivers/ProfileSettings.tsx | 2 +- .../components/drivers/ProfileStats.tsx | 1 - .../components/leagues/CreateLeagueForm.tsx | 36 +- .../components/leagues/CreateLeagueWizard.tsx | 316 +++-- .../leagues/LeagueBasicsSection.tsx | 2 +- .../website/components/leagues/LeagueCard.tsx | 9 +- .../components/leagues/LeagueDropSection.tsx | 4 +- .../components/leagues/LeagueMembers.tsx | 14 +- .../leagues/LeagueScoringSection.tsx | 4 +- .../leagues/LeagueSponsorshipsSection.tsx | 3 +- .../leagues/LeagueStructureSection.tsx | 15 +- .../leagues/LeagueTimingsSection.tsx | 8 +- .../leagues/LeagueVisibilitySection.tsx | 14 +- apps/website/components/teams/TeamAdmin.tsx | 8 +- apps/website/components/teams/TeamRoster.tsx | 75 +- .../components/teams/TeamStandings.tsx | 22 +- apps/website/hooks/useLeagueScoringPresets.ts | 15 + apps/website/hooks/useLeagueWizardService.ts | 82 ++ apps/website/lib/api/index.ts | 29 +- apps/website/lib/leagueMembership.ts | 24 + apps/website/lib/leagueRoles.ts | 8 + apps/website/lib/services/auth/AuthService.ts | 13 + .../drivers/DriverRegistrationService.ts | 2 +- .../leagues/LeagueMembershipService.ts | 24 +- .../lib/services/leagues/LeagueService.ts | 85 +- .../lib/services/media/MediaService.ts | 14 + .../services/sponsors/SponsorshipService.ts | 8 +- apps/website/lib/types/League.ts | 10 + .../lib/types/LeagueConfigFormModel.ts | 54 + apps/website/lib/types/LeagueMembership.ts | 7 + .../lib/types/LeagueScoringConfigDTO.ts | 5 + apps/website/lib/types/MembershipRole.ts | 1 + apps/website/lib/types/Weekday.ts | 1 + apps/website/lib/types/WizardErrors.ts | 21 + .../lib/types/generated/LeagueStandingDTO.ts | 8 + .../website/lib/types/generated/PaymentDto.ts | 9 - .../lib/utilities/LeagueRoleUtility.ts | 4 +- .../AvailableLeaguesViewModel.test.ts | 8 +- .../lib/view-models/DriverProfileViewModel.ts | 6 +- .../view-models/LeagueDetailPageViewModel.ts | 12 +- .../view-models/LeaguePageDetailViewModel.ts | 24 + .../LeagueScoringPresetsViewModel.ts | 2 +- .../lib/view-models/MembershipFeeViewModel.ts | 4 +- .../lib/view-models/PaymentViewModel.ts | 2 +- .../lib/view-models/StandingEntryViewModel.ts | 2 +- apps/website/tsconfig.json | 2 - package-lock.json | 203 +++- 69 files changed, 1624 insertions(+), 938 deletions(-) create mode 100644 apps/website/hooks/useLeagueScoringPresets.ts create mode 100644 apps/website/hooks/useLeagueWizardService.ts create mode 100644 apps/website/lib/leagueMembership.ts create mode 100644 apps/website/lib/leagueRoles.ts create mode 100644 apps/website/lib/types/League.ts create mode 100644 apps/website/lib/types/LeagueConfigFormModel.ts create mode 100644 apps/website/lib/types/LeagueMembership.ts create mode 100644 apps/website/lib/types/LeagueScoringConfigDTO.ts create mode 100644 apps/website/lib/types/MembershipRole.ts create mode 100644 apps/website/lib/types/Weekday.ts create mode 100644 apps/website/lib/types/WizardErrors.ts delete mode 100644 apps/website/lib/types/generated/PaymentDto.ts create mode 100644 apps/website/lib/view-models/LeaguePageDetailViewModel.ts diff --git a/.eslintrc.json b/.eslintrc.json index 22cd5914d..d63dc0077 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -271,6 +271,83 @@ } ] } + }, + { + "files": ["apps/website/**/*.tsx", "apps/website/**/*.ts"], + "rules": { + "no-restricted-syntax": [ + "error", + { + "selector": "TSInterfaceDeclaration[id.name=/ViewModel$/], TSTypeAliasDeclaration[id.name=/ViewModel$/], TSClassDeclaration[id.name=/ViewModel$/]", + "message": "ViewModel types must be defined in apps/website/lib/view-models, not in components." + }, + { + "selector": "TSInterfaceDeclaration[id.name=/DTO$/], TSTypeAliasDeclaration[id.name=/DTO$/], TSClassDeclaration[id.name=/DTO$/]", + "message": "DTO types are forbidden in website components. Use ViewModels instead." + } + ], + "no-restricted-imports": [ + "error", + { + "paths": [ + { + "name": "@core/racing", + "message": "Imports from @core are forbidden in website components" + }, + { + "name": "@core/analytics", + "message": "Imports from @core are forbidden in website components" + }, + { + "name": "@core/identity", + "message": "Imports from @core are forbidden in website components" + }, + { + "name": "@core/media", + "message": "Imports from @core are forbidden in website components" + }, + { + "name": "@core/notifications", + "message": "Imports from @core are forbidden in website components" + }, + { + "name": "@core/payments", + "message": "Imports from @core are forbidden in website components" + }, + { + "name": "@core/shared", + "message": "Imports from @core are forbidden in website components" + }, + { + "name": "@core/social", + "message": "Imports from @core are forbidden in website components" + }, + { + "name": "@adapters", + "message": "Imports from @adapters are forbidden in website components" + }, + { + "name": "@api", + "message": "Imports from @api are forbidden in website components" + } + ], + "patterns": [ + { + "group": ["@core/*"], + "message": "Imports from @core are forbidden in website components" + }, + { + "group": ["@adapters/*"], + "message": "Imports from @adapters are forbidden in website components" + }, + { + "group": ["@api/*"], + "message": "Imports from @api are forbidden in website components" + } + ] + } + ] + } } ] } \ No newline at end of file diff --git a/apps/api/openapi.json b/apps/api/openapi.json index 37fc890c9..e8398c153 100644 --- a/apps/api/openapi.json +++ b/apps/api/openapi.json @@ -8,66 +8,6 @@ "paths": {}, "components": { "schemas": { - "UpdateTeamOutputDTO": { - "type": "object", - "properties": { - "success": { - "type": "boolean" - } - }, - "required": [ - "success" - ] - }, - "GetTeamMembershipOutputDTO": { - "type": "object", - "properties": { - "role": { - "type": "string" - }, - "joinedAt": { - "type": "string" - }, - "isActive": { - "type": "boolean" - } - }, - "required": [ - "role", - "joinedAt", - "isActive" - ] - }, - "CreateTeamOutputDTO": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "success": { - "type": "boolean" - } - }, - "required": [ - "id", - "success" - ] - }, - "CreateTeamInputDTO": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "tag": { - "type": "string" - } - }, - "required": [ - "name", - "tag" - ] - }, "SponsorshipRequestDTO": { "type": "object", "properties": { @@ -647,6 +587,231 @@ "respondedBy" ] }, + "UpdateTeamOutputDTO": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + } + }, + "required": [ + "success" + ] + }, + "GetTeamMembershipOutputDTO": { + "type": "object", + "properties": { + "role": { + "type": "string" + }, + "joinedAt": { + "type": "string" + }, + "isActive": { + "type": "boolean" + } + }, + "required": [ + "role", + "joinedAt", + "isActive" + ] + }, + "CreateTeamOutputDTO": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "success": { + "type": "boolean" + } + }, + "required": [ + "id", + "success" + ] + }, + "CreateTeamInputDTO": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "tag": { + "type": "string" + } + }, + "required": [ + "name", + "tag" + ] + }, + "UpdatePaymentStatusInputDTO": { + "type": "object", + "properties": { + "paymentId": { + "type": "string" + } + }, + "required": [ + "paymentId" + ] + }, + "PaymentDTO": { + "type": "object", + "properties": { + "id": { + "type": "string" + } + }, + "required": [ + "id" + ] + }, + "MembershipFeeDTO": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "leagueId": { + "type": "string" + } + }, + "required": [ + "id", + "leagueId" + ] + }, + "MemberPaymentDTO": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "feeId": { + "type": "string" + }, + "driverId": { + "type": "string" + }, + "amount": { + "type": "number" + }, + "platformFee": { + "type": "number" + }, + "netAmount": { + "type": "number" + } + }, + "required": [ + "id", + "feeId", + "driverId", + "amount", + "platformFee", + "netAmount" + ] + }, + "PrizeDTO": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "leagueId": { + "type": "string" + }, + "seasonId": { + "type": "string" + }, + "position": { + "type": "number" + }, + "name": { + "type": "string" + }, + "amount": { + "type": "number" + } + }, + "required": [ + "id", + "leagueId", + "seasonId", + "position", + "name", + "amount" + ] + }, + "WalletDTO": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "leagueId": { + "type": "string" + }, + "balance": { + "type": "number" + }, + "totalRevenue": { + "type": "number" + }, + "totalPlatformFees": { + "type": "number" + }, + "totalWithdrawn": { + "type": "number" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "currency": { + "type": "string" + } + }, + "required": [ + "id", + "leagueId", + "balance", + "totalRevenue", + "totalPlatformFees", + "totalWithdrawn", + "createdAt", + "currency" + ] + }, + "TransactionDTO": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "walletId": { + "type": "string" + } + }, + "required": [ + "id", + "walletId" + ] + }, + "DeletePrizeResultDTO": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + } + }, + "required": [ + "success" + ] + }, "WithdrawFromRaceParamsDTO": { "type": "object", "properties": { @@ -1411,171 +1576,6 @@ "name" ] }, - "UpdatePaymentStatusInputDTO": { - "type": "object", - "properties": { - "paymentId": { - "type": "string" - } - }, - "required": [ - "paymentId" - ] - }, - "PaymentDTO": { - "type": "object", - "properties": { - "id": { - "type": "string" - } - }, - "required": [ - "id" - ] - }, - "MembershipFeeDTO": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "leagueId": { - "type": "string" - } - }, - "required": [ - "id", - "leagueId" - ] - }, - "MemberPaymentDTO": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "feeId": { - "type": "string" - }, - "driverId": { - "type": "string" - }, - "amount": { - "type": "number" - }, - "platformFee": { - "type": "number" - }, - "netAmount": { - "type": "number" - } - }, - "required": [ - "id", - "feeId", - "driverId", - "amount", - "platformFee", - "netAmount" - ] - }, - "PrizeDTO": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "leagueId": { - "type": "string" - }, - "seasonId": { - "type": "string" - }, - "position": { - "type": "number" - }, - "name": { - "type": "string" - }, - "amount": { - "type": "number" - } - }, - "required": [ - "id", - "leagueId", - "seasonId", - "position", - "name", - "amount" - ] - }, - "WalletDTO": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "leagueId": { - "type": "string" - }, - "balance": { - "type": "number" - }, - "totalRevenue": { - "type": "number" - }, - "totalPlatformFees": { - "type": "number" - }, - "totalWithdrawn": { - "type": "number" - }, - "createdAt": { - "type": "string", - "format": "date-time" - }, - "currency": { - "type": "string" - } - }, - "required": [ - "id", - "leagueId", - "balance", - "totalRevenue", - "totalPlatformFees", - "totalWithdrawn", - "createdAt", - "currency" - ] - }, - "TransactionDTO": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "walletId": { - "type": "string" - } - }, - "required": [ - "id", - "walletId" - ] - }, - "DeletePrizeResultDTO": { - "type": "object", - "properties": { - "success": { - "type": "boolean" - } - }, - "required": [ - "success" - ] - }, "UploadMediaOutputDTO": { "type": "object", "properties": { @@ -1683,6 +1683,307 @@ "success" ] }, + "GetDriverRegistrationStatusQueryDTO": { + "type": "object", + "properties": { + "raceId": { + "type": "string" + }, + "driverId": { + "type": "string" + } + }, + "required": [ + "raceId", + "driverId" + ] + }, + "GetDriverOutputDTO": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "iracingId": { + "type": "string" + }, + "name": { + "type": "string" + }, + "country": { + "type": "string" + }, + "bio": { + "type": "string" + }, + "joinedAt": { + "type": "string" + } + }, + "required": [ + "id", + "iracingId", + "name", + "country", + "joinedAt" + ] + }, + "DriverStatsDTO": { + "type": "object", + "properties": { + "totalDrivers": { + "type": "number" + } + }, + "required": [ + "totalDrivers" + ] + }, + "DriverRegistrationStatusDTO": { + "type": "object", + "properties": { + "isRegistered": { + "type": "boolean" + }, + "raceId": { + "type": "string" + }, + "driverId": { + "type": "string" + } + }, + "required": [ + "isRegistered", + "raceId", + "driverId" + ] + }, + "DriverProfileTeamMembershipDTO": { + "type": "object", + "properties": { + "teamId": { + "type": "string" + }, + "teamName": { + "type": "string" + } + }, + "required": [ + "teamId", + "teamName" + ] + }, + "DriverProfileStatsDTO": { + "type": "object", + "properties": { + "totalRaces": { + "type": "number" + }, + "wins": { + "type": "number" + }, + "podiums": { + "type": "number" + }, + "dnfs": { + "type": "number" + } + }, + "required": [ + "totalRaces", + "wins", + "podiums", + "dnfs" + ] + }, + "DriverProfileSocialSummaryDTO": { + "type": "object", + "properties": { + "friendsCount": { + "type": "number" + } + }, + "required": [ + "friendsCount" + ] + }, + "DriverProfileSocialFriendSummaryDTO": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "country": { + "type": "string" + }, + "avatarUrl": { + "type": "string" + } + }, + "required": [ + "id", + "name", + "country", + "avatarUrl" + ] + }, + "DriverProfileFinishDistributionDTO": { + "type": "object", + "properties": { + "totalRaces": { + "type": "number" + }, + "wins": { + "type": "number" + }, + "podiums": { + "type": "number" + }, + "topTen": { + "type": "number" + }, + "dnfs": { + "type": "number" + }, + "other": { + "type": "number" + } + }, + "required": [ + "totalRaces", + "wins", + "podiums", + "topTen", + "dnfs", + "other" + ] + }, + "DriverProfileDriverSummaryDTO": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "country": { + "type": "string" + }, + "avatarUrl": { + "type": "string" + } + }, + "required": [ + "id", + "name", + "country", + "avatarUrl" + ] + }, + "DriverProfileAchievementDTO": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + } + }, + "required": [ + "id", + "title", + "description" + ] + }, + "DriverLeaderboardItemDTO": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "rating": { + "type": "number" + }, + "skillLevel": { + "type": "string" + }, + "nationality": { + "type": "string" + }, + "racesCompleted": { + "type": "number" + }, + "wins": { + "type": "number" + }, + "podiums": { + "type": "number" + }, + "isActive": { + "type": "boolean" + }, + "rank": { + "type": "number" + } + }, + "required": [ + "id", + "name", + "rating", + "skillLevel", + "nationality", + "racesCompleted", + "wins", + "podiums", + "isActive", + "rank" + ] + }, + "CompleteOnboardingOutputDTO": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + } + }, + "required": [ + "success" + ] + }, + "CompleteOnboardingInputDTO": { + "type": "object", + "properties": { + "firstName": { + "type": "string" + }, + "lastName": { + "type": "string" + }, + "displayName": { + "type": "string" + }, + "country": { + "type": "string" + } + }, + "required": [ + "firstName", + "lastName", + "displayName", + "country" + ] + }, "WithdrawFromLeagueWalletOutputDTO": { "type": "object", "properties": { @@ -2283,307 +2584,6 @@ "leagueId" ] }, - "GetDriverRegistrationStatusQueryDTO": { - "type": "object", - "properties": { - "raceId": { - "type": "string" - }, - "driverId": { - "type": "string" - } - }, - "required": [ - "raceId", - "driverId" - ] - }, - "GetDriverOutputDTO": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "iracingId": { - "type": "string" - }, - "name": { - "type": "string" - }, - "country": { - "type": "string" - }, - "bio": { - "type": "string" - }, - "joinedAt": { - "type": "string" - } - }, - "required": [ - "id", - "iracingId", - "name", - "country", - "joinedAt" - ] - }, - "DriverStatsDTO": { - "type": "object", - "properties": { - "totalDrivers": { - "type": "number" - } - }, - "required": [ - "totalDrivers" - ] - }, - "DriverRegistrationStatusDTO": { - "type": "object", - "properties": { - "isRegistered": { - "type": "boolean" - }, - "raceId": { - "type": "string" - }, - "driverId": { - "type": "string" - } - }, - "required": [ - "isRegistered", - "raceId", - "driverId" - ] - }, - "DriverProfileTeamMembershipDTO": { - "type": "object", - "properties": { - "teamId": { - "type": "string" - }, - "teamName": { - "type": "string" - } - }, - "required": [ - "teamId", - "teamName" - ] - }, - "DriverProfileStatsDTO": { - "type": "object", - "properties": { - "totalRaces": { - "type": "number" - }, - "wins": { - "type": "number" - }, - "podiums": { - "type": "number" - }, - "dnfs": { - "type": "number" - } - }, - "required": [ - "totalRaces", - "wins", - "podiums", - "dnfs" - ] - }, - "DriverProfileSocialSummaryDTO": { - "type": "object", - "properties": { - "friendsCount": { - "type": "number" - } - }, - "required": [ - "friendsCount" - ] - }, - "DriverProfileSocialFriendSummaryDTO": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "name": { - "type": "string" - }, - "country": { - "type": "string" - }, - "avatarUrl": { - "type": "string" - } - }, - "required": [ - "id", - "name", - "country", - "avatarUrl" - ] - }, - "DriverProfileFinishDistributionDTO": { - "type": "object", - "properties": { - "totalRaces": { - "type": "number" - }, - "wins": { - "type": "number" - }, - "podiums": { - "type": "number" - }, - "topTen": { - "type": "number" - }, - "dnfs": { - "type": "number" - }, - "other": { - "type": "number" - } - }, - "required": [ - "totalRaces", - "wins", - "podiums", - "topTen", - "dnfs", - "other" - ] - }, - "DriverProfileDriverSummaryDTO": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "name": { - "type": "string" - }, - "country": { - "type": "string" - }, - "avatarUrl": { - "type": "string" - } - }, - "required": [ - "id", - "name", - "country", - "avatarUrl" - ] - }, - "DriverProfileAchievementDTO": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "title": { - "type": "string" - }, - "description": { - "type": "string" - } - }, - "required": [ - "id", - "title", - "description" - ] - }, - "DriverLeaderboardItemDTO": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "name": { - "type": "string" - }, - "rating": { - "type": "number" - }, - "skillLevel": { - "type": "string" - }, - "nationality": { - "type": "string" - }, - "racesCompleted": { - "type": "number" - }, - "wins": { - "type": "number" - }, - "podiums": { - "type": "number" - }, - "isActive": { - "type": "boolean" - }, - "rank": { - "type": "number" - } - }, - "required": [ - "id", - "name", - "rating", - "skillLevel", - "nationality", - "racesCompleted", - "wins", - "podiums", - "isActive", - "rank" - ] - }, - "CompleteOnboardingOutputDTO": { - "type": "object", - "properties": { - "success": { - "type": "boolean" - } - }, - "required": [ - "success" - ] - }, - "CompleteOnboardingInputDTO": { - "type": "object", - "properties": { - "firstName": { - "type": "string" - }, - "lastName": { - "type": "string" - }, - "displayName": { - "type": "string" - }, - "country": { - "type": "string" - } - }, - "required": [ - "firstName", - "lastName", - "displayName", - "country" - ] - }, "AuthenticatedUserDTO": { "type": "object", "properties": { diff --git a/apps/api/src/domain/league/dtos/LeagueStandingDTO.ts b/apps/api/src/domain/league/dtos/LeagueStandingDTO.ts index e20e09b20..d2119f7df 100644 --- a/apps/api/src/domain/league/dtos/LeagueStandingDTO.ts +++ b/apps/api/src/domain/league/dtos/LeagueStandingDTO.ts @@ -19,5 +19,17 @@ export class LeagueStandingDTO { @ApiProperty() @IsNumber() - rank!: number; + position!: number; + + @ApiProperty() + @IsNumber() + wins!: number; + + @ApiProperty() + @IsNumber() + podiums!: number; + + @ApiProperty() + @IsNumber() + races!: number; } \ No newline at end of file diff --git a/apps/website/app/auth/iracing/callback/route.ts b/apps/website/app/auth/iracing/callback/route.ts index a0c372c5b..116d6483c 100644 --- a/apps/website/app/auth/iracing/callback/route.ts +++ b/apps/website/app/auth/iracing/callback/route.ts @@ -1,6 +1,6 @@ import { cookies } from 'next/headers'; import { NextResponse } from 'next/server'; - +import { ServiceFactory } from '@/lib/services/ServiceFactory'; const STATE_COOKIE = 'gp_demo_auth_state'; @@ -22,7 +22,8 @@ export async function GET(request: Request) { return NextResponse.redirect('/auth/iracing'); } - const authService = getAuthService(); + const serviceFactory = new ServiceFactory(process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3001'); + const authService = serviceFactory.createAuthService(); const loginInput = returnTo ? { code, state, returnTo } : { code, state }; await authService.loginWithIracingCallback(loginInput); diff --git a/apps/website/app/auth/iracing/page.tsx b/apps/website/app/auth/iracing/page.tsx index 845077a37..f421513fc 100644 --- a/apps/website/app/auth/iracing/page.tsx +++ b/apps/website/app/auth/iracing/page.tsx @@ -14,7 +14,6 @@ import { Trophy, BarChart3, CheckCircle2, - Loader2, } from 'lucide-react'; import Card from '@/components/ui/Card'; diff --git a/apps/website/app/drivers/page.tsx b/apps/website/app/drivers/page.tsx index 9d6b9ae00..9006542ff 100644 --- a/apps/website/app/drivers/page.tsx +++ b/apps/website/app/drivers/page.tsx @@ -27,11 +27,11 @@ import Button from '@/components/ui/Button'; import Input from '@/components/ui/Input'; import Card from '@/components/ui/Card'; import Heading from '@/components/ui/Heading'; -import { useServices } from '@/lib/services/ServiceProvider'; -import type { DriverLeaderboardViewModel } from '@/lib/view-models'; +import { useDriverLeaderboard } from '@/hooks/useDriverService'; import Image from 'next/image'; import type { DriverLeaderboardItemViewModel } from '@/lib/view-models/DriverLeaderboardItemViewModel'; +import type { DriverLeaderboardViewModel } from '@/lib/view-models/DriverLeaderboardViewModel'; // ============================================================================ // DEMO DATA @@ -373,28 +373,13 @@ function RecentActivity({ drivers, onDriverClick }: RecentActivityProps) { export default function DriversPage() { const router = useRouter(); - const { driverService } = useServices(); - const [drivers, setDrivers] = useState([]); - const [viewModel, setViewModel] = useState(null); - const [loading, setLoading] = useState(true); + const { data: viewModel, isLoading: loading } = useDriverLeaderboard(); const [searchQuery, setSearchQuery] = useState(''); - const [totalRaces, setTotalRaces] = useState(0); - const [totalWins, setTotalWins] = useState(0); - const [activeCount, setActiveCount] = useState(0); - useEffect(() => { - const load = async () => { - const vm = await driverService.getDriverLeaderboard(); - setViewModel(vm); - setDrivers(vm.drivers); - setTotalRaces(vm.totalRaces); - setTotalWins(vm.totalWins); - setActiveCount(vm.activeCount); - setLoading(false); - }; - - void load(); - }, [driverService]); + const drivers = viewModel?.drivers || []; + const totalRaces = viewModel?.totalRaces || 0; + const totalWins = viewModel?.totalWins || 0; + const activeCount = viewModel?.activeCount || 0; const handleDriverClick = (driverId: string) => { router.push(`/drivers/${driverId}`); diff --git a/apps/website/app/leagues/[id]/layout.tsx b/apps/website/app/leagues/[id]/layout.tsx index e6021af7d..b86acf480 100644 --- a/apps/website/app/leagues/[id]/layout.tsx +++ b/apps/website/app/leagues/[id]/layout.tsx @@ -4,7 +4,7 @@ import Breadcrumbs from '@/components/layout/Breadcrumbs'; import LeagueHeader from '@/components/leagues/LeagueHeader'; import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId'; import { useServices } from '@/lib/services/ServiceProvider'; -import { LeagueDetailViewModel } from '@/lib/view-models/LeagueDetailViewModel'; +import { LeaguePageDetailViewModel } from '@/lib/view-models/LeaguePageDetailViewModel'; import { useParams, usePathname, useRouter } from 'next/navigation'; import React, { useEffect, useState } from 'react'; @@ -20,7 +20,7 @@ export default function LeagueLayout({ const currentDriverId = useEffectiveDriverId(); const { leagueService } = useServices(); - const [leagueDetail, setLeagueDetail] = useState(null); + const [leagueDetail, setLeagueDetail] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { diff --git a/apps/website/app/leagues/[id]/schedule/page.tsx b/apps/website/app/leagues/[id]/schedule/page.tsx index 21dd71596..fcf107bac 100644 --- a/apps/website/app/leagues/[id]/schedule/page.tsx +++ b/apps/website/app/leagues/[id]/schedule/page.tsx @@ -3,7 +3,7 @@ import LeagueSchedule from '@/components/leagues/LeagueSchedule'; import Card from '@/components/ui/Card'; import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId'; -import { isLeagueAdminOrHigherRole } from '@/lib/leagueRoles'; +import { LeagueRoleUtility } from '@/lib/utilities/LeagueRoleUtility'; import { useServices } from '@/lib/services/ServiceProvider'; import { useParams, useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; @@ -21,7 +21,7 @@ export default function LeagueSchedulePage() { async function checkAdmin() { await leagueMembershipService.fetchLeagueMemberships(leagueId); const membership = leagueMembershipService.getMembership(leagueId, currentDriverId); - setIsAdmin(membership ? isLeagueAdminOrHigherRole(membership.role) : false); + setIsAdmin(membership ? LeagueRoleUtility.isLeagueAdminOrHigherRole(membership.role) : false); } checkAdmin(); }, [leagueId, currentDriverId, leagueMembershipService]); diff --git a/apps/website/app/leagues/[id]/settings/page.tsx b/apps/website/app/leagues/[id]/settings/page.tsx index ebfff0c11..6ba311294 100644 --- a/apps/website/app/leagues/[id]/settings/page.tsx +++ b/apps/website/app/leagues/[id]/settings/page.tsx @@ -4,9 +4,9 @@ import { ReadonlyLeagueInfo } from '@/components/leagues/ReadonlyLeagueInfo'; import LeagueOwnershipTransfer from '@/components/leagues/LeagueOwnershipTransfer'; import Card from '@/components/ui/Card'; import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId'; -import { isLeagueAdminOrHigherRole } from '@/lib/leagueRoles'; +import { LeagueRoleUtility } from '@/lib/utilities/LeagueRoleUtility'; import { useServices } from '@/lib/services/ServiceProvider'; -import type { LeagueConfigFormModel } from '@core/racing/application'; +import type { LeagueConfigFormModel } from '@/lib/types/LeagueConfigFormModel'; import { LeagueSettingsViewModel } from '@/lib/view-models/LeagueSettingsViewModel'; import { AlertTriangle, Settings, UserCog } from 'lucide-react'; import { useParams, useRouter } from 'next/navigation'; @@ -27,7 +27,7 @@ export default function LeagueSettingsPage() { async function checkAdmin() { const memberships = await leagueMembershipService.fetchLeagueMemberships(leagueId); const membership = leagueMembershipService.getMembership(leagueId, currentDriverId); - setIsAdmin(membership ? isLeagueAdminOrHigherRole(membership.role) : false); + setIsAdmin(membership ? LeagueRoleUtility.isLeagueAdminOrHigherRole(membership.role) : false); } checkAdmin(); }, [leagueId, currentDriverId, leagueMembershipService]); diff --git a/apps/website/app/leagues/[id]/sponsorships/page.tsx b/apps/website/app/leagues/[id]/sponsorships/page.tsx index 7f7b69fed..cdcb0aba2 100644 --- a/apps/website/app/leagues/[id]/sponsorships/page.tsx +++ b/apps/website/app/leagues/[id]/sponsorships/page.tsx @@ -3,7 +3,7 @@ import { LeagueSponsorshipsSection } from '@/components/leagues/LeagueSponsorshipsSection'; import Card from '@/components/ui/Card'; import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId'; -import { isLeagueAdminOrHigherRole } from '@/lib/leagueRoles'; +import { LeagueRoleUtility } from '@/lib/utilities/LeagueRoleUtility'; import { useServices } from '@/lib/services/ServiceProvider'; import { LeagueDetailViewModel } from '@/lib/view-models/LeagueDetailViewModel'; import { AlertTriangle, Building } from 'lucide-react'; @@ -31,7 +31,7 @@ export default function LeagueSponsorshipsPage() { const membership = leagueMembershipService.getMembership(leagueId, currentDriverId); setLeague(leagueDetail); - setIsAdmin(membership ? isLeagueAdminOrHigherRole(membership.role) : false); + setIsAdmin(membership ? LeagueRoleUtility.isLeagueAdminOrHigherRole(membership.role) : false); } catch (err) { console.error('Failed to load league:', err); } finally { diff --git a/apps/website/app/leagues/[id]/standings/page.tsx b/apps/website/app/leagues/[id]/standings/page.tsx index 4724b83c3..1e333f5d4 100644 --- a/apps/website/app/leagues/[id]/standings/page.tsx +++ b/apps/website/app/leagues/[id]/standings/page.tsx @@ -4,12 +4,13 @@ import StandingsTable from '@/components/leagues/StandingsTable'; import LeagueChampionshipStats from '@/components/leagues/LeagueChampionshipStats'; import Card from '@/components/ui/Card'; import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId'; -import type { LeagueMembership, MembershipRole } from '@/lib/types'; -import { isLeagueAdminOrHigherRole } from '@/lib/leagueRoles'; +import type { LeagueMembership } from '@/lib/types/LeagueMembership'; +import type { MembershipRoleDTO } from '@/lib/types/generated/MembershipRoleDTO'; +import { LeagueRoleUtility } from '@/lib/utilities/LeagueRoleUtility'; import { useServices } from '@/lib/services/ServiceProvider'; -import { DriverViewModel } from '@/lib/view-models'; -import type { LeagueStandingsViewModel } from '@/lib/view-models'; -import type { StandingEntryViewModel } from '@/lib/view-models/StandingEntryViewModel'; +import { DriverViewModel } from '@/lib/view-models/DriverViewModel'; +import { LeagueStandingsViewModel } from '@/lib/view-models/LeagueStandingsViewModel'; +import { StandingEntryViewModel } from '@/lib/view-models/StandingEntryViewModel'; import { useParams } from 'next/navigation'; import { useCallback, useEffect, useState } from 'react'; @@ -37,7 +38,7 @@ export default function LeagueStandingsPage() { // Check if current user is admin const membership = vm.memberships.find(m => m.driverId === currentDriverId); - setIsAdmin(membership ? isLeagueAdminOrHigherRole(membership.role) : false); + setIsAdmin(membership ? LeagueRoleUtility.isLeagueAdminOrHigherRole(membership.role) : false); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to load standings'); } finally { @@ -62,7 +63,7 @@ export default function LeagueStandingsPage() { } }; - const handleUpdateRole = async (driverId: string, newRole: MembershipRole) => { + const handleUpdateRole = async (driverId: string, newRole: MembershipRoleDTO['value']) => { try { await leagueService.updateMemberRole(leagueId, currentDriverId, driverId, newRole); await loadData(); diff --git a/apps/website/app/profile/leagues/page.tsx b/apps/website/app/profile/leagues/page.tsx index 6ee3b18b7..cd172b841 100644 --- a/apps/website/app/profile/leagues/page.tsx +++ b/apps/website/app/profile/leagues/page.tsx @@ -4,7 +4,7 @@ import Button from '@/components/ui/Button'; import Card from '@/components/ui/Card'; import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId'; import { useServices } from '@/lib/services/ServiceProvider'; -import type { LeagueMembership } from '@core/racing/domain/entities/LeagueMembership'; +import type { LeagueMembership } from '@/lib/types/LeagueMembership'; import type { LeagueSummaryViewModel } from '@/lib/view-models/LeagueSummaryViewModel'; import Link from 'next/link'; import { useEffect, useState } from 'react'; diff --git a/apps/website/app/races/[id]/page.tsx b/apps/website/app/races/[id]/page.tsx index 7683f94a7..669456824 100644 --- a/apps/website/app/races/[id]/page.tsx +++ b/apps/website/app/races/[id]/page.tsx @@ -11,7 +11,6 @@ import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId'; import { useRaceDetail, useRegisterForRace, useWithdrawFromRace, useCancelRace, useCompleteRace, useReopenRace } from '@/hooks/useRaceService'; import { useLeagueMembership } from '@/hooks/useLeagueMembershipService'; import { LeagueMembershipUtility } from '@/lib/utilities/LeagueMembershipUtility'; -import type { RaceDetailViewModel } from '@/lib/view-models/RaceDetailViewModel'; import { AlertTriangle, ArrowLeft, diff --git a/apps/website/app/races/[id]/results/page.tsx b/apps/website/app/races/[id]/results/page.tsx index 16c2987a8..ca7791066 100644 --- a/apps/website/app/races/[id]/results/page.tsx +++ b/apps/website/app/races/[id]/results/page.tsx @@ -11,7 +11,7 @@ import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId'; import { useRaceResultsDetail, useRaceWithSOF } from '@/hooks/useRaceService'; import { useLeagueMembership } from '@/hooks/useLeagueMembershipService'; import { LeagueRoleUtility } from '@/lib/utilities/LeagueRoleUtility'; -import type { RaceResultsDetailViewModel } from '@/lib/view-models'; +import type { RaceResultsDetailViewModel } from '@/lib/view-models/RaceResultsDetailViewModel'; import { ArrowLeft, Calendar, Trophy, Users, Zap } from 'lucide-react'; import { useParams, useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; diff --git a/apps/website/app/sponsor/billing/page.tsx b/apps/website/app/sponsor/billing/page.tsx index eb82a6d68..23a5e4603 100644 --- a/apps/website/app/sponsor/billing/page.tsx +++ b/apps/website/app/sponsor/billing/page.tsx @@ -304,18 +304,6 @@ export default function SponsorBillingPage() { ); } - const handleSetDefault = (methodId: string) => { - setPaymentMethods(methods => - methods.map(m => ({ ...m, isDefault: m.id === methodId })) - ); - }; - - const handleRemoveMethod = (methodId: string) => { - if (confirm('Remove this payment method?')) { - setPaymentMethods(methods => methods.filter(m => m.id !== methodId)); - } - }; - const handleSetDefault = (methodId: string) => { // In a real app, this would call an API console.log('Setting default payment method:', methodId); diff --git a/apps/website/app/sponsor/leagues/[id]/page.tsx b/apps/website/app/sponsor/leagues/[id]/page.tsx index 174903d29..4aad0ece9 100644 --- a/apps/website/app/sponsor/leagues/[id]/page.tsx +++ b/apps/website/app/sponsor/leagues/[id]/page.tsx @@ -89,7 +89,8 @@ export default function SponsorLeagueDetailPage() { ); } - const config = data.league.tierConfig; + const league = data.league; + const config = league.tierConfig; return (
@@ -99,7 +100,7 @@ export default function SponsorLeagueDetailPage() { Leagues - {league.name} + {data.league.name}
{/* Header */} @@ -107,7 +108,7 @@ export default function SponsorLeagueDetailPage() {
- ⭐ {league.tier} + ⭐ {data.league.tier} Active Season diff --git a/apps/website/components/alpha/CompanionInstructions.tsx b/apps/website/components/alpha/CompanionInstructions.tsx index c30bab5cd..08d4d2942 100644 --- a/apps/website/components/alpha/CompanionInstructions.tsx +++ b/apps/website/components/alpha/CompanionInstructions.tsx @@ -1,9 +1,9 @@ 'use client'; +import { Race } from '@core/racing/domain/entities/Race'; // TODO forbidden core import import { useState } from 'react'; -import Card from '../ui/Card'; import Button from '../ui/Button'; -import { Race } from '@core/racing/domain/entities/Race'; +import Card from '../ui/Card'; interface CompanionInstructionsProps { race: Race; diff --git a/apps/website/components/drivers/DriverCard.tsx b/apps/website/components/drivers/DriverCard.tsx index 0be60a622..d104861f5 100644 --- a/apps/website/components/drivers/DriverCard.tsx +++ b/apps/website/components/drivers/DriverCard.tsx @@ -1,7 +1,7 @@ import Card from '@/components/ui/Card'; import RankBadge from '@/components/drivers/RankBadge'; import DriverIdentity from '@/components/drivers/DriverIdentity'; -import type { DriverDTO } from '@core/racing/application/dto/DriverDTO'; +import { DriverViewModel } from '@/lib/view-models/DriverViewModel'; export interface DriverCardProps { id: string; @@ -29,13 +29,11 @@ export default function DriverCard(props: DriverCardProps) { onClick, } = props; - const driver: DriverDTO = { + // Create a proper DriverViewModel instance + const driverViewModel = new DriverViewModel({ id, - iracingId: '', name, - country: nationality, - joinedAt: '', - }; + }); return (
{driver.name} void; } - + interface DriverProfileStatsViewModel { rating: number; wins: number; @@ -33,36 +30,49 @@ interface DriverProfileStatsViewModel { overallRank?: number; } -type DriverProfileOverviewViewModel = ProfileOverviewOutputPort | null; +interface DriverTeamViewModel { + team: { + name: string; + tag: string; + }; +} export default function DriverProfile({ driver, isOwnProfile = false, onEditClick }: DriverProfileProps) { - const [profileData, setProfileData] = useState(null); + const { driverService } = useServices(); + const [profileData, setProfileData] = useState(null); const [teamData, setTeamData] = useState(null); useEffect(() => { const load = async () => { - // Load profile data using GetProfileOverviewUseCase - const profileUseCase = getGetProfileOverviewUseCase(); - const profileViewModel = await profileUseCase.execute({ driverId: driver.id }); - setProfileData(profileViewModel); - - // Load team data using caller-owned presenter - const teamUseCase = getGetDriverTeamUseCase(); - const driverTeamPresenter = new DriverTeamPresenter(); - await teamUseCase.execute({ driverId: driver.id }, driverTeamPresenter); - const teamResult = driverTeamPresenter.getViewModel(); - setTeamData(teamResult ?? null); + try { + // Load driver profile + const profile = await driverService.getDriverProfile(driver.id); + + // Extract stats from profile + if (profile.stats) { + setProfileData(profile.stats); + } + + // Load team data if available + if (profile.teamMemberships && profile.teamMemberships.length > 0) { + const currentTeam = profile.teamMemberships.find(m => m.isCurrent) || profile.teamMemberships[0]; + setTeamData({ + team: { + name: currentTeam.teamName, + tag: currentTeam.teamTag ?? '' + } + }); + } + } catch (error) { + console.error('Failed to load driver profile data:', error); + } }; void load(); - }, [driver.id]); + }, [driver.id, driverService]); - const driverStats = profileData?.stats || null; - const primaryLeagueId = getPrimaryLeagueIdForDriver(driver.id); - const leagueRank = primaryLeagueId - ? getLeagueRankings(driver.id, primaryLeagueId) - : { rank: 0, totalDrivers: 0, percentile: 0 }; - const globalRank = profileData?.driver?.globalRank ?? null; - const totalDrivers = profileData?.driver?.totalDrivers ?? 0; + const driverStats = profileData; + const globalRank = profileData?.overallRank ?? null; + const totalDrivers = 1000; // Placeholder const performanceStats = driverStats ? { winRate: driverStats.totalRaces > 0 ? (driverStats.wins / driverStats.totalRaces) * 100 : 0, @@ -83,14 +93,6 @@ export default function DriverProfile({ driver, isOwnProfile = false, onEditClic percentile: driverStats.percentile ?? 0, rating: driverStats.rating ?? 0, }, - { - type: 'league' as const, - name: 'Primary League', - rank: leagueRank.rank, - totalDrivers: leagueRank.totalDrivers, - percentile: leagueRank.percentile, - rating: driverStats.rating ?? 0, - }, ] : []; return ( diff --git a/apps/website/components/drivers/HeroSection.tsx b/apps/website/components/drivers/HeroSection.tsx index 30e27d5cc..7fa23761c 100644 --- a/apps/website/components/drivers/HeroSection.tsx +++ b/apps/website/components/drivers/HeroSection.tsx @@ -1,6 +1,6 @@ -import { Users, Trophy, ChevronRight } from 'lucide-react'; import Heading from '@/components/ui/Heading'; -import Button from '@/components/ui/Button'; +import { Trophy, Users } from 'lucide-react'; +import Button from '../ui/Button'; interface HeroSectionProps { icon?: React.ElementType; diff --git a/apps/website/components/drivers/ProfileRaceHistory.tsx b/apps/website/components/drivers/ProfileRaceHistory.tsx index 50bae1b46..65ce313e2 100644 --- a/apps/website/components/drivers/ProfileRaceHistory.tsx +++ b/apps/website/components/drivers/ProfileRaceHistory.tsx @@ -4,9 +4,7 @@ import { useState, useEffect } from 'react'; import Card from '../ui/Card'; import Button from '../ui/Button'; import RaceResultCard from '../races/RaceResultCard'; -import { Race } from '@core/racing/domain/entities/Race'; -import { Result } from '@core/racing/domain/entities/Result'; -import { League } from '@core/racing/domain/entities/League'; +import { useServices } from '@/lib/services/ServiceProvider'; interface RaceHistoryProps { driverId: string; diff --git a/apps/website/components/drivers/ProfileSettings.tsx b/apps/website/components/drivers/ProfileSettings.tsx index 2b7f7bec6..516643616 100644 --- a/apps/website/components/drivers/ProfileSettings.tsx +++ b/apps/website/components/drivers/ProfileSettings.tsx @@ -1,7 +1,7 @@ 'use client'; import { useState } from 'react'; -import type { DriverDTO } from '@core/racing/application/dto/DriverDTO'; +import type { DriverDTO } from '@/lib/types/generated/DriverDTO'; import Card from '../ui/Card'; import Button from '../ui/Button'; import Input from '../ui/Input'; diff --git a/apps/website/components/drivers/ProfileStats.tsx b/apps/website/components/drivers/ProfileStats.tsx index 31213d3b1..cdb8eb336 100644 --- a/apps/website/components/drivers/ProfileStats.tsx +++ b/apps/website/components/drivers/ProfileStats.tsx @@ -4,7 +4,6 @@ import Card from '../ui/Card'; import RankBadge from './RankBadge'; import { useState, useEffect } from 'react'; import { getPrimaryLeagueIdForDriver } from '@/lib/leagueMembership'; -import type { ProfileOverviewOutputPort } from '@core/racing/application/ports/output/ProfileOverviewOutputPort'; interface ProfileStatsProps { driverId?: string; diff --git a/apps/website/components/leagues/CreateLeagueForm.tsx b/apps/website/components/leagues/CreateLeagueForm.tsx index 84027b5fb..22aa81cdc 100644 --- a/apps/website/components/leagues/CreateLeagueForm.tsx +++ b/apps/website/components/leagues/CreateLeagueForm.tsx @@ -4,7 +4,8 @@ import { useState, FormEvent } from 'react'; import { useRouter } from 'next/navigation'; import Input from '../ui/Input'; import Button from '../ui/Button'; -import { League } from '@core/racing/domain/entities/League'; +import { useServices } from '@/lib/services/ServiceProvider'; +import { useAuth } from '@/lib/auth/AuthContext'; interface FormErrors { name?: string; @@ -49,6 +50,9 @@ export default function CreateLeagueForm() { return Object.keys(newErrors).length === 0; }; + const { session } = useAuth(); + const { driverService, leagueService } = useServices(); + const handleSubmit = async (e: FormEvent) => { e.preventDefault(); @@ -56,12 +60,16 @@ export default function CreateLeagueForm() { if (!validateForm()) return; + if (!session?.user.userId) { + setErrors({ submit: 'You must be logged in to create a league' }); + return; + } + setLoading(true); try { - const driverRepo = getDriverRepository(); - const drivers = await driverRepo.findAll(); - const currentDriver = drivers[0]; + // Get current driver + const currentDriver = await driverService.getDriverProfile(session.user.userId); if (!currentDriver) { setErrors({ submit: 'No driver profile found. Please create a profile first.' }); @@ -69,22 +77,16 @@ export default function CreateLeagueForm() { return; } - const leagueRepo = getLeagueRepository(); - - const league = League.create({ - id: crypto.randomUUID(), + // Create league using the league service + const input = { name: formData.name.trim(), description: formData.description.trim(), - ownerId: currentDriver.id, - settings: { - pointsSystem: formData.pointsSystem, - sessionDuration: formData.sessionDuration, - qualifyingFormat: 'open', - }, - }); + visibility: 'public' as const, + ownerId: session.user.userId, + }; - await leagueRepo.create(league); - router.push(`/leagues/${league.id}`); + const result = await leagueService.createLeague(input); + router.push(`/leagues/${result.leagueId}`); router.refresh(); } catch (error) { setErrors({ diff --git a/apps/website/components/leagues/CreateLeagueWizard.tsx b/apps/website/components/leagues/CreateLeagueWizard.tsx index 2d40f9553..f862a2196 100644 --- a/apps/website/components/leagues/CreateLeagueWizard.tsx +++ b/apps/website/components/leagues/CreateLeagueWizard.tsx @@ -1,44 +1,46 @@ 'use client'; -import React, { useEffect, useState, FormEvent, useCallback } from 'react'; -import { useRouter } from 'next/navigation'; +import LeagueReviewSummary from '@/components/leagues/LeagueReviewSummary'; +import Button from '@/components/ui/Button'; +import Card from '@/components/ui/Card'; +import Heading from '@/components/ui/Heading'; +import Input from '@/components/ui/Input'; import { useAuth } from '@/lib/auth/AuthContext'; import { - FileText, - Users, - Calendar, - Trophy, + AlertCircle, Award, + Calendar, + Check, CheckCircle2, ChevronLeft, ChevronRight, + FileText, Loader2, - AlertCircle, - Sparkles, - Check, Scale, + Sparkles, + Trophy, + Users, } from 'lucide-react'; -import Card from '@/components/ui/Card'; -import Button from '@/components/ui/Button'; -import Heading from '@/components/ui/Heading'; -import LeagueReviewSummary from '@/components/leagues/LeagueReviewSummary'; -import Input from '@/components/ui/Input'; +import { useRouter } from 'next/navigation'; +import { FormEvent, useCallback, useEffect, useState } from 'react'; import { LeagueWizardCommandModel } from '@/lib/command-models/leagues/LeagueWizardCommandModel'; -import { LeagueWizardService } from '@/lib/services/leagues/LeagueWizardService'; -import type { LeagueScoringPresetDTO } from '@core/racing/application/ports/LeagueScoringPresetProvider'; -import type { LeagueConfigFormModel } from '@core/racing/application'; +import { useCreateLeagueWizard } from '@/hooks/useLeagueWizardService'; +import { useLeagueScoringPresets } from '@/hooks/useLeagueScoringPresets'; import { LeagueBasicsSection } from './LeagueBasicsSection'; -import { LeagueVisibilitySection } from './LeagueVisibilitySection'; -import { LeagueStructureSection } from './LeagueStructureSection'; -import { - LeagueScoringSection, - ScoringPatternSection, - ChampionshipsSection, -} from './LeagueScoringSection'; import { LeagueDropSection } from './LeagueDropSection'; -import { LeagueTimingsSection } from './LeagueTimingsSection'; +import { + ChampionshipsSection, + ScoringPatternSection +} from './LeagueScoringSection'; import { LeagueStewardingSection } from './LeagueStewardingSection'; +import { LeagueStructureSection } from './LeagueStructureSection'; +import { LeagueTimingsSection } from './LeagueTimingsSection'; +import { LeagueVisibilitySection } from './LeagueVisibilitySection'; +import type { LeagueConfigFormModel } from '@/lib/types/LeagueConfigFormModel'; +import type { LeagueScoringPresetDTO } from '@/lib/types/generated/LeagueScoringPresetDTO'; +import type { Weekday } from '@/lib/types/Weekday'; +import type { WizardErrors } from '@/lib/types/WizardErrors'; // ============================================================================ // LOCAL STORAGE PERSISTENCE @@ -47,6 +49,7 @@ import { LeagueStewardingSection } from './LeagueStewardingSection'; const STORAGE_KEY = 'gridpilot_league_wizard_draft'; const STORAGE_HIGHEST_STEP_KEY = 'gridpilot_league_wizard_highest_step'; +// TODO there is a better place for this function saveFormToStorage(form: LeagueWizardFormModel): void { try { localStorage.setItem(STORAGE_KEY, JSON.stringify(form)); @@ -55,6 +58,7 @@ function saveFormToStorage(form: LeagueWizardFormModel): void { } } +// TODO there is a better place for this function loadFormFromStorage(): LeagueWizardFormModel | null { try { const stored = localStorage.getItem(STORAGE_KEY); @@ -152,8 +156,6 @@ function stepToStepName(step: Step): StepName { } } -import { WizardErrors } from '@/lib/types/WizardErrors'; - function getDefaultSeasonStartDate(): string { // Default to next Saturday const now = new Date(); @@ -214,9 +216,8 @@ function createDefaultForm(): LeagueWizardFormModel { sessionCount: 2, roundsPlanned: 8, // Default to Saturday races, weekly, starting next week - weekdays: ['Sat'] as import('@gridpilot/racing/domain/types/Weekday').Weekday[], + weekdays: ['Sat'] as Weekday[], recurrenceStrategy: 'weekly' as const, - raceStartTime: '20:00', timezoneId: 'UTC', seasonStartDate: defaultSeasonStartDate, }, @@ -277,41 +278,93 @@ export default function CreateLeagueWizard({ stepName, onStepChange }: CreateLea } }, [step, isHydrated]); - useEffect(() => { - async function loadPresets() { - try { - const query = getListLeagueScoringPresetsQuery(); - const result = await query.execute(); - setPresets(result); - const firstPreset = result[0]; - if (firstPreset) { - setForm((prev) => ({ - ...prev, - scoring: { - ...prev.scoring, - patternId: prev.scoring.patternId || firstPreset.id, - customScoringEnabled: prev.scoring.customScoringEnabled ?? false, - }, - })); - } - } catch (err) { - setErrors((prev) => ({ - ...prev, - submit: - err instanceof Error - ? err.message - : 'Failed to load scoring presets', - })); - } finally { - setPresetsLoading(false); - } - } + // Use the react-query hook for scoring presets + const { data: queryPresets, error: presetsError } = useLeagueScoringPresets(); - loadPresets(); - }, []); + // Sync presets from query to local state + useEffect(() => { + if (queryPresets) { + setPresets(queryPresets); + const firstPreset = queryPresets[0]; + if (firstPreset && !form.scoring?.patternId) { + setForm((prev) => ({ + ...prev, + scoring: { + ...prev.scoring, + patternId: firstPreset.id, + customScoringEnabled: false, + }, + })); + } + setPresetsLoading(false); + } + }, [queryPresets, form.scoring?.patternId]); + + // Handle presets error + useEffect(() => { + if (presetsError) { + setErrors((prev) => ({ + ...prev, + submit: presetsError instanceof Error ? presetsError.message : 'Failed to load scoring presets', + })); + } + }, [presetsError]); + + // Use the create league mutation + const createLeagueMutation = useCreateLeagueWizard(); const validateStep = (currentStep: Step): boolean => { - const stepErrors = LeagueWizardCommandModel.validateLeagueWizardStep(form, currentStep); + // Convert form to LeagueWizardFormData for validation + const formData: any = { + leagueId: form.leagueId || '', + basics: { + name: form.basics?.name || '', + description: form.basics?.description || '', + visibility: (form.basics?.visibility as 'public' | 'private' | 'unlisted') || 'public', + gameId: form.basics?.gameId || 'iracing', + }, + structure: { + mode: (form.structure?.mode as 'solo' | 'fixedTeams') || 'solo', + maxDrivers: form.structure?.maxDrivers || 0, + maxTeams: form.structure?.maxTeams || 0, + driversPerTeam: form.structure?.driversPerTeam || 0, + }, + championships: { + enableDriverChampionship: form.championships?.enableDriverChampionship ?? true, + enableTeamChampionship: form.championships?.enableTeamChampionship ?? false, + enableNationsChampionship: form.championships?.enableNationsChampionship ?? false, + enableTrophyChampionship: form.championships?.enableTrophyChampionship ?? false, + }, + scoring: { + patternId: form.scoring?.patternId || '', + customScoringEnabled: form.scoring?.customScoringEnabled ?? false, + }, + dropPolicy: { + strategy: (form.dropPolicy?.strategy as 'none' | 'bestNResults' | 'dropWorstN') || 'bestNResults', + n: form.dropPolicy?.n || 6, + }, + timings: { + practiceMinutes: form.timings?.practiceMinutes || 0, + qualifyingMinutes: form.timings?.qualifyingMinutes || 0, + sprintRaceMinutes: form.timings?.sprintRaceMinutes || 0, + mainRaceMinutes: form.timings?.mainRaceMinutes || 0, + sessionCount: form.timings?.sessionCount || 0, + roundsPlanned: form.timings?.roundsPlanned || 0, + }, + stewarding: { + decisionMode: (form.stewarding?.decisionMode as 'owner_only' | 'admin_vote' | 'steward_panel') || 'admin_only', + requiredVotes: form.stewarding?.requiredVotes || 0, + requireDefense: form.stewarding?.requireDefense ?? false, + defenseTimeLimit: form.stewarding?.defenseTimeLimit || 0, + voteTimeLimit: form.stewarding?.voteTimeLimit || 0, + protestDeadlineHours: form.stewarding?.protestDeadlineHours || 0, + stewardingClosesHours: form.stewarding?.stewardingClosesHours || 0, + notifyAccusedOnProtest: form.stewarding?.notifyAccusedOnProtest ?? true, + notifyOnVoteRequired: form.stewarding?.notifyOnVoteRequired ?? true, + }, + }; + + const stepErrors = LeagueWizardCommandModel.validateLeagueWizardStep(formData, currentStep); setErrors((prev) => ({ ...prev, ...stepErrors, @@ -354,7 +407,57 @@ export default function CreateLeagueWizard({ stepName, onStepChange }: CreateLea return; } - const allErrors = LeagueWizardCommandModel.validateAllLeagueWizardSteps(form); + // Convert form to LeagueWizardFormData for validation + const formData: any = { + leagueId: form.leagueId || '', + basics: { + name: form.basics?.name || '', + description: form.basics?.description || '', + visibility: (form.basics?.visibility as 'public' | 'private' | 'unlisted') || 'public', + gameId: form.basics?.gameId || 'iracing', + }, + structure: { + mode: (form.structure?.mode as 'solo' | 'fixedTeams') || 'solo', + maxDrivers: form.structure?.maxDrivers || 0, + maxTeams: form.structure?.maxTeams || 0, + driversPerTeam: form.structure?.driversPerTeam || 0, + }, + championships: { + enableDriverChampionship: form.championships?.enableDriverChampionship ?? true, + enableTeamChampionship: form.championships?.enableTeamChampionship ?? false, + enableNationsChampionship: form.championships?.enableNationsChampionship ?? false, + enableTrophyChampionship: form.championships?.enableTrophyChampionship ?? false, + }, + scoring: { + patternId: form.scoring?.patternId || '', + customScoringEnabled: form.scoring?.customScoringEnabled ?? false, + }, + dropPolicy: { + strategy: (form.dropPolicy?.strategy as 'none' | 'bestNResults' | 'dropWorstN') || 'bestNResults', + n: form.dropPolicy?.n || 6, + }, + timings: { + practiceMinutes: form.timings?.practiceMinutes || 0, + qualifyingMinutes: form.timings?.qualifyingMinutes || 0, + sprintRaceMinutes: form.timings?.sprintRaceMinutes || 0, + mainRaceMinutes: form.timings?.mainRaceMinutes || 0, + sessionCount: form.timings?.sessionCount || 0, + roundsPlanned: form.timings?.roundsPlanned || 0, + }, + stewarding: { + decisionMode: (form.stewarding?.decisionMode as 'owner_only' | 'admin_vote' | 'steward_panel') || 'admin_only', + requiredVotes: form.stewarding?.requiredVotes || 0, + requireDefense: form.stewarding?.requireDefense ?? false, + defenseTimeLimit: form.stewarding?.defenseTimeLimit || 0, + voteTimeLimit: form.stewarding?.voteTimeLimit || 0, + protestDeadlineHours: form.stewarding?.protestDeadlineHours || 0, + stewardingClosesHours: form.stewarding?.stewardingClosesHours || 0, + notifyAccusedOnProtest: form.stewarding?.notifyAccusedOnProtest ?? true, + notifyOnVoteRequired: form.stewarding?.notifyOnVoteRequired ?? true, + }, + }; + + const allErrors = LeagueWizardCommandModel.validateAllLeagueWizardSteps(formData); setErrors((prev) => ({ ...prev, ...allErrors, @@ -372,9 +475,13 @@ export default function CreateLeagueWizard({ stepName, onStepChange }: CreateLea }); try { - const result = await LeagueWizardService.createLeagueFromConfig(form, ownerId); + // Use the mutation to create the league + const result = await createLeagueMutation.mutateAsync({ form, ownerId }); + // Clear the draft on successful creation clearFormStorage(); + + // Navigate to the new league router.push(`/leagues/${result.leagueId}`); } catch (err) { const message = @@ -387,12 +494,79 @@ export default function CreateLeagueWizard({ stepName, onStepChange }: CreateLea } }; - const currentPreset = - presets.find((p) => p.id === form.scoring.patternId) ?? null; - // Handler for scoring preset selection - delegates to application-level config helper const handleScoringPresetChange = (patternId: string) => { - setForm((prev) => LeagueWizardCommandModel.applyScoringPresetToConfig(prev, patternId)); + setForm((prev) => { + // Convert to LeagueWizardFormData for the command model + const formData: any = { + leagueId: prev.leagueId || '', + basics: { + name: prev.basics?.name || '', + description: prev.basics?.description || '', + visibility: (prev.basics?.visibility as 'public' | 'private' | 'unlisted') || 'public', + gameId: prev.basics?.gameId || 'iracing', + }, + structure: { + mode: (prev.structure?.mode as 'solo' | 'fixedTeams') || 'solo', + maxDrivers: prev.structure?.maxDrivers || 24, + maxTeams: prev.structure?.maxTeams || 0, + driversPerTeam: prev.structure?.driversPerTeam || 0, + }, + championships: { + enableDriverChampionship: prev.championships?.enableDriverChampionship ?? true, + enableTeamChampionship: prev.championships?.enableTeamChampionship ?? false, + enableNationsChampionship: prev.championships?.enableNationsChampionship ?? false, + enableTrophyChampionship: prev.championships?.enableTrophyChampionship ?? false, + }, + scoring: { + patternId: prev.scoring?.patternId, + customScoringEnabled: prev.scoring?.customScoringEnabled ?? false, + }, + dropPolicy: { + strategy: (prev.dropPolicy?.strategy as 'none' | 'bestNResults' | 'dropWorstN') || 'bestNResults', + n: prev.dropPolicy?.n || 6, + }, + timings: { + practiceMinutes: prev.timings?.practiceMinutes || 0, + qualifyingMinutes: prev.timings?.qualifyingMinutes || 0, + sprintRaceMinutes: prev.timings?.sprintRaceMinutes || 0, + mainRaceMinutes: prev.timings?.mainRaceMinutes || 0, + sessionCount: prev.timings?.sessionCount || 0, + roundsPlanned: prev.timings?.roundsPlanned || 0, + raceDayOfWeek: prev.timings?.raceDayOfWeek || 0, + raceTimeUtc: prev.timings?.raceTimeUtc || '', + weekdays: (prev.timings?.weekdays as Weekday[]) || [], + recurrenceStrategy: prev.timings?.recurrenceStrategy || '', + timezoneId: prev.timings?.timezoneId || '', + seasonStartDate: prev.timings?.seasonStartDate || '', + }, + stewarding: { + decisionMode: (prev.stewarding?.decisionMode as 'owner_only' | 'admin_vote' | 'steward_panel') || 'admin_only', + requiredVotes: prev.stewarding?.requiredVotes || 2, + requireDefense: prev.stewarding?.requireDefense ?? false, + defenseTimeLimit: prev.stewarding?.defenseTimeLimit || 48, + voteTimeLimit: prev.stewarding?.voteTimeLimit || 72, + protestDeadlineHours: prev.stewarding?.protestDeadlineHours || 48, + stewardingClosesHours: prev.stewarding?.stewardingClosesHours || 168, + notifyAccusedOnProtest: prev.stewarding?.notifyAccusedOnProtest ?? true, + notifyOnVoteRequired: prev.stewarding?.notifyOnVoteRequired ?? true, + }, + }; + + const updated = LeagueWizardCommandModel.applyScoringPresetToConfig(formData, patternId); + + // Convert back to LeagueWizardFormModel + return { + basics: updated.basics, + structure: updated.structure, + championships: updated.championships, + scoring: updated.scoring, + dropPolicy: updated.dropPolicy, + timings: updated.timings, + stewarding: updated.stewarding, + seasonName: prev.seasonName, + } as LeagueWizardFormModel; + }); }; const steps = [ @@ -723,7 +897,7 @@ export default function CreateLeagueWizard({ stepName, onStepChange }: CreateLea
{/* Scoring Pattern Selection */} - - + +
{errors.submit && ( diff --git a/apps/website/components/leagues/LeagueBasicsSection.tsx b/apps/website/components/leagues/LeagueBasicsSection.tsx index 02a888a5c..bf3a1372d 100644 --- a/apps/website/components/leagues/LeagueBasicsSection.tsx +++ b/apps/website/components/leagues/LeagueBasicsSection.tsx @@ -3,7 +3,7 @@ import React from 'react'; import { FileText, Gamepad2, AlertCircle, Check } from 'lucide-react'; import Input from '@/components/ui/Input'; -import type { LeagueConfigFormModel } from '@core/racing/application'; +import type { LeagueConfigFormModel } from '@/lib/types/LeagueConfigFormModel'; interface LeagueBasicsSectionProps { form: LeagueConfigFormModel; diff --git a/apps/website/components/leagues/LeagueCard.tsx b/apps/website/components/leagues/LeagueCard.tsx index 22c9d0bd2..974379028 100644 --- a/apps/website/components/leagues/LeagueCard.tsx +++ b/apps/website/components/leagues/LeagueCard.tsx @@ -12,8 +12,9 @@ import { ChevronRight, Sparkles, } from 'lucide-react'; -import type { LeagueSummaryViewModel } from '@core/racing/application/presenters/IAllLeaguesWithCapacityAndScoringPresenter'; +import type { LeagueSummaryViewModel } from '@/lib/view-models/LeagueSummaryViewModel'; import { getLeagueCoverClasses } from '@/lib/leagueCovers'; +import { useServices } from '@/lib/services/ServiceProvider'; interface LeagueCardProps { league: LeagueSummaryViewModel; @@ -71,9 +72,9 @@ function isNewLeague(createdAt: string | Date): boolean { } export default function LeagueCard({ league, onClick }: LeagueCardProps) { - const imageService = getImageService(); - const coverUrl = imageService.getLeagueCover(league.id); - const logoUrl = imageService.getLeagueLogo(league.id); + const { mediaService } = useServices(); + const coverUrl = mediaService.getLeagueCover(league.id); + const logoUrl = mediaService.getLeagueLogo(league.id); const ChampionshipIcon = getChampionshipIcon(league.scoring?.primaryChampionshipType); const championshipLabel = getChampionshipLabel(league.scoring?.primaryChampionshipType); diff --git a/apps/website/components/leagues/LeagueDropSection.tsx b/apps/website/components/leagues/LeagueDropSection.tsx index b036fdb0d..7a62bf02b 100644 --- a/apps/website/components/leagues/LeagueDropSection.tsx +++ b/apps/website/components/leagues/LeagueDropSection.tsx @@ -3,7 +3,7 @@ import React, { useState, useRef, useEffect } from 'react'; import { TrendingDown, Check, HelpCircle, X, Zap } from 'lucide-react'; import { createPortal } from 'react-dom'; -import type { LeagueConfigFormModel } from '@core/racing/application'; +import type { LeagueConfigFormModel } from '@/lib/types/LeagueConfigFormModel'; // ============================================================================ // INFO FLYOUT (duplicated for self-contained component) @@ -245,7 +245,7 @@ export function LeagueDropSection({ readOnly, }: LeagueDropSectionProps) { const disabled = readOnly || !onChange; - const dropPolicy = form.dropPolicy; + const dropPolicy = form.dropPolicy || { strategy: 'none' as const }; const [showDropFlyout, setShowDropFlyout] = useState(false); const [activeDropRuleFlyout, setActiveDropRuleFlyout] = useState(null); const dropInfoRef = useRef(null); diff --git a/apps/website/components/leagues/LeagueMembers.tsx b/apps/website/components/leagues/LeagueMembers.tsx index 997ab3516..371ffb075 100644 --- a/apps/website/components/leagues/LeagueMembers.tsx +++ b/apps/website/components/leagues/LeagueMembers.tsx @@ -3,8 +3,8 @@ import DriverIdentity from '../drivers/DriverIdentity'; import { useEffectiveDriverId } from '../../hooks/useEffectiveDriverId'; import { useServices } from '../../lib/services/ServiceProvider'; -import type { LeagueMembership } from '@core/racing/domain/entities/LeagueMembership'; -import type { MembershipRole } from '@core/racing/domain/entities/MembershipRole'; +import type { LeagueMembership } from '@/lib/types/LeagueMembership'; +import type { MembershipRole } from '@/lib/types/MembershipRole'; import type { DriverDTO } from '@/lib/types/generated/DriverDTO'; import { useCallback, useEffect, useState } from 'react'; @@ -66,10 +66,16 @@ export default function LeagueMembers({ }; const getRoleOrder = (role: MembershipRole): number => { - const order = { owner: 0, admin: 1, steward: 2, member: 3 }; + const order: Record = { owner: 0, admin: 1, steward: 2, member: 3 }; return order[role]; }; + const getDriverStats = (driverId: string): { rating: number; wins: number; overallRank: number } | null => { + // This would typically come from a driver stats service + // For now, return null as the original implementation was missing + return null; + }; + const sortedMembers = [...members].sort((a, b) => { switch (sortBy) { case 'role': @@ -105,6 +111,8 @@ export default function LeagueMembers({ return 'bg-blue-500/10 text-blue-400 border-blue-500/30'; case 'member': return 'bg-primary-blue/10 text-primary-blue border-primary-blue/30'; + default: + return 'bg-gray-500/10 text-gray-400 border-gray-500/30'; } }; diff --git a/apps/website/components/leagues/LeagueScoringSection.tsx b/apps/website/components/leagues/LeagueScoringSection.tsx index f085c26c3..1bd8e2abd 100644 --- a/apps/website/components/leagues/LeagueScoringSection.tsx +++ b/apps/website/components/leagues/LeagueScoringSection.tsx @@ -3,8 +3,8 @@ import React, { useState, useRef, useEffect } from 'react'; import { Trophy, Award, Check, Zap, Settings, Globe, Medal, Plus, Minus, RotateCcw, HelpCircle, X } from 'lucide-react'; import { createPortal } from 'react-dom'; -import type { LeagueScoringPresetDTO } from '@core/racing/application/ports/LeagueScoringPresetProvider'; -import type { LeagueConfigFormModel } from '@core/racing/application'; +import type { LeagueScoringPresetDTO } from '@/hooks/useLeagueScoringPresets'; +import type { LeagueConfigFormModel } from '@/lib/types/LeagueConfigFormModel'; // ============================================================================ // INFO FLYOUT COMPONENT diff --git a/apps/website/components/leagues/LeagueSponsorshipsSection.tsx b/apps/website/components/leagues/LeagueSponsorshipsSection.tsx index 9a7cee861..46d92bfd3 100644 --- a/apps/website/components/leagues/LeagueSponsorshipsSection.tsx +++ b/apps/website/components/leagues/LeagueSponsorshipsSection.tsx @@ -7,7 +7,7 @@ import Button from '../ui/Button'; import Input from '../ui/Input'; import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId'; -import { PendingSponsorshipRequestsPresenter } from '@/lib/presenters/PendingSponsorshipRequestsPresenter'; +import { useServices } from '@/lib/services/ServiceProvider'; interface SponsorshipSlot { tier: 'main' | 'secondary'; @@ -29,6 +29,7 @@ export function LeagueSponsorshipsSection({ readOnly = false }: LeagueSponsorshipsSectionProps) { const currentDriverId = useEffectiveDriverId(); + const { sponsorshipService } = useServices(); const [slots, setSlots] = useState([ { tier: 'main', price: 500, isOccupied: false }, { tier: 'secondary', price: 200, isOccupied: false }, diff --git a/apps/website/components/leagues/LeagueStructureSection.tsx b/apps/website/components/leagues/LeagueStructureSection.tsx index 8205173ab..72724708e 100644 --- a/apps/website/components/leagues/LeagueStructureSection.tsx +++ b/apps/website/components/leagues/LeagueStructureSection.tsx @@ -4,8 +4,7 @@ import { User, Users2, Info, Check, HelpCircle, X } from 'lucide-react'; import { useState, useRef, useEffect, useMemo } from 'react'; import { createPortal } from 'react-dom'; import Input from '@/components/ui/Input'; -import type { LeagueConfigFormModel } from '@core/racing/application'; -import { GameConstraints } from '@core/racing/domain/value-objects/GameConstraints'; +import type { LeagueConfigFormModel } from '@/lib/types/LeagueConfigFormModel'; // ============================================================================ // INFO FLYOUT COMPONENT @@ -233,8 +232,16 @@ export function LeagueStructureSection({ // Get game-specific constraints const gameConstraints = useMemo( - () => GameConstraints.forGame(form.basics.gameId), - [form.basics.gameId] + () => ({ + minDrivers: 1, + maxDrivers: 100, + defaultMaxDrivers: 24, + minTeams: 1, + maxTeams: 50, + minDriversPerTeam: 1, + maxDriversPerTeam: 10, + }), + [form.basics?.gameId] ); return ( diff --git a/apps/website/components/leagues/LeagueTimingsSection.tsx b/apps/website/components/leagues/LeagueTimingsSection.tsx index 6dca997bd..9178dceb1 100644 --- a/apps/website/components/leagues/LeagueTimingsSection.tsx +++ b/apps/website/components/leagues/LeagueTimingsSection.tsx @@ -17,13 +17,9 @@ import { Globe, MapPin, Pencil, - Link2, } from 'lucide-react'; -import type { - LeagueConfigFormModel, - LeagueSchedulePreviewDTO, -} from '@core/racing/application'; -import type { Weekday } from '@core/racing/domain/types/Weekday'; +import type { LeagueConfigFormModel } from '@/lib/types/LeagueConfigFormModel'; +import type { Weekday } from '@/lib/types/Weekday'; import Input from '@/components/ui/Input'; import RangeField from '@/components/ui/RangeField'; diff --git a/apps/website/components/leagues/LeagueVisibilitySection.tsx b/apps/website/components/leagues/LeagueVisibilitySection.tsx index 607504de0..8a9b49e97 100644 --- a/apps/website/components/leagues/LeagueVisibilitySection.tsx +++ b/apps/website/components/leagues/LeagueVisibilitySection.tsx @@ -3,7 +3,7 @@ import { Trophy, Users, Check, HelpCircle, X } from 'lucide-react'; import { useState, useRef, useEffect } from 'react'; import { createPortal } from 'react-dom'; -import type { LeagueConfigFormModel } from '@core/racing/application'; +import type { LeagueConfigFormModel } from '@/lib/types/LeagueConfigFormModel'; // Minimum drivers for ranked leagues const MIN_RANKED_DRIVERS = 10; @@ -132,14 +132,14 @@ export function LeagueVisibilitySection({ const unrankedInfoRef = useRef(null); // Normalize visibility to new terminology - const isRanked = basics.visibility === 'ranked' || basics.visibility === 'public'; + const isRanked = basics.visibility === 'public'; // Auto-update minDrivers when switching to ranked - const handleVisibilityChange = (visibility: 'ranked' | 'unranked') => { + const handleVisibilityChange = (visibility: 'public' | 'private' | 'unlisted') => { if (!onChange) return; - // If switching to ranked and current maxDrivers is below minimum, update it - if (visibility === 'ranked' && form.structure.maxDrivers < MIN_RANKED_DRIVERS) { + // If switching to public and current maxDrivers is below minimum, update it + if (visibility === 'public' && (form.structure?.maxDrivers ?? 0) < MIN_RANKED_DRIVERS) { onChange({ ...form, basics: { ...form.basics, visibility }, @@ -172,7 +172,7 @@ export function LeagueVisibilitySection({