This commit is contained in:
2025-12-12 01:11:36 +01:00
parent ec3ddc3a5c
commit 6a88fe93ab
125 changed files with 1513 additions and 803 deletions

View File

@@ -33,17 +33,20 @@ function hashString(input: string): number {
export function getDriverAvatar(driverId: string): string {
const index = hashString(driverId) % DRIVER_AVATARS.length;
return DRIVER_AVATARS[index];
const avatar = DRIVER_AVATARS[index] ?? DRIVER_AVATARS[0];
return avatar;
}
export function getTeamLogo(teamId: string): string {
const index = hashString(teamId) % TEAM_LOGOS.length;
return TEAM_LOGOS[index];
const logo = TEAM_LOGOS[index] ?? TEAM_LOGOS[0];
return logo;
}
export function getLeagueBanner(leagueId: string): string {
const index = hashString(leagueId) % LEAGUE_BANNERS.length;
return LEAGUE_BANNERS[index];
const banner = LEAGUE_BANNERS[index] ?? LEAGUE_BANNERS[0];
return banner;
}
export interface LeagueCoverImage {

View File

@@ -81,7 +81,7 @@ export class DemoAvatarGenerationAdapter implements AvatarGenerationPort {
// For demo, return placeholder URLs based on suit color
// In production, these would be actual AI-generated images
const colorAvatars = this.placeholderAvatars[options.suitColor] ?? this.placeholderAvatars.blue;
const colorAvatars = this.getPlaceholderAvatars(options.suitColor) ?? [];
// Generate unique URLs with a hash to simulate different generations
const hash = this.generateHash((options.facePhotoUrl ?? '') + Date.now());
@@ -104,6 +104,14 @@ export class DemoAvatarGenerationAdapter implements AvatarGenerationPort {
return new Promise((resolve) => setTimeout(resolve, ms));
}
private getPlaceholderAvatars(color: string): string[] | undefined {
const avatars = this.placeholderAvatars[color];
if (!avatars || avatars.length === 0) {
return this.placeholderAvatars.blue;
}
return avatars;
}
private generateHash(input: string): string {
let hash = 0;
for (let i = 0; i < input.length; i += 1) {

View File

@@ -7,7 +7,8 @@ export class DemoImageServiceAdapter implements ImageServicePort {
getDriverAvatar(driverId: string): string {
const numericSuffixMatch = driverId.match(/(\d+)$/);
if (numericSuffixMatch) {
const numericSuffix = Number.parseInt(numericSuffixMatch[1], 10);
const numericSuffixString = numericSuffixMatch[1] ?? '';
const numericSuffix = Number.parseInt(numericSuffixString, 10);
return numericSuffix % 2 === 0 ? FEMALE_DEFAULT_AVATAR : MALE_DEFAULT_AVATAR;
}

View File

@@ -38,7 +38,14 @@ export class InMemoryAvatarGenerationRepository implements IAvatarGenerationRepo
async findLatestByUserId(userId: string): Promise<AvatarGenerationRequest | null> {
const userRequests = await this.findByUserId(userId);
return userRequests.length > 0 ? userRequests[0] : null;
if (userRequests.length === 0) {
return null;
}
const latest = userRequests[0];
if (!latest) {
return null;
}
return latest;
}
async delete(id: string): Promise<void> {

View File

@@ -23,11 +23,12 @@ export function createFeedEvents(
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);
const coreDrivers = faker.helpers.shuffle(drivers).slice(0, Math.min(16, drivers.length));
coreDrivers.forEach((driver, index) => {
const league = pickOne(leagues);
const race = completedRaces[index % Math.max(1, completedRaces.length)];
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);
@@ -166,23 +167,54 @@ 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,
}));
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 function buildTopLeagues(leagues: League[]): Array<League & { bannerUrl: string }> {
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) => ({
...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),
}));
}
@@ -252,5 +284,9 @@ export function buildLatestResults(
* 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 }))];
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]!;
}

