refactor page to use services
This commit is contained in:
71
apps/website/lib/api/dashboard/DashboardApiClient.ts
Normal file
71
apps/website/lib/api/dashboard/DashboardApiClient.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { BaseApiClient } from '../base/BaseApiClient';
|
||||
|
||||
// DTOs
|
||||
export type DriverDto = {
|
||||
id: string;
|
||||
name: string;
|
||||
avatarUrl: string;
|
||||
country: string;
|
||||
totalRaces: number;
|
||||
wins: number;
|
||||
podiums: number;
|
||||
rating: number;
|
||||
globalRank: number;
|
||||
consistency: number;
|
||||
};
|
||||
|
||||
export type RaceDto = {
|
||||
id: string;
|
||||
track: string;
|
||||
car: string;
|
||||
scheduledAt: string; // ISO date string
|
||||
isMyLeague: boolean;
|
||||
leagueName?: string;
|
||||
};
|
||||
|
||||
export type LeagueStandingDto = {
|
||||
leagueId: string;
|
||||
leagueName: string;
|
||||
position: number;
|
||||
points: number;
|
||||
totalDrivers: number;
|
||||
};
|
||||
|
||||
export type FeedItemDto = {
|
||||
id: string;
|
||||
type: string;
|
||||
headline: string;
|
||||
body: string | null;
|
||||
timestamp: string; // ISO date string
|
||||
ctaHref?: string;
|
||||
ctaLabel?: string;
|
||||
};
|
||||
|
||||
export type FriendDto = {
|
||||
id: string;
|
||||
name: string;
|
||||
avatarUrl: string;
|
||||
country: string;
|
||||
};
|
||||
|
||||
export type DashboardOverviewDto = {
|
||||
currentDriver: DriverDto;
|
||||
nextRace: RaceDto | null;
|
||||
upcomingRaces: RaceDto[];
|
||||
leagueStandings: LeagueStandingDto[];
|
||||
feedItems: FeedItemDto[];
|
||||
friends: FriendDto[];
|
||||
activeLeaguesCount: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Dashboard API Client
|
||||
*
|
||||
* Handles dashboard overview data aggregation.
|
||||
*/
|
||||
export class DashboardApiClient extends BaseApiClient {
|
||||
/** Get dashboard overview data */
|
||||
getDashboardOverview(): Promise<DashboardOverviewDto> {
|
||||
return this.get<DashboardOverviewDto>('/dashboard/overview');
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { BaseApiClient } from '../base/BaseApiClient';
|
||||
// Import generated types
|
||||
import type { CompleteOnboardingInputDTO, CompleteOnboardingOutputDTO, DriverRegistrationStatusDTO, DriverLeaderboardItemDTO } from '../../types/generated';
|
||||
import type { CompleteOnboardingInputDTO, CompleteOnboardingOutputDTO, DriverRegistrationStatusDTO, DriverLeaderboardItemDTO, DriverProfileDTO } from '../../types/generated';
|
||||
|
||||
// TODO: Create proper DriverDTO in generated types
|
||||
type DriverDTO = {
|
||||
@@ -40,4 +40,14 @@ export class DriversApiClient extends BaseApiClient {
|
||||
getRegistrationStatus(driverId: string, raceId: string): Promise<DriverRegistrationStatusDTO> {
|
||||
return this.get<DriverRegistrationStatusDTO>(`/drivers/${driverId}/races/${raceId}/registration-status`);
|
||||
}
|
||||
|
||||
/** Get driver by ID */
|
||||
getDriver(driverId: string): Promise<DriverDTO | null> {
|
||||
return this.get<DriverDTO | null>(`/drivers/${driverId}`);
|
||||
}
|
||||
|
||||
/** Get driver profile with full details */
|
||||
getDriverProfile(driverId: string): Promise<DriverProfileDTO> {
|
||||
return this.get<DriverProfileDTO>(`/drivers/${driverId}/profile`);
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { MediaApiClient } from './media/MediaApiClient';
|
||||
import { AnalyticsApiClient } from './analytics/AnalyticsApiClient';
|
||||
import { AuthApiClient } from './auth/AuthApiClient';
|
||||
import { PaymentsApiClient } from './payments/PaymentsApiClient';
|
||||
import { DashboardApiClient } from './dashboard/DashboardApiClient';
|
||||
|
||||
/**
|
||||
* Main API Client
|
||||
@@ -23,6 +24,7 @@ export class ApiClient {
|
||||
public readonly analytics: AnalyticsApiClient;
|
||||
public readonly auth: AuthApiClient;
|
||||
public readonly payments: PaymentsApiClient;
|
||||
public readonly dashboard: DashboardApiClient;
|
||||
|
||||
constructor(baseUrl: string) {
|
||||
this.leagues = new LeaguesApiClient(baseUrl);
|
||||
@@ -34,6 +36,7 @@ export class ApiClient {
|
||||
this.analytics = new AnalyticsApiClient(baseUrl);
|
||||
this.auth = new AuthApiClient(baseUrl);
|
||||
this.payments = new PaymentsApiClient(baseUrl);
|
||||
this.dashboard = new DashboardApiClient(baseUrl);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -49,4 +49,32 @@ export class LeaguesApiClient extends BaseApiClient {
|
||||
removeMember(leagueId: string, performerDriverId: string, targetDriverId: string): Promise<{ success: boolean }> {
|
||||
return this.patch<{ success: boolean }>(`/leagues/${leagueId}/members/${targetDriverId}/remove`, { performerDriverId });
|
||||
}
|
||||
|
||||
/** Get league seasons */
|
||||
getSeasons(leagueId: string): Promise<{ seasons: Array<{ id: string; status: string }> }> {
|
||||
return this.get<{ seasons: Array<{ id: string; status: string }> }>(`/leagues/${leagueId}/seasons`);
|
||||
}
|
||||
|
||||
/** Get season sponsorships */
|
||||
getSeasonSponsorships(seasonId: string): Promise<{ sponsorships: Array<{ sponsorId: string; tier: string; status: string }> }> {
|
||||
return this.get<{ sponsorships: Array<{ sponsorId: string; tier: string; status: string }> }>(`/seasons/${seasonId}/sponsorships`);
|
||||
}
|
||||
|
||||
/** Get league config */
|
||||
getLeagueConfig(leagueId: string): Promise<{ config: any }> {
|
||||
return this.get<{ config: any }>(`/leagues/${leagueId}/config`);
|
||||
}
|
||||
|
||||
/** Get league scoring presets */
|
||||
getScoringPresets(): Promise<{ presets: any[] }> {
|
||||
return this.get<{ presets: any[] }>(`/leagues/scoring-presets`);
|
||||
}
|
||||
|
||||
/** Transfer league ownership */
|
||||
transferOwnership(leagueId: string, currentOwnerId: string, newOwnerId: string): Promise<{ success: boolean }> {
|
||||
return this.post<{ success: boolean }>(`/leagues/${leagueId}/transfer-ownership`, {
|
||||
currentOwnerId,
|
||||
newOwnerId,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,15 @@
|
||||
import { BaseApiClient } from '../base/BaseApiClient';
|
||||
import type {
|
||||
RequestAvatarGenerationInputDto,
|
||||
RequestAvatarGenerationOutputDto,
|
||||
UploadMediaInputDto,
|
||||
UploadMediaOutputDto,
|
||||
GetMediaOutputDto,
|
||||
DeleteMediaOutputDto,
|
||||
GetAvatarOutputDto,
|
||||
UpdateAvatarInputDto,
|
||||
UpdateAvatarOutputDto,
|
||||
DeleteMediaOutputDto,
|
||||
GetMediaOutputDto,
|
||||
RequestAvatarGenerationInputDto,
|
||||
RequestAvatarGenerationOutputDto,
|
||||
UpdateAvatarInputDto,
|
||||
UpdateAvatarOutputDto,
|
||||
UploadMediaInputDto,
|
||||
UploadMediaOutputDto,
|
||||
} from '../../dtos';
|
||||
import type { GetAvatarOutputDto } from '../../types/GetAvatarOutputDto';
|
||||
import { BaseApiClient } from '../base/BaseApiClient';
|
||||
|
||||
/**
|
||||
* Media API Client
|
||||
|
||||
33
apps/website/lib/api/protests/ProtestsApiClient.ts
Normal file
33
apps/website/lib/api/protests/ProtestsApiClient.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { BaseApiClient } from '../base/BaseApiClient';
|
||||
import type {
|
||||
LeagueAdminProtestsDTO,
|
||||
ApplyPenaltyCommandDTO,
|
||||
RequestProtestDefenseCommandDTO,
|
||||
} from '../../types';
|
||||
|
||||
/**
|
||||
* Protests API Client
|
||||
*
|
||||
* Handles all protest-related API operations.
|
||||
*/
|
||||
export class ProtestsApiClient extends BaseApiClient {
|
||||
/** Get protests for a league */
|
||||
getLeagueProtests(leagueId: string): Promise<LeagueAdminProtestsDTO> {
|
||||
return this.get<LeagueAdminProtestsDTO>(`/leagues/${leagueId}/protests`);
|
||||
}
|
||||
|
||||
/** Get a specific protest for a league */
|
||||
getLeagueProtest(leagueId: string, protestId: string): Promise<LeagueAdminProtestsDTO> {
|
||||
return this.get<LeagueAdminProtestsDTO>(`/leagues/${leagueId}/protests/${protestId}`);
|
||||
}
|
||||
|
||||
/** Apply a penalty */
|
||||
applyPenalty(input: ApplyPenaltyCommandDTO): Promise<void> {
|
||||
return this.post<void>('/races/penalties/apply', input);
|
||||
}
|
||||
|
||||
/** Request protest defense */
|
||||
requestDefense(input: RequestProtestDefenseCommandDTO): Promise<void> {
|
||||
return this.post<void>('/races/protests/defense/request', input);
|
||||
}
|
||||
}
|
||||
@@ -39,4 +39,9 @@ export class SponsorsApiClient extends BaseApiClient {
|
||||
getSponsorships(sponsorId: string): Promise<SponsorSponsorshipsDTO | null> {
|
||||
return this.get<SponsorSponsorshipsDTO | null>(`/sponsors/${sponsorId}/sponsorships`);
|
||||
}
|
||||
|
||||
/** Get sponsor by ID */
|
||||
getSponsor(sponsorId: string): Promise<SponsorDTO | null> {
|
||||
return this.get<SponsorDTO | null>(`/sponsors/${sponsorId}`);
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,16 @@
|
||||
import { BaseApiClient } from '../base/BaseApiClient';
|
||||
import { LeagueMemberDTO } from '@/lib/types/generated/LeagueMemberDTO';
|
||||
import type {
|
||||
AllTeamsDto,
|
||||
TeamDetailsDto,
|
||||
TeamMembersDto,
|
||||
TeamJoinRequestsDto,
|
||||
CreateTeamInputDto,
|
||||
CreateTeamOutputDto,
|
||||
DriverTeamDto,
|
||||
TeamDetailsDto,
|
||||
TeamJoinRequestsDto,
|
||||
TeamMembersDto,
|
||||
UpdateTeamInputDto,
|
||||
UpdateTeamOutputDto,
|
||||
DriverTeamDto,
|
||||
} from '../../dtos';
|
||||
import { BaseApiClient } from '../base/BaseApiClient';
|
||||
|
||||
/**
|
||||
* Teams API Client
|
||||
@@ -51,4 +52,9 @@ export class TeamsApiClient extends BaseApiClient {
|
||||
getDriverTeam(driverId: string): Promise<DriverTeamDto | null> {
|
||||
return this.get<DriverTeamDto | null>(`/teams/driver/${driverId}`);
|
||||
}
|
||||
|
||||
/** Get membership for a driver in a team */
|
||||
getMembership(teamId: string, driverId: string): Promise<LeagueMemberDTO | null> {
|
||||
return this.get<LeagueMemberDTO | null>(`/teams/${teamId}/members/${driverId}`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { ApplyPenaltyCommandDTO } from '../../types';
|
||||
|
||||
export type PenaltyType = 'time_penalty' | 'grid_penalty' | 'points_deduction' | 'disqualification' | 'warning' | 'license_points';
|
||||
|
||||
export interface ProtestDecisionData {
|
||||
decision: 'uphold' | 'dismiss' | null;
|
||||
penaltyType: PenaltyType;
|
||||
penaltyValue: number;
|
||||
stewardNotes: string;
|
||||
}
|
||||
|
||||
export class ProtestDecisionCommandModel {
|
||||
decision: 'uphold' | 'dismiss' | null = null;
|
||||
penaltyType: PenaltyType = 'time_penalty';
|
||||
penaltyValue: number = 5;
|
||||
stewardNotes: string = '';
|
||||
|
||||
constructor(initial: Partial<ProtestDecisionData> = {}) {
|
||||
this.decision = initial.decision ?? null;
|
||||
this.penaltyType = initial.penaltyType ?? 'time_penalty';
|
||||
this.penaltyValue = initial.penaltyValue ?? 5;
|
||||
this.stewardNotes = initial.stewardNotes ?? '';
|
||||
}
|
||||
|
||||
get isValid(): boolean {
|
||||
return this.decision !== null && this.stewardNotes.trim().length > 0;
|
||||
}
|
||||
|
||||
get canSubmit(): boolean {
|
||||
return this.isValid;
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
this.decision = null;
|
||||
this.penaltyType = 'time_penalty';
|
||||
this.penaltyValue = 5;
|
||||
this.stewardNotes = '';
|
||||
}
|
||||
|
||||
toApplyPenaltyCommand(raceId: string, driverId: string, stewardId: string, protestId: string): ApplyPenaltyCommandDTO {
|
||||
return {
|
||||
raceId,
|
||||
driverId,
|
||||
stewardId,
|
||||
type: this.penaltyType,
|
||||
value: this.getPenaltyValue(),
|
||||
reason: 'Protest upheld', // TODO: Make this configurable
|
||||
protestId,
|
||||
notes: this.stewardNotes,
|
||||
};
|
||||
}
|
||||
|
||||
private getPenaltyValue(): number {
|
||||
// Some penalties don't require a value
|
||||
const penaltiesWithoutValue: PenaltyType[] = ['disqualification', 'warning'];
|
||||
return penaltiesWithoutValue.includes(this.penaltyType) ? 0 : this.penaltyValue;
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,8 @@ import { PaymentsApiClient } from '../api/payments/PaymentsApiClient';
|
||||
import { AuthApiClient } from '../api/auth/AuthApiClient';
|
||||
import { AnalyticsApiClient } from '../api/analytics/AnalyticsApiClient';
|
||||
import { MediaApiClient } from '../api/media/MediaApiClient';
|
||||
import { DashboardApiClient } from '../api/dashboard/DashboardApiClient';
|
||||
import { ProtestsApiClient } from '../api/protests/ProtestsApiClient';
|
||||
import { ConsoleErrorReporter } from '../infrastructure/logging/ConsoleErrorReporter';
|
||||
import { ConsoleLogger } from '../infrastructure/logging/ConsoleLogger';
|
||||
|
||||
@@ -19,17 +21,20 @@ import { TeamService } from './teams/TeamService';
|
||||
import { TeamJoinService } from './teams/TeamJoinService';
|
||||
import { LeagueService } from './leagues/LeagueService';
|
||||
import { LeagueMembershipService } from './leagues/LeagueMembershipService';
|
||||
import { LeagueSettingsService } from './leagues/LeagueSettingsService';
|
||||
import { SponsorService } from './sponsors/SponsorService';
|
||||
import { SponsorshipService } from './sponsors/SponsorshipService';
|
||||
import { PaymentService } from './payments/PaymentService';
|
||||
import { AnalyticsService } from './analytics/AnalyticsService';
|
||||
import { DashboardService } from './analytics/DashboardService';
|
||||
import { DashboardService as AnalyticsDashboardService } from './analytics/DashboardService';
|
||||
import { DashboardService } from './dashboard/DashboardService';
|
||||
import { MediaService } from './media/MediaService';
|
||||
import { AvatarService } from './media/AvatarService';
|
||||
import { WalletService } from './payments/WalletService';
|
||||
import { MembershipFeeService } from './payments/MembershipFeeService';
|
||||
import { AuthService } from './auth/AuthService';
|
||||
import { SessionService } from './auth/SessionService';
|
||||
import { ProtestService } from './protests/ProtestService';
|
||||
|
||||
/**
|
||||
* ServiceFactory - Composition root for all services
|
||||
@@ -52,6 +57,8 @@ export class ServiceFactory {
|
||||
auth: AuthApiClient;
|
||||
analytics: AnalyticsApiClient;
|
||||
media: MediaApiClient;
|
||||
dashboard: DashboardApiClient;
|
||||
protests: ProtestsApiClient;
|
||||
};
|
||||
|
||||
constructor(baseUrl: string) {
|
||||
@@ -66,6 +73,8 @@ export class ServiceFactory {
|
||||
auth: new AuthApiClient(baseUrl, this.errorReporter, this.logger),
|
||||
analytics: new AnalyticsApiClient(baseUrl, this.errorReporter, this.logger),
|
||||
media: new MediaApiClient(baseUrl, this.errorReporter, this.logger),
|
||||
dashboard: new DashboardApiClient(baseUrl, this.errorReporter, this.logger),
|
||||
protests: new ProtestsApiClient(baseUrl, this.errorReporter, this.logger),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -115,15 +124,22 @@ export class ServiceFactory {
|
||||
* Create LeagueService instance
|
||||
*/
|
||||
createLeagueService(): LeagueService {
|
||||
return new LeagueService(this.apiClients.leagues);
|
||||
return new LeagueService(this.apiClients.leagues, this.apiClients.drivers, this.apiClients.sponsors, this.apiClients.races);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create LeagueMembershipService instance
|
||||
*/
|
||||
createLeagueMembershipService(): LeagueMembershipService {
|
||||
return new LeagueMembershipService(this.apiClients.leagues);
|
||||
}
|
||||
* Create LeagueMembershipService instance
|
||||
*/
|
||||
createLeagueMembershipService(): LeagueMembershipService {
|
||||
return new LeagueMembershipService(this.apiClients.leagues);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create LeagueSettingsService instance
|
||||
*/
|
||||
createLeagueSettingsService(): LeagueSettingsService {
|
||||
return new LeagueSettingsService(this.apiClients.leagues, this.apiClients.drivers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create SponsorService instance
|
||||
@@ -154,11 +170,18 @@ export class ServiceFactory {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create DashboardService instance
|
||||
*/
|
||||
createDashboardService(): DashboardService {
|
||||
return new DashboardService(this.apiClients.analytics);
|
||||
}
|
||||
* Create Analytics DashboardService instance
|
||||
*/
|
||||
createAnalyticsDashboardService(): AnalyticsDashboardService {
|
||||
return new AnalyticsDashboardService(this.apiClients.analytics);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create DashboardService instance
|
||||
*/
|
||||
createDashboardService(): DashboardService {
|
||||
return new DashboardService(this.apiClients.dashboard);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create MediaService instance
|
||||
@@ -201,4 +224,11 @@ export class ServiceFactory {
|
||||
createSessionService(): SessionService {
|
||||
return new SessionService(this.apiClients.auth);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create ProtestService instance
|
||||
*/
|
||||
createProtestService(): ProtestService {
|
||||
return new ProtestService(this.apiClients.protests);
|
||||
}
|
||||
}
|
||||
22
apps/website/lib/services/dashboard/DashboardService.ts
Normal file
22
apps/website/lib/services/dashboard/DashboardService.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { DashboardOverviewViewModel } from '../../view-models/DashboardOverviewViewModel';
|
||||
import { DashboardApiClient } from '../../api/dashboard/DashboardApiClient';
|
||||
|
||||
/**
|
||||
* Dashboard Service
|
||||
*
|
||||
* Orchestrates dashboard operations by coordinating API calls and view model creation.
|
||||
* All dependencies are injected via constructor.
|
||||
*/
|
||||
export class DashboardService {
|
||||
constructor(
|
||||
private readonly apiClient: DashboardApiClient
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get dashboard overview data with view model transformation
|
||||
*/
|
||||
async getDashboardOverview(): Promise<DashboardOverviewViewModel> {
|
||||
const dto = await this.apiClient.getDashboardOverview();
|
||||
return new DashboardOverviewViewModel(dto);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
import { DriversApiClient } from "@/lib/api/drivers/DriversApiClient";
|
||||
import { CompleteOnboardingInputDTO } from "@/lib/types/generated/CompleteOnboardingInputDTO";
|
||||
import { DriverProfileDTO } from "@/lib/types/generated/DriverProfileDTO";
|
||||
import { CompleteOnboardingViewModel } from "@/lib/view-models/CompleteOnboardingViewModel";
|
||||
import { DriverLeaderboardViewModel } from "@/lib/view-models/DriverLeaderboardViewModel";
|
||||
import { DriverViewModel } from "@/lib/view-models/DriverViewModel";
|
||||
import { DriverProfileViewModel } from "@/lib/view-models/DriverProfileViewModel";
|
||||
|
||||
// TODO: Create proper DriverDTO in generated types
|
||||
type DriverDTO = {
|
||||
@@ -41,8 +43,8 @@ export class DriverService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current driver with view model transformation
|
||||
*/
|
||||
* Get current driver with view model transformation
|
||||
*/
|
||||
async getCurrentDriver(): Promise<DriverViewModel | null> {
|
||||
const dto = await this.apiClient.getCurrent();
|
||||
if (!dto) {
|
||||
@@ -50,4 +52,12 @@ export class DriverService {
|
||||
}
|
||||
return new DriverViewModel(dto);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get driver profile with full details and view model transformation
|
||||
*/
|
||||
async getDriverProfile(driverId: string): Promise<DriverProfileViewModel> {
|
||||
const dto = await this.apiClient.getDriverProfile(driverId);
|
||||
return new DriverProfileViewModel(dto);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
import { LeaguesApiClient } from "@/lib/api/leagues/LeaguesApiClient";
|
||||
import { DriversApiClient } from "@/lib/api/drivers/DriversApiClient";
|
||||
import { SponsorsApiClient } from "@/lib/api/sponsors/SponsorsApiClient";
|
||||
import { RacesApiClient } from "@/lib/api/races/RacesApiClient";
|
||||
import { CreateLeagueInputDTO } from "@/lib/types/generated/CreateLeagueInputDTO";
|
||||
import { LeagueWithCapacityDTO } from "@/lib/types/generated/LeagueWithCapacityDTO";
|
||||
import { CreateLeagueViewModel } from "@/lib/view-models/CreateLeagueViewModel";
|
||||
@@ -8,7 +11,14 @@ import { LeagueStandingsViewModel } from "@/lib/view-models/LeagueStandingsViewM
|
||||
import { LeagueStatsViewModel } from "@/lib/view-models/LeagueStatsViewModel";
|
||||
import { LeagueSummaryViewModel } from "@/lib/view-models/LeagueSummaryViewModel";
|
||||
import { RemoveMemberViewModel } from "@/lib/view-models/RemoveMemberViewModel";
|
||||
import { LeagueDetailViewModel } from "@/lib/view-models/LeagueDetailViewModel";
|
||||
import { LeagueDetailPageViewModel, SponsorInfo } from "@/lib/view-models/LeagueDetailPageViewModel";
|
||||
import { SubmitBlocker, ThrottleBlocker } from "@/lib/blockers";
|
||||
import { DriverDTO } from "@/lib/types/DriverDTO";
|
||||
import { RaceDTO } from "@/lib/types/generated/RaceDTO";
|
||||
import { LeagueScoringConfigDTO } from "@/lib/types/LeagueScoringConfigDTO";
|
||||
import { LeagueStatsDTO } from "@/lib/types/generated/LeagueStatsDTO";
|
||||
import { LeagueMembershipsDTO } from "@/lib/types/generated/LeagueMembershipsDTO";
|
||||
|
||||
|
||||
/**
|
||||
@@ -22,7 +32,10 @@ export class LeagueService {
|
||||
private readonly throttle = new ThrottleBlocker(500);
|
||||
|
||||
constructor(
|
||||
private readonly apiClient: LeaguesApiClient
|
||||
private readonly apiClient: LeaguesApiClient,
|
||||
private readonly driversApiClient: DriversApiClient,
|
||||
private readonly sponsorsApiClient: SponsorsApiClient,
|
||||
private readonly racesApiClient: RacesApiClient
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -93,4 +106,156 @@ export class LeagueService {
|
||||
const dto = await this.apiClient.removeMember(leagueId, performerDriverId, targetDriverId);
|
||||
return new RemoveMemberViewModel(dto);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get league detail with owner, membership, and sponsor info
|
||||
*/
|
||||
async getLeagueDetail(leagueId: string, currentDriverId: string): Promise<LeagueDetailViewModel | null> {
|
||||
// For now, assume league data comes from getAllWithCapacity or a new endpoint
|
||||
// Since API may not have detailed league, we'll mock or assume
|
||||
// In real implementation, add getLeagueDetail to API
|
||||
const allLeagues = await this.apiClient.getAllWithCapacity();
|
||||
const leagueDto = allLeagues.leagues.find(l => l.id === leagueId);
|
||||
if (!leagueDto) return null;
|
||||
|
||||
// Assume league has description, ownerId - need to update DTO
|
||||
const league = {
|
||||
id: leagueDto.id,
|
||||
name: leagueDto.name,
|
||||
description: 'Description not available', // TODO: add to API
|
||||
ownerId: 'owner-id', // TODO: add to API
|
||||
};
|
||||
|
||||
// Get owner
|
||||
const owner = await this.driversApiClient.getDriver(league.ownerId);
|
||||
const ownerName = owner ? owner.name : `${league.ownerId.slice(0, 8)}...`;
|
||||
|
||||
// Get membership
|
||||
const membershipsDto = await this.apiClient.getMemberships(leagueId);
|
||||
const membership = membershipsDto.members.find(m => m.driverId === currentDriverId);
|
||||
const isAdmin = membership ? ['admin', 'owner'].includes(membership.role) : false;
|
||||
|
||||
// Get main sponsor
|
||||
let mainSponsor = null;
|
||||
try {
|
||||
const seasonsDto = await this.apiClient.getSeasons(leagueId);
|
||||
const activeSeason = seasonsDto.seasons.find((s: any) => s.status === 'active') ?? seasonsDto.seasons[0];
|
||||
if (activeSeason) {
|
||||
const sponsorshipsDto = await this.apiClient.getSeasonSponsorships(activeSeason.id);
|
||||
const mainSponsorship = sponsorshipsDto.sponsorships.find((s: any) => s.tier === 'main' && s.status === 'active');
|
||||
if (mainSponsorship) {
|
||||
const sponsor = await this.sponsorsApiClient.getSponsor(mainSponsorship.sponsorId);
|
||||
if (sponsor) {
|
||||
mainSponsor = {
|
||||
name: sponsor.name,
|
||||
logoUrl: sponsor.logoUrl ?? '',
|
||||
websiteUrl: sponsor.websiteUrl ?? '',
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to load main sponsor:', error);
|
||||
}
|
||||
|
||||
return new LeagueDetailViewModel(
|
||||
league.id,
|
||||
league.name,
|
||||
league.description,
|
||||
league.ownerId,
|
||||
ownerName,
|
||||
mainSponsor,
|
||||
isAdmin
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get comprehensive league detail page data
|
||||
*/
|
||||
async getLeagueDetailPageData(leagueId: string): Promise<LeagueDetailPageViewModel | null> {
|
||||
try {
|
||||
// Get league basic info
|
||||
const allLeagues = await this.apiClient.getAllWithCapacity();
|
||||
const league = allLeagues.leagues.find(l => l.id === leagueId);
|
||||
if (!league) return null;
|
||||
|
||||
// Get owner
|
||||
const owner = await this.driversApiClient.getDriver(league.ownerId);
|
||||
|
||||
// Get scoring config - TODO: implement API endpoint
|
||||
const scoringConfig: LeagueScoringConfigDTO | null = null; // TODO: fetch from API
|
||||
|
||||
// Get all drivers - TODO: implement API endpoint for all drivers
|
||||
const drivers: DriverDTO[] = []; // TODO: fetch from API
|
||||
|
||||
// Get memberships
|
||||
const memberships = await this.apiClient.getMemberships(leagueId);
|
||||
|
||||
// Get all races for this league - TODO: implement API endpoint
|
||||
const allRaces: RaceDTO[] = []; // TODO: fetch from API
|
||||
|
||||
// Get league stats
|
||||
const leagueStats = await this.apiClient.getTotal(); // TODO: get stats for specific league
|
||||
|
||||
// Get sponsors
|
||||
const sponsors = await this.getLeagueSponsors(leagueId);
|
||||
|
||||
return new LeagueDetailPageViewModel(
|
||||
league,
|
||||
owner,
|
||||
scoringConfig,
|
||||
drivers,
|
||||
memberships,
|
||||
allRaces,
|
||||
leagueStats,
|
||||
sponsors
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Failed to load league detail page data:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get sponsors for a league
|
||||
*/
|
||||
private async getLeagueSponsors(leagueId: string): Promise<SponsorInfo[]> {
|
||||
try {
|
||||
const seasons = await this.apiClient.getSeasons(leagueId);
|
||||
const activeSeason = seasons.seasons.find((s: any) => s.status === 'active') ?? seasons.seasons[0];
|
||||
|
||||
if (!activeSeason) return [];
|
||||
|
||||
const sponsorships = await this.apiClient.getSeasonSponsorships(activeSeason.id);
|
||||
const activeSponsorships = sponsorships.sponsorships.filter((s: any) => s.status === 'active');
|
||||
|
||||
const sponsorInfos: SponsorInfo[] = [];
|
||||
for (const sponsorship of activeSponsorships) {
|
||||
const sponsor = await this.sponsorsApiClient.getSponsor(sponsorship.sponsorId);
|
||||
if (sponsor) {
|
||||
// TODO: Get tagline from testing support or API
|
||||
sponsorInfos.push({
|
||||
id: sponsor.id,
|
||||
name: sponsor.name,
|
||||
logoUrl: sponsor.logoUrl ?? '',
|
||||
websiteUrl: sponsor.websiteUrl ?? '',
|
||||
tier: sponsorship.tier,
|
||||
tagline: '', // TODO: fetch tagline
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Sort: main sponsors first, then secondary
|
||||
sponsorInfos.sort((a, b) => {
|
||||
if (a.tier === 'main' && b.tier !== 'main') return -1;
|
||||
if (a.tier !== 'main' && b.tier === 'main') return 1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
return sponsorInfos;
|
||||
} catch (error) {
|
||||
console.warn('Failed to load sponsors:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
95
apps/website/lib/services/leagues/LeagueSettingsService.ts
Normal file
95
apps/website/lib/services/leagues/LeagueSettingsService.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { LeaguesApiClient } from "@/lib/api/leagues/LeaguesApiClient";
|
||||
import { DriversApiClient } from "@/lib/api/drivers/DriversApiClient";
|
||||
import type { LeagueConfigFormModel } from "@/lib/types/LeagueConfigFormModel";
|
||||
import type { LeagueScoringPresetDTO } from "@/lib/types/LeagueScoringPresetDTO";
|
||||
import type { DriverDTO } from "@/lib/types/DriverDTO";
|
||||
import { LeagueSettingsViewModel } from "@/lib/view-models/LeagueSettingsViewModel";
|
||||
import { DriverSummaryViewModel } from "@/lib/view-models/DriverSummaryViewModel";
|
||||
|
||||
/**
|
||||
* League Settings Service
|
||||
*
|
||||
* Orchestrates league settings operations by coordinating API calls and view model creation.
|
||||
* All dependencies are injected via constructor.
|
||||
*/
|
||||
export class LeagueSettingsService {
|
||||
constructor(
|
||||
private readonly leaguesApiClient: LeaguesApiClient,
|
||||
private readonly driversApiClient: DriversApiClient
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get league settings with view model transformation
|
||||
*/
|
||||
async getLeagueSettings(leagueId: string): Promise<LeagueSettingsViewModel | null> {
|
||||
try {
|
||||
// Get league basic info
|
||||
const allLeagues = await this.leaguesApiClient.getAllWithCapacity();
|
||||
const leagueDto = allLeagues.leagues.find(l => l.id === leagueId);
|
||||
if (!leagueDto) return null;
|
||||
|
||||
// Assume league has ownerId - need to update API
|
||||
const league = {
|
||||
id: leagueDto.id,
|
||||
name: leagueDto.name,
|
||||
ownerId: 'owner-id', // TODO: add to API
|
||||
};
|
||||
|
||||
// Get config
|
||||
const configDto = await this.leaguesApiClient.getLeagueConfig(leagueId);
|
||||
const config: LeagueConfigFormModel = configDto.config;
|
||||
|
||||
// Get presets
|
||||
const presetsDto = await this.leaguesApiClient.getScoringPresets();
|
||||
const presets: LeagueScoringPresetDTO[] = presetsDto.presets;
|
||||
|
||||
// Get owner
|
||||
const ownerDriver = await this.driversApiClient.getDriver(league.ownerId);
|
||||
let owner: DriverSummaryViewModel | null = null;
|
||||
if (ownerDriver) {
|
||||
// TODO: get rating and rank from API
|
||||
owner = new DriverSummaryViewModel({
|
||||
driver: ownerDriver,
|
||||
rating: ownerDriver.rating ?? null,
|
||||
rank: null, // TODO: get from API
|
||||
});
|
||||
}
|
||||
|
||||
// Get members
|
||||
const membershipsDto = await this.leaguesApiClient.getMemberships(leagueId);
|
||||
const members: DriverDTO[] = [];
|
||||
for (const member of membershipsDto.members) {
|
||||
if (member.driverId !== league.ownerId && member.role !== 'owner') {
|
||||
const driver = await this.driversApiClient.getDriver(member.driverId);
|
||||
if (driver) {
|
||||
members.push(driver);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new LeagueSettingsViewModel({
|
||||
league,
|
||||
config,
|
||||
presets,
|
||||
owner,
|
||||
members,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to load league settings:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transfer league ownership
|
||||
*/
|
||||
async transferOwnership(leagueId: string, currentOwnerId: string, newOwnerId: string): Promise<boolean> {
|
||||
try {
|
||||
const result = await this.leaguesApiClient.transferOwnership(leagueId, currentOwnerId, newOwnerId);
|
||||
return result.success;
|
||||
} catch (error) {
|
||||
console.error('Failed to transfer ownership:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,10 +34,17 @@ export class MediaService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete media by ID with view model transformation
|
||||
*/
|
||||
async deleteMedia(mediaId: string): Promise<DeleteMediaViewModel> {
|
||||
const dto = await this.apiClient.deleteMedia(mediaId);
|
||||
return new DeleteMediaViewModel(dto);
|
||||
}
|
||||
}
|
||||
* Delete media by ID with view model transformation
|
||||
*/
|
||||
async deleteMedia(mediaId: string): Promise<DeleteMediaViewModel> {
|
||||
const dto = await this.apiClient.deleteMedia(mediaId);
|
||||
return new DeleteMediaViewModel(dto);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get team logo URL
|
||||
*/
|
||||
getTeamLogo(teamId: string): string {
|
||||
return `/api/media/teams/${teamId}/logo`;
|
||||
}
|
||||
}
|
||||
70
apps/website/lib/services/protests/ProtestService.ts
Normal file
70
apps/website/lib/services/protests/ProtestService.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { ProtestsApiClient } from '../../api/protests/ProtestsApiClient';
|
||||
import { ProtestViewModel } from '../../view-models/ProtestViewModel';
|
||||
import type { LeagueAdminProtestsDTO, ApplyPenaltyCommandDTO, RequestProtestDefenseCommandDTO, DriverSummaryDTO } from '../../types';
|
||||
|
||||
/**
|
||||
* Protest Service
|
||||
*
|
||||
* Orchestrates protest operations by coordinating API calls and view model creation.
|
||||
* All dependencies are injected via constructor.
|
||||
*/
|
||||
export class ProtestService {
|
||||
constructor(
|
||||
private readonly apiClient: ProtestsApiClient
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get protests for a league with view model transformation
|
||||
*/
|
||||
async getLeagueProtests(leagueId: string): Promise<{
|
||||
protests: ProtestViewModel[];
|
||||
racesById: LeagueAdminProtestsDTO['racesById'];
|
||||
driversById: LeagueAdminProtestsDTO['driversById'];
|
||||
}> {
|
||||
const dto = await this.apiClient.getLeagueProtests(leagueId);
|
||||
return {
|
||||
protests: dto.protests.map(protest => new ProtestViewModel(protest)),
|
||||
racesById: dto.racesById,
|
||||
driversById: dto.driversById,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single protest by ID from league protests
|
||||
*/
|
||||
async getProtestById(leagueId: string, protestId: string): Promise<{
|
||||
protest: ProtestViewModel;
|
||||
race: LeagueAdminProtestsDTO['racesById'][string];
|
||||
protestingDriver: DriverSummaryDTO;
|
||||
accusedDriver: DriverSummaryDTO;
|
||||
} | null> {
|
||||
const dto = await this.apiClient.getLeagueProtest(leagueId, protestId);
|
||||
const protest = dto.protests[0];
|
||||
if (!protest) return null;
|
||||
|
||||
const race = Object.values(dto.racesById)[0];
|
||||
const protestingDriver = dto.driversById[protest.protestingDriverId];
|
||||
const accusedDriver = dto.driversById[protest.accusedDriverId];
|
||||
|
||||
return {
|
||||
protest: new ProtestViewModel(protest),
|
||||
race,
|
||||
protestingDriver,
|
||||
accusedDriver,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a penalty
|
||||
*/
|
||||
async applyPenalty(input: ApplyPenaltyCommandDTO): Promise<void> {
|
||||
await this.apiClient.applyPenalty(input);
|
||||
}
|
||||
|
||||
/**
|
||||
* Request protest defense
|
||||
*/
|
||||
async requestDefense(input: RequestProtestDefenseCommandDTO): Promise<void> {
|
||||
await this.apiClient.requestDefense(input);
|
||||
}
|
||||
}
|
||||
@@ -54,6 +54,34 @@ export class RaceService {
|
||||
return new RaceStatsViewModel(dto);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register for a race
|
||||
*/
|
||||
async registerForRace(raceId: string, leagueId: string, driverId: string): Promise<void> {
|
||||
await this.apiClient.register(raceId, { leagueId, driverId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Withdraw from a race
|
||||
*/
|
||||
async withdrawFromRace(raceId: string, driverId: string): Promise<void> {
|
||||
await this.apiClient.withdraw(raceId, { driverId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel a race
|
||||
*/
|
||||
async cancelRace(raceId: string): Promise<void> {
|
||||
await this.apiClient.cancel(raceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete a race
|
||||
*/
|
||||
async completeRace(raceId: string): Promise<void> {
|
||||
await this.apiClient.complete(raceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform API races page data to view model format
|
||||
*/
|
||||
|
||||
@@ -4,6 +4,7 @@ import { TeamSummaryViewModel } from '@/lib/view-models/TeamSummaryViewModel';
|
||||
import { CreateTeamViewModel } from '@/lib/view-models/CreateTeamViewModel';
|
||||
import { UpdateTeamViewModel } from '@/lib/view-models/UpdateTeamViewModel';
|
||||
import { DriverTeamViewModel } from '@/lib/view-models/DriverTeamViewModel';
|
||||
import { LeagueMemberDTO } from '@/lib/types/generated/LeagueMemberDTO';
|
||||
import type { TeamsApiClient } from '../../api/teams/TeamsApiClient';
|
||||
|
||||
// TODO: Move these types to apps/website/lib/types/generated when available
|
||||
@@ -71,10 +72,33 @@ export class TeamService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get driver's team with view model transformation
|
||||
*/
|
||||
async getDriverTeam(driverId: string): Promise<DriverTeamViewModel | null> {
|
||||
const dto = await this.apiClient.getDriverTeam(driverId);
|
||||
return dto ? new DriverTeamViewModel(dto) : null;
|
||||
}
|
||||
}
|
||||
* Get driver's team with view model transformation
|
||||
*/
|
||||
async getDriverTeam(driverId: string): Promise<DriverTeamViewModel | null> {
|
||||
const dto = await this.apiClient.getDriverTeam(driverId);
|
||||
return dto ? new DriverTeamViewModel(dto) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get team membership for a driver
|
||||
*/
|
||||
async getMembership(teamId: string, driverId: string): Promise<LeagueMemberDTO | null> {
|
||||
return this.apiClient.getMembership(teamId, driverId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a driver from the team
|
||||
*/
|
||||
async removeMembership(teamId: string, driverId: string): Promise<void> {
|
||||
// TODO: Implement when API endpoint is available
|
||||
throw new Error('Not implemented: API endpoint for removing team membership');
|
||||
}
|
||||
|
||||
/**
|
||||
* Update team membership role
|
||||
*/
|
||||
async updateMembership(teamId: string, driverId: string, role: string): Promise<void> {
|
||||
// TODO: Implement when API endpoint is available
|
||||
throw new Error('Not implemented: API endpoint for updating team membership role');
|
||||
}
|
||||
}
|
||||
13
apps/website/lib/types/DriverDTO.ts
Normal file
13
apps/website/lib/types/DriverDTO.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:sync-types
|
||||
*/
|
||||
|
||||
export interface DriverDTO {
|
||||
id: string;
|
||||
name: string;
|
||||
avatarUrl?: string;
|
||||
iracingId?: string;
|
||||
rating?: number;
|
||||
}
|
||||
9
apps/website/lib/types/GetAvatarOutputDto.ts
Normal file
9
apps/website/lib/types/GetAvatarOutputDto.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:sync-types
|
||||
*/
|
||||
|
||||
export interface GetAvatarOutputDto {
|
||||
avatarUrl: string;
|
||||
}
|
||||
15
apps/website/lib/types/LeagueScoringPresetDTO.ts
Normal file
15
apps/website/lib/types/LeagueScoringPresetDTO.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export type LeagueScoringPresetPrimaryChampionshipType =
|
||||
| 'driver'
|
||||
| 'team'
|
||||
| 'nations'
|
||||
| 'trophy';
|
||||
|
||||
export interface LeagueScoringPresetDTO {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
primaryChampionshipType: LeagueScoringPresetPrimaryChampionshipType;
|
||||
sessionSummary: string;
|
||||
bonusSummary: string;
|
||||
dropPolicySummary: string;
|
||||
}
|
||||
14
apps/website/lib/types/RaceDetailEntryDTO.ts
Normal file
14
apps/website/lib/types/RaceDetailEntryDTO.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:sync-types
|
||||
*/
|
||||
|
||||
export interface RaceDetailEntryDTO {
|
||||
id: string;
|
||||
name: string;
|
||||
country: string;
|
||||
avatarUrl: string;
|
||||
rating: number | null;
|
||||
isCurrentUser: boolean;
|
||||
}
|
||||
@@ -8,5 +8,9 @@ export interface ApplyPenaltyCommandDTO {
|
||||
raceId: string;
|
||||
driverId: string;
|
||||
stewardId: string;
|
||||
enum: string;
|
||||
type: string;
|
||||
value?: number;
|
||||
reason: string;
|
||||
protestId?: string;
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
100
apps/website/lib/types/generated/DriverProfileDTO.ts
Normal file
100
apps/website/lib/types/generated/DriverProfileDTO.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
export interface DriverProfileDriverSummaryDTO {
|
||||
id: string;
|
||||
name: string;
|
||||
country: string;
|
||||
avatarUrl: string;
|
||||
iracingId: string | null;
|
||||
joinedAt: string;
|
||||
rating: number | null;
|
||||
globalRank: number | null;
|
||||
consistency: number | null;
|
||||
bio: string | null;
|
||||
totalDrivers: number | null;
|
||||
}
|
||||
|
||||
export interface DriverProfileStatsDTO {
|
||||
totalRaces: number;
|
||||
wins: number;
|
||||
podiums: number;
|
||||
dnfs: number;
|
||||
avgFinish: number | null;
|
||||
bestFinish: number | null;
|
||||
worstFinish: number | null;
|
||||
finishRate: number | null;
|
||||
winRate: number | null;
|
||||
podiumRate: number | null;
|
||||
percentile: number | null;
|
||||
rating: number | null;
|
||||
consistency: number | null;
|
||||
overallRank: number | null;
|
||||
}
|
||||
|
||||
export interface DriverProfileFinishDistributionDTO {
|
||||
totalRaces: number;
|
||||
wins: number;
|
||||
podiums: number;
|
||||
topTen: number;
|
||||
dnfs: number;
|
||||
other: number;
|
||||
}
|
||||
|
||||
export interface DriverProfileTeamMembershipDTO {
|
||||
teamId: string;
|
||||
teamName: string;
|
||||
teamTag: string | null;
|
||||
role: string;
|
||||
joinedAt: string;
|
||||
isCurrent: boolean;
|
||||
}
|
||||
|
||||
export interface DriverProfileSocialFriendSummaryDTO {
|
||||
id: string;
|
||||
name: string;
|
||||
country: string;
|
||||
avatarUrl: string;
|
||||
}
|
||||
|
||||
export interface DriverProfileSocialSummaryDTO {
|
||||
friendsCount: number;
|
||||
friends: DriverProfileSocialFriendSummaryDTO[];
|
||||
}
|
||||
|
||||
export type DriverProfileSocialPlatform = 'twitter' | 'youtube' | 'twitch' | 'discord';
|
||||
|
||||
export type DriverProfileAchievementRarity = 'common' | 'rare' | 'epic' | 'legendary';
|
||||
|
||||
export interface DriverProfileAchievementDTO {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
icon: 'trophy' | 'medal' | 'star' | 'crown' | 'target' | 'zap';
|
||||
rarity: DriverProfileAchievementRarity;
|
||||
earnedAt: string;
|
||||
}
|
||||
|
||||
export interface DriverProfileSocialHandleDTO {
|
||||
platform: DriverProfileSocialPlatform;
|
||||
handle: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface DriverProfileExtendedProfileDTO {
|
||||
socialHandles: DriverProfileSocialHandleDTO[];
|
||||
achievements: DriverProfileAchievementDTO[];
|
||||
racingStyle: string;
|
||||
favoriteTrack: string;
|
||||
favoriteCar: string;
|
||||
timezone: string;
|
||||
availableHours: string;
|
||||
lookingForTeam: boolean;
|
||||
openToRequests: boolean;
|
||||
}
|
||||
|
||||
export interface DriverProfileDTO {
|
||||
currentDriver: DriverProfileDriverSummaryDTO | null;
|
||||
stats: DriverProfileStatsDTO | null;
|
||||
finishDistribution: DriverProfileFinishDistributionDTO | null;
|
||||
teamMemberships: DriverProfileTeamMembershipDTO[];
|
||||
socialSummary: DriverProfileSocialSummaryDTO;
|
||||
extendedProfile: DriverProfileExtendedProfileDTO | null;
|
||||
}
|
||||
19
apps/website/lib/types/generated/LeagueAdminProtestsDTO.ts
Normal file
19
apps/website/lib/types/generated/LeagueAdminProtestsDTO.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:sync-types
|
||||
*/
|
||||
|
||||
import { ProtestDTO } from './ProtestDTO';
|
||||
import { RaceDTO } from './RaceDTO';
|
||||
|
||||
export interface DriverSummaryDTO {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface LeagueAdminProtestsDTO {
|
||||
protests: ProtestDTO[];
|
||||
racesById: { [raceId: string]: RaceDTO };
|
||||
driversById: { [driverId: string]: DriverSummaryDTO };
|
||||
}
|
||||
181
apps/website/lib/view-models/DashboardOverviewViewModel.ts
Normal file
181
apps/website/lib/view-models/DashboardOverviewViewModel.ts
Normal file
@@ -0,0 +1,181 @@
|
||||
import { DashboardOverviewDto, DriverDto, RaceDto, LeagueStandingDto, FeedItemDto, FriendDto } from '../api/dashboard/DashboardApiClient';
|
||||
|
||||
export class DriverViewModel {
|
||||
constructor(private readonly dto: DriverDto) {}
|
||||
|
||||
get id(): string {
|
||||
return this.dto.id;
|
||||
}
|
||||
|
||||
get name(): string {
|
||||
return this.dto.name;
|
||||
}
|
||||
|
||||
get avatarUrl(): string {
|
||||
return this.dto.avatarUrl;
|
||||
}
|
||||
|
||||
get country(): string {
|
||||
return this.dto.country;
|
||||
}
|
||||
|
||||
get totalRaces(): number {
|
||||
return this.dto.totalRaces;
|
||||
}
|
||||
|
||||
get wins(): number {
|
||||
return this.dto.wins;
|
||||
}
|
||||
|
||||
get podiums(): number {
|
||||
return this.dto.podiums;
|
||||
}
|
||||
|
||||
get rating(): number {
|
||||
return this.dto.rating;
|
||||
}
|
||||
|
||||
get globalRank(): number {
|
||||
return this.dto.globalRank;
|
||||
}
|
||||
|
||||
get consistency(): number {
|
||||
return this.dto.consistency;
|
||||
}
|
||||
}
|
||||
|
||||
export class RaceViewModel {
|
||||
constructor(private readonly dto: RaceDto) {}
|
||||
|
||||
get id(): string {
|
||||
return this.dto.id;
|
||||
}
|
||||
|
||||
get track(): string {
|
||||
return this.dto.track;
|
||||
}
|
||||
|
||||
get car(): string {
|
||||
return this.dto.car;
|
||||
}
|
||||
|
||||
get scheduledAt(): Date {
|
||||
return new Date(this.dto.scheduledAt);
|
||||
}
|
||||
|
||||
get isMyLeague(): boolean {
|
||||
return this.dto.isMyLeague;
|
||||
}
|
||||
|
||||
get leagueName(): string | undefined {
|
||||
return this.dto.leagueName;
|
||||
}
|
||||
}
|
||||
|
||||
export class LeagueStandingViewModel {
|
||||
constructor(private readonly dto: LeagueStandingDto) {}
|
||||
|
||||
get leagueId(): string {
|
||||
return this.dto.leagueId;
|
||||
}
|
||||
|
||||
get leagueName(): string {
|
||||
return this.dto.leagueName;
|
||||
}
|
||||
|
||||
get position(): number {
|
||||
return this.dto.position;
|
||||
}
|
||||
|
||||
get points(): number {
|
||||
return this.dto.points;
|
||||
}
|
||||
|
||||
get totalDrivers(): number {
|
||||
return this.dto.totalDrivers;
|
||||
}
|
||||
}
|
||||
|
||||
export class DashboardFeedItemSummaryViewModel {
|
||||
constructor(private readonly dto: FeedItemDto) {}
|
||||
|
||||
get id(): string {
|
||||
return this.dto.id;
|
||||
}
|
||||
|
||||
get type(): string {
|
||||
return this.dto.type;
|
||||
}
|
||||
|
||||
get headline(): string {
|
||||
return this.dto.headline;
|
||||
}
|
||||
|
||||
get body(): string | null {
|
||||
return this.dto.body;
|
||||
}
|
||||
|
||||
get timestamp(): Date {
|
||||
return new Date(this.dto.timestamp);
|
||||
}
|
||||
|
||||
get ctaHref(): string | undefined {
|
||||
return this.dto.ctaHref;
|
||||
}
|
||||
|
||||
get ctaLabel(): string | undefined {
|
||||
return this.dto.ctaLabel;
|
||||
}
|
||||
}
|
||||
|
||||
export class FriendViewModel {
|
||||
constructor(private readonly dto: FriendDto) {}
|
||||
|
||||
get id(): string {
|
||||
return this.dto.id;
|
||||
}
|
||||
|
||||
get name(): string {
|
||||
return this.dto.name;
|
||||
}
|
||||
|
||||
get avatarUrl(): string {
|
||||
return this.dto.avatarUrl;
|
||||
}
|
||||
|
||||
get country(): string {
|
||||
return this.dto.country;
|
||||
}
|
||||
}
|
||||
|
||||
export class DashboardOverviewViewModel {
|
||||
constructor(private readonly dto: DashboardOverviewDto) {}
|
||||
|
||||
get currentDriver(): DriverViewModel {
|
||||
return new DriverViewModel(this.dto.currentDriver);
|
||||
}
|
||||
|
||||
get nextRace(): RaceViewModel | null {
|
||||
return this.dto.nextRace ? new RaceViewModel(this.dto.nextRace) : null;
|
||||
}
|
||||
|
||||
get upcomingRaces(): RaceViewModel[] {
|
||||
return this.dto.upcomingRaces.map(dto => new RaceViewModel(dto));
|
||||
}
|
||||
|
||||
get leagueStandings(): LeagueStandingViewModel[] {
|
||||
return this.dto.leagueStandings.map(dto => new LeagueStandingViewModel(dto));
|
||||
}
|
||||
|
||||
get feedItems(): DashboardFeedItemSummaryViewModel[] {
|
||||
return this.dto.feedItems.map(dto => new DashboardFeedItemSummaryViewModel(dto));
|
||||
}
|
||||
|
||||
get friends(): FriendViewModel[] {
|
||||
return this.dto.friends.map(dto => new FriendViewModel(dto));
|
||||
}
|
||||
|
||||
get activeLeaguesCount(): number {
|
||||
return this.dto.activeLeaguesCount;
|
||||
}
|
||||
}
|
||||
141
apps/website/lib/view-models/DriverProfileViewModel.ts
Normal file
141
apps/website/lib/view-models/DriverProfileViewModel.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
export interface DriverProfileDriverSummaryViewModel {
|
||||
id: string;
|
||||
name: string;
|
||||
country: string;
|
||||
avatarUrl: string;
|
||||
iracingId: string | null;
|
||||
joinedAt: string;
|
||||
rating: number | null;
|
||||
globalRank: number | null;
|
||||
consistency: number | null;
|
||||
bio: string | null;
|
||||
totalDrivers: number | null;
|
||||
}
|
||||
|
||||
export interface DriverProfileStatsViewModel {
|
||||
totalRaces: number;
|
||||
wins: number;
|
||||
podiums: number;
|
||||
dnfs: number;
|
||||
avgFinish: number | null;
|
||||
bestFinish: number | null;
|
||||
worstFinish: number | null;
|
||||
finishRate: number | null;
|
||||
winRate: number | null;
|
||||
podiumRate: number | null;
|
||||
percentile: number | null;
|
||||
rating: number | null;
|
||||
consistency: number | null;
|
||||
overallRank: number | null;
|
||||
}
|
||||
|
||||
export interface DriverProfileFinishDistributionViewModel {
|
||||
totalRaces: number;
|
||||
wins: number;
|
||||
podiums: number;
|
||||
topTen: number;
|
||||
dnfs: number;
|
||||
other: number;
|
||||
}
|
||||
|
||||
export interface DriverProfileTeamMembershipViewModel {
|
||||
teamId: string;
|
||||
teamName: string;
|
||||
teamTag: string | null;
|
||||
role: string;
|
||||
joinedAt: string;
|
||||
isCurrent: boolean;
|
||||
}
|
||||
|
||||
export interface DriverProfileSocialFriendSummaryViewModel {
|
||||
id: string;
|
||||
name: string;
|
||||
country: string;
|
||||
avatarUrl: string;
|
||||
}
|
||||
|
||||
export interface DriverProfileSocialSummaryViewModel {
|
||||
friendsCount: number;
|
||||
friends: DriverProfileSocialFriendSummaryViewModel[];
|
||||
}
|
||||
|
||||
export type DriverProfileSocialPlatform = 'twitter' | 'youtube' | 'twitch' | 'discord';
|
||||
|
||||
export type DriverProfileAchievementRarity = 'common' | 'rare' | 'epic' | 'legendary';
|
||||
|
||||
export interface DriverProfileAchievementViewModel {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
icon: 'trophy' | 'medal' | 'star' | 'crown' | 'target' | 'zap';
|
||||
rarity: DriverProfileAchievementRarity;
|
||||
earnedAt: string;
|
||||
}
|
||||
|
||||
export interface DriverProfileSocialHandleViewModel {
|
||||
platform: DriverProfileSocialPlatform;
|
||||
handle: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface DriverProfileExtendedProfileViewModel {
|
||||
socialHandles: DriverProfileSocialHandleViewModel[];
|
||||
achievements: DriverProfileAchievementViewModel[];
|
||||
racingStyle: string;
|
||||
favoriteTrack: string;
|
||||
favoriteCar: string;
|
||||
timezone: string;
|
||||
availableHours: string;
|
||||
lookingForTeam: boolean;
|
||||
openToRequests: boolean;
|
||||
}
|
||||
|
||||
export interface DriverProfileViewModel {
|
||||
currentDriver: DriverProfileDriverSummaryViewModel | null;
|
||||
stats: DriverProfileStatsViewModel | null;
|
||||
finishDistribution: DriverProfileFinishDistributionViewModel | null;
|
||||
teamMemberships: DriverProfileTeamMembershipViewModel[];
|
||||
socialSummary: DriverProfileSocialSummaryViewModel;
|
||||
extendedProfile: DriverProfileExtendedProfileViewModel | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Driver Profile View Model
|
||||
*
|
||||
* Represents a fully prepared UI state for driver profile display.
|
||||
* Transforms API DTOs into UI-ready data structures.
|
||||
*/
|
||||
export class DriverProfileViewModel {
|
||||
constructor(private readonly dto: DriverProfileViewModel) {}
|
||||
|
||||
get currentDriver(): DriverProfileDriverSummaryViewModel | null {
|
||||
return this.dto.currentDriver;
|
||||
}
|
||||
|
||||
get stats(): DriverProfileStatsViewModel | null {
|
||||
return this.dto.stats;
|
||||
}
|
||||
|
||||
get finishDistribution(): DriverProfileFinishDistributionViewModel | null {
|
||||
return this.dto.finishDistribution;
|
||||
}
|
||||
|
||||
get teamMemberships(): DriverProfileTeamMembershipViewModel[] {
|
||||
return this.dto.teamMemberships;
|
||||
}
|
||||
|
||||
get socialSummary(): DriverProfileSocialSummaryViewModel {
|
||||
return this.dto.socialSummary;
|
||||
}
|
||||
|
||||
get extendedProfile(): DriverProfileExtendedProfileViewModel | null {
|
||||
return this.dto.extendedProfile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the raw DTO for serialization or further processing
|
||||
*/
|
||||
toDTO(): DriverProfileViewModel {
|
||||
return this.dto;
|
||||
}
|
||||
}
|
||||
21
apps/website/lib/view-models/DriverSummaryViewModel.ts
Normal file
21
apps/website/lib/view-models/DriverSummaryViewModel.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { DriverDTO } from '../types/DriverDTO';
|
||||
|
||||
/**
|
||||
* View Model for driver summary with rating and rank
|
||||
* Transform from DTO to ViewModel with UI fields
|
||||
*/
|
||||
export class DriverSummaryViewModel {
|
||||
driver: DriverDTO;
|
||||
rating: number | null;
|
||||
rank: number | null;
|
||||
|
||||
constructor(dto: {
|
||||
driver: DriverDTO;
|
||||
rating?: number | null;
|
||||
rank?: number | null;
|
||||
}) {
|
||||
this.driver = dto.driver;
|
||||
this.rating = dto.rating ?? null;
|
||||
this.rank = dto.rank ?? null;
|
||||
}
|
||||
}
|
||||
192
apps/website/lib/view-models/LeagueDetailPageViewModel.ts
Normal file
192
apps/website/lib/view-models/LeagueDetailPageViewModel.ts
Normal file
@@ -0,0 +1,192 @@
|
||||
import { LeagueWithCapacityDTO } from '../types/generated/LeagueWithCapacityDTO';
|
||||
import { LeagueStatsDTO } from '../types/generated/LeagueStatsDTO';
|
||||
import { LeagueMembershipsDTO } from '../types/generated/LeagueMembershipsDTO';
|
||||
import { LeagueScheduleDTO } from '../types/generated/LeagueScheduleDTO';
|
||||
import { LeagueStandingsDTO } from '../types/generated/LeagueStandingsDTO';
|
||||
import { DriverDTO } from '../types/DriverDTO';
|
||||
import { RaceDTO } from '../types/generated/RaceDTO';
|
||||
import { LeagueScoringConfigDTO } from '../types/LeagueScoringConfigDTO';
|
||||
|
||||
// Sponsor info type
|
||||
export interface SponsorInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
logoUrl?: string;
|
||||
websiteUrl?: string;
|
||||
tier: 'main' | 'secondary';
|
||||
tagline?: string;
|
||||
}
|
||||
|
||||
// Driver summary for management section
|
||||
export interface DriverSummary {
|
||||
driver: DriverDTO;
|
||||
rating: number | null;
|
||||
rank: number | null;
|
||||
}
|
||||
|
||||
// League membership with role
|
||||
export interface LeagueMembershipWithRole {
|
||||
driverId: string;
|
||||
role: 'owner' | 'admin' | 'steward' | 'member';
|
||||
status: 'active' | 'inactive';
|
||||
joinedAt: string;
|
||||
}
|
||||
|
||||
export class LeagueDetailPageViewModel {
|
||||
// League basic info
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
ownerId: string;
|
||||
createdAt: string;
|
||||
settings: {
|
||||
maxDrivers?: number;
|
||||
};
|
||||
socialLinks?: {
|
||||
discordUrl?: string;
|
||||
youtubeUrl?: string;
|
||||
websiteUrl?: string;
|
||||
};
|
||||
|
||||
// Owner info
|
||||
owner: DriverDTO | null;
|
||||
|
||||
// Scoring configuration
|
||||
scoringConfig: LeagueScoringConfigDTO | null;
|
||||
|
||||
// Drivers and memberships
|
||||
drivers: DriverDTO[];
|
||||
memberships: LeagueMembershipWithRole[];
|
||||
|
||||
// Races
|
||||
allRaces: RaceDTO[];
|
||||
runningRaces: RaceDTO[];
|
||||
|
||||
// Stats
|
||||
averageSOF: number | null;
|
||||
completedRacesCount: number;
|
||||
|
||||
// Sponsors
|
||||
sponsors: SponsorInfo[];
|
||||
|
||||
// Sponsor insights data
|
||||
sponsorInsights: {
|
||||
avgViewsPerRace: number;
|
||||
totalImpressions: number;
|
||||
engagementRate: string;
|
||||
estimatedReach: number;
|
||||
mainSponsorAvailable: boolean;
|
||||
secondarySlotsAvailable: number;
|
||||
mainSponsorPrice: number;
|
||||
secondaryPrice: number;
|
||||
tier: 'premium' | 'standard' | 'starter';
|
||||
trustScore: number;
|
||||
discordMembers: number;
|
||||
monthlyActivity: number;
|
||||
};
|
||||
|
||||
// Driver summaries for management
|
||||
ownerSummary: DriverSummary | null;
|
||||
adminSummaries: DriverSummary[];
|
||||
stewardSummaries: DriverSummary[];
|
||||
|
||||
constructor(
|
||||
league: LeagueWithCapacityDTO,
|
||||
owner: DriverDTO | null,
|
||||
scoringConfig: LeagueScoringConfigDTO | null,
|
||||
drivers: DriverDTO[],
|
||||
memberships: LeagueMembershipsDTO,
|
||||
allRaces: RaceDTO[],
|
||||
leagueStats: LeagueStatsDTO,
|
||||
sponsors: SponsorInfo[]
|
||||
) {
|
||||
this.id = league.id;
|
||||
this.name = league.name;
|
||||
this.description = league.description;
|
||||
this.ownerId = league.ownerId;
|
||||
this.createdAt = league.createdAt;
|
||||
this.settings = {
|
||||
maxDrivers: league.maxDrivers,
|
||||
};
|
||||
this.socialLinks = league.socialLinks;
|
||||
|
||||
this.owner = owner;
|
||||
this.scoringConfig = scoringConfig;
|
||||
this.drivers = drivers;
|
||||
this.memberships = memberships.memberships.map(m => ({
|
||||
driverId: m.driverId,
|
||||
role: m.role,
|
||||
status: m.status,
|
||||
joinedAt: m.joinedAt,
|
||||
}));
|
||||
|
||||
this.allRaces = allRaces;
|
||||
this.runningRaces = allRaces.filter(r => r.status === 'running');
|
||||
|
||||
this.averageSOF = leagueStats.averageSOF ?? null;
|
||||
this.completedRacesCount = leagueStats.completedRaces ?? 0;
|
||||
|
||||
this.sponsors = sponsors;
|
||||
|
||||
// Calculate sponsor insights
|
||||
const memberCount = this.memberships.length;
|
||||
const mainSponsorTaken = this.sponsors.some(s => s.tier === 'main');
|
||||
const secondaryTaken = this.sponsors.filter(s => s.tier === 'secondary').length;
|
||||
|
||||
this.sponsorInsights = {
|
||||
avgViewsPerRace: 5400 + memberCount * 50,
|
||||
totalImpressions: 45000 + memberCount * 500,
|
||||
engagementRate: (3.5 + (memberCount / 50)).toFixed(1),
|
||||
estimatedReach: memberCount * 150,
|
||||
mainSponsorAvailable: !mainSponsorTaken,
|
||||
secondarySlotsAvailable: Math.max(0, 2 - secondaryTaken),
|
||||
mainSponsorPrice: 800 + Math.floor(memberCount * 10),
|
||||
secondaryPrice: 250 + Math.floor(memberCount * 3),
|
||||
tier: (this.averageSOF && this.averageSOF > 3000 ? 'premium' : this.averageSOF && this.averageSOF > 2000 ? 'standard' : 'starter') as 'premium' | 'standard' | 'starter',
|
||||
trustScore: Math.min(100, 60 + memberCount + this.completedRacesCount),
|
||||
discordMembers: memberCount * 3,
|
||||
monthlyActivity: Math.min(100, 40 + this.completedRacesCount * 2),
|
||||
};
|
||||
|
||||
// Build driver summaries
|
||||
this.ownerSummary = this.buildDriverSummary(this.ownerId);
|
||||
this.adminSummaries = this.memberships
|
||||
.filter(m => m.role === 'admin')
|
||||
.slice(0, 3)
|
||||
.map(m => this.buildDriverSummary(m.driverId))
|
||||
.filter((s): s is DriverSummary => s !== null);
|
||||
this.stewardSummaries = this.memberships
|
||||
.filter(m => m.role === 'steward')
|
||||
.slice(0, 3)
|
||||
.map(m => this.buildDriverSummary(m.driverId))
|
||||
.filter((s): s is DriverSummary => s !== null);
|
||||
}
|
||||
|
||||
private buildDriverSummary(driverId: string): DriverSummary | null {
|
||||
const driver = this.drivers.find(d => d.id === driverId);
|
||||
if (!driver) return null;
|
||||
|
||||
// TODO: Get driver stats and rankings from service
|
||||
// For now, return basic info
|
||||
return {
|
||||
driver,
|
||||
rating: null, // TODO: fetch from service
|
||||
rank: null, // TODO: fetch from service
|
||||
};
|
||||
}
|
||||
|
||||
// UI helper methods
|
||||
get isSponsorMode(): boolean {
|
||||
// TODO: implement sponsor mode check
|
||||
return false;
|
||||
}
|
||||
|
||||
get currentUserMembership(): LeagueMembershipWithRole | null {
|
||||
// TODO: get current user ID and find membership
|
||||
return null;
|
||||
}
|
||||
|
||||
get canEndRaces(): boolean {
|
||||
return this.currentUserMembership?.role === 'admin' || this.currentUserMembership?.role === 'owner';
|
||||
}
|
||||
}
|
||||
35
apps/website/lib/view-models/LeagueDetailViewModel.ts
Normal file
35
apps/website/lib/view-models/LeagueDetailViewModel.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
export interface MainSponsorInfo {
|
||||
name: string;
|
||||
logoUrl: string;
|
||||
websiteUrl: string;
|
||||
}
|
||||
|
||||
export class LeagueDetailViewModel {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
ownerId: string;
|
||||
ownerName: string;
|
||||
mainSponsor: MainSponsorInfo | null;
|
||||
isAdmin: boolean;
|
||||
|
||||
constructor(
|
||||
id: string,
|
||||
name: string,
|
||||
description: string,
|
||||
ownerId: string,
|
||||
ownerName: string,
|
||||
mainSponsor: MainSponsorInfo | null,
|
||||
isAdmin: boolean
|
||||
) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.description = description;
|
||||
this.ownerId = ownerId;
|
||||
this.ownerName = ownerName;
|
||||
this.mainSponsor = mainSponsor;
|
||||
this.isAdmin = isAdmin;
|
||||
}
|
||||
|
||||
// UI-specific getters can be added here if needed
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { LeagueScoringPresetDTO } from '../types/LeagueScoringPresetDTO';
|
||||
|
||||
/**
|
||||
* View Model for league scoring presets
|
||||
* Transform from DTO to ViewModel with UI fields
|
||||
*/
|
||||
export class LeagueScoringPresetsViewModel {
|
||||
presets: LeagueScoringPresetDTO[];
|
||||
totalCount: number;
|
||||
|
||||
constructor(dto: {
|
||||
presets: LeagueScoringPresetDTO[];
|
||||
totalCount?: number;
|
||||
}) {
|
||||
this.presets = dto.presets;
|
||||
this.totalCount = dto.totalCount ?? dto.presets.length;
|
||||
}
|
||||
}
|
||||
39
apps/website/lib/view-models/LeagueSettingsViewModel.ts
Normal file
39
apps/website/lib/view-models/LeagueSettingsViewModel.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import type { LeagueConfigFormModel } from '../types/LeagueConfigFormModel';
|
||||
import type { LeagueScoringPresetDTO } from '../types/LeagueScoringPresetDTO';
|
||||
import type { DriverDTO } from '../types/DriverDTO';
|
||||
import { LeagueScoringPresetsViewModel } from './LeagueScoringPresetsViewModel';
|
||||
import { DriverSummaryViewModel } from './DriverSummaryViewModel';
|
||||
|
||||
/**
|
||||
* View Model for league settings page
|
||||
* Combines league config, presets, owner, and members
|
||||
*/
|
||||
export class LeagueSettingsViewModel {
|
||||
league: {
|
||||
id: string;
|
||||
name: string;
|
||||
ownerId: string;
|
||||
};
|
||||
config: LeagueConfigFormModel;
|
||||
presets: LeagueScoringPresetDTO[];
|
||||
owner: DriverSummaryViewModel | null;
|
||||
members: DriverDTO[];
|
||||
|
||||
constructor(dto: {
|
||||
league: {
|
||||
id: string;
|
||||
name: string;
|
||||
ownerId: string;
|
||||
};
|
||||
config: LeagueConfigFormModel;
|
||||
presets: LeagueScoringPresetDTO[];
|
||||
owner: DriverSummaryViewModel | null;
|
||||
members: DriverDTO[];
|
||||
}) {
|
||||
this.league = dto.league;
|
||||
this.config = dto.config;
|
||||
this.presets = dto.presets;
|
||||
this.owner = dto.owner;
|
||||
this.members = dto.members;
|
||||
}
|
||||
}
|
||||
@@ -7,29 +7,27 @@ import { ProtestDTO } from '../types/generated/ProtestDTO';
|
||||
export class ProtestViewModel {
|
||||
id: string;
|
||||
raceId: string;
|
||||
complainantId: string;
|
||||
defendantId: string;
|
||||
protestingDriverId: string;
|
||||
accusedDriverId: string;
|
||||
description: string;
|
||||
status: string;
|
||||
createdAt: string;
|
||||
submittedAt: string;
|
||||
|
||||
constructor(dto: ProtestDTO) {
|
||||
this.id = dto.id;
|
||||
this.raceId = dto.raceId;
|
||||
this.complainantId = dto.complainantId;
|
||||
this.defendantId = dto.defendantId;
|
||||
this.protestingDriverId = dto.protestingDriverId;
|
||||
this.accusedDriverId = dto.accusedDriverId;
|
||||
this.description = dto.description;
|
||||
this.status = dto.status;
|
||||
this.createdAt = dto.createdAt;
|
||||
this.submittedAt = dto.submittedAt;
|
||||
}
|
||||
|
||||
/** UI-specific: Formatted created date */
|
||||
get formattedCreatedAt(): string {
|
||||
return new Date(this.createdAt).toLocaleString();
|
||||
/** UI-specific: Formatted submitted date */
|
||||
get formattedSubmittedAt(): string {
|
||||
return new Date(this.submittedAt).toLocaleString();
|
||||
}
|
||||
|
||||
/** UI-specific: Status display */
|
||||
/** UI-specific: Status display - placeholder since status not in current DTO */
|
||||
get statusDisplay(): string {
|
||||
return this.status.charAt(0).toUpperCase() + this.status.slice(1);
|
||||
return 'Pending'; // TODO: Update when status is added to DTO
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { RaceDetailViewModel } from './RaceDetailViewModel';
|
||||
import type { RaceDetailRaceDTO } from '../types/generated/RaceDetailRaceDTO';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import type { RaceDetailLeagueDTO } from '../types/generated/RaceDetailLeagueDTO';
|
||||
import type { RaceDetailEntryDTO } from '../types/generated/RaceDetailEntryDTO';
|
||||
import type { RaceDetailRaceDTO } from '../types/generated/RaceDetailRaceDTO';
|
||||
import type { RaceDetailRegistrationDTO } from '../types/generated/RaceDetailRegistrationDTO';
|
||||
import type { RaceDetailUserResultDTO } from '../types/generated/RaceDetailUserResultDTO';
|
||||
import type { RaceDetailEntryDTO } from '../types/RaceDetailEntryDTO';
|
||||
import { RaceDetailViewModel } from './RaceDetailViewModel';
|
||||
|
||||
describe('RaceDetailViewModel', () => {
|
||||
const createMockRace = (overrides?: Partial<RaceDetailRaceDTO>): RaceDetailRaceDTO => ({
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { RaceDetailRaceDTO } from '../types/generated/RaceDetailRaceDTO';
|
||||
import { RaceDetailLeagueDTO } from '../types/generated/RaceDetailLeagueDTO';
|
||||
import { RaceDetailEntryDTO } from '../types/generated/RaceDetailEntryDTO';
|
||||
import { RaceDetailRaceDTO } from '../types/generated/RaceDetailRaceDTO';
|
||||
import { RaceDetailRegistrationDTO } from '../types/generated/RaceDetailRegistrationDTO';
|
||||
import { RaceDetailUserResultDTO } from '../types/generated/RaceDetailUserResultDTO';
|
||||
import { RaceDetailEntryDTO } from '../types/RaceDetailEntryDTO';
|
||||
|
||||
export class RaceDetailViewModel {
|
||||
race: RaceDetailRaceDTO | null;
|
||||
|
||||
Reference in New Issue
Block a user