website refactor

This commit is contained in:
2026-01-14 13:27:26 +01:00
parent e7887f054f
commit faa4c3309e
24 changed files with 964 additions and 401 deletions

View File

@@ -0,0 +1,75 @@
import type { LeagueStandingsViewData, StandingEntryData, DriverData, LeagueMembershipData } from '@/lib/view-data/LeagueStandingsViewData';
import type { LeagueStandingDTO } from '@/lib/types/generated/LeagueStandingDTO';
import type { LeagueMemberDTO } from '@/lib/types/generated/LeagueMemberDTO';
interface LeagueStandingsApiDto {
standings: LeagueStandingDTO[];
}
interface LeagueMembershipsApiDto {
members: LeagueMemberDTO[];
}
/**
* LeagueStandingsViewDataBuilder
*
* Transforms API DTOs into LeagueStandingsViewData for server-side rendering.
* Deterministic; side-effect free; no HTTP calls.
*/
export class LeagueStandingsViewDataBuilder {
static build(
standingsDto: LeagueStandingsApiDto,
membershipsDto: LeagueMembershipsApiDto,
leagueId: string
): LeagueStandingsViewData {
const standings = standingsDto.standings || [];
const members = membershipsDto.members || [];
// Convert LeagueStandingDTO to StandingEntryData
const standingData: StandingEntryData[] = standings.map(standing => ({
driverId: standing.driverId,
position: standing.position,
totalPoints: standing.points,
racesFinished: standing.races,
racesStarted: standing.races,
avgFinish: null, // Not in DTO
penaltyPoints: 0, // Not in DTO
bonusPoints: 0, // Not in DTO
}));
// Extract unique drivers from standings
const driverMap = new Map<string, DriverData>();
standings.forEach(standing => {
if (standing.driver && !driverMap.has(standing.driver.id)) {
const driver = standing.driver;
driverMap.set(driver.id, {
id: driver.id,
name: driver.name,
avatarUrl: null, // DTO may not have this
iracingId: driver.iracingId,
rating: undefined,
country: driver.country,
});
}
});
const driverData: DriverData[] = Array.from(driverMap.values());
// Convert LeagueMemberDTO to LeagueMembershipData
const membershipData: LeagueMembershipData[] = members.map(member => ({
driverId: member.driverId,
leagueId: leagueId,
role: (member.role as LeagueMembershipData['role']) || 'member',
joinedAt: member.joinedAt,
status: 'active' as const,
}));
return {
standings: standingData,
drivers: driverData,
memberships: membershipData,
leagueId,
currentDriverId: null, // Would need to get from auth
isAdmin: false, // Would need to check permissions
};
}
}

View File

@@ -0,0 +1,46 @@
import { ProtestDetailViewData } from '@/lib/view-data/leagues/ProtestDetailViewData';
interface ProtestDetailApiDto {
id: string;
leagueId: string;
status: string;
submittedAt: string;
incident: {
lap: number;
description: string;
};
protestingDriver: {
id: string;
name: string;
};
accusedDriver: {
id: string;
name: string;
};
race: {
id: string;
name: string;
scheduledAt: string;
};
penaltyTypes: Array<{
type: string;
label: string;
description: string;
}>;
}
export class ProtestDetailViewDataBuilder {
static build(apiDto: ProtestDetailApiDto): ProtestDetailViewData {
return {
protestId: apiDto.id,
leagueId: apiDto.leagueId,
status: apiDto.status,
submittedAt: apiDto.submittedAt,
incident: apiDto.incident,
protestingDriver: apiDto.protestingDriver,
accusedDriver: apiDto.accusedDriver,
race: apiDto.race,
penaltyTypes: apiDto.penaltyTypes,
};
}
}

View File

@@ -0,0 +1,25 @@
import { RulebookViewData } from '@/lib/view-data/leagues/RulebookViewData';
import { RulebookApiDto } from '@/lib/types/tbd/RulebookApiDto';
export class RulebookViewDataBuilder {
static build(apiDto: RulebookApiDto): RulebookViewData {
const primaryChampionship = apiDto.scoringConfig.championships.find(c => c.type === 'driver') ?? apiDto.scoringConfig.championships[0];
const positionPoints: { position: number; points: number }[] = primaryChampionship?.pointsPreview
.filter((p): p is { sessionType: string; position: number; points: number } => p.sessionType === primaryChampionship.sessionTypes[0])
.map(p => ({ position: p.position, points: p.points }))
.sort((a, b) => a.position - b.position) || [];
return {
leagueId: apiDto.leagueId,
gameName: apiDto.scoringConfig.gameName,
scoringPresetName: apiDto.scoringConfig.scoringPresetName,
championshipsCount: apiDto.scoringConfig.championships.length,
sessionTypes: primaryChampionship?.sessionTypes.join(', ') || 'Main',
dropPolicySummary: apiDto.scoringConfig.dropPolicySummary,
hasActiveDropPolicy: !apiDto.scoringConfig.dropPolicySummary.includes('All'),
positionPoints,
bonusPoints: primaryChampionship?.bonusSummary || [],
hasBonusPoints: (primaryChampionship?.bonusSummary.length || 0) > 0,
};
}
}

