refactor page to use services

This commit is contained in:
2025-12-18 15:58:09 +01:00
parent f54fa5de5b
commit fc386db06a
45 changed files with 2254 additions and 1292 deletions

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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';
}
}

View 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
}

View File

@@ -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;
}
}

View 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;
}
}

View File

@@ -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
}
}

View File

@@ -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 => ({

View File

@@ -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;