website refactor

This commit is contained in:
2026-01-14 23:31:57 +01:00
parent fbae5e6185
commit c1a86348d7
93 changed files with 7268 additions and 9088 deletions

View File

@@ -1,447 +1,232 @@
'use client';
import Card from '@/components/ui/Card';
import Button from '@/components/ui/Button';
import Heading from '@/components/ui/Heading';
import Image from 'next/image';
import Link from 'next/link';
import React from 'react';
import CreateDriverForm from '@/components/drivers/CreateDriverForm';
import ProfileRaceHistory from '@/components/drivers/ProfileRaceHistory';
import ProfileSettings from '@/components/drivers/ProfileSettings';
import { AchievementGrid } from '@/components/profile/AchievementGrid';
import { ProfileHero } from '@/components/profile/ProfileHero';
import { ProfileStatGrid } from '@/components/profile/ProfileStatGrid';
import { ProfileTabs } from '@/components/profile/ProfileTabs';
import { TeamMembershipGrid } from '@/components/profile/TeamMembershipGrid';
import type { ProfileViewData } from '@/lib/view-data/ProfileViewData';
import { Box } from '@/ui/Box';
import { Button } from '@/ui/Button';
import { Card } from '@/ui/Card';
import { Container } from '@/ui/Container';
import { Heading } from '@/ui/Heading';
import { Icon } from '@/ui/Icon';
import { Stack } from '@/ui/Stack';
import { Surface } from '@/ui/Surface';
import { Text } from '@/ui/Text';
import {
Activity,
Award,
BarChart3,
Calendar,
ChevronRight,
Clock,
Edit3,
ExternalLink,
Flag,
Globe,
History,
MessageCircle,
Percent,
Settings,
Shield,
Star,
Target,
TrendingUp,
Trophy,
Twitch,
Twitter,
User,
UserPlus,
Users,
Youtube,
Zap,
Medal,
Crown,
} from 'lucide-react';
import { useEffect, useState } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import ProfileSettings from '@/components/drivers/ProfileSettings';
import ProfileRaceHistory from '@/components/drivers/ProfileRaceHistory';
import CreateDriverForm from '@/components/drivers/CreateDriverForm';
import Breadcrumbs from '@/components/layout/Breadcrumbs';
type ProfileTab = 'overview' | 'history' | 'stats';
export type ProfileTab = 'overview' | 'history' | 'stats';
interface ProfileTemplateProps {
viewData: ProfileViewData | null;
viewData: ProfileViewData;
mode: 'profile-exists' | 'needs-profile';
activeTab: ProfileTab;
onTabChange: (tab: ProfileTab) => void;
editMode: boolean;
onEditModeChange: (edit: boolean) => void;
friendRequestSent: boolean;
onFriendRequestSend: () => void;
onSaveSettings: (updates: { bio?: string; country?: string }) => Promise<void>;
}
function getAchievementIcon(icon: NonNullable<ProfileViewData['extendedProfile']>['achievements'][number]['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(platformLabel: string) {
switch (platformLabel) {
case 'twitter':
return Twitter;
case 'youtube':
return Youtube;
case 'twitch':
return Twitch;
case 'discord':
return MessageCircle;
default:
return Globe;
}
}
export function ProfileTemplate({ viewData, mode, onSaveSettings }: ProfileTemplateProps) {
const router = useRouter();
const searchParams = useSearchParams();
const tabParam = searchParams.get('tab') as ProfileTab | null;
const [editMode, setEditMode] = useState(false);
const [activeTab, setActiveTab] = useState<ProfileTab>(tabParam || 'overview');
const [friendRequestSent, setFriendRequestSent] = useState(false);
useEffect(() => {
if (searchParams.get('tab') !== 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, searchParams, router]);
useEffect(() => {
const tab = searchParams.get('tab') as ProfileTab | null;
if (tab && tab !== activeTab) {
setActiveTab(tab);
}
}, [searchParams]);
export function ProfileTemplate({
viewData,
mode,
activeTab,
onTabChange,
editMode,
onEditModeChange,
friendRequestSent,
onFriendRequestSend,
onSaveSettings,
}: ProfileTemplateProps) {
if (mode === 'needs-profile') {
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>
<Container size="md">
<Stack align="center" gap={4} mb={8}>
<Surface variant="muted" rounded="xl" border padding={4}>
<Icon icon={User} size={8} color="#3b82f6" />
</Surface>
<Box>
<Heading level={1}>Create Your Driver Profile</Heading>
<Text color="text-gray-400">Join the GridPilot community and start your racing journey</Text>
</Box>
</Stack>
<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>
);
}
if (!viewData) {
return (
<div className="max-w-4xl mx-auto px-4">
<Card className="text-center py-12">
<User className="w-16 h-16 text-gray-600 mx-auto mb-4" />
<p className="text-gray-400 mb-2">Unable to load profile</p>
</Card>
</div>
<Box maxWidth="42rem" mx="auto">
<Card>
<Stack gap={6}>
<Box>
<Heading level={2}>Get Started</Heading>
<Text size="sm" color="text-gray-400">
Create your driver profile to join leagues, compete in races, and connect with other drivers.
</Text>
</Box>
<CreateDriverForm />
</Stack>
</Card>
</Box>
</Container>
);
}
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>
<Container size="md">
<Stack gap={6}>
<Stack direction="row" align="center" justify="between">
<Heading level={1}>Edit Profile</Heading>
<Button variant="secondary" onClick={() => onEditModeChange(false)}>
Cancel
</Button>
</Stack>
{/* ProfileSettings expects a DriverProfileDriverSummaryViewModel; keep existing component usage by passing a minimal compatible shape */}
<ProfileSettings
driver={{
id: viewData.driver.id,
name: viewData.driver.name,
country: viewData.driver.countryCode,
avatarUrl: viewData.driver.avatarUrl,
iracingId: viewData.driver.iracingId,
joinedAt: new Date().toISOString(),
rating: null,
globalRank: null,
consistency: null,
bio: viewData.driver.bio,
totalDrivers: null,
}}
onSave={async (updates) => {
await onSaveSettings(updates);
setEditMode(false);
}}
/>
</div>
<ProfileSettings
driver={{
id: viewData.driver.id,
name: viewData.driver.name,
country: viewData.driver.countryCode,
avatarUrl: viewData.driver.avatarUrl,
iracingId: viewData.driver.iracingId || '',
joinedAt: new Date().toISOString(),
rating: null,
globalRank: null,
consistency: null,
bio: viewData.driver.bio,
totalDrivers: null,
}}
onSave={async (updates) => {
await onSaveSettings(updates);
onEditModeChange(false);
}}
/>
</Stack>
</Container>
);
}
return (
<div className="max-w-7xl mx-auto px-4 pb-12 space-y-6">
{/* Hero */}
<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">
<div className="relative p-6 md:p-8">
<div className="flex flex-col md:flex-row md:items-start gap-6">
<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={viewData.driver.avatarUrl}
alt={viewData.driver.name}
width={144}
height={144}
className="w-full h-full object-cover"
/>
</div>
</div>
<div className="absolute -bottom-1 -right-1 w-6 h-6 rounded-full bg-performance-green border-4 border-iron-gray" />
</div>
<Container size="lg">
<Stack gap={6}>
{/* Back Navigation */}
<Box>
<Button
variant="secondary"
onClick={() => {}}
icon={<Icon icon={History} size={4} />}
>
Back to Drivers
</Button>
</Box>
<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">{viewData.driver.name}</h1>
<span className="text-4xl" aria-label={`Country: ${viewData.driver.countryCode}`}>{viewData.driver.countryFlag}</span>
{viewData.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">
[{viewData.teamMemberships[0].teamTag || 'TEAM'}]
</span>
)}
</div>
{/* Breadcrumb */}
<Breadcrumbs
items={[
{ label: 'Home', href: '/' },
{ label: 'Drivers', href: '/drivers' },
{ label: viewData.driver.name },
]}
/>
{viewData.stats && (
<div className="flex flex-wrap items-center gap-4 mb-4">
<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">{viewData.stats.ratingLabel}</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">{viewData.stats.globalRankLabel}</span>
<span className="text-xs text-gray-400">Global</span>
</div>
</div>
)}
<ProfileHero
driver={{
...viewData.driver,
country: viewData.driver.countryCode,
iracingId: Number(viewData.driver.iracingId) || 0,
joinedAt: new Date().toISOString(), // Placeholder
}}
stats={viewData.stats ? { rating: Number(viewData.stats.ratingLabel) || 0 } : null}
globalRank={Number(viewData.stats?.globalRankLabel) || 0}
timezone={viewData.extendedProfile?.timezone || 'UTC'}
socialHandles={viewData.extendedProfile?.socialHandles.map(s => ({ ...s, platform: s.platformLabel as any })) || []}
onAddFriend={onFriendRequestSend}
friendRequestSent={friendRequestSent}
/>
<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: {viewData.driver.iracingId ?? '—'}
</span>
<span className="flex items-center gap-1.5">
<Calendar className="w-4 h-4" />
Joined {viewData.driver.joinedAtLabel}
</span>
{viewData.extendedProfile && (
<span className="flex items-center gap-1.5">
<Clock className="w-4 h-4" />
{viewData.extendedProfile.timezone}
</span>
)}
</div>
</div>
<div className="flex flex-col gap-2">
<Button variant="primary" onClick={() => setEditMode(true)} className="flex items-center gap-2">
<Edit3 className="w-4 h-4" />
Edit Profile
</Button>
<Button
variant="secondary"
onClick={() => setFriendRequestSent(true)}
disabled={friendRequestSent}
className="w-full flex items-center gap-2"
>
<UserPlus className="w-4 h-4" />
{friendRequestSent ? 'Request Sent' : 'Add Friend'}
</Button>
<Link href=routes.protected.profileLeagues>
<Button variant="secondary" className="w-full flex items-center gap-2">
<Flag className="w-4 h-4" />
My Leagues
</Button>
</Link>
</div>
</div>
{viewData.extendedProfile && viewData.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>
{viewData.extendedProfile.socialHandles.map((social) => {
const Icon = getSocialIcon(social.platformLabel);
return (
<a
key={`${social.platformLabel}-${social.handle}`}
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 hover:text-white"
>
<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>
{viewData.driver.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">{viewData.driver.bio}</p>
</Card>
)}
{viewData.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">({viewData.teamMemberships.length})</span>
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{viewData.teamMemberships.map((membership) => (
<Link
key={membership.teamId}
href={membership.href}
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.roleLabel}</span>
<span>Since {membership.joinedAtLabel}</span>
</div>
</div>
<ChevronRight className="w-4 h-4 text-gray-500 group-hover:text-purple-400 transition-colors" />
</Link>
))}
</div>
</Card>
)}
{/* Tabs */}
<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>
{activeTab === 'history' && (
<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={viewData.driver.id} />
</Card>
)}
{activeTab === 'stats' && viewData.stats && (
<div className="space-y-6">
{viewData.driver.bio && (
<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-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">{viewData.stats.totalRacesLabel}</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">{viewData.stats.winsLabel}</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">{viewData.stats.podiumsLabel}</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">{viewData.stats.consistencyLabel}</div>
<div className="text-xs text-gray-500 uppercase tracking-wider">Consistency</div>
</div>
</div>
<Stack gap={3}>
<Heading level={2} icon={<Icon icon={User} size={5} color="#3b82f6" />}>
About
</Heading>
<Text color="text-gray-300" block>{viewData.driver.bio}</Text>
</Stack>
</Card>
</div>
)}
)}
{activeTab === 'overview' && viewData.extendedProfile && (
<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">{viewData.extendedProfile.achievements.length} earned</span>
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{viewData.extendedProfile.achievements.map((achievement) => {
const Icon = getAchievementIcon(achievement.icon);
return (
<div key={achievement.id} className="p-4 rounded-xl border border-charcoal-outline bg-iron-gray/30">
<div className="flex items-start gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-iron-gray/50 border border-charcoal-outline">
<Icon className="w-5 h-5 text-yellow-400" />
</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">{achievement.earnedAtLabel}</p>
</div>
</div>
</div>
);
})}
</div>
</Card>
)}
</div>
{viewData.teamMemberships.length > 0 && (
<TeamMembershipGrid
memberships={viewData.teamMemberships.map(m => ({
team: { id: m.teamId, name: m.teamName },
role: m.roleLabel,
joinedAt: new Date() // Placeholder
}))}
/>
)}
<ProfileTabs activeTab={activeTab as any} onTabChange={onTabChange as any} />
{activeTab === 'history' && (
<Card>
<Stack gap={4}>
<Heading level={2} icon={<Icon icon={History} size={5} color="#f87171" />}>
Race History
</Heading>
<ProfileRaceHistory driverId={viewData.driver.id} />
</Stack>
</Card>
)}
{activeTab === 'stats' && viewData.stats && (
<Card>
<Stack gap={6}>
<Heading level={2} icon={<Icon icon={Activity} size={5} color="#00f2ff" />}>
Performance Overview
</Heading>
<ProfileStatGrid
stats={[
{ label: 'Races', value: viewData.stats.totalRacesLabel },
{ label: 'Wins', value: viewData.stats.winsLabel, color: '#10b981' },
{ label: 'Podiums', value: viewData.stats.podiumsLabel, color: '#f59e0b' },
{ label: 'Consistency', value: viewData.stats.consistencyLabel, color: '#3b82f6' },
]}
/>
</Stack>
</Card>
)}
{activeTab === 'overview' && viewData.extendedProfile && (
<Card>
<Stack gap={4}>
<Stack direction="row" align="center" justify="between">
<Heading level={2} icon={<Icon icon={Award} size={5} color="#facc15" />}>
Achievements
</Heading>
<Text size="sm" color="text-gray-400" weight="normal">{viewData.extendedProfile.achievements.length} earned</Text>
</Stack>
<AchievementGrid
achievements={viewData.extendedProfile.achievements.map(a => ({
...a,
rarity: a.rarityLabel,
earnedAt: new Date() // Placeholder
}))}
/>
</Stack>
</Card>
)}
</Stack>
</Container>
);
}