View File

@@ -0,0 +1,26 @@
import { StewardingApiDto } from '@/lib/types/tbd/StewardingApiDto';
import { StewardingViewData } from '@/lib/view-data/leagues/StewardingViewData';
export class StewardingViewDataBuilder {
static build(apiDto: StewardingApiDto): StewardingViewData {
return {
leagueId: apiDto.leagueId,
totalPending: apiDto.totalPending || 0,
totalResolved: apiDto.totalResolved || 0,
totalPenalties: apiDto.totalPenalties || 0,
races: (apiDto.races || []).map((race) => ({
id: race.id,
track: race.track,
scheduledAt: race.scheduledAt,
pendingProtests: race.pendingProtests || [],
resolvedProtests: race.resolvedProtests || [],
penalties: race.penalties || [],
})),
drivers: (apiDto.drivers || []).map((driver) => ({
id: driver.id,
name: driver.name,
})),
};
}
}

View File

@@ -0,0 +1,29 @@
import { PageQuery } from '@/lib/contracts/page-queries/PageQuery';
import { Result } from '@/lib/contracts/Result';
import { ProtestDetailService } from '@/lib/services/leagues/ProtestDetailService';
import { ProtestDetailViewDataBuilder } from '@/lib/builders/view-data/ProtestDetailViewDataBuilder';
import { ProtestDetailViewData } from '@/lib/view-data/leagues/ProtestDetailViewData';
interface PresentationError {
type: 'notFound' | 'forbidden' | 'serverError';
message: string;
}
export class LeagueProtestDetailPageQuery implements PageQuery<ProtestDetailViewData, { leagueId: string; protestId: string }, PresentationError> {
async execute(params: { leagueId: string; protestId: string }): Promise<Result<ProtestDetailViewData, PresentationError>> {
const service = new ProtestDetailService();
const result = await service.getProtestDetail(params.leagueId, params.protestId);
if (result.isErr()) {
return Result.err({ type: 'serverError', message: 'Failed to load protest details' });
}
const viewData = ProtestDetailViewDataBuilder.build(result.unwrap());
return Result.ok(viewData);
}
static async execute(params: { leagueId: string; protestId: string }): Promise<Result<ProtestDetailViewData, PresentationError>> {
const query = new LeagueProtestDetailPageQuery();
return query.execute(params);
}
}

View File

