harden media

This commit is contained in:
2025-12-31 15:39:28 +01:00
parent 92226800df
commit 8260bf7baf
413 changed files with 8361 additions and 1544 deletions

View File

@@ -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',
},
});
}

View File

@@ -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}

View File

@@ -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">

View File

@@ -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

View 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 });
}
}

View 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 });
}
}

View 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 });
}
}

View 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 });
}
}

View 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 });
}
}

View 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 });
}
}

View 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 });
}
}

View File

@@ -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>

View File

@@ -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}

View File

@@ -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}

View File

@@ -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">

View File

@@ -33,6 +33,7 @@ export default function DriverCard(props: DriverCardProps) {
const driverViewModel = new DriverViewModel({
id,
name,
avatarUrl: null,
});
return (

View File

@@ -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">

View File

@@ -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 */}

View File

@@ -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>

View File

@@ -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">

View File

@@ -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 {

View File

@@ -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,

View File

@@ -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">

View File

@@ -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">

View File

@@ -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>

View File

@@ -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');
});
});

View File

@@ -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(() => {

View File

@@ -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}

View File

@@ -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" />

View File

@@ -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 */}

View File

@@ -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}`);

View File

@@ -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">

View File

@@ -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 */}

View 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>
);
}

View 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;

View File

@@ -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

View File

@@ -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,
}),
);

View File

@@ -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;
}
}
}

View File

@@ -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`;
}
}

View File

@@ -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));
}
/**

View File

@@ -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));
}
/**

View File

@@ -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));
}
/**

View File

@@ -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));
}
/**

View File

@@ -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 = {

View File

@@ -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) {

View File

@@ -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
*/

View File

@@ -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
*/

View File

@@ -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
*/

View File

@@ -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
*/

View File

@@ -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
*/

View File

@@ -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
*/

View File

@@ -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
*/

View File

@@ -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
*/

View File

@@ -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
*/

View File

@@ -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
*/

View File

@@ -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
*/

View File

@@ -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
*/

View File

@@ -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
*/

View File

@@ -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
*/

View File

@@ -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
*/

View File

@@ -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
*/

View File

@@ -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
*/

View File

@@ -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
*/

View File

@@ -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
*/

View File

@@ -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
*/

View File

@@ -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
*/

View File

@@ -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
*/

View File

@@ -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
*/

View File

@@ -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
*/

View File

@@ -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
*/

View File

@@ -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
*/

View File

@@ -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
*/

View File

@@ -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
*/

View File

@@ -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
*/

View File

@@ -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
*/

View File

@@ -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
*/

View File

@@ -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;

View File

@@ -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
*/

View File

@@ -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
*/

View File

@@ -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;
}

View File

@@ -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
*/

View File

@@ -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
*/

View File

@@ -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
*/

View File

@@ -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
*/

View File

@@ -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
*/

View File

@@ -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
*/

View File

@@ -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
*/

View File

@@ -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
*/

View File

@@ -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
*/

View File

@@ -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;

View File

@@ -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
*/

View File

@@ -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
*/

View File

@@ -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;
}

View File

@@ -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
*/

View File

@@ -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
*/

View File

@@ -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
*/

View File

@@ -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
*/

View File

@@ -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
*/

View File

@@ -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
*/

View File

@@ -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