@@ -0,0 +1,508 @@
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 '@gridpilot/testing-support' ;
import { getTeamLogo , getLeagueBanner , getDriverAvatar } from '@gridpilot/testing-support' ;
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 ;
}
export type RacingSeedData = {
drivers : Driver [ ] ;
leagues : League [ ] ;
races : Race [ ] ;
results : Result [ ] ;
standings : Standing [ ] ;
memberships : RacingMembership [ ] ;
friendships : Friendship [ ] ;
feedEvents : FeedItem [ ] ;
teams : DemoTeamDTO [ ] ;
} ;
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 ,
} ;
function pickOne < T > ( items : readonly T [ ] ) : T {
return items [ Math . floor ( faker . number . int ( { min : 0 , max : items.length - 1 } ) ) ] ;
}
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 ;
}
function createLeagues ( ownerIds : string [ ] ) : League [ ] {
const leagueNames = [
'Global GT Masters' ,
'Midnight Endurance Series' ,
'Virtual Touring Cup' ,
'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 settings = {
pointsSystem : faker.helpers.arrayElement ( [ 'f1-2024' , 'indycar' ] ) ,
sessionDuration : faker.helpers.arrayElement ( [ 45 , 60 , 90 , 120 ] ) ,
qualifyingFormat : faker.helpers.arrayElement ( [ 'open' , 'single-lap' ] ) ,
} ;
leagues . push (
League . create ( {
id ,
name ,
description : faker.lorem.sentence ( ) ,
ownerId ,
settings ,
createdAt : faker.date.past ( ) ,
} ) ,
) ;
}
return leagues ;
}
function createTeams ( leagues : League [ ] ) : 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 ;
}
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 1– 3 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 ;
}
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 ;
}
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 ;
}
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 ( ) ;
}
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 ;
}
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' ) ;
const globalDrivers = faker . helpers . shuffle ( drivers ) . slice ( 0 , 10 ) ;
globalDrivers . forEach ( ( driver , index ) = > {
const league = pickOne ( leagues ) ;
const race = completedRaces [ index % Math . max ( 1 , completedRaces . length ) ] ;
const minutesAgo = 15 + index * 10 ;
const baseTimestamp = new Date ( now . getTime ( ) - minutesAgo * 60 * 1000 ) ;
events . push ( {
id : ` friend-joined-league: ${ driver . id } : ${ minutesAgo } ` ,
type : 'friend-joined-league' ,
timestamp : baseTimestamp ,
actorDriverId : driver.id ,
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 } ` ,
} ) ;
events . push ( {
id : ` friend-finished-race: ${ driver . id } : ${ minutesAgo } ` ,
type : 'friend-finished-race' ,
timestamp : new Date ( baseTimestamp . getTime ( ) - 10 * 60 * 1000 ) ,
actorDriverId : driver.id ,
leagueId : race.leagueId ,
raceId : race.id ,
position : ( index % 5 ) + 1 ,
headline : ` ${ driver . name } finished P ${ ( index % 5 ) + 1 } at ${ race . track } ` ,
body : ` ${ driver . name } secured a strong result in ${ race . car } . ` ,
ctaLabel : 'View results' ,
ctaHref : ` /races/ ${ race . id } /results ` ,
} ) ;
events . push ( {
id : ` league-highlight: ${ league . id } : ${ minutesAgo } ` ,
type : 'league-highlight' ,
timestamp : new Date ( baseTimestamp . getTime ( ) - 30 * 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 } ` ,
} ) ;
} ) ;
const sorted = events
. slice ( )
. sort ( ( a , b ) = > b . timestamp . getTime ( ) - a . timestamp . getTime ( ) ) ;
return sorted ;
}
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 ) ;
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 ) ;
return {
drivers ,
leagues ,
races ,
results ,
standings ,
memberships ,
friendships ,
feedEvents ,
teams ,
} ;
}
/**
* Singleton seed used by website demo helpers.
* This mirrors the previous apps/website/lib/demo-data/index.ts behavior.
*/
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 ;
/**
* Derived friend DTOs for UI consumption.
* This preserves the previous demo-data `friends` shape.
*/
export const friends : FriendDTO [ ] = staticSeed . drivers . map ( ( driver ) = > ( {
driverId : driver.id ,
displayName : driver.name ,
avatarUrl : getDriverAvatar ( driver . id ) ,
isOnline : true ,
lastSeen : new Date ( ) ,
primaryLeagueId : staticSeed.memberships.find ( ( m ) = > m . driverId === driver . id ) ? . leagueId ,
primaryTeamId : staticSeed.memberships.find ( ( m ) = > m . driverId === driver . id ) ? . teamId ,
} ) ) ;
export const topLeagues = leagues . map ( ( league ) = > ( {
. . . league ,
bannerUrl : getLeagueBanner ( league . id ) ,
} ) ) ;
export type RaceWithResultsDTO = {
raceId : string ;
track : string ;
car : string ;
scheduledAt : Date ;
winnerDriverId : string ;
winnerName : string ;
} ;
export function getUpcomingRaces ( 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 ;
}
export function getLatestResults ( 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 ;
}