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 { FriendDTO } from '@core/social/application/dto/FriendDTO'; 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[], ): FriendDTO[] { return drivers.map((driver) => { const membership = memberships.find((m) => m.driverId === driver.id); const base: FriendDTO = { 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(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]!; }