This commit is contained in:
2025-12-10 15:41:44 +01:00
parent fbbcf414a4
commit 6d61be9c51
22 changed files with 1721 additions and 1987 deletions

View File

@@ -52,29 +52,10 @@ interface DriverListItem {
// ============================================================================
// DEMO DATA
// ============================================================================
const DEMO_DRIVERS: DriverListItem[] = [
{ id: 'demo-1', name: 'Max Verstappen', rating: 4250, skillLevel: 'pro', nationality: 'NL', racesCompleted: 156, wins: 47, podiums: 89, isActive: true, rank: 1 },
{ id: 'demo-2', name: 'Lewis Hamilton', rating: 4180, skillLevel: 'pro', nationality: 'GB', racesCompleted: 198, wins: 52, podiums: 112, isActive: true, rank: 2 },
{ id: 'demo-3', name: 'Charles Leclerc', rating: 3950, skillLevel: 'pro', nationality: 'MC', racesCompleted: 134, wins: 28, podiums: 67, isActive: true, rank: 3 },
{ id: 'demo-4', name: 'Lando Norris', rating: 3820, skillLevel: 'advanced', nationality: 'GB', racesCompleted: 112, wins: 18, podiums: 45, isActive: true, rank: 4 },
{ id: 'demo-5', name: 'Carlos Sainz', rating: 3750, skillLevel: 'advanced', nationality: 'ES', racesCompleted: 145, wins: 15, podiums: 52, isActive: true, rank: 5 },
{ id: 'demo-6', name: 'Oscar Piastri', rating: 3680, skillLevel: 'advanced', nationality: 'AU', racesCompleted: 78, wins: 8, podiums: 24, isActive: true, rank: 6 },
{ id: 'demo-7', name: 'George Russell', rating: 3620, skillLevel: 'advanced', nationality: 'GB', racesCompleted: 98, wins: 6, podiums: 31, isActive: true, rank: 7 },
{ id: 'demo-8', name: 'Fernando Alonso', rating: 3580, skillLevel: 'advanced', nationality: 'ES', racesCompleted: 256, wins: 32, podiums: 98, isActive: true, rank: 8 },
{ id: 'demo-9', name: 'Nico Hülkenberg', rating: 3420, skillLevel: 'advanced', nationality: 'DE', racesCompleted: 167, wins: 2, podiums: 18, isActive: true, rank: 9 },
{ id: 'demo-10', name: 'Yuki Tsunoda', rating: 3250, skillLevel: 'intermediate', nationality: 'JP', racesCompleted: 89, wins: 1, podiums: 8, isActive: true, rank: 10 },
{ id: 'demo-11', name: 'Alex Albon', rating: 3180, skillLevel: 'intermediate', nationality: 'TH', racesCompleted: 102, wins: 0, podiums: 4, isActive: true, rank: 11 },
{ id: 'demo-12', name: 'Kevin Magnussen', rating: 3050, skillLevel: 'intermediate', nationality: 'DK', racesCompleted: 145, wins: 0, podiums: 2, isActive: true, rank: 12 },
{ id: 'demo-13', name: 'Pierre Gasly', rating: 2980, skillLevel: 'intermediate', nationality: 'FR', racesCompleted: 124, wins: 1, podiums: 5, isActive: true, rank: 13 },
{ id: 'demo-14', name: 'Esteban Ocon', rating: 2920, skillLevel: 'intermediate', nationality: 'FR', racesCompleted: 118, wins: 1, podiums: 4, isActive: true, rank: 14 },
{ id: 'demo-15', name: 'Lance Stroll', rating: 2850, skillLevel: 'intermediate', nationality: 'CA', racesCompleted: 134, wins: 0, podiums: 3, isActive: true, rank: 15 },
{ id: 'demo-16', name: 'Zhou Guanyu', rating: 2650, skillLevel: 'intermediate', nationality: 'CN', racesCompleted: 67, wins: 0, podiums: 0, isActive: true, rank: 16 },
{ id: 'demo-17', name: 'Daniel Ricciardo', rating: 2500, skillLevel: 'intermediate', nationality: 'AU', racesCompleted: 189, wins: 8, podiums: 32, isActive: false, rank: 17 },
{ id: 'demo-18', name: 'Valtteri Bottas', rating: 2450, skillLevel: 'intermediate', nationality: 'FI', racesCompleted: 212, wins: 10, podiums: 67, isActive: false, rank: 18 },
{ id: 'demo-19', name: 'Logan Sargeant', rating: 1850, skillLevel: 'beginner', nationality: 'US', racesCompleted: 34, wins: 0, podiums: 0, isActive: false, rank: 19 },
{ id: 'demo-20', name: 'Nyck de Vries', rating: 1750, skillLevel: 'beginner', nationality: 'NL', racesCompleted: 12, wins: 0, podiums: 0, isActive: false, rank: 20 },
];
//
// In alpha, all driver listings come from the in-memory repositories wired
// through the DI container. We intentionally avoid hardcoded fallback driver
// lists here so that the demo data stays consistent across pages.
// ============================================================================
// SKILL LEVEL CONFIG
@@ -464,7 +445,7 @@ export default function DriversPage() {
return rankA - rankB || b.rating - a.rating;
});
setDrivers(items.length > 0 ? items : DEMO_DRIVERS);
setDrivers(items);
setLoading(false);
};
@@ -472,7 +453,6 @@ export default function DriversPage() {
}, []);
const handleDriverClick = (driverId: string) => {
if (driverId.startsWith('demo-')) return;
router.push(`/drivers/${driverId}`);
};

View File

@@ -41,28 +41,6 @@ interface DriverListItem {
rank: number;
}
// ============================================================================
// DEMO DATA
// ============================================================================
const DEMO_DRIVERS: DriverListItem[] = [
{ id: 'demo-1', name: 'Max Verstappen', rating: 4250, skillLevel: 'pro', nationality: 'NL', racesCompleted: 156, wins: 47, podiums: 89, rank: 1 },
{ id: 'demo-2', name: 'Lewis Hamilton', rating: 4180, skillLevel: 'pro', nationality: 'GB', racesCompleted: 198, wins: 52, podiums: 112, rank: 2 },
{ id: 'demo-3', name: 'Charles Leclerc', rating: 3950, skillLevel: 'pro', nationality: 'MC', racesCompleted: 134, wins: 28, podiums: 67, rank: 3 },
{ id: 'demo-4', name: 'Lando Norris', rating: 3820, skillLevel: 'advanced', nationality: 'GB', racesCompleted: 112, wins: 18, podiums: 45, rank: 4 },
{ id: 'demo-5', name: 'Carlos Sainz', rating: 3750, skillLevel: 'advanced', nationality: 'ES', racesCompleted: 145, wins: 15, podiums: 52, rank: 5 },
{ id: 'demo-6', name: 'Oscar Piastri', rating: 3680, skillLevel: 'advanced', nationality: 'AU', racesCompleted: 78, wins: 8, podiums: 24, rank: 6 },
{ id: 'demo-7', name: 'George Russell', rating: 3620, skillLevel: 'advanced', nationality: 'GB', racesCompleted: 98, wins: 6, podiums: 31, rank: 7 },
{ id: 'demo-8', name: 'Fernando Alonso', rating: 3580, skillLevel: 'advanced', nationality: 'ES', racesCompleted: 256, wins: 32, podiums: 98, rank: 8 },
{ id: 'demo-9', name: 'Nico Hülkenberg', rating: 3420, skillLevel: 'advanced', nationality: 'DE', racesCompleted: 167, wins: 2, podiums: 18, rank: 9 },
{ id: 'demo-10', name: 'Yuki Tsunoda', rating: 3250, skillLevel: 'intermediate', nationality: 'JP', racesCompleted: 89, wins: 1, podiums: 8, rank: 10 },
{ id: 'demo-11', name: 'Alex Albon', rating: 3180, skillLevel: 'intermediate', nationality: 'TH', racesCompleted: 102, wins: 0, podiums: 4, rank: 11 },
{ id: 'demo-12', name: 'Kevin Magnussen', rating: 3050, skillLevel: 'intermediate', nationality: 'DK', racesCompleted: 145, wins: 0, podiums: 2, rank: 12 },
{ id: 'demo-13', name: 'Pierre Gasly', rating: 2980, skillLevel: 'intermediate', nationality: 'FR', racesCompleted: 124, wins: 1, podiums: 5, rank: 13 },
{ id: 'demo-14', name: 'Esteban Ocon', rating: 2920, skillLevel: 'intermediate', nationality: 'FR', racesCompleted: 118, wins: 1, podiums: 4, rank: 14 },
{ id: 'demo-15', name: 'Lance Stroll', rating: 2850, skillLevel: 'intermediate', nationality: 'CA', racesCompleted: 134, wins: 0, podiums: 3, rank: 15 },
];
// ============================================================================
// SKILL LEVEL CONFIG
// ============================================================================
@@ -247,7 +225,7 @@ export default function DriverLeaderboardPage() {
};
});
setDrivers(items.length > 0 ? items : DEMO_DRIVERS);
setDrivers(items);
setLoading(false);
};

View File

@@ -51,31 +51,6 @@ interface TeamDisplayData {
performanceLevel: SkillLevel;
}
// ============================================================================
// DEMO DATA
// ============================================================================
const DEMO_DRIVERS: DriverListItem[] = [
{ id: 'demo-1', name: 'Max Verstappen', rating: 4250, skillLevel: 'pro', nationality: 'NL', wins: 47, podiums: 89, rank: 1 },
{ id: 'demo-2', name: 'Lewis Hamilton', rating: 4180, skillLevel: 'pro', nationality: 'GB', wins: 52, podiums: 112, rank: 2 },
{ id: 'demo-3', name: 'Charles Leclerc', rating: 3950, skillLevel: 'pro', nationality: 'MC', wins: 28, podiums: 67, rank: 3 },
{ id: 'demo-4', name: 'Lando Norris', rating: 3820, skillLevel: 'advanced', nationality: 'GB', wins: 18, podiums: 45, rank: 4 },
{ id: 'demo-5', name: 'Carlos Sainz', rating: 3750, skillLevel: 'advanced', nationality: 'ES', wins: 15, podiums: 52, rank: 5 },
{ id: 'demo-6', name: 'Oscar Piastri', rating: 3680, skillLevel: 'advanced', nationality: 'AU', wins: 8, podiums: 24, rank: 6 },
{ id: 'demo-7', name: 'George Russell', rating: 3620, skillLevel: 'advanced', nationality: 'GB', wins: 6, podiums: 31, rank: 7 },
{ id: 'demo-8', name: 'Fernando Alonso', rating: 3580, skillLevel: 'advanced', nationality: 'ES', wins: 32, podiums: 98, rank: 8 },
{ id: 'demo-9', name: 'Nico Hülkenberg', rating: 3420, skillLevel: 'advanced', nationality: 'DE', wins: 2, podiums: 18, rank: 9 },
{ id: 'demo-10', name: 'Yuki Tsunoda', rating: 3250, skillLevel: 'intermediate', nationality: 'JP', wins: 1, podiums: 8, rank: 10 },
];
const DEMO_TEAMS: TeamDisplayData[] = [
{ id: 'demo-team-1', name: 'Apex Predators Racing', memberCount: 8, rating: 4850, totalWins: 47, totalRaces: 156, performanceLevel: 'pro' },
{ id: 'demo-team-2', name: 'Velocity Esports', memberCount: 12, rating: 5200, totalWins: 63, totalRaces: 198, performanceLevel: 'pro' },
{ id: 'demo-team-3', name: 'Nitro Motorsport', memberCount: 6, rating: 4720, totalWins: 38, totalRaces: 112, performanceLevel: 'pro' },
{ id: 'demo-team-4', name: 'Horizon Racing Collective', memberCount: 10, rating: 3800, totalWins: 24, totalRaces: 89, performanceLevel: 'advanced' },
{ id: 'demo-team-5', name: 'Phoenix Rising eSports', memberCount: 7, rating: 3650, totalWins: 19, totalRaces: 76, performanceLevel: 'advanced' },
];
// ============================================================================
// SKILL LEVEL CONFIG
// ============================================================================
@@ -420,12 +395,12 @@ export default function LeaderboardsPage() {
}),
);
setDrivers(driverItems.length > 0 ? driverItems : DEMO_DRIVERS);
setTeams(teamData.length > 0 ? teamData : DEMO_TEAMS);
setDrivers(driverItems);
setTeams(teamData);
} catch (error) {
console.error('Failed to load leaderboard data:', error);
setDrivers(DEMO_DRIVERS);
setTeams(DEMO_TEAMS);
setDrivers([]);
setTeams([]);
} finally {
setLoading(false);
}
@@ -435,12 +410,10 @@ export default function LeaderboardsPage() {
}, []);
const handleDriverClick = (driverId: string) => {
if (driverId.startsWith('demo-')) return;
router.push(`/drivers/${driverId}`);
};
const handleTeamClick = (teamId: string) => {
if (teamId.startsWith('demo-team-')) return;
router.push(`/teams/${teamId}`);
};

