api client refactor
This commit is contained in:
30
apps/website/lib/view-models/AnalyticsDashboardViewModel.ts
Normal file
30
apps/website/lib/view-models/AnalyticsDashboardViewModel.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
// Analytics dashboard view model
|
||||
// Represents dashboard data for analytics
|
||||
|
||||
export class AnalyticsDashboardViewModel {
|
||||
totalUsers: number;
|
||||
activeUsers: number;
|
||||
totalRaces: number;
|
||||
totalLeagues: number;
|
||||
|
||||
constructor(data: { totalUsers: number; activeUsers: number; totalRaces: number; totalLeagues: number }) {
|
||||
Object.assign(this, data);
|
||||
}
|
||||
|
||||
/** UI-specific: User engagement rate */
|
||||
get userEngagementRate(): number {
|
||||
return this.totalUsers > 0 ? (this.activeUsers / this.totalUsers) * 100 : 0;
|
||||
}
|
||||
|
||||
/** UI-specific: Formatted engagement rate */
|
||||
get formattedEngagementRate(): string {
|
||||
return `${this.userEngagementRate.toFixed(1)}%`;
|
||||
}
|
||||
|
||||
/** UI-specific: Activity level */
|
||||
get activityLevel(): string {
|
||||
if (this.userEngagementRate > 70) return 'High';
|
||||
if (this.userEngagementRate > 40) return 'Medium';
|
||||
return 'Low';
|
||||
}
|
||||
}
|
||||
35
apps/website/lib/view-models/AnalyticsMetricsViewModel.ts
Normal file
35
apps/website/lib/view-models/AnalyticsMetricsViewModel.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
// Analytics metrics view model
|
||||
// Represents metrics data for analytics
|
||||
|
||||
export class AnalyticsMetricsViewModel {
|
||||
pageViews: number;
|
||||
uniqueVisitors: number;
|
||||
averageSessionDuration: number;
|
||||
bounceRate: number;
|
||||
|
||||
constructor(data: { pageViews: number; uniqueVisitors: number; averageSessionDuration: number; bounceRate: number }) {
|
||||
Object.assign(this, data);
|
||||
}
|
||||
|
||||
/** UI-specific: Formatted page views */
|
||||
get formattedPageViews(): string {
|
||||
return this.pageViews.toLocaleString();
|
||||
}
|
||||
|
||||
/** UI-specific: Formatted unique visitors */
|
||||
get formattedUniqueVisitors(): string {
|
||||
return this.uniqueVisitors.toLocaleString();
|
||||
}
|
||||
|
||||
/** UI-specific: Formatted session duration */
|
||||
get formattedSessionDuration(): string {
|
||||
const minutes = Math.floor(this.averageSessionDuration / 60);
|
||||
const seconds = Math.floor(this.averageSessionDuration % 60);
|
||||
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
/** UI-specific: Formatted bounce rate */
|
||||
get formattedBounceRate(): string {
|
||||
return `${this.bounceRate.toFixed(1)}%`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import { DriverLeaderboardItemDto } from '../dtos';
|
||||
|
||||
export class DriverLeaderboardItemViewModel implements DriverLeaderboardItemDto {
|
||||
id: string;
|
||||
name: string;
|
||||
avatarUrl?: string;
|
||||
rating: number;
|
||||
wins: number;
|
||||
races: number;
|
||||
skillLevel: string;
|
||||
isActive: boolean;
|
||||
nationality: string;
|
||||
podiums: number;
|
||||
|
||||
position: number;
|
||||
private previousRating?: number;
|
||||
|
||||
constructor(dto: DriverLeaderboardItemDto, position: number, previousRating?: number) {
|
||||
Object.assign(this, dto);
|
||||
this.position = position;
|
||||
this.previousRating = previousRating;
|
||||
}
|
||||
|
||||
/** UI-specific: Skill level color */
|
||||
get skillLevelColor(): string {
|
||||
switch (this.skillLevel) {
|
||||
case 'beginner': return 'green';
|
||||
case 'intermediate': return 'yellow';
|
||||
case 'advanced': return 'orange';
|
||||
case 'expert': return 'red';
|
||||
default: return 'gray';
|
||||
}
|
||||
}
|
||||
|
||||
/** UI-specific: Skill level icon */
|
||||
get skillLevelIcon(): string {
|
||||
switch (this.skillLevel) {
|
||||
case 'beginner': return '🥉';
|
||||
case 'intermediate': return '🥈';
|
||||
case 'advanced': return '🥇';
|
||||
case 'expert': return '👑';
|
||||
default: return '🏁';
|
||||
}
|
||||
}
|
||||
|
||||
/** UI-specific: Win rate */
|
||||
get winRate(): number {
|
||||
return this.races > 0 ? (this.wins / this.races) * 100 : 0;
|
||||
}
|
||||
|
||||
/** UI-specific: Formatted win rate */
|
||||
get winRateFormatted(): string {
|
||||
return `${this.winRate.toFixed(1)}%`;
|
||||
}
|
||||
|
||||
/** UI-specific: Rating trend */
|
||||
get ratingTrend(): 'up' | 'down' | 'same' {
|
||||
if (!this.previousRating) return 'same';
|
||||
if (this.rating > this.previousRating) return 'up';
|
||||
if (this.rating < this.previousRating) return 'down';
|
||||
return 'same';
|
||||
}
|
||||
|
||||
/** UI-specific: Rating change indicator */
|
||||
get ratingChangeIndicator(): string {
|
||||
const change = this.previousRating ? this.rating - this.previousRating : 0;
|
||||
if (change > 0) return `+${change}`;
|
||||
if (change < 0) return `${change}`;
|
||||
return '0';
|
||||
}
|
||||
|
||||
/** UI-specific: Position badge */
|
||||
get positionBadge(): string {
|
||||
return this.position.toString();
|
||||
}
|
||||
}
|
||||
28
apps/website/lib/view-models/DriverLeaderboardViewModel.ts
Normal file
28
apps/website/lib/view-models/DriverLeaderboardViewModel.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { DriversLeaderboardDto, DriverLeaderboardItemDto } from '../dtos';
|
||||
import { DriverLeaderboardItemViewModel } from './DriverLeaderboardItemViewModel';
|
||||
|
||||
export class DriverLeaderboardViewModel implements DriversLeaderboardDto {
|
||||
drivers: DriverLeaderboardItemViewModel[];
|
||||
|
||||
constructor(dto: DriversLeaderboardDto & { 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);
|
||||
});
|
||||
}
|
||||
|
||||
/** UI-specific: Total races across all drivers */
|
||||
get totalRaces(): number {
|
||||
return this.drivers.reduce((sum, driver) => sum + driver.races, 0);
|
||||
}
|
||||
|
||||
/** UI-specific: Total wins across all drivers */
|
||||
get totalWins(): number {
|
||||
return this.drivers.reduce((sum, driver) => sum + driver.wins, 0);
|
||||
}
|
||||
|
||||
/** UI-specific: Active drivers count */
|
||||
get activeCount(): number {
|
||||
return this.drivers.filter(driver => driver.isActive).length;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { DriverRegistrationStatusDto } from '../dtos';
|
||||
|
||||
export class DriverRegistrationStatusViewModel implements DriverRegistrationStatusDto {
|
||||
isRegistered: boolean;
|
||||
raceId: string;
|
||||
driverId: string;
|
||||
|
||||
constructor(dto: DriverRegistrationStatusDto) {
|
||||
Object.assign(this, dto);
|
||||
}
|
||||
|
||||
/** UI-specific: Status message */
|
||||
get statusMessage(): string {
|
||||
return this.isRegistered ? 'Registered for this race' : 'Not registered';
|
||||
}
|
||||
|
||||
/** UI-specific: Status color */
|
||||
get statusColor(): string {
|
||||
return this.isRegistered ? 'green' : 'red';
|
||||
}
|
||||
|
||||
/** UI-specific: Badge variant */
|
||||
get statusBadgeVariant(): string {
|
||||
return this.isRegistered ? 'success' : 'warning';
|
||||
}
|
||||
|
||||
/** UI-specific: Registration button text */
|
||||
get registrationButtonText(): string {
|
||||
return this.isRegistered ? 'Withdraw' : 'Register';
|
||||
}
|
||||
|
||||
/** UI-specific: Whether can register (assuming always can if not registered) */
|
||||
get canRegister(): boolean {
|
||||
return !this.isRegistered;
|
||||
}
|
||||
}
|
||||
16
apps/website/lib/view-models/LeagueAdminViewModel.ts
Normal file
16
apps/website/lib/view-models/LeagueAdminViewModel.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import type { LeagueAdminDto } from '../dtos';
|
||||
import type { LeagueMemberViewModel, LeagueJoinRequestViewModel } from './';
|
||||
|
||||
/**
|
||||
* League admin view model
|
||||
* Transform from DTO to ViewModel with UI fields
|
||||
*/
|
||||
export interface LeagueAdminViewModel {
|
||||
config: LeagueAdminDto['config'];
|
||||
members: LeagueMemberViewModel[];
|
||||
joinRequests: LeagueJoinRequestViewModel[];
|
||||
// Total pending requests count
|
||||
pendingRequestsCount: number;
|
||||
// Whether there are any pending requests
|
||||
hasPendingRequests: boolean;
|
||||
}
|
||||
14
apps/website/lib/view-models/LeagueJoinRequestViewModel.ts
Normal file
14
apps/website/lib/view-models/LeagueJoinRequestViewModel.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import type { LeagueJoinRequestDto } from '../dtos';
|
||||
|
||||
/**
|
||||
* League join request view model
|
||||
* Transform from DTO to ViewModel with UI fields
|
||||
*/
|
||||
export interface LeagueJoinRequestViewModel extends LeagueJoinRequestDto {
|
||||
// Formatted request date
|
||||
formattedRequestedAt: string;
|
||||
// Whether the request can be approved by current user
|
||||
canApprove: boolean;
|
||||
// Whether the request can be rejected by current user
|
||||
canReject: boolean;
|
||||
}
|
||||
39
apps/website/lib/view-models/LeagueMemberViewModel.ts
Normal file
39
apps/website/lib/view-models/LeagueMemberViewModel.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { LeagueMemberDto, DriverDto } from '../dtos';
|
||||
|
||||
export class LeagueMemberViewModel implements LeagueMemberDto {
|
||||
driverId: string;
|
||||
driver?: DriverDto;
|
||||
role: string;
|
||||
joinedAt: string;
|
||||
|
||||
private currentUserId: string;
|
||||
|
||||
constructor(dto: LeagueMemberDto, currentUserId: string) {
|
||||
Object.assign(this, dto);
|
||||
this.currentUserId = currentUserId;
|
||||
}
|
||||
|
||||
/** UI-specific: Formatted join date */
|
||||
get formattedJoinedAt(): string {
|
||||
return new Date(this.joinedAt).toLocaleDateString();
|
||||
}
|
||||
|
||||
/** UI-specific: Badge variant for role */
|
||||
get roleBadgeVariant(): string {
|
||||
switch (this.role) {
|
||||
case 'owner': return 'primary';
|
||||
case 'admin': return 'secondary';
|
||||
default: return 'default';
|
||||
}
|
||||
}
|
||||
|
||||
/** UI-specific: Whether this member is the owner */
|
||||
get isOwner(): boolean {
|
||||
return this.role === 'owner';
|
||||
}
|
||||
|
||||
/** UI-specific: Whether this is the current user */
|
||||
get isCurrentUser(): boolean {
|
||||
return this.driverId === this.currentUserId;
|
||||
}
|
||||
}
|
||||
19
apps/website/lib/view-models/LeagueStandingsViewModel.ts
Normal file
19
apps/website/lib/view-models/LeagueStandingsViewModel.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { LeagueStandingsDto, StandingEntryDto, DriverDto, LeagueMembership } from '../dtos';
|
||||
import { StandingEntryViewModel } from './StandingEntryViewModel';
|
||||
|
||||
export class LeagueStandingsViewModel implements LeagueStandingsDto {
|
||||
standings: StandingEntryViewModel[];
|
||||
drivers: DriverDto[];
|
||||
memberships: LeagueMembership[];
|
||||
|
||||
constructor(dto: LeagueStandingsDto & { standings: StandingEntryDto[] }, currentUserId: string, previousStandings?: StandingEntryDto[]) {
|
||||
const leaderPoints = dto.standings[0]?.points || 0;
|
||||
this.standings = dto.standings.map((entry, index) => {
|
||||
const nextPoints = dto.standings[index + 1]?.points || entry.points;
|
||||
const previousPosition = previousStandings?.find(p => p.driverId === entry.driverId)?.position;
|
||||
return new StandingEntryViewModel(entry, leaderPoints, nextPoints, currentUserId, previousPosition);
|
||||
});
|
||||
this.drivers = dto.drivers;
|
||||
this.memberships = dto.memberships;
|
||||
}
|
||||
}
|
||||
63
apps/website/lib/view-models/LeagueSummaryViewModel.ts
Normal file
63
apps/website/lib/view-models/LeagueSummaryViewModel.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { LeagueSummaryDto } from '../dtos';
|
||||
|
||||
export class LeagueSummaryViewModel implements LeagueSummaryDto {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
logoUrl?: string;
|
||||
coverImage?: string;
|
||||
memberCount: number;
|
||||
maxMembers: number;
|
||||
isPublic: boolean;
|
||||
ownerId: string;
|
||||
ownerName?: string;
|
||||
scoringType?: string;
|
||||
status?: string;
|
||||
|
||||
constructor(dto: LeagueSummaryDto) {
|
||||
Object.assign(this, dto);
|
||||
}
|
||||
|
||||
/** UI-specific: Formatted capacity display */
|
||||
get formattedCapacity(): string {
|
||||
return `${this.memberCount}/${this.maxMembers}`;
|
||||
}
|
||||
|
||||
/** UI-specific: Capacity bar percentage */
|
||||
get capacityBarPercent(): number {
|
||||
return (this.memberCount / this.maxMembers) * 100;
|
||||
}
|
||||
|
||||
/** UI-specific: Label for join button */
|
||||
get joinButtonLabel(): string {
|
||||
if (this.isFull) return 'Full';
|
||||
return this.isJoinable ? 'Join League' : 'Request to Join';
|
||||
}
|
||||
|
||||
/** UI-specific: Whether the league is full */
|
||||
get isFull(): boolean {
|
||||
return this.memberCount >= this.maxMembers;
|
||||
}
|
||||
|
||||
/** UI-specific: Whether the league is joinable */
|
||||
get isJoinable(): boolean {
|
||||
return this.isPublic && !this.isFull;
|
||||
}
|
||||
|
||||
/** UI-specific: Color for member progress */
|
||||
get memberProgressColor(): string {
|
||||
const percent = this.capacityBarPercent;
|
||||
if (percent < 50) return 'green';
|
||||
if (percent < 80) return 'yellow';
|
||||
return 'red';
|
||||
}
|
||||
|
||||
/** UI-specific: Badge variant for status */
|
||||
get statusBadgeVariant(): string {
|
||||
switch (this.status) {
|
||||
case 'active': return 'success';
|
||||
case 'inactive': return 'secondary';
|
||||
default: return 'default';
|
||||
}
|
||||
}
|
||||
}
|
||||
32
apps/website/lib/view-models/MembershipFeeViewModel.ts
Normal file
32
apps/website/lib/view-models/MembershipFeeViewModel.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { MembershipFeeDto } from '../dtos';
|
||||
|
||||
export class MembershipFeeViewModel implements MembershipFeeDto {
|
||||
leagueId: string;
|
||||
amount: number;
|
||||
currency: string;
|
||||
period: string;
|
||||
|
||||
constructor(dto: MembershipFeeDto) {
|
||||
Object.assign(this, dto);
|
||||
}
|
||||
|
||||
/** UI-specific: Formatted amount */
|
||||
get formattedAmount(): string {
|
||||
return `${this.currency} ${this.amount.toFixed(2)}`;
|
||||
}
|
||||
|
||||
/** UI-specific: Period display */
|
||||
get periodDisplay(): string {
|
||||
switch (this.period) {
|
||||
case 'monthly': return 'Monthly';
|
||||
case 'yearly': return 'Yearly';
|
||||
case 'season': return 'Per Season';
|
||||
default: return this.period;
|
||||
}
|
||||
}
|
||||
|
||||
/** UI-specific: Amount per period */
|
||||
get amountPerPeriod(): string {
|
||||
return `${this.formattedAmount} ${this.periodDisplay.toLowerCase()}`;
|
||||
}
|
||||
}
|
||||
38
apps/website/lib/view-models/PaymentViewModel.ts
Normal file
38
apps/website/lib/view-models/PaymentViewModel.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { PaymentDto } from '../dtos';
|
||||
|
||||
export class PaymentViewModel implements PaymentDto {
|
||||
id: string;
|
||||
amount: number;
|
||||
currency: string;
|
||||
status: string;
|
||||
createdAt: string;
|
||||
|
||||
constructor(dto: PaymentDto) {
|
||||
Object.assign(this, dto);
|
||||
}
|
||||
|
||||
/** UI-specific: Formatted amount */
|
||||
get formattedAmount(): string {
|
||||
return `${this.currency} ${this.amount.toFixed(2)}`;
|
||||
}
|
||||
|
||||
/** UI-specific: Status color */
|
||||
get statusColor(): string {
|
||||
switch (this.status) {
|
||||
case 'completed': return 'green';
|
||||
case 'pending': return 'yellow';
|
||||
case 'failed': return 'red';
|
||||
default: return 'gray';
|
||||
}
|
||||
}
|
||||
|
||||
/** UI-specific: Formatted created date */
|
||||
get formattedCreatedAt(): string {
|
||||
return new Date(this.createdAt).toLocaleString();
|
||||
}
|
||||
|
||||
/** UI-specific: Status display */
|
||||
get statusDisplay(): string {
|
||||
return this.status.charAt(0).toUpperCase() + this.status.slice(1);
|
||||
}
|
||||
}
|
||||
34
apps/website/lib/view-models/PrizeViewModel.ts
Normal file
34
apps/website/lib/view-models/PrizeViewModel.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { PrizeDto } from '../dtos';
|
||||
|
||||
export class PrizeViewModel implements PrizeDto {
|
||||
id: string;
|
||||
name: string;
|
||||
amount: number;
|
||||
currency: string;
|
||||
position?: number;
|
||||
|
||||
constructor(dto: PrizeDto) {
|
||||
Object.assign(this, dto);
|
||||
}
|
||||
|
||||
/** UI-specific: Formatted amount */
|
||||
get formattedAmount(): string {
|
||||
return `${this.currency} ${this.amount.toFixed(2)}`;
|
||||
}
|
||||
|
||||
/** UI-specific: Position display */
|
||||
get positionDisplay(): string {
|
||||
if (!this.position) return 'Special';
|
||||
switch (this.position) {
|
||||
case 1: return '1st Place';
|
||||
case 2: return '2nd Place';
|
||||
case 3: return '3rd Place';
|
||||
default: return `${this.position}th Place`;
|
||||
}
|
||||
}
|
||||
|
||||
/** UI-specific: Prize description */
|
||||
get prizeDescription(): string {
|
||||
return `${this.name} - ${this.formattedAmount}`;
|
||||
}
|
||||
}
|
||||
13
apps/website/lib/view-models/ProtestViewModel.ts
Normal file
13
apps/website/lib/view-models/ProtestViewModel.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Protest view model
|
||||
* Represents a race protest
|
||||
*/
|
||||
export interface ProtestViewModel {
|
||||
id: string;
|
||||
raceId: string;
|
||||
complainantId: string;
|
||||
defendantId: string;
|
||||
description: string;
|
||||
status: string;
|
||||
createdAt: string;
|
||||
}
|
||||
57
apps/website/lib/view-models/RaceDetailViewModel.ts
Normal file
57
apps/website/lib/view-models/RaceDetailViewModel.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { RaceDetailDto, RaceDetailRaceDto, RaceDetailLeagueDto, RaceDetailEntryDto, RaceDetailRegistrationDto, RaceDetailUserResultDto } from '../dtos';
|
||||
|
||||
export class RaceDetailViewModel implements RaceDetailDto {
|
||||
race: RaceDetailRaceDto | null;
|
||||
league: RaceDetailLeagueDto | null;
|
||||
entryList: RaceDetailEntryDto[];
|
||||
registration: RaceDetailRegistrationDto;
|
||||
userResult: RaceDetailUserResultDto | null;
|
||||
error?: string;
|
||||
|
||||
constructor(dto: RaceDetailDto) {
|
||||
Object.assign(this, dto);
|
||||
}
|
||||
|
||||
/** UI-specific: Whether user is registered */
|
||||
get isRegistered(): boolean {
|
||||
return this.registration.isRegistered;
|
||||
}
|
||||
|
||||
/** UI-specific: Whether user can register */
|
||||
get canRegister(): boolean {
|
||||
return this.registration.canRegister;
|
||||
}
|
||||
|
||||
/** UI-specific: Race status display */
|
||||
get raceStatusDisplay(): string {
|
||||
if (!this.race) return 'Unknown';
|
||||
switch (this.race.status) {
|
||||
case 'upcoming': return 'Upcoming';
|
||||
case 'live': return 'Live';
|
||||
case 'finished': return 'Finished';
|
||||
default: return this.race.status;
|
||||
}
|
||||
}
|
||||
|
||||
/** UI-specific: Formatted scheduled time */
|
||||
get formattedScheduledTime(): string {
|
||||
return this.race ? new Date(this.race.scheduledTime).toLocaleString() : '';
|
||||
}
|
||||
|
||||
/** UI-specific: Entry list count */
|
||||
get entryCount(): number {
|
||||
return this.entryList.length;
|
||||
}
|
||||
|
||||
/** UI-specific: Whether race has results */
|
||||
get hasResults(): boolean {
|
||||
return this.userResult !== null;
|
||||
}
|
||||
|
||||
/** UI-specific: Registration status message */
|
||||
get registrationStatusMessage(): string {
|
||||
if (this.isRegistered) return 'You are registered for this race';
|
||||
if (this.canRegister) return 'You can register for this race';
|
||||
return 'Registration not available';
|
||||
}
|
||||
}
|
||||
55
apps/website/lib/view-models/RaceListItemViewModel.ts
Normal file
55
apps/website/lib/view-models/RaceListItemViewModel.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { RaceListItemDto } from '../dtos';
|
||||
|
||||
export class RaceListItemViewModel implements RaceListItemDto {
|
||||
id: string;
|
||||
name: string;
|
||||
leagueId: string;
|
||||
leagueName: string;
|
||||
scheduledTime: string;
|
||||
status: string;
|
||||
trackName?: string;
|
||||
|
||||
constructor(dto: RaceListItemDto) {
|
||||
Object.assign(this, dto);
|
||||
}
|
||||
|
||||
/** UI-specific: Formatted scheduled time */
|
||||
get formattedScheduledTime(): string {
|
||||
return new Date(this.scheduledTime).toLocaleString();
|
||||
}
|
||||
|
||||
/** UI-specific: Badge variant for status */
|
||||
get statusBadgeVariant(): string {
|
||||
switch (this.status) {
|
||||
case 'upcoming': return 'info';
|
||||
case 'live': return 'success';
|
||||
case 'finished': return 'secondary';
|
||||
default: return 'default';
|
||||
}
|
||||
}
|
||||
|
||||
/** UI-specific: Whether race is upcoming */
|
||||
get isUpcoming(): boolean {
|
||||
return this.status === 'upcoming';
|
||||
}
|
||||
|
||||
/** UI-specific: Whether race is live */
|
||||
get isLive(): boolean {
|
||||
return this.status === 'live';
|
||||
}
|
||||
|
||||
/** UI-specific: Time until start in minutes */
|
||||
get timeUntilStart(): number {
|
||||
const now = new Date();
|
||||
const scheduled = new Date(this.scheduledTime);
|
||||
return Math.max(0, Math.floor((scheduled.getTime() - now.getTime()) / (1000 * 60)));
|
||||
}
|
||||
|
||||
/** UI-specific: Display for time until start */
|
||||
get timeUntilStartDisplay(): string {
|
||||
const minutes = this.timeUntilStart;
|
||||
if (minutes < 60) return `${minutes}m`;
|
||||
const hours = Math.floor(minutes / 60);
|
||||
return `${hours}h ${minutes % 60}m`;
|
||||
}
|
||||
}
|
||||
70
apps/website/lib/view-models/RaceResultViewModel.ts
Normal file
70
apps/website/lib/view-models/RaceResultViewModel.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { RaceResultDto } from '../dtos';
|
||||
|
||||
export class RaceResultViewModel implements RaceResultDto {
|
||||
id: string;
|
||||
raceId: string;
|
||||
driverId: string;
|
||||
driverName: string;
|
||||
avatarUrl: string;
|
||||
position: number;
|
||||
startPosition: number;
|
||||
incidents: number;
|
||||
fastestLap: number;
|
||||
positionChange: number;
|
||||
isPodium: boolean;
|
||||
isClean: boolean;
|
||||
|
||||
constructor(dto: RaceResultDto) {
|
||||
Object.assign(this, dto);
|
||||
}
|
||||
|
||||
/** UI-specific: Display for position change */
|
||||
get positionChangeDisplay(): string {
|
||||
if (this.positionChange > 0) return `+${this.positionChange}`;
|
||||
if (this.positionChange < 0) return `${this.positionChange}`;
|
||||
return '0';
|
||||
}
|
||||
|
||||
/** UI-specific: Color for position change */
|
||||
get positionChangeColor(): string {
|
||||
if (this.positionChange > 0) return 'green';
|
||||
if (this.positionChange < 0) return 'red';
|
||||
return 'gray';
|
||||
}
|
||||
|
||||
/** UI-specific: Whether this is the winner */
|
||||
get isWinner(): boolean {
|
||||
return this.position === 1;
|
||||
}
|
||||
|
||||
/** UI-specific: Whether has fastest lap */
|
||||
get hasFastestLap(): boolean {
|
||||
return this.fastestLap > 0;
|
||||
}
|
||||
|
||||
/** UI-specific: Badge for position */
|
||||
get positionBadge(): string {
|
||||
return this.position.toString();
|
||||
}
|
||||
|
||||
/** UI-specific: Color for incidents badge */
|
||||
get incidentsBadgeColor(): string {
|
||||
if (this.incidents === 0) return 'green';
|
||||
if (this.incidents <= 2) return 'yellow';
|
||||
return 'red';
|
||||
}
|
||||
|
||||
/** UI-specific: Formatted lap time */
|
||||
get lapTimeFormatted(): string {
|
||||
if (this.fastestLap <= 0) return '--:--.---';
|
||||
const minutes = Math.floor(this.fastestLap / 60);
|
||||
const seconds = Math.floor(this.fastestLap % 60);
|
||||
const milliseconds = Math.floor((this.fastestLap % 1) * 1000);
|
||||
return `${minutes}:${seconds.toString().padStart(2, '0')}.${milliseconds.toString().padStart(3, '0')}`;
|
||||
}
|
||||
|
||||
/** Compatibility with old DTO interface */
|
||||
getPositionChange(): number {
|
||||
return this.positionChange;
|
||||
}
|
||||
}
|
||||
63
apps/website/lib/view-models/RaceResultsDetailViewModel.ts
Normal file
63
apps/website/lib/view-models/RaceResultsDetailViewModel.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { RaceResultsDetailDto, RaceResultDto } from '../dtos';
|
||||
import { RaceResultViewModel } from './RaceResultViewModel';
|
||||
|
||||
export class RaceResultsDetailViewModel implements RaceResultsDetailDto {
|
||||
raceId: string;
|
||||
track: string;
|
||||
results: RaceResultViewModel[];
|
||||
league?: { id: string; name: string };
|
||||
race?: { id: string; track: string; scheduledAt: string };
|
||||
drivers: { id: string; name: string }[];
|
||||
pointsSystem: Record<number, number>;
|
||||
fastestLapTime: number;
|
||||
penalties: { driverId: string; type: string; value?: number }[];
|
||||
currentDriverId: string;
|
||||
|
||||
private currentUserId: string;
|
||||
|
||||
constructor(dto: RaceResultsDetailDto & { results: RaceResultDto[] }, currentUserId: string) {
|
||||
this.raceId = dto.raceId;
|
||||
this.track = dto.track;
|
||||
this.results = dto.results.map(r => new RaceResultViewModel({ ...r, raceId: dto.raceId }));
|
||||
this.league = dto.league;
|
||||
this.race = dto.race;
|
||||
this.drivers = dto.drivers;
|
||||
this.pointsSystem = dto.pointsSystem;
|
||||
this.fastestLapTime = dto.fastestLapTime;
|
||||
this.penalties = dto.penalties;
|
||||
this.currentDriverId = dto.currentDriverId;
|
||||
this.currentUserId = currentUserId;
|
||||
}
|
||||
|
||||
/** UI-specific: Results sorted by position */
|
||||
get resultsByPosition(): RaceResultViewModel[] {
|
||||
return [...this.results].sort((a, b) => a.position - b.position);
|
||||
}
|
||||
|
||||
/** UI-specific: Results sorted by fastest lap */
|
||||
get resultsByFastestLap(): RaceResultViewModel[] {
|
||||
return [...this.results].sort((a, b) => a.fastestLap - b.fastestLap);
|
||||
}
|
||||
|
||||
/** UI-specific: Clean drivers only */
|
||||
get cleanDrivers(): RaceResultViewModel[] {
|
||||
return this.results.filter(r => r.isClean);
|
||||
}
|
||||
|
||||
/** UI-specific: Current user's result */
|
||||
get currentUserResult(): RaceResultViewModel | undefined {
|
||||
return this.results.find(r => r.driverId === this.currentUserId);
|
||||
}
|
||||
|
||||
/** UI-specific: Race stats */
|
||||
get stats(): { totalDrivers: number; cleanRate: number; averageIncidents: number } {
|
||||
const total = this.results.length;
|
||||
const clean = this.cleanDrivers.length;
|
||||
const totalIncidents = this.results.reduce((sum, r) => sum + r.incidents, 0);
|
||||
return {
|
||||
totalDrivers: total,
|
||||
cleanRate: total > 0 ? (clean / total) * 100 : 0,
|
||||
averageIncidents: total > 0 ? totalIncidents / total : 0
|
||||
};
|
||||
}
|
||||
}
|
||||
36
apps/website/lib/view-models/SessionViewModel.ts
Normal file
36
apps/website/lib/view-models/SessionViewModel.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { SessionDataDto } from '../dtos';
|
||||
|
||||
export class SessionViewModel implements SessionDataDto {
|
||||
userId: string;
|
||||
email: string;
|
||||
displayName?: string;
|
||||
driverId?: string;
|
||||
isAuthenticated: boolean;
|
||||
|
||||
constructor(dto: SessionDataDto) {
|
||||
Object.assign(this, dto);
|
||||
}
|
||||
|
||||
/** UI-specific: User greeting */
|
||||
get greeting(): string {
|
||||
return `Hello, ${this.displayName || this.email}!`;
|
||||
}
|
||||
|
||||
/** UI-specific: Avatar initials */
|
||||
get avatarInitials(): string {
|
||||
if (this.displayName) {
|
||||
return this.displayName.split(' ').map(n => n[0]).join('').toUpperCase();
|
||||
}
|
||||
return this.email[0].toUpperCase();
|
||||
}
|
||||
|
||||
/** UI-specific: Whether has driver profile */
|
||||
get hasDriverProfile(): boolean {
|
||||
return !!this.driverId;
|
||||
}
|
||||
|
||||
/** UI-specific: Authentication status display */
|
||||
get authStatusDisplay(): string {
|
||||
return this.isAuthenticated ? 'Logged In' : 'Logged Out';
|
||||
}
|
||||
}
|
||||
27
apps/website/lib/view-models/SponsorViewModel.ts
Normal file
27
apps/website/lib/view-models/SponsorViewModel.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { SponsorDto } from '../dtos';
|
||||
|
||||
export class SponsorViewModel implements SponsorDto {
|
||||
id: string;
|
||||
name: string;
|
||||
logoUrl?: string;
|
||||
websiteUrl?: string;
|
||||
|
||||
constructor(dto: SponsorDto) {
|
||||
Object.assign(this, dto);
|
||||
}
|
||||
|
||||
/** UI-specific: Display name */
|
||||
get displayName(): string {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
/** UI-specific: Whether has website */
|
||||
get hasWebsite(): boolean {
|
||||
return !!this.websiteUrl;
|
||||
}
|
||||
|
||||
/** UI-specific: Website link text */
|
||||
get websiteLinkText(): string {
|
||||
return 'Visit Website';
|
||||
}
|
||||
}
|
||||
41
apps/website/lib/view-models/SponsorshipDetailViewModel.ts
Normal file
41
apps/website/lib/view-models/SponsorshipDetailViewModel.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { SponsorshipDetailDto } from '../dtos';
|
||||
|
||||
export class SponsorshipDetailViewModel implements SponsorshipDetailDto {
|
||||
id: string;
|
||||
leagueId: string;
|
||||
leagueName: string;
|
||||
seasonId: string;
|
||||
tier: 'main' | 'secondary';
|
||||
status: string;
|
||||
amount: number;
|
||||
currency: string;
|
||||
|
||||
constructor(dto: SponsorshipDetailDto) {
|
||||
Object.assign(this, dto);
|
||||
}
|
||||
|
||||
/** UI-specific: Formatted amount */
|
||||
get formattedAmount(): string {
|
||||
return `${this.currency} ${this.amount.toLocaleString()}`;
|
||||
}
|
||||
|
||||
/** UI-specific: Tier badge variant */
|
||||
get tierBadgeVariant(): string {
|
||||
return this.tier === 'main' ? 'primary' : 'secondary';
|
||||
}
|
||||
|
||||
/** UI-specific: Status color */
|
||||
get statusColor(): string {
|
||||
switch (this.status) {
|
||||
case 'active': return 'green';
|
||||
case 'pending': return 'yellow';
|
||||
case 'expired': return 'red';
|
||||
default: return 'gray';
|
||||
}
|
||||
}
|
||||
|
||||
/** UI-specific: Status display */
|
||||
get statusDisplay(): string {
|
||||
return this.status.charAt(0).toUpperCase() + this.status.slice(1);
|
||||
}
|
||||
}
|
||||
61
apps/website/lib/view-models/StandingEntryViewModel.ts
Normal file
61
apps/website/lib/view-models/StandingEntryViewModel.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { StandingEntryDto, DriverDto } from '../dtos';
|
||||
|
||||
export class StandingEntryViewModel implements StandingEntryDto {
|
||||
driverId: string;
|
||||
driver?: DriverDto;
|
||||
position: number;
|
||||
points: number;
|
||||
wins: number;
|
||||
podiums: number;
|
||||
races: number;
|
||||
|
||||
private leaderPoints: number;
|
||||
private nextPoints: number;
|
||||
private currentUserId: string;
|
||||
private previousPosition?: number;
|
||||
|
||||
constructor(dto: StandingEntryDto, leaderPoints: number, nextPoints: number, currentUserId: string, previousPosition?: number) {
|
||||
Object.assign(this, dto);
|
||||
this.leaderPoints = leaderPoints;
|
||||
this.nextPoints = nextPoints;
|
||||
this.currentUserId = currentUserId;
|
||||
this.previousPosition = previousPosition;
|
||||
}
|
||||
|
||||
/** UI-specific: Badge for position display */
|
||||
get positionBadge(): string {
|
||||
return this.position.toString();
|
||||
}
|
||||
|
||||
/** UI-specific: Points difference to leader */
|
||||
get pointsGapToLeader(): number {
|
||||
return this.points - this.leaderPoints;
|
||||
}
|
||||
|
||||
/** UI-specific: Points difference to next position */
|
||||
get pointsGapToNext(): number {
|
||||
return this.points - this.nextPoints;
|
||||
}
|
||||
|
||||
/** UI-specific: Whether this entry is the current user */
|
||||
get isCurrentUser(): boolean {
|
||||
return this.driverId === this.currentUserId;
|
||||
}
|
||||
|
||||
/** UI-specific: Trend compared to previous */
|
||||
get trend(): 'up' | 'down' | 'same' {
|
||||
if (!this.previousPosition) return 'same';
|
||||
if (this.position < this.previousPosition) return 'up';
|
||||
if (this.position > this.previousPosition) return 'down';
|
||||
return 'same';
|
||||
}
|
||||
|
||||
/** UI-specific: Arrow for trend */
|
||||
get trendArrow(): string {
|
||||
switch (this.trend) {
|
||||
case 'up': return '↑';
|
||||
case 'down': return '↓';
|
||||
default: return '-';
|
||||
}
|
||||
}
|
||||
}
|
||||
47
apps/website/lib/view-models/TeamDetailsViewModel.ts
Normal file
47
apps/website/lib/view-models/TeamDetailsViewModel.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { TeamDetailsDto, TeamMemberDto } from '../dtos';
|
||||
import { TeamMemberViewModel } from './TeamMemberViewModel';
|
||||
|
||||
export class TeamDetailsViewModel implements TeamDetailsDto {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
logoUrl?: string;
|
||||
memberCount: number;
|
||||
ownerId: string;
|
||||
members: TeamMemberViewModel[];
|
||||
|
||||
private currentUserId: string;
|
||||
|
||||
constructor(dto: TeamDetailsDto & { members: TeamMemberDto[] }, currentUserId: string) {
|
||||
this.id = dto.id;
|
||||
this.name = dto.name;
|
||||
this.description = dto.description;
|
||||
this.logoUrl = dto.logoUrl;
|
||||
this.memberCount = dto.memberCount;
|
||||
this.ownerId = dto.ownerId;
|
||||
this.members = dto.members.map(m => new TeamMemberViewModel(m, currentUserId, dto.ownerId));
|
||||
this.currentUserId = currentUserId;
|
||||
}
|
||||
|
||||
/** UI-specific: Whether current user is owner */
|
||||
get isOwner(): boolean {
|
||||
return this.currentUserId === this.ownerId;
|
||||
}
|
||||
|
||||
/** UI-specific: Whether can add members */
|
||||
get canAddMembers(): boolean {
|
||||
return this.isOwner && this.memberCount < 10; // Assuming max 10
|
||||
}
|
||||
|
||||
/** UI-specific: Member management actions available */
|
||||
get memberActionsAvailable(): boolean {
|
||||
return this.isOwner;
|
||||
}
|
||||
|
||||
/** UI-specific: Team status */
|
||||
get teamStatus(): string {
|
||||
if (this.memberCount < 5) return 'Recruiting';
|
||||
if (this.memberCount < 10) return 'Active';
|
||||
return 'Full';
|
||||
}
|
||||
}
|
||||
48
apps/website/lib/view-models/TeamJoinRequestViewModel.ts
Normal file
48
apps/website/lib/view-models/TeamJoinRequestViewModel.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { TeamJoinRequestItemDto } from '../dtos';
|
||||
|
||||
export class TeamJoinRequestViewModel implements TeamJoinRequestItemDto {
|
||||
id: string;
|
||||
teamId: string;
|
||||
driverId: string;
|
||||
requestedAt: string;
|
||||
message?: string;
|
||||
|
||||
private currentUserId: string;
|
||||
private isOwner: boolean;
|
||||
|
||||
constructor(dto: TeamJoinRequestItemDto, currentUserId: string, isOwner: boolean) {
|
||||
Object.assign(this, dto);
|
||||
this.currentUserId = currentUserId;
|
||||
this.isOwner = isOwner;
|
||||
}
|
||||
|
||||
/** UI-specific: Whether current user can approve */
|
||||
get canApprove(): boolean {
|
||||
return this.isOwner;
|
||||
}
|
||||
|
||||
/** UI-specific: Formatted requested date */
|
||||
get formattedRequestedAt(): string {
|
||||
return new Date(this.requestedAt).toLocaleString();
|
||||
}
|
||||
|
||||
/** UI-specific: Request status (pending) */
|
||||
get status(): string {
|
||||
return 'Pending';
|
||||
}
|
||||
|
||||
/** UI-specific: Status color */
|
||||
get statusColor(): string {
|
||||
return 'yellow';
|
||||
}
|
||||
|
||||
/** UI-specific: Approve button text */
|
||||
get approveButtonText(): string {
|
||||
return 'Approve';
|
||||
}
|
||||
|
||||
/** UI-specific: Reject button text */
|
||||
get rejectButtonText(): string {
|
||||
return 'Reject';
|
||||
}
|
||||
}
|
||||
47
apps/website/lib/view-models/TeamMemberViewModel.ts
Normal file
47
apps/website/lib/view-models/TeamMemberViewModel.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { TeamMemberDto, DriverDto } from '../dtos';
|
||||
|
||||
export class TeamMemberViewModel implements TeamMemberDto {
|
||||
driverId: string;
|
||||
driver?: DriverDto;
|
||||
role: string;
|
||||
joinedAt: string;
|
||||
|
||||
private currentUserId: string;
|
||||
private teamOwnerId: string;
|
||||
|
||||
constructor(dto: TeamMemberDto, currentUserId: string, teamOwnerId: string) {
|
||||
Object.assign(this, dto);
|
||||
this.currentUserId = currentUserId;
|
||||
this.teamOwnerId = teamOwnerId;
|
||||
}
|
||||
|
||||
/** UI-specific: Role badge variant */
|
||||
get roleBadgeVariant(): string {
|
||||
switch (this.role) {
|
||||
case 'owner': return 'primary';
|
||||
case 'captain': return 'secondary';
|
||||
case 'member': return 'default';
|
||||
default: return 'default';
|
||||
}
|
||||
}
|
||||
|
||||
/** UI-specific: Whether this member is the owner */
|
||||
get isOwner(): boolean {
|
||||
return this.driverId === this.teamOwnerId;
|
||||
}
|
||||
|
||||
/** UI-specific: Whether current user can manage this member */
|
||||
get canManage(): boolean {
|
||||
return this.currentUserId === this.teamOwnerId && this.driverId !== this.currentUserId;
|
||||
}
|
||||
|
||||
/** UI-specific: Whether this is the current user */
|
||||
get isCurrentUser(): boolean {
|
||||
return this.driverId === this.currentUserId;
|
||||
}
|
||||
|
||||
/** UI-specific: Formatted joined date */
|
||||
get formattedJoinedAt(): string {
|
||||
return new Date(this.joinedAt).toLocaleDateString();
|
||||
}
|
||||
}
|
||||
41
apps/website/lib/view-models/TeamSummaryViewModel.ts
Normal file
41
apps/website/lib/view-models/TeamSummaryViewModel.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { TeamSummaryDto } from '../dtos';
|
||||
|
||||
export class TeamSummaryViewModel implements TeamSummaryDto {
|
||||
id: string;
|
||||
name: string;
|
||||
logoUrl?: string;
|
||||
memberCount: number;
|
||||
rating: number;
|
||||
|
||||
private maxMembers = 10; // Assuming max members
|
||||
|
||||
constructor(dto: TeamSummaryDto) {
|
||||
Object.assign(this, dto);
|
||||
}
|
||||
|
||||
/** UI-specific: Whether team is full */
|
||||
get isFull(): boolean {
|
||||
return this.memberCount >= this.maxMembers;
|
||||
}
|
||||
|
||||
/** UI-specific: Rating display */
|
||||
get ratingDisplay(): string {
|
||||
return this.rating.toFixed(0);
|
||||
}
|
||||
|
||||
/** UI-specific: Member count display */
|
||||
get memberCountDisplay(): string {
|
||||
return `${this.memberCount}/${this.maxMembers}`;
|
||||
}
|
||||
|
||||
/** UI-specific: Status indicator */
|
||||
get statusIndicator(): string {
|
||||
if (this.isFull) return 'Full';
|
||||
return 'Open';
|
||||
}
|
||||
|
||||
/** UI-specific: Status color */
|
||||
get statusColor(): string {
|
||||
return this.isFull ? 'red' : 'green';
|
||||
}
|
||||
}
|
||||
37
apps/website/lib/view-models/UserProfileViewModel.ts
Normal file
37
apps/website/lib/view-models/UserProfileViewModel.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { DriverDto } from '../dtos';
|
||||
|
||||
export class UserProfileViewModel implements DriverDto {
|
||||
id: string;
|
||||
name: string;
|
||||
avatarUrl?: string;
|
||||
iracingId?: string;
|
||||
rating?: number;
|
||||
|
||||
constructor(dto: DriverDto) {
|
||||
Object.assign(this, dto);
|
||||
}
|
||||
|
||||
/** UI-specific: Formatted rating */
|
||||
get formattedRating(): string {
|
||||
return this.rating ? this.rating.toFixed(0) : 'Unrated';
|
||||
}
|
||||
|
||||
/** UI-specific: Whether has iRacing ID */
|
||||
get hasIracingId(): boolean {
|
||||
return !!this.iracingId;
|
||||
}
|
||||
|
||||
/** UI-specific: Profile completeness percentage */
|
||||
get profileCompleteness(): number {
|
||||
let complete = 1; // id always there
|
||||
if (this.avatarUrl) complete++;
|
||||
if (this.iracingId) complete++;
|
||||
if (this.rating) complete++;
|
||||
return Math.round((complete / 4) * 100);
|
||||
}
|
||||
|
||||
/** UI-specific: Avatar initials */
|
||||
get avatarInitials(): string {
|
||||
return this.name.split(' ').map(n => n[0]).join('').toUpperCase();
|
||||
}
|
||||
}
|
||||
34
apps/website/lib/view-models/WalletTransactionViewModel.ts
Normal file
34
apps/website/lib/view-models/WalletTransactionViewModel.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { WalletTransactionDto } from '../dtos';
|
||||
|
||||
export class WalletTransactionViewModel implements WalletTransactionDto {
|
||||
id: string;
|
||||
type: 'deposit' | 'withdrawal';
|
||||
amount: number;
|
||||
description?: string;
|
||||
createdAt: string;
|
||||
|
||||
constructor(dto: WalletTransactionDto) {
|
||||
Object.assign(this, dto);
|
||||
}
|
||||
|
||||
/** UI-specific: Formatted amount with sign */
|
||||
get formattedAmount(): string {
|
||||
const sign = this.type === 'deposit' ? '+' : '-';
|
||||
return `${sign}$${this.amount.toFixed(2)}`;
|
||||
}
|
||||
|
||||
/** UI-specific: Amount color */
|
||||
get amountColor(): string {
|
||||
return this.type === 'deposit' ? 'green' : 'red';
|
||||
}
|
||||
|
||||
/** UI-specific: Type display */
|
||||
get typeDisplay(): string {
|
||||
return this.type.charAt(0).toUpperCase() + this.type.slice(1);
|
||||
}
|
||||
|
||||
/** UI-specific: Formatted created date */
|
||||
get formattedCreatedAt(): string {
|
||||
return new Date(this.createdAt).toLocaleString();
|
||||
}
|
||||
}
|
||||
36
apps/website/lib/view-models/WalletViewModel.ts
Normal file
36
apps/website/lib/view-models/WalletViewModel.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { WalletDto, WalletTransactionDto } from '../dtos';
|
||||
import { WalletTransactionViewModel } from './WalletTransactionViewModel';
|
||||
|
||||
export class WalletViewModel implements WalletDto {
|
||||
driverId: string;
|
||||
balance: number;
|
||||
currency: string;
|
||||
transactions: WalletTransactionViewModel[];
|
||||
|
||||
constructor(dto: WalletDto & { transactions: WalletTransactionDto[] }) {
|
||||
this.driverId = dto.driverId;
|
||||
this.balance = dto.balance;
|
||||
this.currency = dto.currency;
|
||||
this.transactions = dto.transactions.map(t => new WalletTransactionViewModel(t));
|
||||
}
|
||||
|
||||
/** UI-specific: Formatted balance */
|
||||
get formattedBalance(): string {
|
||||
return `${this.currency} ${this.balance.toFixed(2)}`;
|
||||
}
|
||||
|
||||
/** UI-specific: Balance color */
|
||||
get balanceColor(): string {
|
||||
return this.balance >= 0 ? 'green' : 'red';
|
||||
}
|
||||
|
||||
/** UI-specific: Recent transactions (last 5) */
|
||||
get recentTransactions(): WalletTransactionViewModel[] {
|
||||
return this.transactions.slice(0, 5);
|
||||
}
|
||||
|
||||
/** UI-specific: Total transactions count */
|
||||
get totalTransactions(): number {
|
||||
return this.transactions.length;
|
||||
}
|
||||
}
|
||||
41
apps/website/lib/view-models/index.ts
Normal file
41
apps/website/lib/view-models/index.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
// Analytics ViewModels
|
||||
export { AnalyticsDashboardViewModel } from './AnalyticsDashboardViewModel';
|
||||
export { AnalyticsMetricsViewModel } from './AnalyticsMetricsViewModel';
|
||||
|
||||
// Auth ViewModels
|
||||
export { SessionViewModel } from './SessionViewModel';
|
||||
export { UserProfileViewModel } from './UserProfileViewModel';
|
||||
|
||||
// Driver ViewModels
|
||||
export { DriverLeaderboardItemViewModel } from './DriverLeaderboardItemViewModel';
|
||||
export { DriverLeaderboardViewModel } from './DriverLeaderboardViewModel';
|
||||
export { DriverRegistrationStatusViewModel } from './DriverRegistrationStatusViewModel';
|
||||
|
||||
// League ViewModels
|
||||
export { LeagueMemberViewModel } from './LeagueMemberViewModel';
|
||||
export { LeagueStandingsViewModel } from './LeagueStandingsViewModel';
|
||||
export { LeagueSummaryViewModel } from './LeagueSummaryViewModel';
|
||||
export { StandingEntryViewModel } from './StandingEntryViewModel';
|
||||
|
||||
// Payments ViewModels
|
||||
export { MembershipFeeViewModel } from './MembershipFeeViewModel';
|
||||
export { PaymentViewModel } from './PaymentViewModel';
|
||||
export { PrizeViewModel } from './PrizeViewModel';
|
||||
export { WalletTransactionViewModel } from './WalletTransactionViewModel';
|
||||
export { WalletViewModel } from './WalletViewModel';
|
||||
|
||||
// Race ViewModels
|
||||
export { RaceDetailViewModel } from './RaceDetailViewModel';
|
||||
export { RaceListItemViewModel } from './RaceListItemViewModel';
|
||||
export { RaceResultViewModel } from './RaceResultViewModel';
|
||||
export { RaceResultsDetailViewModel } from './RaceResultsDetailViewModel';
|
||||
|
||||
// Sponsor ViewModels
|
||||
export { SponsorViewModel } from './SponsorViewModel';
|
||||
export { SponsorshipDetailViewModel } from './SponsorshipDetailViewModel';
|
||||
|
||||
// Team ViewModels
|
||||
export { TeamDetailsViewModel } from './TeamDetailsViewModel';
|
||||
export { TeamJoinRequestViewModel } from './TeamJoinRequestViewModel';
|
||||
export { TeamMemberViewModel } from './TeamMemberViewModel';
|
||||
export { TeamSummaryViewModel } from './TeamSummaryViewModel';
|
||||
Reference in New Issue
Block a user