1057 lines
45 KiB
TypeScript
1057 lines
45 KiB
TypeScript
'use client';
|
|
|
|
import CreateDriverForm from '@/components/drivers/CreateDriverForm';
|
|
import ProfileRaceHistory from '@/components/drivers/ProfileRaceHistory';
|
|
import ProfileSettings from '@/components/drivers/ProfileSettings';
|
|
import Button from '@/components/ui/Button';
|
|
import Card from '@/components/ui/Card';
|
|
import Heading from '@/components/ui/Heading';
|
|
import { useEffectiveDriverId } from '@/hooks/useEffectiveDriverId';
|
|
import { useServices } from '@/lib/services/ServiceProvider';
|
|
import type {
|
|
DriverProfileAchievementViewModel,
|
|
DriverProfileSocialHandleViewModel,
|
|
DriverProfileViewModel
|
|
} from '@/lib/view-models/DriverProfileViewModel';
|
|
import { getMediaUrl } from '@/lib/utilities/media';
|
|
import {
|
|
Activity,
|
|
Award,
|
|
BarChart3,
|
|
Calendar,
|
|
ChevronRight,
|
|
Clock,
|
|
Crown,
|
|
Edit3,
|
|
ExternalLink,
|
|
Flag,
|
|
Globe,
|
|
History,
|
|
Medal,
|
|
MessageCircle,
|
|
Percent,
|
|
Settings,
|
|
Shield,
|
|
Star,
|
|
Target,
|
|
TrendingUp,
|
|
Trophy,
|
|
Twitch,
|
|
Twitter,
|
|
User,
|
|
UserPlus,
|
|
Users,
|
|
Youtube,
|
|
Zap,
|
|
} from 'lucide-react';
|
|
import Image from 'next/image';
|
|
import Link from 'next/link';
|
|
import { useRouter, useSearchParams } from 'next/navigation';
|
|
import { useEffect, useState } from 'react';
|
|
|
|
// ============================================================================
|
|
// TYPES
|
|
// ============================================================================
|
|
|
|
type ProfileTab = 'overview' | 'history' | 'stats';
|
|
|
|
// ============================================================================
|
|
// HELPER COMPONENTS
|
|
// ============================================================================
|
|
|
|
function getCountryFlag(countryCode: string): string {
|
|
const code = countryCode.toUpperCase();
|
|
if (code.length === 2) {
|
|
const codePoints = [...code].map(char => 127397 + char.charCodeAt(0));
|
|
return String.fromCodePoint(...codePoints);
|
|
}
|
|
return '🏁';
|
|
}
|
|
|
|
function getRarityColor(rarity: DriverProfileAchievementViewModel['rarity']) {
|
|
switch (rarity) {
|
|
case 'common':
|
|
return 'text-gray-400 bg-gray-400/10 border-gray-400/30';
|
|
case 'rare':
|
|
return 'text-primary-blue bg-primary-blue/10 border-primary-blue/30';
|
|
case 'epic':
|
|
return 'text-purple-400 bg-purple-400/10 border-purple-400/30';
|
|
case 'legendary':
|
|
return 'text-yellow-400 bg-yellow-400/10 border-yellow-400/30';
|
|
}
|
|
}
|
|
|
|
function getAchievementIcon(icon: DriverProfileAchievementViewModel['icon']) {
|
|
switch (icon) {
|
|
case 'trophy':
|
|
return Trophy;
|
|
case 'medal':
|
|
return Medal;
|
|
case 'star':
|
|
return Star;
|
|
case 'crown':
|
|
return Crown;
|
|
case 'target':
|
|
return Target;
|
|
case 'zap':
|
|
return Zap;
|
|
}
|
|
}
|
|
|
|
function getSocialIcon(platform: DriverProfileSocialHandleViewModel['platform']) {
|
|
switch (platform) {
|
|
case 'twitter':
|
|
return Twitter;
|
|
case 'youtube':
|
|
return Youtube;
|
|
case 'twitch':
|
|
return Twitch;
|
|
case 'discord':
|
|
return MessageCircle;
|
|
}
|
|
}
|
|
|
|
function getSocialColor(platform: DriverProfileSocialHandleViewModel['platform']) {
|
|
switch (platform) {
|
|
case 'twitter':
|
|
return 'hover:text-sky-400 hover:bg-sky-400/10';
|
|
case 'youtube':
|
|
return 'hover:text-red-500 hover:bg-red-500/10';
|
|
case 'twitch':
|
|
return 'hover:text-purple-400 hover:bg-purple-400/10';
|
|
case 'discord':
|
|
return 'hover:text-indigo-400 hover:bg-indigo-400/10';
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// STAT DIAGRAM COMPONENTS
|
|
// ============================================================================
|
|
|
|
interface CircularProgressProps {
|
|
value: number;
|
|
max: number;
|
|
label: string;
|
|
color: string;
|
|
size?: number;
|
|
}
|
|
|
|
function CircularProgress({ value, max, label, color, size = 80 }: CircularProgressProps) {
|
|
const percentage = Math.min((value / max) * 100, 100);
|
|
const strokeWidth = 6;
|
|
const radius = (size - strokeWidth) / 2;
|
|
const circumference = radius * 2 * Math.PI;
|
|
const strokeDashoffset = circumference - (percentage / 100) * circumference;
|
|
|
|
return (
|
|
<div className="flex flex-col items-center">
|
|
<div className="relative" style={{ width: size, height: size }}>
|
|
<svg className="transform -rotate-90" width={size} height={size}>
|
|
<circle
|
|
cx={size / 2}
|
|
cy={size / 2}
|
|
r={radius}
|
|
stroke="currentColor"
|
|
strokeWidth={strokeWidth}
|
|
fill="transparent"
|
|
className="text-charcoal-outline"
|
|
/>
|
|
<circle
|
|
cx={size / 2}
|
|
cy={size / 2}
|
|
r={radius}
|
|
stroke="currentColor"
|
|
strokeWidth={strokeWidth}
|
|
fill="transparent"
|
|
strokeDasharray={circumference}
|
|
strokeDashoffset={strokeDashoffset}
|
|
strokeLinecap="round"
|
|
className={color}
|
|
style={{ transition: 'stroke-dashoffset 0.5s ease-in-out' }}
|
|
/>
|
|
</svg>
|
|
<div className="absolute inset-0 flex items-center justify-center">
|
|
<span className="text-lg font-bold text-white">{percentage.toFixed(0)}%</span>
|
|
</div>
|
|
</div>
|
|
<span className="text-xs text-gray-400 mt-2">{label}</span>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface BarChartProps {
|
|
data: { label: string; value: number; color: string }[];
|
|
maxValue: number;
|
|
}
|
|
|
|
function HorizontalBarChart({ data, maxValue }: BarChartProps) {
|
|
return (
|
|
<div className="space-y-3">
|
|
{data.map((item) => (
|
|
<div key={item.label}>
|
|
<div className="flex justify-between text-sm mb-1">
|
|
<span className="text-gray-400">{item.label}</span>
|
|
<span className="text-white font-medium">{item.value}</span>
|
|
</div>
|
|
<div className="h-2 bg-charcoal-outline rounded-full overflow-hidden">
|
|
<div
|
|
className={`h-full rounded-full ${item.color} transition-all duration-500 ease-out`}
|
|
style={{ width: `${Math.min((item.value / maxValue) * 100, 100)}%` }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface FinishDistributionProps {
|
|
wins: number;
|
|
podiums: number;
|
|
topTen: number;
|
|
total: number;
|
|
}
|
|
|
|
function FinishDistributionChart({ wins, podiums, topTen, total }: FinishDistributionProps) {
|
|
const outsideTopTen = total - topTen;
|
|
const podiumsNotWins = podiums - wins;
|
|
const topTenNotPodium = topTen - podiums;
|
|
|
|
const segments = [
|
|
{ label: 'Wins', value: wins, color: 'bg-performance-green', textColor: 'text-performance-green' },
|
|
{ label: 'Podiums', value: podiumsNotWins, color: 'bg-warning-amber', textColor: 'text-warning-amber' },
|
|
{ label: 'Top 10', value: topTenNotPodium, color: 'bg-primary-blue', textColor: 'text-primary-blue' },
|
|
{ label: 'Other', value: outsideTopTen, color: 'bg-gray-600', textColor: 'text-gray-400' },
|
|
].filter(s => s.value > 0);
|
|
|
|
return (
|
|
<div className="space-y-3">
|
|
<div className="h-4 rounded-full overflow-hidden flex bg-charcoal-outline">
|
|
{segments.map((segment, index) => (
|
|
<div
|
|
key={segment.label}
|
|
className={`${segment.color} transition-all duration-500`}
|
|
style={{ width: `${(segment.value / total) * 100}%` }}
|
|
/>
|
|
))}
|
|
</div>
|
|
<div className="flex flex-wrap gap-4 justify-center">
|
|
{segments.map((segment) => (
|
|
<div key={segment.label} className="flex items-center gap-2">
|
|
<div className={`w-3 h-3 rounded-full ${segment.color}`} />
|
|
<span className={`text-xs ${segment.textColor}`}>
|
|
{segment.label}: {segment.value} ({((segment.value / total) * 100).toFixed(0)}%)
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// ============================================================================
|
|
// MAIN PAGE COMPONENT
|
|
// ============================================================================
|
|
|
|
export default function ProfilePage() {
|
|
const router = useRouter();
|
|
const searchParams = useSearchParams();
|
|
const tabParam = searchParams.get('tab') as ProfileTab | null;
|
|
|
|
const { driverService, mediaService } = useServices();
|
|
|
|
const [profileData, setProfileData] = useState<DriverProfileViewModel | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [editMode, setEditMode] = useState(false);
|
|
const [activeTab, setActiveTab] = useState<ProfileTab>(tabParam || 'overview');
|
|
const [friendRequestSent, setFriendRequestSent] = useState(false);
|
|
|
|
const effectiveDriverId = useEffectiveDriverId();
|
|
const isOwnProfile = true; // This page is always your own profile
|
|
|
|
useEffect(() => {
|
|
if (!effectiveDriverId) {
|
|
return;
|
|
}
|
|
|
|
const loadData = async () => {
|
|
setLoading(true);
|
|
try {
|
|
const profileViewModel = await driverService.getDriverProfile(effectiveDriverId);
|
|
setProfileData(profileViewModel);
|
|
} catch (error) {
|
|
console.error('Failed to load profile:', error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
void loadData();
|
|
}, [effectiveDriverId, driverService]);
|
|
|
|
// Update URL when tab changes
|
|
useEffect(() => {
|
|
if (tabParam !== activeTab) {
|
|
const params = new URLSearchParams(searchParams.toString());
|
|
if (activeTab === 'overview') {
|
|
params.delete('tab');
|
|
} else {
|
|
params.set('tab', activeTab);
|
|
}
|
|
const query = params.toString();
|
|
router.replace(`/profile${query ? `?${query}` : ''}`, { scroll: false });
|
|
}
|
|
}, [activeTab, tabParam, searchParams, router]);
|
|
|
|
// Sync tab from URL on mount and param change
|
|
useEffect(() => {
|
|
if (tabParam && tabParam !== activeTab) {
|
|
setActiveTab(tabParam);
|
|
}
|
|
}, [tabParam]);
|
|
|
|
const handleSaveSettings = async (updates: { bio?: string; country?: string }) => {
|
|
if (!profileData?.currentDriver) return;
|
|
|
|
try {
|
|
const updatedProfile = await driverService.updateProfile(updates);
|
|
setProfileData(updatedProfile);
|
|
setEditMode(false);
|
|
} catch (error) {
|
|
console.error('Failed to update profile:', error);
|
|
}
|
|
};
|
|
|
|
const handleAddFriend = () => {
|
|
setFriendRequestSent(true);
|
|
// In production, this would call a use case
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="max-w-6xl mx-auto px-4">
|
|
<div className="flex items-center justify-center min-h-[400px]">
|
|
<div className="flex flex-col items-center gap-4">
|
|
<div className="w-10 h-10 border-2 border-primary-blue border-t-transparent rounded-full animate-spin" />
|
|
<p className="text-gray-400">Loading profile...</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!profileData?.currentDriver) {
|
|
return (
|
|
<div className="max-w-4xl mx-auto px-4">
|
|
<div className="text-center mb-8">
|
|
<div className="flex h-16 w-16 items-center justify-center rounded-2xl bg-gradient-to-br from-primary-blue/20 to-purple-600/10 border border-primary-blue/30 mx-auto mb-4">
|
|
<User className="w-8 h-8 text-primary-blue" />
|
|
</div>
|
|
<Heading level={1} className="mb-2">Create Your Driver Profile</Heading>
|
|
<p className="text-gray-400">
|
|
Join the GridPilot community and start your racing journey
|
|
</p>
|
|
</div>
|
|
|
|
<Card className="max-w-2xl mx-auto">
|
|
<div className="mb-6">
|
|
<h2 className="text-xl font-semibold text-white mb-2">Get Started</h2>
|
|
<p className="text-gray-400 text-sm">
|
|
Create your driver profile to join leagues, compete in races, and connect with other drivers.
|
|
</p>
|
|
</div>
|
|
<CreateDriverForm />
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Extract data from profileData ViewModel
|
|
const currentDriver = profileData.currentDriver;
|
|
const stats = profileData.stats;
|
|
const teamMemberships = profileData.teamMemberships;
|
|
const socialSummary = profileData.socialSummary;
|
|
const extendedProfile = profileData.extendedProfile;
|
|
const globalRank = currentDriver?.globalRank || null;
|
|
|
|
// Show edit mode
|
|
if (editMode) {
|
|
return (
|
|
<div className="max-w-4xl mx-auto px-4 space-y-6">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<Heading level={1}>Edit Profile</Heading>
|
|
<Button variant="secondary" onClick={() => setEditMode(false)}>
|
|
Cancel
|
|
</Button>
|
|
</div>
|
|
<ProfileSettings driver={profileData.currentDriver} onSave={handleSaveSettings} />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="max-w-7xl mx-auto px-4 pb-12 space-y-6">
|
|
{/* Hero Header Section */}
|
|
<div className="relative rounded-2xl overflow-hidden bg-gradient-to-br from-iron-gray/80 via-iron-gray/60 to-deep-graphite border border-charcoal-outline">
|
|
{/* Background Pattern */}
|
|
<div className="absolute inset-0 opacity-5">
|
|
<div className="absolute inset-0" style={{
|
|
backgroundImage: `url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23ffffff' fill-opacity='0.4'%3E%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E")`,
|
|
}} />
|
|
</div>
|
|
|
|
<div className="relative p-6 md:p-8">
|
|
<div className="flex flex-col md:flex-row md:items-start gap-6">
|
|
{/* Avatar */}
|
|
<div className="relative">
|
|
<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={getMediaUrl('driver-avatar', currentDriver.id)}
|
|
alt={currentDriver.name}
|
|
width={144}
|
|
height={144}
|
|
className="w-full h-full object-cover"
|
|
/>
|
|
</div>
|
|
</div>
|
|
{/* Online status indicator */}
|
|
<div className="absolute -bottom-1 -right-1 w-6 h-6 rounded-full bg-performance-green border-4 border-iron-gray" />
|
|
</div>
|
|
|
|
{/* Driver Info */}
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex flex-wrap items-center gap-3 mb-2">
|
|
<h1 className="text-3xl md:text-4xl font-bold text-white">{currentDriver.name}</h1>
|
|
<span className="text-4xl" aria-label={`Country: ${currentDriver.country}`}>
|
|
{getCountryFlag(currentDriver.country)}
|
|
</span>
|
|
{teamMemberships.length > 0 && teamMemberships[0] && (
|
|
<span className="px-3 py-1 bg-purple-600/20 text-purple-400 rounded-full text-sm font-semibold border border-purple-600/30">
|
|
[{teamMemberships[0].teamTag || 'TEAM'}]
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
{/* Rating and Rank */}
|
|
<div className="flex flex-wrap items-center gap-4 mb-4">
|
|
{stats && (
|
|
<>
|
|
<div className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-primary-blue/10 border border-primary-blue/30">
|
|
<Star className="w-4 h-4 text-primary-blue" />
|
|
<span className="font-mono font-bold text-primary-blue">
|
|
{stats.rating ?? 0}
|
|
</span>
|
|
<span className="text-xs text-gray-400">Rating</span>
|
|
</div>
|
|
<div className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-yellow-400/10 border border-yellow-400/30">
|
|
<Trophy className="w-4 h-4 text-yellow-400" />
|
|
<span className="font-mono font-bold text-yellow-400">#{globalRank}</span>
|
|
<span className="text-xs text-gray-400">Global</span>
|
|
</div>
|
|
</>
|
|
)}
|
|
{teamMemberships.length > 0 && teamMemberships[0] && (
|
|
<Link
|
|
href={`/teams/${teamMemberships[0].teamId}`}
|
|
className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-purple-600/10 border border-purple-600/30 hover:bg-purple-600/20 transition-colors"
|
|
>
|
|
<Users className="w-4 h-4 text-purple-400" />
|
|
<span className="text-purple-400 font-medium">{teamMemberships[0].teamName}</span>
|
|
<ChevronRight className="w-3 h-3 text-purple-400" />
|
|
</Link>
|
|
)}
|
|
</div>
|
|
|
|
{/* Meta info */}
|
|
<div className="flex flex-wrap items-center gap-4 text-sm text-gray-400">
|
|
<span className="flex items-center gap-1.5">
|
|
<Globe className="w-4 h-4" />
|
|
iRacing: {currentDriver.iracingId}
|
|
</span>
|
|
<span className="flex items-center gap-1.5">
|
|
<Calendar className="w-4 h-4" />
|
|
Joined {new Date(currentDriver.joinedAt).toLocaleDateString('en-US', { month: 'short', year: 'numeric' })}
|
|
</span>
|
|
{extendedProfile && (
|
|
<span className="flex items-center gap-1.5">
|
|
<Clock className="w-4 h-4" />
|
|
{extendedProfile.timezone}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Action Buttons */}
|
|
<div className="flex flex-col gap-2">
|
|
{isOwnProfile ? (
|
|
<Button
|
|
variant="primary"
|
|
onClick={() => setEditMode(true)}
|
|
className="flex items-center gap-2"
|
|
>
|
|
<Edit3 className="w-4 h-4" />
|
|
Edit Profile
|
|
</Button>
|
|
) : (
|
|
<>
|
|
<Button
|
|
variant="primary"
|
|
onClick={handleAddFriend}
|
|
disabled={friendRequestSent}
|
|
className="flex items-center gap-2"
|
|
>
|
|
<UserPlus className="w-4 h-4" />
|
|
{friendRequestSent ? 'Request Sent' : 'Add Friend'}
|
|
</Button>
|
|
</>
|
|
)}
|
|
<Link href="/profile/leagues">
|
|
<Button variant="secondary" className="w-full flex items-center gap-2">
|
|
<Flag className="w-4 h-4" />
|
|
My Leagues
|
|
</Button>
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Social Handles */}
|
|
{extendedProfile && extendedProfile.socialHandles.length > 0 && (
|
|
<div className="mt-6 pt-6 border-t border-charcoal-outline/50">
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
<span className="text-sm text-gray-500 mr-2">Connect:</span>
|
|
{extendedProfile.socialHandles.map((social) => {
|
|
const Icon = getSocialIcon(social.platform);
|
|
return (
|
|
<a
|
|
key={social.platform}
|
|
href={social.url}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className={`flex items-center gap-2 px-3 py-1.5 rounded-lg bg-iron-gray/50 border border-charcoal-outline text-gray-400 transition-all ${getSocialColor(social.platform)}`}
|
|
>
|
|
<Icon className="w-4 h-4" />
|
|
<span className="text-sm">{social.handle}</span>
|
|
<ExternalLink className="w-3 h-3 opacity-50" />
|
|
</a>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Bio Section */}
|
|
{currentDriver.bio && (
|
|
<Card>
|
|
<h2 className="text-lg font-semibold text-white mb-3 flex items-center gap-2">
|
|
<User className="w-5 h-5 text-primary-blue" />
|
|
About
|
|
</h2>
|
|
<p className="text-gray-300 leading-relaxed">{currentDriver.bio}</p>
|
|
</Card>
|
|
)}
|
|
|
|
{/* Team Memberships */}
|
|
{teamMemberships.length > 0 && (
|
|
<Card>
|
|
<h2 className="text-lg font-semibold text-white mb-4 flex items-center gap-2">
|
|
<Shield className="w-5 h-5 text-purple-400" />
|
|
Team Memberships
|
|
<span className="text-sm text-gray-500 font-normal">({teamMemberships.length})</span>
|
|
</h2>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
{teamMemberships.map((membership) => (
|
|
<Link
|
|
key={membership.teamId}
|
|
href={`/teams/${membership.teamId}`}
|
|
className="flex items-center gap-4 p-4 rounded-xl bg-iron-gray/30 border border-charcoal-outline hover:border-purple-400/30 hover:bg-iron-gray/50 transition-all group"
|
|
>
|
|
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-purple-600/20 border border-purple-600/30">
|
|
<Users className="w-6 h-6 text-purple-400" />
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<p className="text-white font-semibold truncate group-hover:text-purple-400 transition-colors">
|
|
{membership.teamName}
|
|
</p>
|
|
<div className="flex items-center gap-2 text-xs text-gray-400">
|
|
<span className="px-2 py-0.5 rounded-full bg-purple-600/20 text-purple-400 capitalize">
|
|
{membership.role}
|
|
</span>
|
|
<span>
|
|
Since {new Date(membership.joinedAt).toLocaleDateString('en-US', { month: 'short', year: 'numeric' })}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<ChevronRight className="w-4 h-4 text-gray-500 group-hover:text-purple-400 transition-colors" />
|
|
</Link>
|
|
))}
|
|
</div>
|
|
</Card>
|
|
)}
|
|
|
|
{/* Key Stats Overview with Diagrams */}
|
|
{stats && (
|
|
<Card>
|
|
<h2 className="text-lg font-semibold text-white mb-6 flex items-center gap-2">
|
|
<Activity className="w-5 h-5 text-neon-aqua" />
|
|
Performance Overview
|
|
</h2>
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
|
{/* Circular Progress Charts */}
|
|
<div className="flex flex-col items-center">
|
|
<div className="flex gap-6 mb-4">
|
|
<CircularProgress
|
|
value={stats.wins}
|
|
max={stats.totalRaces}
|
|
label="Win Rate"
|
|
color="text-performance-green"
|
|
/>
|
|
<CircularProgress
|
|
value={stats.podiums}
|
|
max={stats.totalRaces}
|
|
label="Podium Rate"
|
|
color="text-warning-amber"
|
|
/>
|
|
</div>
|
|
<div className="flex gap-6">
|
|
<CircularProgress
|
|
value={stats.consistency ?? 0}
|
|
max={100}
|
|
label="Consistency"
|
|
color="text-primary-blue"
|
|
/>
|
|
<CircularProgress
|
|
value={stats.totalRaces - stats.dnfs}
|
|
max={stats.totalRaces}
|
|
label="Finish Rate"
|
|
color="text-neon-aqua"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Finish Distribution */}
|
|
<div className="md:col-span-2">
|
|
<h3 className="text-sm font-medium text-gray-400 mb-4 flex items-center gap-2">
|
|
<BarChart3 className="w-4 h-4" />
|
|
Finish Distribution
|
|
</h3>
|
|
<FinishDistributionChart
|
|
wins={stats.wins}
|
|
podiums={stats.podiums}
|
|
topTen={Math.min(stats.totalRaces - stats.dnfs, stats.totalRaces * 0.7)}
|
|
total={stats.totalRaces}
|
|
/>
|
|
|
|
<div className="mt-6 grid grid-cols-2 gap-4">
|
|
<div className="p-4 rounded-xl bg-deep-graphite border border-charcoal-outline">
|
|
<div className="flex items-center gap-2 mb-2">
|
|
<TrendingUp className="w-4 h-4 text-performance-green" />
|
|
<span className="text-xs text-gray-500 uppercase">Best Finish</span>
|
|
</div>
|
|
<p className="text-2xl font-bold text-performance-green">P{stats.bestFinish}</p>
|
|
</div>
|
|
<div className="p-4 rounded-xl bg-deep-graphite border border-charcoal-outline">
|
|
<div className="flex items-center gap-2 mb-2">
|
|
<Target className="w-4 h-4 text-primary-blue" />
|
|
<span className="text-xs text-gray-500 uppercase">Avg Finish</span>
|
|
</div>
|
|
<p className="text-2xl font-bold text-primary-blue">
|
|
P{(stats.avgFinish ?? 0).toFixed(1)}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
)}
|
|
|
|
{/* Tab Navigation */}
|
|
<div className="flex items-center gap-1 p-1.5 rounded-xl bg-iron-gray/50 border border-charcoal-outline w-fit relative z-10">
|
|
<button
|
|
type="button"
|
|
onClick={() => setActiveTab('overview')}
|
|
className={`flex items-center gap-2 px-5 py-2.5 rounded-lg text-sm font-medium transition-all cursor-pointer select-none ${
|
|
activeTab === 'overview'
|
|
? 'bg-primary-blue text-white shadow-lg shadow-primary-blue/25'
|
|
: 'text-gray-400 hover:text-white hover:bg-iron-gray/80'
|
|
}`}
|
|
>
|
|
<User className="w-4 h-4" />
|
|
Overview
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => setActiveTab('history')}
|
|
className={`flex items-center gap-2 px-5 py-2.5 rounded-lg text-sm font-medium transition-all cursor-pointer select-none ${
|
|
activeTab === 'history'
|
|
? 'bg-primary-blue text-white shadow-lg shadow-primary-blue/25'
|
|
: 'text-gray-400 hover:text-white hover:bg-iron-gray/80'
|
|
}`}
|
|
>
|
|
<History className="w-4 h-4" />
|
|
Race History
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => setActiveTab('stats')}
|
|
className={`flex items-center gap-2 px-5 py-2.5 rounded-lg text-sm font-medium transition-all cursor-pointer select-none ${
|
|
activeTab === 'stats'
|
|
? 'bg-primary-blue text-white shadow-lg shadow-primary-blue/25'
|
|
: 'text-gray-400 hover:text-white hover:bg-iron-gray/80'
|
|
}`}
|
|
>
|
|
<BarChart3 className="w-4 h-4" />
|
|
Detailed Stats
|
|
</button>
|
|
</div>
|
|
|
|
{/* Tab Content */}
|
|
{activeTab === 'overview' && (
|
|
<>
|
|
{/* Racing Profile & Quick Stats */}
|
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
{/* Career Stats Summary */}
|
|
<Card className="lg:col-span-2">
|
|
<h2 className="text-lg font-semibold text-white mb-4 flex items-center gap-2">
|
|
<TrendingUp className="w-5 h-5 text-performance-green" />
|
|
Career Statistics
|
|
</h2>
|
|
{stats ? (
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
|
<div className="p-4 rounded-xl bg-deep-graphite border border-charcoal-outline text-center">
|
|
<div className="text-3xl font-bold text-white mb-1">{stats.totalRaces}</div>
|
|
<div className="text-xs text-gray-500 uppercase tracking-wider">Races</div>
|
|
</div>
|
|
<div className="p-4 rounded-xl bg-deep-graphite border border-charcoal-outline text-center">
|
|
<div className="text-3xl font-bold text-performance-green mb-1">{stats.wins}</div>
|
|
<div className="text-xs text-gray-500 uppercase tracking-wider">Wins</div>
|
|
</div>
|
|
<div className="p-4 rounded-xl bg-deep-graphite border border-charcoal-outline text-center">
|
|
<div className="text-3xl font-bold text-warning-amber mb-1">{stats.podiums}</div>
|
|
<div className="text-xs text-gray-500 uppercase tracking-wider">Podiums</div>
|
|
</div>
|
|
<div className="p-4 rounded-xl bg-deep-graphite border border-charcoal-outline text-center">
|
|
<div className="text-3xl font-bold text-primary-blue mb-1">
|
|
{stats.consistency ?? 0}%
|
|
</div>
|
|
<div className="text-xs text-gray-500 uppercase tracking-wider">Consistency</div>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<p className="text-gray-400 text-sm">
|
|
No race statistics available yet. Join a league and compete to start building your record!
|
|
</p>
|
|
)}
|
|
</Card>
|
|
|
|
{/* Racing Preferences */}
|
|
<Card className="relative overflow-hidden">
|
|
{/* Background accent */}
|
|
<div className="absolute top-0 right-0 w-32 h-32 bg-gradient-to-bl from-neon-aqua/10 to-transparent rounded-bl-full" />
|
|
|
|
<h2 className="text-lg font-semibold text-white mb-5 flex items-center gap-2 relative">
|
|
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-neon-aqua/20 border border-neon-aqua/30">
|
|
<Settings className="w-4 h-4 text-neon-aqua" />
|
|
</div>
|
|
Racing Profile
|
|
</h2>
|
|
|
|
<div className="space-y-4 relative">
|
|
{extendedProfile && (
|
|
<>
|
|
{/* Racing Style - Featured */}
|
|
<div className="p-4 rounded-xl bg-gradient-to-r from-neon-aqua/10 to-transparent border border-neon-aqua/20">
|
|
<div className="flex items-center gap-3">
|
|
<Zap className="w-5 h-5 text-neon-aqua" />
|
|
<div>
|
|
<span className="text-xs text-gray-500 uppercase tracking-wider block">Racing Style</span>
|
|
<p className="text-white font-semibold text-lg">{extendedProfile.racingStyle}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Track & Car Grid */}
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<div className="p-3 rounded-lg bg-iron-gray/30 border border-charcoal-outline">
|
|
<div className="flex items-center gap-2 mb-1">
|
|
<Flag className="w-3.5 h-3.5 text-red-400" />
|
|
<span className="text-[10px] text-gray-500 uppercase tracking-wider">Track</span>
|
|
</div>
|
|
<p className="text-white font-medium text-sm truncate">{extendedProfile.favoriteTrack}</p>
|
|
</div>
|
|
<div className="p-3 rounded-lg bg-iron-gray/30 border border-charcoal-outline">
|
|
<div className="flex items-center gap-2 mb-1">
|
|
<Target className="w-3.5 h-3.5 text-primary-blue" />
|
|
<span className="text-[10px] text-gray-500 uppercase tracking-wider">Car</span>
|
|
</div>
|
|
<p className="text-white font-medium text-sm truncate">{extendedProfile.favoriteCar}</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Availability */}
|
|
<div className="flex items-center gap-3 p-3 rounded-lg bg-iron-gray/30 border border-charcoal-outline">
|
|
<Clock className="w-4 h-4 text-warning-amber" />
|
|
<div>
|
|
<span className="text-[10px] text-gray-500 uppercase tracking-wider block">Available</span>
|
|
<p className="text-white font-medium text-sm">{extendedProfile.availableHours}</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Status badges */}
|
|
<div className="pt-4 border-t border-charcoal-outline/50 space-y-2">
|
|
{extendedProfile.lookingForTeam && (
|
|
<div className="flex items-center gap-2 px-4 py-3 rounded-xl bg-gradient-to-r from-performance-green/20 to-performance-green/5 border border-performance-green/30">
|
|
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-performance-green/20">
|
|
<Users className="w-4 h-4 text-performance-green" />
|
|
</div>
|
|
<div>
|
|
<span className="text-sm text-performance-green font-semibold block">Looking for Team</span>
|
|
<span className="text-xs text-gray-500">Open to recruitment offers</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
{extendedProfile.openToRequests && (
|
|
<div className="flex items-center gap-2 px-4 py-3 rounded-xl bg-gradient-to-r from-primary-blue/20 to-primary-blue/5 border border-primary-blue/30">
|
|
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-primary-blue/20">
|
|
<UserPlus className="w-4 h-4 text-primary-blue" />
|
|
</div>
|
|
<div>
|
|
<span className="text-sm text-primary-blue font-semibold block">Open to Requests</span>
|
|
<span className="text-xs text-gray-500">Accepting friend invites</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Achievements */}
|
|
{extendedProfile && extendedProfile.achievements.length > 0 && (
|
|
<Card>
|
|
<h2 className="text-lg font-semibold text-white mb-4 flex items-center gap-2">
|
|
<Award className="w-5 h-5 text-yellow-400" />
|
|
Achievements
|
|
<span className="ml-auto text-sm text-gray-500">{extendedProfile.achievements.length} earned</span>
|
|
</h2>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
{extendedProfile.achievements.map((achievement) => {
|
|
const Icon = getAchievementIcon(achievement.icon);
|
|
const rarityClasses = getRarityColor(achievement.rarity);
|
|
return (
|
|
<div
|
|
key={achievement.id}
|
|
className={`p-4 rounded-xl border ${rarityClasses} transition-all hover:scale-105`}
|
|
>
|
|
<div className="flex items-start gap-3">
|
|
<div className={`flex h-10 w-10 items-center justify-center rounded-lg ${rarityClasses.split(' ')[1]}`}>
|
|
<Icon className={`w-5 h-5 ${rarityClasses.split(' ')[0]}`} />
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<p className="text-white font-semibold text-sm">{achievement.title}</p>
|
|
<p className="text-gray-400 text-xs mt-0.5">{achievement.description}</p>
|
|
<p className="text-gray-500 text-xs mt-1">
|
|
{new Date(achievement.earnedAt).toLocaleDateString('en-US', {
|
|
month: 'short',
|
|
day: 'numeric',
|
|
year: 'numeric',
|
|
})}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</Card>
|
|
)}
|
|
|
|
{/* Friends Preview */}
|
|
{socialSummary && socialSummary.friends.length > 0 && (
|
|
<Card>
|
|
<div className="flex items-center justify-between mb-4">
|
|
<h2 className="text-lg font-semibold text-white flex items-center gap-2">
|
|
<Users className="w-5 h-5 text-purple-400" />
|
|
Friends
|
|
<span className="text-sm text-gray-500 font-normal">({socialSummary.friendsCount})</span>
|
|
</h2>
|
|
</div>
|
|
<div className="flex flex-wrap gap-3">
|
|
{socialSummary.friends.slice(0, 8).map((friend) => (
|
|
<Link
|
|
key={friend.id}
|
|
href={`/drivers/${friend.id}`}
|
|
className="flex items-center gap-2 px-3 py-2 rounded-lg bg-iron-gray/50 border border-charcoal-outline hover:border-purple-400/30 hover:bg-iron-gray transition-all"
|
|
>
|
|
<div className="w-8 h-8 rounded-full overflow-hidden bg-gradient-to-br from-primary-blue to-purple-600">
|
|
<Image
|
|
src={getMediaUrl('driver-avatar', friend.id)}
|
|
alt={friend.name}
|
|
width={32}
|
|
height={32}
|
|
className="w-full h-full object-cover"
|
|
/>
|
|
</div>
|
|
<span className="text-sm text-white">{friend.name}</span>
|
|
<span className="text-lg">{getCountryFlag(friend.country)}</span>
|
|
</Link>
|
|
))}
|
|
{socialSummary.friendsCount > 8 && (
|
|
<div className="flex items-center px-3 py-2 text-sm text-gray-400">
|
|
+{socialSummary.friendsCount - 8} more
|
|
</div>
|
|
)}
|
|
</div>
|
|
</Card>
|
|
)}
|
|
</>
|
|
)}
|
|
|
|
{activeTab === 'history' && currentDriver && (
|
|
<Card>
|
|
<h2 className="text-lg font-semibold text-white mb-4 flex items-center gap-2">
|
|
<History className="w-5 h-5 text-red-400" />
|
|
Race History
|
|
</h2>
|
|
<ProfileRaceHistory driverId={currentDriver.id} />
|
|
</Card>
|
|
)}
|
|
|
|
{activeTab === 'stats' && stats && (
|
|
<div className="space-y-6">
|
|
{/* Detailed Performance Metrics */}
|
|
<Card>
|
|
<h2 className="text-lg font-semibold text-white mb-6 flex items-center gap-2">
|
|
<BarChart3 className="w-5 h-5 text-primary-blue" />
|
|
Detailed Performance Metrics
|
|
</h2>
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
|
{/* Performance Bars */}
|
|
<div>
|
|
<h3 className="text-sm font-medium text-gray-400 mb-4">Results Breakdown</h3>
|
|
<HorizontalBarChart
|
|
data={[
|
|
{ label: 'Wins', value: stats.wins, color: 'bg-performance-green' },
|
|
{ label: 'Podiums (2nd-3rd)', value: stats.podiums - stats.wins, color: 'bg-warning-amber' },
|
|
{ label: 'DNFs', value: stats.dnfs, color: 'bg-red-500' },
|
|
]}
|
|
maxValue={stats.totalRaces}
|
|
/>
|
|
</div>
|
|
|
|
{/* Key Metrics */}
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div className="p-4 rounded-xl bg-deep-graphite border border-charcoal-outline">
|
|
<div className="flex items-center gap-2 mb-2">
|
|
<Percent className="w-4 h-4 text-performance-green" />
|
|
<span className="text-xs text-gray-500 uppercase">Win Rate</span>
|
|
</div>
|
|
<p className="text-2xl font-bold text-performance-green">
|
|
{((stats.wins / stats.totalRaces) * 100).toFixed(1)}%
|
|
</p>
|
|
</div>
|
|
<div className="p-4 rounded-xl bg-deep-graphite border border-charcoal-outline">
|
|
<div className="flex items-center gap-2 mb-2">
|
|
<Percent className="w-4 h-4 text-warning-amber" />
|
|
<span className="text-xs text-gray-500 uppercase">Podium Rate</span>
|
|
</div>
|
|
<p className="text-2xl font-bold text-warning-amber">
|
|
{((stats.podiums / stats.totalRaces) * 100).toFixed(1)}%
|
|
</p>
|
|
</div>
|
|
<div className="p-4 rounded-xl bg-deep-graphite border border-charcoal-outline">
|
|
<div className="flex items-center gap-2 mb-2">
|
|
<Activity className="w-4 h-4 text-primary-blue" />
|
|
<span className="text-xs text-gray-500 uppercase">Consistency</span>
|
|
</div>
|
|
<p className="text-2xl font-bold text-primary-blue">
|
|
{stats.consistency ?? 0}%
|
|
</p>
|
|
</div>
|
|
<div className="p-4 rounded-xl bg-deep-graphite border border-charcoal-outline">
|
|
<div className="flex items-center gap-2 mb-2">
|
|
<Zap className="w-4 h-4 text-neon-aqua" />
|
|
<span className="text-xs text-gray-500 uppercase">Finish Rate</span>
|
|
</div>
|
|
<p className="text-2xl font-bold text-neon-aqua">
|
|
{(((stats.totalRaces - stats.dnfs) / stats.totalRaces) * 100).toFixed(1)}%
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
|
|
{/* Position Statistics */}
|
|
<Card>
|
|
<h2 className="text-lg font-semibold text-white mb-6 flex items-center gap-2">
|
|
<Flag className="w-5 h-5 text-red-400" />
|
|
Position Statistics
|
|
</h2>
|
|
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
|
<div className="p-4 rounded-xl bg-gradient-to-br from-performance-green/20 to-performance-green/5 border border-performance-green/30 text-center">
|
|
<div className="text-4xl font-bold text-performance-green mb-1">P{stats.bestFinish}</div>
|
|
<div className="text-xs text-gray-400 uppercase">Best Finish</div>
|
|
</div>
|
|
<div className="p-4 rounded-xl bg-gradient-to-br from-primary-blue/20 to-primary-blue/5 border border-primary-blue/30 text-center">
|
|
<div className="text-4xl font-bold text-primary-blue mb-1">
|
|
P{(stats.avgFinish ?? 0).toFixed(1)}
|
|
</div>
|
|
<div className="text-xs text-gray-400 uppercase">Avg Finish</div>
|
|
</div>
|
|
<div className="p-4 rounded-xl bg-gradient-to-br from-warning-amber/20 to-warning-amber/5 border border-warning-amber/30 text-center">
|
|
<div className="text-4xl font-bold text-warning-amber mb-1">P{stats.worstFinish}</div>
|
|
<div className="text-xs text-gray-400 uppercase">Worst Finish</div>
|
|
</div>
|
|
<div className="p-4 rounded-xl bg-gradient-to-br from-red-500/20 to-red-500/5 border border-red-500/30 text-center">
|
|
<div className="text-4xl font-bold text-red-400 mb-1">{stats.dnfs}</div>
|
|
<div className="text-xs text-gray-400 uppercase">DNFs</div>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
|
|
{/* Global Rankings */}
|
|
<Card>
|
|
<h2 className="text-lg font-semibold text-white mb-6 flex items-center gap-2">
|
|
<Trophy className="w-5 h-5 text-yellow-400" />
|
|
Global Rankings
|
|
</h2>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
<div className="p-6 rounded-xl bg-gradient-to-br from-yellow-400/20 to-yellow-600/5 border border-yellow-400/30 text-center">
|
|
<Trophy className="w-8 h-8 text-yellow-400 mx-auto mb-3" />
|
|
<div className="text-3xl font-bold text-yellow-400 mb-1">#{globalRank}</div>
|
|
<div className="text-sm text-gray-400">Global Rank</div>
|
|
</div>
|
|
<div className="p-6 rounded-xl bg-gradient-to-br from-primary-blue/20 to-primary-blue/5 border border-primary-blue/30 text-center">
|
|
<Star className="w-8 h-8 text-primary-blue mx-auto mb-3" />
|
|
<div className="text-3xl font-bold text-primary-blue mb-1">
|
|
{stats.rating ?? 0}
|
|
</div>
|
|
<div className="text-sm text-gray-400">Rating</div>
|
|
</div>
|
|
<div className="p-6 rounded-xl bg-gradient-to-br from-purple-400/20 to-purple-600/5 border border-purple-400/30 text-center">
|
|
<TrendingUp className="w-8 h-8 text-purple-400 mx-auto mb-3" />
|
|
<div className="text-3xl font-bold text-purple-400 mb-1">Top {stats.percentile}%</div>
|
|
<div className="text-sm text-gray-400">Percentile</div>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
)}
|
|
|
|
{activeTab === 'stats' && !stats && (
|
|
<Card className="text-center py-12">
|
|
<BarChart3 className="w-16 h-16 text-gray-600 mx-auto mb-4" />
|
|
<p className="text-gray-400 mb-2">No statistics available yet</p>
|
|
<p className="text-sm text-gray-500">Join a league and complete races to see detailed stats</p>
|
|
</Card>
|
|
)}
|
|
</div>
|
|
);
|
|
} |