seed data
This commit is contained in:
@@ -78,6 +78,10 @@ export class SeedRacingData {
|
|||||||
return process.env.DATABASE_URL ? 'postgres' : 'inmemory';
|
return process.env.DATABASE_URL ? 'postgres' : 'inmemory';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getMediaBaseUrl(): string {
|
||||||
|
return process.env.NODE_ENV === 'development' ? 'http://localhost:3001' : 'https://api.gridpilot.io';
|
||||||
|
}
|
||||||
|
|
||||||
async execute(): Promise<void> {
|
async execute(): Promise<void> {
|
||||||
const existingDrivers = await this.seedDeps.driverRepository.findAll();
|
const existingDrivers = await this.seedDeps.driverRepository.findAll();
|
||||||
const persistence = this.getApiPersistence();
|
const persistence = this.getApiPersistence();
|
||||||
@@ -462,7 +466,7 @@ export class SeedRacingData {
|
|||||||
})));
|
})));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
logoUrl: `https://api.gridpilot.io/media/team/${team.id}/logo.png`,
|
logoUrl: `${this.getMediaBaseUrl()}/api/media/teams/${team.id}/logo`,
|
||||||
performanceLevel,
|
performanceLevel,
|
||||||
specialization,
|
specialization,
|
||||||
region,
|
region,
|
||||||
@@ -482,20 +486,21 @@ export class SeedRacingData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async seedMediaAssets(seed: any): Promise<void> {
|
private async seedMediaAssets(seed: any): Promise<void> {
|
||||||
// Seed driver avatars
|
const baseUrl = this.getMediaBaseUrl();
|
||||||
for (const driver of seed.drivers) {
|
|
||||||
const avatarUrl = `https://api.gridpilot.io/media/driver/${driver.id}/avatar.png`;
|
// Seed driver avatars using static files
|
||||||
|
for (const driver of seed.drivers) {
|
||||||
|
const avatarUrl = this.getDriverAvatarUrl(driver.id);
|
||||||
|
|
||||||
// Type assertion to access the helper method
|
|
||||||
const mediaRepo = this.seedDeps.mediaRepository as any;
|
const mediaRepo = this.seedDeps.mediaRepository as any;
|
||||||
if (mediaRepo.setDriverAvatar) {
|
if (mediaRepo.setDriverAvatar) {
|
||||||
mediaRepo.setDriverAvatar(driver.id, avatarUrl);
|
mediaRepo.setDriverAvatar(driver.id, avatarUrl);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Seed team logos
|
// Seed team logos using API routes
|
||||||
for (const team of seed.teams) {
|
for (const team of seed.teams) {
|
||||||
const logoUrl = `https://api.gridpilot.io/media/team/${team.id}/logo.png`;
|
const logoUrl = `${baseUrl}/api/media/teams/${team.id}/logo`;
|
||||||
|
|
||||||
const mediaRepo = this.seedDeps.mediaRepository as any;
|
const mediaRepo = this.seedDeps.mediaRepository as any;
|
||||||
if (mediaRepo.setTeamLogo) {
|
if (mediaRepo.setTeamLogo) {
|
||||||
@@ -505,7 +510,7 @@ export class SeedRacingData {
|
|||||||
|
|
||||||
// Seed track images
|
// Seed track images
|
||||||
for (const track of seed.tracks || []) {
|
for (const track of seed.tracks || []) {
|
||||||
const trackImageUrl = `https://api.gridpilot.io/media/track/${track.id}/image.png`;
|
const trackImageUrl = `${baseUrl}/api/media/tracks/${track.id}/image`;
|
||||||
|
|
||||||
const mediaRepo = this.seedDeps.mediaRepository as any;
|
const mediaRepo = this.seedDeps.mediaRepository as any;
|
||||||
if (mediaRepo.setTrackImage) {
|
if (mediaRepo.setTrackImage) {
|
||||||
@@ -516,7 +521,7 @@ export class SeedRacingData {
|
|||||||
// Seed category icons (if categories exist)
|
// Seed category icons (if categories exist)
|
||||||
const categories = ['beginner', 'intermediate', 'advanced', 'pro', 'endurance', 'sprint'];
|
const categories = ['beginner', 'intermediate', 'advanced', 'pro', 'endurance', 'sprint'];
|
||||||
for (const category of categories) {
|
for (const category of categories) {
|
||||||
const iconUrl = `https://api.gridpilot.io/media/category/${category}/icon.png`;
|
const iconUrl = `${baseUrl}/api/media/categories/${category}/icon`;
|
||||||
|
|
||||||
const mediaRepo = this.seedDeps.mediaRepository as any;
|
const mediaRepo = this.seedDeps.mediaRepository as any;
|
||||||
if (mediaRepo.setCategoryIcon) {
|
if (mediaRepo.setCategoryIcon) {
|
||||||
@@ -526,7 +531,7 @@ export class SeedRacingData {
|
|||||||
|
|
||||||
// Seed sponsor logos
|
// Seed sponsor logos
|
||||||
for (const sponsor of seed.sponsors || []) {
|
for (const sponsor of seed.sponsors || []) {
|
||||||
const logoUrl = `https://api.gridpilot.io/media/sponsor/${sponsor.id}/logo.png`;
|
const logoUrl = `${baseUrl}/api/media/sponsors/${sponsor.id}/logo`;
|
||||||
|
|
||||||
const mediaRepo = this.seedDeps.mediaRepository as any;
|
const mediaRepo = this.seedDeps.mediaRepository as any;
|
||||||
if (mediaRepo.setSponsorLogo) {
|
if (mediaRepo.setSponsorLogo) {
|
||||||
@@ -537,6 +542,48 @@ export class SeedRacingData {
|
|||||||
this.logger.info(`[Bootstrap] Seeded media assets for ${seed.drivers.length} drivers, ${seed.teams.length} teams`);
|
this.logger.info(`[Bootstrap] Seeded media assets for ${seed.drivers.length} drivers, ${seed.teams.length} teams`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get deterministic avatar URL for a driver based on their ID
|
||||||
|
* Uses static files from the website public directory
|
||||||
|
*/
|
||||||
|
private getDriverAvatarUrl(driverId: string): string {
|
||||||
|
// Deterministic selection based on driver ID
|
||||||
|
const numericSuffixMatch = driverId.match(/(\d+)$/);
|
||||||
|
let useFemale = false;
|
||||||
|
let useNeutral = false;
|
||||||
|
|
||||||
|
if (numericSuffixMatch && numericSuffixMatch[1]) {
|
||||||
|
const numericSuffix = parseInt(numericSuffixMatch[1], 10);
|
||||||
|
// 40% female, 40% male, 20% neutral
|
||||||
|
if (numericSuffix % 5 === 0) {
|
||||||
|
useNeutral = true;
|
||||||
|
} else if (numericSuffix % 2 === 0) {
|
||||||
|
useFemale = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback hash
|
||||||
|
let hash = 0;
|
||||||
|
for (let i = 0; i < driverId.length; i++) {
|
||||||
|
hash = (hash * 31 + driverId.charCodeAt(i)) | 0;
|
||||||
|
}
|
||||||
|
const hashValue = Math.abs(hash);
|
||||||
|
if (hashValue % 5 === 0) {
|
||||||
|
useNeutral = true;
|
||||||
|
} else if (hashValue % 2 === 0) {
|
||||||
|
useFemale = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return static file paths that Next.js can serve
|
||||||
|
if (useNeutral) {
|
||||||
|
return '/images/avatars/neutral-default-avatar.jpeg';
|
||||||
|
} else if (useFemale) {
|
||||||
|
return '/images/avatars/female-default-avatar.jpeg';
|
||||||
|
} else {
|
||||||
|
return '/images/avatars/male-default-avatar.jpg';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async clearExistingRacingData(): Promise<void> {
|
private async clearExistingRacingData(): Promise<void> {
|
||||||
// Get all existing drivers
|
// Get all existing drivers
|
||||||
const drivers = await this.seedDeps.driverRepository.findAll();
|
const drivers = await this.seedDeps.driverRepository.findAll();
|
||||||
|
|||||||
@@ -25,8 +25,51 @@ export class RacingDriverFactory {
|
|||||||
private readonly persistence: 'postgres' | 'inmemory' = 'inmemory',
|
private readonly persistence: 'postgres' | 'inmemory' = 'inmemory',
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get deterministic avatar URL for a driver based on their ID
|
||||||
|
* Uses static files from the website public directory
|
||||||
|
*/
|
||||||
|
getDriverAvatarUrl(driverId: string): string {
|
||||||
|
// Deterministic selection based on driver ID
|
||||||
|
const numericSuffixMatch = driverId.match(/(\d+)$/);
|
||||||
|
let useFemale = false;
|
||||||
|
let useNeutral = false;
|
||||||
|
|
||||||
|
if (numericSuffixMatch && numericSuffixMatch[1]) {
|
||||||
|
const numericSuffix = parseInt(numericSuffixMatch[1], 10);
|
||||||
|
// 40% female, 40% male, 20% neutral
|
||||||
|
if (numericSuffix % 5 === 0) {
|
||||||
|
useNeutral = true;
|
||||||
|
} else if (numericSuffix % 2 === 0) {
|
||||||
|
useFemale = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback hash
|
||||||
|
let hash = 0;
|
||||||
|
for (let i = 0; i < driverId.length; i++) {
|
||||||
|
hash = (hash * 31 + driverId.charCodeAt(i)) | 0;
|
||||||
|
}
|
||||||
|
const hashValue = Math.abs(hash);
|
||||||
|
if (hashValue % 5 === 0) {
|
||||||
|
useNeutral = true;
|
||||||
|
} else if (hashValue % 2 === 0) {
|
||||||
|
useFemale = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return static file paths that Next.js can serve
|
||||||
|
if (useNeutral) {
|
||||||
|
return '/images/avatars/neutral-default-avatar.jpeg';
|
||||||
|
} else if (useFemale) {
|
||||||
|
return '/images/avatars/female-default-avatar.jpeg';
|
||||||
|
} else {
|
||||||
|
return '/images/avatars/male-default-avatar.jpg';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
create(): Driver[] {
|
create(): Driver[] {
|
||||||
const countries = ['DE', 'NL', 'FR', 'GB', 'US', 'CA', 'SE', 'NO', 'IT', 'ES', 'AU', 'BR', 'JP', 'KR', 'RU', 'PL', 'CZ', 'HU', 'AT', 'CH'] as const;
|
const countries = ['DE', 'NL', 'FR', 'GB', 'US', 'CA', 'SE', 'NO', 'IT', 'ES', 'AU', 'BR', 'JP', 'KR', 'RU', 'PL', 'CZ', 'HU', 'AT', 'CH'] as const;
|
||||||
|
const categories = ['beginner', 'intermediate', 'advanced', 'pro', 'endurance', 'sprint'];
|
||||||
|
|
||||||
return Array.from({ length: this.driverCount }, (_, idx) => {
|
return Array.from({ length: this.driverCount }, (_, idx) => {
|
||||||
const i = idx + 1;
|
const i = idx + 1;
|
||||||
@@ -53,6 +96,9 @@ export class RacingDriverFactory {
|
|||||||
joinedAt = faker.date.past({ years: 2, refDate: this.baseDate });
|
joinedAt = faker.date.past({ years: 2, refDate: this.baseDate });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Assign category - use all available categories
|
||||||
|
const category = faker.helpers.arrayElement(categories);
|
||||||
|
|
||||||
const driverData: {
|
const driverData: {
|
||||||
id: string;
|
id: string;
|
||||||
iracingId: string;
|
iracingId: string;
|
||||||
@@ -60,12 +106,14 @@ export class RacingDriverFactory {
|
|||||||
country: string;
|
country: string;
|
||||||
bio?: string;
|
bio?: string;
|
||||||
joinedAt?: Date;
|
joinedAt?: Date;
|
||||||
|
category?: string;
|
||||||
} = {
|
} = {
|
||||||
id: seedId(`driver-${i}`, this.persistence),
|
id: seedId(`driver-${i}`, this.persistence),
|
||||||
iracingId: String(100000 + i),
|
iracingId: String(100000 + i),
|
||||||
name: faker.person.fullName(),
|
name: faker.person.fullName(),
|
||||||
country: faker.helpers.arrayElement(countries),
|
country: faker.helpers.arrayElement(countries),
|
||||||
joinedAt,
|
joinedAt,
|
||||||
|
category,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (hasBio) {
|
if (hasBio) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { League } from '@core/racing/domain/entities/League';
|
import { League, LeagueSettings } from '@core/racing/domain/entities/League';
|
||||||
import { Driver } from '@core/racing/domain/entities/Driver';
|
import { Driver } from '@core/racing/domain/entities/Driver';
|
||||||
import { faker } from '@faker-js/faker';
|
import { faker } from '@faker-js/faker';
|
||||||
import { seedId } from './SeedIdHelper';
|
import { seedId } from './SeedIdHelper';
|
||||||
@@ -11,7 +11,7 @@ export class RacingLeagueFactory {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
create(): League[] {
|
create(): League[] {
|
||||||
const leagueCount = 30;
|
const leagueCount = 120; // Expanded to 100+ leagues
|
||||||
|
|
||||||
// Create diverse league configurations covering ALL enum combinations
|
// Create diverse league configurations covering ALL enum combinations
|
||||||
// Points systems: f1-2024, indycar, custom (3)
|
// Points systems: f1-2024, indycar, custom (3)
|
||||||
@@ -20,6 +20,8 @@ export class RacingLeagueFactory {
|
|||||||
// Decision modes: admin_only, steward_decides, steward_vote, member_vote, steward_veto, member_veto (6)
|
// Decision modes: admin_only, steward_decides, steward_vote, member_vote, steward_veto, member_veto (6)
|
||||||
// Total combinations: 3 * 2 * 2 * 6 = 72, but we'll sample 30 covering extremes
|
// Total combinations: 3 * 2 * 2 * 6 = 72, but we'll sample 30 covering extremes
|
||||||
|
|
||||||
|
// Category types for leagues
|
||||||
|
|
||||||
const leagueConfigs = [
|
const leagueConfigs = [
|
||||||
// 1-5: Ranked, F1-2024, various stewarding
|
// 1-5: Ranked, F1-2024, various stewarding
|
||||||
{
|
{
|
||||||
@@ -277,7 +279,8 @@ export class RacingLeagueFactory {
|
|||||||
return Array.from({ length: leagueCount }, (_, idx) => {
|
return Array.from({ length: leagueCount }, (_, idx) => {
|
||||||
const i = idx + 1;
|
const i = idx + 1;
|
||||||
const owner = faker.helpers.arrayElement(this.drivers);
|
const owner = faker.helpers.arrayElement(this.drivers);
|
||||||
const config = leagueConfigs[idx]!;
|
// Cycle through the 30 configs for variety
|
||||||
|
const config = leagueConfigs[idx % 30]!;
|
||||||
|
|
||||||
const createdAt =
|
const createdAt =
|
||||||
// Ensure some "New" leagues (created within 7 days) so `/leagues` featured categories are populated.
|
// Ensure some "New" leagues (created within 7 days) so `/leagues` featured categories are populated.
|
||||||
@@ -310,33 +313,29 @@ export class RacingLeagueFactory {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const leagueData: {
|
// Determine category based on scoring configuration
|
||||||
id: string;
|
let category: string | undefined;
|
||||||
name: string;
|
if (config.pointsSystem === 'f1-2024') {
|
||||||
description: string;
|
if (config.qualifyingFormat === 'single-lap') {
|
||||||
ownerId: string;
|
category = 'driver';
|
||||||
settings: {
|
} else {
|
||||||
pointsSystem: 'f1-2024' | 'indycar' | 'custom';
|
category = 'team';
|
||||||
maxDrivers: number;
|
}
|
||||||
sessionDuration: number;
|
} else if (config.pointsSystem === 'indycar') {
|
||||||
qualifyingFormat: 'open' | 'single-lap';
|
category = 'nations';
|
||||||
visibility?: 'ranked' | 'unranked';
|
} else if (config.pointsSystem === 'custom') {
|
||||||
stewarding?: {
|
category = 'trophy';
|
||||||
decisionMode: 'admin_only' | 'steward_decides' | 'steward_vote' | 'member_vote' | 'steward_veto' | 'member_veto';
|
}
|
||||||
requiredVotes?: number;
|
|
||||||
requireDefense?: boolean;
|
// Override some leagues to have endurance or sprint categories
|
||||||
defenseTimeLimit?: number;
|
if (idx % 8 === 0) {
|
||||||
voteTimeLimit?: number;
|
category = 'endurance';
|
||||||
protestDeadlineHours?: number;
|
} else if (idx % 7 === 0) {
|
||||||
stewardingClosesHours?: number;
|
category = 'sprint';
|
||||||
notifyAccusedOnProtest?: boolean;
|
}
|
||||||
notifyOnVoteRequired?: boolean;
|
|
||||||
};
|
// Build the league data object
|
||||||
};
|
const leagueData = {
|
||||||
createdAt: Date;
|
|
||||||
socialLinks?: { discordUrl?: string; youtubeUrl?: string; websiteUrl?: string };
|
|
||||||
participantCount?: number;
|
|
||||||
} = {
|
|
||||||
id: seedId(`league-${i}`, this.persistence),
|
id: seedId(`league-${i}`, this.persistence),
|
||||||
name: faker.company.name() + ' Racing League',
|
name: faker.company.name() + ' Racing League',
|
||||||
description: faker.lorem.sentences(2),
|
description: faker.lorem.sentences(2),
|
||||||
@@ -356,6 +355,7 @@ export class RacingLeagueFactory {
|
|||||||
notifyOnVoteRequired: true,
|
notifyOnVoteRequired: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
category,
|
||||||
createdAt,
|
createdAt,
|
||||||
participantCount,
|
participantCount,
|
||||||
};
|
};
|
||||||
@@ -374,11 +374,37 @@ export class RacingLeagueFactory {
|
|||||||
if (type === 'website') socialLinks.websiteUrl = faker.internet.url();
|
if (type === 'website') socialLinks.websiteUrl = faker.internet.url();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Create the final league data with optional fields
|
||||||
|
const finalLeagueData: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
ownerId: string;
|
||||||
|
settings?: Partial<LeagueSettings>;
|
||||||
|
category?: string | undefined;
|
||||||
|
createdAt?: Date;
|
||||||
|
socialLinks?: {
|
||||||
|
discordUrl?: string;
|
||||||
|
youtubeUrl?: string;
|
||||||
|
websiteUrl?: string;
|
||||||
|
};
|
||||||
|
participantCount?: number;
|
||||||
|
} = {
|
||||||
|
id: leagueData.id,
|
||||||
|
name: leagueData.name,
|
||||||
|
description: leagueData.description,
|
||||||
|
ownerId: leagueData.ownerId,
|
||||||
|
settings: leagueData.settings,
|
||||||
|
category: leagueData.category,
|
||||||
|
createdAt: leagueData.createdAt,
|
||||||
|
participantCount: leagueData.participantCount,
|
||||||
|
};
|
||||||
|
|
||||||
if (Object.keys(socialLinks).length > 0) {
|
if (Object.keys(socialLinks).length > 0) {
|
||||||
leagueData.socialLinks = socialLinks;
|
finalLeagueData.socialLinks = socialLinks;
|
||||||
}
|
}
|
||||||
|
|
||||||
return League.create(leagueData);
|
return League.create(finalLeagueData);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -14,6 +14,7 @@ export interface TeamStats {
|
|||||||
totalWins: number;
|
totalWins: number;
|
||||||
totalRaces: number;
|
totalRaces: number;
|
||||||
rating: number;
|
rating: number;
|
||||||
|
category?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class RacingTeamFactory {
|
export class RacingTeamFactory {
|
||||||
@@ -33,6 +34,9 @@ export class RacingTeamFactory {
|
|||||||
{ min: 0, max: 3 },
|
{ min: 0, max: 3 },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 30-50% of teams are recruiting
|
||||||
|
const isRecruiting = faker.datatype.boolean({ probability: 0.4 });
|
||||||
|
|
||||||
return Team.create({
|
return Team.create({
|
||||||
id: seedId(`team-${i}`, this.persistence),
|
id: seedId(`team-${i}`, this.persistence),
|
||||||
name: faker.company.name() + ' Racing',
|
name: faker.company.name() + ' Racing',
|
||||||
@@ -40,6 +44,7 @@ export class RacingTeamFactory {
|
|||||||
description: faker.lorem.sentences(2),
|
description: faker.lorem.sentences(2),
|
||||||
ownerId: owner.id,
|
ownerId: owner.id,
|
||||||
leagues: teamLeagues,
|
leagues: teamLeagues,
|
||||||
|
isRecruiting,
|
||||||
createdAt: faker.date.past({ years: 2, refDate: this.baseDate }),
|
createdAt: faker.date.past({ years: 2, refDate: this.baseDate }),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -256,6 +261,10 @@ export class RacingTeamFactory {
|
|||||||
specialization = 'mixed';
|
specialization = 'mixed';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Determine category - use all available categories
|
||||||
|
const categories = ['beginner', 'intermediate', 'advanced', 'pro', 'endurance', 'sprint'];
|
||||||
|
const category = faker.helpers.arrayElement(categories);
|
||||||
|
|
||||||
// Generate region and languages
|
// Generate region and languages
|
||||||
const region = faker.helpers.arrayElement(regions);
|
const region = faker.helpers.arrayElement(regions);
|
||||||
const languageCount = faker.number.int({ min: 1, max: 3 });
|
const languageCount = faker.number.int({ min: 1, max: 3 });
|
||||||
@@ -273,6 +282,7 @@ export class RacingTeamFactory {
|
|||||||
totalWins,
|
totalWins,
|
||||||
totalRaces,
|
totalRaces,
|
||||||
rating,
|
rating,
|
||||||
|
category,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -8,42 +8,21 @@ export class InMemoryImageServiceAdapter implements IImageServicePort {
|
|||||||
|
|
||||||
getDriverAvatar(driverId: string): string {
|
getDriverAvatar(driverId: string): string {
|
||||||
this.logger.debug(`[InMemoryImageServiceAdapter] Getting avatar for driver: ${driverId}`);
|
this.logger.debug(`[InMemoryImageServiceAdapter] Getting avatar for driver: ${driverId}`);
|
||||||
const driverNumber = Number(driverId.replace('driver-', ''));
|
return `/media/avatar/${driverId}`;
|
||||||
const index = Number.isFinite(driverNumber) ? driverNumber % 3 : 0;
|
|
||||||
|
|
||||||
const avatars = [
|
|
||||||
'/images/avatars/male-default-avatar.jpg',
|
|
||||||
'/images/avatars/female-default-avatar.jpeg',
|
|
||||||
'/images/avatars/neutral-default-avatar.jpeg',
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
return avatars[index] ?? avatars[0];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getTeamLogo(teamId: string): string {
|
getTeamLogo(teamId: string): string {
|
||||||
this.logger.debug(`[InMemoryImageServiceAdapter] Getting logo for team: ${teamId}`);
|
this.logger.debug(`[InMemoryImageServiceAdapter] Getting logo for team: ${teamId}`);
|
||||||
const teamNumber = Number(teamId.replace('team-', ''));
|
return `/media/teams/${teamId}/logo`;
|
||||||
const index = Number.isFinite(teamNumber) ? teamNumber % 6 : 0;
|
|
||||||
|
|
||||||
const logos = [
|
|
||||||
'/images/ff1600.jpeg',
|
|
||||||
'/images/header.jpeg',
|
|
||||||
'/images/avatars/male-default-avatar.jpg',
|
|
||||||
'/images/avatars/female-default-avatar.jpeg',
|
|
||||||
'/images/avatars/neutral-default-avatar.jpeg',
|
|
||||||
'/images/leagues/placeholder-cover.svg',
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
return logos[index] ?? logos[0];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getLeagueCover(leagueId: string): string {
|
getLeagueCover(leagueId: string): string {
|
||||||
this.logger.debug(`[InMemoryImageServiceAdapter] Getting cover for league: ${leagueId}`);
|
this.logger.debug(`[InMemoryImageServiceAdapter] Getting cover for league: ${leagueId}`);
|
||||||
return '/images/header.jpeg';
|
return `/media/leagues/${leagueId}/cover`;
|
||||||
}
|
}
|
||||||
|
|
||||||
getLeagueLogo(leagueId: string): string {
|
getLeagueLogo(leagueId: string): string {
|
||||||
this.logger.debug(`[InMemoryImageServiceAdapter] Getting logo for league: ${leagueId}`);
|
this.logger.debug(`[InMemoryImageServiceAdapter] Getting logo for league: ${leagueId}`);
|
||||||
return '/images/ff1600.jpeg';
|
return `/media/leagues/${leagueId}/logo`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,32 +1,18 @@
|
|||||||
/**
|
import type { Logger } from '@core/shared/application/Logger';
|
||||||
* Infrastructure Adapter: InMemoryTeamStatsRepository
|
|
||||||
*
|
|
||||||
* In-memory implementation of ITeamStatsRepository.
|
|
||||||
* Stores computed team statistics for caching and frontend queries.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { ITeamStatsRepository, TeamStats } from '@core/racing/domain/repositories/ITeamStatsRepository';
|
import type { ITeamStatsRepository, TeamStats } from '@core/racing/domain/repositories/ITeamStatsRepository';
|
||||||
import type { Logger } from '@core/shared/application';
|
|
||||||
|
|
||||||
export class InMemoryTeamStatsRepository implements ITeamStatsRepository {
|
export class InMemoryTeamStatsRepository implements ITeamStatsRepository {
|
||||||
private stats = new Map<string, TeamStats>();
|
private readonly stats = new Map<string, TeamStats>();
|
||||||
|
|
||||||
constructor(private readonly logger: Logger) {
|
constructor(private readonly logger: Logger) {}
|
||||||
this.logger.info('[InMemoryTeamStatsRepository] Initialized.');
|
|
||||||
}
|
|
||||||
|
|
||||||
async getTeamStats(teamId: string): Promise<TeamStats | null> {
|
async getTeamStats(teamId: string): Promise<TeamStats | null> {
|
||||||
this.logger.debug(`[InMemoryTeamStatsRepository] Getting stats for team: ${teamId}`);
|
this.logger.debug(`[InMemoryTeamStatsRepository] Getting stats for team: ${teamId}`);
|
||||||
return this.stats.get(teamId) ?? null;
|
return this.stats.get(teamId) || null;
|
||||||
}
|
|
||||||
|
|
||||||
getTeamStatsSync(teamId: string): TeamStats | null {
|
|
||||||
this.logger.debug(`[InMemoryTeamStatsRepository] Getting stats (sync) for team: ${teamId}`);
|
|
||||||
return this.stats.get(teamId) ?? null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveTeamStats(teamId: string, stats: TeamStats): Promise<void> {
|
async saveTeamStats(teamId: string, stats: TeamStats): Promise<void> {
|
||||||
this.logger.debug(`[InMemoryTeamStatsRepository] Saving stats for team: ${teamId}`);
|
this.logger.debug(`[InMemoryTeamStatsRepository] Saving stats for team: ${teamId}`, stats);
|
||||||
this.stats.set(teamId, stats);
|
this.stats.set(teamId, stats);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,7 +22,7 @@ export class InMemoryTeamStatsRepository implements ITeamStatsRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async clear(): Promise<void> {
|
async clear(): Promise<void> {
|
||||||
this.logger.info('[InMemoryTeamStatsRepository] Clearing all stats');
|
this.logger.debug('[InMemoryTeamStatsRepository] Clearing all stats');
|
||||||
this.stats.clear();
|
this.stats.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -19,4 +19,7 @@ export class DriverOrmEntity {
|
|||||||
|
|
||||||
@Column({ type: 'timestamptz' })
|
@Column({ type: 'timestamptz' })
|
||||||
joinedAt!: Date;
|
joinedAt!: Date;
|
||||||
|
|
||||||
|
@Column({ type: 'text', nullable: true })
|
||||||
|
category!: string | null;
|
||||||
}
|
}
|
||||||
@@ -19,6 +19,9 @@ export class LeagueOrmEntity {
|
|||||||
@Column({ type: 'jsonb' })
|
@Column({ type: 'jsonb' })
|
||||||
settings!: SerializedLeagueSettings;
|
settings!: SerializedLeagueSettings;
|
||||||
|
|
||||||
|
@Column({ type: 'text', nullable: true })
|
||||||
|
category!: string | null;
|
||||||
|
|
||||||
@CreateDateColumn({ type: 'timestamptz' })
|
@CreateDateColumn({ type: 'timestamptz' })
|
||||||
createdAt!: Date;
|
createdAt!: Date;
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,12 @@ export class TeamOrmEntity {
|
|||||||
@Column({ type: 'uuid', array: true })
|
@Column({ type: 'uuid', array: true })
|
||||||
leagues!: string[];
|
leagues!: string[];
|
||||||
|
|
||||||
|
@Column({ type: 'text', nullable: true })
|
||||||
|
category!: string | null;
|
||||||
|
|
||||||
|
@Column({ type: 'boolean', default: false })
|
||||||
|
isRecruiting!: boolean;
|
||||||
|
|
||||||
@Column({ type: 'timestamptz' })
|
@Column({ type: 'timestamptz' })
|
||||||
createdAt!: Date;
|
createdAt!: Date;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { Column, Entity, PrimaryColumn } from 'typeorm';
|
||||||
|
|
||||||
|
@Entity({ name: 'team_stats' })
|
||||||
|
export class TeamStatsOrmEntity {
|
||||||
|
@PrimaryColumn({ type: 'uuid' })
|
||||||
|
teamId!: string;
|
||||||
|
|
||||||
|
@Column({ type: 'text' })
|
||||||
|
logoUrl!: string;
|
||||||
|
|
||||||
|
@Column({ type: 'text' })
|
||||||
|
performanceLevel!: string;
|
||||||
|
|
||||||
|
@Column({ type: 'text' })
|
||||||
|
specialization!: string;
|
||||||
|
|
||||||
|
@Column({ type: 'text' })
|
||||||
|
region!: string;
|
||||||
|
|
||||||
|
@Column({ type: 'text', array: true })
|
||||||
|
languages!: string[];
|
||||||
|
|
||||||
|
@Column({ type: 'integer' })
|
||||||
|
totalWins!: number;
|
||||||
|
|
||||||
|
@Column({ type: 'integer' })
|
||||||
|
totalRaces!: number;
|
||||||
|
|
||||||
|
@Column({ type: 'integer' })
|
||||||
|
rating!: number;
|
||||||
|
|
||||||
|
@Column({ type: 'text', nullable: true })
|
||||||
|
category!: string | null;
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ export class DriverOrmMapper {
|
|||||||
entity.country = domain.country.toString();
|
entity.country = domain.country.toString();
|
||||||
entity.bio = domain.bio?.toString() ?? null;
|
entity.bio = domain.bio?.toString() ?? null;
|
||||||
entity.joinedAt = domain.joinedAt.toDate();
|
entity.joinedAt = domain.joinedAt.toDate();
|
||||||
|
entity.category = domain.category ?? null;
|
||||||
return entity;
|
return entity;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,14 +25,31 @@ export class DriverOrmMapper {
|
|||||||
assertNonEmptyString(entityName, 'country', entity.country);
|
assertNonEmptyString(entityName, 'country', entity.country);
|
||||||
assertDate(entityName, 'joinedAt', entity.joinedAt);
|
assertDate(entityName, 'joinedAt', entity.joinedAt);
|
||||||
assertOptionalStringOrNull(entityName, 'bio', entity.bio);
|
assertOptionalStringOrNull(entityName, 'bio', entity.bio);
|
||||||
|
assertOptionalStringOrNull(entityName, 'category', entity.category);
|
||||||
|
|
||||||
return Driver.rehydrate({
|
const props: {
|
||||||
|
id: string;
|
||||||
|
iracingId: string;
|
||||||
|
name: string;
|
||||||
|
country: string;
|
||||||
|
bio?: string;
|
||||||
|
joinedAt: Date;
|
||||||
|
category?: string;
|
||||||
|
} = {
|
||||||
id: entity.id,
|
id: entity.id,
|
||||||
iracingId: entity.iracingId,
|
iracingId: entity.iracingId,
|
||||||
name: entity.name,
|
name: entity.name,
|
||||||
country: entity.country,
|
country: entity.country,
|
||||||
...(entity.bio !== null && entity.bio !== undefined ? { bio: entity.bio } : {}),
|
|
||||||
joinedAt: entity.joinedAt,
|
joinedAt: entity.joinedAt,
|
||||||
});
|
};
|
||||||
|
|
||||||
|
if (entity.bio !== null && entity.bio !== undefined) {
|
||||||
|
props.bio = entity.bio;
|
||||||
|
}
|
||||||
|
if (entity.category !== null && entity.category !== undefined) {
|
||||||
|
props.category = entity.category;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Driver.rehydrate(props);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -155,6 +155,7 @@ export class LeagueOrmMapper {
|
|||||||
entity.description = domain.description.toString();
|
entity.description = domain.description.toString();
|
||||||
entity.ownerId = domain.ownerId.toString();
|
entity.ownerId = domain.ownerId.toString();
|
||||||
entity.settings = serializeLeagueSettings(domain.settings);
|
entity.settings = serializeLeagueSettings(domain.settings);
|
||||||
|
entity.category = domain.category ?? null;
|
||||||
entity.createdAt = domain.createdAt.toDate();
|
entity.createdAt = domain.createdAt.toDate();
|
||||||
entity.participantCount = domain.getParticipantCount();
|
entity.participantCount = domain.getParticipantCount();
|
||||||
entity.discordUrl = domain.socialLinks?.discordUrl ?? null;
|
entity.discordUrl = domain.socialLinks?.discordUrl ?? null;
|
||||||
@@ -172,6 +173,7 @@ export class LeagueOrmMapper {
|
|||||||
description: entity.description,
|
description: entity.description,
|
||||||
ownerId: entity.ownerId,
|
ownerId: entity.ownerId,
|
||||||
settings,
|
settings,
|
||||||
|
category: entity.category ?? undefined,
|
||||||
createdAt: entity.createdAt,
|
createdAt: entity.createdAt,
|
||||||
participantCount: entity.participantCount,
|
participantCount: entity.participantCount,
|
||||||
...(entity.discordUrl || entity.youtubeUrl || entity.websiteUrl
|
...(entity.discordUrl || entity.youtubeUrl || entity.websiteUrl
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ export class TeamOrmMapper {
|
|||||||
entity.description = domain.description.toString();
|
entity.description = domain.description.toString();
|
||||||
entity.ownerId = domain.ownerId.toString();
|
entity.ownerId = domain.ownerId.toString();
|
||||||
entity.leagues = domain.leagues.map((l) => l.toString());
|
entity.leagues = domain.leagues.map((l) => l.toString());
|
||||||
|
entity.category = domain.category ?? null;
|
||||||
|
entity.isRecruiting = domain.isRecruiting;
|
||||||
entity.createdAt = domain.createdAt.toDate();
|
entity.createdAt = domain.createdAt.toDate();
|
||||||
return entity;
|
return entity;
|
||||||
}
|
}
|
||||||
@@ -45,15 +47,32 @@ export class TeamOrmMapper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return Team.rehydrate({
|
const rehydrateProps: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
tag: string;
|
||||||
|
description: string;
|
||||||
|
ownerId: string;
|
||||||
|
leagues: string[];
|
||||||
|
category?: string;
|
||||||
|
isRecruiting: boolean;
|
||||||
|
createdAt: Date;
|
||||||
|
} = {
|
||||||
id: entity.id,
|
id: entity.id,
|
||||||
name: entity.name,
|
name: entity.name,
|
||||||
tag: entity.tag,
|
tag: entity.tag,
|
||||||
description: entity.description,
|
description: entity.description,
|
||||||
ownerId: entity.ownerId,
|
ownerId: entity.ownerId,
|
||||||
leagues: entity.leagues,
|
leagues: entity.leagues,
|
||||||
|
isRecruiting: entity.isRecruiting ?? false,
|
||||||
createdAt: entity.createdAt,
|
createdAt: entity.createdAt,
|
||||||
});
|
};
|
||||||
|
|
||||||
|
if (entity.category !== null && entity.category !== undefined) {
|
||||||
|
rehydrateProps.category = entity.category;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Team.rehydrate(rehydrateProps);
|
||||||
} catch {
|
} catch {
|
||||||
throw new TypeOrmPersistenceSchemaError({ entityName, fieldName: '__root', reason: 'invalid_shape' });
|
throw new TypeOrmPersistenceSchemaError({ entityName, fieldName: '__root', reason: 'invalid_shape' });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import type { TeamStats } from '@core/racing/domain/repositories/ITeamStatsRepository';
|
||||||
|
|
||||||
|
import { TeamStatsOrmEntity } from '../entities/TeamStatsOrmEntity';
|
||||||
|
import {
|
||||||
|
assertNonEmptyString,
|
||||||
|
assertInteger,
|
||||||
|
assertArray,
|
||||||
|
assertEnumValue
|
||||||
|
} from '../schema/TypeOrmSchemaGuards';
|
||||||
|
|
||||||
|
const PERFORMANCE_LEVELS = ['beginner', 'intermediate', 'advanced', 'pro'] as const;
|
||||||
|
const SPECIALIZATIONS = ['endurance', 'sprint', 'mixed'] as const;
|
||||||
|
|
||||||
|
export class TeamStatsOrmMapper {
|
||||||
|
toOrmEntity(teamId: string, domain: TeamStats): TeamStatsOrmEntity {
|
||||||
|
const entity = new TeamStatsOrmEntity();
|
||||||
|
entity.teamId = teamId;
|
||||||
|
entity.logoUrl = domain.logoUrl;
|
||||||
|
entity.performanceLevel = domain.performanceLevel;
|
||||||
|
entity.specialization = domain.specialization;
|
||||||
|
entity.region = domain.region;
|
||||||
|
entity.languages = domain.languages;
|
||||||
|
entity.totalWins = domain.totalWins;
|
||||||
|
entity.totalRaces = domain.totalRaces;
|
||||||
|
entity.rating = domain.rating;
|
||||||
|
entity.category = domain.category ?? null;
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
toDomain(entity: TeamStatsOrmEntity): TeamStats {
|
||||||
|
const entityName = 'TeamStats';
|
||||||
|
|
||||||
|
assertNonEmptyString(entityName, 'teamId', entity.teamId);
|
||||||
|
assertNonEmptyString(entityName, 'logoUrl', entity.logoUrl);
|
||||||
|
assertEnumValue(entityName, 'performanceLevel', entity.performanceLevel, PERFORMANCE_LEVELS);
|
||||||
|
assertEnumValue(entityName, 'specialization', entity.specialization, SPECIALIZATIONS);
|
||||||
|
assertNonEmptyString(entityName, 'region', entity.region);
|
||||||
|
assertArray(entityName, 'languages', entity.languages);
|
||||||
|
assertInteger(entityName, 'totalWins', entity.totalWins);
|
||||||
|
assertInteger(entityName, 'totalRaces', entity.totalRaces);
|
||||||
|
assertInteger(entityName, 'rating', entity.rating);
|
||||||
|
|
||||||
|
const result: TeamStats = {
|
||||||
|
logoUrl: entity.logoUrl,
|
||||||
|
performanceLevel: entity.performanceLevel as 'beginner' | 'intermediate' | 'advanced' | 'pro',
|
||||||
|
specialization: entity.specialization as 'endurance' | 'sprint' | 'mixed',
|
||||||
|
region: entity.region,
|
||||||
|
languages: entity.languages,
|
||||||
|
totalWins: entity.totalWins,
|
||||||
|
totalRaces: entity.totalRaces,
|
||||||
|
rating: entity.rating,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (entity.category !== null && entity.category !== undefined) {
|
||||||
|
result.category = entity.category;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import type { ITeamStatsRepository, TeamStats } from '@core/racing/domain/repositories/ITeamStatsRepository';
|
||||||
|
import type { Repository } from 'typeorm';
|
||||||
|
|
||||||
|
import { TeamStatsOrmEntity } from '../entities/TeamStatsOrmEntity';
|
||||||
|
import { TeamStatsOrmMapper } from '../mappers/TeamStatsOrmMapper';
|
||||||
|
|
||||||
|
export class TypeOrmTeamStatsRepository implements ITeamStatsRepository {
|
||||||
|
constructor(
|
||||||
|
private readonly repo: Repository<TeamStatsOrmEntity>,
|
||||||
|
private readonly mapper: TeamStatsOrmMapper,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async getTeamStats(teamId: string): Promise<TeamStats | null> {
|
||||||
|
const entity = await this.repo.findOne({ where: { teamId } });
|
||||||
|
return entity ? this.mapper.toDomain(entity) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveTeamStats(teamId: string, stats: TeamStats): Promise<void> {
|
||||||
|
const entity = this.mapper.toOrmEntity(teamId, stats);
|
||||||
|
await this.repo.save(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAllStats(): Promise<Map<string, TeamStats>> {
|
||||||
|
const entities = await this.repo.find();
|
||||||
|
const statsMap = new Map<string, TeamStats>();
|
||||||
|
|
||||||
|
for (const entity of entities) {
|
||||||
|
statsMap.set(entity.teamId, this.mapper.toDomain(entity));
|
||||||
|
}
|
||||||
|
|
||||||
|
return statsMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
async clear(): Promise<void> {
|
||||||
|
await this.repo.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2710,6 +2710,21 @@
|
|||||||
},
|
},
|
||||||
"joinedAt": {
|
"joinedAt": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"rating": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"experienceLevel": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"wins": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"podiums": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"totalRaces": {
|
||||||
|
"type": "number"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
@@ -6607,6 +6622,21 @@
|
|||||||
"items": {
|
"items": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"totalWins": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"totalRaces": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"performanceLevel": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"logoUrl": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"rating": {
|
||||||
|
"type": "number"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
|
|||||||
@@ -18,6 +18,11 @@ export class DashboardDriverSummaryDTO {
|
|||||||
@IsString()
|
@IsString()
|
||||||
avatarUrl!: string;
|
avatarUrl!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ nullable: true })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
category?: string | null;
|
||||||
|
|
||||||
@ApiProperty({ nullable: true })
|
@ApiProperty({ nullable: true })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsNumber()
|
@IsNumber()
|
||||||
|
|||||||
@@ -20,6 +20,11 @@ export class DashboardDriverSummaryDTO {
|
|||||||
@IsString()
|
@IsString()
|
||||||
avatarUrl?: string | null;
|
avatarUrl?: string | null;
|
||||||
|
|
||||||
|
@ApiProperty({ nullable: true })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
category?: string | null;
|
||||||
|
|
||||||
@ApiProperty({ nullable: true })
|
@ApiProperty({ nullable: true })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsNumber()
|
@IsNumber()
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ export class DashboardOverviewPresenter implements UseCaseOutputPort<DashboardOv
|
|||||||
name: String(data.currentDriver.driver.name),
|
name: String(data.currentDriver.driver.name),
|
||||||
country: String(data.currentDriver.driver.country),
|
country: String(data.currentDriver.driver.country),
|
||||||
avatarUrl: data.currentDriver.avatarUrl,
|
avatarUrl: data.currentDriver.avatarUrl,
|
||||||
|
category: data.currentDriver.driver.category ?? null,
|
||||||
rating: data.currentDriver.rating,
|
rating: data.currentDriver.rating,
|
||||||
globalRank: data.currentDriver.globalRank,
|
globalRank: data.currentDriver.globalRank,
|
||||||
totalRaces: data.currentDriver.totalRaces,
|
totalRaces: data.currentDriver.totalRaces,
|
||||||
|
|||||||
@@ -18,4 +18,7 @@ export class DriverDTO {
|
|||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
joinedAt!: string;
|
joinedAt!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ required: false })
|
||||||
|
category?: string;
|
||||||
}
|
}
|
||||||
@@ -13,6 +13,9 @@ export class DriverLeaderboardItemDTO {
|
|||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
skillLevel!: string; // Assuming skillLevel is a string like 'Rookie', 'Pro', etc.
|
skillLevel!: string; // Assuming skillLevel is a string like 'Rookie', 'Pro', etc.
|
||||||
|
|
||||||
|
@ApiProperty({ required: false })
|
||||||
|
category?: string;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
nationality!: string;
|
nationality!: string;
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ export class DriverProfileDriverSummaryDTO {
|
|||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
joinedAt!: string;
|
joinedAt!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ nullable: true })
|
||||||
|
category!: string | null;
|
||||||
|
|
||||||
@ApiProperty({ nullable: true })
|
@ApiProperty({ nullable: true })
|
||||||
rating!: number | null;
|
rating!: number | null;
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ export class GetDriverOutputDTO {
|
|||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
joinedAt!: string;
|
joinedAt!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ required: false })
|
||||||
|
category?: string;
|
||||||
|
|
||||||
@ApiProperty({ required: false })
|
@ApiProperty({ required: false })
|
||||||
rating?: number;
|
rating?: number;
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ export class DriverPresenter {
|
|||||||
country: driver.country.toString(),
|
country: driver.country.toString(),
|
||||||
joinedAt: driver.joinedAt.toDate().toISOString(),
|
joinedAt: driver.joinedAt.toDate().toISOString(),
|
||||||
...(driver.bio ? { bio: driver.bio.toString() } : {}),
|
...(driver.bio ? { bio: driver.bio.toString() } : {}),
|
||||||
|
...(driver.category ? { category: driver.category } : {}),
|
||||||
// Add stats fields
|
// Add stats fields
|
||||||
...(stats ? {
|
...(stats ? {
|
||||||
rating: stats.rating,
|
rating: stats.rating,
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export class DriverProfilePresenter
|
|||||||
avatarUrl: this.getAvatarUrl(result.driverInfo.driver.id) || '',
|
avatarUrl: this.getAvatarUrl(result.driverInfo.driver.id) || '',
|
||||||
iracingId: result.driverInfo.driver.iracingId.toString(),
|
iracingId: result.driverInfo.driver.iracingId.toString(),
|
||||||
joinedAt: result.driverInfo.driver.joinedAt.toDate().toISOString(),
|
joinedAt: result.driverInfo.driver.joinedAt.toDate().toISOString(),
|
||||||
|
category: result.driverInfo.driver.category || null,
|
||||||
rating: result.driverInfo.rating,
|
rating: result.driverInfo.rating,
|
||||||
globalRank: result.driverInfo.globalRank,
|
globalRank: result.driverInfo.globalRank,
|
||||||
consistency: result.driverInfo.consistency,
|
consistency: result.driverInfo.consistency,
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export class DriversLeaderboardPresenter {
|
|||||||
name: item.driver.name.toString(),
|
name: item.driver.name.toString(),
|
||||||
rating: item.rating,
|
rating: item.rating,
|
||||||
skillLevel: item.skillLevel,
|
skillLevel: item.skillLevel,
|
||||||
|
...(item.driver.category !== undefined ? { category: item.driver.category } : {}),
|
||||||
nationality: item.driver.country.toString(),
|
nationality: item.driver.country.toString(),
|
||||||
racesCompleted: item.racesCompleted,
|
racesCompleted: item.racesCompleted,
|
||||||
wins: item.wins,
|
wins: item.wins,
|
||||||
|
|||||||
@@ -95,6 +95,11 @@ export class LeagueWithCapacityAndScoringDTO {
|
|||||||
@IsNumber()
|
@IsNumber()
|
||||||
usedSlots!: number;
|
usedSlots!: number;
|
||||||
|
|
||||||
|
@ApiProperty({ required: false, nullable: true })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
category?: string;
|
||||||
|
|
||||||
@ApiProperty({ required: false, nullable: true, type: LeagueCapacityAndScoringSocialLinksDTO })
|
@ApiProperty({ required: false, nullable: true, type: LeagueCapacityAndScoringSocialLinksDTO })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@ValidateNested()
|
@ValidateNested()
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export class AllLeaguesWithCapacityAndScoringPresenter
|
|||||||
: {}),
|
: {}),
|
||||||
},
|
},
|
||||||
usedSlots: summary.currentDrivers,
|
usedSlots: summary.currentDrivers,
|
||||||
|
...(summary.league.category ? { category: summary.league.category } : {}),
|
||||||
...mapSocialLinks(summary.league.socialLinks),
|
...mapSocialLinks(summary.league.socialLinks),
|
||||||
...(summary.scoringConfig && summary.game && summary.preset
|
...(summary.scoringConfig && summary.game && summary.preset
|
||||||
? {
|
? {
|
||||||
|
|||||||
@@ -91,6 +91,70 @@ function buildLeagueCoverSvg(leagueId: string): string {
|
|||||||
</svg>`;
|
</svg>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildDriverAvatarSvg(driverId: string): string {
|
||||||
|
const hue = hashToHue(driverId);
|
||||||
|
const initials = deriveLeagueLabel(driverId);
|
||||||
|
const bg = `hsl(${hue} 70% 38%)`;
|
||||||
|
const border = `hsl(${hue} 70% 28%)`;
|
||||||
|
|
||||||
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="96" height="96" viewBox="0 0 96 96" role="img" aria-label="Driver avatar">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
|
||||||
|
<stop offset="0%" stop-color="${bg}"/>
|
||||||
|
<stop offset="100%" stop-color="hsl(${hue} 80% 46%)"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<circle cx="48" cy="48" r="44" fill="url(#g)" stroke="${border}" stroke-width="3"/>
|
||||||
|
<text x="48" y="56" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial" font-size="32" font-weight="800" text-anchor="middle" fill="white">${initials}</text>
|
||||||
|
</svg>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildTrackImageSvg(trackId: string): string {
|
||||||
|
const hue = hashToHue(trackId);
|
||||||
|
const label = escapeXml(deriveLeagueLabel(trackId));
|
||||||
|
const bg1 = `hsl(${hue} 70% 28%)`;
|
||||||
|
const bg2 = `hsl(${(hue + 20) % 360} 65% 35%)`;
|
||||||
|
|
||||||
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="400" viewBox="0 0 1200 400" role="img" aria-label="Track image">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
|
||||||
|
<stop offset="0%" stop-color="${bg1}"/>
|
||||||
|
<stop offset="100%" stop-color="${bg2}"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<rect width="1200" height="400" fill="url(#bg)"/>
|
||||||
|
|
||||||
|
<!-- Track outline -->
|
||||||
|
<path d="M 200 200 Q 400 100 600 200 T 1000 200" fill="none" stroke="rgba(255,255,255,0.2)" stroke-width="8" stroke-linecap="round"/>
|
||||||
|
<path d="M 200 220 Q 400 120 600 220 T 1000 220" fill="none" stroke="rgba(255,255,255,0.1)" stroke-width="6" stroke-linecap="round"/>
|
||||||
|
|
||||||
|
<text x="64" y="110" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial" font-size="40" font-weight="800" fill="rgba(255,255,255,0.92)">Track ${label}</text>
|
||||||
|
<text x="64" y="165" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial" font-size="22" font-weight="600" fill="rgba(255,255,255,0.75)">${escapeXml(trackId)}</text>
|
||||||
|
</svg>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCategoryIconSvg(categoryId: string): string {
|
||||||
|
const hue = hashToHue(categoryId);
|
||||||
|
const label = escapeXml(categoryId.substring(0, 3).toUpperCase());
|
||||||
|
const bg = `hsl(${hue} 70% 38%)`;
|
||||||
|
const border = `hsl(${hue} 70% 28%)`;
|
||||||
|
|
||||||
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64" role="img" aria-label="Category icon">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
|
||||||
|
<stop offset="0%" stop-color="${bg}"/>
|
||||||
|
<stop offset="100%" stop-color="hsl(${hue} 80% 46%)"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<rect x="2" y="2" width="60" height="60" rx="12" fill="url(#g)" stroke="${border}" stroke-width="2"/>
|
||||||
|
<text x="32" y="40" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial" font-size="22" font-weight="800" text-anchor="middle" fill="white">${label}</text>
|
||||||
|
</svg>`;
|
||||||
|
}
|
||||||
|
|
||||||
@ApiTags('media')
|
@ApiTags('media')
|
||||||
@Controller('media')
|
@Controller('media')
|
||||||
export class MediaController {
|
export class MediaController {
|
||||||
@@ -159,6 +223,132 @@ export class MediaController {
|
|||||||
res.status(HttpStatus.OK).send(svg);
|
res.status(HttpStatus.OK).send(svg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Public()
|
||||||
|
@Get('teams/:teamId/logo')
|
||||||
|
@ApiOperation({ summary: 'Get team logo (placeholder)' })
|
||||||
|
@ApiParam({ name: 'teamId', description: 'Team ID' })
|
||||||
|
async getTeamLogo(
|
||||||
|
@Param('teamId') teamId: string,
|
||||||
|
@Res() res: Response,
|
||||||
|
): Promise<void> {
|
||||||
|
const svg = buildLeagueLogoSvg(teamId);
|
||||||
|
res.setHeader('Content-Type', 'image/svg+xml; charset=utf-8');
|
||||||
|
res.setHeader('Cache-Control', 'public, max-age=86400');
|
||||||
|
res.status(HttpStatus.OK).send(svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Public()
|
||||||
|
@Get('team/:teamId/logo')
|
||||||
|
@ApiOperation({ summary: 'Get team logo (singular path)' })
|
||||||
|
@ApiParam({ name: 'teamId', description: 'Team ID' })
|
||||||
|
async getTeamLogoSingular(
|
||||||
|
@Param('teamId') teamId: string,
|
||||||
|
@Res() res: Response,
|
||||||
|
): Promise<void> {
|
||||||
|
const svg = buildLeagueLogoSvg(teamId);
|
||||||
|
res.setHeader('Content-Type', 'image/svg+xml; charset=utf-8');
|
||||||
|
res.setHeader('Cache-Control', 'public, max-age=86400');
|
||||||
|
res.status(HttpStatus.OK).send(svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Public()
|
||||||
|
@Get('team/:teamId/logo.png')
|
||||||
|
@ApiOperation({ summary: 'Get team logo with .png extension' })
|
||||||
|
@ApiParam({ name: 'teamId', description: 'Team ID' })
|
||||||
|
async getTeamLogoPng(
|
||||||
|
@Param('teamId') teamId: string,
|
||||||
|
@Res() res: Response,
|
||||||
|
): Promise<void> {
|
||||||
|
const svg = buildLeagueLogoSvg(teamId);
|
||||||
|
res.setHeader('Content-Type', 'image/svg+xml; charset=utf-8');
|
||||||
|
res.setHeader('Cache-Control', 'public, max-age=86400');
|
||||||
|
res.status(HttpStatus.OK).send(svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Public()
|
||||||
|
@Get('teams/:teamId/cover')
|
||||||
|
@ApiOperation({ summary: 'Get team cover (placeholder)' })
|
||||||
|
@ApiParam({ name: 'teamId', description: 'Team ID' })
|
||||||
|
async getTeamCover(
|
||||||
|
@Param('teamId') teamId: string,
|
||||||
|
@Res() res: Response,
|
||||||
|
): Promise<void> {
|
||||||
|
const svg = buildLeagueCoverSvg(teamId);
|
||||||
|
res.setHeader('Content-Type', 'image/svg+xml; charset=utf-8');
|
||||||
|
res.setHeader('Cache-Control', 'public, max-age=86400');
|
||||||
|
res.status(HttpStatus.OK).send(svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Public()
|
||||||
|
@Get('drivers/:driverId/avatar')
|
||||||
|
@ApiOperation({ summary: 'Get driver avatar (placeholder)' })
|
||||||
|
@ApiParam({ name: 'driverId', description: 'Driver ID' })
|
||||||
|
async getDriverAvatar(
|
||||||
|
@Param('driverId') driverId: string,
|
||||||
|
@Res() res: Response,
|
||||||
|
): Promise<void> {
|
||||||
|
const svg = buildDriverAvatarSvg(driverId);
|
||||||
|
res.setHeader('Content-Type', 'image/svg+xml; charset=utf-8');
|
||||||
|
res.setHeader('Cache-Control', 'public, max-age=86400');
|
||||||
|
res.status(HttpStatus.OK).send(svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Public()
|
||||||
|
@Get('avatar/:driverId')
|
||||||
|
@ApiOperation({ summary: 'Get driver avatar (alternative path)' })
|
||||||
|
@ApiParam({ name: 'driverId', description: 'Driver ID' })
|
||||||
|
async getDriverAvatarAlt(
|
||||||
|
@Param('driverId') driverId: string,
|
||||||
|
@Res() res: Response,
|
||||||
|
): Promise<void> {
|
||||||
|
const svg = buildDriverAvatarSvg(driverId);
|
||||||
|
res.setHeader('Content-Type', 'image/svg+xml; charset=utf-8');
|
||||||
|
res.setHeader('Cache-Control', 'public, max-age=86400');
|
||||||
|
res.status(HttpStatus.OK).send(svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Public()
|
||||||
|
@Get('tracks/:trackId/image')
|
||||||
|
@ApiOperation({ summary: 'Get track image (placeholder)' })
|
||||||
|
@ApiParam({ name: 'trackId', description: 'Track ID' })
|
||||||
|
async getTrackImage(
|
||||||
|
@Param('trackId') trackId: string,
|
||||||
|
@Res() res: Response,
|
||||||
|
): Promise<void> {
|
||||||
|
const svg = buildTrackImageSvg(trackId);
|
||||||
|
res.setHeader('Content-Type', 'image/svg+xml; charset=utf-8');
|
||||||
|
res.setHeader('Cache-Control', 'public, max-age=86400');
|
||||||
|
res.status(HttpStatus.OK).send(svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Public()
|
||||||
|
@Get('categories/:categoryId/icon')
|
||||||
|
@ApiOperation({ summary: 'Get category icon (placeholder)' })
|
||||||
|
@ApiParam({ name: 'categoryId', description: 'Category ID' })
|
||||||
|
async getCategoryIcon(
|
||||||
|
@Param('categoryId') categoryId: string,
|
||||||
|
@Res() res: Response,
|
||||||
|
): Promise<void> {
|
||||||
|
const svg = buildCategoryIconSvg(categoryId);
|
||||||
|
res.setHeader('Content-Type', 'image/svg+xml; charset=utf-8');
|
||||||
|
res.setHeader('Cache-Control', 'public, max-age=86400');
|
||||||
|
res.status(HttpStatus.OK).send(svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Public()
|
||||||
|
@Get('sponsors/:sponsorId/logo')
|
||||||
|
@ApiOperation({ summary: 'Get sponsor logo (placeholder)' })
|
||||||
|
@ApiParam({ name: 'sponsorId', description: 'Sponsor ID' })
|
||||||
|
async getSponsorLogo(
|
||||||
|
@Param('sponsorId') sponsorId: string,
|
||||||
|
@Res() res: Response,
|
||||||
|
): Promise<void> {
|
||||||
|
const svg = buildLeagueLogoSvg(sponsorId);
|
||||||
|
res.setHeader('Content-Type', 'image/svg+xml; charset=utf-8');
|
||||||
|
res.setHeader('Cache-Control', 'public, max-age=86400');
|
||||||
|
res.status(HttpStatus.OK).send(svg);
|
||||||
|
}
|
||||||
|
|
||||||
@Public()
|
@Public()
|
||||||
@Get(':mediaId')
|
@Get(':mediaId')
|
||||||
@ApiOperation({ summary: 'Get media by ID' })
|
@ApiOperation({ summary: 'Get media by ID' })
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ describe('TeamController', () => {
|
|||||||
it('should return team details', async () => {
|
it('should return team details', async () => {
|
||||||
const teamId = 'team-123';
|
const teamId = 'team-123';
|
||||||
const userId = 'user-456';
|
const userId = 'user-456';
|
||||||
const result = { team: { id: teamId, name: 'Team', tag: 'TAG', description: 'Desc', ownerId: 'owner', leagues: [] }, membership: null, canManage: false };
|
const result = { team: { id: teamId, name: 'Team', tag: 'TAG', description: 'Desc', ownerId: 'owner', leagues: [], isRecruiting: false }, membership: null, canManage: false };
|
||||||
service.getDetails.mockResolvedValue(result);
|
service.getDetails.mockResolvedValue(result);
|
||||||
|
|
||||||
const mockReq = { user: { userId } } as any;
|
const mockReq = { user: { userId } } as any;
|
||||||
@@ -132,7 +132,7 @@ describe('TeamController', () => {
|
|||||||
describe('getDriverTeam', () => {
|
describe('getDriverTeam', () => {
|
||||||
it('should return driver team', async () => {
|
it('should return driver team', async () => {
|
||||||
const driverId = 'driver-123';
|
const driverId = 'driver-123';
|
||||||
const result = { team: { id: 'team-456', name: 'Team', tag: 'TAG', description: 'Desc', ownerId: 'owner', leagues: [] }, membership: { role: 'member' as const, joinedAt: '2023-01-01', isActive: true }, isOwner: false, canManage: false };
|
const result = { team: { id: 'team-456', name: 'Team', tag: 'TAG', description: 'Desc', ownerId: 'owner', leagues: [], isRecruiting: false }, membership: { role: 'member' as const, joinedAt: '2023-01-01', isActive: true }, isOwner: false, canManage: false };
|
||||||
service.getDriverTeam.mockResolvedValue(result);
|
service.getDriverTeam.mockResolvedValue(result);
|
||||||
|
|
||||||
const response = await controller.getDriverTeam(driverId);
|
const response = await controller.getDriverTeam(driverId);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Provider } from '@nestjs/common';
|
import { Provider } from '@nestjs/common';
|
||||||
|
|
||||||
import { IMAGE_SERVICE_TOKEN, LOGGER_TOKEN, TEAM_STATS_REPOSITORY_TOKEN, MEDIA_REPOSITORY_TOKEN } from './TeamTokens';
|
import { IMAGE_SERVICE_TOKEN, LOGGER_TOKEN, MEDIA_REPOSITORY_TOKEN } from './TeamTokens';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
TEAM_REPOSITORY_TOKEN,
|
TEAM_REPOSITORY_TOKEN,
|
||||||
@@ -8,18 +8,15 @@ export {
|
|||||||
DRIVER_REPOSITORY_TOKEN,
|
DRIVER_REPOSITORY_TOKEN,
|
||||||
IMAGE_SERVICE_TOKEN,
|
IMAGE_SERVICE_TOKEN,
|
||||||
LOGGER_TOKEN,
|
LOGGER_TOKEN,
|
||||||
TEAM_STATS_REPOSITORY_TOKEN,
|
|
||||||
MEDIA_REPOSITORY_TOKEN,
|
MEDIA_REPOSITORY_TOKEN,
|
||||||
} from './TeamTokens';
|
} from './TeamTokens';
|
||||||
|
|
||||||
// Import core interfaces
|
// Import core interfaces
|
||||||
import type { Logger } from '@core/shared/application/Logger';
|
import type { Logger } from '@core/shared/application/Logger';
|
||||||
import type { ITeamStatsRepository } from '@core/racing/domain/repositories/ITeamStatsRepository';
|
|
||||||
|
|
||||||
// Import concrete in-memory implementations
|
// Import concrete in-memory implementations
|
||||||
import { InMemoryImageServiceAdapter } from '@adapters/media/ports/InMemoryImageServiceAdapter';
|
import { InMemoryImageServiceAdapter } from '@adapters/media/ports/InMemoryImageServiceAdapter';
|
||||||
import { ConsoleLogger } from '@adapters/logging/ConsoleLogger';
|
import { ConsoleLogger } from '@adapters/logging/ConsoleLogger';
|
||||||
import { InMemoryTeamStatsRepository } from '@adapters/racing/persistence/inmemory/InMemoryTeamStatsRepository';
|
|
||||||
import { InMemoryMediaRepository } from '@adapters/racing/persistence/media/InMemoryMediaRepository';
|
import { InMemoryMediaRepository } from '@adapters/racing/persistence/media/InMemoryMediaRepository';
|
||||||
|
|
||||||
// Import presenters
|
// Import presenters
|
||||||
@@ -35,11 +32,6 @@ export const TeamProviders: Provider[] = [
|
|||||||
provide: LOGGER_TOKEN,
|
provide: LOGGER_TOKEN,
|
||||||
useClass: ConsoleLogger,
|
useClass: ConsoleLogger,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
provide: TEAM_STATS_REPOSITORY_TOKEN,
|
|
||||||
useFactory: (logger: Logger) => new InMemoryTeamStatsRepository(logger),
|
|
||||||
inject: [LOGGER_TOKEN],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
provide: MEDIA_REPOSITORY_TOKEN,
|
provide: MEDIA_REPOSITORY_TOKEN,
|
||||||
useFactory: (logger: Logger) => new InMemoryMediaRepository(logger),
|
useFactory: (logger: Logger) => new InMemoryMediaRepository(logger),
|
||||||
@@ -47,7 +39,6 @@ export const TeamProviders: Provider[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: AllTeamsPresenter,
|
provide: AllTeamsPresenter,
|
||||||
useFactory: (teamStatsRepository: ITeamStatsRepository) => new AllTeamsPresenter(teamStatsRepository),
|
useFactory: () => new AllTeamsPresenter(),
|
||||||
inject: [TEAM_STATS_REPOSITORY_TOKEN],
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -113,7 +113,6 @@ describe('TeamService', () => {
|
|||||||
|
|
||||||
const teamStatsRepository = {
|
const teamStatsRepository = {
|
||||||
getTeamStats: vi.fn(),
|
getTeamStats: vi.fn(),
|
||||||
getTeamStatsSync: vi.fn(),
|
|
||||||
saveTeamStats: vi.fn(),
|
saveTeamStats: vi.fn(),
|
||||||
getAllStats: vi.fn(),
|
getAllStats: vi.fn(),
|
||||||
clear: vi.fn(),
|
clear: vi.fn(),
|
||||||
@@ -126,6 +125,10 @@ describe('TeamService', () => {
|
|||||||
saveDriverAvatar: vi.fn(),
|
saveDriverAvatar: vi.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const resultRepository = {
|
||||||
|
findAll: vi.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
const allTeamsPresenter = {
|
const allTeamsPresenter = {
|
||||||
reset: vi.fn(),
|
reset: vi.fn(),
|
||||||
present: vi.fn(),
|
present: vi.fn(),
|
||||||
@@ -140,6 +143,7 @@ describe('TeamService', () => {
|
|||||||
logger,
|
logger,
|
||||||
teamStatsRepository as unknown as never,
|
teamStatsRepository as unknown as never,
|
||||||
mediaRepository as unknown as never,
|
mediaRepository as unknown as never,
|
||||||
|
resultRepository as unknown as never,
|
||||||
allTeamsPresenter as unknown as never
|
allTeamsPresenter as unknown as never
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -37,9 +37,10 @@ import { CreateTeamPresenter } from './presenters/CreateTeamPresenter';
|
|||||||
import { UpdateTeamPresenter } from './presenters/UpdateTeamPresenter';
|
import { UpdateTeamPresenter } from './presenters/UpdateTeamPresenter';
|
||||||
|
|
||||||
// Tokens
|
// Tokens
|
||||||
import { TEAM_REPOSITORY_TOKEN, TEAM_MEMBERSHIP_REPOSITORY_TOKEN, DRIVER_REPOSITORY_TOKEN, LOGGER_TOKEN, TEAM_STATS_REPOSITORY_TOKEN, MEDIA_REPOSITORY_TOKEN } from './TeamTokens';
|
import { TEAM_REPOSITORY_TOKEN, TEAM_MEMBERSHIP_REPOSITORY_TOKEN, DRIVER_REPOSITORY_TOKEN, LOGGER_TOKEN, TEAM_STATS_REPOSITORY_TOKEN, MEDIA_REPOSITORY_TOKEN, RESULT_REPOSITORY_TOKEN } from './TeamTokens';
|
||||||
import type { ITeamStatsRepository } from '@core/racing/domain/repositories/ITeamStatsRepository';
|
import type { ITeamStatsRepository } from '@core/racing/domain/repositories/ITeamStatsRepository';
|
||||||
import type { IMediaRepository } from '@core/racing/domain/repositories/IMediaRepository';
|
import type { IMediaRepository } from '@core/racing/domain/repositories/IMediaRepository';
|
||||||
|
import type { IResultRepository } from '@core/racing/domain/repositories/IResultRepository';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TeamService {
|
export class TeamService {
|
||||||
@@ -50,6 +51,7 @@ export class TeamService {
|
|||||||
@Inject(LOGGER_TOKEN) private readonly logger: Logger,
|
@Inject(LOGGER_TOKEN) private readonly logger: Logger,
|
||||||
@Inject(TEAM_STATS_REPOSITORY_TOKEN) private readonly teamStatsRepository: ITeamStatsRepository,
|
@Inject(TEAM_STATS_REPOSITORY_TOKEN) private readonly teamStatsRepository: ITeamStatsRepository,
|
||||||
@Inject(MEDIA_REPOSITORY_TOKEN) private readonly mediaRepository: IMediaRepository,
|
@Inject(MEDIA_REPOSITORY_TOKEN) private readonly mediaRepository: IMediaRepository,
|
||||||
|
@Inject(RESULT_REPOSITORY_TOKEN) private readonly resultRepository: IResultRepository,
|
||||||
private readonly allTeamsPresenter: AllTeamsPresenter,
|
private readonly allTeamsPresenter: AllTeamsPresenter,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@@ -61,6 +63,7 @@ export class TeamService {
|
|||||||
this.membershipRepository,
|
this.membershipRepository,
|
||||||
this.teamStatsRepository,
|
this.teamStatsRepository,
|
||||||
this.mediaRepository,
|
this.mediaRepository,
|
||||||
|
this.resultRepository,
|
||||||
this.logger,
|
this.logger,
|
||||||
this.allTeamsPresenter
|
this.allTeamsPresenter
|
||||||
);
|
);
|
||||||
@@ -174,13 +177,13 @@ export class TeamService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getDriverTeam(driverId: string): Promise<GetDriverTeamOutputDTO | null> {
|
async getDriverTeam(driverId: string): Promise<GetDriverTeamOutputDTO | null> {
|
||||||
this.logger.debug(`[TeamService] Fetching driver team for driverId: ${driverId}`);
|
this.logger.debug(`[TeamService] Fetching team for driverId: ${driverId}`);
|
||||||
|
|
||||||
const presenter = new DriverTeamPresenter();
|
const presenter = new DriverTeamPresenter();
|
||||||
const useCase = new GetDriverTeamUseCase(this.teamRepository, this.membershipRepository, this.logger, presenter);
|
const useCase = new GetDriverTeamUseCase(this.teamRepository, this.membershipRepository, this.logger, presenter);
|
||||||
const result = await useCase.execute({ driverId });
|
const result = await useCase.execute({ driverId });
|
||||||
if (result.isErr()) {
|
if (result.isErr()) {
|
||||||
this.logger.error(`Error fetching driver team for driverId: ${driverId}: ${result.error?.details?.message || 'Unknown error'}`);
|
this.logger.error(`Error fetching team for driverId: ${driverId}: ${result.error?.details?.message || 'Unknown error'}`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,3 +5,4 @@ export const IMAGE_SERVICE_TOKEN = 'IImageServicePort';
|
|||||||
export const LOGGER_TOKEN = 'Logger';
|
export const LOGGER_TOKEN = 'Logger';
|
||||||
export const TEAM_STATS_REPOSITORY_TOKEN = 'ITeamStatsRepository';
|
export const TEAM_STATS_REPOSITORY_TOKEN = 'ITeamStatsRepository';
|
||||||
export const MEDIA_REPOSITORY_TOKEN = 'IMediaRepository';
|
export const MEDIA_REPOSITORY_TOKEN = 'IMediaRepository';
|
||||||
|
export const RESULT_REPOSITORY_TOKEN = 'IResultRepository';
|
||||||
@@ -326,6 +326,15 @@ export class TeamDTO {
|
|||||||
@IsArray()
|
@IsArray()
|
||||||
leagues!: string[];
|
leagues!: string[];
|
||||||
|
|
||||||
|
@ApiProperty({ required: false })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
category?: string | undefined;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
@IsBoolean()
|
||||||
|
isRecruiting!: boolean;
|
||||||
|
|
||||||
@ApiProperty({ required: false })
|
@ApiProperty({ required: false })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
|
|||||||
@@ -37,10 +37,16 @@ export class TeamListItemDTO {
|
|||||||
@ApiProperty({ required: false, enum: ['beginner', 'intermediate', 'advanced', 'pro'] })
|
@ApiProperty({ required: false, enum: ['beginner', 'intermediate', 'advanced', 'pro'] })
|
||||||
performanceLevel?: 'beginner' | 'intermediate' | 'advanced' | 'pro';
|
performanceLevel?: 'beginner' | 'intermediate' | 'advanced' | 'pro';
|
||||||
|
|
||||||
|
@ApiProperty({ required: false })
|
||||||
|
category?: string | undefined;
|
||||||
|
|
||||||
@ApiProperty({ required: false })
|
@ApiProperty({ required: false })
|
||||||
logoUrl?: string;
|
logoUrl?: string;
|
||||||
|
|
||||||
@ApiProperty({ required: false })
|
@ApiProperty({ required: false })
|
||||||
rating?: number;
|
rating?: number;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
isRecruiting!: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,53 +1,40 @@
|
|||||||
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
|
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
|
||||||
import type { GetAllTeamsResult } from '@core/racing/application/use-cases/GetAllTeamsUseCase';
|
import type { GetAllTeamsResult } from '@core/racing/application/use-cases/GetAllTeamsUseCase';
|
||||||
import { GetAllTeamsOutputDTO } from '../dtos/GetAllTeamsOutputDTO';
|
import { GetAllTeamsOutputDTO } from '../dtos/GetAllTeamsOutputDTO';
|
||||||
import type { ITeamStatsRepository } from '@core/racing/domain/repositories/ITeamStatsRepository';
|
import { TeamListItemDTO } from '../dtos/TeamListItemDTO';
|
||||||
|
|
||||||
export class AllTeamsPresenter implements UseCaseOutputPort<GetAllTeamsResult> {
|
export class AllTeamsPresenter implements UseCaseOutputPort<GetAllTeamsResult> {
|
||||||
private model: GetAllTeamsOutputDTO | null = null;
|
private model: GetAllTeamsOutputDTO | null = null;
|
||||||
|
|
||||||
constructor(
|
|
||||||
private readonly teamStatsRepository: ITeamStatsRepository
|
|
||||||
) {}
|
|
||||||
|
|
||||||
reset(): void {
|
reset(): void {
|
||||||
this.model = null;
|
this.model = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
present(result: GetAllTeamsResult): void {
|
present(result: GetAllTeamsResult): void {
|
||||||
this.model = {
|
const teams: TeamListItemDTO[] = result.teams.map(team => {
|
||||||
teams: result.teams.map(team => {
|
const dto = new TeamListItemDTO();
|
||||||
const stats = this.teamStatsRepository.getTeamStatsSync(team.id.toString());
|
dto.id = team.id;
|
||||||
|
dto.name = team.name;
|
||||||
|
dto.tag = team.tag;
|
||||||
|
dto.description = team.description || '';
|
||||||
|
dto.memberCount = team.memberCount;
|
||||||
|
dto.leagues = team.leagues || [];
|
||||||
|
dto.totalWins = team.totalWins ?? 0;
|
||||||
|
dto.totalRaces = team.totalRaces ?? 0;
|
||||||
|
dto.performanceLevel = (team.performanceLevel as 'beginner' | 'intermediate' | 'advanced' | 'pro') ?? 'intermediate';
|
||||||
|
dto.specialization = (team.specialization as 'endurance' | 'sprint' | 'mixed') ?? 'mixed';
|
||||||
|
dto.region = team.region ?? '';
|
||||||
|
dto.languages = team.languages ?? [];
|
||||||
|
// Return relative URL for proxying through Next.js rewrites
|
||||||
|
dto.logoUrl = `/api/media/teams/${team.id}/logo`;
|
||||||
|
dto.rating = team.rating ?? 0;
|
||||||
|
dto.category = team.category;
|
||||||
|
dto.isRecruiting = team.isRecruiting;
|
||||||
|
return dto;
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
this.model = {
|
||||||
id: team.id,
|
teams,
|
||||||
name: team.name.toString(),
|
|
||||||
tag: team.tag.toString(),
|
|
||||||
description: team.description?.toString() || '',
|
|
||||||
memberCount: team.memberCount,
|
|
||||||
leagues: team.leagues?.map(l => l.toString()) || [],
|
|
||||||
// Add stats fields
|
|
||||||
...(stats ? {
|
|
||||||
totalWins: stats.totalWins,
|
|
||||||
totalRaces: stats.totalRaces,
|
|
||||||
performanceLevel: stats.performanceLevel,
|
|
||||||
specialization: stats.specialization,
|
|
||||||
region: stats.region,
|
|
||||||
languages: stats.languages,
|
|
||||||
logoUrl: stats.logoUrl,
|
|
||||||
rating: stats.rating,
|
|
||||||
} : {
|
|
||||||
totalWins: 0,
|
|
||||||
totalRaces: 0,
|
|
||||||
performanceLevel: 'beginner',
|
|
||||||
specialization: 'mixed',
|
|
||||||
region: '',
|
|
||||||
languages: [],
|
|
||||||
logoUrl: '',
|
|
||||||
rating: 0,
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
totalCount: result.totalCount ?? result.teams.length,
|
totalCount: result.totalCount ?? result.teams.length,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export class DriverTeamPresenter implements UseCaseOutputPort<GetDriverTeamResul
|
|||||||
description: result.team.description?.toString() || '',
|
description: result.team.description?.toString() || '',
|
||||||
ownerId: result.team.ownerId.toString(),
|
ownerId: result.team.ownerId.toString(),
|
||||||
leagues: result.team.leagues?.map(l => l.toString()) || [],
|
leagues: result.team.leagues?.map(l => l.toString()) || [],
|
||||||
|
isRecruiting: result.team.isRecruiting,
|
||||||
createdAt: result.team.createdAt.toDate().toISOString(),
|
createdAt: result.team.createdAt.toDate().toISOString(),
|
||||||
},
|
},
|
||||||
membership: {
|
membership: {
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ export class TeamDetailsPresenter implements UseCaseOutputPort<GetTeamDetailsRes
|
|||||||
description: result.team.description?.toString() || '',
|
description: result.team.description?.toString() || '',
|
||||||
ownerId: result.team.ownerId.toString(),
|
ownerId: result.team.ownerId.toString(),
|
||||||
leagues: result.team.leagues?.map(l => l.toString()) || [],
|
leagues: result.team.leagues?.map(l => l.toString()) || [],
|
||||||
|
category: result.team.category,
|
||||||
|
isRecruiting: result.team.isRecruiting,
|
||||||
createdAt: result.team.createdAt.toDate().toISOString(),
|
createdAt: result.team.createdAt.toDate().toISOString(),
|
||||||
},
|
},
|
||||||
membership: result.membership
|
membership: result.membership
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ import {
|
|||||||
TeamMembershipOrmEntity,
|
TeamMembershipOrmEntity,
|
||||||
TeamOrmEntity,
|
TeamOrmEntity,
|
||||||
} from '@adapters/racing/persistence/typeorm/entities/TeamOrmEntities';
|
} from '@adapters/racing/persistence/typeorm/entities/TeamOrmEntities';
|
||||||
|
import { TeamStatsOrmEntity } from '@adapters/racing/persistence/typeorm/entities/TeamStatsOrmEntity';
|
||||||
|
|
||||||
import { TypeOrmDriverRepository } from '@adapters/racing/persistence/typeorm/repositories/TypeOrmDriverRepository';
|
import { TypeOrmDriverRepository } from '@adapters/racing/persistence/typeorm/repositories/TypeOrmDriverRepository';
|
||||||
import { TypeOrmLeagueMembershipRepository } from '@adapters/racing/persistence/typeorm/repositories/TypeOrmLeagueMembershipRepository';
|
import { TypeOrmLeagueMembershipRepository } from '@adapters/racing/persistence/typeorm/repositories/TypeOrmLeagueMembershipRepository';
|
||||||
@@ -79,9 +80,11 @@ import { TypeOrmTeamMembershipRepository, TypeOrmTeamRepository } from '@adapter
|
|||||||
|
|
||||||
// Import in-memory implementations for new repositories (TypeORM versions not yet implemented)
|
// Import in-memory implementations for new repositories (TypeORM versions not yet implemented)
|
||||||
import { InMemoryDriverStatsRepository } from '@adapters/racing/persistence/inmemory/InMemoryDriverStatsRepository';
|
import { InMemoryDriverStatsRepository } from '@adapters/racing/persistence/inmemory/InMemoryDriverStatsRepository';
|
||||||
import { InMemoryTeamStatsRepository } from '@adapters/racing/persistence/inmemory/InMemoryTeamStatsRepository';
|
|
||||||
import { InMemoryMediaRepository } from '@adapters/racing/persistence/media/InMemoryMediaRepository';
|
import { InMemoryMediaRepository } from '@adapters/racing/persistence/media/InMemoryMediaRepository';
|
||||||
|
|
||||||
|
// Import TypeORM repository for team stats
|
||||||
|
import { TypeOrmTeamStatsRepository } from '@adapters/racing/persistence/typeorm/repositories/TypeOrmTeamStatsRepository';
|
||||||
|
|
||||||
import { DriverOrmMapper } from '@adapters/racing/persistence/typeorm/mappers/DriverOrmMapper';
|
import { DriverOrmMapper } from '@adapters/racing/persistence/typeorm/mappers/DriverOrmMapper';
|
||||||
import { LeagueMembershipOrmMapper } from '@adapters/racing/persistence/typeorm/mappers/LeagueMembershipOrmMapper';
|
import { LeagueMembershipOrmMapper } from '@adapters/racing/persistence/typeorm/mappers/LeagueMembershipOrmMapper';
|
||||||
import { LeagueOrmMapper } from '@adapters/racing/persistence/typeorm/mappers/LeagueOrmMapper';
|
import { LeagueOrmMapper } from '@adapters/racing/persistence/typeorm/mappers/LeagueOrmMapper';
|
||||||
@@ -105,6 +108,7 @@ import {
|
|||||||
import { MoneyOrmMapper } from '@adapters/racing/persistence/typeorm/mappers/MoneyOrmMapper';
|
import { MoneyOrmMapper } from '@adapters/racing/persistence/typeorm/mappers/MoneyOrmMapper';
|
||||||
import { PenaltyOrmMapper, ProtestOrmMapper } from '@adapters/racing/persistence/typeorm/mappers/StewardingOrmMappers';
|
import { PenaltyOrmMapper, ProtestOrmMapper } from '@adapters/racing/persistence/typeorm/mappers/StewardingOrmMappers';
|
||||||
import { TeamMembershipOrmMapper, TeamOrmMapper } from '@adapters/racing/persistence/typeorm/mappers/TeamOrmMappers';
|
import { TeamMembershipOrmMapper, TeamOrmMapper } from '@adapters/racing/persistence/typeorm/mappers/TeamOrmMappers';
|
||||||
|
import { TeamStatsOrmMapper } from '@adapters/racing/persistence/typeorm/mappers/TeamStatsOrmMapper';
|
||||||
|
|
||||||
import { getPointsSystems } from '@adapters/bootstrap/PointsSystems';
|
import { getPointsSystems } from '@adapters/bootstrap/PointsSystems';
|
||||||
import type { Logger } from '@core/shared/application/Logger';
|
import type { Logger } from '@core/shared/application/Logger';
|
||||||
@@ -126,6 +130,7 @@ const typeOrmFeatureImports = [
|
|||||||
TeamOrmEntity,
|
TeamOrmEntity,
|
||||||
TeamMembershipOrmEntity,
|
TeamMembershipOrmEntity,
|
||||||
TeamJoinRequestOrmEntity,
|
TeamJoinRequestOrmEntity,
|
||||||
|
TeamStatsOrmEntity,
|
||||||
|
|
||||||
PenaltyOrmEntity,
|
PenaltyOrmEntity,
|
||||||
ProtestOrmEntity,
|
ProtestOrmEntity,
|
||||||
@@ -155,6 +160,7 @@ const typeOrmFeatureImports = [
|
|||||||
|
|
||||||
{ provide: TeamOrmMapper, useFactory: () => new TeamOrmMapper() },
|
{ provide: TeamOrmMapper, useFactory: () => new TeamOrmMapper() },
|
||||||
{ provide: TeamMembershipOrmMapper, useFactory: () => new TeamMembershipOrmMapper() },
|
{ provide: TeamMembershipOrmMapper, useFactory: () => new TeamMembershipOrmMapper() },
|
||||||
|
{ provide: TeamStatsOrmMapper, useFactory: () => new TeamStatsOrmMapper() },
|
||||||
|
|
||||||
{ provide: PenaltyOrmMapper, useFactory: () => new PenaltyOrmMapper() },
|
{ provide: PenaltyOrmMapper, useFactory: () => new PenaltyOrmMapper() },
|
||||||
{ provide: ProtestOrmMapper, useFactory: () => new ProtestOrmMapper() },
|
{ provide: ProtestOrmMapper, useFactory: () => new ProtestOrmMapper() },
|
||||||
@@ -321,8 +327,8 @@ const typeOrmFeatureImports = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: TEAM_STATS_REPOSITORY_TOKEN,
|
provide: TEAM_STATS_REPOSITORY_TOKEN,
|
||||||
useFactory: (logger: Logger) => new InMemoryTeamStatsRepository(logger),
|
useFactory: (repo: Repository<TeamStatsOrmEntity>, mapper: TeamStatsOrmMapper) => new TypeOrmTeamStatsRepository(repo, mapper),
|
||||||
inject: ['Logger'],
|
inject: [getRepositoryToken(TeamStatsOrmEntity), TeamStatsOrmMapper],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: MEDIA_REPOSITORY_TOKEN,
|
provide: MEDIA_REPOSITORY_TOKEN,
|
||||||
|
|||||||
@@ -52,6 +52,25 @@ const SKILL_LEVELS: {
|
|||||||
{ id: 'beginner', label: 'Beginner', icon: Shield, color: 'text-green-400', bgColor: 'bg-green-400/10', borderColor: 'border-green-400/30', description: 'Learning the ropes' },
|
{ id: 'beginner', label: 'Beginner', icon: Shield, color: 'text-green-400', bgColor: 'bg-green-400/10', borderColor: 'border-green-400/30', description: 'Learning the ropes' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// CATEGORY CONFIG
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
const CATEGORIES: {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
color: string;
|
||||||
|
bgColor: string;
|
||||||
|
borderColor: string;
|
||||||
|
}[] = [
|
||||||
|
{ id: 'beginner', label: 'Beginner', color: 'text-green-400', bgColor: 'bg-green-400/10', borderColor: 'border-green-400/30' },
|
||||||
|
{ id: 'intermediate', label: 'Intermediate', color: 'text-primary-blue', bgColor: 'bg-primary-blue/10', borderColor: 'border-primary-blue/30' },
|
||||||
|
{ id: 'advanced', label: 'Advanced', color: 'text-purple-400', bgColor: 'bg-purple-400/10', borderColor: 'border-purple-400/30' },
|
||||||
|
{ id: 'pro', label: 'Pro', color: 'text-yellow-400', bgColor: 'bg-yellow-400/10', borderColor: 'border-yellow-400/30' },
|
||||||
|
{ id: 'endurance', label: 'Endurance', color: 'text-orange-400', bgColor: 'bg-orange-400/10', borderColor: 'border-orange-400/30' },
|
||||||
|
{ id: 'sprint', label: 'Sprint', color: 'text-red-400', bgColor: 'bg-red-400/10', borderColor: 'border-red-400/30' },
|
||||||
|
];
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// FEATURED DRIVER CARD COMPONENT
|
// FEATURED DRIVER CARD COMPONENT
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -64,6 +83,7 @@ interface FeaturedDriverCardProps {
|
|||||||
|
|
||||||
function FeaturedDriverCard({ driver, position, onClick }: FeaturedDriverCardProps) {
|
function FeaturedDriverCard({ driver, position, onClick }: FeaturedDriverCardProps) {
|
||||||
const levelConfig = SKILL_LEVELS.find((l) => l.id === driver.skillLevel);
|
const levelConfig = SKILL_LEVELS.find((l) => l.id === driver.skillLevel);
|
||||||
|
const categoryConfig = CATEGORIES.find((c) => c.id === driver.category);
|
||||||
|
|
||||||
const getBorderColor = (pos: number) => {
|
const getBorderColor = (pos: number) => {
|
||||||
switch (pos) {
|
switch (pos) {
|
||||||
@@ -98,9 +118,16 @@ function FeaturedDriverCard({ driver, position, onClick }: FeaturedDriverCardPro
|
|||||||
<span className="text-lg font-bold text-gray-400">#{position}</span>
|
<span className="text-lg font-bold text-gray-400">#{position}</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<span className={`px-2 py-1 rounded-full text-[10px] font-medium ${levelConfig?.bgColor} ${levelConfig?.color} border ${levelConfig?.borderColor}`}>
|
<div className="flex gap-2">
|
||||||
{levelConfig?.label}
|
{categoryConfig && (
|
||||||
</span>
|
<span className={`px-2 py-1 rounded-full text-[10px] font-medium ${categoryConfig.bgColor} ${categoryConfig.color} border ${categoryConfig.borderColor}`}>
|
||||||
|
{categoryConfig.label}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span className={`px-2 py-1 rounded-full text-[10px] font-medium ${levelConfig?.bgColor} ${levelConfig?.color} border ${levelConfig?.borderColor}`}>
|
||||||
|
{levelConfig?.label}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Avatar & Name */}
|
{/* Avatar & Name */}
|
||||||
@@ -200,6 +227,66 @@ function SkillDistribution({ drivers }: SkillDistributionProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// CATEGORY DISTRIBUTION COMPONENT
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
interface CategoryDistributionProps {
|
||||||
|
drivers: DriverLeaderboardItemViewModel[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function CategoryDistribution({ drivers }: CategoryDistributionProps) {
|
||||||
|
const distribution = CATEGORIES.map((category) => ({
|
||||||
|
...category,
|
||||||
|
count: drivers.filter((d) => d.category === category.id).length,
|
||||||
|
percentage: drivers.length > 0
|
||||||
|
? Math.round((drivers.filter((d) => d.category === category.id).length / drivers.length) * 100)
|
||||||
|
: 0,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mb-10">
|
||||||
|
<div className="flex items-center gap-3 mb-4">
|
||||||
|
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-purple-400/10 border border-purple-400/20">
|
||||||
|
<BarChart3 className="w-5 h-5 text-purple-400" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 className="text-lg font-semibold text-white">Category Distribution</h2>
|
||||||
|
<p className="text-xs text-gray-500">Driver population by category</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
{distribution.map((category) => (
|
||||||
|
<div
|
||||||
|
key={category.id}
|
||||||
|
className={`p-4 rounded-xl ${category.bgColor} border ${category.borderColor}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
<span className={`text-2xl font-bold ${category.color}`}>{category.count}</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-white font-medium mb-1">{category.label}</p>
|
||||||
|
<div className="w-full h-2 rounded-full bg-deep-graphite/50 overflow-hidden">
|
||||||
|
<div
|
||||||
|
className={`h-full rounded-full transition-all duration-500 ${
|
||||||
|
category.id === 'beginner' ? 'bg-green-400' :
|
||||||
|
category.id === 'intermediate' ? 'bg-primary-blue' :
|
||||||
|
category.id === 'advanced' ? 'bg-purple-400' :
|
||||||
|
category.id === 'pro' ? 'bg-yellow-400' :
|
||||||
|
category.id === 'endurance' ? 'bg-orange-400' :
|
||||||
|
'bg-red-400'
|
||||||
|
}`}
|
||||||
|
style={{ width: `${category.percentage}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-gray-500 mt-1">{category.percentage}% of drivers</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// LEADERBOARD PREVIEW COMPONENT
|
// LEADERBOARD PREVIEW COMPONENT
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -258,6 +345,7 @@ function LeaderboardPreview({ drivers, onDriverClick }: LeaderboardPreviewProps)
|
|||||||
<div className="divide-y divide-charcoal-outline/50">
|
<div className="divide-y divide-charcoal-outline/50">
|
||||||
{top5.map((driver, index) => {
|
{top5.map((driver, index) => {
|
||||||
const levelConfig = SKILL_LEVELS.find((l) => l.id === driver.skillLevel);
|
const levelConfig = SKILL_LEVELS.find((l) => l.id === driver.skillLevel);
|
||||||
|
const categoryConfig = CATEGORIES.find((c) => c.id === driver.category);
|
||||||
const position = index + 1;
|
const position = index + 1;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -285,6 +373,9 @@ function LeaderboardPreview({ drivers, onDriverClick }: LeaderboardPreviewProps)
|
|||||||
<div className="flex items-center gap-2 text-xs text-gray-500">
|
<div className="flex items-center gap-2 text-xs text-gray-500">
|
||||||
<Flag className="w-3 h-3" />
|
<Flag className="w-3 h-3" />
|
||||||
{driver.nationality}
|
{driver.nationality}
|
||||||
|
{categoryConfig && (
|
||||||
|
<span className={categoryConfig.color}>{categoryConfig.label}</span>
|
||||||
|
)}
|
||||||
<span className={levelConfig?.color}>{levelConfig?.label}</span>
|
<span className={levelConfig?.color}>{levelConfig?.label}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -336,6 +427,7 @@ function RecentActivity({ drivers, onDriverClick }: RecentActivityProps) {
|
|||||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-3">
|
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-3">
|
||||||
{activeDrivers.map((driver) => {
|
{activeDrivers.map((driver) => {
|
||||||
const levelConfig = SKILL_LEVELS.find((l) => l.id === driver.skillLevel);
|
const levelConfig = SKILL_LEVELS.find((l) => l.id === driver.skillLevel);
|
||||||
|
const categoryConfig = CATEGORIES.find((c) => c.id === driver.category);
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={driver.id}
|
key={driver.id}
|
||||||
@@ -350,7 +442,12 @@ function RecentActivity({ drivers, onDriverClick }: RecentActivityProps) {
|
|||||||
<p className="text-sm font-medium text-white truncate group-hover:text-performance-green transition-colors">
|
<p className="text-sm font-medium text-white truncate group-hover:text-performance-green transition-colors">
|
||||||
{driver.name}
|
{driver.name}
|
||||||
</p>
|
</p>
|
||||||
<p className={`text-xs ${levelConfig?.color}`}>{levelConfig?.label}</p>
|
<div className="flex items-center justify-center gap-1 text-xs">
|
||||||
|
{categoryConfig && (
|
||||||
|
<span className={categoryConfig.color}>{categoryConfig.label}</span>
|
||||||
|
)}
|
||||||
|
<span className={levelConfig?.color}>{levelConfig?.label}</span>
|
||||||
|
</div>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -516,6 +613,9 @@ export default function DriversPage() {
|
|||||||
{/* Skill Distribution */}
|
{/* Skill Distribution */}
|
||||||
{!searchQuery && <SkillDistribution drivers={drivers} />}
|
{!searchQuery && <SkillDistribution drivers={drivers} />}
|
||||||
|
|
||||||
|
{/* Category Distribution */}
|
||||||
|
{!searchQuery && <CategoryDistribution drivers={drivers} />}
|
||||||
|
|
||||||
{/* Leaderboard Preview */}
|
{/* Leaderboard Preview */}
|
||||||
<LeaderboardPreview drivers={filteredDrivers} onDriverClick={handleDriverClick} />
|
<LeaderboardPreview drivers={filteredDrivers} onDriverClick={handleDriverClick} />
|
||||||
|
|
||||||
|
|||||||
@@ -422,7 +422,15 @@ export default function LeaguesPage() {
|
|||||||
// Group leagues by category for slider view
|
// Group leagues by category for slider view
|
||||||
const leaguesByCategory = CATEGORIES.reduce(
|
const leaguesByCategory = CATEGORIES.reduce(
|
||||||
(acc, category) => {
|
(acc, category) => {
|
||||||
acc[category.id] = searchFilteredLeagues.filter(category.filter);
|
// First try to use the dedicated category field, fall back to scoring-based filtering
|
||||||
|
acc[category.id] = searchFilteredLeagues.filter((league) => {
|
||||||
|
// If league has a category field, use it directly
|
||||||
|
if (league.category) {
|
||||||
|
return league.category === category.id;
|
||||||
|
}
|
||||||
|
// Otherwise fall back to the existing scoring-based filter
|
||||||
|
return category.filter(league);
|
||||||
|
});
|
||||||
return acc;
|
return acc;
|
||||||
},
|
},
|
||||||
{} as Record<CategoryId, LeagueSummaryViewModel[]>,
|
{} as Record<CategoryId, LeagueSummaryViewModel[]>,
|
||||||
|
|||||||
@@ -213,6 +213,12 @@ export default function TeamDetailPage() {
|
|||||||
|
|
||||||
<div className="flex items-center gap-4 text-sm text-gray-400">
|
<div className="flex items-center gap-4 text-sm text-gray-400">
|
||||||
<span>{memberships.length} {memberships.length === 1 ? 'member' : 'members'}</span>
|
<span>{memberships.length} {memberships.length === 1 ? 'member' : 'members'}</span>
|
||||||
|
{team.category && (
|
||||||
|
<span className="flex items-center gap-1 text-purple-400">
|
||||||
|
<span className="w-2 h-2 rounded-full bg-purple-400"></span>
|
||||||
|
{team.category}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
{team.createdAt && (
|
{team.createdAt && (
|
||||||
<span>
|
<span>
|
||||||
Founded {new Date(team.createdAt).toLocaleDateString('en-US', { month: 'short', year: 'numeric' })}
|
Founded {new Date(team.createdAt).toLocaleDateString('en-US', { month: 'short', year: 'numeric' })}
|
||||||
@@ -267,6 +273,9 @@ export default function TeamDetailPage() {
|
|||||||
<h3 className="text-xl font-semibold text-white mb-4">Quick Stats</h3>
|
<h3 className="text-xl font-semibold text-white mb-4">Quick Stats</h3>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<StatItem label="Members" value={memberships.length.toString()} color="text-primary-blue" />
|
<StatItem label="Members" value={memberships.length.toString()} color="text-primary-blue" />
|
||||||
|
{team.category && (
|
||||||
|
<StatItem label="Category" value={team.category} color="text-purple-400" />
|
||||||
|
)}
|
||||||
{leagueCount > 0 && (
|
{leagueCount > 0 && (
|
||||||
<StatItem label="Leagues" value={leagueCount.toString()} color="text-green-400" />
|
<StatItem label="Leagues" value={leagueCount.toString()} color="text-green-400" />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -416,6 +416,12 @@ export default function TeamLeaderboardPage() {
|
|||||||
</p>
|
</p>
|
||||||
<div className="flex items-center gap-2 text-xs text-gray-500 flex-wrap">
|
<div className="flex items-center gap-2 text-xs text-gray-500 flex-wrap">
|
||||||
<span className={`${levelConfig?.color}`}>{levelConfig?.label}</span>
|
<span className={`${levelConfig?.color}`}>{levelConfig?.label}</span>
|
||||||
|
{team.category && (
|
||||||
|
<span className="flex items-center gap-1 text-purple-400">
|
||||||
|
<span className="w-1.5 h-1.5 rounded-full bg-purple-400"></span>
|
||||||
|
{team.category}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
{team.region && (
|
{team.region && (
|
||||||
<span className="flex items-center gap-1 text-gray-400">
|
<span className="flex items-center gap-1 text-gray-400">
|
||||||
<Globe className="w-3 h-3 text-neon-aqua" />
|
<Globe className="w-3 h-3 text-neon-aqua" />
|
||||||
|
|||||||
@@ -92,9 +92,17 @@ export default function TeamLeaderboardPreview({ teams, onTeamClick }: TeamLeade
|
|||||||
<p className="text-white font-medium truncate group-hover:text-purple-400 transition-colors">
|
<p className="text-white font-medium truncate group-hover:text-purple-400 transition-colors">
|
||||||
{team.name}
|
{team.name}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex items-center gap-2 text-xs text-gray-500">
|
<div className="flex items-center gap-2 text-xs text-gray-500 flex-wrap">
|
||||||
<Users className="w-3 h-3" />
|
{team.category && (
|
||||||
{team.memberCount} members
|
<span className="flex items-center gap-1 text-purple-400">
|
||||||
|
<span className="w-1.5 h-1.5 rounded-full bg-purple-400"></span>
|
||||||
|
{team.category}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<Users className="w-3 h-3" />
|
||||||
|
{team.memberCount} members
|
||||||
|
</span>
|
||||||
<span className={levelConfig?.color}>{levelConfig?.label}</span>
|
<span className={levelConfig?.color}>{levelConfig?.label}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -51,6 +51,48 @@ function getChampionshipLabel(type?: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getCategoryLabel(category?: string): string {
|
||||||
|
if (!category) return '';
|
||||||
|
|
||||||
|
switch (category) {
|
||||||
|
case 'driver':
|
||||||
|
return 'Driver';
|
||||||
|
case 'team':
|
||||||
|
return 'Team';
|
||||||
|
case 'nations':
|
||||||
|
return 'Nations';
|
||||||
|
case 'trophy':
|
||||||
|
return 'Trophy';
|
||||||
|
case 'endurance':
|
||||||
|
return 'Endurance';
|
||||||
|
case 'sprint':
|
||||||
|
return 'Sprint';
|
||||||
|
default:
|
||||||
|
return category.charAt(0).toUpperCase() + category.slice(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCategoryColor(category?: string): string {
|
||||||
|
if (!category) return 'bg-gray-500/20 text-gray-400 border-gray-500/30';
|
||||||
|
|
||||||
|
switch (category) {
|
||||||
|
case 'driver':
|
||||||
|
return 'bg-purple-500/20 text-purple-400 border-purple-500/30';
|
||||||
|
case 'team':
|
||||||
|
return 'bg-blue-500/20 text-blue-400 border-blue-500/30';
|
||||||
|
case 'nations':
|
||||||
|
return 'bg-green-500/20 text-green-400 border-green-500/30';
|
||||||
|
case 'trophy':
|
||||||
|
return 'bg-yellow-500/20 text-yellow-400 border-yellow-500/30';
|
||||||
|
case 'endurance':
|
||||||
|
return 'bg-orange-500/20 text-orange-400 border-orange-500/30';
|
||||||
|
case 'sprint':
|
||||||
|
return 'bg-red-500/20 text-red-400 border-red-500/30';
|
||||||
|
default:
|
||||||
|
return 'bg-gray-500/20 text-gray-400 border-gray-500/30';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getGameColor(gameId?: string): string {
|
function getGameColor(gameId?: string): string {
|
||||||
switch (gameId) {
|
switch (gameId) {
|
||||||
case 'iracing':
|
case 'iracing':
|
||||||
@@ -81,6 +123,8 @@ export default function LeagueCard({ league, onClick }: LeagueCardProps) {
|
|||||||
const gameColorClass = getGameColor(league.scoring?.gameId);
|
const gameColorClass = getGameColor(league.scoring?.gameId);
|
||||||
const isNew = isNewLeague(league.createdAt);
|
const isNew = isNewLeague(league.createdAt);
|
||||||
const isTeamLeague = league.maxTeams && league.maxTeams > 0;
|
const isTeamLeague = league.maxTeams && league.maxTeams > 0;
|
||||||
|
const categoryLabel = getCategoryLabel(league.category);
|
||||||
|
const categoryColorClass = getCategoryColor(league.category);
|
||||||
|
|
||||||
// Calculate fill percentage - use teams for team leagues, drivers otherwise
|
// Calculate fill percentage - use teams for team leagues, drivers otherwise
|
||||||
const usedSlots = isTeamLeague ? (league.usedTeamSlots ?? 0) : (league.usedDriverSlots ?? 0);
|
const usedSlots = isTeamLeague ? (league.usedTeamSlots ?? 0) : (league.usedDriverSlots ?? 0);
|
||||||
@@ -128,6 +172,11 @@ export default function LeagueCard({ league, onClick }: LeagueCardProps) {
|
|||||||
{league.scoring.gameName}
|
{league.scoring.gameName}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
{league.category && (
|
||||||
|
<span className={`px-2 py-0.5 rounded-full text-[10px] font-semibold border ${categoryColorClass}`}>
|
||||||
|
{categoryLabel}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Championship Type Badge - Top Right */}
|
{/* Championship Type Badge - Top Right */}
|
||||||
|
|||||||
@@ -91,7 +91,13 @@ export default function FeaturedRecruiting({ teams, onTeamClick }: FeaturedRecru
|
|||||||
</h3>
|
</h3>
|
||||||
<p className="text-xs text-gray-500 line-clamp-2 mb-3">{team.description}</p>
|
<p className="text-xs text-gray-500 line-clamp-2 mb-3">{team.description}</p>
|
||||||
|
|
||||||
<div className="flex items-center gap-3 text-xs text-gray-400">
|
<div className="flex items-center gap-2 text-xs text-gray-400 flex-wrap">
|
||||||
|
{team.category && (
|
||||||
|
<span className="flex items-center gap-1 text-purple-400">
|
||||||
|
<span className="w-1.5 h-1.5 rounded-full bg-purple-400"></span>
|
||||||
|
{team.category}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
<span className="flex items-center gap-1">
|
<span className="flex items-center gap-1">
|
||||||
<Users className="w-3 h-3" />
|
<Users className="w-3 h-3" />
|
||||||
{team.memberCount}
|
{team.memberCount}
|
||||||
|
|||||||
@@ -88,8 +88,9 @@ export default function SkillLevelSection({
|
|||||||
id={team.id}
|
id={team.id}
|
||||||
name={team.name}
|
name={team.name}
|
||||||
description={team.description ?? ''}
|
description={team.description ?? ''}
|
||||||
|
logo={team.logoUrl}
|
||||||
memberCount={team.memberCount}
|
memberCount={team.memberCount}
|
||||||
rating={null}
|
rating={team.rating}
|
||||||
totalWins={team.totalWins}
|
totalWins={team.totalWins}
|
||||||
totalRaces={team.totalRaces}
|
totalRaces={team.totalRaces}
|
||||||
performanceLevel={team.performanceLevel as SkillLevel}
|
performanceLevel={team.performanceLevel as SkillLevel}
|
||||||
@@ -97,6 +98,7 @@ export default function SkillLevelSection({
|
|||||||
specialization={specialization(team.specialization)}
|
specialization={specialization(team.specialization)}
|
||||||
region={team.region ?? ''}
|
region={team.region ?? ''}
|
||||||
languages={team.languages}
|
languages={team.languages}
|
||||||
|
category={team.category}
|
||||||
onClick={() => onTeamClick(team.id)}
|
onClick={() => onTeamClick(team.id)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ interface TeamCardProps {
|
|||||||
region?: string;
|
region?: string;
|
||||||
languages?: string[] | undefined;
|
languages?: string[] | undefined;
|
||||||
leagues?: string[];
|
leagues?: string[];
|
||||||
|
category?: string;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,6 +78,7 @@ export default function TeamCard({
|
|||||||
specialization,
|
specialization,
|
||||||
region,
|
region,
|
||||||
languages,
|
languages,
|
||||||
|
category,
|
||||||
onClick,
|
onClick,
|
||||||
}: TeamCardProps) {
|
}: TeamCardProps) {
|
||||||
const { mediaService } = useServices();
|
const { mediaService } = useServices();
|
||||||
@@ -119,21 +121,27 @@ export default function TeamCard({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Performance Level */}
|
{/* Performance Level & Category */}
|
||||||
{performanceBadge && (
|
<div className="mt-1.5 flex items-center gap-2 flex-wrap">
|
||||||
<div className="mt-1.5 flex items-center gap-2">
|
{performanceBadge && (
|
||||||
<span className={`flex items-center gap-1 px-2 py-0.5 rounded-full text-[10px] font-medium border ${performanceBadge.bgColor}`}>
|
<span className={`flex items-center gap-1 px-2 py-0.5 rounded-full text-[10px] font-medium border ${performanceBadge.bgColor}`}>
|
||||||
<performanceBadge.icon className={`w-3 h-3 ${performanceBadge.color}`} />
|
<performanceBadge.icon className={`w-3 h-3 ${performanceBadge.color}`} />
|
||||||
<span className={performanceBadge.color}>{performanceBadge.label}</span>
|
<span className={performanceBadge.color}>{performanceBadge.label}</span>
|
||||||
</span>
|
</span>
|
||||||
{specializationBadge && (
|
)}
|
||||||
<span className="flex items-center gap-1 text-[10px] text-gray-500">
|
{specializationBadge && (
|
||||||
<specializationBadge.icon className={`w-3 h-3 ${specializationBadge.color}`} />
|
<span className="flex items-center gap-1 text-[10px] text-gray-500">
|
||||||
{specializationBadge.label}
|
<specializationBadge.icon className={`w-3 h-3 ${specializationBadge.color}`} />
|
||||||
</span>
|
{specializationBadge.label}
|
||||||
)}
|
</span>
|
||||||
</div>
|
)}
|
||||||
)}
|
{category && (
|
||||||
|
<span className="flex items-center gap-1 px-2 py-0.5 rounded-full text-[10px] font-medium bg-purple-500/20 border border-purple-500/30 text-purple-400">
|
||||||
|
<span className="w-2 h-2 rounded-full bg-purple-400"></span>
|
||||||
|
{category}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -140,7 +140,13 @@ export default function TeamLeaderboardPreview({
|
|||||||
<p className="text-white font-medium truncate group-hover:text-purple-400 transition-colors">
|
<p className="text-white font-medium truncate group-hover:text-purple-400 transition-colors">
|
||||||
{team.name}
|
{team.name}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex items-center gap-3 text-xs text-gray-500">
|
<div className="flex items-center gap-2 text-xs text-gray-500 flex-wrap">
|
||||||
|
{team.category && (
|
||||||
|
<span className="flex items-center gap-1 text-purple-400">
|
||||||
|
<span className="w-1.5 h-1.5 rounded-full bg-purple-400"></span>
|
||||||
|
{team.category}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
<span className="flex items-center gap-1">
|
<span className="flex items-center gap-1">
|
||||||
<Users className="w-3 h-3" />
|
<Users className="w-3 h-3" />
|
||||||
{team.memberCount}
|
{team.memberCount}
|
||||||
@@ -161,7 +167,7 @@ export default function TeamLeaderboardPreview({
|
|||||||
{/* Rating */}
|
{/* Rating */}
|
||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
<p className="text-purple-400 font-mono font-semibold">
|
<p className="text-purple-400 font-mono font-semibold">
|
||||||
{'—'}
|
{typeof team.rating === 'number' ? Math.round(team.rating).toLocaleString() : '—'}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-gray-500">Rating</p>
|
<p className="text-xs text-gray-500">Rating</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -140,6 +140,13 @@ export default function TopThreePodium({ teams, onClick }: TopThreePodiumProps)
|
|||||||
{team.name}
|
{team.name}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
{/* Category */}
|
||||||
|
{team.category && (
|
||||||
|
<p className="text-xs text-purple-400 text-center mt-1">
|
||||||
|
{team.category}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Rating */}
|
{/* Rating */}
|
||||||
<p className={`text-lg md:text-xl font-mono font-bold ${getPositionColor(position)} text-center`}>
|
<p className={`text-lg md:text-xl font-mono font-bold ${getPositionColor(position)} text-center`}>
|
||||||
{'—'}
|
{'—'}
|
||||||
|
|||||||
@@ -121,6 +121,7 @@ export class LeagueService {
|
|||||||
structureSummary: league.scoring?.scoringPresetName ?? 'Custom rules',
|
structureSummary: league.scoring?.scoringPresetName ?? 'Custom rules',
|
||||||
scoringPatternSummary: league.scoring?.scoringPatternSummary,
|
scoringPatternSummary: league.scoring?.scoringPatternSummary,
|
||||||
timingSummary: league.timingSummary ?? '',
|
timingSummary: league.timingSummary ?? '',
|
||||||
|
...(league.category ? { category: league.category } : {}),
|
||||||
...(league.scoring ? { scoring: league.scoring } : {}),
|
...(league.scoring ? { scoring: league.scoring } : {}),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { DeleteMediaViewModel } from '@/lib/view-models/DeleteMediaViewModel';
|
|||||||
import { MediaViewModel } from '@/lib/view-models/MediaViewModel';
|
import { MediaViewModel } from '@/lib/view-models/MediaViewModel';
|
||||||
import { UploadMediaViewModel } from '@/lib/view-models/UploadMediaViewModel';
|
import { UploadMediaViewModel } from '@/lib/view-models/UploadMediaViewModel';
|
||||||
import type { MediaApiClient } from '../../api/media/MediaApiClient';
|
import type { MediaApiClient } from '../../api/media/MediaApiClient';
|
||||||
|
import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl';
|
||||||
|
|
||||||
// Local request shape mirroring the media upload API contract until a generated type is available
|
// Local request shape mirroring the media upload API contract until a generated type is available
|
||||||
type UploadMediaRequest = { file: File; type: string; category?: string };
|
type UploadMediaRequest = { file: File; type: string; category?: string };
|
||||||
@@ -43,6 +44,7 @@ export class MediaService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get team logo URL
|
* Get team logo URL
|
||||||
|
* Returns relative URL for proxying through Next.js rewrites
|
||||||
*/
|
*/
|
||||||
getTeamLogo(teamId: string): string {
|
getTeamLogo(teamId: string): string {
|
||||||
return `/api/media/teams/${teamId}/logo`;
|
return `/api/media/teams/${teamId}/logo`;
|
||||||
@@ -50,6 +52,7 @@ export class MediaService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get driver avatar URL
|
* Get driver avatar URL
|
||||||
|
* Returns relative URL for proxying through Next.js rewrites
|
||||||
*/
|
*/
|
||||||
getDriverAvatar(driverId: string): string {
|
getDriverAvatar(driverId: string): string {
|
||||||
return `/api/media/avatar/${driverId}`;
|
return `/api/media/avatar/${driverId}`;
|
||||||
@@ -57,6 +60,7 @@ export class MediaService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get league cover URL
|
* Get league cover URL
|
||||||
|
* Returns relative URL for proxying through Next.js rewrites
|
||||||
*/
|
*/
|
||||||
getLeagueCover(leagueId: string): string {
|
getLeagueCover(leagueId: string): string {
|
||||||
return `/api/media/leagues/${leagueId}/cover`;
|
return `/api/media/leagues/${leagueId}/cover`;
|
||||||
@@ -64,6 +68,7 @@ export class MediaService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get league logo URL
|
* Get league logo URL
|
||||||
|
* Returns relative URL for proxying through Next.js rewrites
|
||||||
*/
|
*/
|
||||||
getLeagueLogo(leagueId: string): string {
|
getLeagueLogo(leagueId: string): string {
|
||||||
return `/api/media/leagues/${leagueId}/logo`;
|
return `/api/media/leagues/${leagueId}/logo`;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Auto-generated DTO from OpenAPI spec
|
* Auto-generated DTO from OpenAPI spec
|
||||||
* Spec SHA256: 16aae91ec085d450f377d8c914d93df23e86783b2b124fedd1075307d2c5b17f
|
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||||
* This file is generated by scripts/generate-api-types.ts
|
* This file is generated by scripts/generate-api-types.ts
|
||||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Auto-generated DTO from OpenAPI spec
|
* Auto-generated DTO from OpenAPI spec
|
||||||
* Spec SHA256: 16aae91ec085d450f377d8c914d93df23e86783b2b124fedd1075307d2c5b17f
|
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||||
* This file is generated by scripts/generate-api-types.ts
|
* This file is generated by scripts/generate-api-types.ts
|
||||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Auto-generated DTO from OpenAPI spec
|
* Auto-generated DTO from OpenAPI spec
|
||||||
* Spec SHA256: 16aae91ec085d450f377d8c914d93df23e86783b2b124fedd1075307d2c5b17f
|
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||||
* This file is generated by scripts/generate-api-types.ts
|
* This file is generated by scripts/generate-api-types.ts
|
||||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Auto-generated DTO from OpenAPI spec
|
* Auto-generated DTO from OpenAPI spec
|
||||||
* Spec SHA256: 16aae91ec085d450f377d8c914d93df23e86783b2b124fedd1075307d2c5b17f
|
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||||
* This file is generated by scripts/generate-api-types.ts
|
* This file is generated by scripts/generate-api-types.ts
|
||||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Auto-generated DTO from OpenAPI spec
|
* Auto-generated DTO from OpenAPI spec
|
||||||
* Spec SHA256: 16aae91ec085d450f377d8c914d93df23e86783b2b124fedd1075307d2c5b17f
|
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||||
* This file is generated by scripts/generate-api-types.ts
|
* This file is generated by scripts/generate-api-types.ts
|
||||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Auto-generated DTO from OpenAPI spec
|
* Auto-generated DTO from OpenAPI spec
|
||||||
* Spec SHA256: 16aae91ec085d450f377d8c914d93df23e86783b2b124fedd1075307d2c5b17f
|
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||||
* This file is generated by scripts/generate-api-types.ts
|
* This file is generated by scripts/generate-api-types.ts
|
||||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Auto-generated DTO from OpenAPI spec
|
* Auto-generated DTO from OpenAPI spec
|
||||||
* Spec SHA256: 16aae91ec085d450f377d8c914d93df23e86783b2b124fedd1075307d2c5b17f
|
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||||
* This file is generated by scripts/generate-api-types.ts
|
* This file is generated by scripts/generate-api-types.ts
|
||||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Auto-generated DTO from OpenAPI spec
|
* Auto-generated DTO from OpenAPI spec
|
||||||
* Spec SHA256: 16aae91ec085d450f377d8c914d93df23e86783b2b124fedd1075307d2c5b17f
|
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||||
* This file is generated by scripts/generate-api-types.ts
|
* This file is generated by scripts/generate-api-types.ts
|
||||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Auto-generated DTO from OpenAPI spec
|
* Auto-generated DTO from OpenAPI spec
|
||||||
* Spec SHA256: 16aae91ec085d450f377d8c914d93df23e86783b2b124fedd1075307d2c5b17f
|
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||||
* This file is generated by scripts/generate-api-types.ts
|
* This file is generated by scripts/generate-api-types.ts
|
||||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Auto-generated DTO from OpenAPI spec
|
* Auto-generated DTO from OpenAPI spec
|
||||||
* Spec SHA256: 16aae91ec085d450f377d8c914d93df23e86783b2b124fedd1075307d2c5b17f
|
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||||
* This file is generated by scripts/generate-api-types.ts
|
* This file is generated by scripts/generate-api-types.ts
|
||||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Auto-generated DTO from OpenAPI spec
|
* Auto-generated DTO from OpenAPI spec
|
||||||
* Spec SHA256: 16aae91ec085d450f377d8c914d93df23e86783b2b124fedd1075307d2c5b17f
|
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||||
* This file is generated by scripts/generate-api-types.ts
|
* This file is generated by scripts/generate-api-types.ts
|
||||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Auto-generated DTO from OpenAPI spec
|
* Auto-generated DTO from OpenAPI spec
|
||||||
* Spec SHA256: 16aae91ec085d450f377d8c914d93df23e86783b2b124fedd1075307d2c5b17f
|
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||||
* This file is generated by scripts/generate-api-types.ts
|
* This file is generated by scripts/generate-api-types.ts
|
||||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Auto-generated DTO from OpenAPI spec
|
* Auto-generated DTO from OpenAPI spec
|
||||||
* Spec SHA256: 16aae91ec085d450f377d8c914d93df23e86783b2b124fedd1075307d2c5b17f
|
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||||
* This file is generated by scripts/generate-api-types.ts
|
* This file is generated by scripts/generate-api-types.ts
|
||||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Auto-generated DTO from OpenAPI spec
|
* Auto-generated DTO from OpenAPI spec
|
||||||
* Spec SHA256: 16aae91ec085d450f377d8c914d93df23e86783b2b124fedd1075307d2c5b17f
|
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||||
* This file is generated by scripts/generate-api-types.ts
|
* This file is generated by scripts/generate-api-types.ts
|
||||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Auto-generated DTO from OpenAPI spec
|
* Auto-generated DTO from OpenAPI spec
|
||||||
* Spec SHA256: 16aae91ec085d450f377d8c914d93df23e86783b2b124fedd1075307d2c5b17f
|
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||||
* This file is generated by scripts/generate-api-types.ts
|
* This file is generated by scripts/generate-api-types.ts
|
||||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Auto-generated DTO from OpenAPI spec
|
* Auto-generated DTO from OpenAPI spec
|
||||||
* Spec SHA256: 16aae91ec085d450f377d8c914d93df23e86783b2b124fedd1075307d2c5b17f
|
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||||
* This file is generated by scripts/generate-api-types.ts
|
* This file is generated by scripts/generate-api-types.ts
|
||||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Auto-generated DTO from OpenAPI spec
|
* Auto-generated DTO from OpenAPI spec
|
||||||
* Spec SHA256: 16aae91ec085d450f377d8c914d93df23e86783b2b124fedd1075307d2c5b17f
|
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||||
* This file is generated by scripts/generate-api-types.ts
|
* This file is generated by scripts/generate-api-types.ts
|
||||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Auto-generated DTO from OpenAPI spec
|
* Auto-generated DTO from OpenAPI spec
|
||||||
* Spec SHA256: 16aae91ec085d450f377d8c914d93df23e86783b2b124fedd1075307d2c5b17f
|
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||||
* This file is generated by scripts/generate-api-types.ts
|
* This file is generated by scripts/generate-api-types.ts
|
||||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Auto-generated DTO from OpenAPI spec
|
* Auto-generated DTO from OpenAPI spec
|
||||||
* Spec SHA256: 16aae91ec085d450f377d8c914d93df23e86783b2b124fedd1075307d2c5b17f
|
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||||
* This file is generated by scripts/generate-api-types.ts
|
* This file is generated by scripts/generate-api-types.ts
|
||||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Auto-generated DTO from OpenAPI spec
|
* Auto-generated DTO from OpenAPI spec
|
||||||
* Spec SHA256: 16aae91ec085d450f377d8c914d93df23e86783b2b124fedd1075307d2c5b17f
|
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||||
* This file is generated by scripts/generate-api-types.ts
|
* This file is generated by scripts/generate-api-types.ts
|
||||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Auto-generated DTO from OpenAPI spec
|
* Auto-generated DTO from OpenAPI spec
|
||||||
* Spec SHA256: 16aae91ec085d450f377d8c914d93df23e86783b2b124fedd1075307d2c5b17f
|
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||||
* This file is generated by scripts/generate-api-types.ts
|
* This file is generated by scripts/generate-api-types.ts
|
||||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Auto-generated DTO from OpenAPI spec
|
* Auto-generated DTO from OpenAPI spec
|
||||||
* Spec SHA256: 16aae91ec085d450f377d8c914d93df23e86783b2b124fedd1075307d2c5b17f
|
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||||
* This file is generated by scripts/generate-api-types.ts
|
* This file is generated by scripts/generate-api-types.ts
|
||||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Auto-generated DTO from OpenAPI spec
|
* Auto-generated DTO from OpenAPI spec
|
||||||
* Spec SHA256: 16aae91ec085d450f377d8c914d93df23e86783b2b124fedd1075307d2c5b17f
|
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||||
* This file is generated by scripts/generate-api-types.ts
|
* This file is generated by scripts/generate-api-types.ts
|
||||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Auto-generated DTO from OpenAPI spec
|
* Auto-generated DTO from OpenAPI spec
|
||||||
* Spec SHA256: 16aae91ec085d450f377d8c914d93df23e86783b2b124fedd1075307d2c5b17f
|
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||||
* This file is generated by scripts/generate-api-types.ts
|
* This file is generated by scripts/generate-api-types.ts
|
||||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Auto-generated DTO from OpenAPI spec
|
* Auto-generated DTO from OpenAPI spec
|
||||||
* Spec SHA256: 16aae91ec085d450f377d8c914d93df23e86783b2b124fedd1075307d2c5b17f
|
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||||
* This file is generated by scripts/generate-api-types.ts
|
* This file is generated by scripts/generate-api-types.ts
|
||||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Auto-generated DTO from OpenAPI spec
|
* Auto-generated DTO from OpenAPI spec
|
||||||
* Spec SHA256: 16aae91ec085d450f377d8c914d93df23e86783b2b124fedd1075307d2c5b17f
|
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||||
* This file is generated by scripts/generate-api-types.ts
|
* This file is generated by scripts/generate-api-types.ts
|
||||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Auto-generated DTO from OpenAPI spec
|
* Auto-generated DTO from OpenAPI spec
|
||||||
* Spec SHA256: 16aae91ec085d450f377d8c914d93df23e86783b2b124fedd1075307d2c5b17f
|
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||||
* This file is generated by scripts/generate-api-types.ts
|
* This file is generated by scripts/generate-api-types.ts
|
||||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Auto-generated DTO from OpenAPI spec
|
* Auto-generated DTO from OpenAPI spec
|
||||||
* Spec SHA256: 16aae91ec085d450f377d8c914d93df23e86783b2b124fedd1075307d2c5b17f
|
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||||
* This file is generated by scripts/generate-api-types.ts
|
* This file is generated by scripts/generate-api-types.ts
|
||||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Auto-generated DTO from OpenAPI spec
|
* Auto-generated DTO from OpenAPI spec
|
||||||
* Spec SHA256: 16aae91ec085d450f377d8c914d93df23e86783b2b124fedd1075307d2c5b17f
|
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||||
* This file is generated by scripts/generate-api-types.ts
|
* This file is generated by scripts/generate-api-types.ts
|
||||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Auto-generated DTO from OpenAPI spec
|
* Auto-generated DTO from OpenAPI spec
|
||||||
* Spec SHA256: 16aae91ec085d450f377d8c914d93df23e86783b2b124fedd1075307d2c5b17f
|
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||||
* This file is generated by scripts/generate-api-types.ts
|
* This file is generated by scripts/generate-api-types.ts
|
||||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Auto-generated DTO from OpenAPI spec
|
* Auto-generated DTO from OpenAPI spec
|
||||||
* Spec SHA256: 16aae91ec085d450f377d8c914d93df23e86783b2b124fedd1075307d2c5b17f
|
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||||
* This file is generated by scripts/generate-api-types.ts
|
* This file is generated by scripts/generate-api-types.ts
|
||||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Auto-generated DTO from OpenAPI spec
|
* Auto-generated DTO from OpenAPI spec
|
||||||
* Spec SHA256: 16aae91ec085d450f377d8c914d93df23e86783b2b124fedd1075307d2c5b17f
|
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||||
* This file is generated by scripts/generate-api-types.ts
|
* This file is generated by scripts/generate-api-types.ts
|
||||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||||
*/
|
*/
|
||||||
@@ -10,6 +10,7 @@ export interface DashboardDriverSummaryDTO {
|
|||||||
name: string;
|
name: string;
|
||||||
country: string;
|
country: string;
|
||||||
avatarUrl: string;
|
avatarUrl: string;
|
||||||
|
category?: string;
|
||||||
rating?: number;
|
rating?: number;
|
||||||
globalRank?: number;
|
globalRank?: number;
|
||||||
totalRaces: number;
|
totalRaces: number;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Auto-generated DTO from OpenAPI spec
|
* Auto-generated DTO from OpenAPI spec
|
||||||
* Spec SHA256: 16aae91ec085d450f377d8c914d93df23e86783b2b124fedd1075307d2c5b17f
|
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||||
* This file is generated by scripts/generate-api-types.ts
|
* This file is generated by scripts/generate-api-types.ts
|
||||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Auto-generated DTO from OpenAPI spec
|
* Auto-generated DTO from OpenAPI spec
|
||||||
* Spec SHA256: 16aae91ec085d450f377d8c914d93df23e86783b2b124fedd1075307d2c5b17f
|
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||||
* This file is generated by scripts/generate-api-types.ts
|
* This file is generated by scripts/generate-api-types.ts
|
||||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Auto-generated DTO from OpenAPI spec
|
* Auto-generated DTO from OpenAPI spec
|
||||||
* Spec SHA256: 16aae91ec085d450f377d8c914d93df23e86783b2b124fedd1075307d2c5b17f
|
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||||
* This file is generated by scripts/generate-api-types.ts
|
* This file is generated by scripts/generate-api-types.ts
|
||||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Auto-generated DTO from OpenAPI spec
|
* Auto-generated DTO from OpenAPI spec
|
||||||
* Spec SHA256: 16aae91ec085d450f377d8c914d93df23e86783b2b124fedd1075307d2c5b17f
|
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||||
* This file is generated by scripts/generate-api-types.ts
|
* This file is generated by scripts/generate-api-types.ts
|
||||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Auto-generated DTO from OpenAPI spec
|
* Auto-generated DTO from OpenAPI spec
|
||||||
* Spec SHA256: 16aae91ec085d450f377d8c914d93df23e86783b2b124fedd1075307d2c5b17f
|
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||||
* This file is generated by scripts/generate-api-types.ts
|
* This file is generated by scripts/generate-api-types.ts
|
||||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Auto-generated DTO from OpenAPI spec
|
* Auto-generated DTO from OpenAPI spec
|
||||||
* Spec SHA256: 16aae91ec085d450f377d8c914d93df23e86783b2b124fedd1075307d2c5b17f
|
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||||
* This file is generated by scripts/generate-api-types.ts
|
* This file is generated by scripts/generate-api-types.ts
|
||||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Auto-generated DTO from OpenAPI spec
|
* Auto-generated DTO from OpenAPI spec
|
||||||
* Spec SHA256: 16aae91ec085d450f377d8c914d93df23e86783b2b124fedd1075307d2c5b17f
|
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||||
* This file is generated by scripts/generate-api-types.ts
|
* This file is generated by scripts/generate-api-types.ts
|
||||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Auto-generated DTO from OpenAPI spec
|
* Auto-generated DTO from OpenAPI spec
|
||||||
* Spec SHA256: 16aae91ec085d450f377d8c914d93df23e86783b2b124fedd1075307d2c5b17f
|
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||||
* This file is generated by scripts/generate-api-types.ts
|
* This file is generated by scripts/generate-api-types.ts
|
||||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Auto-generated DTO from OpenAPI spec
|
* Auto-generated DTO from OpenAPI spec
|
||||||
* Spec SHA256: 16aae91ec085d450f377d8c914d93df23e86783b2b124fedd1075307d2c5b17f
|
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||||
* This file is generated by scripts/generate-api-types.ts
|
* This file is generated by scripts/generate-api-types.ts
|
||||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Auto-generated DTO from OpenAPI spec
|
* Auto-generated DTO from OpenAPI spec
|
||||||
* Spec SHA256: 16aae91ec085d450f377d8c914d93df23e86783b2b124fedd1075307d2c5b17f
|
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||||
* This file is generated by scripts/generate-api-types.ts
|
* This file is generated by scripts/generate-api-types.ts
|
||||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||||
*/
|
*/
|
||||||
@@ -12,4 +12,5 @@ export interface DriverDTO {
|
|||||||
country: string;
|
country: string;
|
||||||
bio?: string;
|
bio?: string;
|
||||||
joinedAt: string;
|
joinedAt: string;
|
||||||
|
category?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Auto-generated DTO from OpenAPI spec
|
* Auto-generated DTO from OpenAPI spec
|
||||||
* Spec SHA256: 16aae91ec085d450f377d8c914d93df23e86783b2b124fedd1075307d2c5b17f
|
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||||
* This file is generated by scripts/generate-api-types.ts
|
* This file is generated by scripts/generate-api-types.ts
|
||||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||||
*/
|
*/
|
||||||
@@ -10,6 +10,7 @@ export interface DriverLeaderboardItemDTO {
|
|||||||
name: string;
|
name: string;
|
||||||
rating: number;
|
rating: number;
|
||||||
skillLevel: string;
|
skillLevel: string;
|
||||||
|
category?: string;
|
||||||
nationality: string;
|
nationality: string;
|
||||||
racesCompleted: number;
|
racesCompleted: number;
|
||||||
wins: number;
|
wins: number;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Auto-generated DTO from OpenAPI spec
|
* Auto-generated DTO from OpenAPI spec
|
||||||
* Spec SHA256: 16aae91ec085d450f377d8c914d93df23e86783b2b124fedd1075307d2c5b17f
|
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||||
* This file is generated by scripts/generate-api-types.ts
|
* This file is generated by scripts/generate-api-types.ts
|
||||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Auto-generated DTO from OpenAPI spec
|
* Auto-generated DTO from OpenAPI spec
|
||||||
* Spec SHA256: 16aae91ec085d450f377d8c914d93df23e86783b2b124fedd1075307d2c5b17f
|
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||||
* This file is generated by scripts/generate-api-types.ts
|
* This file is generated by scripts/generate-api-types.ts
|
||||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||||
*/
|
*/
|
||||||
@@ -12,6 +12,7 @@ export interface DriverProfileDriverSummaryDTO {
|
|||||||
avatarUrl: string;
|
avatarUrl: string;
|
||||||
iracingId?: string;
|
iracingId?: string;
|
||||||
joinedAt: string;
|
joinedAt: string;
|
||||||
|
category?: string;
|
||||||
rating?: number;
|
rating?: number;
|
||||||
globalRank?: number;
|
globalRank?: number;
|
||||||
consistency?: number;
|
consistency?: number;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Auto-generated DTO from OpenAPI spec
|
* Auto-generated DTO from OpenAPI spec
|
||||||
* Spec SHA256: 16aae91ec085d450f377d8c914d93df23e86783b2b124fedd1075307d2c5b17f
|
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||||
* This file is generated by scripts/generate-api-types.ts
|
* This file is generated by scripts/generate-api-types.ts
|
||||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Auto-generated DTO from OpenAPI spec
|
* Auto-generated DTO from OpenAPI spec
|
||||||
* Spec SHA256: 16aae91ec085d450f377d8c914d93df23e86783b2b124fedd1075307d2c5b17f
|
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||||
* This file is generated by scripts/generate-api-types.ts
|
* This file is generated by scripts/generate-api-types.ts
|
||||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||||
*/
|
*/
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user