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