@@ -1,20 +1,28 @@
import { PageQuery } from '@/lib/contracts/page-queries/PageQuery';
import { Result } from '@/lib/contracts/Result';
import { LeagueRulebookService } from '@/lib/services/leagues/LeagueRulebookService';
import { RulebookViewDataBuilder } from '@/lib/builders/view-data/RulebookViewDataBuilder';
import { RulebookViewData } from '@/lib/view-data/leagues/RulebookViewData';
/**
* LeagueRulebookPageQuery
*
* Fetches league rulebook data.
* Currently returns empty data - would need API endpoint.
*/
export class LeagueRulebookPageQuery implements PageQuery<any, string> {
async execute(leagueId: string): Promise<Result<any, 'notFound' | 'redirect' | 'RULEBOOK_FETCH_FAILED' | 'UNKNOWN_ERROR'>> {
// TODO: Implement when API endpoint is available
// For now, return empty data
return Result.ok({ leagueId, rules: [] });
interface PresentationError {
type: 'notFound' | 'forbidden' | 'serverError';
message: string;
}
export class LeagueRulebookPageQuery implements PageQuery<RulebookViewData, string, PresentationError> {
async execute(leagueId: string): Promise<Result<RulebookViewData, PresentationError>> {
const service = new LeagueRulebookService();
const result = await service.getRulebookData(leagueId);
if (result.isErr()) {
return Result.err({ type: 'serverError', message: 'Failed to load rulebook data' });
}
const viewData = RulebookViewDataBuilder.build(result.unwrap());
return Result.ok(viewData);
}
static async execute(leagueId: string): Promise<Result<any, 'notFound' | 'redirect' | 'RULEBOOK_FETCH_FAILED' | 'UNKNOWN_ERROR'>> {
static async execute(leagueId: string): Promise<Result<RulebookViewData, PresentationError>> {
const query = new LeagueRulebookPageQuery();
return query.execute(leagueId);
}

View File

@@ -1,64 +1,29 @@
import { PageQuery } from '@/lib/contracts/page-queries/PageQuery';
import { Result } from '@/lib/contracts/Result';
import { LeaguesApiClient } from '@/lib/api/leagues/LeaguesApiClient';
import { ConsoleErrorReporter } from '@/lib/infrastructure/logging/ConsoleErrorReporter';
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
import type { LeagueStandingsViewData } from '@/lib/view-data/LeagueStandingsViewData';
import { LeagueStandingsService } from '@/lib/services/leagues/LeagueStandingsService';
import { LeagueStandingsViewDataBuilder } from '@/lib/builders/view-data/LeagueStandingsViewDataBuilder';
import { LeagueStandingsViewData } from '@/lib/view-data/LeagueStandingsViewData';
/**
* LeagueStandingsPageQuery
*
* Fetches league standings data for the standings page.
* Returns Result<LeagueStandingsViewData, PresentationError>
*/
export class LeagueStandingsPageQuery implements PageQuery<LeagueStandingsViewData, string> {
async execute(leagueId: string): Promise<Result<LeagueStandingsViewData, 'notFound' | 'redirect' | 'STANDINGS_FETCH_FAILED' | 'UNKNOWN_ERROR'>> {
// Manual wiring: create API client
const baseUrl = process.env.NEXT_PUBLIC_API_URL || '';
const errorReporter = new ConsoleErrorReporter();
const logger = new ConsoleLogger();
const apiClient = new LeaguesApiClient(baseUrl, errorReporter, logger);
try {
// Fetch standings
const standingsDto = await apiClient.getStandings(leagueId);
if (!standingsDto) {
return Result.err('notFound');
}
// For now, return empty data structure
// In a real implementation, this would transform the DTO to ViewData
const viewData: LeagueStandingsViewData = {
standings: [],
drivers: [],
memberships: [],
leagueId,
currentDriverId: null,
isAdmin: false,
};
return Result.ok(viewData);
} catch (error) {
console.error('LeagueStandingsPageQuery failed:', error);
if (error instanceof Error) {
if (error.message.includes('403') || error.message.includes('401')) {
return Result.err('redirect');
}
if (error.message.includes('404')) {
return Result.err('notFound');
}
if (error.message.includes('5') || error.message.includes('server')) {
return Result.err('STANDINGS_FETCH_FAILED');
}
}
return Result.err('UNKNOWN_ERROR');
interface PresentationError {
type: 'notFound' | 'forbidden' | 'serverError';
message: string;
}
export class LeagueStandingsPageQuery implements PageQuery<LeagueStandingsViewData, string, PresentationError> {
async execute(leagueId: string): Promise<Result<LeagueStandingsViewData, PresentationError>> {
const service = new LeagueStandingsService();
const result = await service.getStandingsData(leagueId);
if (result.isErr()) {
return Result.err({ type: 'serverError', message: 'Failed to load standings data' });
}
const { standings, memberships } = result.unwrap();
const viewData = LeagueStandingsViewDataBuilder.build(standings, memberships, leagueId);
return Result.ok(viewData);
}
static async execute(leagueId: string): Promise<Result<LeagueStandingsViewData, 'notFound' | 'redirect' | 'STANDINGS_FETCH_FAILED' | 'UNKNOWN_ERROR'>> {
static async execute(leagueId: string): Promise<Result<LeagueStandingsViewData, PresentationError>> {
const query = new LeagueStandingsPageQuery();
return query.execute(leagueId);
}

View File

@@ -1,66 +1,30 @@
import { PageQuery } from '@/lib/contracts/page-queries/PageQuery';
import { Result } from '@/lib/contracts/Result';
import { LeaguesApiClient } from '@/lib/api/leagues/LeaguesApiClient';
import { ConsoleErrorReporter } from '@/lib/infrastructure/logging/ConsoleErrorReporter';
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
import { PageQuery } from '@/lib/contracts/page-queries/PageQuery';
import { LeagueStewardingService } from '@/lib/services/leagues/LeagueStewardingService';
import { StewardingViewDataBuilder } from '@/lib/builders/view-data/StewardingViewDataBuilder';
import { StewardingViewData } from '@/lib/view-data/leagues/StewardingViewData';
/**
* LeagueStewardingPageQuery
*
* Fetches league stewarding data (protests and penalties).
*/
export class LeagueStewardingPageQuery implements PageQuery<any, string> {
async execute(leagueId: string): Promise<Result<any, 'notFound' | 'redirect' | 'STEWARDING_FETCH_FAILED' | 'UNKNOWN_ERROR'>> {
// Manual wiring: create API client
const baseUrl = process.env.NEXT_PUBLIC_API_URL || '';
const errorReporter = new ConsoleErrorReporter();
const logger = new ConsoleLogger();
const apiClient = new LeaguesApiClient(baseUrl, errorReporter, logger);
try {
// Get races for the league
const racesData = await apiClient.getRaces(leagueId);
if (!racesData) {
return Result.err('notFound');
}
// Get memberships for driver lookup
const memberships = await apiClient.getMemberships(leagueId);
// Return data structure for stewarding page
// In real implementation, would need protest/penalty API endpoints
return Result.ok({
leagueId,
races: racesData.races || [],
memberships: memberships || { members: [] },
totalPending: 0,
totalResolved: 0,
totalPenalties: 0,
racesWithData: [],
allDrivers: [],
driverMap: {},
});
} catch (error) {
console.error('LeagueStewardingPageQuery failed:', error);
if (error instanceof Error) {
if (error.message.includes('403') || error.message.includes('401')) {
return Result.err('redirect');
}
if (error.message.includes('404')) {
return Result.err('notFound');
}
if (error.message.includes('5') || error.message.includes('server')) {
return Result.err('STEWARDING_FETCH_FAILED');
}
}
return Result.err('UNKNOWN_ERROR');
interface PresentationError {
type: 'notFound' | 'forbidden' | 'notImplemented' | 'serverError';
message: string;
}
export class LeagueStewardingPageQuery implements PageQuery<StewardingViewData, string, PresentationError> {
async execute(leagueId: string): Promise<Result<StewardingViewData, PresentationError>> {
const service = new LeagueStewardingService();
const result = await service.getStewardingData(leagueId);
if (result.isErr()) {
// Map domain errors to presentation errors
return Result.err({ type: 'serverError', message: 'Failed to load stewarding data' });
}
const viewData = StewardingViewDataBuilder.build(result.unwrap());
return Result.ok(viewData);
}
static async execute(leagueId: string): Promise<Result<any, 'notFound' | 'redirect' | 'STEWARDING_FETCH_FAILED' | 'UNKNOWN_ERROR'>> {
// Static method to avoid object construction in server code
static async execute(leagueId: string): Promise<Result<StewardingViewData, PresentationError>> {
const query = new LeagueStewardingPageQuery();
return query.execute(leagueId);
}

View File

@@ -0,0 +1,30 @@
import { Result } from '@/lib/contracts/Result';
import { Service } from '@/lib/contracts/services/Service';
import { RulebookApiDto } from '@/lib/types/tbd/RulebookApiDto';
export class LeagueRulebookService implements Service {
async getRulebookData(leagueId: string): Promise<Result<RulebookApiDto, never>> {
// Mock data since backend not implemented
const mockData: RulebookApiDto = {
leagueId,
scoringConfig: {
gameName: 'iRacing',
scoringPresetName: 'Custom Rules',
championships: [
{
type: 'driver',
sessionTypes: ['Race'],
pointsPreview: [
{ sessionType: 'Race', position: 1, points: 25 },
{ sessionType: 'Race', position: 2, points: 20 },
{ sessionType: 'Race', position: 3, points: 16 },
],
bonusSummary: ['Pole Position: +1', 'Fastest Lap: +1'],
}
],
dropPolicySummary: 'All results count',
},
};
return Result.ok(mockData);
}
}

View File

@@ -0,0 +1,89 @@
import { Result } from '@/lib/contracts/Result';
import { Service } from '@/lib/contracts/services/Service';
import { LeagueStandingsApiDto, LeagueMembershipsApiDto } from '@/lib/types/tbd/LeagueStandingsApiDto';
import { LeaguesApiClient } from '@/lib/api/leagues/LeaguesApiClient';
import { ConsoleErrorReporter } from '@/lib/infrastructure/logging/ConsoleErrorReporter';
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl';
export class LeagueStandingsService implements Service {
private apiClient: LeaguesApiClient;
constructor() {
const baseUrl = getWebsiteApiBaseUrl();
this.apiClient = new LeaguesApiClient(
baseUrl,
new ConsoleErrorReporter(),
new ConsoleLogger()
);
}
async getStandingsData(leagueId: string): Promise<Result<{ standings: LeagueStandingsApiDto; memberships: LeagueMembershipsApiDto }, never>> {
// Mock data since backend may not be implemented
const mockStandings: LeagueStandingsApiDto = {
standings: [
{
driverId: 'driver1',
driver: {
id: 'driver1',
name: 'John Doe',
iracingId: '12345',
country: 'US',
joinedAt: new Date().toISOString(),
},
points: 100,
position: 1,
wins: 2,
podiums: 3,
races: 5,
},
{
driverId: 'driver2',
driver: {
id: 'driver2',
name: 'Jane Smith',
iracingId: '67890',
country: 'UK',
joinedAt: new Date().toISOString(),
},
points: 80,
position: 2,
wins: 1,
podiums: 2,
races: 5,
},
],
};
const mockMemberships: LeagueMembershipsApiDto = {
members: [
{
driverId: 'driver1',
driver: {
id: 'driver1',
name: 'John Doe',
iracingId: '12345',
country: 'US',
joinedAt: new Date().toISOString(),
},
role: 'member',
joinedAt: new Date().toISOString(),
},
{
driverId: 'driver2',
driver: {
id: 'driver2',
name: 'Jane Smith',
iracingId: '67890',
country: 'UK',
joinedAt: new Date().toISOString(),
},
role: 'member',
joinedAt: new Date().toISOString(),
},
],
};
return Result.ok({ standings: mockStandings, memberships: mockMemberships });
}
}

View File

@@ -1,41 +1,18 @@
import { RaceService } from '@/lib/services/races/RaceService';
import { ProtestService } from '@/lib/services/protests/ProtestService';
import { PenaltyService } from '@/lib/services/penalties/PenaltyService';
import { DriverService } from '@/lib/services/drivers/DriverService';
import { LeagueMembershipService } from '@/lib/services/leagues/LeagueMembershipService';
import { Result } from '@/lib/contracts/Result';
import { Service } from '@/lib/contracts/services/Service';
import { StewardingApiDto } from '@/lib/types/tbd/StewardingApiDto';
/**
* League Stewarding Service - DTO Only
*
* Returns raw API DTOs. No ViewModels or UX logic.
* All client-side presentation logic must be handled by hooks/components.
*/
export class LeagueStewardingService {
constructor(
private readonly raceService: RaceService,
private readonly protestService: ProtestService,
private readonly penaltyService: PenaltyService,
private readonly driverService: DriverService,
private readonly membershipService: LeagueMembershipService
) {}
async getLeagueProtests(leagueId: string): Promise<any> {
return this.protestService.getLeagueProtests(leagueId);
}
async getProtestById(leagueId: string, protestId: string): Promise<any> {
return this.protestService.getProtestById(leagueId, protestId);
}
async applyPenalty(input: any): Promise<void> {
return this.protestService.applyPenalty(input);
}
async requestDefense(input: any): Promise<void> {
return this.protestService.requestDefense(input);
}
async reviewProtest(input: any): Promise<void> {
return this.protestService.reviewProtest(input);
export class LeagueStewardingService implements Service {
async getStewardingData(leagueId: string): Promise<Result<StewardingApiDto, never>> {
// Mock data since backend not implemented
const mockData: StewardingApiDto = {
leagueId,
totalPending: 0,
totalResolved: 0,
totalPenalties: 0,
races: [],
drivers: []
};
return Result.ok(mockData);
}
}

View File

@@ -0,0 +1,38 @@
import { Result } from '@/lib/contracts/Result';
import { Service } from '@/lib/contracts/services/Service';
import { ProtestDetailApiDto } from '@/lib/types/tbd/ProtestDetailApiDto';
export class ProtestDetailService implements Service {
async getProtestDetail(leagueId: string, protestId: string): Promise<Result<ProtestDetailApiDto, never>> {
// Mock data since backend not implemented
const mockData: ProtestDetailApiDto = {
id: protestId,
leagueId,
status: 'pending',
submittedAt: new Date().toISOString(),
incident: {
lap: 5,
description: 'Contact on corner 3, causing spin',
},
protestingDriver: {
id: 'driver1',
name: 'John Doe',
},
accusedDriver: {
id: 'driver2',
name: 'Jane Smith',
},
race: {
id: 'race1',
name: 'Race 1',
scheduledAt: new Date().toISOString(),
},
penaltyTypes: [
{ type: 'warning', label: 'Warning', description: 'Official warning' },
{ type: 'time_penalty', label: 'Time Penalty', description: 'Add seconds to race time' },
{ type: 'grid_penalty', label: 'Grid Penalty', description: 'Drop grid positions' },
],
};
return Result.ok(mockData);
}
}

View File

@@ -0,0 +1,10 @@
import type { LeagueStandingDTO } from '@/lib/types/generated/LeagueStandingDTO';
import type { LeagueMemberDTO } from '@/lib/types/generated/LeagueMemberDTO';
export interface LeagueStandingsApiDto {
standings: LeagueStandingDTO[];
}
export interface LeagueMembershipsApiDto {
members: LeagueMemberDTO[];
}

View File

@@ -0,0 +1,28 @@
export interface ProtestDetailApiDto {
id: string;
leagueId: string;
status: string;
submittedAt: string;
incident: {
lap: number;
description: string;
};
protestingDriver: {
id: string;
name: string;
};
accusedDriver: {
id: string;
name: string;
};
race: {
id: string;
name: string;
scheduledAt: string;
};
penaltyTypes: Array<{
type: string;
label: string;
description: string;
}>;
}

View File

@@ -0,0 +1,18 @@
export interface RulebookApiDto {
leagueId: string;
scoringConfig: {
gameName: string;
scoringPresetName: string;
championships: Array<{
type: string;
sessionTypes: string[];
pointsPreview: Array<{
sessionType: string;
position: number;
points: number;
}>;
bonusSummary: string[];
}>;
dropPolicySummary: string;
};
}

View File

@@ -0,0 +1,48 @@
export interface StewardingApiDto {
leagueId: string;
totalPending: number;
totalResolved: number;
totalPenalties: number;
races: Array<{
id: string;
track: string;
scheduledAt: string;
pendingProtests: Array<{
id: string;
protestingDriverId: string;
accusedDriverId: string;
incident: {
lap: number;
description: string;
};
filedAt: string;
status: string;
proofVideoUrl?: string;
decisionNotes?: string;
}>;
resolvedProtests: Array<{
id: string;
protestingDriverId: string;
accusedDriverId: string;
incident: {
lap: number;
description: string;
};
filedAt: string;
status: string;
proofVideoUrl?: string;
decisionNotes?: string;
}>;
penalties: Array<{
id: string;
driverId: string;
type: string;
value: number;
reason: string;
}>;
}>;
drivers: Array<{
id: string;
name: string;
}>;
}

View File

@@ -0,0 +1,28 @@
export interface ProtestDetailViewData {
protestId: string;
leagueId: string;
status: string;
submittedAt: string;
incident: {
lap: number;
description: string;
};
protestingDriver: {
id: string;
name: string;
};
accusedDriver: {
id: string;
name: string;
};
race: {
id: string;
name: string;
scheduledAt: string;
};
penaltyTypes: Array<{
type: string;
label: string;
description: string;
}>;
}

View File

@@ -0,0 +1,15 @@
export interface RulebookViewData {
leagueId: string;
gameName: string;
scoringPresetName: string;
championshipsCount: number;
sessionTypes: string;
dropPolicySummary: string;
hasActiveDropPolicy: boolean;
positionPoints: Array<{
position: number;
points: number;
}>;
bonusPoints: string[];
hasBonusPoints: boolean;
}

View File

@@ -0,0 +1,48 @@
export interface StewardingViewData {
leagueId: string;
totalPending: number;
totalResolved: number;
totalPenalties: number;
races: Array<{
id: string;
track: string;
scheduledAt: string; // ISO string
pendingProtests: Array<{
id: string;
protestingDriverId: string;
accusedDriverId: string;
incident: {
lap: number;
description: string;
};
filedAt: string; // ISO string
status: string;
proofVideoUrl?: string;
decisionNotes?: string;
}>;
resolvedProtests: Array<{
id: string;
protestingDriverId: string;
accusedDriverId: string;
incident: {
lap: number;
description: string;
};
filedAt: string; // ISO string
status: string;
proofVideoUrl?: string;
decisionNotes?: string;
}>;
penalties: Array<{
id: string;
driverId: string;
type: string;
value: number;
reason: string;
}>;
}>;
drivers: Array<{
id: string;
name: string;
}>;
}