website cleanup

This commit is contained in:
2025-12-24 21:44:58 +01:00
parent 9b683a59d3
commit d78854a4c6
277 changed files with 6141 additions and 2693 deletions

View File

@@ -1,7 +1,12 @@
import { DashboardOverviewDto, DriverDto, RaceDto, LeagueStandingDto, FeedItemDto, FriendDto } from '../api/dashboard/DashboardApiClient';
import type { DashboardOverviewDTO } from '@/lib/types/generated/DashboardOverviewDTO';
import type { DashboardDriverSummaryDTO } from '@/lib/types/generated/DashboardDriverSummaryDTO';
import type { DashboardRaceSummaryDTO } from '@/lib/types/generated/DashboardRaceSummaryDTO';
import type { DashboardLeagueStandingSummaryDTO } from '@/lib/types/generated/DashboardLeagueStandingSummaryDTO';
import type { DashboardFeedItemSummaryDTO } from '@/lib/types/generated/DashboardFeedItemSummaryDTO';
import type { DashboardFriendSummaryDTO } from '@/lib/types/generated/DashboardFriendSummaryDTO';
export class DriverViewModel {
constructor(private readonly dto: DriverDto) {}
export class DashboardDriverSummaryViewModel {
constructor(private readonly dto: DashboardDriverSummaryDTO) {}
get id(): string {
return this.dto.id;
@@ -32,25 +37,33 @@ export class DriverViewModel {
}
get rating(): number {
return this.dto.rating;
return this.dto.rating ?? 0;
}
get globalRank(): number {
return this.dto.globalRank;
return this.dto.globalRank ?? 0;
}
get consistency(): number {
return this.dto.consistency;
return this.dto.consistency ?? 0;
}
}
export class RaceViewModel {
constructor(private readonly dto: RaceDto) {}
export class DashboardRaceSummaryViewModel {
constructor(private readonly dto: DashboardRaceSummaryDTO) {}
get id(): string {
return this.dto.id;
}
get leagueId(): string {
return this.dto.leagueId;
}
get leagueName(): string {
return this.dto.leagueName;
}
get track(): string {
return this.dto.track;
}
@@ -63,17 +76,17 @@ export class RaceViewModel {
return new Date(this.dto.scheduledAt);
}
get status(): string {
return this.dto.status;
}
get isMyLeague(): boolean {
return this.dto.isMyLeague;
}
get leagueName(): string | undefined {
return this.dto.leagueName;
}
}
export class LeagueStandingViewModel {
constructor(private readonly dto: LeagueStandingDto) {}
export class DashboardLeagueStandingSummaryViewModel {
constructor(private readonly dto: DashboardLeagueStandingSummaryDTO) {}
get leagueId(): string {
return this.dto.leagueId;
@@ -97,7 +110,7 @@ export class LeagueStandingViewModel {
}
export class DashboardFeedItemSummaryViewModel {
constructor(private readonly dto: FeedItemDto) {}
constructor(private readonly dto: DashboardFeedItemSummaryDTO) {}
get id(): string {
return this.dto.id;
@@ -111,7 +124,7 @@ export class DashboardFeedItemSummaryViewModel {
return this.dto.headline;
}
get body(): string | null {
get body(): string | undefined {
return this.dto.body;
}
@@ -128,8 +141,8 @@ export class DashboardFeedItemSummaryViewModel {
}
}
export class FriendViewModel {
constructor(private readonly dto: FriendDto) {}
export class DashboardFriendSummaryViewModel {
constructor(private readonly dto: DashboardFriendSummaryDTO) {}
get id(): string {
return this.dto.id;
@@ -149,33 +162,42 @@ export class FriendViewModel {
}
export class DashboardOverviewViewModel {
constructor(private readonly dto: DashboardOverviewDto) {}
constructor(private readonly dto: DashboardOverviewDTO) {}
get currentDriver(): DriverViewModel {
return new DriverViewModel(this.dto.currentDriver);
get currentDriver(): DashboardDriverSummaryViewModel {
// DTO uses optional property; enforce a consistent object for the UI
return new DashboardDriverSummaryViewModel(this.dto.currentDriver ?? {
id: '',
name: '',
country: '',
avatarUrl: '',
totalRaces: 0,
wins: 0,
podiums: 0,
});
}
get nextRace(): RaceViewModel | null {
return this.dto.nextRace ? new RaceViewModel(this.dto.nextRace) : null;
get nextRace(): DashboardRaceSummaryViewModel | null {
return this.dto.nextRace ? new DashboardRaceSummaryViewModel(this.dto.nextRace) : null;
}
get upcomingRaces(): RaceViewModel[] {
return this.dto.upcomingRaces.map(dto => new RaceViewModel(dto));
get upcomingRaces(): DashboardRaceSummaryViewModel[] {
return this.dto.upcomingRaces.map((r) => new DashboardRaceSummaryViewModel(r));
}
get leagueStandings(): LeagueStandingViewModel[] {
return this.dto.leagueStandings.map(dto => new LeagueStandingViewModel(dto));
get leagueStandings(): DashboardLeagueStandingSummaryViewModel[] {
return this.dto.leagueStandingsSummaries.map((s) => new DashboardLeagueStandingSummaryViewModel(s));
}
get feedItems(): DashboardFeedItemSummaryViewModel[] {
return this.dto.feedItems.map(dto => new DashboardFeedItemSummaryViewModel(dto));
return this.dto.feedSummary.items.map((i) => new DashboardFeedItemSummaryViewModel(i));
}
get friends(): FriendViewModel[] {
return this.dto.friends.map(dto => new FriendViewModel(dto));
get friends(): DashboardFriendSummaryViewModel[] {
return this.dto.friends.map((f) => new DashboardFriendSummaryViewModel(f));
}
get activeLeaguesCount(): number {
return this.dto.activeLeaguesCount;
}
}
}

View File

@@ -1,4 +1,4 @@
import { DriverLeaderboardItemDTO } from '../types/generated/DriverLeaderboardItemDTO';
import type { DriverLeaderboardItemDTO } from '../types/generated/DriverLeaderboardItemDTO';
export class DriverLeaderboardItemViewModel {
id: string;
@@ -16,7 +16,7 @@ export class DriverLeaderboardItemViewModel {
position: number;
private previousRating?: number;
constructor(dto: DriverLeaderboardItemDTO & { avatarUrl: string }, position: number, previousRating?: number) {
constructor(dto: DriverLeaderboardItemDTO, position: number, previousRating?: number) {
this.id = dto.id;
this.name = dto.name;
this.rating = dto.rating;
@@ -27,7 +27,7 @@ export class DriverLeaderboardItemViewModel {
this.podiums = dto.podiums;
this.isActive = dto.isActive;
this.rank = dto.rank;
this.avatarUrl = dto.avatarUrl;
this.avatarUrl = dto.avatarUrl ?? '';
this.position = position;
this.previousRating = previousRating;
}
@@ -84,4 +84,4 @@ export class DriverLeaderboardItemViewModel {
get positionBadge(): string {
return this.position.toString();
}
}
}

View File

@@ -4,7 +4,10 @@ import { DriverLeaderboardItemViewModel } from './DriverLeaderboardItemViewModel
export class DriverLeaderboardViewModel {
drivers: DriverLeaderboardItemViewModel[];
constructor(dto: { drivers: (DriverLeaderboardItemDTO & { avatarUrl: string })[] }, previousDrivers?: (DriverLeaderboardItemDTO & { avatarUrl: string })[]) {
constructor(
dto: { drivers: DriverLeaderboardItemDTO[] },
previousDrivers?: DriverLeaderboardItemDTO[],
) {
this.drivers = dto.drivers.map((driver, index) => {
const previous = previousDrivers?.find(p => p.id === driver.id);
return new DriverLeaderboardItemViewModel(driver, index + 1, previous?.rating);
@@ -25,4 +28,4 @@ export class DriverLeaderboardViewModel {
get activeCount(): number {
return this.drivers.filter(driver => driver.isActive).length;
}
}
}

View File

@@ -9,8 +9,8 @@ import type { LeagueMemberDTO } from '../types/generated/LeagueMemberDTO';
export class LeagueMembershipsViewModel {
memberships: LeagueMemberViewModel[];
constructor(dto: { memberships: LeagueMemberDTO[] }, currentUserId: string) {
this.memberships = dto.memberships.map(membership => new LeagueMemberViewModel(membership, currentUserId));
constructor(dto: { members: LeagueMemberDTO[] }, currentUserId: string) {
this.memberships = dto.members.map(membership => new LeagueMemberViewModel(membership, currentUserId));
}
/** UI-specific: Number of members */
@@ -22,4 +22,4 @@ export class LeagueMembershipsViewModel {
get hasMembers(): boolean {
return this.memberCount > 0;
}
}
}

View File

@@ -1,12 +1,15 @@
import { describe, it, expect } from 'vitest';
import { LeagueScoringPresetsViewModel } from './LeagueScoringPresetsViewModel';
import type { LeagueScoringPresetDTO } from '@core/racing/application/ports/LeagueScoringPresetProvider';
import type { LeagueScoringPresetDTO } from '@/lib/types/generated/LeagueScoringPresetDTO';
const createPreset = (overrides: Partial<LeagueScoringPresetDTO> = {}): LeagueScoringPresetDTO => ({
id: 'preset-1',
name: 'Standard scoring',
description: 'Top 15 get points',
gameId: 'iracing',
primaryChampionshipType: 'driver',
sessionSummary: 'Sprint + Main',
bonusSummary: 'None',
dropPolicySummary: 'Best 6',
...overrides,
} as LeagueScoringPresetDTO);

View File

@@ -1,14 +1,49 @@
import { describe, it, expect } from 'vitest';
import { LeagueSettingsViewModel } from './LeagueSettingsViewModel';
import { DriverSummaryViewModel } from './DriverSummaryViewModel';
import type { LeagueConfigFormModel } from '@core/racing/application';
import type { LeagueScoringPresetDTO } from '@core/racing/application/ports/LeagueScoringPresetProvider';
import type { LeagueConfigFormModel } from '@/lib/types/LeagueConfigFormModel';
import type { LeagueScoringPresetDTO } from '@/lib/types/generated/LeagueScoringPresetDTO';
const createConfig = (overrides: Partial<LeagueConfigFormModel> = {}): LeagueConfigFormModel => ({
name: 'Pro League',
description: 'Top tier competition',
maxDrivers: 40,
maxTeams: 10,
basics: {
name: 'Pro League',
description: 'Top tier competition',
visibility: 'public',
gameId: 'iracing',
},
structure: {
mode: 'solo',
maxDrivers: 40,
},
championships: {
enableDriverChampionship: true,
enableTeamChampionship: false,
enableNationsChampionship: false,
enableTrophyChampionship: false,
},
scoring: {
patternId: 'sprint-main-driver',
customScoringEnabled: false,
},
dropPolicy: {
strategy: 'bestNResults',
n: 6,
},
timings: {
qualifyingMinutes: 30,
mainRaceMinutes: 40,
roundsPlanned: 8,
},
stewarding: {
decisionMode: 'admin_vote',
requireDefense: false,
defenseTimeLimit: 24,
voteTimeLimit: 48,
protestDeadlineHours: 24,
stewardingClosesHours: 168,
notifyAccusedOnProtest: true,
notifyOnVoteRequired: true,
},
...overrides,
} as LeagueConfigFormModel);
@@ -16,7 +51,10 @@ const createPreset = (overrides: Partial<LeagueScoringPresetDTO> = {}): LeagueSc
id: 'preset-1',
name: 'Standard scoring',
description: 'Top 15 get points',
gameId: 'iracing',
primaryChampionshipType: 'driver',
sessionSummary: 'Sprint + Main',
bonusSummary: 'None',
dropPolicySummary: 'Best 6',
...overrides,
} as LeagueScoringPresetDTO);

View File

@@ -1,5 +1,5 @@
// DTO matching the backend RacesPageDataRaceDTO
interface RaceListItemDTO {
export interface RaceListItemDTO {
id: string;
track: string;
car: string;
@@ -7,7 +7,7 @@ interface RaceListItemDTO {
status: string;
leagueId: string;
leagueName: string;
strengthOfField: number | null;
strengthOfField?: number | null;
isUpcoming: boolean;
isLive: boolean;
isPast: boolean;
@@ -34,7 +34,7 @@ export class RaceListItemViewModel {
this.status = dto.status;
this.leagueId = dto.leagueId;
this.leagueName = dto.leagueName;
this.strengthOfField = dto.strengthOfField;
this.strengthOfField = dto.strengthOfField ?? null;
this.isUpcoming = dto.isUpcoming;
this.isLive = dto.isLive;
this.isPast = dto.isPast;
@@ -70,4 +70,4 @@ export class RaceListItemViewModel {
const hours = Math.floor(minutes / 60);
return `${hours}h ${minutes % 60}m`;
}
}
}

View File

@@ -1,20 +1,10 @@
import { RaceListItemViewModel } from './RaceListItemViewModel';
import type { RacesPageDataRaceDTO } from '../types/generated/RacesPageDataRaceDTO';
import type { RaceListItemDTO } from './RaceListItemViewModel';
// DTO matching the backend RacesPageDataDTO
interface RacesPageDTO {
races: Array<{
id: string;
track: string;
car: string;
scheduledAt: string;
status: string;
leagueId: string;
leagueName: string;
strengthOfField: number | null;
isUpcoming: boolean;
isLive: boolean;
isPast: boolean;
}>;
races: RacesPageDataRaceDTO[];
}
/**
@@ -25,7 +15,17 @@ export class RacesPageViewModel {
races: RaceListItemViewModel[];
constructor(dto: RacesPageDTO) {
this.races = dto.races.map(r => new RaceListItemViewModel(r));
this.races = dto.races.map((r) => {
const normalized: RaceListItemDTO = {
...r,
strengthOfField: (r as any).strengthOfField ?? null,
isUpcoming: r.isUpcoming,
isLive: r.isLive,
isPast: r.isPast,
};
return new RaceListItemViewModel(normalized);
});
}
/** UI-specific: Total races */
@@ -62,4 +62,4 @@ export class RacesPageViewModel {
get completedRaces(): RaceListItemViewModel[] {
return this.races.filter(r => r.status === 'completed');
}
}
}

View File

@@ -16,6 +16,24 @@ export class SessionViewModel {
driverId?: string;
isAuthenticated: boolean = true;
/**
* Compatibility accessor.
* Some legacy components expect `session.user.*`.
*/
get user(): {
userId: string;
email: string;
displayName: string;
primaryDriverId?: string | null;
} {
return {
userId: this.userId,
email: this.email,
displayName: this.displayName,
primaryDriverId: this.driverId ?? null,
};
}
/** UI-specific: User greeting */
get greeting(): string {
return `Hello, ${this.displayName}!`;
@@ -26,7 +44,7 @@ export class SessionViewModel {
if (this.displayName) {
return this.displayName.split(' ').map(n => n[0]).join('').toUpperCase();
}
return this.email[0].toUpperCase();
return (this.email?.[0] ?? '?').toUpperCase();
}
/** UI-specific: Whether has driver profile */
@@ -38,4 +56,4 @@ export class SessionViewModel {
get authStatusDisplay(): string {
return this.isAuthenticated ? 'Logged In' : 'Logged Out';
}
}
}

View File

@@ -1,4 +1,4 @@
import { PendingRequestDTO } from '@/components/sponsors/PendingSponsorshipRequests';
import type { SponsorshipRequestDTO } from '@/lib/types/generated/SponsorshipRequestDTO';
export class SponsorshipRequestViewModel {
id: string;
@@ -14,17 +14,18 @@ export class SponsorshipRequestViewModel {
platformFee: number;
netAmount: number;
constructor(dto: PendingRequestDTO) {
constructor(dto: SponsorshipRequestDTO) {
this.id = dto.id;
this.sponsorId = dto.sponsorId;
this.sponsorName = dto.sponsorName;
this.sponsorLogo = dto.sponsorLogo;
this.tier = dto.tier;
// 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;
this.createdAt = dto.createdAt;
this.createdAt = new Date(dto.createdAt);
this.platformFee = dto.platformFee;
this.netAmount = dto.netAmount;
}
@@ -51,4 +52,4 @@ export class SponsorshipRequestViewModel {
get tierBadgeVariant(): 'primary' | 'secondary' {
return this.tier === 'main' ? 'primary' : 'secondary';
}
}
}

View File

@@ -1,4 +1,4 @@
import type { TeamListItemDTO } from '@/lib/types/generated/GetAllTeamsOutputDTO';
import type { TeamListItemDTO } from '@/lib/types/generated/TeamListItemDTO';
interface TeamCardDTO {
id: string;

View File

@@ -1,24 +1,25 @@
// Note: No generated DTO available for TeamJoinRequest yet
export interface TeamJoinRequestDTO {
id: string;
teamId: string;
driverId: string;
requestedAt: string;
message?: string;
}
import type { TeamJoinRequestDTO } from '@/lib/types/generated/TeamJoinRequestDTO';
export class TeamJoinRequestViewModel {
id: string;
teamId: string;
requestId: string;
driverId: string;
driverName: string;
teamId: string;
requestStatus: string;
requestedAt: string;
message?: string;
avatarUrl: string;
private currentUserId: string;
private isOwner: boolean;
private readonly currentUserId: string;
private readonly isOwner: boolean;
constructor(dto: TeamJoinRequestDTO, currentUserId: string, isOwner: boolean) {
Object.assign(this, dto);
this.requestId = dto.requestId;
this.driverId = dto.driverId;
this.driverName = dto.driverName;
this.teamId = dto.teamId;
this.requestStatus = dto.status;
this.requestedAt = dto.requestedAt;
this.avatarUrl = dto.avatarUrl;
this.currentUserId = currentUserId;
this.isOwner = isOwner;
}
@@ -33,13 +34,10 @@ export class TeamJoinRequestViewModel {
return new Date(this.requestedAt).toLocaleString();
}
/** UI-specific: Request status (pending) */
get status(): string {
return 'Pending';
}
/** UI-specific: Status color */
get statusColor(): string {
if (this.requestStatus === 'approved') return 'green';
if (this.requestStatus === 'rejected') return 'red';
return 'yellow';
}
@@ -52,4 +50,4 @@ export class TeamJoinRequestViewModel {
get rejectButtonText(): string {
return 'Reject';
}
}
}

View File

@@ -1,9 +1,18 @@
import type { TeamMemberDTO } from '@/lib/types/generated/GetTeamMembersOutputDTO';
import type { TeamMemberDTO } from '@/lib/types/generated/TeamMemberDTO';
type TeamMemberRole = 'owner' | 'manager' | 'member';
function normalizeTeamRole(role: string): TeamMemberRole {
if (role === 'owner' || role === 'manager' || role === 'member') return role;
// Backwards compatibility
if (role === 'admin') return 'manager';
return 'member';
}
export class TeamMemberViewModel {
driverId: string;
driverName: string;
role: 'owner' | 'admin' | 'member';
role: TeamMemberRole;
joinedAt: string;
isActive: boolean;
avatarUrl: string;
@@ -14,7 +23,7 @@ export class TeamMemberViewModel {
constructor(dto: TeamMemberDTO, currentUserId: string, teamOwnerId: string) {
this.driverId = dto.driverId;
this.driverName = dto.driverName;
this.role = dto.role;
this.role = normalizeTeamRole(dto.role);
this.joinedAt = dto.joinedAt;
this.isActive = dto.isActive;
this.avatarUrl = dto.avatarUrl;
@@ -26,7 +35,7 @@ export class TeamMemberViewModel {
get roleBadgeVariant(): string {
switch (this.role) {
case 'owner': return 'primary';
case 'admin': return 'secondary';
case 'manager': return 'secondary';
case 'member': return 'default';
default: return 'default';
}
@@ -51,4 +60,4 @@ export class TeamMemberViewModel {
get formattedJoinedAt(): string {
return new Date(this.joinedAt).toLocaleDateString();
}
}
}

View File

@@ -1,4 +1,4 @@
import type { TeamListItemDTO } from '@/lib/types/generated/GetAllTeamsOutputDTO';
import type { TeamListItemDTO } from '@/lib/types/generated/TeamListItemDTO';
export class TeamSummaryViewModel {
id: string;
@@ -54,4 +54,4 @@ export class TeamSummaryViewModel {
get statusColor(): string {
return this.isFull ? 'red' : 'green';
}
}
}