more seeds
This commit is contained in:
@@ -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