more seeds
This commit is contained in:
@@ -47,6 +47,7 @@ export const BootstrapProviders: Provider[] = [
|
||||
driverRepository: RacingSeedDependencies['driverRepository'],
|
||||
leagueRepository: RacingSeedDependencies['leagueRepository'],
|
||||
seasonRepository: RacingSeedDependencies['seasonRepository'],
|
||||
leagueScoringConfigRepository: RacingSeedDependencies['leagueScoringConfigRepository'],
|
||||
seasonSponsorshipRepository: RacingSeedDependencies['seasonSponsorshipRepository'],
|
||||
sponsorshipRequestRepository: RacingSeedDependencies['sponsorshipRequestRepository'],
|
||||
leagueWalletRepository: RacingSeedDependencies['leagueWalletRepository'],
|
||||
@@ -67,6 +68,7 @@ export const BootstrapProviders: Provider[] = [
|
||||
driverRepository,
|
||||
leagueRepository,
|
||||
seasonRepository,
|
||||
leagueScoringConfigRepository,
|
||||
seasonSponsorshipRepository,
|
||||
sponsorshipRequestRepository,
|
||||
leagueWalletRepository,
|
||||
@@ -88,6 +90,7 @@ export const BootstrapProviders: Provider[] = [
|
||||
'IDriverRepository',
|
||||
'ILeagueRepository',
|
||||
'ISeasonRepository',
|
||||
'ILeagueScoringConfigRepository',
|
||||
'ISeasonSponsorshipRepository',
|
||||
'ISponsorshipRequestRepository',
|
||||
'ILeagueWalletRepository',
|
||||
|
||||
@@ -3,6 +3,7 @@ import { ApiBody, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||
import { Public } from '../auth/Public';
|
||||
import { LeagueService } from './LeagueService';
|
||||
import { AllLeaguesWithCapacityDTO } from './dtos/AllLeaguesWithCapacityDTO';
|
||||
import { AllLeaguesWithCapacityAndScoringDTO } from './dtos/AllLeaguesWithCapacityAndScoringDTO';
|
||||
import { ApproveJoinRequestInputDTO } from './dtos/ApproveJoinRequestInputDTO';
|
||||
import { ApproveJoinRequestOutputDTO } from './dtos/ApproveJoinRequestOutputDTO';
|
||||
import { CreateLeagueInputDTO } from './dtos/CreateLeagueInputDTO';
|
||||
@@ -49,6 +50,14 @@ export class LeagueController {
|
||||
return this.leagueService.getAllLeaguesWithCapacity();
|
||||
}
|
||||
|
||||
@Public()
|
||||
@Get('all-with-capacity-and-scoring')
|
||||
@ApiOperation({ summary: 'Get all leagues with capacity and scoring information' })
|
||||
@ApiResponse({ status: 200, description: 'List of leagues with capacity and scoring', type: AllLeaguesWithCapacityAndScoringDTO })
|
||||
async getAllLeaguesWithCapacityAndScoring(): Promise<AllLeaguesWithCapacityAndScoringDTO> {
|
||||
return this.leagueService.getAllLeaguesWithCapacityAndScoring();
|
||||
}
|
||||
|
||||
@Public()
|
||||
@Get('total-leagues')
|
||||
@ApiOperation({ summary: 'Get the total number of leagues' })
|
||||
|
||||
@@ -15,16 +15,15 @@ import type { Logger } from '@core/shared/application/Logger';
|
||||
// Import concrete in-memory implementations
|
||||
import type { ILeagueWalletRepository } from "@core/racing/domain/repositories/ILeagueWalletRepository";
|
||||
import type { ITransactionRepository } from "@core/racing/domain/repositories/ITransactionRepository";
|
||||
import { listLeagueScoringPresets } from '@adapters/bootstrap/LeagueScoringPresets';
|
||||
import { getLeagueScoringPresetById, listLeagueScoringPresets } from '@adapters/bootstrap/LeagueScoringPresets';
|
||||
import { ConsoleLogger } from '@adapters/logging/ConsoleLogger';
|
||||
import { InMemoryGameRepository } from '@adapters/racing/persistence/inmemory/InMemoryGameRepository';
|
||||
import { InMemoryLeagueScoringConfigRepository } from '@adapters/racing/persistence/inmemory/InMemoryLeagueScoringConfigRepository';
|
||||
import { InMemoryLeagueStandingsRepository } from '@adapters/racing/persistence/inmemory/InMemoryLeagueStandingsRepository';
|
||||
|
||||
// Import use cases
|
||||
import { ApproveLeagueJoinRequestUseCase } from '@core/racing/application/use-cases/ApproveLeagueJoinRequestUseCase';
|
||||
import { CreateLeagueWithSeasonAndScoringUseCase } from '@core/racing/application/use-cases/CreateLeagueWithSeasonAndScoringUseCase';
|
||||
import { GetAllLeaguesWithCapacityUseCase } from '@core/racing/application/use-cases/GetAllLeaguesWithCapacityUseCase';
|
||||
import { GetAllLeaguesWithCapacityAndScoringUseCase } from '@core/racing/application/use-cases/GetAllLeaguesWithCapacityAndScoringUseCase';
|
||||
import { GetLeagueAdminPermissionsUseCase } from '@core/racing/application/use-cases/GetLeagueAdminPermissionsUseCase';
|
||||
import { GetLeagueFullConfigUseCase } from '@core/racing/application/use-cases/GetLeagueFullConfigUseCase';
|
||||
import { GetLeagueJoinRequestsUseCase } from '@core/racing/application/use-cases/GetLeagueJoinRequestsUseCase';
|
||||
@@ -50,6 +49,7 @@ import { WithdrawFromLeagueWalletUseCase } from '@core/racing/application/use-ca
|
||||
|
||||
// Import presenters
|
||||
import { AllLeaguesWithCapacityPresenter } from './presenters/AllLeaguesWithCapacityPresenter';
|
||||
import { AllLeaguesWithCapacityAndScoringPresenter } from './presenters/AllLeaguesWithCapacityAndScoringPresenter';
|
||||
import { ApproveLeagueJoinRequestPresenter } from './presenters/ApproveLeagueJoinRequestPresenter';
|
||||
import { CreateLeaguePresenter } from './presenters/CreateLeaguePresenter';
|
||||
import { GetLeagueAdminPermissionsPresenter } from './presenters/GetLeagueAdminPermissionsPresenter';
|
||||
@@ -89,6 +89,7 @@ export const TRANSACTION_REPOSITORY_TOKEN = 'ITransactionRepository';
|
||||
export const LOGGER_TOKEN = 'Logger';
|
||||
export const GET_LEAGUE_STANDINGS_USE_CASE = 'GetLeagueStandingsUseCase';
|
||||
export const GET_ALL_LEAGUES_WITH_CAPACITY_USE_CASE = 'GetAllLeaguesWithCapacityUseCase';
|
||||
export const GET_ALL_LEAGUES_WITH_CAPACITY_AND_SCORING_USE_CASE = 'GetAllLeaguesWithCapacityAndScoringUseCase';
|
||||
export const GET_LEAGUE_STATS_USE_CASE = 'GetLeagueStatsUseCase';
|
||||
export const GET_LEAGUE_FULL_CONFIG_USE_CASE = 'GetLeagueFullConfigUseCase';
|
||||
export const GET_LEAGUE_SCORING_CONFIG_USE_CASE = 'GetLeagueScoringConfigUseCase';
|
||||
@@ -114,6 +115,7 @@ export const WITHDRAW_FROM_LEAGUE_WALLET_USE_CASE = 'WithdrawFromLeagueWalletUse
|
||||
export const GET_SEASON_SPONSORSHIPS_USE_CASE = 'GetSeasonSponsorshipsUseCase';
|
||||
|
||||
export const GET_ALL_LEAGUES_WITH_CAPACITY_OUTPUT_PORT_TOKEN = 'GetAllLeaguesWithCapacityOutputPort_TOKEN';
|
||||
export const GET_ALL_LEAGUES_WITH_CAPACITY_AND_SCORING_OUTPUT_PORT_TOKEN = 'GetAllLeaguesWithCapacityAndScoringOutputPort_TOKEN';
|
||||
export const GET_LEAGUE_STANDINGS_OUTPUT_PORT_TOKEN = 'GetLeagueStandingsOutputPort_TOKEN';
|
||||
export const GET_LEAGUE_PROTESTS_OUTPUT_PORT_TOKEN = 'GetLeagueProtestsOutputPort_TOKEN';
|
||||
export const GET_SEASON_SPONSORSHIPS_OUTPUT_PORT_TOKEN = 'GetSeasonSponsorshipsOutputPort_TOKEN';
|
||||
@@ -144,22 +146,13 @@ export const LeagueProviders: Provider[] = [
|
||||
useFactory: (logger: Logger) => new InMemoryLeagueStandingsRepository(logger), // Factory for InMemoryLeagueStandingsRepository
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: LEAGUE_SCORING_CONFIG_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: Logger) => new InMemoryLeagueScoringConfigRepository(logger), // Factory for InMemoryLeagueScoringConfigRepository
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: GAME_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: Logger) => new InMemoryGameRepository(logger),
|
||||
inject: [LOGGER_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: LOGGER_TOKEN,
|
||||
useClass: ConsoleLogger,
|
||||
},
|
||||
// Presenters
|
||||
AllLeaguesWithCapacityPresenter,
|
||||
AllLeaguesWithCapacityAndScoringPresenter,
|
||||
ApproveLeagueJoinRequestPresenter,
|
||||
CreateLeaguePresenter,
|
||||
GetLeagueAdminPermissionsPresenter,
|
||||
@@ -189,6 +182,10 @@ export const LeagueProviders: Provider[] = [
|
||||
provide: GET_ALL_LEAGUES_WITH_CAPACITY_OUTPUT_PORT_TOKEN,
|
||||
useExisting: AllLeaguesWithCapacityPresenter,
|
||||
},
|
||||
{
|
||||
provide: GET_ALL_LEAGUES_WITH_CAPACITY_AND_SCORING_OUTPUT_PORT_TOKEN,
|
||||
useExisting: AllLeaguesWithCapacityAndScoringPresenter,
|
||||
},
|
||||
{
|
||||
provide: GET_LEAGUE_STANDINGS_OUTPUT_PORT_TOKEN,
|
||||
useExisting: LeagueStandingsPresenter,
|
||||
@@ -284,6 +281,34 @@ export const LeagueProviders: Provider[] = [
|
||||
new GetAllLeaguesWithCapacityUseCase(leagueRepo, membershipRepo),
|
||||
inject: [LEAGUE_REPOSITORY_TOKEN, LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN],
|
||||
},
|
||||
{
|
||||
provide: GET_ALL_LEAGUES_WITH_CAPACITY_AND_SCORING_USE_CASE,
|
||||
useFactory: (
|
||||
leagueRepo: ILeagueRepository,
|
||||
membershipRepo: ILeagueMembershipRepository,
|
||||
seasonRepo: ISeasonRepository,
|
||||
scoringRepo: import('@core/racing/domain/repositories/ILeagueScoringConfigRepository').ILeagueScoringConfigRepository,
|
||||
gameRepo: import('@core/racing/domain/repositories/IGameRepository').IGameRepository,
|
||||
output: AllLeaguesWithCapacityAndScoringPresenter,
|
||||
) =>
|
||||
new GetAllLeaguesWithCapacityAndScoringUseCase(
|
||||
leagueRepo,
|
||||
membershipRepo,
|
||||
seasonRepo,
|
||||
scoringRepo,
|
||||
gameRepo,
|
||||
{ getPresetById: getLeagueScoringPresetById },
|
||||
output,
|
||||
),
|
||||
inject: [
|
||||
LEAGUE_REPOSITORY_TOKEN,
|
||||
LEAGUE_MEMBERSHIP_REPOSITORY_TOKEN,
|
||||
SEASON_REPOSITORY_TOKEN,
|
||||
LEAGUE_SCORING_CONFIG_REPOSITORY_TOKEN,
|
||||
GAME_REPOSITORY_TOKEN,
|
||||
GET_ALL_LEAGUES_WITH_CAPACITY_AND_SCORING_OUTPUT_PORT_TOKEN,
|
||||
],
|
||||
},
|
||||
{
|
||||
provide: GET_LEAGUE_STANDINGS_USE_CASE,
|
||||
useFactory: (
|
||||
|
||||
@@ -10,6 +10,7 @@ describe('LeagueService', () => {
|
||||
const err = async () => Result.err({ code: 'REPOSITORY_ERROR', details: { message: 'boom' } });
|
||||
|
||||
const getAllLeaguesWithCapacityUseCase: any = { execute: vi.fn(async () => Result.ok({ leagues: [] })) };
|
||||
const getAllLeaguesWithCapacityAndScoringUseCase: any = { execute: vi.fn(ok) };
|
||||
const getLeagueStandingsUseCase = { execute: vi.fn(ok) };
|
||||
const getLeagueStatsUseCase = { execute: vi.fn(ok) };
|
||||
const getLeagueFullConfigUseCase: any = { execute: vi.fn(ok) };
|
||||
@@ -35,6 +36,10 @@ describe('LeagueService', () => {
|
||||
const getSeasonSponsorshipsUseCase = { execute: vi.fn(ok) };
|
||||
|
||||
const allLeaguesWithCapacityPresenter = { present: vi.fn(), getViewModel: vi.fn(() => ({ leagues: [] })) };
|
||||
const allLeaguesWithCapacityAndScoringPresenter = {
|
||||
present: vi.fn(),
|
||||
getViewModel: vi.fn(() => ({ leagues: [], totalCount: 0 })),
|
||||
};
|
||||
const leagueStandingsPresenter = { getResponseModel: vi.fn(() => ({ standings: [] })) };
|
||||
const leagueProtestsPresenter = { getResponseModel: vi.fn(() => ({ protests: [] })) };
|
||||
const seasonSponsorshipsPresenter = { getViewModel: vi.fn(() => ({ sponsorships: [] })) };
|
||||
@@ -62,6 +67,7 @@ describe('LeagueService', () => {
|
||||
|
||||
const service = new LeagueService(
|
||||
getAllLeaguesWithCapacityUseCase as any,
|
||||
getAllLeaguesWithCapacityAndScoringUseCase as any,
|
||||
getLeagueStandingsUseCase as any,
|
||||
getLeagueStatsUseCase as any,
|
||||
getLeagueFullConfigUseCase as any,
|
||||
@@ -87,6 +93,7 @@ describe('LeagueService', () => {
|
||||
getSeasonSponsorshipsUseCase as any,
|
||||
logger as any,
|
||||
allLeaguesWithCapacityPresenter as any,
|
||||
allLeaguesWithCapacityAndScoringPresenter as any,
|
||||
leagueStandingsPresenter as any,
|
||||
leagueProtestsPresenter as any,
|
||||
seasonSponsorshipsPresenter as any,
|
||||
@@ -149,6 +156,7 @@ describe('LeagueService', () => {
|
||||
});
|
||||
|
||||
await expect(service.getAllLeaguesWithCapacity()).resolves.toEqual({ leagues: [] });
|
||||
await expect(service.getAllLeaguesWithCapacityAndScoring()).resolves.toEqual({ leagues: [], totalCount: 0 });
|
||||
|
||||
// Error branch: getAllLeaguesWithCapacity throws on result.isErr()
|
||||
getAllLeaguesWithCapacityUseCase.execute.mockResolvedValueOnce(Result.err({ code: 'REPOSITORY_ERROR', details: { message: 'boom' } }));
|
||||
|
||||
@@ -33,6 +33,7 @@ import { WithdrawFromLeagueWalletOutputDTO } from './dtos/WithdrawFromLeagueWall
|
||||
|
||||
// Core imports for view models
|
||||
import type { AllLeaguesWithCapacityDTO as AllLeaguesWithCapacityViewModel } from './dtos/AllLeaguesWithCapacityDTO';
|
||||
import type { AllLeaguesWithCapacityAndScoringDTO as AllLeaguesWithCapacityAndScoringViewModel } from './dtos/AllLeaguesWithCapacityAndScoringDTO';
|
||||
import type { CreateLeagueViewModel } from './dtos/CreateLeagueDTO';
|
||||
import type { JoinLeagueOutputDTO } from './dtos/JoinLeagueOutputDTO';
|
||||
import { TotalLeaguesDTO } from './dtos/TotalLeaguesDTO';
|
||||
@@ -46,6 +47,7 @@ import type { Logger } from '@core/shared/application';
|
||||
import { ApproveLeagueJoinRequestUseCase } from '@core/racing/application/use-cases/ApproveLeagueJoinRequestUseCase';
|
||||
import { CreateLeagueWithSeasonAndScoringUseCase } from '@core/racing/application/use-cases/CreateLeagueWithSeasonAndScoringUseCase';
|
||||
import { GetAllLeaguesWithCapacityUseCase } from '@core/racing/application/use-cases/GetAllLeaguesWithCapacityUseCase';
|
||||
import { GetAllLeaguesWithCapacityAndScoringUseCase } from '@core/racing/application/use-cases/GetAllLeaguesWithCapacityAndScoringUseCase';
|
||||
import { GetLeagueAdminPermissionsUseCase } from '@core/racing/application/use-cases/GetLeagueAdminPermissionsUseCase';
|
||||
import { GetLeagueFullConfigUseCase } from '@core/racing/application/use-cases/GetLeagueFullConfigUseCase';
|
||||
import { GetLeagueJoinRequestsUseCase } from '@core/racing/application/use-cases/GetLeagueJoinRequestsUseCase';
|
||||
@@ -70,6 +72,7 @@ import { WithdrawFromLeagueWalletUseCase } from '@core/racing/application/use-ca
|
||||
|
||||
// API Presenters
|
||||
import { AllLeaguesWithCapacityPresenter } from './presenters/AllLeaguesWithCapacityPresenter';
|
||||
import { AllLeaguesWithCapacityAndScoringPresenter } from './presenters/AllLeaguesWithCapacityAndScoringPresenter';
|
||||
import { ApproveLeagueJoinRequestPresenter } from './presenters/ApproveLeagueJoinRequestPresenter';
|
||||
import { CreateLeaguePresenter } from './presenters/CreateLeaguePresenter';
|
||||
import { GetLeagueAdminPermissionsPresenter } from './presenters/GetLeagueAdminPermissionsPresenter';
|
||||
@@ -98,6 +101,7 @@ import {
|
||||
LOGGER_TOKEN,
|
||||
GET_ALL_LEAGUES_WITH_CAPACITY_USE_CASE,
|
||||
GET_LEAGUE_STANDINGS_USE_CASE,
|
||||
GET_ALL_LEAGUES_WITH_CAPACITY_AND_SCORING_USE_CASE,
|
||||
GET_LEAGUE_STATS_USE_CASE,
|
||||
GET_LEAGUE_FULL_CONFIG_USE_CASE,
|
||||
GET_LEAGUE_SCORING_CONFIG_USE_CASE,
|
||||
@@ -121,6 +125,7 @@ import {
|
||||
WITHDRAW_FROM_LEAGUE_WALLET_USE_CASE,
|
||||
GET_SEASON_SPONSORSHIPS_USE_CASE,
|
||||
GET_ALL_LEAGUES_WITH_CAPACITY_OUTPUT_PORT_TOKEN,
|
||||
GET_ALL_LEAGUES_WITH_CAPACITY_AND_SCORING_OUTPUT_PORT_TOKEN,
|
||||
GET_LEAGUE_STANDINGS_OUTPUT_PORT_TOKEN,
|
||||
GET_LEAGUE_PROTESTS_OUTPUT_PORT_TOKEN,
|
||||
GET_SEASON_SPONSORSHIPS_OUTPUT_PORT_TOKEN,
|
||||
@@ -149,6 +154,7 @@ import {
|
||||
export class LeagueService {
|
||||
constructor(
|
||||
@Inject(GET_ALL_LEAGUES_WITH_CAPACITY_USE_CASE) private readonly getAllLeaguesWithCapacityUseCase: GetAllLeaguesWithCapacityUseCase,
|
||||
@Inject(GET_ALL_LEAGUES_WITH_CAPACITY_AND_SCORING_USE_CASE) private readonly getAllLeaguesWithCapacityAndScoringUseCase: GetAllLeaguesWithCapacityAndScoringUseCase,
|
||||
@Inject(GET_LEAGUE_STANDINGS_USE_CASE) private readonly getLeagueStandingsUseCase: GetLeagueStandingsUseCase,
|
||||
@Inject(GET_LEAGUE_STATS_USE_CASE) private readonly getLeagueStatsUseCase: GetLeagueStatsUseCase,
|
||||
@Inject(GET_LEAGUE_FULL_CONFIG_USE_CASE) private readonly getLeagueFullConfigUseCase: GetLeagueFullConfigUseCase,
|
||||
@@ -175,6 +181,7 @@ export class LeagueService {
|
||||
@Inject(LOGGER_TOKEN) private readonly logger: Logger,
|
||||
// Injected presenters
|
||||
@Inject(GET_ALL_LEAGUES_WITH_CAPACITY_OUTPUT_PORT_TOKEN) private readonly allLeaguesWithCapacityPresenter: AllLeaguesWithCapacityPresenter,
|
||||
@Inject(GET_ALL_LEAGUES_WITH_CAPACITY_AND_SCORING_OUTPUT_PORT_TOKEN) private readonly allLeaguesWithCapacityAndScoringPresenter: AllLeaguesWithCapacityAndScoringPresenter,
|
||||
@Inject(GET_LEAGUE_STANDINGS_OUTPUT_PORT_TOKEN) private readonly leagueStandingsPresenter: LeagueStandingsPresenter,
|
||||
@Inject(GET_LEAGUE_PROTESTS_OUTPUT_PORT_TOKEN) private readonly leagueProtestsPresenter: GetLeagueProtestsPresenter,
|
||||
@Inject(GET_SEASON_SPONSORSHIPS_OUTPUT_PORT_TOKEN) private readonly seasonSponsorshipsPresenter: GetSeasonSponsorshipsPresenter,
|
||||
@@ -218,6 +225,22 @@ export class LeagueService {
|
||||
return this.allLeaguesWithCapacityPresenter.getViewModel();
|
||||
}
|
||||
|
||||
async getAllLeaguesWithCapacityAndScoring(): Promise<AllLeaguesWithCapacityAndScoringViewModel> {
|
||||
this.logger.debug('[LeagueService] Fetching all leagues with capacity and scoring.');
|
||||
|
||||
const result = await this.getAllLeaguesWithCapacityAndScoringUseCase.execute({});
|
||||
|
||||
if (result.isErr()) {
|
||||
const err = result.unwrapErr();
|
||||
this.logger.error('[LeagueService] Failed to fetch leagues with capacity and scoring', new Error(err.code), {
|
||||
details: err.details,
|
||||
});
|
||||
throw new Error(err.code);
|
||||
}
|
||||
|
||||
return this.allLeaguesWithCapacityAndScoringPresenter.getViewModel();
|
||||
}
|
||||
|
||||
async getTotalLeagues(): Promise<TotalLeaguesDTO> {
|
||||
this.logger.debug('[LeagueService] Fetching total leagues count.');
|
||||
await this.getTotalLeaguesUseCase.execute({});
|
||||
|
||||
@@ -14,6 +14,7 @@ export const LOGGER_TOKEN = 'Logger';
|
||||
|
||||
export const GET_LEAGUE_STANDINGS_USE_CASE = 'GetLeagueStandingsUseCase';
|
||||
export const GET_ALL_LEAGUES_WITH_CAPACITY_USE_CASE = 'GetAllLeaguesWithCapacityUseCase';
|
||||
export const GET_ALL_LEAGUES_WITH_CAPACITY_AND_SCORING_USE_CASE = 'GetAllLeaguesWithCapacityAndScoringUseCase';
|
||||
export const GET_LEAGUE_STATS_USE_CASE = 'GetLeagueStatsUseCase';
|
||||
export const GET_LEAGUE_FULL_CONFIG_USE_CASE = 'GetLeagueFullConfigUseCase';
|
||||
export const GET_LEAGUE_SCORING_CONFIG_USE_CASE = 'GetLeagueScoringConfigUseCase';
|
||||
@@ -39,6 +40,7 @@ export const WITHDRAW_FROM_LEAGUE_WALLET_USE_CASE = 'WithdrawFromLeagueWalletUse
|
||||
export const GET_SEASON_SPONSORSHIPS_USE_CASE = 'GetSeasonSponsorshipsUseCase';
|
||||
|
||||
export const GET_ALL_LEAGUES_WITH_CAPACITY_OUTPUT_PORT_TOKEN = 'GetAllLeaguesWithCapacityOutputPort_TOKEN';
|
||||
export const GET_ALL_LEAGUES_WITH_CAPACITY_AND_SCORING_OUTPUT_PORT_TOKEN = 'GetAllLeaguesWithCapacityAndScoringOutputPort_TOKEN';
|
||||
export const GET_LEAGUE_STANDINGS_OUTPUT_PORT_TOKEN = 'GetLeagueStandingsOutputPort_TOKEN';
|
||||
export const GET_LEAGUE_PROTESTS_OUTPUT_PORT_TOKEN = 'GetLeagueProtestsOutputPort_TOKEN';
|
||||
export const GET_SEASON_SPONSORSHIPS_OUTPUT_PORT_TOKEN = 'GetSeasonSponsorshipsOutputPort_TOKEN';
|
||||
|
||||
@@ -1,14 +1,124 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNumber, IsArray, ValidateNested } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
import { LeagueSummaryDTO } from './LeagueSummaryDTO';
|
||||
import { IsArray, IsNumber, IsOptional, IsString, ValidateNested } from 'class-validator';
|
||||
|
||||
export class LeagueCapacityAndScoringSocialLinksDTO {
|
||||
@ApiProperty({ required: false, nullable: true })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
discordUrl?: string;
|
||||
|
||||
@ApiProperty({ required: false, nullable: true })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
youtubeUrl?: string;
|
||||
|
||||
@ApiProperty({ required: false, nullable: true })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
websiteUrl?: string;
|
||||
}
|
||||
|
||||
export class LeagueCapacityAndScoringSettingsDTO {
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
maxDrivers!: number;
|
||||
|
||||
@ApiProperty({ required: false, nullable: true })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
sessionDuration?: number;
|
||||
|
||||
@ApiProperty({ required: false, nullable: true })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
qualifyingFormat?: string;
|
||||
}
|
||||
|
||||
export class LeagueCapacityAndScoringSummaryScoringDTO {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
gameId!: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
gameName!: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
primaryChampionshipType!: 'driver' | 'team' | 'nations' | 'trophy';
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
scoringPresetId!: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
scoringPresetName!: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
dropPolicySummary!: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
scoringPatternSummary!: string;
|
||||
}
|
||||
|
||||
export class LeagueWithCapacityAndScoringDTO {
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
id!: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
name!: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
description!: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
ownerId!: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
createdAt!: string;
|
||||
|
||||
@ApiProperty({ type: LeagueCapacityAndScoringSettingsDTO })
|
||||
@ValidateNested()
|
||||
@Type(() => LeagueCapacityAndScoringSettingsDTO)
|
||||
settings!: LeagueCapacityAndScoringSettingsDTO;
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
usedSlots!: number;
|
||||
|
||||
@ApiProperty({ required: false, nullable: true, type: LeagueCapacityAndScoringSocialLinksDTO })
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(() => LeagueCapacityAndScoringSocialLinksDTO)
|
||||
socialLinks?: LeagueCapacityAndScoringSocialLinksDTO;
|
||||
|
||||
@ApiProperty({ required: false, nullable: true, type: LeagueCapacityAndScoringSummaryScoringDTO })
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(() => LeagueCapacityAndScoringSummaryScoringDTO)
|
||||
scoring?: LeagueCapacityAndScoringSummaryScoringDTO;
|
||||
|
||||
@ApiProperty({ required: false, nullable: true })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
timingSummary?: string;
|
||||
}
|
||||
|
||||
export class AllLeaguesWithCapacityAndScoringDTO {
|
||||
@ApiProperty({ type: [LeagueSummaryDTO] })
|
||||
@ApiProperty({ type: [LeagueWithCapacityAndScoringDTO] })
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => LeagueSummaryDTO)
|
||||
leagues!: LeagueSummaryDTO[];
|
||||
@Type(() => LeagueWithCapacityAndScoringDTO)
|
||||
leagues!: LeagueWithCapacityAndScoringDTO[];
|
||||
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
|
||||
import type { GetAllLeaguesWithCapacityAndScoringResult } from '@core/racing/application/use-cases/GetAllLeaguesWithCapacityAndScoringUseCase';
|
||||
import type {
|
||||
AllLeaguesWithCapacityAndScoringDTO,
|
||||
LeagueWithCapacityAndScoringDTO,
|
||||
} from '../dtos/AllLeaguesWithCapacityAndScoringDTO';
|
||||
|
||||
export class AllLeaguesWithCapacityAndScoringPresenter
|
||||
implements UseCaseOutputPort<GetAllLeaguesWithCapacityAndScoringResult>
|
||||
{
|
||||
private result: AllLeaguesWithCapacityAndScoringDTO | null = null;
|
||||
|
||||
present(result: GetAllLeaguesWithCapacityAndScoringResult): void {
|
||||
const leagues: LeagueWithCapacityAndScoringDTO[] = result.leagues.map((summary) => {
|
||||
const timingSummary = summary.preset
|
||||
? formatTimingSummary(summary.preset.defaultTimings.mainRaceMinutes)
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
id: summary.league.id.toString(),
|
||||
name: summary.league.name.toString(),
|
||||
description: summary.league.description?.toString() || '',
|
||||
ownerId: summary.league.ownerId.toString(),
|
||||
createdAt: summary.league.createdAt.toDate().toISOString(),
|
||||
settings: {
|
||||
maxDrivers: summary.maxDrivers,
|
||||
...(summary.league.settings.sessionDuration !== undefined
|
||||
? { sessionDuration: summary.league.settings.sessionDuration }
|
||||
: {}),
|
||||
...(summary.league.settings.qualifyingFormat !== undefined
|
||||
? { qualifyingFormat: summary.league.settings.qualifyingFormat.toString() }
|
||||
: {}),
|
||||
},
|
||||
usedSlots: summary.currentDrivers,
|
||||
...mapSocialLinks(summary.league.socialLinks),
|
||||
...(summary.scoringConfig && summary.game && summary.preset
|
||||
? {
|
||||
scoring: {
|
||||
gameId: summary.game.id.toString(),
|
||||
gameName: summary.game.name.toString(),
|
||||
primaryChampionshipType: summary.preset.primaryChampionshipType,
|
||||
scoringPresetId: summary.scoringConfig.scoringPresetId?.toString() ?? 'custom',
|
||||
scoringPresetName: summary.preset.name,
|
||||
dropPolicySummary: summary.preset.dropPolicySummary,
|
||||
scoringPatternSummary: summary.preset.sessionSummary,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
...(timingSummary ? { timingSummary } : {}),
|
||||
};
|
||||
});
|
||||
|
||||
this.result = {
|
||||
leagues,
|
||||
totalCount: leagues.length,
|
||||
};
|
||||
}
|
||||
|
||||
getViewModel(): AllLeaguesWithCapacityAndScoringDTO {
|
||||
if (!this.result) throw new Error('Presenter not presented');
|
||||
return this.result;
|
||||
}
|
||||
}
|
||||
|
||||
function formatTimingSummary(mainRaceMinutes: number): string {
|
||||
if (!Number.isFinite(mainRaceMinutes) || mainRaceMinutes <= 0) return '';
|
||||
|
||||
if (mainRaceMinutes >= 60 && mainRaceMinutes % 60 === 0) {
|
||||
return `${mainRaceMinutes / 60}h Race`;
|
||||
}
|
||||
|
||||
return `${mainRaceMinutes}m Race`;
|
||||
}
|
||||
|
||||
type SocialLinksInput = {
|
||||
discordUrl?: string | undefined;
|
||||
youtubeUrl?: string | undefined;
|
||||
websiteUrl?: string | undefined;
|
||||
};
|
||||
|
||||
type SocialLinksOutput = {
|
||||
discordUrl?: string;
|
||||
youtubeUrl?: string;
|
||||
websiteUrl?: string;
|
||||
};
|
||||
|
||||
function mapSocialLinks(socialLinks: SocialLinksInput | undefined): { socialLinks: SocialLinksOutput } | {} {
|
||||
if (!socialLinks) return {};
|
||||
|
||||
const mapped: SocialLinksOutput = {
|
||||
...(socialLinks.discordUrl ? { discordUrl: socialLinks.discordUrl } : {}),
|
||||
...(socialLinks.youtubeUrl ? { youtubeUrl: socialLinks.youtubeUrl } : {}),
|
||||
...(socialLinks.websiteUrl ? { websiteUrl: socialLinks.websiteUrl } : {}),
|
||||
};
|
||||
|
||||
return Object.keys(mapped).length > 0 ? { socialLinks: mapped } : {};
|
||||
}
|
||||
@@ -21,6 +21,76 @@ type RequestAvatarGenerationInput = RequestAvatarGenerationInputDTO;
|
||||
type UploadMediaInput = UploadMediaInputDTO;
|
||||
type UpdateAvatarInput = UpdateAvatarInputDTO;
|
||||
|
||||
function hashToHue(input: string): number {
|
||||
let hash = 0;
|
||||
for (let i = 0; i < input.length; i += 1) {
|
||||
hash = (hash * 31 + input.charCodeAt(i)) | 0;
|
||||
}
|
||||
return Math.abs(hash) % 360;
|
||||
}
|
||||
|
||||
function escapeXml(input: string): string {
|
||||
return input
|
||||
.replaceAll('&', '\u0026amp;')
|
||||
.replaceAll('<', '\u0026lt;')
|
||||
.replaceAll('>', '\u0026gt;')
|
||||
.replaceAll('"', '\u0026quot;')
|
||||
.replaceAll("'", '\u0026apos;');
|
||||
}
|
||||
|
||||
function deriveLeagueLabel(leagueId: string): string {
|
||||
const digits = leagueId.match(/\d+/)?.[0];
|
||||
if (digits) return digits.slice(-2);
|
||||
return leagueId.replaceAll(/[^a-zA-Z]/g, '').slice(0, 2).toUpperCase() || 'GP';
|
||||
}
|
||||
|
||||
function buildLeagueLogoSvg(leagueId: string): string {
|
||||
const hue = hashToHue(leagueId);
|
||||
const label = escapeXml(deriveLeagueLabel(leagueId));
|
||||
const bg = `hsl(${hue} 70% 38%)`;
|
||||
const border = `hsl(${hue} 70% 28%)`;
|
||||
|
||||
return `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="96" height="96" viewBox="0 0 96 96" role="img" aria-label="League logo">
|
||||
<defs>
|
||||
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0%" stop-color="${bg}"/>
|
||||
<stop offset="100%" stop-color="hsl(${hue} 80% 46%)"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect x="2" y="2" width="92" height="92" rx="18" fill="url(#g)" stroke="${border}" stroke-width="4"/>
|
||||
<text x="48" y="56" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial" font-size="34" font-weight="800" text-anchor="middle" fill="white">${label}</text>
|
||||
</svg>`;
|
||||
}
|
||||
|
||||
function buildLeagueCoverSvg(leagueId: string): string {
|
||||
const hue = hashToHue(leagueId);
|
||||
const title = escapeXml(leagueId);
|
||||
const bg1 = `hsl(${hue} 70% 28%)`;
|
||||
const bg2 = `hsl(${(hue + 35) % 360} 85% 35%)`;
|
||||
|
||||
return `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="400" viewBox="0 0 1200 400" role="img" aria-label="League cover">
|
||||
<defs>
|
||||
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0%" stop-color="${bg1}"/>
|
||||
<stop offset="100%" stop-color="${bg2}"/>
|
||||
</linearGradient>
|
||||
<pattern id="grid" width="40" height="40" patternUnits="userSpaceOnUse">
|
||||
<path d="M 40 0 L 0 0 0 40" fill="none" stroke="rgba(255,255,255,0.07)" stroke-width="2"/>
|
||||
</pattern>
|
||||
</defs>
|
||||
|
||||
<rect width="1200" height="400" fill="url(#bg)"/>
|
||||
<rect width="1200" height="400" fill="url(#grid)"/>
|
||||
<circle cx="1020" cy="120" r="180" fill="rgba(255,255,255,0.06)"/>
|
||||
<circle cx="1080" cy="170" r="120" fill="rgba(255,255,255,0.05)"/>
|
||||
|
||||
<text x="64" y="110" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial" font-size="40" font-weight="800" fill="rgba(255,255,255,0.92)">GridPilot League</text>
|
||||
<text x="64" y="165" font-family="ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial" font-size="22" font-weight="600" fill="rgba(255,255,255,0.75)">${title}</text>
|
||||
</svg>`;
|
||||
}
|
||||
|
||||
@ApiTags('media')
|
||||
@Controller('media')
|
||||
export class MediaController {
|
||||
@@ -61,6 +131,34 @@ export class MediaController {
|
||||
}
|
||||
}
|
||||
|
||||
@Public()
|
||||
@Get('leagues/:leagueId/logo')
|
||||
@ApiOperation({ summary: 'Get league logo (placeholder)' })
|
||||
@ApiParam({ name: 'leagueId', description: 'League ID' })
|
||||
async getLeagueLogo(
|
||||
@Param('leagueId') leagueId: string,
|
||||
@Res() res: Response,
|
||||
): Promise<void> {
|
||||
const svg = buildLeagueLogoSvg(leagueId);
|
||||
res.setHeader('Content-Type', 'image/svg+xml; charset=utf-8');
|
||||
res.setHeader('Cache-Control', 'public, max-age=86400');
|
||||
res.status(HttpStatus.OK).send(svg);
|
||||
}
|
||||
|
||||
@Public()
|
||||
@Get('leagues/:leagueId/cover')
|
||||
@ApiOperation({ summary: 'Get league cover (placeholder)' })
|
||||
@ApiParam({ name: 'leagueId', description: 'League ID' })
|
||||
async getLeagueCover(
|
||||
@Param('leagueId') leagueId: string,
|
||||
@Res() res: Response,
|
||||
): Promise<void> {
|
||||
const svg = buildLeagueCoverSvg(leagueId);
|
||||
res.setHeader('Content-Type', 'image/svg+xml; charset=utf-8');
|
||||
res.setHeader('Cache-Control', 'public, max-age=86400');
|
||||
res.status(HttpStatus.OK).send(svg);
|
||||
}
|
||||
|
||||
@Public()
|
||||
@Get(':mediaId')
|
||||
@ApiOperation({ summary: 'Get media by ID' })
|
||||
|
||||
@@ -15,6 +15,8 @@ import type { IPenaltyRepository } from '@core/racing/domain/repositories/IPenal
|
||||
import type { IProtestRepository } from '@core/racing/domain/repositories/IProtestRepository';
|
||||
import type { ISeasonRepository } from '@core/racing/domain/repositories/ISeasonRepository';
|
||||
import type { ISeasonSponsorshipRepository } from '@core/racing/domain/repositories/ISeasonSponsorshipRepository';
|
||||
import type { ILeagueScoringConfigRepository } from '@core/racing/domain/repositories/ILeagueScoringConfigRepository';
|
||||
import type { IGameRepository } from '@core/racing/domain/repositories/IGameRepository';
|
||||
import type { ILeagueWalletRepository } from '@core/racing/domain/repositories/ILeagueWalletRepository';
|
||||
import type { ITransactionRepository } from '@core/racing/domain/repositories/ITransactionRepository';
|
||||
import type { ISponsorRepository } from '@core/racing/domain/repositories/ISponsorRepository';
|
||||
@@ -38,6 +40,8 @@ import { InMemoryPenaltyRepository } from '@adapters/racing/persistence/inmemory
|
||||
import { InMemoryProtestRepository } from '@adapters/racing/persistence/inmemory/InMemoryProtestRepository';
|
||||
import { InMemorySeasonRepository } from '@adapters/racing/persistence/inmemory/InMemorySeasonRepository';
|
||||
import { InMemorySeasonSponsorshipRepository } from '@adapters/racing/persistence/inmemory/InMemorySeasonSponsorshipRepository';
|
||||
import { InMemoryLeagueScoringConfigRepository } from '@adapters/racing/persistence/inmemory/InMemoryLeagueScoringConfigRepository';
|
||||
import { InMemoryGameRepository } from '@adapters/racing/persistence/inmemory/InMemoryGameRepository';
|
||||
import { InMemoryLeagueWalletRepository } from '@adapters/racing/persistence/inmemory/InMemoryLeagueWalletRepository';
|
||||
import { InMemoryTransactionRepository } from '@adapters/racing/persistence/inmemory/InMemoryTransactionRepository';
|
||||
import { InMemorySponsorRepository } from '@adapters/racing/persistence/inmemory/InMemorySponsorRepository';
|
||||
@@ -57,6 +61,8 @@ export const PENALTY_REPOSITORY_TOKEN = 'IPenaltyRepository';
|
||||
export const PROTEST_REPOSITORY_TOKEN = 'IProtestRepository';
|
||||
export const SEASON_REPOSITORY_TOKEN = 'ISeasonRepository';
|
||||
export const SEASON_SPONSORSHIP_REPOSITORY_TOKEN = 'ISeasonSponsorshipRepository';
|
||||
export const LEAGUE_SCORING_CONFIG_REPOSITORY_TOKEN = 'ILeagueScoringConfigRepository';
|
||||
export const GAME_REPOSITORY_TOKEN = 'IGameRepository';
|
||||
export const LEAGUE_WALLET_REPOSITORY_TOKEN = 'ILeagueWalletRepository';
|
||||
export const TRANSACTION_REPOSITORY_TOKEN = 'ITransactionRepository';
|
||||
export const SPONSOR_REPOSITORY_TOKEN = 'ISponsorRepository';
|
||||
@@ -137,6 +143,16 @@ export const SPONSORSHIP_REQUEST_REPOSITORY_TOKEN = 'ISponsorshipRequestReposito
|
||||
useFactory: (logger: Logger): ISeasonSponsorshipRepository => new InMemorySeasonSponsorshipRepository(logger),
|
||||
inject: ['Logger'],
|
||||
},
|
||||
{
|
||||
provide: LEAGUE_SCORING_CONFIG_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: Logger): ILeagueScoringConfigRepository => new InMemoryLeagueScoringConfigRepository(logger),
|
||||
inject: ['Logger'],
|
||||
},
|
||||
{
|
||||
provide: GAME_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: Logger): IGameRepository => new InMemoryGameRepository(logger),
|
||||
inject: ['Logger'],
|
||||
},
|
||||
{
|
||||
provide: LEAGUE_WALLET_REPOSITORY_TOKEN,
|
||||
useFactory: (logger: Logger): ILeagueWalletRepository => new InMemoryLeagueWalletRepository(logger),
|
||||
@@ -177,6 +193,8 @@ export const SPONSORSHIP_REQUEST_REPOSITORY_TOKEN = 'ISponsorshipRequestReposito
|
||||
PROTEST_REPOSITORY_TOKEN,
|
||||
SEASON_REPOSITORY_TOKEN,
|
||||
SEASON_SPONSORSHIP_REPOSITORY_TOKEN,
|
||||
LEAGUE_SCORING_CONFIG_REPOSITORY_TOKEN,
|
||||
GAME_REPOSITORY_TOKEN,
|
||||
LEAGUE_WALLET_REPOSITORY_TOKEN,
|
||||
TRANSACTION_REPOSITORY_TOKEN,
|
||||
SPONSOR_REPOSITORY_TOKEN,
|
||||
|
||||
@@ -105,12 +105,12 @@ export default function LeagueCard({ league, onClick }: LeagueCardProps) {
|
||||
<div className="relative h-full rounded-xl bg-iron-gray border border-charcoal-outline overflow-hidden transition-all duration-200 hover:border-primary-blue/50 hover:shadow-[0_0_30px_rgba(25,140,255,0.15)] hover:bg-iron-gray/80">
|
||||
{/* Cover Image */}
|
||||
<div className="relative h-32 overflow-hidden">
|
||||
<Image
|
||||
<img
|
||||
src={coverUrl}
|
||||
alt={`${league.name} cover`}
|
||||
fill
|
||||
className="object-cover transition-transform duration-300 group-hover:scale-105"
|
||||
sizes="(min-width: 1024px) 33vw, (min-width: 768px) 50vw, 100vw"
|
||||
className="absolute inset-0 h-full w-full object-cover transition-transform duration-300 group-hover:scale-105"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
{/* Gradient Overlay */}
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-iron-gray via-iron-gray/60 to-transparent" />
|
||||
@@ -141,12 +141,14 @@ export default function LeagueCard({ league, onClick }: LeagueCardProps) {
|
||||
{/* Logo */}
|
||||
<div className="absolute left-4 -bottom-6 z-10">
|
||||
<div className="w-12 h-12 rounded-lg overflow-hidden border-2 border-iron-gray bg-deep-graphite shadow-xl">
|
||||
<Image
|
||||
<img
|
||||
src={logoUrl}
|
||||
alt={`${league.name} logo`}
|
||||
width={48}
|
||||
height={48}
|
||||
className="w-full h-full object-cover"
|
||||
className="h-full w-full object-cover"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -37,12 +37,14 @@ export default function LeagueHeader({
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="h-16 w-16 rounded-xl overflow-hidden border-2 border-charcoal-outline bg-iron-gray shadow-lg">
|
||||
<Image
|
||||
<img
|
||||
src={logoUrl}
|
||||
alt={`${leagueName} logo`}
|
||||
width={64}
|
||||
height={64}
|
||||
className="w-full h-full object-cover"
|
||||
className="h-full w-full object-cover"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
@@ -11,6 +11,7 @@ import type { RaceDTO } from '../../types/generated/RaceDTO';
|
||||
import type { GetLeagueAdminConfigOutputDTO } from '../../types/generated/GetLeagueAdminConfigOutputDTO';
|
||||
import type { LeagueScoringPresetDTO } from '../../types/generated/LeagueScoringPresetDTO';
|
||||
import type { LeagueSeasonSummaryDTO } from '../../types/generated/LeagueSeasonSummaryDTO';
|
||||
import type { AllLeaguesWithCapacityAndScoringDTO } from '../../types/AllLeaguesWithCapacityAndScoringDTO';
|
||||
|
||||
/**
|
||||
* Leagues API Client
|
||||
@@ -23,6 +24,11 @@ export class LeaguesApiClient extends BaseApiClient {
|
||||
return this.get<AllLeaguesWithCapacityDTO>('/leagues/all-with-capacity');
|
||||
}
|
||||
|
||||
/** Get all leagues with capacity + scoring summary (for leagues page filters) */
|
||||
getAllWithCapacityAndScoring(): Promise<AllLeaguesWithCapacityAndScoringDTO> {
|
||||
return this.get<AllLeaguesWithCapacityAndScoringDTO>('/leagues/all-with-capacity-and-scoring');
|
||||
}
|
||||
|
||||
/** Get total number of leagues */
|
||||
getTotal(): Promise<TotalLeaguesDTO> {
|
||||
return this.get<TotalLeaguesDTO>('/leagues/total-leagues');
|
||||
|
||||
@@ -18,6 +18,7 @@ describe('LeagueService', () => {
|
||||
beforeEach(() => {
|
||||
mockApiClient = {
|
||||
getAllWithCapacity: vi.fn(),
|
||||
getAllWithCapacityAndScoring: vi.fn(),
|
||||
getStandings: vi.fn(),
|
||||
getTotal: vi.fn(),
|
||||
getSchedule: vi.fn(),
|
||||
@@ -30,7 +31,7 @@ describe('LeagueService', () => {
|
||||
});
|
||||
|
||||
describe('getAllLeagues', () => {
|
||||
it('should call apiClient.getAllWithCapacity and return array of LeagueSummaryViewModel', async () => {
|
||||
it('should call apiClient.getAllWithCapacityAndScoring and return array of LeagueSummaryViewModel', async () => {
|
||||
const mockDto = {
|
||||
totalCount: 2,
|
||||
leagues: [
|
||||
@@ -39,11 +40,11 @@ describe('LeagueService', () => {
|
||||
],
|
||||
} as any;
|
||||
|
||||
mockApiClient.getAllWithCapacity.mockResolvedValue(mockDto);
|
||||
mockApiClient.getAllWithCapacityAndScoring.mockResolvedValue(mockDto);
|
||||
|
||||
const result = await service.getAllLeagues();
|
||||
|
||||
expect(mockApiClient.getAllWithCapacity).toHaveBeenCalled();
|
||||
expect(mockApiClient.getAllWithCapacityAndScoring).toHaveBeenCalled();
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result.map((l) => l.id)).toEqual(['league-1', 'league-2']);
|
||||
});
|
||||
@@ -51,16 +52,16 @@ describe('LeagueService', () => {
|
||||
it('should handle empty leagues array', async () => {
|
||||
const mockDto = { totalCount: 0, leagues: [] } as any;
|
||||
|
||||
mockApiClient.getAllWithCapacity.mockResolvedValue(mockDto);
|
||||
mockApiClient.getAllWithCapacityAndScoring.mockResolvedValue(mockDto);
|
||||
|
||||
const result = await service.getAllLeagues();
|
||||
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should throw error when apiClient.getAllWithCapacity fails', async () => {
|
||||
it('should throw error when apiClient.getAllWithCapacityAndScoring fails', async () => {
|
||||
const error = new Error('API call failed');
|
||||
mockApiClient.getAllWithCapacity.mockRejectedValue(error);
|
||||
mockApiClient.getAllWithCapacityAndScoring.mockRejectedValue(error);
|
||||
|
||||
await expect(service.getAllLeagues()).rejects.toThrow('API call failed');
|
||||
});
|
||||
|
||||
@@ -44,17 +44,20 @@ export class LeagueService {
|
||||
* Get all leagues with view model transformation
|
||||
*/
|
||||
async getAllLeagues(): Promise<LeagueSummaryViewModel[]> {
|
||||
const dto = await this.apiClient.getAllWithCapacity();
|
||||
return dto.leagues.map((league: LeagueWithCapacityDTO) => ({
|
||||
const dto = await this.apiClient.getAllWithCapacityAndScoring();
|
||||
|
||||
return dto.leagues.map((league) => ({
|
||||
id: league.id,
|
||||
name: league.name,
|
||||
description: (league as any).description ?? '',
|
||||
ownerId: (league as any).ownerId ?? '',
|
||||
createdAt: (league as any).createdAt ?? '',
|
||||
maxDrivers: (league as any).settings?.maxDrivers ?? 0,
|
||||
usedDriverSlots: (league as any).usedSlots ?? 0,
|
||||
structureSummary: 'TBD',
|
||||
timingSummary: 'TBD'
|
||||
description: league.description,
|
||||
ownerId: league.ownerId,
|
||||
createdAt: league.createdAt,
|
||||
maxDrivers: league.settings?.maxDrivers ?? 0,
|
||||
usedDriverSlots: league.usedSlots ?? 0,
|
||||
structureSummary: league.scoring?.scoringPresetName ?? 'Custom rules',
|
||||
scoringPatternSummary: league.scoring?.scoringPatternSummary,
|
||||
timingSummary: league.timingSummary ?? '',
|
||||
...(league.scoring ? { scoring: league.scoring } : {}),
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -162,16 +165,16 @@ export class LeagueService {
|
||||
// For now, assume league data comes from getAllWithCapacity or a new endpoint
|
||||
// Since API may not have detailed league, we'll mock or assume
|
||||
// In real implementation, add getLeagueDetail to API
|
||||
const allLeagues = await this.apiClient.getAllWithCapacity();
|
||||
const leagueDto = allLeagues.leagues.find((l: any) => l.id === leagueId);
|
||||
const allLeagues = await this.apiClient.getAllWithCapacityAndScoring();
|
||||
const leagueDto = allLeagues.leagues.find((l) => l.id === leagueId);
|
||||
if (!leagueDto) return null;
|
||||
|
||||
// LeagueWithCapacityDTO already carries core fields; fall back to placeholder description/owner when not provided
|
||||
const league = {
|
||||
id: leagueDto.id,
|
||||
name: leagueDto.name,
|
||||
description: (leagueDto as any).description ?? 'Description not available',
|
||||
ownerId: (leagueDto as any).ownerId ?? 'owner-id',
|
||||
description: leagueDto.description ?? 'Description not available',
|
||||
ownerId: leagueDto.ownerId ?? 'owner-id',
|
||||
};
|
||||
|
||||
// Get owner
|
||||
@@ -245,12 +248,12 @@ export class LeagueService {
|
||||
|
||||
try {
|
||||
// Get league basic info
|
||||
const allLeagues = await this.apiClient.getAllWithCapacity();
|
||||
const league = allLeagues.leagues.find((l: any) => l.id === leagueId);
|
||||
const allLeagues = await this.apiClient.getAllWithCapacityAndScoring();
|
||||
const league = allLeagues.leagues.find((l) => l.id === leagueId);
|
||||
if (!league) return null;
|
||||
|
||||
// Get owner
|
||||
const owner = await this.driversApiClient.getDriver((league as any).ownerId);
|
||||
const owner = await this.driversApiClient.getDriver(league.ownerId);
|
||||
|
||||
// League scoring configuration is not exposed separately yet; use null to indicate "not configured" in the UI
|
||||
const scoringConfig: LeagueScoringConfigDTO | null = null;
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
export type LeagueCapacityAndScoringPrimaryChampionshipType =
|
||||
| 'driver'
|
||||
| 'team'
|
||||
| 'nations'
|
||||
| 'trophy';
|
||||
|
||||
export type LeagueCapacityAndScoringSummaryScoringDTO = {
|
||||
gameId: string;
|
||||
gameName: string;
|
||||
primaryChampionshipType: LeagueCapacityAndScoringPrimaryChampionshipType;
|
||||
scoringPresetId: string;
|
||||
scoringPresetName: string;
|
||||
dropPolicySummary: string;
|
||||
scoringPatternSummary: string;
|
||||
};
|
||||
|
||||
export type LeagueCapacityAndScoringSocialLinksDTO = {
|
||||
discordUrl?: string;
|
||||
youtubeUrl?: string;
|
||||
websiteUrl?: string;
|
||||
};
|
||||
|
||||
export type LeagueCapacityAndScoringSettingsDTO = {
|
||||
maxDrivers: number;
|
||||
sessionDuration?: number;
|
||||
qualifyingFormat?: string;
|
||||
};
|
||||
|
||||
export type LeagueWithCapacityAndScoringDTO = {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
ownerId: string;
|
||||
createdAt: string;
|
||||
settings: LeagueCapacityAndScoringSettingsDTO;
|
||||
usedSlots: number;
|
||||
socialLinks?: LeagueCapacityAndScoringSocialLinksDTO;
|
||||
scoring?: LeagueCapacityAndScoringSummaryScoringDTO;
|
||||
timingSummary?: string;
|
||||
};
|
||||
|
||||
export type AllLeaguesWithCapacityAndScoringDTO = {
|
||||
leagues: LeagueWithCapacityAndScoringDTO[];
|
||||
totalCount: number;
|
||||
};
|
||||
@@ -20,6 +20,24 @@ const nextConfig = {
|
||||
hostname: 'picsum.photos',
|
||||
},
|
||||
],
|
||||
dangerouslyAllowSVG: true,
|
||||
contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;",
|
||||
contentDispositionType: 'inline',
|
||||
},
|
||||
async rewrites() {
|
||||
const rawBaseUrl =
|
||||
process.env.API_BASE_URL ??
|
||||
process.env.NEXT_PUBLIC_API_BASE_URL ??
|
||||
'http://localhost:3001';
|
||||
|
||||
const baseUrl = rawBaseUrl.endsWith('/') ? rawBaseUrl.slice(0, -1) : rawBaseUrl;
|
||||
|
||||
return [
|
||||
{
|
||||
source: '/api/media/:path*',
|
||||
destination: `${baseUrl}/media/:path*`,
|
||||
},
|
||||
];
|
||||
},
|
||||
typescript: {
|
||||
ignoreBuildErrors: false,
|
||||
|
||||
Reference in New Issue
Block a user