View File

@@ -48,223 +48,6 @@ interface TeamDisplayData {
languages?: string[];
}
// ============================================================================
// DEMO TEAMS DATA
// ============================================================================
const DEMO_TEAMS: TeamDisplayData[] = [
{
id: 'demo-team-1',
name: 'Apex Predators Racing',
description: 'Elite GT3 team competing at the highest level.',
memberCount: 8,
rating: 4850,
totalWins: 47,
totalRaces: 156,
performanceLevel: 'pro',
isRecruiting: true,
createdAt: new Date(Date.now() - 180 * 24 * 60 * 60 * 1000),
specialization: 'mixed',
region: '🇺🇸 North America',
languages: ['English'],
},
{
id: 'demo-team-2',
name: 'Velocity Esports',
description: 'Professional sim racing team with sponsors.',
memberCount: 12,
rating: 5200,
totalWins: 63,
totalRaces: 198,
performanceLevel: 'pro',
isRecruiting: false,
createdAt: new Date(Date.now() - 365 * 24 * 60 * 60 * 1000),
specialization: 'endurance',
region: '🇬🇧 Europe',
languages: ['English', 'German'],
},
{
id: 'demo-team-3',
name: 'Nitro Motorsport',
description: 'Championship-winning sprint specialists.',
memberCount: 6,
rating: 4720,
totalWins: 38,
totalRaces: 112,
performanceLevel: 'pro',
isRecruiting: true,
createdAt: new Date(Date.now() - 90 * 24 * 60 * 60 * 1000),
specialization: 'sprint',
region: '🇩🇪 Germany',
languages: ['German', 'English'],
},
{
id: 'demo-team-4',
name: 'Horizon Racing Collective',
description: 'Ambitious team on the rise.',
memberCount: 10,
rating: 3800,
totalWins: 24,
totalRaces: 89,
performanceLevel: 'advanced',
isRecruiting: true,
createdAt: new Date(Date.now() - 60 * 24 * 60 * 60 * 1000),
specialization: 'mixed',
region: '🇳🇱 Netherlands',
languages: ['Dutch', 'English'],
},
{
id: 'demo-team-5',
name: 'Phoenix Rising eSports',
description: 'From the ashes to the podium.',
memberCount: 7,
rating: 3650,
totalWins: 19,
totalRaces: 76,
performanceLevel: 'advanced',
isRecruiting: true,
createdAt: new Date(Date.now() - 45 * 24 * 60 * 60 * 1000),
specialization: 'endurance',
region: '🇫🇷 France',
languages: ['French', 'English'],
},
{
id: 'demo-team-6',
name: 'Thunderbolt Racing',
description: 'Fast and furious sprint racing.',
memberCount: 5,
rating: 3420,
totalWins: 15,
totalRaces: 54,
performanceLevel: 'advanced',
isRecruiting: false,
createdAt: new Date(Date.now() - 120 * 24 * 60 * 60 * 1000),
specialization: 'sprint',
region: '🇮🇹 Italy',
languages: ['Italian', 'English'],
},
{
id: 'demo-team-7',
name: 'Grid Starters',
description: 'Growing together as racers.',
memberCount: 9,
rating: 2800,
totalWins: 11,
totalRaces: 67,
performanceLevel: 'intermediate',
isRecruiting: true,
createdAt: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000),
specialization: 'mixed',
region: '🇪🇸 Spain',
languages: ['Spanish', 'English'],
},
{
id: 'demo-team-8',
name: 'Midnight Racers',
description: 'Night owls who love endurance racing.',
memberCount: 6,
rating: 2650,
totalWins: 8,
totalRaces: 42,
performanceLevel: 'intermediate',
isRecruiting: true,
createdAt: new Date(Date.now() - 15 * 24 * 60 * 60 * 1000),
specialization: 'endurance',
region: '🌍 International',
languages: ['English'],
},
{
id: 'demo-team-9',
name: 'Casual Speedsters',
description: 'Racing for fun, improving together.',
memberCount: 4,
rating: 2400,
totalWins: 5,
totalRaces: 31,
performanceLevel: 'intermediate',
isRecruiting: true,
createdAt: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000),
specialization: 'sprint',
region: '🇵🇱 Poland',
languages: ['Polish', 'English'],
},
{
id: 'demo-team-10',
name: 'Fresh Rubber Racing',
description: 'New team for new racers!',
memberCount: 3,
rating: 1800,
totalWins: 2,
totalRaces: 18,
performanceLevel: 'beginner',
isRecruiting: true,
createdAt: new Date(Date.now() - 3 * 24 * 60 * 60 * 1000),
specialization: 'mixed',
region: '🇧🇷 Brazil',
languages: ['Portuguese', 'English'],
},
{
id: 'demo-team-11',
name: 'Rookie Revolution',
description: 'First time racers welcome!',
memberCount: 5,
rating: 1650,
totalWins: 1,
totalRaces: 12,
performanceLevel: 'beginner',
isRecruiting: true,
createdAt: new Date(Date.now() - 5 * 24 * 60 * 60 * 1000),
specialization: 'sprint',
region: '🇦🇺 Australia',
languages: ['English'],
},
{
id: 'demo-team-12',
name: 'Pit Lane Pioneers',
description: 'Learning endurance racing from scratch.',
memberCount: 4,
rating: 1500,
totalWins: 0,
totalRaces: 8,
performanceLevel: 'beginner',
isRecruiting: true,
createdAt: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000),
specialization: 'endurance',
region: '🇯🇵 Japan',
languages: ['Japanese', 'English'],
},
{
id: 'demo-team-13',
name: 'Shadow Squadron',
description: 'Elite drivers emerging from the shadows.',
memberCount: 6,
rating: 4100,
totalWins: 12,
totalRaces: 34,
performanceLevel: 'advanced',
isRecruiting: true,
createdAt: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000),
specialization: 'mixed',
region: '🇸🇪 Scandinavia',
languages: ['Swedish', 'Norwegian', 'English'],
},
{
id: 'demo-team-14',
name: 'Turbo Collective',
description: 'Fast, furious, and friendly.',
memberCount: 4,
rating: 3200,
totalWins: 7,
totalRaces: 28,
performanceLevel: 'intermediate',
isRecruiting: true,
createdAt: new Date(Date.now() - 12 * 60 * 60 * 1000),
specialization: 'sprint',
region: '🇨🇦 Canada',
languages: ['English', 'French'],
},
];
// ============================================================================
// SKILL LEVEL CONFIG
// ============================================================================
@@ -537,7 +320,7 @@ export default function TeamLeaderboardPage() {
}
};
const teams = [...realTeams, ...DEMO_TEAMS];
const teams = realTeams;
const handleTeamClick = (teamId: string) => {
if (teamId.startsWith('demo-team-')) {

View File

@@ -53,228 +53,6 @@ interface TeamDisplayData {
languages?: string[];
}
// ============================================================================
// DEMO TEAMS DATA
// ============================================================================
const DEMO_TEAMS: TeamDisplayData[] = [
// Pro Teams
{
id: 'demo-team-1',
name: 'Apex Predators Racing',
description: 'Elite GT3 team competing at the highest level. Multiple championship winners seeking consistent drivers.',
memberCount: 8,
rating: 4850,
totalWins: 47,
totalRaces: 156,
performanceLevel: 'pro',
isRecruiting: true,
createdAt: new Date(Date.now() - 180 * 24 * 60 * 60 * 1000),
specialization: 'mixed',
region: '🇺🇸 North America',
languages: ['English'],
},
{
id: 'demo-team-2',
name: 'Velocity Esports',
description: 'Professional sim racing team with sponsors. Competing in major endurance events worldwide.',
memberCount: 12,
rating: 5200,
totalWins: 63,
totalRaces: 198,
performanceLevel: 'pro',
isRecruiting: false,
createdAt: new Date(Date.now() - 365 * 24 * 60 * 60 * 1000),
specialization: 'endurance',
region: '🇬🇧 Europe',
languages: ['English', 'German'],
},
{
id: 'demo-team-3',
name: 'Nitro Motorsport',
description: 'Championship-winning sprint specialists. Fast, consistent, and always fighting for podiums.',
memberCount: 6,
rating: 4720,
totalWins: 38,
totalRaces: 112,
performanceLevel: 'pro',
isRecruiting: true,
createdAt: new Date(Date.now() - 90 * 24 * 60 * 60 * 1000),
specialization: 'sprint',
region: '🇩🇪 Germany',
languages: ['German', 'English'],
},
// Advanced Teams
{
id: 'demo-team-4',
name: 'Horizon Racing Collective',
description: 'Ambitious team on the rise. Building towards professional competition with dedicated drivers.',
memberCount: 10,
rating: 3800,
totalWins: 24,
totalRaces: 89,
performanceLevel: 'advanced',
isRecruiting: true,
createdAt: new Date(Date.now() - 60 * 24 * 60 * 60 * 1000),
specialization: 'mixed',
region: '🇳🇱 Netherlands',
languages: ['Dutch', 'English'],
},
{
id: 'demo-team-5',
name: 'Phoenix Rising eSports',
description: 'From the ashes to the podium. A team built on improvement and teamwork.',
memberCount: 7,
rating: 3650,
totalWins: 19,
totalRaces: 76,
performanceLevel: 'advanced',
isRecruiting: true,
createdAt: new Date(Date.now() - 45 * 24 * 60 * 60 * 1000),
specialization: 'endurance',
region: '🇫🇷 France',
languages: ['French', 'English'],
},
{
id: 'demo-team-6',
name: 'Thunderbolt Racing',
description: 'Fast and furious sprint racing. We live for wheel-to-wheel battles.',
memberCount: 5,
rating: 3420,
totalWins: 15,
totalRaces: 54,
performanceLevel: 'advanced',
isRecruiting: false,
createdAt: new Date(Date.now() - 120 * 24 * 60 * 60 * 1000),
specialization: 'sprint',
region: '🇮🇹 Italy',
languages: ['Italian', 'English'],
},
// Intermediate Teams
{
id: 'demo-team-7',
name: 'Grid Starters',
description: 'Growing together as racers. Friendly competition with a focus on learning and fun.',
memberCount: 9,
rating: 2800,
totalWins: 11,
totalRaces: 67,
performanceLevel: 'intermediate',
isRecruiting: true,
createdAt: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000),
specialization: 'mixed',
region: '🇪🇸 Spain',
languages: ['Spanish', 'English'],
},
{
id: 'demo-team-8',
name: 'Midnight Racers',
description: 'Night owls who love endurance racing. Join us for late-night stints and good vibes.',
memberCount: 6,
rating: 2650,
totalWins: 8,
totalRaces: 42,
performanceLevel: 'intermediate',
isRecruiting: true,
createdAt: new Date(Date.now() - 15 * 24 * 60 * 60 * 1000),
specialization: 'endurance',
region: '🌍 International',
languages: ['English'],
},
{
id: 'demo-team-9',
name: 'Casual Speedsters',
description: 'Racing for fun, improving together. No pressure, just clean racing.',
memberCount: 4,
rating: 2400,
totalWins: 5,
totalRaces: 31,
performanceLevel: 'intermediate',
isRecruiting: true,
createdAt: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000),
specialization: 'sprint',
region: '🇵🇱 Poland',
languages: ['Polish', 'English'],
},
// Beginner Teams
{
id: 'demo-team-10',
name: 'Fresh Rubber Racing',
description: 'New team for new racers! Learn the basics together in a supportive environment.',
memberCount: 3,
rating: 1800,
totalWins: 2,
totalRaces: 18,
performanceLevel: 'beginner',
isRecruiting: true,
createdAt: new Date(Date.now() - 3 * 24 * 60 * 60 * 1000),
specialization: 'mixed',
region: '🇧🇷 Brazil',
languages: ['Portuguese', 'English'],
},
{
id: 'demo-team-11',
name: 'Rookie Revolution',
description: 'First time racers welcome! We all start somewhere.',
memberCount: 5,
rating: 1650,
totalWins: 1,
totalRaces: 12,
performanceLevel: 'beginner',
isRecruiting: true,
createdAt: new Date(Date.now() - 5 * 24 * 60 * 60 * 1000),
specialization: 'sprint',
region: '🇦🇺 Australia',
languages: ['English'],
},
{
id: 'demo-team-12',
name: 'Pit Lane Pioneers',
description: 'Learning endurance racing from scratch. Long races, longer friendships.',
memberCount: 4,
rating: 1500,
totalWins: 0,
totalRaces: 8,
performanceLevel: 'beginner',
isRecruiting: true,
createdAt: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000),
specialization: 'endurance',
region: '🇯🇵 Japan',
languages: ['Japanese', 'English'],
},
// Recently Added
{
id: 'demo-team-13',
name: 'Shadow Squadron',
description: 'Elite drivers emerging from the shadows. Watch out for us this season.',
memberCount: 6,
rating: 4100,
totalWins: 12,
totalRaces: 34,
performanceLevel: 'advanced',
isRecruiting: true,
createdAt: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000),
specialization: 'mixed',
region: '🇸🇪 Scandinavia',
languages: ['Swedish', 'Norwegian', 'English'],
},
{
id: 'demo-team-14',
name: 'Turbo Collective',
description: 'Fast, furious, and friendly. Sprint racing specialists looking for quick racers.',
memberCount: 4,
rating: 3200,
totalWins: 7,
totalRaces: 28,
performanceLevel: 'intermediate',
isRecruiting: true,
createdAt: new Date(Date.now() - 12 * 60 * 60 * 1000),
specialization: 'sprint',
region: '🇨🇦 Canada',
languages: ['English', 'French'],
},
];
// ============================================================================
// SKILL LEVEL CONFIG
// ============================================================================
@@ -745,7 +523,7 @@ export default function TeamsPage() {
}
};
const teams = [...realTeams, ...DEMO_TEAMS];
const teams = realTeams;
const handleTeamClick = (teamId: string) => {
if (teamId.startsWith('demo-team-')) {

View File

@@ -3,7 +3,7 @@
import { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { useEffectiveDriverId } from '@/lib/currentDriver';
import { getSendNotificationUseCase } from '@/lib/di-container';
import { getSendNotificationUseCase, getRaceRepository, getLeagueRepository } from '@/lib/di-container';
import type { NotificationUrgency } from '@gridpilot/notifications/application';
import {
Bell,
@@ -170,38 +170,64 @@ export default function DevToolbar() {
setSending(true);
try {
const sendNotification = getSendNotificationUseCase();
const raceRepository = getRaceRepository();
const leagueRepository = getLeagueRepository();
const [allRaces, allLeagues] = await Promise.all([
raceRepository.findAll(),
leagueRepository.findAll(),
]);
const completedRaces = allRaces.filter((race: any) => race.status === 'completed');
const scheduledRaces = allRaces.filter((race: any) => race.status === 'scheduled');
const primaryRace = completedRaces[0] ?? allRaces[0];
const secondaryRace = scheduledRaces[0] ?? allRaces[1] ?? primaryRace;
const primaryLeague = allLeagues[0];
let title: string;
let body: string;
let notificationType: 'protest_filed' | 'protest_defense_requested' | 'protest_vote_required';
let actionUrl: string;
switch (selectedType) {
case 'protest_filed':
case 'protest_filed': {
const raceId = primaryRace?.id;
title = '🚨 Protest Filed Against You';
body = 'Max Verstappen has filed a protest against you for unsafe rejoining at Turn 3, Lap 12 during the Spa-Francorchamps race.';
body =
'A protest has been filed against you for unsafe rejoining during a recent race. Please review the incident details.';
notificationType = 'protest_filed';
actionUrl = '/races/race-1/stewarding';
actionUrl = raceId ? `/races/${raceId}/stewarding` : '/races';
break;
case 'defense_requested':
}
case 'defense_requested': {
const raceId = secondaryRace?.id ?? primaryRace?.id;
title = '⚖️ Defense Requested';
body = 'A steward has requested your defense regarding the incident at Turn 1 in the Monza race. Please provide your side of the story within 48 hours.';
body =
'A steward has requested your defense regarding a recent incident. Please provide your side of the story within 48 hours.';
notificationType = 'protest_defense_requested';
actionUrl = '/races/race-2/stewarding';
actionUrl = raceId ? `/races/${raceId}/stewarding` : '/races';
break;
case 'vote_required':
}
case 'vote_required': {
const leagueId = primaryLeague?.id;
title = '🗳️ Your Vote Required';
body = 'As a league steward, you are required to vote on the protest: Driver A vs Driver B - Causing a collision at Eau Rouge.';
body =
'As a league steward, you are required to vote on an open protest. Please review the case and cast your vote.';
notificationType = 'protest_vote_required';
actionUrl = '/leagues/league-1/stewarding';
actionUrl = leagueId ? `/leagues/${leagueId}/stewarding` : '/leagues';
break;
}
}
// For modal urgency, add actions
const actions = selectedUrgency === 'modal' ? [
{ label: 'View Protest', type: 'primary' as const, href: actionUrl, actionId: 'view' },
{ label: 'Dismiss', type: 'secondary' as const, actionId: 'dismiss' },
] : undefined;
const actions =
selectedUrgency === 'modal'
? [
{ label: 'View Protest', type: 'primary' as const, href: actionUrl, actionId: 'view' },
{ label: 'Dismiss', type: 'secondary' as const, actionId: 'dismiss' },
]
: undefined;
await sendNotification.execute({
recipientId: currentDriverId,
@@ -214,9 +240,12 @@ export default function DevToolbar() {
actions,
data: {
protestId: `demo-protest-${Date.now()}`,
raceId: 'race-1',
leagueId: 'league-1',
deadline: selectedUrgency === 'modal' ? new Date(Date.now() + 48 * 60 * 60 * 1000) : undefined,
raceId: primaryRace?.id,
leagueId: primaryLeague?.id,
deadline:
selectedUrgency === 'modal'
? new Date(Date.now() + 48 * 60 * 60 * 1000)
: undefined,
},
});

View File

@@ -9,6 +9,7 @@ import DriverRankings from './DriverRankings';
import PerformanceMetrics from './PerformanceMetrics';
import { useEffect, useState } from 'react';
import { getDriverStats, getLeagueRankings, getGetDriverTeamQuery, getAllDriverRankings } from '@/lib/di-container';
import { getPrimaryLeagueIdForDriver } from '@/lib/leagueMembership';
import type { GetDriverTeamQueryResultDTO } from '@gridpilot/racing/application/dto/TeamCommandAndQueryDTO';
interface DriverProfileProps {
@@ -19,7 +20,10 @@ interface DriverProfileProps {
export default function DriverProfile({ driver, isOwnProfile = false, onEditClick }: DriverProfileProps) {
const driverStats = getDriverStats(driver.id);
const leagueRank = getLeagueRankings(driver.id, 'league-1');
const primaryLeagueId = getPrimaryLeagueIdForDriver(driver.id);
const leagueRank = primaryLeagueId
? getLeagueRankings(driver.id, primaryLeagueId)
: { rank: 0, totalDrivers: 0, percentile: 0 };
const allRankings = getAllDriverRankings();
const [teamData, setTeamData] = useState<GetDriverTeamQueryResultDTO | null>(null);
@@ -53,7 +57,7 @@ export default function DriverProfile({ driver, isOwnProfile = false, onEditClic
},
{
type: 'league' as const,
name: 'European GT Championship',
name: 'Primary League',
rank: leagueRank.rank,
totalDrivers: leagueRank.totalDrivers,
percentile: leagueRank.percentile,

View File

@@ -3,6 +3,7 @@
import Card from '../ui/Card';
import RankBadge from './RankBadge';
import { getDriverStats, getAllDriverRankings, getLeagueRankings } from '@/lib/di-container';
import { getPrimaryLeagueIdForDriver } from '@/lib/leagueMembership';
interface ProfileStatsProps {
driverId?: string;
@@ -19,7 +20,9 @@ interface ProfileStatsProps {
export default function ProfileStats({ driverId, stats }: ProfileStatsProps) {
const driverStats = driverId ? getDriverStats(driverId) : null;
const allRankings = getAllDriverRankings();
const leagueRank = driverId ? getLeagueRankings(driverId, 'league-1') : null;
const primaryLeagueId = driverId ? getPrimaryLeagueIdForDriver(driverId) : null;
const leagueRank =
driverId && primaryLeagueId ? getLeagueRankings(driverId, primaryLeagueId) : null;
const defaultStats = stats || (driverStats
? {
@@ -115,7 +118,7 @@ export default function ProfileStats({ driverId, stats }: ProfileStatsProps) {
<div className="flex items-center gap-3">
<RankBadge rank={leagueRank.rank} size="md" />
<div>
<div className="text-white font-medium">European GT Championship</div>
<div className="text-white font-medium">Primary League</div>
<div className="text-sm text-gray-400">
{leagueRank.rank} of {leagueRank.totalDrivers} drivers
</div>

View File

@@ -2,7 +2,7 @@ import Card from '@/components/ui/Card';
import Button from '@/components/ui/Button';
import Image from 'next/image';
import type { FeedItem } from '@gridpilot/social/domain/entities/FeedItem';
import { friends } from '@gridpilot/testing-support';
import { getDriverRepository, getImageService, getSocialRepository } from '@/lib/di-container';
function timeAgo(timestamp: Date): string {
const diffMs = Date.now() - timestamp.getTime();
@@ -15,16 +15,39 @@ function timeAgo(timestamp: Date): string {
return `${diffDays} d ago`;
}
function getActor(item: FeedItem) {
async function resolveActor(item: FeedItem) {
const driverRepo = getDriverRepository();
const imageService = getImageService();
const socialRepo = getSocialRepository();
if (item.actorFriendId) {
const friend = friends.find(f => f.driverId === item.actorFriendId);
if (friend) {
return {
name: friend.displayName,
avatarUrl: friend.avatarUrl
};
// Try social graph first (friend display name/avatar)
try {
const friend = await socialRepo.getFriendByDriverId?.(item.actorFriendId);
if (friend) {
return {
name: friend.displayName ?? friend.driverName ?? `Driver ${item.actorFriendId}`,
avatarUrl: friend.avatarUrl ?? imageService.getDriverAvatar(item.actorFriendId),
};
}
} catch {
// fall through to driver lookup
}
// Fallback to driver entity + image service
try {
const driver = await driverRepo.findById(item.actorFriendId);
if (driver) {
return {
name: driver.name,
avatarUrl: imageService.getDriverAvatar(driver.id),
};
}
} catch {
// ignore and return null below
}
}
return null;
}
@@ -33,7 +56,22 @@ interface FeedItemCardProps {
}
export default function FeedItemCard({ item }: FeedItemCardProps) {
const actor = getActor(item);
const [actor, setActor] = useState<{ name: string; avatarUrl: string } | null>(null);
useEffect(() => {
let cancelled = false;
void (async () => {
const resolved = await resolveActor(item);
if (!cancelled) {
setActor(resolved);
}
})();
return () => {
cancelled = true;
};
}, [item]);
return (
<div className="flex gap-4">
@@ -68,7 +106,7 @@ export default function FeedItemCard({ item }: FeedItemCardProps) {
{timeAgo(item.timestamp)}
</span>
</div>
{(item.ctaHref && item.ctaLabel) && (
{item.ctaHref && item.ctaLabel && (
<div className="mt-3">
<Button
as="a"

View File

@@ -16,5 +16,25 @@ export function useEffectiveDriverId(): string {
}
| undefined;
return user?.primaryDriverId ?? 'driver-1';
// In alpha mode, if the user has no bound driver yet, fall back to the
// first seeded driver from the in-memory repository instead of a hardcoded ID.
if (user?.primaryDriverId) {
return user.primaryDriverId;
}
try {
// Lazy-load to avoid importing DI facade at module evaluation time
const { getDriverRepository } = require('./di-container') as typeof import('./di-container');
const repo = getDriverRepository();
// In-memory repository is synchronous for findAll in the demo implementation
const allDrivers = repo.findAllSync?.() as Array<{ id: string }> | undefined;
if (allDrivers && allDrivers.length > 0) {
return allDrivers[0].id;
}
} catch {
// Ignore and fall back to legacy default below
}
// Legacy fallback: preserved only as a last resort for demo
return '';
}

View File

@@ -149,7 +149,7 @@ export function configureDIContainer(): void {
// Create seed data
const seedData = createStaticRacingSeed(42);
const primaryDriverId = seedData.drivers[0]?.id ?? 'driver-1';
const primaryDriverId = seedData.drivers[0]!.id;
// Create driver statistics from seed data
const driverStats = createDemoDriverStats(seedData.drivers);
@@ -556,7 +556,7 @@ export function configureDIContainer(): void {
name: t.name,
tag: t.tag,
description: t.description,
ownerId: seedData.drivers[0]?.id ?? 'driver-1',
ownerId: seedData.drivers[0]!.id,
leagues: [t.primaryLeagueId],
createdAt: new Date(),
}))
@@ -640,13 +640,8 @@ export function configureDIContainer(): void {
);
const sponsorshipPricingRepo = new InMemorySponsorshipPricingRepository();
// Seed sponsorship pricings from demo data
seedData.sponsorshipPricings?.forEach(pricing => {
(sponsorshipPricingRepo as any).pricings.set(
`${pricing.entityType}-${pricing.entityId}`,
pricing
);
});
// Seed sponsorship pricings from demo data using domain SponsorshipPricing
sponsorshipPricingRepo.seed(seedData.sponsorshipPricings ?? []);
container.registerInstance<ISponsorshipPricingRepository>(
DI_TOKENS.SponsorshipPricingRepository,
sponsorshipPricingRepo

View File

@@ -5,7 +5,6 @@ import type {
MembershipRole,
MembershipStatus,
} from '@gridpilot/racing/domain/entities/LeagueMembership';
import { leagues, memberships as seedMemberships, drivers } from '@gridpilot/testing-support';
/**
* Lightweight league membership model mirroring the domain type but with
@@ -18,112 +17,49 @@ export interface LeagueMembership extends Omit<DomainLeagueMembership, 'joinedAt
const leagueMemberships = new Map<string, LeagueMembership[]>();
/**
* Initialize league memberships once from static seed data.
* Initialize league memberships once from the in-memory league membership repository
* that is seeded via the static racing seed in the DI container.
*
* - All seeded memberships become active members.
* - League owners are guaranteed to have an owner membership.
* This avoids depending on raw testing-support seed exports and keeps all demo
* membership data flowing through the same in-memory repositories used elsewhere.
*/
(function initializeLeagueMembershipsFromSeed() {
(async function initializeLeagueMembershipsFromRepository() {
if (leagueMemberships.size > 0) {
return;
}
const byLeague = new Map<string, LeagueMembership[]>();
try {
const { getLeagueRepository, getLeagueMembershipRepository } = await import('./di-container');
const leagueRepo = getLeagueRepository();
const membershipRepo = getLeagueMembershipRepository();
for (const membership of seedMemberships) {
const list = byLeague.get(membership.leagueId) ?? [];
const joinedAt = new Date().toISOString();
const allLeagues = await leagueRepo.findAll();
const byLeague = new Map<string, LeagueMembership[]>();
list.push({
leagueId: membership.leagueId,
driverId: membership.driverId,
role: 'member',
status: 'active',
joinedAt,
});
for (const league of allLeagues) {
const memberships = await membershipRepo.getLeagueMembers(league.id);
byLeague.set(membership.leagueId, list);
}
const mapped: LeagueMembership[] = memberships.map((membership) => ({
leagueId: membership.leagueId,
driverId: membership.driverId,
role: membership.role,
status: membership.status,
joinedAt:
membership.joinedAt instanceof Date
? membership.joinedAt.toISOString()
: new Date().toISOString(),
}));
for (const league of leagues) {
const list = byLeague.get(league.id) ?? [];
const existingOwner = list.find((m) => m.driverId === league.ownerId);
if (existingOwner) {
existingOwner.role = 'owner';
} else {
const joinedAt = new Date().toISOString();
list.unshift({
leagueId: league.id,
driverId: league.ownerId,
role: 'owner',
status: 'active',
joinedAt,
});
byLeague.set(league.id, mapped);
}
byLeague.set(league.id, list);
}
// Seed sample league admins for the primary driver's league (alpha demo)
const primaryDriverId = drivers[0]?.id ?? 'driver-1';
const primaryLeagueForAdmins = leagues.find((l) => l.ownerId === primaryDriverId) ?? leagues[0];
if (primaryLeagueForAdmins) {
const adminCandidates = drivers
.filter((d) => d.id !== primaryLeagueForAdmins.ownerId)
.slice(0, 2);
adminCandidates.forEach((driver) => {
const list = byLeague.get(primaryLeagueForAdmins.id) ?? [];
const existing = list.find((m) => m.driverId === driver.id);
if (existing) {
if (existing.role !== 'owner') {
existing.role = 'admin';
}
} else {
const joinedAt = new Date().toISOString();
list.push({
leagueId: primaryLeagueForAdmins.id,
driverId: driver.id,
role: 'admin',
status: 'active',
joinedAt,
});
}
byLeague.set(primaryLeagueForAdmins.id, list);
});
}
// Seed sample league stewards for the primary driver's league (alpha demo)
if (primaryLeagueForAdmins) {
const stewardCandidates = drivers
.filter((d) => d.id !== primaryLeagueForAdmins.ownerId)
.slice(2, 5);
stewardCandidates.forEach((driver) => {
const list = byLeague.get(primaryLeagueForAdmins.id) ?? [];
const existing = list.find((m) => m.driverId === driver.id);
if (existing) {
if (existing.role !== 'owner' && existing.role !== 'admin') {
existing.role = 'steward';
}
} else {
const joinedAt = new Date().toISOString();
list.push({
leagueId: primaryLeagueForAdmins.id,
driverId: driver.id,
role: 'steward',
status: 'active',
joinedAt,
});
}
byLeague.set(primaryLeagueForAdmins.id, list);
});
}
for (const [leagueId, list] of byLeague.entries()) {
leagueMemberships.set(leagueId, list);
for (const [leagueId, list] of byLeague.entries()) {
leagueMemberships.set(leagueId, list);
}
} catch (error) {
// In alpha/demo mode we tolerate failures here; callers will see empty memberships.
// eslint-disable-next-line no-console
console.error('Failed to initialize league memberships from repository', error);
}
})();
@@ -137,6 +73,19 @@ export function getLeagueMembers(leagueId: string): LeagueMembership[] {
return [...(leagueMemberships.get(leagueId) ?? [])];
}
/**
* Derive a driver's primary league from in-memory league memberships.
* Prefers any active membership and returns the first matching league.
*/
export function getPrimaryLeagueIdForDriver(driverId: string): string | null {
for (const [leagueId, members] of leagueMemberships.entries()) {
if (members.some((m) => m.driverId === driverId && m.status === 'active')) {
return leagueId;
}
}
return null;
}
export function isOwnerOrAdmin(leagueId: string, driverId: string): boolean {
const membership = getMembership(leagueId, driverId);
if (!membership) return false;

View File

@@ -1,4 +1,9 @@
export * from './src/faker/faker';
export * from './src/images/images';
export * from './src/racing/StaticRacingSeed';
export * from './src/racing/DemoData';
export * from './src/racing/RacingSeedCore';
export * from './src/racing/RacingSponsorshipSeed';
export * from './src/racing/RacingFeedSeed';
export * from './src/racing/RacingStaticSeed';
export * from './src/racing/DemoTracks';
export * from './src/racing/DemoCars';
export * from './src/racing/DemoDriverStats';

View File

@@ -0,0 +1,93 @@
import { Car } from '@gridpilot/racing/domain/entities/Car';
/**
* Demo car data for iRacing.
* Extracted from the legacy DemoData module so that cars
* live in their own focused file.
*/
export const DEMO_CARS: Car[] = [
Car.create({
id: 'car-porsche-992',
name: '911 GT3 R',
shortName: '992 GT3R',
manufacturer: 'Porsche',
carClass: 'gt',
license: 'B',
year: 2023,
horsepower: 565,
weight: 1300,
gameId: 'iracing',
}),
Car.create({
id: 'car-ferrari-296',
name: '296 GT3',
shortName: '296 GT3',
manufacturer: 'Ferrari',
carClass: 'gt',
license: 'B',
year: 2023,
horsepower: 600,
weight: 1270,
gameId: 'iracing',
}),
Car.create({
id: 'car-mclaren-720s',
name: '720S GT3 Evo',
shortName: '720S',
manufacturer: 'McLaren',
carClass: 'gt',
license: 'B',
year: 2023,
horsepower: 552,
weight: 1290,
gameId: 'iracing',
}),
Car.create({
id: 'car-mercedes-gt3',
name: 'AMG GT3 2020',
shortName: 'AMG GT3',
manufacturer: 'Mercedes',
carClass: 'gt',
license: 'B',
year: 2020,
horsepower: 550,
weight: 1285,
gameId: 'iracing',
}),
Car.create({
id: 'car-lmp2',
name: 'Dallara P217 LMP2',
shortName: 'LMP2',
manufacturer: 'Dallara',
carClass: 'prototype',
license: 'A',
year: 2021,
horsepower: 600,
weight: 930,
gameId: 'iracing',
}),
Car.create({
id: 'car-f4',
name: 'Formula 4',
shortName: 'F4',
manufacturer: 'Tatuus',
carClass: 'formula',
license: 'D',
year: 2022,
horsepower: 160,
weight: 570,
gameId: 'iracing',
}),
Car.create({
id: 'car-mx5',
name: 'MX-5 Cup',
shortName: 'MX5',
manufacturer: 'Mazda',
carClass: 'sports',
license: 'D',
year: 2023,
horsepower: 181,
weight: 1128,
gameId: 'iracing',
}),
];

View File

@@ -1,253 +0,0 @@
import { Track } from '@gridpilot/racing/domain/entities/Track';
import { Car } from '@gridpilot/racing/domain/entities/Car';
/**
* Driver statistics and ranking data
*/
export interface DriverStats {
driverId: string;
rating: number;
totalRaces: number;
wins: number;
podiums: number;
dnfs: number;
avgFinish: number;
bestFinish: number;
worstFinish: number;
consistency: number;
overallRank: number;
percentile: number;
}
/**
* Demo track data for iRacing
*/
export const DEMO_TRACKS: Track[] = [
Track.create({
id: 'track-spa',
name: 'Spa-Francorchamps',
shortName: 'SPA',
country: 'Belgium',
category: 'road',
difficulty: 'advanced',
lengthKm: 7.004,
turns: 19,
imageUrl: '/images/tracks/spa.jpg',
gameId: 'iracing',
}),
Track.create({
id: 'track-monza',
name: 'Autodromo Nazionale Monza',
shortName: 'MON',
country: 'Italy',
category: 'road',
difficulty: 'intermediate',
lengthKm: 5.793,
turns: 11,
imageUrl: '/images/tracks/monza.jpg',
gameId: 'iracing',
}),
Track.create({
id: 'track-nurburgring',
name: 'Nürburgring Grand Prix',
shortName: 'NUR',
country: 'Germany',
category: 'road',
difficulty: 'advanced',
lengthKm: 5.148,
turns: 15,
imageUrl: '/images/tracks/nurburgring.jpg',
gameId: 'iracing',
}),
Track.create({
id: 'track-silverstone',
name: 'Silverstone Circuit',
shortName: 'SIL',
country: 'United Kingdom',
category: 'road',
difficulty: 'intermediate',
lengthKm: 5.891,
turns: 18,
imageUrl: '/images/tracks/silverstone.jpg',
gameId: 'iracing',
}),
Track.create({
id: 'track-suzuka',
name: 'Suzuka International Racing Course',
shortName: 'SUZ',
country: 'Japan',
category: 'road',
difficulty: 'expert',
lengthKm: 5.807,
turns: 18,
imageUrl: '/images/tracks/suzuka.jpg',
gameId: 'iracing',
}),
Track.create({
id: 'track-daytona',
name: 'Daytona International Speedway',
shortName: 'DAY',
country: 'United States',
category: 'oval',
difficulty: 'intermediate',
lengthKm: 4.023,
turns: 4,
imageUrl: '/images/tracks/daytona.jpg',
gameId: 'iracing',
}),
Track.create({
id: 'track-laguna',
name: 'WeatherTech Raceway Laguna Seca',
shortName: 'LAG',
country: 'United States',
category: 'road',
difficulty: 'advanced',
lengthKm: 3.602,
turns: 11,
imageUrl: '/images/tracks/laguna.jpg',
gameId: 'iracing',
}),
];
/**
* Demo car data for iRacing
*/
export const DEMO_CARS: Car[] = [
Car.create({
id: 'car-porsche-992',
name: '911 GT3 R',
shortName: '992 GT3R',
manufacturer: 'Porsche',
carClass: 'gt',
license: 'B',
year: 2023,
horsepower: 565,
weight: 1300,
gameId: 'iracing',
}),
Car.create({
id: 'car-ferrari-296',
name: '296 GT3',
shortName: '296 GT3',
manufacturer: 'Ferrari',
carClass: 'gt',
license: 'B',
year: 2023,
horsepower: 600,
weight: 1270,
gameId: 'iracing',
}),
Car.create({
id: 'car-mclaren-720s',
name: '720S GT3 Evo',
shortName: '720S',
manufacturer: 'McLaren',
carClass: 'gt',
license: 'B',
year: 2023,
horsepower: 552,
weight: 1290,
gameId: 'iracing',
}),
Car.create({
id: 'car-mercedes-gt3',
name: 'AMG GT3 2020',
shortName: 'AMG GT3',
manufacturer: 'Mercedes',
carClass: 'gt',
license: 'B',
year: 2020,
horsepower: 550,
weight: 1285,
gameId: 'iracing',
}),
Car.create({
id: 'car-lmp2',
name: 'Dallara P217 LMP2',
shortName: 'LMP2',
manufacturer: 'Dallara',
carClass: 'prototype',
license: 'A',
year: 2021,
horsepower: 600,
weight: 930,
gameId: 'iracing',
}),
Car.create({
id: 'car-f4',
name: 'Formula 4',
shortName: 'F4',
manufacturer: 'Tatuus',
carClass: 'formula',
license: 'D',
year: 2022,
horsepower: 160,
weight: 570,
gameId: 'iracing',
}),
Car.create({
id: 'car-mx5',
name: 'MX-5 Cup',
shortName: 'MX5',
manufacturer: 'Mazda',
carClass: 'sports',
license: 'D',
year: 2023,
horsepower: 181,
weight: 1128,
gameId: 'iracing',
}),
];
/**
* Create demo driver statistics based on seed data
*/
export function createDemoDriverStats(drivers: Array<{ id: string }>): Record<string, DriverStats> {
const stats: Record<string, DriverStats> = {};
drivers.forEach((driver, index) => {
const totalRaces = 40 + index * 5;
const wins = Math.max(0, Math.floor(totalRaces * 0.2) - index);
const podiums = Math.max(wins * 2, 0);
const dnfs = Math.max(0, Math.floor(index / 2));
const rating = 1500 + index * 25;
stats[driver.id] = {
driverId: driver.id,
rating,
totalRaces,
wins,
podiums,
dnfs,
avgFinish: 4,
bestFinish: 1,
worstFinish: 20,
consistency: 80,
overallRank: index + 1,
percentile: Math.max(0, 100 - index),
};
});
return stats;
}
/**
* Get league-specific rankings for a driver (demo implementation)
*/
export function getDemoLeagueRankings(driverId: string, leagueId: string): {
rank: number;
totalDrivers: number;
percentile: number;
} {
// Mock league rankings (in production, calculate from actual league membership)
const mockLeagueRanks: Record<string, Record<string, any>> = {
'league-1': {
'driver-1': { rank: 1, totalDrivers: 12, percentile: 92 },
'driver-2': { rank: 2, totalDrivers: 12, percentile: 84 },
'driver-3': { rank: 4, totalDrivers: 12, percentile: 67 },
'driver-4': { rank: 5, totalDrivers: 12, percentile: 58 },
},
};
return mockLeagueRanks[leagueId]?.[driverId] || { rank: 0, totalDrivers: 0, percentile: 0 };
}

View File

@@ -0,0 +1,75 @@
/**
* Driver statistics and ranking data used for demo seeding.
* Split out from the legacy DemoData module to keep responsibilities focused.
*/
export interface DriverStats {
driverId: string;
rating: number;
totalRaces: number;
wins: number;
podiums: number;
dnfs: number;
avgFinish: number;
bestFinish: number;
worstFinish: number;
consistency: number;
overallRank: number;
percentile: number;
}
/**
* Create demo driver statistics based on seed data.
* This is deterministic for a given driver ordering so it can be reused
* by any in-memory repository wiring.
*/
export function createDemoDriverStats(drivers: Array<{ id: string }>): Record<string, DriverStats> {
const stats: Record<string, DriverStats> = {};
drivers.forEach((driver, index) => {
const totalRaces = 40 + index * 5;
const wins = Math.max(0, Math.floor(totalRaces * 0.2) - index);
const podiums = Math.max(wins * 2, 0);
const dnfs = Math.max(0, Math.floor(index / 2));
const rating = 1500 + index * 25;
stats[driver.id] = {
driverId: driver.id,
rating,
totalRaces,
wins,
podiums,
dnfs,
avgFinish: 4,
bestFinish: 1,
worstFinish: 20,
consistency: 80,
overallRank: index + 1,
percentile: Math.max(0, 100 - index),
};
});
return stats;
}
/**
* Get league-specific rankings for a driver (demo implementation).
* In production this would be calculated from actual league membership
* and results; here we keep a very small static example for UI wiring.
*/
export function getDemoLeagueRankings(driverId: string, leagueId: string): {
rank: number;
totalDrivers: number;
percentile: number;
} {
// Mock league rankings (in production, calculate from actual league membership)
const mockLeagueRanks: Record<string, Record<string, any>> = {
'league-1': {
'driver-1': { rank: 1, totalDrivers: 12, percentile: 92 },
'driver-2': { rank: 2, totalDrivers: 12, percentile: 84 },
'driver-3': { rank: 4, totalDrivers: 12, percentile: 67 },
'driver-4': { rank: 5, totalDrivers: 12, percentile: 58 },
},
};
return mockLeagueRanks[leagueId]?.[driverId] || { rank: 0, totalDrivers: 0, percentile: 0 };
}

View File

@@ -0,0 +1,93 @@
import { Track } from '@gridpilot/racing/domain/entities/Track';
/**
* Demo track data for iRacing.
* Extracted from the legacy DemoData module so that tracks
* live in their own focused file.
*/
export const DEMO_TRACKS: Track[] = [
Track.create({
id: 'track-spa',
name: 'Spa-Francorchamps',
shortName: 'SPA',
country: 'Belgium',
category: 'road',
difficulty: 'advanced',
lengthKm: 7.004,
turns: 19,
imageUrl: '/images/tracks/spa.jpg',
gameId: 'iracing',
}),
Track.create({
id: 'track-monza',
name: 'Autodromo Nazionale Monza',
shortName: 'MON',
country: 'Italy',
category: 'road',
difficulty: 'intermediate',
lengthKm: 5.793,
turns: 11,
imageUrl: '/images/tracks/monza.jpg',
gameId: 'iracing',
}),
Track.create({
id: 'track-nurburgring',
name: 'Nürburgring Grand Prix',
shortName: 'NUR',
country: 'Germany',
category: 'road',
difficulty: 'advanced',
lengthKm: 5.148,
turns: 15,
imageUrl: '/images/tracks/nurburgring.jpg',
gameId: 'iracing',
}),
Track.create({
id: 'track-silverstone',
name: 'Silverstone Circuit',
shortName: 'SIL',
country: 'United Kingdom',
category: 'road',
difficulty: 'intermediate',
lengthKm: 5.891,
turns: 18,
imageUrl: '/images/tracks/silverstone.jpg',
gameId: 'iracing',
}),
Track.create({
id: 'track-suzuka',
name: 'Suzuka International Racing Course',
shortName: 'SUZ',
country: 'Japan',
category: 'road',
difficulty: 'expert',
lengthKm: 5.807,
turns: 18,
imageUrl: '/images/tracks/suzuka.jpg',
gameId: 'iracing',
}),
Track.create({
id: 'track-daytona',
name: 'Daytona International Speedway',
shortName: 'DAY',
country: 'United States',
category: 'oval',
difficulty: 'intermediate',
lengthKm: 4.023,
turns: 4,
imageUrl: '/images/tracks/daytona.jpg',
gameId: 'iracing',
}),
Track.create({
id: 'track-laguna',
name: 'WeatherTech Raceway Laguna Seca',
shortName: 'LAG',
country: 'United States',
category: 'road',
difficulty: 'advanced',
lengthKm: 3.602,
turns: 11,
imageUrl: '/images/tracks/laguna.jpg',
gameId: 'iracing',
}),
];

View File

@@ -0,0 +1,256 @@
import { Driver } from '@gridpilot/racing/domain/entities/Driver';
import { League } from '@gridpilot/racing/domain/entities/League';
import { Race } from '@gridpilot/racing/domain/entities/Race';
import type { Result } from '@gridpilot/racing/domain/entities/Result';
import type { FeedItem } from '@gridpilot/social/domain/entities/FeedItem';
import type { FriendDTO } from '@gridpilot/social/application/dto/FriendDTO';
import { faker } from '../faker/faker';
import { getLeagueBanner, getDriverAvatar } from '../images/images';
import type { Friendship, RacingMembership } from './RacingSeedCore';
/**
* Feed events and derived racing demo data.
* Extracted from the legacy StaticRacingSeed module to keep files smaller.
*/
export function createFeedEvents(
drivers: Driver[],
leagues: League[],
races: Race[],
friendships: Friendship[],
): FeedItem[] {
const events: FeedItem[] = [];
const now = new Date();
const completedRaces = races.filter((race) => race.status === 'completed');
// Focus the global feed around a stable “core” of demo drivers
const coreDrivers = faker.helpers.shuffle(drivers).slice(0, 16);
coreDrivers.forEach((driver, index) => {
const league = pickOne(leagues);
const race = completedRaces[index % Math.max(1, completedRaces.length)];
const minutesAgo = 10 + index * 5;
const baseTimestamp = new Date(now.getTime() - minutesAgo * 60 * 1000);
const actorFriendId = driver.id;
// Joined league
events.push({
id: `friend-joined-league:${driver.id}:${minutesAgo}`,
type: 'friend-joined-league',
timestamp: baseTimestamp,
actorDriverId: driver.id,
actorFriendId,
leagueId: league.id,
headline: `${driver.name} joined ${league.name}`,
body: 'They are now registered for the full season.',
ctaLabel: 'View league',
ctaHref: `/leagues/${league.id}`,
});
// Finished race / podium highlight
const finishingPosition = (index % 5) + 1;
events.push({
id: `friend-finished-race:${driver.id}:${minutesAgo}`,
type: 'friend-finished-race',
timestamp: new Date(baseTimestamp.getTime() - 8 * 60 * 1000),
actorDriverId: driver.id,
actorFriendId,
leagueId: race.leagueId,
raceId: race.id,
position: finishingPosition,
headline: `${driver.name} finished P${finishingPosition} at ${race.track}`,
body:
finishingPosition <= 3
? `${driver.name} scored a podium in ${race.car}.`
: `${driver.name} secured a strong result in ${race.car}.`,
ctaLabel: 'View results',
ctaHref: `/races/${race.id}/results`,
});
// New personal best
events.push({
id: `friend-new-personal-best:${driver.id}:${minutesAgo}`,
type: 'friend-new-personal-best',
timestamp: new Date(baseTimestamp.getTime() - 20 * 60 * 1000),
actorDriverId: driver.id,
actorFriendId,
leagueId: race.leagueId,
raceId: race.id,
headline: `${driver.name} set a new personal best at ${race.track}`,
body: 'Consistency and pace are trending up this season.',
ctaLabel: 'View lap chart',
ctaHref: `/races/${race.id}/analysis`,
});
// Joined team (where applicable)
const driverFriendships = friendships.filter((f) => f.driverId === driver.id);
if (driverFriendships.length > 0) {
const friend = pickOne(driverFriendships);
const teammate = drivers.find((d) => d.id === friend.friendId);
if (teammate) {
events.push({
id: `friend-joined-team:${driver.id}:${minutesAgo}`,
type: 'friend-joined-team',
timestamp: new Date(baseTimestamp.getTime() - 30 * 60 * 1000),
actorDriverId: driver.id,
actorFriendId,
headline: `${driver.name} and ${teammate.name} are now teammates`,
body: 'They will be sharing strategy and setups this season.',
ctaLabel: 'View team',
ctaHref: '/teams',
});
}
}
// League highlight
events.push({
id: `league-highlight:${league.id}:${minutesAgo}`,
type: 'league-highlight',
timestamp: new Date(baseTimestamp.getTime() - 45 * 60 * 1000),
leagueId: league.id,
headline: `${league.name} active with ${drivers.length}+ drivers`,
body: 'Participation is growing. Perfect time to join the grid.',
ctaLabel: 'Explore league',
ctaHref: `/leagues/${league.id}`,
});
});
// Global “system” events: new race scheduled and results posted
const upcomingRaces = races.filter((race) => race.status === 'scheduled').slice(0, 8);
upcomingRaces.forEach((race, index) => {
const minutesAgo = 60 + index * 15;
const timestamp = new Date(now.getTime() - minutesAgo * 60 * 1000);
events.push({
id: `new-race-scheduled:${race.id}`,
type: 'new-race-scheduled',
timestamp,
leagueId: race.leagueId,
raceId: race.id,
headline: `New race scheduled at ${race.track}`,
body: `${race.car}${race.scheduledAt.toLocaleString()}`,
ctaLabel: 'View schedule',
ctaHref: `/races/${race.id}`,
});
});
const completedForResults = completedRaces.slice(0, 8);
completedForResults.forEach((race, index) => {
const minutesAgo = 180 + index * 20;
const timestamp = new Date(now.getTime() - minutesAgo * 60 * 1000);
events.push({
id: `new-result-posted:${race.id}`,
type: 'new-result-posted',
timestamp,
leagueId: race.leagueId,
raceId: race.id,
headline: `Results posted for ${race.track}`,
body: 'Standings and stats updated across the grid.',
ctaLabel: 'View classification',
ctaHref: `/races/${race.id}/results`,
});
});
const sorted = events
.slice()
.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
return sorted;
}
/**
* Derived friend DTOs for UI consumption.
* This preserves the previous demo-data `friends` shape while
* keeping generation logic separate from the main seed.
*/
export function buildFriends(
drivers: Driver[],
memberships: RacingMembership[],
): FriendDTO[] {
return drivers.map((driver) => ({
driverId: driver.id,
displayName: driver.name,
avatarUrl: getDriverAvatar(driver.id),
isOnline: true,
lastSeen: new Date(),
primaryLeagueId: memberships.find((m) => m.driverId === driver.id)?.leagueId,
primaryTeamId: memberships.find((m) => m.driverId === driver.id)?.teamId,
}));
}
/**
* Build top leagues with banner URLs for UI.
*/
export function buildTopLeagues(leagues: League[]): Array<League & { bannerUrl: string }> {
return leagues.map((league) => ({
...league,
bannerUrl: getLeagueBanner(league.id),
}));
}
export type RaceWithResultsDTO = {
raceId: string;
track: string;
car: string;
scheduledAt: Date;
winnerDriverId: string;
winnerName: string;
};
/**
* Utility to get upcoming races from a given race list.
*/
export function buildUpcomingRaces(
races: Race[],
limit?: number,
): readonly Race[] {
const upcoming = races.filter((race) => race.status === 'scheduled');
const sorted = upcoming
.slice()
.sort((a, b) => a.scheduledAt.getTime() - b.scheduledAt.getTime());
return typeof limit === 'number' ? sorted.slice(0, limit) : sorted;
}
/**
* Utility to get latest race results from races + results + drivers.
*/
export function buildLatestResults(
races: Race[],
results: Result[],
drivers: Driver[],
limit?: number,
): readonly RaceWithResultsDTO[] {
const completedRaces = races.filter((race) => race.status === 'completed');
const joined = completedRaces.map((race) => {
const raceResults = results
.filter((result) => result.raceId === race.id)
.slice()
.sort((a, b) => a.position - b.position);
const winner = raceResults[0];
const winnerDriver =
winner && drivers.find((driver) => driver.id === winner.driverId);
return {
raceId: race.id,
track: race.track,
car: race.car,
scheduledAt: race.scheduledAt,
winnerDriverId: winner?.driverId ?? '',
winnerName: winnerDriver?.name ?? 'Winner',
};
});
const sorted = joined
.slice()
.sort((a, b) => b.scheduledAt.getTime() - a.scheduledAt.getTime());
return typeof limit === 'number' ? sorted.slice(0, limit) : sorted;
}
/**
* Local helper: random pick from an array.
* Kept here to avoid importing from core in callers that only care about feed.
*/
function pickOne<T>(items: readonly T[]): T {
return items[Math.floor(faker.number.int({ min: 0, max: items.length - 1 }))];
}

View File

@@ -0,0 +1,367 @@
import { Driver } from '@gridpilot/racing/domain/entities/Driver';
import { League } from '@gridpilot/racing/domain/entities/League';
import { Race } from '@gridpilot/racing/domain/entities/Race';
import { Result } from '@gridpilot/racing/domain/entities/Result';
import { Standing } from '@gridpilot/racing/domain/entities/Standing';
import { faker } from '../faker/faker';
/**
* Core racing seed types and generators (drivers, leagues, teams, races, standings).
* Extracted from the legacy StaticRacingSeed module to keep files smaller and focused.
*/
export type RacingMembership = {
driverId: string;
leagueId: string;
teamId?: string;
};
export type Friendship = {
driverId: string;
friendId: string;
};
export interface DemoTeamDTO {
id: string;
name: string;
tag: string;
description: string;
logoUrl: string;
primaryLeagueId: string;
memberCount: number;
}
/**
* Championship points table used when aggregating standings.
*/
export const POINTS_TABLE: Record<number, number> = {
1: 25,
2: 18,
3: 15,
4: 12,
5: 10,
6: 8,
7: 6,
8: 4,
9: 2,
10: 1,
};
export function pickOne<T>(items: readonly T[]): T {
return items[Math.floor(faker.number.int({ min: 0, max: items.length - 1 }))];
}
export function createDrivers(count: number): Driver[] {
const drivers: Driver[] = [];
for (let i = 0; i < count; i++) {
const id = `driver-${i + 1}`;
const name = faker.person.fullName();
const country = faker.location.countryCode('alpha-2');
const iracingId = faker.string.numeric(6);
drivers.push(
Driver.create({
id,
iracingId,
name,
country,
bio: faker.lorem.sentence(),
joinedAt: faker.date.past(),
}),
);
}
return drivers;
}
export function createLeagues(ownerIds: string[]): League[] {
const leagueNames = [
'GridPilot Sprint Series',
'GridPilot Endurance Cup',
'GridPilot Club Ladder',
'Sprint Challenge League',
'Club Racers Collective',
'Sim Racing Alliance',
'Pacific Time Attack',
'Nordic Night Series',
];
const leagues: League[] = [];
const leagueCount = 6 + faker.number.int({ min: 0, max: 2 });
for (let i = 0; i < leagueCount; i++) {
const id = `league-${i + 1}`;
const name = leagueNames[i] ?? faker.company.name();
const ownerId = pickOne(ownerIds);
const maxDriversOptions = [24, 32, 48, 64];
let settings = {
pointsSystem: faker.helpers.arrayElement(['f1-2024', 'indycar']),
sessionDuration: faker.helpers.arrayElement([45, 60, 90, 120]),
qualifyingFormat: faker.helpers.arrayElement(['open', 'single-lap']),
maxDrivers: faker.helpers.arrayElement(maxDriversOptions),
} as const;
if (i === 0) {
settings = {
...settings,
maxDrivers: 24,
};
} else if (i === 1) {
settings = {
...settings,
maxDrivers: 24,
};
} else if (i === 2) {
settings = {
...settings,
maxDrivers: 40,
};
}
const socialLinks =
i === 0
? {
discordUrl: 'https://discord.gg/gridpilot-demo',
youtubeUrl: 'https://youtube.com/@gridpilot-demo',
websiteUrl: 'https://gridpilot-demo.example.com',
}
: i === 1
? {
discordUrl: 'https://discord.gg/gridpilot-endurance',
youtubeUrl: 'https://youtube.com/@gridpilot-endurance',
}
: i === 2
? {
websiteUrl: 'https://virtual-touring.example.com',
}
: undefined;
leagues.push(
League.create({
id,
name,
description: faker.lorem.sentence(),
ownerId,
settings,
createdAt: faker.date.past(),
socialLinks,
}),
);
}
return leagues;
}
export function createTeams(leagues: League[], getTeamLogo: (id: string) => string): DemoTeamDTO[] {
const teams: DemoTeamDTO[] = [];
const teamCount = 24 + faker.number.int({ min: 0, max: 12 });
for (let i = 0; i < teamCount; i++) {
const id = `team-${i + 1}`;
const primaryLeague = pickOne(leagues);
const name = faker.company.name();
const tag = faker.string.alpha({ length: 4 }).toUpperCase();
const memberCount = faker.number.int({ min: 2, max: 8 });
teams.push({
id,
name,
tag,
description: faker.lorem.sentence(),
logoUrl: getTeamLogo(id),
primaryLeagueId: primaryLeague.id,
memberCount,
});
}
return teams;
}
export function createMemberships(
drivers: Driver[],
leagues: League[],
teams: DemoTeamDTO[],
): RacingMembership[] {
const memberships: RacingMembership[] = [];
const teamsByLeague = new Map<string, DemoTeamDTO[]>();
teams.forEach((team) => {
const list = teamsByLeague.get(team.primaryLeagueId) ?? [];
list.push(team);
teamsByLeague.set(team.primaryLeagueId, list);
});
drivers.forEach((driver) => {
// Each driver participates in 13 leagues
const leagueSampleSize = faker.number.int({ min: 1, max: Math.min(3, leagues.length) });
const shuffledLeagues = faker.helpers.shuffle(leagues).slice(0, leagueSampleSize);
shuffledLeagues.forEach((league) => {
const leagueTeams = teamsByLeague.get(league.id) ?? [];
const team =
leagueTeams.length > 0 && faker.datatype.boolean()
? pickOne(leagueTeams)
: undefined;
memberships.push({
driverId: driver.id,
leagueId: league.id,
teamId: team?.id,
});
});
});
return memberships;
}
export function createRaces(leagues: League[]): Race[] {
const races: Race[] = [];
const raceCount = 60 + faker.number.int({ min: 0, max: 20 });
const tracks = [
'Monza GP',
'Spa-Francorchamps',
'Suzuka',
'Mount Panorama',
'Silverstone GP',
'Interlagos',
'Imola',
'Laguna Seca',
];
const cars = [
'GT3 Porsche 911',
'GT3 BMW M4',
'LMP3 Prototype',
'GT4 Alpine',
'Touring Civic',
];
const baseDate = new Date();
for (let i = 0; i < raceCount; i++) {
const id = `race-${i + 1}`;
const league = pickOne(leagues);
const offsetDays = faker.number.int({ min: -30, max: 45 });
const scheduledAt = new Date(baseDate.getTime() + offsetDays * 24 * 60 * 60 * 1000);
const status = scheduledAt.getTime() < baseDate.getTime() ? 'completed' : 'scheduled';
races.push(
Race.create({
id,
leagueId: league.id,
scheduledAt,
track: faker.helpers.arrayElement(tracks),
car: faker.helpers.arrayElement(cars),
sessionType: 'race',
status,
}),
);
}
return races;
}
export function createResults(drivers: Driver[], races: Race[]): Result[] {
const results: Result[] = [];
const completedRaces = races.filter((race) => race.status === 'completed');
completedRaces.forEach((race) => {
const participantCount = faker.number.int({ min: 20, max: 32 });
const shuffledDrivers = faker.helpers.shuffle(drivers).slice(0, participantCount);
shuffledDrivers.forEach((driver, index) => {
const position = index + 1;
const startPosition = faker.number.int({ min: 1, max: participantCount });
const fastestLap = 90_000 + index * 250 + faker.number.int({ min: 0, max: 2_000 });
const incidents = faker.number.int({ min: 0, max: 6 });
results.push(
Result.create({
id: `${race.id}-${driver.id}`,
raceId: race.id,
driverId: driver.id,
position,
startPosition,
fastestLap,
incidents,
}),
);
});
});
return results;
}
export function createStandings(leagues: League[], results: Result[]): Standing[] {
const standingsByLeague = new Map<string, Standing[]>();
leagues.forEach((league) => {
const leagueRaceIds = new Set(
results
.filter((result) => {
return result.raceId.startsWith('race-');
})
.map((result) => result.raceId),
);
const leagueResults = results.filter((result) => leagueRaceIds.has(result.raceId));
const standingsMap = new Map<string, Standing>();
leagueResults.forEach((result) => {
const key = result.driverId;
let standing = standingsMap.get(key);
if (!standing) {
standing = Standing.create({
leagueId: league.id,
driverId: result.driverId,
});
}
standing = standing.addRaceResult(result.position, POINTS_TABLE);
standingsMap.set(key, standing);
});
const sortedStandings = Array.from(standingsMap.values()).sort((a, b) => {
if (b.points !== a.points) {
return b.points - a.points;
}
if (b.wins !== a.wins) {
return b.wins - a.wins;
}
return b.racesCompleted - a.racesCompleted;
});
const finalizedStandings = sortedStandings.map((standing, index) =>
standing.updatePosition(index + 1),
);
standingsByLeague.set(league.id, finalizedStandings);
});
return Array.from(standingsByLeague.values()).flat();
}
export function createFriendships(drivers: Driver[]): Friendship[] {
const friendships: Friendship[] = [];
drivers.forEach((driver, index) => {
const friendCount = faker.number.int({ min: 3, max: 8 });
for (let offset = 1; offset <= friendCount; offset++) {
const friendIndex = (index + offset) % drivers.length;
const friend = drivers[friendIndex];
if (friend.id === driver.id) continue;
friendships.push({
driverId: driver.id,
friendId: friend.id,
});
}
});
return friendships;
}

View File

@@ -0,0 +1,439 @@
import { League } from '@gridpilot/racing/domain/entities/League';
import { Race } from '@gridpilot/racing/domain/entities/Race';
import { Driver } from '@gridpilot/racing/domain/entities/Driver';
import { Money } from '@gridpilot/racing/domain/value-objects/Money';
import { SponsorshipPricing } from '@gridpilot/racing/domain/value-objects/SponsorshipPricing';
import {
SponsorshipRequest,
type SponsorableEntityType,
} from '@gridpilot/racing/domain/entities/SponsorshipRequest';
import type { DemoTeamDTO } from './RacingSeedCore';
/**
* Demo sponsor data for seeding.
*/
export interface DemoSponsorDTO {
id: string;
name: string;
contactEmail: string;
logoUrl: string;
websiteUrl: string;
tagline: string;
}
/**
* Demo season sponsorship data.
* This remains a simple DTO since the SeasonSponsorship
* domain entity is instantiated in the DI config.
*/
export interface DemoSeasonSponsorshipDTO {
id: string;
seasonId: string;
sponsorId: string;
tier: 'main' | 'secondary';
pricingAmount: number;
pricingCurrency: 'USD' | 'EUR' | 'GBP';
status: 'pending' | 'active' | 'cancelled';
description?: string;
}
/**
* Demo sponsorship request data for seeding.
* Backed directly by the SponsorshipRequest domain entity.
*/
export type DemoSponsorshipRequestDTO = SponsorshipRequest;
/**
* Demo sponsorship pricing configuration for entities, using the
* SponsorshipPricing value object to keep pricing logic in the domain.
*/
export interface DemoSponsorshipPricingDTO {
entityType: SponsorableEntityType;
entityId: string;
pricing: SponsorshipPricing;
}
/**
* Demo sponsors data - realistic sim racing sponsors.
*/
export const DEMO_SPONSORS: DemoSponsorDTO[] = [
{
id: 'sponsor-fanatec',
name: 'Fanatec',
contactEmail: 'partnerships@fanatec.com',
logoUrl: '/images/sponsors/fanatec.svg',
websiteUrl: 'https://fanatec.com',
tagline: "The world's leading sim racing hardware",
},
{
id: 'sponsor-simucube',
name: 'Simucube',
contactEmail: 'sponsors@simucube.com',
logoUrl: '/images/sponsors/simucube.svg',
websiteUrl: 'https://simucube.com',
tagline: 'Professional Direct Drive Wheels',
},
{
id: 'sponsor-heusinkveld',
name: 'Heusinkveld',
contactEmail: 'info@heusinkveld.com',
logoUrl: '/images/sponsors/heusinkveld.svg',
websiteUrl: 'https://heusinkveld.com',
tagline: 'Sim Racing Pedals & Hardware',
},
{
id: 'sponsor-trak-racer',
name: 'Trak Racer',
contactEmail: 'partnerships@trakracer.com',
logoUrl: '/images/sponsors/trak-racer.svg',
websiteUrl: 'https://trakracer.com',
tagline: 'Premium Racing Simulators & Cockpits',
},
{
id: 'sponsor-simlab',
name: 'Sim-Lab',
contactEmail: 'sponsor@sim-lab.eu',
logoUrl: '/images/sponsors/simlab.svg',
websiteUrl: 'https://sim-lab.eu',
tagline: 'Aluminum Profile Sim Racing Rigs',
},
{
id: 'sponsor-motionrig',
name: 'MotionRig Pro',
contactEmail: 'business@motionrigpro.com',
logoUrl: '/images/sponsors/motionrig.svg',
websiteUrl: 'https://motionrigpro.com',
tagline: 'Feel every turn, every bump',
},
];
/**
* Create season sponsorships linking sponsors to leagues.
*/
export function createSeasonSponsorships(
leagues: League[],
sponsors: DemoSponsorDTO[],
): DemoSeasonSponsorshipDTO[] {
const sponsorships: DemoSeasonSponsorshipDTO[] = [];
const FANATEC_ID = sponsors.find((s) => s.id === 'sponsor-fanatec')?.id ?? 'sponsor-fanatec';
const HEUSINKVELD_ID =
sponsors.find((s) => s.id === 'sponsor-heusinkveld')?.id ?? 'sponsor-heusinkveld';
const SIMUCUBE_ID = sponsors.find((s) => s.id === 'sponsor-simucube')?.id ?? 'sponsor-simucube';
const TRAK_RACER_ID =
sponsors.find((s) => s.id === 'sponsor-trak-racer')?.id ?? 'sponsor-trak-racer';
const SIMLAB_ID = sponsors.find((s) => s.id === 'sponsor-simlab')?.id ?? 'sponsor-simlab';
const MOTIONRIG_ID =
sponsors.find((s) => s.id === 'sponsor-motionrig')?.id ?? 'sponsor-motionrig';
// GridPilot Sprint Series - sponsored by Fanatec (main) + Heusinkveld & Simucube (secondary)
const sprintLeague = leagues.find((l) => l.name === 'GridPilot Sprint Series');
if (sprintLeague) {
sponsorships.push({
id: `sponsorship-${sprintLeague.id}-fanatec`,
seasonId: `season-${sprintLeague.id}-demo`,
sponsorId: FANATEC_ID,
tier: 'main',
pricingAmount: 5000,
pricingCurrency: 'USD',
status: 'active',
description: 'Main sponsor for the Sprint Series - premium wheel branding',
});
sponsorships.push({
id: `sponsorship-${sprintLeague.id}-heusinkveld`,
seasonId: `season-${sprintLeague.id}-demo`,
sponsorId: HEUSINKVELD_ID,
tier: 'secondary',
pricingAmount: 2000,
pricingCurrency: 'USD',
status: 'active',
});
sponsorships.push({
id: `sponsorship-${sprintLeague.id}-simucube`,
seasonId: `season-${sprintLeague.id}-demo`,
sponsorId: SIMUCUBE_ID,
tier: 'secondary',
pricingAmount: 2000,
pricingCurrency: 'USD',
status: 'active',
});
}
// GridPilot Endurance Cup - sponsored by Trak Racer (main) + Sim-Lab (secondary)
const enduranceLeague = leagues.find((l) => l.name === 'GridPilot Endurance Cup');
if (enduranceLeague) {
sponsorships.push({
id: `sponsorship-${enduranceLeague.id}-trakracer`,
seasonId: `season-${enduranceLeague.id}-demo`,
sponsorId: TRAK_RACER_ID,
tier: 'main',
pricingAmount: 7500,
pricingCurrency: 'USD',
status: 'active',
description: 'Endurance series naming rights',
});
sponsorships.push({
id: `sponsorship-${enduranceLeague.id}-simlab`,
seasonId: `season-${enduranceLeague.id}-demo`,
sponsorId: SIMLAB_ID,
tier: 'secondary',
pricingAmount: 3000,
pricingCurrency: 'USD',
status: 'active',
});
}
// GridPilot Club Ladder - sponsored by MotionRig Pro (main)
const clubLeague = leagues.find((l) => l.name === 'GridPilot Club Ladder');
if (clubLeague) {
sponsorships.push({
id: `sponsorship-${clubLeague.id}-motionrig`,
seasonId: `season-${clubLeague.id}-demo`,
sponsorId: MOTIONRIG_ID,
tier: 'main',
pricingAmount: 3500,
pricingCurrency: 'USD',
status: 'active',
description: 'Club ladder motion platform showcase',
});
}
return sponsorships;
}
/**
* Create sponsorship pricing configurations for demo entities.
* Uses the SponsorshipPricing value object to ensure domain consistency
* and to provide a mix of season, team, driver and race offerings.
*/
export function createSponsorshipPricings(
leagues: League[],
teams: DemoTeamDTO[],
drivers: Driver[],
races: Race[],
): DemoSponsorshipPricingDTO[] {
const pricings: DemoSponsorshipPricingDTO[] = [];
// League/Season pricing - all leagues can accept sponsorships, with varied configs
leagues.forEach((league, index) => {
let pricing = SponsorshipPricing.defaultLeague();
// Vary league pricing/availability for demo richness
if (index % 3 === 1) {
// Some leagues closed for applications
pricing = pricing.setAcceptingApplications(false);
} else if (index % 3 === 2) {
// Some leagues with main-only sponsorship
pricing = pricing.updateSecondarySlot({ available: false, maxSlots: 0 });
} else {
// Slightly higher price for featured leagues
pricing = pricing.updateMainSlot({
price: Money.create(1000 + index * 50, 'USD'),
});
}
pricings.push({
entityType: 'season',
entityId: `season-${league.id}-demo`,
pricing,
});
});
// Team pricing - first 10 teams accept sponsorships using team defaults,
// with some teams pausing applications.
teams.slice(0, 10).forEach((team, index) => {
let pricing = SponsorshipPricing.defaultTeam();
if (index % 4 === 1) {
// Teams with main + secondary but not currently accepting
pricing = pricing.setAcceptingApplications(false);
} else if (index % 4 === 2) {
// Teams with only secondary slots
pricing = pricing.updateMainSlot({ available: false, maxSlots: 0 });
} else if (index % 4 === 3) {
// Teams with premium main slot pricing
pricing = pricing.updateMainSlot({
price: Money.create(750 + index * 25, 'USD'),
});
}
pricings.push({
entityType: 'team',
entityId: team.id,
pricing,
});
});
// Driver pricing - first 20 drivers accept sponsorships with varied availability.
drivers.slice(0, 20).forEach((driver, index) => {
let pricing = SponsorshipPricing.defaultDriver();
if (index % 3 === 0) {
// Higher profile drivers
pricing = pricing.updateMainSlot({
price: Money.create(250 + index * 10, 'USD'),
});
} else if (index % 3 === 1) {
// Drivers temporarily not accepting sponsorships
pricing = pricing.setAcceptingApplications(false);
}
pricings.push({
entityType: 'driver',
entityId: driver.id,
pricing,
});
});
// Race pricing - upcoming races can have title sponsors with different tiers
const upcomingRaces = races.filter((r) => r.status === 'scheduled').slice(0, 10);
upcomingRaces.forEach((race, index) => {
let pricing = SponsorshipPricing.defaultRace();
if (index % 2 === 0) {
// Premium events with higher pricing
pricing = pricing.updateMainSlot({
price: Money.create(350 + index * 30, 'USD'),
});
}
pricings.push({
entityType: 'race',
entityId: race.id,
pricing,
});
});
return pricings;
}
/**
* Create demo sponsorship requests (some pending, some accepted/rejected).
* Uses the SponsorshipRequest domain entity and Money value object so that
* all downstream sponsor flows can rely on domain behavior.
*/
export function createSponsorshipRequests(
sponsors: DemoSponsorDTO[],
leagues: League[],
teams: DemoTeamDTO[],
drivers: Driver[],
races: Race[],
): DemoSponsorshipRequestDTO[] {
const requests: DemoSponsorshipRequestDTO[] = [];
const now = new Date();
const SIMUCUBE_ID = sponsors.find((s) => s.id === 'sponsor-simucube')?.id ?? 'sponsor-simucube';
const HEUSINKVELD_ID =
sponsors.find((s) => s.id === 'sponsor-heusinkveld')?.id ?? 'sponsor-heusinkveld';
const TRAK_RACER_ID =
sponsors.find((s) => s.id === 'sponsor-trak-racer')?.id ?? 'sponsor-trak-racer';
const MOTIONRIG_ID =
sponsors.find((s) => s.id === 'sponsor-motionrig')?.id ?? 'sponsor-motionrig';
const SIMLAB_ID = sponsors.find((s) => s.id === 'sponsor-simlab')?.id ?? 'sponsor-simlab';
// Pending request: Simucube wants to sponsor a driver
if (drivers.length > 6) {
requests.push(
SponsorshipRequest.create({
id: 'req-simucube-driver-1',
sponsorId: SIMUCUBE_ID,
entityType: 'driver',
entityId: drivers[5].id,
tier: 'main',
offeredAmount: Money.create(250, 'USD'),
message:
'We would love to sponsor your racing career! Simucube offers the best direct drive wheels in sim racing.',
createdAt: new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000), // 2 days ago
}),
);
}
// Pending request: Heusinkveld wants to sponsor a team
if (teams.length > 3) {
requests.push(
SponsorshipRequest.create({
id: 'req-heusinkveld-team-1',
sponsorId: HEUSINKVELD_ID,
entityType: 'team',
entityId: teams[2].id,
tier: 'main',
offeredAmount: Money.create(550, 'USD'),
message:
'Heusinkveld pedals are known for their precision. We believe your team embodies the same values.',
createdAt: new Date(now.getTime() - 3 * 24 * 60 * 60 * 1000), // 3 days ago
}),
);
}
// Pending request: Trak Racer wants to sponsor a race
const upcomingRace = races.find((r) => r.status === 'scheduled');
if (upcomingRace) {
requests.push(
SponsorshipRequest.create({
id: 'req-trakracer-race-1',
sponsorId: TRAK_RACER_ID,
entityType: 'race',
entityId: upcomingRace.id,
tier: 'main',
offeredAmount: Money.create(350, 'USD'),
message: 'We would like to be the title sponsor for this exciting race event!',
createdAt: new Date(now.getTime() - 1 * 24 * 60 * 60 * 1000), // 1 day ago
}),
);
}
// Pending request: MotionRig Pro wants secondary spot on a league season
const clubLeague = leagues.find((l) => l.name === 'Sprint Challenge League');
if (clubLeague) {
requests.push(
SponsorshipRequest.create({
id: 'req-motionrig-league-1',
sponsorId: MOTIONRIG_ID,
entityType: 'season',
entityId: `season-${clubLeague.id}-demo`,
tier: 'secondary',
offeredAmount: Money.create(1500, 'USD'),
message:
'MotionRig Pro would love to be a secondary sponsor. Our motion platforms are perfect for your competitive drivers.',
createdAt: new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000), // 5 days ago
}),
);
}
// Already accepted request (for history)
if (teams.length > 0) {
requests.push(
SponsorshipRequest.create({
id: 'req-simlab-team-accepted',
sponsorId: SIMLAB_ID,
entityType: 'team',
entityId: teams[0].id,
tier: 'secondary',
offeredAmount: Money.create(300, 'USD'),
message: 'Sim-Lab rigs are the foundation of any competitive setup.',
status: 'accepted',
createdAt: new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000), // 30 days ago
}),
);
}
// Already rejected request (for history)
if (drivers.length > 10) {
requests.push(
SponsorshipRequest.create({
id: 'req-motionrig-driver-rejected',
sponsorId: MOTIONRIG_ID,
entityType: 'driver',
entityId: drivers[10].id,
tier: 'main',
offeredAmount: Money.create(150, 'USD'),
message: 'Would you like to represent MotionRig Pro?',
status: 'rejected',
createdAt: new Date(now.getTime() - 20 * 24 * 60 * 60 * 1000), // 20 days ago
}),
);
}
return requests;
}

View File

@@ -0,0 +1,203 @@
import { Driver } from '@gridpilot/racing/domain/entities/Driver';
import { League } from '@gridpilot/racing/domain/entities/League';
import { Race } from '@gridpilot/racing/domain/entities/Race';
import { Result } from '@gridpilot/racing/domain/entities/Result';
import { Standing } from '@gridpilot/racing/domain/entities/Standing';
import type { FeedItem } from '@gridpilot/social/domain/entities/FeedItem';
import type { FriendDTO } from '@gridpilot/social/application/dto/FriendDTO';
import { faker } from '../faker/faker';
import { getTeamLogo } from '../images/images';
import {
createDrivers,
createLeagues,
createTeams,
createMemberships,
createRaces,
createResults,
createStandings,
createFriendships,
type RacingMembership,
type Friendship,
type DemoTeamDTO,
} from './RacingSeedCore';
import {
DEMO_SPONSORS,
createSeasonSponsorships,
createSponsorshipPricings,
createSponsorshipRequests,
type DemoSponsorDTO,
type DemoSeasonSponsorshipDTO,
type DemoSponsorshipRequestDTO,
type DemoSponsorshipPricingDTO,
} from './RacingSponsorshipSeed';
import {
createFeedEvents,
buildFriends,
buildTopLeagues,
buildUpcomingRaces,
buildLatestResults,
type RaceWithResultsDTO,
} from './RacingFeedSeed';
/**
* Aggregated racing seed data used by the website DI container
* and other demo infrastructure.
*/
export type RacingSeedData = {
drivers: Driver[];
leagues: League[];
races: Race[];
results: Result[];
standings: Standing[];
memberships: RacingMembership[];
friendships: Friendship[];
feedEvents: FeedItem[];
teams: DemoTeamDTO[];
sponsors: DemoSponsorDTO[];
seasonSponsorships: DemoSeasonSponsorshipDTO[];
sponsorshipRequests: DemoSponsorshipRequestDTO[];
sponsorshipPricings: DemoSponsorshipPricingDTO[];
};
/**
* Create the full static racing seed from the smaller core/sponsorship/feed modules.
*/
export function createStaticRacingSeed(seed: number): RacingSeedData {
faker.seed(seed);
const drivers = createDrivers(96);
const leagues = createLeagues(drivers.slice(0, 12).map((d) => d.id));
const teams = createTeams(leagues, getTeamLogo);
const memberships = createMemberships(drivers, leagues, teams);
const races = createRaces(leagues);
const results = createResults(drivers, races);
const friendships = createFriendships(drivers);
const feedEvents = createFeedEvents(drivers, leagues, races, friendships);
const standings = createStandings(leagues, results);
const sponsors = DEMO_SPONSORS;
const seasonSponsorships = createSeasonSponsorships(leagues, sponsors);
const sponsorshipPricings = createSponsorshipPricings(leagues, teams, drivers, races);
const sponsorshipRequests = createSponsorshipRequests(sponsors, leagues, teams, drivers, races);
return {
drivers,
leagues,
races,
results,
standings,
memberships,
friendships,
feedEvents,
teams,
sponsors,
seasonSponsorships,
sponsorshipRequests,
sponsorshipPricings,
};
}
/**
* Singleton seed used by website demo helpers.
*
* Alpha demo dataset (deterministic, in-memory only):
* - 90+ drivers across multiple leagues
* - Leagues with precomputed races, results and standings
* - Team memberships and friendships forming social “circles”
* - Feed events referencing real driver, league, race and team IDs
*/
const staticSeed = createStaticRacingSeed(42);
export const drivers = staticSeed.drivers;
export const leagues = staticSeed.leagues;
export const races = staticSeed.races;
export const results = staticSeed.results;
export const standings = staticSeed.standings;
export const teams = staticSeed.teams;
export const memberships = staticSeed.memberships;
export const friendships = staticSeed.friendships;
export const feedEvents = staticSeed.feedEvents;
export const sponsors = staticSeed.sponsors;
export const seasonSponsorships = staticSeed.seasonSponsorships;
export const sponsorshipRequests = staticSeed.sponsorshipRequests;
export const sponsorshipPricings = staticSeed.sponsorshipPricings;
/**
* Derived friend DTOs for UI consumption.
* This preserves the previous demo-data `friends` shape.
*/
export const friends: FriendDTO[] = buildFriends(staticSeed.drivers, staticSeed.memberships);
/**
* Top leagues with banner URLs for UI.
*/
export const topLeagues = buildTopLeagues(leagues);
/**
* Re-export RaceWithResultsDTO and helpers for latest/upcoming races.
*/
export type { RaceWithResultsDTO } from './RacingFeedSeed';
export function getUpcomingRaces(limit?: number): readonly Race[] {
return buildUpcomingRaces(races, limit);
}
export function getLatestResults(limit?: number): readonly RaceWithResultsDTO[] {
return buildLatestResults(races, results, drivers, limit);
}
/**
* Demo league archetype helper for seeding structure and scoring.
* Kept here as the small, focused definition used by DI.
*/
export type DemoLeagueArchetype =
| {
id: 'sprint-series';
name: 'GridPilot Sprint Series';
structure: { mode: 'solo'; maxDrivers: 24 };
scoringPresetId: 'sprint-main-driver';
}
| {
id: 'endurance-cup';
name: 'GridPilot Endurance Cup';
structure: { mode: 'fixedTeams'; maxTeams: 12; driversPerTeam: 2 };
scoringPresetId: 'endurance-main-double';
}
| {
id: 'club-ladder';
name: 'GridPilot Club Ladder';
structure: { mode: 'solo'; maxDrivers: 40 };
scoringPresetId: 'club-default';
};
export function getDemoLeagueArchetypeByName(
leagueName: string,
): DemoLeagueArchetype | undefined {
switch (leagueName) {
case 'GridPilot Sprint Series':
return {
id: 'sprint-series',
name: 'GridPilot Sprint Series',
structure: { mode: 'solo', maxDrivers: 24 },
scoringPresetId: 'sprint-main-driver',
};
case 'GridPilot Endurance Cup':
return {
id: 'endurance-cup',
name: 'GridPilot Endurance Cup',
structure: { mode: 'fixedTeams', maxTeams: 12, driversPerTeam: 2 },
scoringPresetId: 'endurance-main-double',
};
case 'GridPilot Club Ladder':
return {
id: 'club-ladder',
name: 'GridPilot Club Ladder',
structure: { mode: 'solo', maxDrivers: 40 },
scoringPresetId: 'club-default',
};
default:
return undefined;
}
}

File diff suppressed because it is too large Load Diff