Files
gridpilot.gg/testing/fixtures/racing/RacingFeedSeed.ts
2025-12-23 17:31:45 +01:00

292 lines
9.3 KiB
TypeScript

import { Driver } from '@core/racing/domain/entities/Driver';
import { League } from '@core/racing/domain/entities/League';
import { Race } from '@core/racing/domain/entities/Race';
import type { Result } from '@core/racing/domain/entities/Result';
import type { FeedItem } from '@core/social/domain/types/FeedItem';
import type { SocialFriendSummary } from '@core/social/application/types/SocialUser';
import { faker } from '../../helpers/faker/faker';
import { getLeagueBanner, getDriverAvatar } from '../../helpers/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, Math.min(16, drivers.length));
coreDrivers.forEach((driver, index) => {
const league = pickOne(leagues);
const raceSource = completedRaces.length > 0 ? completedRaces : races;
const race = pickOne(raceSource);
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[],
): SocialFriendSummary[] {
return drivers.map((driver) => {
const membership = memberships.find((m) => m.driverId === driver.id);
const base: SocialFriendSummary = {
driverId: driver.id,
displayName: driver.name,
avatarUrl: getDriverAvatar(driver.id),
isOnline: true,
lastSeen: new Date(),
};
const withLeague =
membership?.leagueId !== undefined
? { ...base, primaryLeagueId: membership.leagueId }
: base;
const withTeam =
membership?.teamId !== undefined
? { ...withLeague, primaryTeamId: membership.teamId }
: withLeague;
return withTeam;
});
}
/**
* Build top leagues with banner URLs for UI.
*/
export type LeagueWithBannerDTO = {
id: string;
name: string;
description: string;
ownerId: string;
settings: League['settings'];
createdAt: Date;
socialLinks: League['socialLinks'];
bannerUrl: string;
};
export function buildTopLeagues(leagues: League[]): LeagueWithBannerDTO[] {
return leagues.map((league) => ({
id: league.id,
name: league.name,
description: league.description,
ownerId: league.ownerId,
settings: league.settings,
createdAt: league.createdAt,
socialLinks: league.socialLinks,
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 {
if (items.length === 0) {
throw new Error('pickOne: empty items array');
}
const index = faker.number.int({ min: 0, max: items.length - 1 });
return items[index]!;
}