move static data
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { BaseApiClient } from '../base/BaseApiClient';
|
||||
import { RacePenaltiesDTO } from '../../types/generated/RacePenaltiesDTO';
|
||||
import { ApplyPenaltyCommandDTO } from '../../types/generated/ApplyPenaltyCommandDTO';
|
||||
import type { PenaltyTypesReferenceDTO } from '../../types/PenaltyTypesReferenceDTO';
|
||||
|
||||
/**
|
||||
* Penalties API Client
|
||||
@@ -13,6 +14,11 @@ export class PenaltiesApiClient extends BaseApiClient {
|
||||
return this.get<RacePenaltiesDTO>(`/races/${raceId}/penalties`);
|
||||
}
|
||||
|
||||
/** Get allowed penalty types and semantics */
|
||||
getPenaltyTypesReference(): Promise<PenaltyTypesReferenceDTO> {
|
||||
return this.get<PenaltyTypesReferenceDTO>('/races/reference/penalty-types');
|
||||
}
|
||||
|
||||
/** Apply a penalty */
|
||||
applyPenalty(input: ApplyPenaltyCommandDTO): Promise<void> {
|
||||
return this.post<void>('/races/penalties/apply', input);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { CreateLeagueInputDTO } from '@/lib/types/generated/CreateLeagueInputDTO';
|
||||
import { LeagueWizardValidationMessages } from '@/lib/display-objects/LeagueWizardValidationMessages';
|
||||
import { ScoringPresetApplier } from '@/lib/utilities/ScoringPresetApplier';
|
||||
|
||||
export type WizardStep = 1 | 2 | 3 | 4 | 5 | 6 | 7;
|
||||
|
||||
@@ -263,7 +262,7 @@ export class LeagueWizardCommandModel {
|
||||
patternId,
|
||||
customScoringEnabled: false,
|
||||
};
|
||||
this.timings = ScoringPresetApplier.applyToTimings(patternId, this.timings);
|
||||
// Timing defaults are applied by the UI using the selected preset's `defaultTimings` from the API.
|
||||
}
|
||||
|
||||
toCreateLeagueCommand(ownerId: string): CreateLeagueInputDTO {
|
||||
|
||||
@@ -1,19 +1,15 @@
|
||||
import { ApplyPenaltyCommandDTO } from '../../types/generated/ApplyPenaltyCommandDTO';
|
||||
|
||||
export type PenaltyType = 'time_penalty' | 'grid_penalty' | 'points_deduction' | 'disqualification' | 'warning' | 'license_points';
|
||||
|
||||
export interface ProtestDecisionData {
|
||||
decision: 'uphold' | 'dismiss' | null;
|
||||
penaltyType: PenaltyType;
|
||||
penaltyValue: number;
|
||||
penaltyType: string;
|
||||
penaltyValue?: number;
|
||||
stewardNotes: string;
|
||||
}
|
||||
|
||||
const DEFAULT_PROTEST_REASON = 'Protest upheld';
|
||||
|
||||
export class ProtestDecisionCommandModel {
|
||||
decision: 'uphold' | 'dismiss' | null = null;
|
||||
penaltyType: PenaltyType = 'time_penalty';
|
||||
penaltyType: string = 'time_penalty';
|
||||
penaltyValue: number = 5;
|
||||
stewardNotes: string = '';
|
||||
|
||||
@@ -39,27 +35,37 @@ export class ProtestDecisionCommandModel {
|
||||
this.stewardNotes = '';
|
||||
}
|
||||
|
||||
toApplyPenaltyCommand(raceId: string, driverId: string, stewardId: string, protestId: string): ApplyPenaltyCommandDTO {
|
||||
const reason = this.decision === 'uphold'
|
||||
? DEFAULT_PROTEST_REASON
|
||||
: 'Protest dismissed';
|
||||
toApplyPenaltyCommand(
|
||||
raceId: string,
|
||||
driverId: string,
|
||||
stewardId: string,
|
||||
protestId: string,
|
||||
options?: {
|
||||
requiresValue?: boolean;
|
||||
defaultUpheldReason?: string;
|
||||
defaultDismissedReason?: string;
|
||||
},
|
||||
): ApplyPenaltyCommandDTO {
|
||||
const reason =
|
||||
this.decision === 'uphold'
|
||||
? (options?.defaultUpheldReason ?? 'Protest upheld')
|
||||
: (options?.defaultDismissedReason ?? 'Protest dismissed');
|
||||
|
||||
return {
|
||||
const base: ApplyPenaltyCommandDTO = {
|
||||
raceId,
|
||||
driverId,
|
||||
stewardId,
|
||||
enum: this.penaltyType, // Use penaltyType as enum
|
||||
enum: this.penaltyType,
|
||||
type: this.penaltyType,
|
||||
value: this.getPenaltyValue(),
|
||||
reason,
|
||||
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;
|
||||
if (options?.requiresValue) {
|
||||
return { ...base, value: this.penaltyValue };
|
||||
}
|
||||
|
||||
return base;
|
||||
}
|
||||
}
|
||||
@@ -1,37 +1,32 @@
|
||||
type LeagueRole = 'owner' | 'admin' | 'steward' | 'member';
|
||||
export type LeagueRole = 'owner' | 'admin' | 'steward' | 'member';
|
||||
|
||||
export interface LeagueRoleDisplayData {
|
||||
text: string;
|
||||
badgeClasses: string;
|
||||
}
|
||||
|
||||
export const leagueRoleDisplay: Record<LeagueRole, LeagueRoleDisplayData> = {
|
||||
owner: {
|
||||
text: 'Owner',
|
||||
badgeClasses: 'bg-yellow-500/10 text-yellow-500 border-yellow-500/30',
|
||||
},
|
||||
admin: {
|
||||
text: 'Admin',
|
||||
badgeClasses: 'bg-purple-500/10 text-purple-400 border-purple-500/30',
|
||||
},
|
||||
steward: {
|
||||
text: 'Steward',
|
||||
badgeClasses: 'bg-blue-500/10 text-blue-400 border-blue-500/30',
|
||||
},
|
||||
member: {
|
||||
text: 'Member',
|
||||
badgeClasses: 'bg-primary-blue/10 text-primary-blue border-primary-blue/30',
|
||||
},
|
||||
} as const;
|
||||
|
||||
// For backward compatibility, also export the class with static method
|
||||
export class LeagueRoleDisplay {
|
||||
/**
|
||||
* Centralized display configuration for league membership roles.
|
||||
*/
|
||||
static getLeagueRoleDisplay(role: LeagueRole): LeagueRoleDisplayData {
|
||||
switch (role) {
|
||||
case 'owner':
|
||||
return {
|
||||
text: 'Owner',
|
||||
badgeClasses: 'bg-yellow-500/10 text-yellow-500 border-yellow-500/30',
|
||||
};
|
||||
case 'admin':
|
||||
return {
|
||||
text: 'Admin',
|
||||
badgeClasses: 'bg-purple-500/10 text-purple-400 border-purple-500/30',
|
||||
};
|
||||
case 'steward':
|
||||
return {
|
||||
text: 'Steward',
|
||||
badgeClasses: 'bg-blue-500/10 text-blue-400 border-blue-500/30',
|
||||
};
|
||||
case 'member':
|
||||
default:
|
||||
return {
|
||||
text: 'Member',
|
||||
badgeClasses: 'bg-primary-blue/10 text-primary-blue border-primary-blue/30',
|
||||
};
|
||||
}
|
||||
static getLeagueRoleDisplay(role: LeagueRole) {
|
||||
return leagueRoleDisplay[role];
|
||||
}
|
||||
}
|
||||
@@ -9,8 +9,9 @@ describe('PenaltyService', () => {
|
||||
beforeEach(() => {
|
||||
mockApiClient = {
|
||||
getRacePenalties: vi.fn(),
|
||||
getPenaltyTypesReference: vi.fn(),
|
||||
applyPenalty: vi.fn(),
|
||||
} as Mocked<PenaltiesApiClient>;
|
||||
} as unknown as Mocked<PenaltiesApiClient>;
|
||||
|
||||
service = new PenaltyService(mockApiClient);
|
||||
});
|
||||
@@ -23,9 +24,10 @@ describe('PenaltyService', () => {
|
||||
{ id: 'penalty-1', driverId: 'driver-1', type: 'time', value: 5, reason: 'Incident' },
|
||||
{ id: 'penalty-2', driverId: 'driver-2', type: 'grid', value: 3, reason: 'Qualifying incident' },
|
||||
],
|
||||
driverMap: {},
|
||||
};
|
||||
|
||||
mockApiClient.getRacePenalties.mockResolvedValue(mockDto);
|
||||
mockApiClient.getRacePenalties.mockResolvedValue(mockDto as any);
|
||||
|
||||
const result = await service.findByRaceId(raceId);
|
||||
|
||||
@@ -36,9 +38,9 @@ describe('PenaltyService', () => {
|
||||
|
||||
it('should handle empty penalties array', async () => {
|
||||
const raceId = 'race-123';
|
||||
const mockDto = { penalties: [] };
|
||||
const mockDto = { penalties: [], driverMap: {} };
|
||||
|
||||
mockApiClient.getRacePenalties.mockResolvedValue(mockDto);
|
||||
mockApiClient.getRacePenalties.mockResolvedValue(mockDto as any);
|
||||
|
||||
const result = await service.findByRaceId(raceId);
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { PenaltiesApiClient } from '../../api/penalties/PenaltiesApiClient';
|
||||
import type { PenaltyTypesReferenceDTO } from '../../types/PenaltyTypesReferenceDTO';
|
||||
|
||||
/**
|
||||
* Penalty Service
|
||||
@@ -19,6 +20,13 @@ export class PenaltyService {
|
||||
return dto.penalties;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get allowed penalty types and semantics
|
||||
*/
|
||||
async getPenaltyTypesReference(): Promise<PenaltyTypesReferenceDTO> {
|
||||
return this.apiClient.getPenaltyTypesReference();
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a penalty
|
||||
*/
|
||||
|
||||
@@ -19,17 +19,22 @@ describe('TeamJoinService', () => {
|
||||
const mockDto = {
|
||||
requests: [
|
||||
{
|
||||
id: 'request-1',
|
||||
requestId: 'request-1',
|
||||
teamId: 'team-1',
|
||||
driverId: 'driver-1',
|
||||
driverName: 'Driver One',
|
||||
status: 'pending',
|
||||
requestedAt: '2023-01-01T00:00:00Z',
|
||||
message: 'Please accept me',
|
||||
avatarUrl: 'https://example.com/avatar-1.jpg',
|
||||
},
|
||||
{
|
||||
id: 'request-2',
|
||||
requestId: 'request-2',
|
||||
teamId: 'team-1',
|
||||
driverId: 'driver-2',
|
||||
driverName: 'Driver Two',
|
||||
status: 'pending',
|
||||
requestedAt: '2023-01-02T00:00:00Z',
|
||||
avatarUrl: 'https://example.com/avatar-2.jpg',
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -40,20 +45,30 @@ describe('TeamJoinService', () => {
|
||||
|
||||
expect(mockApiClient.getJoinRequests).toHaveBeenCalledWith('team-1');
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0].id).toBe('request-1');
|
||||
expect(result[0].canApprove).toBe(true);
|
||||
expect(result[1].id).toBe('request-2');
|
||||
expect(result[1].canApprove).toBe(true);
|
||||
|
||||
const first = result[0];
|
||||
const second = result[1];
|
||||
|
||||
expect(first).toBeDefined();
|
||||
expect(second).toBeDefined();
|
||||
|
||||
expect(first!.id).toBe('request-1');
|
||||
expect(first!.canApprove).toBe(true);
|
||||
expect(second!.id).toBe('request-2');
|
||||
expect(second!.canApprove).toBe(true);
|
||||
});
|
||||
|
||||
it('should pass correct parameters to view model constructor', async () => {
|
||||
const mockDto = {
|
||||
requests: [
|
||||
{
|
||||
id: 'request-1',
|
||||
requestId: 'request-1',
|
||||
teamId: 'team-1',
|
||||
driverId: 'driver-1',
|
||||
driverName: 'Driver One',
|
||||
status: 'pending',
|
||||
requestedAt: '2023-01-01T00:00:00Z',
|
||||
avatarUrl: 'https://example.com/avatar-1.jpg',
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -62,7 +77,8 @@ describe('TeamJoinService', () => {
|
||||
|
||||
const result = await service.getJoinRequests('team-1', 'user-1', false);
|
||||
|
||||
expect(result[0].canApprove).toBe(false);
|
||||
expect(result[0]).toBeDefined();
|
||||
expect(result[0]!.canApprove).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ export class TeamJoinService {
|
||||
* a request requires a future management endpoint, so this method fails explicitly.
|
||||
*/
|
||||
async approveJoinRequest(): Promise<never> {
|
||||
throw new Error('Approving team join requests is not supported in this build');
|
||||
throw new Error('Not implemented: API endpoint for approving join requests');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -44,6 +44,6 @@ export class TeamJoinService {
|
||||
* must treat this as an unsupported operation rather than a silent no-op.
|
||||
*/
|
||||
async rejectJoinRequest(): Promise<never> {
|
||||
throw new Error('Rejecting team join requests is not supported in this build');
|
||||
throw new Error('Not implemented: API endpoint for rejecting join requests');
|
||||
}
|
||||
}
|
||||
@@ -4,45 +4,111 @@
|
||||
* Values are primarily sourced from environment variables so that
|
||||
* deployments can provide real company details without hard-coding
|
||||
* production data in the repository.
|
||||
*
|
||||
* Website must remain an API consumer (no adapter imports).
|
||||
*/
|
||||
|
||||
const env = {
|
||||
platformName: process.env.NEXT_PUBLIC_SITE_NAME,
|
||||
platformUrl: process.env.NEXT_PUBLIC_SITE_URL,
|
||||
supportEmail: process.env.NEXT_PUBLIC_SUPPORT_EMAIL,
|
||||
sponsorEmail: process.env.NEXT_PUBLIC_SPONSOR_EMAIL,
|
||||
legalCompanyName: process.env.NEXT_PUBLIC_LEGAL_COMPANY_NAME,
|
||||
legalVatId: process.env.NEXT_PUBLIC_LEGAL_VAT_ID,
|
||||
legalRegisteredCountry: process.env.NEXT_PUBLIC_LEGAL_REGISTERED_COUNTRY,
|
||||
legalRegisteredAddress: process.env.NEXT_PUBLIC_LEGAL_REGISTERED_ADDRESS,
|
||||
} as const;
|
||||
|
||||
export const siteConfig = {
|
||||
export interface SiteConfigData {
|
||||
// Platform Information
|
||||
platformName: env.platformName ?? 'GridPilot',
|
||||
platformUrl: env.platformUrl ?? 'https://gridpilot.com',
|
||||
platformName: string;
|
||||
platformUrl: string;
|
||||
|
||||
// Contact Information
|
||||
supportEmail: env.supportEmail ?? 'support@example.com',
|
||||
sponsorEmail: env.sponsorEmail ?? 'sponsors@example.com',
|
||||
supportEmail: string;
|
||||
sponsorEmail: string;
|
||||
|
||||
// Legal & Business Information
|
||||
legal: {
|
||||
companyName: env.legalCompanyName ?? '',
|
||||
vatId: env.legalVatId ?? '',
|
||||
registeredCountry: env.legalRegisteredCountry ?? '',
|
||||
registeredAddress: env.legalRegisteredAddress ?? '',
|
||||
},
|
||||
|
||||
companyName: string;
|
||||
vatId: string;
|
||||
registeredCountry: string;
|
||||
registeredAddress: string;
|
||||
};
|
||||
|
||||
// Platform Fees
|
||||
fees: {
|
||||
platformFeePercent: 10, // 10% platform fee on sponsorships
|
||||
description: 'Platform fee supports maintenance, analytics, and secure payment processing.',
|
||||
},
|
||||
|
||||
platformFeePercent: number;
|
||||
description: string;
|
||||
};
|
||||
|
||||
// VAT Information
|
||||
vat: {
|
||||
euReverseChargeApplies: boolean;
|
||||
nonEuVatExempt: boolean;
|
||||
standardRate: number;
|
||||
notice: string;
|
||||
euBusinessNotice: string;
|
||||
nonEuNotice: string;
|
||||
};
|
||||
|
||||
// Sponsorship Types Available
|
||||
sponsorshipTypes: {
|
||||
leagues: {
|
||||
enabled: boolean;
|
||||
title: string;
|
||||
description: string;
|
||||
};
|
||||
teams: {
|
||||
enabled: boolean;
|
||||
title: string;
|
||||
description: string;
|
||||
};
|
||||
drivers: {
|
||||
enabled: boolean;
|
||||
title: string;
|
||||
description: string;
|
||||
};
|
||||
races: {
|
||||
enabled: boolean;
|
||||
title: string;
|
||||
description: string;
|
||||
};
|
||||
platform: {
|
||||
enabled: boolean;
|
||||
title: string;
|
||||
description: string;
|
||||
};
|
||||
};
|
||||
|
||||
// Feature Flags for Sponsorship Features
|
||||
features: {
|
||||
liveryPlacement: boolean;
|
||||
leaguePageBranding: boolean;
|
||||
racePageBranding: boolean;
|
||||
profileBadges: boolean;
|
||||
socialMediaMentions: boolean;
|
||||
newsletterInclusion: boolean;
|
||||
homepageAds: boolean;
|
||||
sidebarAds: boolean;
|
||||
broadcastOverlays: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export const siteConfig: SiteConfigData = {
|
||||
// Platform Information
|
||||
platformName: process.env.NEXT_PUBLIC_SITE_NAME ?? 'GridPilot',
|
||||
platformUrl: process.env.NEXT_PUBLIC_SITE_URL ?? 'https://gridpilot.com',
|
||||
|
||||
// Contact Information
|
||||
supportEmail: process.env.NEXT_PUBLIC_SUPPORT_EMAIL ?? 'support@example.com',
|
||||
sponsorEmail: process.env.NEXT_PUBLIC_SPONSOR_EMAIL ?? 'sponsors@example.com',
|
||||
|
||||
// Legal & Business Information
|
||||
legal: {
|
||||
companyName: process.env.NEXT_PUBLIC_LEGAL_COMPANY_NAME ?? '',
|
||||
vatId: process.env.NEXT_PUBLIC_LEGAL_VAT_ID ?? '',
|
||||
registeredCountry: process.env.NEXT_PUBLIC_LEGAL_REGISTERED_COUNTRY ?? '',
|
||||
registeredAddress: process.env.NEXT_PUBLIC_LEGAL_REGISTERED_ADDRESS ?? '',
|
||||
},
|
||||
|
||||
// Platform Fees
|
||||
fees: {
|
||||
platformFeePercent: 10,
|
||||
description: 'Platform fee supports maintenance, analytics, and secure payment processing.',
|
||||
},
|
||||
|
||||
// VAT Information
|
||||
vat: {
|
||||
// Note: All prices displayed are exclusive of VAT
|
||||
euReverseChargeApplies: true,
|
||||
nonEuVatExempt: true,
|
||||
standardRate: 20,
|
||||
@@ -50,7 +116,7 @@ export const siteConfig = {
|
||||
euBusinessNotice: 'EU businesses with a valid VAT ID may apply reverse charge.',
|
||||
nonEuNotice: 'Non-EU businesses are not charged VAT.',
|
||||
},
|
||||
|
||||
|
||||
// Sponsorship Types Available
|
||||
sponsorshipTypes: {
|
||||
leagues: {
|
||||
@@ -79,10 +145,9 @@ export const siteConfig = {
|
||||
description: 'Reach the entire GridPilot audience with strategic platform placements.',
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
// Feature Flags for Sponsorship Features
|
||||
features: {
|
||||
// What sponsors can actually get (no broadcast control)
|
||||
liveryPlacement: true,
|
||||
leaguePageBranding: true,
|
||||
racePageBranding: true,
|
||||
@@ -91,8 +156,7 @@ export const siteConfig = {
|
||||
newsletterInclusion: true,
|
||||
homepageAds: true,
|
||||
sidebarAds: true,
|
||||
// We don't control these
|
||||
broadcastOverlays: false, // We don't control broadcast
|
||||
broadcastOverlays: false,
|
||||
},
|
||||
} as const;
|
||||
|
||||
|
||||
15
apps/website/lib/types/PenaltyTypesReferenceDTO.ts
Normal file
15
apps/website/lib/types/PenaltyTypesReferenceDTO.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export type PenaltyValueKindDTO = 'seconds' | 'grid_positions' | 'points' | 'races' | 'none';
|
||||
|
||||
export interface PenaltyTypeReferenceDTO {
|
||||
type: string;
|
||||
requiresValue: boolean;
|
||||
valueKind: PenaltyValueKindDTO;
|
||||
}
|
||||
|
||||
export interface PenaltyTypesReferenceDTO {
|
||||
penaltyTypes: PenaltyTypeReferenceDTO[];
|
||||
defaultReasons: {
|
||||
upheld: string;
|
||||
dismissed: string;
|
||||
};
|
||||
}
|
||||
@@ -12,4 +12,11 @@ export interface LeagueScoringPresetDTO {
|
||||
sessionSummary: string;
|
||||
bonusSummary: string;
|
||||
dropPolicySummary: string;
|
||||
defaultTimings: {
|
||||
practiceMinutes: number;
|
||||
qualifyingMinutes: number;
|
||||
sprintRaceMinutes: number;
|
||||
mainRaceMinutes: number;
|
||||
sessionCount: number;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -8,30 +8,8 @@ export type Timings = {
|
||||
};
|
||||
|
||||
export class ScoringPresetApplier {
|
||||
static applyToTimings(patternId: string, currentTimings: Timings): Timings {
|
||||
// Website-local fallback mapping (UI convenience only).
|
||||
// Authoritative preset/timing rules should live in the API.
|
||||
switch (patternId) {
|
||||
case 'sprint-main-driver':
|
||||
return {
|
||||
...currentTimings,
|
||||
practiceMinutes: currentTimings.practiceMinutes ?? 20,
|
||||
qualifyingMinutes: currentTimings.qualifyingMinutes ?? 30,
|
||||
sprintRaceMinutes: currentTimings.sprintRaceMinutes ?? 20,
|
||||
mainRaceMinutes: currentTimings.mainRaceMinutes ?? 40,
|
||||
sessionCount: 2,
|
||||
};
|
||||
case 'endurance-main-driver':
|
||||
return {
|
||||
...currentTimings,
|
||||
practiceMinutes: currentTimings.practiceMinutes ?? 30,
|
||||
qualifyingMinutes: currentTimings.qualifyingMinutes ?? 20,
|
||||
sprintRaceMinutes: 0,
|
||||
mainRaceMinutes: currentTimings.mainRaceMinutes ?? 120,
|
||||
sessionCount: 1,
|
||||
};
|
||||
default:
|
||||
return currentTimings;
|
||||
}
|
||||
static applyToTimings(_patternId: string, currentTimings: Timings): Timings {
|
||||
// Deprecated: timing defaults are provided by the API via scoring preset `defaultTimings`.
|
||||
return currentTimings;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
import { Clock, PlayCircle, CheckCircle2, XCircle } from 'lucide-react';
|
||||
|
||||
export const raceStatusConfig = {
|
||||
export interface RaceStatusConfigData {
|
||||
icon: any;
|
||||
color: string;
|
||||
bg: string;
|
||||
border: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export const raceStatusConfig: Record<string, RaceStatusConfigData> = {
|
||||
scheduled: {
|
||||
icon: Clock,
|
||||
color: 'text-primary-blue',
|
||||
@@ -29,4 +37,4 @@ export const raceStatusConfig = {
|
||||
border: 'border-warning-amber/30',
|
||||
label: 'Cancelled',
|
||||
},
|
||||
};
|
||||
} as const;
|
||||
@@ -14,7 +14,7 @@ export class DriverLeaderboardItemViewModel {
|
||||
avatarUrl: string;
|
||||
|
||||
position: number;
|
||||
private previousRating?: number;
|
||||
private previousRating: number | undefined;
|
||||
|
||||
constructor(dto: DriverLeaderboardItemDTO, position: number, previousRating?: number) {
|
||||
this.id = dto.id;
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
import { LeagueScoringPresetDTO } from '@/lib/types/generated/LeagueScoringPresetDTO';
|
||||
|
||||
export type LeagueScoringPresetTimingDefaultsViewModel = {
|
||||
practiceMinutes: number;
|
||||
qualifyingMinutes: number;
|
||||
sprintRaceMinutes: number;
|
||||
mainRaceMinutes: number;
|
||||
sessionCount: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* LeagueScoringPresetViewModel
|
||||
*
|
||||
@@ -10,11 +18,13 @@ export class LeagueScoringPresetViewModel {
|
||||
readonly name: string;
|
||||
readonly sessionSummary: string;
|
||||
readonly bonusSummary?: string;
|
||||
readonly defaultTimings: LeagueScoringPresetTimingDefaultsViewModel;
|
||||
|
||||
constructor(dto: LeagueScoringPresetDTO) {
|
||||
this.id = dto.id;
|
||||
this.name = dto.name;
|
||||
this.sessionSummary = dto.sessionSummary;
|
||||
this.bonusSummary = dto.bonusSummary;
|
||||
this.defaultTimings = dto.defaultTimings;
|
||||
}
|
||||
}
|
||||
@@ -32,4 +32,12 @@ export class RequestAvatarGenerationViewModel {
|
||||
get firstAvatarUrl(): string | undefined {
|
||||
return this.avatarUrls?.[0];
|
||||
}
|
||||
|
||||
get avatarUrl(): string | undefined {
|
||||
return this.firstAvatarUrl;
|
||||
}
|
||||
|
||||
get error(): string | undefined {
|
||||
return this.errorMessage;
|
||||
}
|
||||
}
|
||||
@@ -18,13 +18,13 @@ export class SponsorshipRequestViewModel {
|
||||
this.id = dto.id;
|
||||
this.sponsorId = dto.sponsorId;
|
||||
this.sponsorName = dto.sponsorName;
|
||||
this.sponsorLogo = dto.sponsorLogo;
|
||||
if (dto.sponsorLogo !== undefined) this.sponsorLogo = dto.sponsorLogo;
|
||||
// Backend currently returns tier as string; normalize to our supported tiers.
|
||||
this.tier = dto.tier === 'main' ? 'main' : 'secondary';
|
||||
this.offeredAmount = dto.offeredAmount;
|
||||
this.currency = dto.currency;
|
||||
this.formattedAmount = dto.formattedAmount;
|
||||
this.message = dto.message;
|
||||
if (dto.message !== undefined) this.message = dto.message;
|
||||
this.createdAt = new Date(dto.createdAt);
|
||||
this.platformFee = dto.platformFee;
|
||||
this.netAmount = dto.netAmount;
|
||||
|
||||
@@ -7,10 +7,10 @@ export class TeamDetailsViewModel {
|
||||
description?: string;
|
||||
ownerId!: string;
|
||||
leagues!: string[];
|
||||
createdAt?: string;
|
||||
specialization?: string;
|
||||
region?: string;
|
||||
languages?: string[];
|
||||
createdAt: string | undefined;
|
||||
specialization: string | undefined;
|
||||
region: string | undefined;
|
||||
languages: string[] | undefined;
|
||||
membership: { role: string; joinedAt: string; isActive: boolean } | null;
|
||||
private _canManage: boolean;
|
||||
private currentUserId: string;
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { TeamJoinRequestViewModel, type TeamJoinRequestDTO } from './TeamJoinRequestViewModel';
|
||||
import { TeamJoinRequestViewModel } from './TeamJoinRequestViewModel';
|
||||
import type { TeamJoinRequestDTO } from '@/lib/types/generated/TeamJoinRequestDTO';
|
||||
|
||||
const createTeamJoinRequestDto = (overrides: Partial<TeamJoinRequestDTO> = {}): TeamJoinRequestDTO => ({
|
||||
id: 'request-1',
|
||||
requestId: 'request-1',
|
||||
teamId: 'team-1',
|
||||
driverId: 'driver-1',
|
||||
driverName: 'Driver One',
|
||||
status: 'pending',
|
||||
requestedAt: '2024-01-01T12:00:00Z',
|
||||
message: 'Please let me join',
|
||||
avatarUrl: 'https://example.com/avatar.jpg',
|
||||
...overrides,
|
||||
});
|
||||
|
||||
describe('TeamJoinRequestViewModel', () => {
|
||||
it('maps fields from DTO', () => {
|
||||
const dto = createTeamJoinRequestDto({ id: 'req-123', driverId: 'driver-123' });
|
||||
const dto = createTeamJoinRequestDto({ requestId: 'req-123', driverId: 'driver-123' });
|
||||
|
||||
const vm = new TeamJoinRequestViewModel(dto, 'current-user', true);
|
||||
|
||||
@@ -20,7 +23,6 @@ describe('TeamJoinRequestViewModel', () => {
|
||||
expect(vm.teamId).toBe('team-1');
|
||||
expect(vm.driverId).toBe('driver-123');
|
||||
expect(vm.requestedAt).toBe('2024-01-01T12:00:00Z');
|
||||
expect(vm.message).toBe('Please let me join');
|
||||
});
|
||||
|
||||
it('allows approval only for owners', () => {
|
||||
@@ -34,7 +36,7 @@ describe('TeamJoinRequestViewModel', () => {
|
||||
});
|
||||
|
||||
it('exposes a pending status with yellow color', () => {
|
||||
const dto = createTeamJoinRequestDto();
|
||||
const dto = createTeamJoinRequestDto({ status: 'pending' });
|
||||
const vm = new TeamJoinRequestViewModel(dto, 'owner-user', true);
|
||||
|
||||
expect(vm.status).toBe('Pending');
|
||||
|
||||
@@ -24,6 +24,17 @@ export class TeamJoinRequestViewModel {
|
||||
this.isOwner = isOwner;
|
||||
}
|
||||
|
||||
get id(): string {
|
||||
return this.requestId;
|
||||
}
|
||||
|
||||
get status(): string {
|
||||
if (this.requestStatus === 'pending') return 'Pending';
|
||||
if (this.requestStatus === 'approved') return 'Approved';
|
||||
if (this.requestStatus === 'rejected') return 'Rejected';
|
||||
return this.requestStatus;
|
||||
}
|
||||
|
||||
/** UI-specific: Whether current user can approve */
|
||||
get canApprove(): boolean {
|
||||
return this.isOwner;
|
||||
|
||||
@@ -10,8 +10,8 @@ export class TeamSummaryViewModel {
|
||||
totalRaces: number = 0;
|
||||
performanceLevel: string = '';
|
||||
isRecruiting: boolean = false;
|
||||
specialization?: string;
|
||||
region?: string;
|
||||
specialization: string | undefined;
|
||||
region: string | undefined;
|
||||
languages: string[] = [];
|
||||
leagues: string[] = [];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user