292 lines
9.3 KiB
TypeScript
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]!;
|
|
} |