Files
gridpilot.gg/apps/website/components/drivers/ProfileStats.tsx
2025-12-11 21:06:25 +01:00

269 lines
10 KiB
TypeScript

'use client';
import Card from '../ui/Card';
import RankBadge from './RankBadge';
import { getLeagueRankings, getGetProfileOverviewUseCase } from '@/lib/di-container';
import { useState, useEffect } from 'react';
import { getPrimaryLeagueIdForDriver } from '@/lib/leagueMembership';
import type { ProfileOverviewViewModel } from '@gridpilot/racing/application/presenters/IProfileOverviewPresenter';
interface ProfileStatsProps {
driverId?: string;
stats?: {
totalRaces: number;
wins: number;
podiums: number;
dnfs: number;
avgFinish: number;
completionRate: number;
};
}
type DriverProfileOverviewViewModel = ProfileOverviewViewModel | null;
export default function ProfileStats({ driverId, stats }: ProfileStatsProps) {
const [profileData, setProfileData] = useState<DriverProfileOverviewViewModel>(null);
useEffect(() => {
if (driverId) {
const load = async () => {
const profileUseCase = getGetProfileOverviewUseCase();
const vm = await profileUseCase.execute({ driverId });
setProfileData(vm);
};
void load();
}
}, [driverId]);
const driverStats = profileData?.stats || null;
const totalDrivers = profileData?.currentDriver?.totalDrivers ?? 0;
const primaryLeagueId = driverId ? getPrimaryLeagueIdForDriver(driverId) : null;
const leagueRank =
driverId && primaryLeagueId ? getLeagueRankings(driverId, primaryLeagueId) : null;
const defaultStats =
stats ||
(driverStats
? {
totalRaces: driverStats.totalRaces,
wins: driverStats.wins,
podiums: driverStats.podiums,
dnfs: driverStats.dnfs,
avgFinish: driverStats.avgFinish ?? 0,
completionRate:
driverStats.totalRaces > 0
? ((driverStats.totalRaces - driverStats.dnfs) / driverStats.totalRaces) * 100
: 0,
}
: null);
const winRate =
defaultStats && defaultStats.totalRaces > 0
? ((defaultStats.wins / defaultStats.totalRaces) * 100).toFixed(1)
: '0.0';
const podiumRate =
defaultStats && defaultStats.totalRaces > 0
? ((defaultStats.podiums / defaultStats.totalRaces) * 100).toFixed(1)
: '0.0';
const getTrendIndicator = (value: number) => {
if (value > 0) return '↑';
if (value < 0) return '↓';
return '→';
};
const getPercentileLabel = (percentile: number) => {
if (percentile >= 90) return 'Top 10%';
if (percentile >= 75) return 'Top 25%';
if (percentile >= 50) return 'Top 50%';
return `${(100 - percentile).toFixed(0)}th percentile`;
};
const getPercentileColor = (percentile: number) => {
if (percentile >= 90) return 'text-green-400';
if (percentile >= 75) return 'text-primary-blue';
if (percentile >= 50) return 'text-warning-amber';
return 'text-gray-400';
};
return (
<div className="space-y-6">
{driverStats && (
<Card>
<h3 className="text-xl font-semibold text-white mb-6">Rankings Dashboard</h3>
<div className="space-y-4">
<div className="p-4 rounded-lg bg-deep-graphite border border-charcoal-outline">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-3">
<RankBadge rank={driverStats.overallRank ?? 0} size="lg" />
<div>
<div className="text-white font-medium text-lg">Overall Ranking</div>
<div className="text-sm text-gray-400">
{driverStats.overallRank ?? 0} of {totalDrivers} drivers
</div>
</div>
</div>
<div className="text-right">
<div
className={`text-sm font-medium ${getPercentileColor(driverStats.percentile ?? 0)}`}
>
{getPercentileLabel(driverStats.percentile ?? 0)}
</div>
<div className="text-xs text-gray-500">Global Percentile</div>
</div>
</div>
<div className="grid grid-cols-3 gap-4 pt-3 border-t border-charcoal-outline">
<div className="text-center">
<div className="text-2xl font-bold text-primary-blue">
{driverStats.rating ?? 0}
</div>
<div className="text-xs text-gray-400">Rating</div>
</div>
<div className="text-center">
<div className="text-lg font-bold text-green-400">
{getTrendIndicator(5)} {winRate}%
</div>
<div className="text-xs text-gray-400">Win Rate</div>
</div>
<div className="text-center">
<div className="text-lg font-bold text-warning-amber">
{getTrendIndicator(2)} {podiumRate}%
</div>
<div className="text-xs text-gray-400">Podium Rate</div>
</div>
</div>
</div>
{leagueRank && leagueRank.totalDrivers > 0 && (
<div className="p-4 rounded-lg bg-deep-graphite border border-charcoal-outline">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-3">
<RankBadge rank={leagueRank.rank} size="md" />
<div>
<div className="text-white font-medium">Primary League</div>
<div className="text-sm text-gray-400">
{leagueRank.rank} of {leagueRank.totalDrivers} drivers
</div>
</div>
</div>
<div className="text-right">
<div className={`text-sm font-medium ${getPercentileColor(leagueRank.percentile)}`}>
{getPercentileLabel(leagueRank.percentile)}
</div>
<div className="text-xs text-gray-500">League Percentile</div>
</div>
</div>
</div>
)}
</div>
</Card>
)}
{defaultStats ? (
<>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{[
{
label: 'Total Races',
value: defaultStats.totalRaces,
color: 'text-primary-blue',
},
{ label: 'Wins', value: defaultStats.wins, color: 'text-green-400' },
{
label: 'Podiums',
value: defaultStats.podiums,
color: 'text-warning-amber',
},
{ label: 'DNFs', value: defaultStats.dnfs, color: 'text-red-400' },
{
label: 'Avg Finish',
value: defaultStats.avgFinish.toFixed(1),
color: 'text-white',
},
{
label: 'Completion',
value: `${defaultStats.completionRate.toFixed(1)}%`,
color: 'text-green-400',
},
{ label: 'Win Rate', value: `${winRate}%`, color: 'text-primary-blue' },
{
label: 'Podium Rate',
value: `${podiumRate}%`,
color: 'text-warning-amber',
},
].map((stat, index) => (
<Card key={index} className="text-center">
<div className="text-sm text-gray-400 mb-1">{stat.label}</div>
<div className={`text-2xl font-bold ${stat.color}`}>{stat.value}</div>
</Card>
))}
</div>
</>
) : (
<Card>
<h3 className="text-lg font-semibold text-white mb-2">Career Statistics</h3>
<p className="text-sm text-gray-400">
No statistics available yet. Compete in races to start building your record.
</p>
</Card>
)}
<Card className="bg-charcoal-200/50 border-primary-blue/30">
<div className="flex items-center gap-3 mb-3">
<div className="text-2xl">📊</div>
<h3 className="text-lg font-semibold text-white">Performance by Car Class</h3>
</div>
<p className="text-gray-400 text-sm">
Detailed per-car and per-class performance breakdowns will be available in a future
version once more race history data is tracked.
</p>
</Card>
<Card className="bg-charcoal-200/50 border-primary-blue/30">
<div className="flex items-center gap-3 mb-3">
<div className="text-2xl">📈</div>
<h3 className="text-lg font-semibold text-white">Coming Soon</h3>
</div>
<p className="text-gray-400 text-sm">
Performance trends, track-specific stats, head-to-head comparisons vs friends, and
league member comparisons will be available in production.
</p>
</Card>
</div>
);
}
function PerformanceRow({ label, races, wins, podiums, avgFinish }: {
label: string;
races: number;
wins: number;
podiums: number;
avgFinish: number;
}) {
const winRate = ((wins / races) * 100).toFixed(0);
return (
<div className="flex items-center justify-between p-3 rounded bg-deep-graphite border border-charcoal-outline">
<div className="flex-1">
<div className="text-white font-medium">{label}</div>
<div className="text-gray-500 text-xs">{races} races</div>
</div>
<div className="flex items-center gap-6 text-xs">
<div>
<div className="text-gray-500">Wins</div>
<div className="text-green-400 font-medium">{wins} ({winRate}%)</div>
</div>
<div>
<div className="text-gray-500">Podiums</div>
<div className="text-warning-amber font-medium">{podiums}</div>
</div>
<div>
<div className="text-gray-500">Avg</div>
<div className="text-white font-medium">{avgFinish.toFixed(1)}</div>
</div>
</div>
</div>
);
}