harden media
This commit is contained in:
@@ -1,16 +0,0 @@
|
||||
export const runtime = 'nodejs';
|
||||
|
||||
const ONE_BY_ONE_PNG_BASE64 =
|
||||
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO0pS0kAAAAASUVORK5CYII=';
|
||||
|
||||
export async function GET(): Promise<Response> {
|
||||
const body = Buffer.from(ONE_BY_ONE_PNG_BASE64, 'base64');
|
||||
|
||||
return new Response(body, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'content-type': 'image/png',
|
||||
'cache-control': 'public, max-age=60',
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -38,6 +38,7 @@ import Card from '@/components/ui/Card';
|
||||
import Breadcrumbs from '@/components/layout/Breadcrumbs';
|
||||
import { useServices } from '@/lib/services/ServiceProvider';
|
||||
import { DriverProfileViewModel } from '@/lib/view-models/DriverProfileViewModel';
|
||||
import { mediaConfig } from '@/lib/config/mediaConfig';
|
||||
|
||||
// ============================================================================
|
||||
// TYPES
|
||||
@@ -462,7 +463,7 @@ export default function DriverDetailPage() {
|
||||
<div className="w-28 h-28 md:w-36 md:h-36 rounded-2xl bg-gradient-to-br from-primary-blue to-purple-600 p-1 shadow-xl shadow-primary-blue/20">
|
||||
<div className="w-full h-full rounded-xl overflow-hidden bg-iron-gray">
|
||||
<Image
|
||||
src={driver.avatarUrl}
|
||||
src={driver.avatarUrl || mediaConfig.avatars.defaultFallback}
|
||||
alt={driver.name}
|
||||
width={144}
|
||||
height={144}
|
||||
@@ -851,7 +852,7 @@ export default function DriverDetailPage() {
|
||||
>
|
||||
<div className="w-8 h-8 rounded-full overflow-hidden bg-gradient-to-br from-primary-blue to-purple-600">
|
||||
<Image
|
||||
src={friend.avatarUrl || '/default-avatar.png'}
|
||||
src={friend.avatarUrl || mediaConfig.avatars.defaultFallback}
|
||||
alt={friend.name}
|
||||
width={32}
|
||||
height={32}
|
||||
|
||||
@@ -22,6 +22,7 @@ import Card from '@/components/ui/Card';
|
||||
import Heading from '@/components/ui/Heading';
|
||||
import { useDriverLeaderboard } from '@/hooks/useDriverService';
|
||||
import Image from 'next/image';
|
||||
import { mediaConfig } from '@/lib/config/mediaConfig';
|
||||
|
||||
import type { DriverLeaderboardItemViewModel } from '@/lib/view-models/DriverLeaderboardItemViewModel';
|
||||
|
||||
@@ -133,7 +134,7 @@ function FeaturedDriverCard({ driver, position, onClick }: FeaturedDriverCardPro
|
||||
{/* Avatar & Name */}
|
||||
<div className="flex items-center gap-4 mb-4">
|
||||
<div className="relative w-16 h-16 rounded-full overflow-hidden border-2 border-charcoal-outline group-hover:border-primary-blue transition-colors">
|
||||
<Image src={driver.avatarUrl || '/avatars/default.png'} alt={driver.name} fill className="object-cover" />
|
||||
<Image src={driver.avatarUrl || mediaConfig.avatars.defaultFallback} alt={driver.name} fill className="object-cover" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-white group-hover:text-primary-blue transition-colors">
|
||||
@@ -362,7 +363,7 @@ function LeaderboardPreview({ drivers, onDriverClick }: LeaderboardPreviewProps)
|
||||
|
||||
{/* Avatar */}
|
||||
<div className="relative w-9 h-9 rounded-full overflow-hidden border-2 border-charcoal-outline">
|
||||
<Image src={driver.avatarUrl || '/avatars/default.png'} alt={driver.name} fill className="object-cover" />
|
||||
<Image src={driver.avatarUrl || mediaConfig.avatars.defaultFallback} alt={driver.name} fill className="object-cover" />
|
||||
</div>
|
||||
|
||||
{/* Info */}
|
||||
@@ -436,7 +437,7 @@ function RecentActivity({ drivers, onDriverClick }: RecentActivityProps) {
|
||||
className="p-3 rounded-xl bg-iron-gray/40 border border-charcoal-outline hover:border-performance-green/40 transition-all group text-center"
|
||||
>
|
||||
<div className="relative w-12 h-12 mx-auto rounded-full overflow-hidden border-2 border-charcoal-outline mb-2">
|
||||
<Image src={driver.avatarUrl || '/avatars/default.png'} alt={driver.name} fill className="object-cover" />
|
||||
<Image src={driver.avatarUrl || mediaConfig.avatars.defaultFallback} alt={driver.name} fill className="object-cover" />
|
||||
<div className="absolute bottom-0 right-0 w-3 h-3 rounded-full bg-performance-green border-2 border-iron-gray" />
|
||||
</div>
|
||||
<p className="text-sm font-medium text-white truncate group-hover:text-performance-green transition-colors">
|
||||
|
||||
@@ -30,7 +30,7 @@ export default function LeagueStandingsPage() {
|
||||
try {
|
||||
const vm = await leagueService.getLeagueStandings(leagueId, currentDriverId);
|
||||
setStandings(vm.standings);
|
||||
setDrivers(vm.drivers.map((d) => new DriverViewModel(d)));
|
||||
setDrivers(vm.drivers.map((d) => new DriverViewModel({ ...d, avatarUrl: (d as any).avatarUrl ?? null })));
|
||||
setMemberships(vm.memberships);
|
||||
|
||||
// Check if current user is admin
|
||||
|
||||
37
apps/website/app/media/avatar/[driverId]/route.ts
Normal file
37
apps/website/app/media/avatar/[driverId]/route.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { driverId: string } }
|
||||
) {
|
||||
const { driverId } = params;
|
||||
|
||||
// In test environment, proxy to the mock API
|
||||
const apiBaseUrl = process.env.API_BASE_URL || 'http://localhost:3000';
|
||||
|
||||
try {
|
||||
const response = await fetch(`${apiBaseUrl}/media/avatar/${driverId}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'image/png',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
// Return a fallback image or 404
|
||||
return new NextResponse(null, { status: 404 });
|
||||
}
|
||||
|
||||
const buffer = await response.arrayBuffer();
|
||||
|
||||
return new NextResponse(buffer, {
|
||||
headers: {
|
||||
'Content-Type': 'image/png',
|
||||
'Cache-Control': 'public, max-age=3600',
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching avatar:', error);
|
||||
return new NextResponse(null, { status: 500 });
|
||||
}
|
||||
}
|
||||
32
apps/website/app/media/categories/[categoryId]/icon/route.ts
Normal file
32
apps/website/app/media/categories/[categoryId]/icon/route.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { categoryId: string } }
|
||||
) {
|
||||
const { categoryId } = params;
|
||||
|
||||
const apiBaseUrl = process.env.API_BASE_URL || 'http://localhost:3000';
|
||||
|
||||
try {
|
||||
const response = await fetch(`${apiBaseUrl}/media/categories/${categoryId}/icon`, {
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return new NextResponse(null, { status: 404 });
|
||||
}
|
||||
|
||||
const buffer = await response.arrayBuffer();
|
||||
|
||||
return new NextResponse(buffer, {
|
||||
headers: {
|
||||
'Content-Type': 'image/png',
|
||||
'Cache-Control': 'public, max-age=3600',
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching category icon:', error);
|
||||
return new NextResponse(null, { status: 500 });
|
||||
}
|
||||
}
|
||||
32
apps/website/app/media/leagues/[leagueId]/cover/route.ts
Normal file
32
apps/website/app/media/leagues/[leagueId]/cover/route.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { leagueId: string } }
|
||||
) {
|
||||
const { leagueId } = params;
|
||||
|
||||
const apiBaseUrl = process.env.API_BASE_URL || 'http://localhost:3000';
|
||||
|
||||
try {
|
||||
const response = await fetch(`${apiBaseUrl}/media/leagues/${leagueId}/cover`, {
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return new NextResponse(null, { status: 404 });
|
||||
}
|
||||
|
||||
const buffer = await response.arrayBuffer();
|
||||
|
||||
return new NextResponse(buffer, {
|
||||
headers: {
|
||||
'Content-Type': 'image/png',
|
||||
'Cache-Control': 'public, max-age=3600',
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching league cover:', error);
|
||||
return new NextResponse(null, { status: 500 });
|
||||
}
|
||||
}
|
||||
32
apps/website/app/media/leagues/[leagueId]/logo/route.ts
Normal file
32
apps/website/app/media/leagues/[leagueId]/logo/route.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { leagueId: string } }
|
||||
) {
|
||||
const { leagueId } = params;
|
||||
|
||||
const apiBaseUrl = process.env.API_BASE_URL || 'http://localhost:3000';
|
||||
|
||||
try {
|
||||
const response = await fetch(`${apiBaseUrl}/media/leagues/${leagueId}/logo`, {
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return new NextResponse(null, { status: 404 });
|
||||
}
|
||||
|
||||
const buffer = await response.arrayBuffer();
|
||||
|
||||
return new NextResponse(buffer, {
|
||||
headers: {
|
||||
'Content-Type': 'image/png',
|
||||
'Cache-Control': 'public, max-age=3600',
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching league logo:', error);
|
||||
return new NextResponse(null, { status: 500 });
|
||||
}
|
||||
}
|
||||
32
apps/website/app/media/sponsors/[sponsorId]/logo/route.ts
Normal file
32
apps/website/app/media/sponsors/[sponsorId]/logo/route.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { sponsorId: string } }
|
||||
) {
|
||||
const { sponsorId } = params;
|
||||
|
||||
const apiBaseUrl = process.env.API_BASE_URL || 'http://localhost:3000';
|
||||
|
||||
try {
|
||||
const response = await fetch(`${apiBaseUrl}/media/sponsors/${sponsorId}/logo`, {
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return new NextResponse(null, { status: 404 });
|
||||
}
|
||||
|
||||
const buffer = await response.arrayBuffer();
|
||||
|
||||
return new NextResponse(buffer, {
|
||||
headers: {
|
||||
'Content-Type': 'image/png',
|
||||
'Cache-Control': 'public, max-age=3600',
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching sponsor logo:', error);
|
||||
return new NextResponse(null, { status: 500 });
|
||||
}
|
||||
}
|
||||
38
apps/website/app/media/teams/[teamId]/logo/route.ts
Normal file
38
apps/website/app/media/teams/[teamId]/logo/route.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { teamId: string } }
|
||||
) {
|
||||
const { teamId } = params;
|
||||
|
||||
// In test environment, proxy to the mock API
|
||||
// In production, this would fetch from the actual API
|
||||
const apiBaseUrl = process.env.API_BASE_URL || 'http://localhost:3000';
|
||||
|
||||
try {
|
||||
const response = await fetch(`${apiBaseUrl}/media/teams/${teamId}/logo`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'image/png',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
// Return a fallback image or 404
|
||||
return new NextResponse(null, { status: 404 });
|
||||
}
|
||||
|
||||
const buffer = await response.arrayBuffer();
|
||||
|
||||
return new NextResponse(buffer, {
|
||||
headers: {
|
||||
'Content-Type': 'image/png',
|
||||
'Cache-Control': 'public, max-age=3600',
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching team logo:', error);
|
||||
return new NextResponse(null, { status: 500 });
|
||||
}
|
||||
}
|
||||
32
apps/website/app/media/tracks/[trackId]/image/route.ts
Normal file
32
apps/website/app/media/tracks/[trackId]/image/route.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { trackId: string } }
|
||||
) {
|
||||
const { trackId } = params;
|
||||
|
||||
const apiBaseUrl = process.env.API_BASE_URL || 'http://localhost:3000';
|
||||
|
||||
try {
|
||||
const response = await fetch(`${apiBaseUrl}/media/tracks/${trackId}/image`, {
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return new NextResponse(null, { status: 404 });
|
||||
}
|
||||
|
||||
const buffer = await response.arrayBuffer();
|
||||
|
||||
return new NextResponse(buffer, {
|
||||
headers: {
|
||||
'Content-Type': 'image/png',
|
||||
'Cache-Control': 'public, max-age=3600',
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching track image:', error);
|
||||
return new NextResponse(null, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { redirect } from 'next/navigation';
|
||||
import Image from 'next/image';
|
||||
|
||||
import { getAppMode } from '@/lib/mode';
|
||||
import Hero from '@/components/landing/Hero';
|
||||
@@ -16,6 +17,7 @@ import Card from '@/components/ui/Card';
|
||||
import Button from '@/components/ui/Button';
|
||||
import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl';
|
||||
import { ServiceFactory } from '@/lib/services/ServiceFactory';
|
||||
import { getMediaUrl } from '@/lib/utilities/media';
|
||||
|
||||
export default async function HomePage() {
|
||||
const baseUrl = getWebsiteApiBaseUrl();
|
||||
@@ -299,8 +301,14 @@ export default async function HomePage() {
|
||||
<ul className="space-y-3 text-sm">
|
||||
{teams.slice(0, 4).map(team => (
|
||||
<li key={team.id} className="flex items-start gap-3">
|
||||
<div className="w-10 h-10 rounded-md bg-charcoal-outline flex items-center justify-center text-xs font-semibold text-white">
|
||||
{team.tag}
|
||||
<div className="w-10 h-10 rounded-md bg-charcoal-outline flex items-center justify-center overflow-hidden border border-charcoal-outline">
|
||||
<Image
|
||||
src={team.logoUrl || getMediaUrl('team-logo', team.id)}
|
||||
alt={team.name}
|
||||
width={40}
|
||||
height={40}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-white truncate">{team.name}</p>
|
||||
|
||||
@@ -13,6 +13,7 @@ import type {
|
||||
DriverProfileSocialHandleViewModel,
|
||||
DriverProfileViewModel
|
||||
} from '@/lib/view-models/DriverProfileViewModel';
|
||||
import { getMediaUrl } from '@/lib/utilities/media';
|
||||
import {
|
||||
Activity,
|
||||
Award,
|
||||
@@ -406,7 +407,7 @@ export default function ProfilePage() {
|
||||
<div className="w-28 h-28 md:w-36 md:h-36 rounded-2xl bg-gradient-to-br from-primary-blue to-purple-600 p-1 shadow-xl shadow-primary-blue/20">
|
||||
<div className="w-full h-full rounded-xl overflow-hidden bg-iron-gray">
|
||||
<Image
|
||||
src={mediaService.getDriverAvatar(currentDriver.id)}
|
||||
src={getMediaUrl('driver-avatar', currentDriver.id)}
|
||||
alt={currentDriver.name}
|
||||
width={144}
|
||||
height={144}
|
||||
@@ -888,7 +889,7 @@ export default function ProfilePage() {
|
||||
>
|
||||
<div className="w-8 h-8 rounded-full overflow-hidden bg-gradient-to-br from-primary-blue to-purple-600">
|
||||
<Image
|
||||
src={mediaService.getDriverAvatar(friend.id)}
|
||||
src={getMediaUrl('driver-avatar', friend.id)}
|
||||
alt={friend.name}
|
||||
width={32}
|
||||
height={32}
|
||||
|
||||
@@ -16,6 +16,8 @@ import StatItem from '@/components/teams/StatItem';
|
||||
import { useServices } from '@/lib/services/ServiceProvider';
|
||||
import { TeamDetailsViewModel } from '@/lib/view-models/TeamDetailsViewModel';
|
||||
import { TeamMemberViewModel } from '@/lib/view-models/TeamMemberViewModel';
|
||||
import { getMediaUrl } from '@/lib/utilities/media';
|
||||
import PlaceholderImage from '@/components/ui/PlaceholderImage';
|
||||
|
||||
import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId';
|
||||
|
||||
@@ -191,7 +193,7 @@ export default function TeamDetailPage() {
|
||||
<div className="flex items-start gap-6">
|
||||
<div className="w-24 h-24 bg-charcoal-outline rounded-lg flex items-center justify-center flex-shrink-0 overflow-hidden">
|
||||
<Image
|
||||
src={mediaService.getTeamLogo(team.id)}
|
||||
src={getMediaUrl('team-logo', team.id)}
|
||||
alt={team.name}
|
||||
width={96}
|
||||
height={96}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import Image from 'next/image';
|
||||
import {
|
||||
Users,
|
||||
Trophy,
|
||||
@@ -25,6 +26,7 @@ import Heading from '@/components/ui/Heading';
|
||||
import TopThreePodium from '@/components/teams/TopThreePodium';
|
||||
import { useAllTeams } from '@/hooks/useTeamService';
|
||||
import type { TeamSummaryViewModel } from '@/lib/view-models/TeamSummaryViewModel';
|
||||
import { getMediaUrl } from '@/lib/utilities/media';
|
||||
|
||||
// ============================================================================
|
||||
// TYPES
|
||||
@@ -407,8 +409,14 @@ export default function TeamLeaderboardPage() {
|
||||
|
||||
{/* Team Info */}
|
||||
<div className="col-span-4 lg:col-span-5 flex items-center gap-3">
|
||||
<div className={`flex h-10 w-10 items-center justify-center rounded-lg ${levelConfig?.bgColor} border ${levelConfig?.borderColor}`}>
|
||||
<LevelIcon className={`w-5 h-5 ${levelConfig?.color}`} />
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-charcoal-outline border border-charcoal-outline overflow-hidden">
|
||||
<Image
|
||||
src={team.logoUrl || getMediaUrl('team-logo', team.id)}
|
||||
alt={team.name}
|
||||
width={40}
|
||||
height={40}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-white font-semibold truncate group-hover:text-purple-400 transition-colors">
|
||||
|
||||
@@ -33,6 +33,7 @@ export default function DriverCard(props: DriverCardProps) {
|
||||
const driverViewModel = new DriverViewModel({
|
||||
id,
|
||||
name,
|
||||
avatarUrl: null,
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import Link from 'next/link';
|
||||
import Image from 'next/image';
|
||||
import PlaceholderImage from '@/components/ui/PlaceholderImage';
|
||||
import type { DriverViewModel } from '@/lib/view-models/DriverViewModel';
|
||||
|
||||
export interface DriverIdentityProps {
|
||||
@@ -21,8 +22,8 @@ export default function DriverIdentity(props: DriverIdentityProps) {
|
||||
|
||||
const metaTextClasses = 'text-xs md:text-sm text-gray-400';
|
||||
|
||||
// Use provided avatar URL or fallback to default avatar path
|
||||
const avatarUrl = driver.avatarUrl || `/api/media/avatar/${driver.id}`;
|
||||
// Use provided avatar URL or show placeholder if null
|
||||
const avatarUrl = driver.avatarUrl;
|
||||
|
||||
const content = (
|
||||
<div className="flex items-center gap-3 md:gap-4 flex-1 min-w-0">
|
||||
@@ -30,13 +31,17 @@ export default function DriverIdentity(props: DriverIdentityProps) {
|
||||
className={`rounded-full bg-primary-blue/20 overflow-hidden flex items-center justify-center shrink-0`}
|
||||
style={{ width: avatarSize, height: avatarSize }}
|
||||
>
|
||||
<Image
|
||||
src={avatarUrl}
|
||||
alt={driver.name}
|
||||
width={avatarSize}
|
||||
height={avatarSize}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
{avatarUrl ? (
|
||||
<Image
|
||||
src={avatarUrl}
|
||||
alt={driver.name}
|
||||
width={avatarSize}
|
||||
height={avatarSize}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<PlaceholderImage size={avatarSize} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import React from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import Image from 'next/image';
|
||||
import { Users, Crown, Shield, ChevronRight } from 'lucide-react';
|
||||
import Button from '@/components/ui/Button';
|
||||
import type { TeamSummaryViewModel } from '@/lib/view-models/TeamSummaryViewModel';
|
||||
import { getMediaUrl } from '@/lib/utilities/media';
|
||||
|
||||
interface TeamLeaderboardPreviewProps {
|
||||
teams: TeamSummaryViewModel[];
|
||||
@@ -82,9 +84,15 @@ export default function TeamLeaderboardPreview({ teams, onTeamClick }: TeamLeade
|
||||
{position <= 3 ? <Crown className="w-3.5 h-3.5" /> : position}
|
||||
</div>
|
||||
|
||||
{/* Team Icon */}
|
||||
<div className={`flex h-9 w-9 items-center justify-center rounded-lg ${levelConfig?.bgColor} border ${levelConfig?.borderColor}`}>
|
||||
<LevelIcon className={`w-4 h-4 ${levelConfig?.color}`} />
|
||||
{/* Team Logo */}
|
||||
<div className="flex h-9 w-9 items-center justify-center rounded-lg bg-charcoal-outline border border-charcoal-outline overflow-hidden">
|
||||
<Image
|
||||
src={team.logoUrl || getMediaUrl('team-logo', team.id)}
|
||||
alt={team.name}
|
||||
width={36}
|
||||
height={36}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Info */}
|
||||
|
||||
@@ -14,7 +14,8 @@ import {
|
||||
} from 'lucide-react';
|
||||
import type { LeagueSummaryViewModel } from '@/lib/view-models/LeagueSummaryViewModel';
|
||||
import { getLeagueCoverClasses } from '@/lib/leagueCovers';
|
||||
import { useServices } from '@/lib/services/ServiceProvider';
|
||||
import PlaceholderImage from '@/components/ui/PlaceholderImage';
|
||||
import { getMediaUrl } from '@/lib/utilities/media';
|
||||
|
||||
interface LeagueCardProps {
|
||||
league: LeagueSummaryViewModel;
|
||||
@@ -114,9 +115,8 @@ function isNewLeague(createdAt: string | Date): boolean {
|
||||
}
|
||||
|
||||
export default function LeagueCard({ league, onClick }: LeagueCardProps) {
|
||||
const { mediaService } = useServices();
|
||||
const coverUrl = mediaService.getLeagueCover(league.id);
|
||||
const logoUrl = mediaService.getLeagueLogo(league.id);
|
||||
const coverUrl = getMediaUrl('league-cover', league.id);
|
||||
const logoUrl = league.logoUrl;
|
||||
|
||||
const ChampionshipIcon = getChampionshipIcon(league.scoring?.primaryChampionshipType);
|
||||
const championshipLabel = getChampionshipLabel(league.scoring?.primaryChampionshipType);
|
||||
@@ -190,15 +190,19 @@ 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">
|
||||
<img
|
||||
src={logoUrl}
|
||||
alt={`${league.name} logo`}
|
||||
width={48}
|
||||
height={48}
|
||||
className="h-full w-full object-cover"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
{logoUrl ? (
|
||||
<img
|
||||
src={logoUrl}
|
||||
alt={`${league.name} logo`}
|
||||
width={48}
|
||||
height={48}
|
||||
className="h-full w-full object-cover"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
) : (
|
||||
<PlaceholderImage size={48} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import MembershipStatus from '@/components/leagues/MembershipStatus';
|
||||
import { useServices } from '@/lib/services/ServiceProvider';
|
||||
import { getMediaUrl } from '@/lib/utilities/media';
|
||||
import Image from 'next/image';
|
||||
|
||||
|
||||
@@ -28,8 +28,7 @@ export default function LeagueHeader({
|
||||
ownerId,
|
||||
mainSponsor,
|
||||
}: LeagueHeaderProps) {
|
||||
const { mediaService } = useServices();
|
||||
const logoUrl = mediaService.getLeagueLogo(leagueId);
|
||||
const logoUrl = getMediaUrl('league-logo', leagueId);
|
||||
|
||||
return (
|
||||
<div className="mb-8">
|
||||
|
||||
@@ -43,7 +43,7 @@ export default function LeagueMembers({
|
||||
|
||||
const byId: Record<string, DriverViewModel> = {};
|
||||
for (const dto of driverDtos) {
|
||||
byId[dto.id] = new DriverViewModel(dto);
|
||||
byId[dto.id] = new DriverViewModel({ ...dto, avatarUrl: (dto as any).avatarUrl ?? null });
|
||||
}
|
||||
setDriversById(byId);
|
||||
} else {
|
||||
|
||||
@@ -48,6 +48,7 @@ export default function LeagueOwnershipTransfer({
|
||||
driver={new DriverViewModel({
|
||||
id: ownerSummary.driver.id,
|
||||
name: ownerSummary.driver.name,
|
||||
avatarUrl: (ownerSummary.driver as any).avatarUrl ?? null,
|
||||
iracingId: ownerSummary.driver.iracingId,
|
||||
country: ownerSummary.driver.country,
|
||||
bio: ownerSummary.driver.bio,
|
||||
|
||||
@@ -8,7 +8,8 @@ import type { DriverViewModel } from '@/lib/view-models/DriverViewModel';
|
||||
import type { LeagueMembership } from '@/lib/types/LeagueMembership';
|
||||
import { LeagueRoleDisplay } from '@/lib/display-objects/LeagueRoleDisplay';
|
||||
import CountryFlag from '@/components/ui/CountryFlag';
|
||||
import { useServices } from '@/lib/services/ServiceProvider';
|
||||
import { getMediaUrl } from '@/lib/utilities/media';
|
||||
import PlaceholderImage from '@/components/ui/PlaceholderImage';
|
||||
|
||||
// Position background colors
|
||||
const getPositionBgColor = (position: number): string => {
|
||||
@@ -52,7 +53,6 @@ export default function StandingsTable({
|
||||
onRemoveMember,
|
||||
onUpdateRole
|
||||
}: StandingsTableProps) {
|
||||
const { mediaService } = useServices();
|
||||
const [hoveredRow, setHoveredRow] = useState<string | null>(null);
|
||||
const [activeMenu, setActiveMenu] = useState<{ driverId: string; type: 'member' | 'points' } | null>(null);
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
@@ -320,16 +320,20 @@ export default function StandingsTable({
|
||||
{/* Avatar */}
|
||||
<div className="relative">
|
||||
<div className="w-10 h-10 rounded-full bg-primary-blue/20 overflow-hidden flex items-center justify-center shrink-0">
|
||||
{driver && (
|
||||
<Image
|
||||
src={mediaService.getDriverAvatar(driver.id)}
|
||||
alt={driver.name}
|
||||
width={40}
|
||||
height={40}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{driver && (
|
||||
driver.avatarUrl ? (
|
||||
<Image
|
||||
src={driver.avatarUrl}
|
||||
alt={driver.name}
|
||||
width={40}
|
||||
height={40}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<PlaceholderImage size={40} />
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
{/* Nationality flag */}
|
||||
{driver && driver.country && (
|
||||
<div className="absolute -bottom-1 -right-1">
|
||||
|
||||
@@ -5,13 +5,13 @@ import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import type { DriverViewModel } from '@/lib/view-models/DriverViewModel';
|
||||
import DriverRating from '@/components/profile/DriverRatingPill';
|
||||
import { useServices } from '@/lib/services/ServiceProvider';
|
||||
import PlaceholderImage from '@/components/ui/PlaceholderImage';
|
||||
|
||||
export interface DriverSummaryPillProps {
|
||||
driver: DriverViewModel;
|
||||
rating: number | null;
|
||||
rank: number | null;
|
||||
avatarSrc?: string;
|
||||
avatarSrc?: string | null;
|
||||
onClick?: () => void;
|
||||
href?: string;
|
||||
}
|
||||
@@ -19,21 +19,22 @@ export interface DriverSummaryPillProps {
|
||||
export default function DriverSummaryPill(props: DriverSummaryPillProps) {
|
||||
const { driver, rating, rank, avatarSrc, onClick, href } = props;
|
||||
|
||||
const { mediaService } = useServices();
|
||||
|
||||
const resolvedAvatar =
|
||||
avatarSrc ?? mediaService.getDriverAvatar(driver.id);
|
||||
const resolvedAvatar = avatarSrc;
|
||||
|
||||
const content = (
|
||||
<>
|
||||
<div className="w-8 h-8 rounded-full overflow-hidden bg-charcoal-outline flex items-center justify-center border border-charcoal-outline/80">
|
||||
<Image
|
||||
src={resolvedAvatar}
|
||||
alt={driver.name}
|
||||
width={32}
|
||||
height={32}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
{resolvedAvatar ? (
|
||||
<Image
|
||||
src={resolvedAvatar}
|
||||
alt={driver.name}
|
||||
width={32}
|
||||
height={32}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<PlaceholderImage size={32} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col leading-tight text-left">
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { DriverViewModel } from '@/lib/view-models/DriverViewModel';
|
||||
import Button from '../ui/Button';
|
||||
import DriverRatingPill from '@/components/profile/DriverRatingPill';
|
||||
import CountryFlag from '@/components/ui/CountryFlag';
|
||||
import { useServices } from '@/lib/services/ServiceProvider';
|
||||
import PlaceholderImage from '@/components/ui/PlaceholderImage';
|
||||
|
||||
interface ProfileHeaderProps {
|
||||
driver: DriverViewModel;
|
||||
@@ -26,19 +26,21 @@ export default function ProfileHeader({
|
||||
teamName,
|
||||
teamTag,
|
||||
}: ProfileHeaderProps) {
|
||||
const { mediaService } = useServices();
|
||||
|
||||
return (
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="w-20 h-20 rounded-full bg-gradient-to-br from-primary-blue to-purple-600 overflow-hidden flex items-center justify-center">
|
||||
<Image
|
||||
src={mediaService.getDriverAvatar(driver.id)}
|
||||
alt={driver.name}
|
||||
width={80}
|
||||
height={80}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
{driver.avatarUrl ? (
|
||||
<Image
|
||||
src={driver.avatarUrl}
|
||||
alt={driver.name}
|
||||
width={80}
|
||||
height={80}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<PlaceholderImage size={80} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
||||
@@ -19,9 +19,8 @@ vi.mock('@/hooks/useEffectiveDriverId', () => {
|
||||
};
|
||||
});
|
||||
|
||||
// Mock services hook to inject stub driverService/mediaService
|
||||
const mockFindById = vi.fn<[], Promise<DriverDTO | null>>();
|
||||
const mockGetDriverAvatar = vi.fn<(driverId: string) => string>();
|
||||
// Mock services hook to inject stub driverService
|
||||
const mockFindById = vi.fn();
|
||||
|
||||
vi.mock('@/lib/services/ServiceProvider', () => {
|
||||
return {
|
||||
@@ -30,7 +29,7 @@ vi.mock('@/lib/services/ServiceProvider', () => {
|
||||
findById: mockFindById,
|
||||
},
|
||||
mediaService: {
|
||||
getDriverAvatar: mockGetDriverAvatar,
|
||||
getDriverAvatar: vi.fn(),
|
||||
},
|
||||
}),
|
||||
};
|
||||
@@ -66,7 +65,6 @@ describe('UserPill', () => {
|
||||
mockedAuthValue = { session: null };
|
||||
mockedDriverId = null;
|
||||
mockFindById.mockReset();
|
||||
mockGetDriverAvatar.mockReset();
|
||||
});
|
||||
|
||||
it('renders auth links when there is no session', () => {
|
||||
@@ -94,19 +92,19 @@ describe('UserPill', () => {
|
||||
expect(mockFindById).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('loads driver via driverService and uses mediaService avatar', async () => {
|
||||
it('loads driver via driverService and uses driver avatarUrl', async () => {
|
||||
const driver: DriverDTO = {
|
||||
id: 'driver-1',
|
||||
iracingId: 'ir-123',
|
||||
name: 'Test Driver',
|
||||
country: 'DE',
|
||||
avatarUrl: '/api/media/avatar/driver-1',
|
||||
};
|
||||
|
||||
mockedAuthValue = { session: { user: { id: 'user-1' } } };
|
||||
mockedDriverId = driver.id;
|
||||
|
||||
mockFindById.mockResolvedValue(driver);
|
||||
mockGetDriverAvatar.mockImplementation((driverId: string) => `/api/media/avatar/${driverId}`);
|
||||
|
||||
render(<UserPill />);
|
||||
|
||||
@@ -115,6 +113,5 @@ describe('UserPill', () => {
|
||||
});
|
||||
|
||||
expect(mockFindById).toHaveBeenCalledWith('driver-1');
|
||||
expect(mockGetDriverAvatar).toHaveBeenCalledWith('driver-1');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -106,7 +106,7 @@ export default function UserPill() {
|
||||
|
||||
const dto = await driverService.findById(primaryDriverId);
|
||||
if (!cancelled) {
|
||||
setDriver(dto ? new DriverViewModelClass(dto) : null);
|
||||
setDriver(dto ? new DriverViewModelClass({ ...dto, avatarUrl: (dto as any).avatarUrl ?? null }) : null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,7 +127,7 @@ export default function UserPill() {
|
||||
const rating: number | null = null;
|
||||
const rank: number | null = null;
|
||||
|
||||
const avatarSrc = mediaService.getDriverAvatar(primaryDriverId);
|
||||
const avatarSrc = driver.avatarUrl;
|
||||
|
||||
return {
|
||||
driver,
|
||||
@@ -135,7 +135,7 @@ export default function UserPill() {
|
||||
rating,
|
||||
rank,
|
||||
};
|
||||
}, [session, driver, primaryDriverId, mediaService]);
|
||||
}, [session, driver, primaryDriverId]);
|
||||
|
||||
// Close menu when clicking outside
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { useServices } from '@/lib/services/ServiceProvider';
|
||||
import { getMediaUrl } from '@/lib/utilities/media';
|
||||
|
||||
interface Friend {
|
||||
id: string;
|
||||
@@ -23,8 +23,6 @@ function getCountryFlag(countryCode: string): string {
|
||||
}
|
||||
|
||||
export default function FriendPill({ friend }: FriendPillProps) {
|
||||
const { mediaService } = useServices();
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={`/drivers/${friend.id}`}
|
||||
@@ -32,7 +30,7 @@ export default function FriendPill({ friend }: FriendPillProps) {
|
||||
>
|
||||
<div className="w-8 h-8 rounded-full overflow-hidden bg-gradient-to-br from-primary-blue to-purple-600">
|
||||
<Image
|
||||
src={mediaService.getDriverAvatar(friend.id)}
|
||||
src={getMediaUrl('driver-avatar', friend.id)}
|
||||
alt={friend.name}
|
||||
width={32}
|
||||
height={32}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import Image from 'next/image';
|
||||
import { UserPlus, Users, Trophy } from 'lucide-react';
|
||||
import type { TeamSummaryViewModel } from '@/lib/view-models/TeamSummaryViewModel';
|
||||
import { getMediaUrl } from '@/lib/utilities/media';
|
||||
|
||||
const SKILL_LEVELS: {
|
||||
id: string;
|
||||
@@ -77,8 +79,14 @@ export default function FeaturedRecruiting({ teams, onTeamClick }: FeaturedRecru
|
||||
className="p-4 rounded-xl bg-iron-gray/60 border border-charcoal-outline hover:border-performance-green/40 transition-all duration-200 text-left group"
|
||||
>
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className={`flex h-8 w-8 items-center justify-center rounded-lg ${levelConfig?.bgColor} border ${levelConfig?.borderColor}`}>
|
||||
{/* LevelIcon would be here */}
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-charcoal-outline border border-charcoal-outline overflow-hidden">
|
||||
<Image
|
||||
src={team.logoUrl || getMediaUrl('team-logo', team.id)}
|
||||
alt={team.name}
|
||||
width={32}
|
||||
height={32}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
<span className="flex items-center gap-1 px-2 py-0.5 rounded-full text-[10px] bg-performance-green/10 text-performance-green border border-performance-green/20">
|
||||
<div className="w-1.5 h-1.5 rounded-full bg-performance-green animate-pulse" />
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
Languages,
|
||||
} from 'lucide-react';
|
||||
|
||||
import { useServices } from '@/lib/services/ServiceProvider';
|
||||
import PlaceholderImage from '@/components/ui/PlaceholderImage';
|
||||
|
||||
interface TeamCardProps {
|
||||
id: string;
|
||||
@@ -81,8 +81,7 @@ export default function TeamCard({
|
||||
category,
|
||||
onClick,
|
||||
}: TeamCardProps) {
|
||||
const { mediaService } = useServices();
|
||||
const logoUrl = logo || mediaService.getTeamLogo(id);
|
||||
const logoUrl = logo;
|
||||
const performanceBadge = getPerformanceBadge(performanceLevel);
|
||||
const specializationBadge = getSpecializationBadge(specialization);
|
||||
|
||||
@@ -98,13 +97,17 @@ export default function TeamCard({
|
||||
<div className="flex items-start gap-4">
|
||||
{/* Logo */}
|
||||
<div className="w-14 h-14 rounded-xl bg-charcoal-outline flex items-center justify-center flex-shrink-0 overflow-hidden border border-charcoal-outline">
|
||||
<Image
|
||||
src={logoUrl}
|
||||
alt={name}
|
||||
width={56}
|
||||
height={56}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
{logoUrl ? (
|
||||
<Image
|
||||
src={logoUrl}
|
||||
alt={name}
|
||||
width={56}
|
||||
height={56}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<PlaceholderImage size={56} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Title & Badges */}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { useServices } from '@/lib/services/ServiceProvider';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import Image from 'next/image';
|
||||
import { getMediaUrl } from '@/lib/utilities/media';
|
||||
|
||||
export interface TeamLadderRowProps {
|
||||
rank: number;
|
||||
@@ -26,8 +26,7 @@ export default function TeamLadderRow({
|
||||
totalRaces,
|
||||
}: TeamLadderRowProps) {
|
||||
const router = useRouter();
|
||||
const { mediaService } = useServices();
|
||||
const logo = teamLogoUrl ?? mediaService.getTeamLogo(teamId);
|
||||
const logo = teamLogoUrl ?? getMediaUrl('team-logo', teamId);
|
||||
|
||||
const handleClick = () => {
|
||||
router.push(`/teams/${teamId}`);
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { useRouter } from 'next/navigation';
|
||||
import Image from 'next/image';
|
||||
import { Award, ChevronRight, Crown, Trophy, Users } from 'lucide-react';
|
||||
import Button from '@/components/ui/Button';
|
||||
import type { TeamSummaryViewModel } from '@/lib/view-models/TeamSummaryViewModel';
|
||||
import { getMediaUrl } from '@/lib/utilities/media';
|
||||
|
||||
const SKILL_LEVELS: {
|
||||
id: string;
|
||||
@@ -133,8 +135,14 @@ export default function TeamLeaderboardPreview({
|
||||
</div>
|
||||
|
||||
{/* Team Info */}
|
||||
<div className={`flex h-9 w-9 items-center justify-center rounded-lg ${levelConfig?.bgColor} border ${levelConfig?.borderColor}`}>
|
||||
{/* LevelIcon */}
|
||||
<div className="flex h-9 w-9 items-center justify-center rounded-lg bg-charcoal-outline border border-charcoal-outline overflow-hidden">
|
||||
<Image
|
||||
src={team.logoUrl || getMediaUrl('team-logo', team.id)}
|
||||
alt={team.name}
|
||||
width={36}
|
||||
height={36}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-white font-medium truncate group-hover:text-purple-400 transition-colors">
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import Image from 'next/image';
|
||||
import { Trophy, Crown, Users } from 'lucide-react';
|
||||
import type { TeamSummaryViewModel } from '@/lib/view-models/TeamSummaryViewModel';
|
||||
import { getMediaUrl } from '@/lib/utilities/media';
|
||||
|
||||
const SKILL_LEVELS: {
|
||||
id: string;
|
||||
@@ -128,11 +130,15 @@ export default function TopThreePodium({ teams, onClick }: TopThreePodiumProps)
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Team icon */}
|
||||
<div
|
||||
className={`flex h-16 w-16 md:h-20 md:w-20 items-center justify-center rounded-xl ${levelConfig?.bgColor} border ${levelConfig?.borderColor} mb-3`}
|
||||
>
|
||||
{/* LevelIcon */}
|
||||
{/* Team logo */}
|
||||
<div className="flex h-16 w-16 md:h-20 md:w-20 items-center justify-center rounded-xl bg-charcoal-outline border border-charcoal-outline overflow-hidden mb-3">
|
||||
<Image
|
||||
src={team.logoUrl || getMediaUrl('team-logo', team.id)}
|
||||
alt={team.name}
|
||||
width={80}
|
||||
height={80}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Team name */}
|
||||
|
||||
20
apps/website/components/ui/PlaceholderImage.tsx
Normal file
20
apps/website/components/ui/PlaceholderImage.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { User } from 'lucide-react';
|
||||
|
||||
export interface PlaceholderImageProps {
|
||||
size?: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared placeholder image component for when no avatar/logo URL is available
|
||||
*/
|
||||
export default function PlaceholderImage({ size = 48, className = '' }: PlaceholderImageProps) {
|
||||
return (
|
||||
<div
|
||||
className={`rounded-full bg-charcoal-outline flex items-center justify-center ${className}`}
|
||||
style={{ width: size, height: size }}
|
||||
>
|
||||
<User className="w-6 h-6 text-gray-400" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
50
apps/website/lib/config/mediaConfig.ts
Normal file
50
apps/website/lib/config/mediaConfig.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Media configuration for GridPilot website.
|
||||
* Single source of truth for all media asset paths and URLs.
|
||||
*
|
||||
* Note: This config should be kept in sync with the shared MediaAssetConfig
|
||||
* in adapters/bootstrap/MediaAssetConfig.ts
|
||||
*/
|
||||
|
||||
export interface MediaConfig {
|
||||
avatars: {
|
||||
defaultFallback: string;
|
||||
paths: {
|
||||
male: string;
|
||||
female: string;
|
||||
neutral: string;
|
||||
};
|
||||
};
|
||||
api: {
|
||||
avatar: (driverId: string) => string;
|
||||
teamLogo: (teamId: string) => string;
|
||||
trackImage: (trackId: string) => string;
|
||||
sponsorLogo: (sponsorId: string) => string;
|
||||
categoryIcon: (categoryId: string) => string;
|
||||
};
|
||||
}
|
||||
|
||||
export const mediaConfig: MediaConfig = {
|
||||
avatars: {
|
||||
// Default fallback used when no avatar URL is available
|
||||
defaultFallback: '/images/avatars/neutral-default-avatar.jpeg',
|
||||
|
||||
// Individual avatar type paths
|
||||
paths: {
|
||||
male: '/images/avatars/male-default-avatar.jpg',
|
||||
female: '/images/avatars/female-default-avatar.jpeg',
|
||||
neutral: '/images/avatars/neutral-default-avatar.jpeg',
|
||||
},
|
||||
},
|
||||
|
||||
api: {
|
||||
// Direct media paths (no /api/ prefix) - served by website or API
|
||||
avatar: (driverId: string) => `/media/avatar/${driverId}`,
|
||||
teamLogo: (teamId: string) => `/media/teams/${teamId}/logo`,
|
||||
trackImage: (trackId: string) => `/media/tracks/${trackId}/image`,
|
||||
sponsorLogo: (sponsorId: string) => `/media/sponsors/${sponsorId}/logo`,
|
||||
categoryIcon: (categoryId: string) => `/media/categories/${categoryId}/icon`,
|
||||
},
|
||||
} as const;
|
||||
|
||||
export type MediaConfigType = typeof mediaConfig;
|
||||
@@ -41,7 +41,7 @@ export class DriverService {
|
||||
if (!dto) {
|
||||
return null;
|
||||
}
|
||||
return new DriverViewModel(dto);
|
||||
return new DriverViewModel({ ...dto, avatarUrl: (dto as any).avatarUrl ?? null });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -55,7 +55,7 @@ export class DriverService {
|
||||
id: dto.currentDriver.id,
|
||||
name: dto.currentDriver.name,
|
||||
country: dto.currentDriver.country,
|
||||
avatarUrl: dto.currentDriver.avatarUrl,
|
||||
avatarUrl: dto.currentDriver.avatarUrl || '',
|
||||
iracingId: dto.currentDriver.iracingId ?? null,
|
||||
joinedAt: dto.currentDriver.joinedAt,
|
||||
rating: dto.currentDriver.rating ?? null,
|
||||
@@ -107,7 +107,7 @@ export class DriverService {
|
||||
id: f.id,
|
||||
name: f.name,
|
||||
country: f.country,
|
||||
avatarUrl: f.avatarUrl,
|
||||
avatarUrl: f.avatarUrl || '',
|
||||
})),
|
||||
},
|
||||
extendedProfile: dto.extendedProfile
|
||||
|
||||
@@ -33,7 +33,7 @@ export class LandingService {
|
||||
|
||||
const racesVm = new RacesPageViewModel(racesDto);
|
||||
|
||||
const topLeagues = leaguesDto.leagues.slice(0, 4).map(
|
||||
const topLeagues = (leaguesDto?.leagues || []).slice(0, 4).map(
|
||||
(league: LeagueWithCapacityDTO) => new LeagueCardViewModel({
|
||||
id: league.id,
|
||||
name: league.name,
|
||||
@@ -41,13 +41,14 @@ export class LandingService {
|
||||
}),
|
||||
);
|
||||
|
||||
const teams = teamsDto.teams.slice(0, 4).map(
|
||||
const teams = (teamsDto?.teams || []).slice(0, 4).map(
|
||||
(team: TeamListItemDTO) =>
|
||||
new TeamCardViewModel({
|
||||
id: team.id,
|
||||
name: team.name,
|
||||
tag: team.tag,
|
||||
description: team.description,
|
||||
logoUrl: team.logoUrl,
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -114,6 +114,7 @@ export class LeagueService {
|
||||
id: league.id,
|
||||
name: league.name,
|
||||
description: league.description,
|
||||
logoUrl: league.logoUrl ?? null, // Use API-provided logo URL
|
||||
ownerId: league.ownerId,
|
||||
createdAt: league.createdAt,
|
||||
maxDrivers: league.settings?.maxDrivers ?? 0,
|
||||
@@ -611,4 +612,4 @@ export class LeagueService {
|
||||
const result = await this.apiClient.getScoringPresets();
|
||||
return result.presets;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@ import { DeleteMediaViewModel } from '@/lib/view-models/DeleteMediaViewModel';
|
||||
import { MediaViewModel } from '@/lib/view-models/MediaViewModel';
|
||||
import { UploadMediaViewModel } from '@/lib/view-models/UploadMediaViewModel';
|
||||
import type { MediaApiClient } from '../../api/media/MediaApiClient';
|
||||
import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl';
|
||||
|
||||
// Local request shape mirroring the media upload API contract until a generated type is available
|
||||
type UploadMediaRequest = { file: File; type: string; category?: string };
|
||||
@@ -42,35 +41,4 @@ export class MediaService {
|
||||
return new DeleteMediaViewModel(dto);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get team logo URL
|
||||
* Returns relative URL for proxying through Next.js rewrites
|
||||
*/
|
||||
getTeamLogo(teamId: string): string {
|
||||
return `/api/media/teams/${teamId}/logo`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get driver avatar URL
|
||||
* Returns relative URL for proxying through Next.js rewrites
|
||||
*/
|
||||
getDriverAvatar(driverId: string): string {
|
||||
return `/api/media/avatar/${driverId}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get league cover URL
|
||||
* Returns relative URL for proxying through Next.js rewrites
|
||||
*/
|
||||
getLeagueCover(leagueId: string): string {
|
||||
return `/api/media/leagues/${leagueId}/cover`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get league logo URL
|
||||
* Returns relative URL for proxying through Next.js rewrites
|
||||
*/
|
||||
getLeagueLogo(leagueId: string): string {
|
||||
return `/api/media/leagues/${leagueId}/logo`;
|
||||
}
|
||||
}
|
||||
@@ -34,7 +34,7 @@ export class PaymentService {
|
||||
async getPayments(leagueId?: string, payerId?: string): Promise<PaymentViewModel[]> {
|
||||
const query = (leagueId || payerId) ? { ...(leagueId && { leagueId }), ...(payerId && { payerId }) } : undefined;
|
||||
const dto = await this.apiClient.getPayments(query);
|
||||
return dto.payments.map((payment: PaymentDTO) => new PaymentViewModel(payment));
|
||||
return (dto?.payments || []).map((payment: PaymentDTO) => new PaymentViewModel(payment));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -43,7 +43,7 @@ export class PaymentService {
|
||||
async getPayment(paymentId: string): Promise<PaymentViewModel> {
|
||||
// Note: Assuming the API returns a single payment from the list
|
||||
const dto = await this.apiClient.getPayments();
|
||||
const payment = dto.payments.find((p: PaymentDTO) => p.id === paymentId);
|
||||
const payment = (dto?.payments || []).find((p: PaymentDTO) => p.id === paymentId);
|
||||
if (!payment) {
|
||||
throw new Error(`Payment with ID ${paymentId} not found`);
|
||||
}
|
||||
@@ -72,7 +72,7 @@ export class PaymentService {
|
||||
async getPrizes(leagueId?: string, seasonId?: string): Promise<PrizeViewModel[]> {
|
||||
const query = (leagueId || seasonId) ? { ...(leagueId && { leagueId }), ...(seasonId && { seasonId }) } : undefined;
|
||||
const dto = await this.apiClient.getPrizes(query);
|
||||
return dto.prizes.map((prize: PrizeDTO) => new PrizeViewModel(prize));
|
||||
return (dto?.prizes || []).map((prize: PrizeDTO) => new PrizeViewModel(prize));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -21,7 +21,7 @@ export class SponsorService {
|
||||
*/
|
||||
async getAllSponsors(): Promise<SponsorViewModel[]> {
|
||||
const dto = await this.apiClient.getAll();
|
||||
return dto.sponsors.map((sponsor: SponsorDTO) => new SponsorViewModel(sponsor));
|
||||
return (dto?.sponsors || []).map((sponsor: SponsorDTO) => new SponsorViewModel(sponsor));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -23,8 +23,8 @@ export class TeamJoinService {
|
||||
* Get team join requests with view model transformation
|
||||
*/
|
||||
async getJoinRequests(teamId: string, currentUserId: string, isOwner: boolean): Promise<TeamJoinRequestViewModel[]> {
|
||||
const dto = await this.apiClient.getJoinRequests(teamId) as TeamJoinRequestsDto;
|
||||
return dto.requests.map((r: TeamJoinRequestDTO) => new TeamJoinRequestViewModel(r, currentUserId, isOwner));
|
||||
const dto = await this.apiClient.getJoinRequests(teamId) as TeamJoinRequestsDto | null;
|
||||
return (dto?.requests || []).map((r: TeamJoinRequestDTO) => new TeamJoinRequestViewModel(r, currentUserId, isOwner));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -32,8 +32,8 @@ export class TeamService {
|
||||
* Get all teams with view model transformation
|
||||
*/
|
||||
async getAllTeams(): Promise<TeamSummaryViewModel[]> {
|
||||
const dto: GetAllTeamsOutputDTO = await this.apiClient.getAll();
|
||||
return dto.teams.map((team: TeamListItemDTO) => new TeamSummaryViewModel(team));
|
||||
const dto: GetAllTeamsOutputDTO | null = await this.apiClient.getAll();
|
||||
return (dto?.teams || []).map((team: TeamListItemDTO) => new TeamSummaryViewModel(team));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -34,9 +34,11 @@ export type LeagueWithCapacityAndScoringDTO = {
|
||||
createdAt: string;
|
||||
settings: LeagueCapacityAndScoringSettingsDTO;
|
||||
usedSlots: number;
|
||||
logoUrl?: string | null;
|
||||
socialLinks?: LeagueCapacityAndScoringSocialLinksDTO;
|
||||
scoring?: LeagueCapacityAndScoringSummaryScoringDTO;
|
||||
timingSummary?: string;
|
||||
category?: string;
|
||||
};
|
||||
|
||||
export type AllLeaguesWithCapacityAndScoringDTO = {
|
||||
|
||||
@@ -230,6 +230,30 @@ describe('Website Contract Consumption', () => {
|
||||
// avatarUrls and requestId are optional in failure case
|
||||
});
|
||||
|
||||
it('should handle URL|null pattern for media fields', () => {
|
||||
// Test that media fields use URL|null pattern, not empty strings
|
||||
const driverWithAvatar: DriverDTO = {
|
||||
id: 'driver-123',
|
||||
name: 'Test Driver',
|
||||
avatarUrl: 'https://example.com/avatar.png',
|
||||
country: 'US',
|
||||
};
|
||||
|
||||
const driverWithoutAvatar: DriverDTO = {
|
||||
id: 'driver-456',
|
||||
name: 'No Avatar Driver',
|
||||
avatarUrl: null,
|
||||
country: 'UK',
|
||||
};
|
||||
|
||||
expect(driverWithAvatar.avatarUrl).toBe('https://example.com/avatar.png');
|
||||
expect(driverWithoutAvatar.avatarUrl).toBeNull();
|
||||
|
||||
// Should not use empty strings
|
||||
expect(driverWithAvatar.avatarUrl).not.toBe('');
|
||||
expect(driverWithoutAvatar.avatarUrl).not.toBeUndefined();
|
||||
});
|
||||
|
||||
it('should allow type narrowing based on success flag', () => {
|
||||
function handleAvatarResponse(response: RequestAvatarGenerationOutputDTO) {
|
||||
if (response.success) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||
* Spec SHA256: a5e14ed849c9c55f18facf00106e60c0557da2f69fa246fd717783f0cb0d80ab
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||
* Spec SHA256: a5e14ed849c9c55f18facf00106e60c0557da2f69fa246fd717783f0cb0d80ab
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||
* Spec SHA256: a5e14ed849c9c55f18facf00106e60c0557da2f69fa246fd717783f0cb0d80ab
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||
* Spec SHA256: a5e14ed849c9c55f18facf00106e60c0557da2f69fa246fd717783f0cb0d80ab
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||
* Spec SHA256: a5e14ed849c9c55f18facf00106e60c0557da2f69fa246fd717783f0cb0d80ab
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||
* Spec SHA256: a5e14ed849c9c55f18facf00106e60c0557da2f69fa246fd717783f0cb0d80ab
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||
* Spec SHA256: a5e14ed849c9c55f18facf00106e60c0557da2f69fa246fd717783f0cb0d80ab
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||
* Spec SHA256: a5e14ed849c9c55f18facf00106e60c0557da2f69fa246fd717783f0cb0d80ab
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||
* Spec SHA256: a5e14ed849c9c55f18facf00106e60c0557da2f69fa246fd717783f0cb0d80ab
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||
* Spec SHA256: a5e14ed849c9c55f18facf00106e60c0557da2f69fa246fd717783f0cb0d80ab
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||
* Spec SHA256: a5e14ed849c9c55f18facf00106e60c0557da2f69fa246fd717783f0cb0d80ab
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||
* Spec SHA256: a5e14ed849c9c55f18facf00106e60c0557da2f69fa246fd717783f0cb0d80ab
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||
* Spec SHA256: a5e14ed849c9c55f18facf00106e60c0557da2f69fa246fd717783f0cb0d80ab
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||
* Spec SHA256: a5e14ed849c9c55f18facf00106e60c0557da2f69fa246fd717783f0cb0d80ab
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||
* Spec SHA256: a5e14ed849c9c55f18facf00106e60c0557da2f69fa246fd717783f0cb0d80ab
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||
* Spec SHA256: a5e14ed849c9c55f18facf00106e60c0557da2f69fa246fd717783f0cb0d80ab
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||
* Spec SHA256: a5e14ed849c9c55f18facf00106e60c0557da2f69fa246fd717783f0cb0d80ab
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||
* Spec SHA256: a5e14ed849c9c55f18facf00106e60c0557da2f69fa246fd717783f0cb0d80ab
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||
* Spec SHA256: a5e14ed849c9c55f18facf00106e60c0557da2f69fa246fd717783f0cb0d80ab
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||
* Spec SHA256: a5e14ed849c9c55f18facf00106e60c0557da2f69fa246fd717783f0cb0d80ab
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||
* Spec SHA256: a5e14ed849c9c55f18facf00106e60c0557da2f69fa246fd717783f0cb0d80ab
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||
* Spec SHA256: a5e14ed849c9c55f18facf00106e60c0557da2f69fa246fd717783f0cb0d80ab
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||
* Spec SHA256: a5e14ed849c9c55f18facf00106e60c0557da2f69fa246fd717783f0cb0d80ab
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||
* Spec SHA256: a5e14ed849c9c55f18facf00106e60c0557da2f69fa246fd717783f0cb0d80ab
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||
* Spec SHA256: a5e14ed849c9c55f18facf00106e60c0557da2f69fa246fd717783f0cb0d80ab
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||
* Spec SHA256: a5e14ed849c9c55f18facf00106e60c0557da2f69fa246fd717783f0cb0d80ab
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||
* Spec SHA256: a5e14ed849c9c55f18facf00106e60c0557da2f69fa246fd717783f0cb0d80ab
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||
* Spec SHA256: a5e14ed849c9c55f18facf00106e60c0557da2f69fa246fd717783f0cb0d80ab
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||
* Spec SHA256: a5e14ed849c9c55f18facf00106e60c0557da2f69fa246fd717783f0cb0d80ab
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||
* Spec SHA256: a5e14ed849c9c55f18facf00106e60c0557da2f69fa246fd717783f0cb0d80ab
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||
* Spec SHA256: a5e14ed849c9c55f18facf00106e60c0557da2f69fa246fd717783f0cb0d80ab
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||
* Spec SHA256: a5e14ed849c9c55f18facf00106e60c0557da2f69fa246fd717783f0cb0d80ab
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
@@ -9,7 +9,7 @@ export interface DashboardDriverSummaryDTO {
|
||||
id: string;
|
||||
name: string;
|
||||
country: string;
|
||||
avatarUrl: string;
|
||||
avatarUrl?: string;
|
||||
category?: string;
|
||||
rating?: number;
|
||||
globalRank?: number;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||
* Spec SHA256: a5e14ed849c9c55f18facf00106e60c0557da2f69fa246fd717783f0cb0d80ab
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||
* Spec SHA256: a5e14ed849c9c55f18facf00106e60c0557da2f69fa246fd717783f0cb0d80ab
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||
* Spec SHA256: a5e14ed849c9c55f18facf00106e60c0557da2f69fa246fd717783f0cb0d80ab
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
@@ -9,5 +9,5 @@ export interface DashboardFriendSummaryDTO {
|
||||
id: string;
|
||||
name: string;
|
||||
country: string;
|
||||
avatarUrl: string;
|
||||
avatarUrl?: string;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||
* Spec SHA256: a5e14ed849c9c55f18facf00106e60c0557da2f69fa246fd717783f0cb0d80ab
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||
* Spec SHA256: a5e14ed849c9c55f18facf00106e60c0557da2f69fa246fd717783f0cb0d80ab
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||
* Spec SHA256: a5e14ed849c9c55f18facf00106e60c0557da2f69fa246fd717783f0cb0d80ab
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||
* Spec SHA256: a5e14ed849c9c55f18facf00106e60c0557da2f69fa246fd717783f0cb0d80ab
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||
* Spec SHA256: a5e14ed849c9c55f18facf00106e60c0557da2f69fa246fd717783f0cb0d80ab
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||
* Spec SHA256: a5e14ed849c9c55f18facf00106e60c0557da2f69fa246fd717783f0cb0d80ab
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||
* Spec SHA256: a5e14ed849c9c55f18facf00106e60c0557da2f69fa246fd717783f0cb0d80ab
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||
* Spec SHA256: a5e14ed849c9c55f18facf00106e60c0557da2f69fa246fd717783f0cb0d80ab
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||
* Spec SHA256: a5e14ed849c9c55f18facf00106e60c0557da2f69fa246fd717783f0cb0d80ab
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||
* Spec SHA256: a5e14ed849c9c55f18facf00106e60c0557da2f69fa246fd717783f0cb0d80ab
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
@@ -9,7 +9,7 @@ export interface DriverProfileDriverSummaryDTO {
|
||||
id: string;
|
||||
name: string;
|
||||
country: string;
|
||||
avatarUrl: string;
|
||||
avatarUrl?: string;
|
||||
iracingId?: string;
|
||||
joinedAt: string;
|
||||
category?: string;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||
* Spec SHA256: a5e14ed849c9c55f18facf00106e60c0557da2f69fa246fd717783f0cb0d80ab
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||
* Spec SHA256: a5e14ed849c9c55f18facf00106e60c0557da2f69fa246fd717783f0cb0d80ab
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||
* Spec SHA256: a5e14ed849c9c55f18facf00106e60c0557da2f69fa246fd717783f0cb0d80ab
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
@@ -9,5 +9,5 @@ export interface DriverProfileSocialFriendSummaryDTO {
|
||||
id: string;
|
||||
name: string;
|
||||
country: string;
|
||||
avatarUrl: string;
|
||||
avatarUrl?: string;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||
* Spec SHA256: a5e14ed849c9c55f18facf00106e60c0557da2f69fa246fd717783f0cb0d80ab
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||
* Spec SHA256: a5e14ed849c9c55f18facf00106e60c0557da2f69fa246fd717783f0cb0d80ab
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||
* Spec SHA256: a5e14ed849c9c55f18facf00106e60c0557da2f69fa246fd717783f0cb0d80ab
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||
* Spec SHA256: a5e14ed849c9c55f18facf00106e60c0557da2f69fa246fd717783f0cb0d80ab
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||
* Spec SHA256: a5e14ed849c9c55f18facf00106e60c0557da2f69fa246fd717783f0cb0d80ab
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||
* Spec SHA256: a5e14ed849c9c55f18facf00106e60c0557da2f69fa246fd717783f0cb0d80ab
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Auto-generated DTO from OpenAPI spec
|
||||
* Spec SHA256: c6aeaeed5d0c2f34dff6fe153482ffa67609b1132bb683867accabf828a8086e
|
||||
* Spec SHA256: a5e14ed849c9c55f18facf00106e60c0557da2f69fa246fd717783f0cb0d80ab
|
||||
* This file is generated by scripts/generate-api-types.ts
|
||||
* Do not edit manually - regenerate using: npm run api:generate-types
|
||||
*/
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user