View File

@@ -47,7 +47,11 @@ export const POINTS_TABLE: Record<number, number> = {
};
export function pickOne<T>(items: readonly T[]): T {
return items[Math.floor(faker.number.int({ min: 0, max: items.length - 1 }))];
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]!;
}
export function createDrivers(count: number): Driver[] {
@@ -136,18 +140,31 @@ export function createLeagues(ownerIds: string[]): League[] {
websiteUrl: 'https://virtual-touring.example.com',
}
: undefined;
leagues.push(
League.create({
id,
name,
description: faker.lorem.sentence(),
ownerId,
settings,
createdAt: faker.date.past(),
socialLinks,
}),
);
if (socialLinks) {
leagues.push(
League.create({
id,
name,
description: faker.lorem.sentence(),
ownerId,
settings,
createdAt: faker.date.past(),
socialLinks,
}),
);
} else {
leagues.push(
League.create({
id,
name,
description: faker.lorem.sentence(),
ownerId,
settings,
createdAt: faker.date.past(),
}),
);
}
}
return leagues;
@@ -204,11 +221,16 @@ export function createMemberships(
? pickOne(leagueTeams)
: undefined;
memberships.push({
const membership: RacingMembership = {
driverId: driver.id,
leagueId: league.id,
teamId: team?.id,
});
};
if (team) {
membership.teamId = team.id;
}
memberships.push(membership);
});
});
@@ -354,6 +376,7 @@ export function createFriendships(drivers: Driver[]): Friendship[] {
for (let offset = 1; offset <= friendCount; offset++) {
const friendIndex = (index + offset) % drivers.length;
const friend = drivers[friendIndex];
if (!friend) continue;
if (friend.id === driver.id) continue;
friendships.push({

View File

@@ -334,12 +334,14 @@ export function createSponsorshipRequests(
// 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,
const targetDriver = drivers[5];
if (targetDriver) {
requests.push(
SponsorshipRequest.create({
id: 'req-simucube-driver-1',
sponsorId: SIMUCUBE_ID,
entityType: 'driver',
entityId: targetDriver.id,
tier: 'main',
offeredAmount: Money.create(250, 'USD'),
message:
@@ -347,23 +349,27 @@ export function createSponsorshipRequests(
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
}),
);
const targetTeam = teams[2];
if (targetTeam) {
requests.push(
SponsorshipRequest.create({
id: 'req-heusinkveld-team-1',
sponsorId: HEUSINKVELD_ID,
entityType: 'team',
entityId: targetTeam.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
@@ -403,12 +409,14 @@ export function createSponsorshipRequests(
// 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,
const acceptedTeam = teams[0];
if (acceptedTeam) {
requests.push(
SponsorshipRequest.create({
id: 'req-simlab-team-accepted',
sponsorId: SIMLAB_ID,
entityType: 'team',
entityId: acceptedTeam.id,
tier: 'secondary',
offeredAmount: Money.create(300, 'USD'),
message: 'Sim-Lab rigs are the foundation of any competitive setup.',
@@ -416,16 +424,19 @@ export function createSponsorshipRequests(
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,
const rejectedDriver = drivers[10];
if (rejectedDriver) {
requests.push(
SponsorshipRequest.create({
id: 'req-motionrig-driver-rejected',
sponsorId: MOTIONRIG_ID,
entityType: 'driver',
entityId: rejectedDriver.id,
tier: 'main',
offeredAmount: Money.create(150, 'USD'),
message: 'Would you like to represent MotionRig Pro?',
@@ -433,6 +444,7 @@ export function createSponsorshipRequests(
createdAt: new Date(now.getTime() - 20 * 24 * 60 * 60 * 1000), // 20 days ago
}),
);
}
}
return requests;

View File

@@ -1,5 +1,5 @@
{
"extends": "../../tsconfig.json",
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "dist",