Files
gridpilot.gg/apps/website/app/dashboard/page.tsx
2025-12-17 15:42:53 +01:00

618 lines
25 KiB
TypeScript

'use client';
import Image from 'next/image';
import Link from 'next/link';
import {
Calendar,
Trophy,
Users,
Star,
Clock,
Flag,
TrendingUp,
ChevronRight,
Zap,
Target,
Award,
Activity,
Play,
Bell,
Medal,
Crown,
Heart,
MessageCircle,
UserPlus,
} from 'lucide-react';
import Card from '@/components/ui/Card';
import Button from '@/components/ui/Button';
// TODO: Re-enable API integration once backend is ready
// import type {
// DashboardOverviewViewModel,
// DashboardFeedItemSummaryViewModel,
// } from '@core/racing/application/presenters/IDashboardOverviewPresenter';
// Mock data for prototype
const MOCK_CURRENT_DRIVER = {
id: 'driver-1',
name: 'Max Verstappen',
avatarUrl: 'https://api.dicebear.com/7.x/avataaars/svg?seed=MaxV',
country: 'NL',
totalRaces: 142,
wins: 28,
podiums: 67,
rating: 2847,
globalRank: 15,
consistency: 94,
};
const MOCK_NEXT_RACE = {
id: 'race-1',
track: 'Spa-Francorchamps',
car: 'Porsche 911 GT3 R',
scheduledAt: new Date(Date.now() + 2 * 24 * 60 * 60 * 1000), // 2 days from now
isMyLeague: true,
leagueName: 'GT3 Masters Series',
};
const MOCK_UPCOMING_RACES = [
{
id: 'race-1',
track: 'Spa-Francorchamps',
car: 'Porsche 911 GT3 R',
scheduledAt: new Date(Date.now() + 2 * 24 * 60 * 60 * 1000),
isMyLeague: true,
},
{
id: 'race-2',
track: 'Nürburgring GP',
car: 'BMW M4 GT3',
scheduledAt: new Date(Date.now() + 5 * 24 * 60 * 60 * 1000),
isMyLeague: true,
},
{
id: 'race-3',
track: 'Monza',
car: 'Ferrari 296 GT3',
scheduledAt: new Date(Date.now() + 8 * 24 * 60 * 60 * 1000),
isMyLeague: false,
},
{
id: 'race-4',
track: 'Silverstone',
car: 'Aston Martin Vantage GT3',
scheduledAt: new Date(Date.now() + 12 * 24 * 60 * 60 * 1000),
isMyLeague: true,
},
];
const MOCK_LEAGUE_STANDINGS = [
{
leagueId: 'league-1',
leagueName: 'GT3 Masters Series',
position: 2,
points: 186,
totalDrivers: 24,
},
{
leagueId: 'league-2',
leagueName: 'Endurance Pro League',
position: 5,
points: 142,
totalDrivers: 32,
},
{
leagueId: 'league-3',
leagueName: 'F1 Weekend Warriors',
position: 1,
points: 225,
totalDrivers: 18,
},
];
const MOCK_FEED_ITEMS = [
{
id: 'feed-1',
type: 'win',
headline: 'You won the race at Spa-Francorchamps!',
body: 'Great driving! You finished P1 with a 3.2s gap to second place.',
timestamp: new Date(Date.now() - 2 * 60 * 60 * 1000),
ctaHref: '/races/race-prev-1',
ctaLabel: 'View Results',
},
{
id: 'feed-2',
type: 'friend_join',
headline: 'Lewis Hamilton joined GT3 Masters Series',
body: null,
timestamp: new Date(Date.now() - 8 * 60 * 60 * 1000),
ctaHref: '/leagues/league-1',
ctaLabel: 'View League',
},
{
id: 'feed-3',
type: 'podium',
headline: 'Charles Leclerc finished P2 at Monza',
body: 'Your friend had a great race!',
timestamp: new Date(Date.now() - 24 * 60 * 60 * 1000),
ctaHref: '/drivers/driver-2',
ctaLabel: 'View Profile',
},
];
const MOCK_FRIENDS = [
{ id: 'friend-1', name: 'Lewis Hamilton', avatarUrl: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Lewis', country: 'GB' },
{ id: 'friend-2', name: 'Charles Leclerc', avatarUrl: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Charles', country: 'MC' },
{ id: 'friend-3', name: 'Lando Norris', avatarUrl: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Lando', country: 'GB' },
{ id: 'friend-4', name: 'Oscar Piastri', avatarUrl: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Oscar', country: 'AU' },
];
// Helper functions
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 timeUntil(date: Date): string {
const now = new Date();
const diffMs = date.getTime() - now.getTime();
if (diffMs < 0) return 'Started';
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
const diffDays = Math.floor(diffHours / 24);
if (diffDays > 0) {
return `${diffDays}d ${diffHours % 24}h`;
}
if (diffHours > 0) {
const diffMinutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
return `${diffHours}h ${diffMinutes}m`;
}
const diffMinutes = Math.floor(diffMs / (1000 * 60));
return `${diffMinutes}m`;
}
function timeAgo(timestamp: Date | string): string {
const time = typeof timestamp === 'string' ? new Date(timestamp) : timestamp;
const diffMs = Date.now() - time.getTime();
const diffMinutes = Math.floor(diffMs / 60000);
if (diffMinutes < 1) return 'Just now';
if (diffMinutes < 60) return `${diffMinutes}m ago`;
const diffHours = Math.floor(diffMinutes / 60);
if (diffHours < 24) return `${diffHours}h ago`;
const diffDays = Math.floor(diffHours / 24);
return `${diffDays}d ago`;
}
function getGreeting(): string {
const hour = new Date().getHours();
if (hour < 12) return 'Good morning';
if (hour < 18) return 'Good afternoon';
return 'Good evening';
}
interface FeedItem {
id: string;
type: string;
headline: string;
body: string | null;
timestamp: Date;
ctaHref?: string;
ctaLabel?: string;
}
export default function DashboardPage() {
// TODO: Re-enable API integration once backend is ready
// Currently using mock data for prototype
const currentDriver = MOCK_CURRENT_DRIVER;
const nextRace = MOCK_NEXT_RACE;
const upcomingRaces = MOCK_UPCOMING_RACES;
const leagueStandingsSummaries = MOCK_LEAGUE_STANDINGS;
const feedSummary = { items: MOCK_FEED_ITEMS };
const friends = MOCK_FRIENDS;
const activeLeaguesCount = 3;
const { totalRaces, wins, podiums, rating, globalRank, consistency } = currentDriver;
return (
<main className="min-h-screen bg-deep-graphite">
{/* Hero Section */}
<section className="relative overflow-hidden">
{/* Background Pattern */}
<div className="absolute inset-0 bg-gradient-to-br from-primary-blue/10 via-deep-graphite to-purple-600/5" />
<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 max-w-7xl mx-auto px-6 py-10">
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-8">
{/* Welcome Message */}
<div className="flex items-start gap-5">
<div className="relative">
<div className="w-20 h-20 rounded-2xl bg-gradient-to-br from-primary-blue to-purple-600 p-0.5 shadow-xl shadow-primary-blue/20">
<div className="w-full h-full rounded-xl overflow-hidden bg-iron-gray">
<Image
src={currentDriver.avatarUrl}
alt={currentDriver.name}
width={80}
height={80}
className="w-full h-full object-cover"
/>
</div>
</div>
<div className="absolute -bottom-1 -right-1 w-5 h-5 rounded-full bg-performance-green border-3 border-deep-graphite" />
</div>
<div>
<p className="text-gray-400 text-sm mb-1">{getGreeting()},</p>
<h1 className="text-3xl md:text-4xl font-bold text-white mb-2">
{currentDriver.name}
<span className="ml-3 text-2xl">{getCountryFlag(currentDriver.country)}</span>
</h1>
<div className="flex flex-wrap items-center gap-3">
<div className="flex items-center gap-1.5 px-3 py-1 rounded-full bg-primary-blue/10 border border-primary-blue/30">
<Star className="w-3.5 h-3.5 text-primary-blue" />
<span className="text-sm font-semibold text-primary-blue">{rating}</span>
</div>
<div className="flex items-center gap-1.5 px-3 py-1 rounded-full bg-yellow-400/10 border border-yellow-400/30">
<Trophy className="w-3.5 h-3.5 text-yellow-400" />
<span className="text-sm font-semibold text-yellow-400">#{globalRank}</span>
</div>
<span className="text-xs text-gray-500">{totalRaces} races completed</span>
</div>
</div>
</div>
{/* Quick Actions */}
<div className="flex flex-wrap gap-3">
<Link href="/leagues">
<Button variant="secondary" className="flex items-center gap-2">
<Flag className="w-4 h-4" />
Browse Leagues
</Button>
</Link>
<Link href="/profile">
<Button variant="primary" className="flex items-center gap-2">
<Activity className="w-4 h-4" />
View Profile
</Button>
</Link>
</div>
</div>
{/* Quick Stats Row */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mt-8">
<div className="p-4 rounded-xl bg-iron-gray/50 border border-charcoal-outline backdrop-blur-sm">
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-performance-green/20">
<Trophy className="w-5 h-5 text-performance-green" />
</div>
<div>
<p className="text-2xl font-bold text-white">{wins}</p>
<p className="text-xs text-gray-500">Wins</p>
</div>
</div>
</div>
<div className="p-4 rounded-xl bg-iron-gray/50 border border-charcoal-outline backdrop-blur-sm">
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-warning-amber/20">
<Medal className="w-5 h-5 text-warning-amber" />
</div>
<div>
<p className="text-2xl font-bold text-white">{podiums}</p>
<p className="text-xs text-gray-500">Podiums</p>
</div>
</div>
</div>
<div className="p-4 rounded-xl bg-iron-gray/50 border border-charcoal-outline backdrop-blur-sm">
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-primary-blue/20">
<Target className="w-5 h-5 text-primary-blue" />
</div>
<div>
<p className="text-2xl font-bold text-white">{consistency}%</p>
<p className="text-xs text-gray-500">Consistency</p>
</div>
</div>
</div>
<div className="p-4 rounded-xl bg-iron-gray/50 border border-charcoal-outline backdrop-blur-sm">
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-purple-500/20">
<Users className="w-5 h-5 text-purple-400" />
</div>
<div>
<p className="text-2xl font-bold text-white">{activeLeaguesCount}</p>
<p className="text-xs text-gray-500">Active Leagues</p>
</div>
</div>
</div>
</div>
</div>
</section>
{/* Main Content */}
<section className="max-w-7xl mx-auto px-6 py-8">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Left Column - Main Content */}
<div className="lg:col-span-2 space-y-6">
{/* Next Race Card */}
{nextRace && (
<Card className="relative overflow-hidden bg-gradient-to-br from-iron-gray to-iron-gray/80 border-primary-blue/30">
<div className="absolute top-0 right-0 w-40 h-40 bg-gradient-to-bl from-primary-blue/20 to-transparent rounded-bl-full" />
<div className="relative">
<div className="flex items-center gap-2 mb-4">
<div className="flex items-center gap-2 px-3 py-1 rounded-full bg-primary-blue/20 border border-primary-blue/30">
<Play className="w-3.5 h-3.5 text-primary-blue" />
<span className="text-xs font-semibold text-primary-blue uppercase tracking-wider">Next Race</span>
</div>
{nextRace.isMyLeague && (
<span className="px-2 py-0.5 rounded-full bg-performance-green/20 text-performance-green text-xs font-medium">
Your League
</span>
)}
</div>
<div className="flex flex-col md:flex-row md:items-end md:justify-between gap-4">
<div>
<h2 className="text-2xl font-bold text-white mb-2">{nextRace.track}</h2>
<p className="text-gray-400 mb-3">{nextRace.car}</p>
<div className="flex flex-wrap items-center gap-4 text-sm">
<span className="flex items-center gap-1.5 text-gray-400">
<Calendar className="w-4 h-4" />
{nextRace.scheduledAt.toLocaleDateString('en-US', {
weekday: 'long',
month: 'short',
day: 'numeric',
})}
</span>
<span className="flex items-center gap-1.5 text-gray-400">
<Clock className="w-4 h-4" />
{nextRace.scheduledAt.toLocaleTimeString('en-US', {
hour: '2-digit',
minute: '2-digit',
})}
</span>
</div>
</div>
<div className="flex flex-col items-end gap-3">
<div className="text-right">
<p className="text-xs text-gray-500 uppercase tracking-wider mb-1">Starts in</p>
<p className="text-3xl font-bold text-primary-blue font-mono">{timeUntil(nextRace.scheduledAt)}</p>
</div>
<Link href={`/races/${nextRace.id}`}>
<Button variant="primary" className="flex items-center gap-2">
View Details
<ChevronRight className="w-4 h-4" />
</Button>
</Link>
</div>
</div>
</div>
</Card>
)}
{/* League Standings Preview */}
{leagueStandingsSummaries.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">
<Award className="w-5 h-5 text-yellow-400" />
Your Championship Standings
</h2>
<Link href="/profile/leagues" className="text-sm text-primary-blue hover:underline flex items-center gap-1">
View all <ChevronRight className="w-4 h-4" />
</Link>
</div>
<div className="space-y-3">
{leagueStandingsSummaries.map(({ leagueId, leagueName, position, points, totalDrivers }) => (
<Link
key={leagueId}
href={`/leagues/${leagueId}/standings`}
className="flex items-center gap-4 p-4 rounded-xl bg-deep-graphite border border-charcoal-outline hover:border-primary-blue/30 transition-colors group"
>
<div className={`flex h-12 w-12 items-center justify-center rounded-xl font-bold text-xl ${
position === 1 ? 'bg-yellow-400/20 text-yellow-400' :
position === 2 ? 'bg-gray-300/20 text-gray-300' :
position === 3 ? 'bg-orange-400/20 text-orange-400' :
'bg-iron-gray text-gray-400'
}`}>
{position > 0 ? `P${position}` : '-'}
</div>
<div className="flex-1 min-w-0">
<p className="text-white font-semibold truncate group-hover:text-primary-blue transition-colors">
{leagueName}
</p>
<p className="text-sm text-gray-500">
{points} points {totalDrivers} drivers
</p>
</div>
<div className="flex items-center gap-2">
{position <= 3 && position > 0 && (
<Crown className={`w-5 h-5 ${
position === 1 ? 'text-yellow-400' :
position === 2 ? 'text-gray-300' :
'text-orange-400'
}`} />
)}
<ChevronRight className="w-5 h-5 text-gray-500 group-hover:text-primary-blue transition-colors" />
</div>
</Link>
))}
</div>
</Card>
)}
{/* Activity Feed */}
<Card>
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold text-white flex items-center gap-2">
<Activity className="w-5 h-5 text-cyan-400" />
Recent Activity
</h2>
</div>
{feedSummary.items.length > 0 ? (
<div className="space-y-4">
{feedSummary.items.slice(0, 5).map((item) => (
<FeedItemRow key={item.id} item={item} />
))}
</div>
) : (
<div className="text-center py-8">
<Activity className="w-12 h-12 text-gray-600 mx-auto mb-3" />
<p className="text-gray-400 mb-2">No activity yet</p>
<p className="text-sm text-gray-500">Join leagues and add friends to see activity here</p>
</div>
)}
</Card>
</div>
{/* Right Column - Sidebar */}
<div className="space-y-6">
{/* Upcoming Races */}
<Card>
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-white flex items-center gap-2">
<Calendar className="w-5 h-5 text-red-400" />
Upcoming Races
</h3>
<Link href="/races" className="text-xs text-primary-blue hover:underline">
View all
</Link>
</div>
{upcomingRaces.length > 0 ? (
<div className="space-y-3">
{upcomingRaces.slice(0, 5).map((race) => {
const isMyRace = race.isMyLeague;
return (
<Link
key={race.id}
href={`/races/${race.id}`}
className="block p-3 rounded-lg bg-deep-graphite border border-charcoal-outline hover:border-primary-blue/30 transition-colors"
>
<div className="flex items-start justify-between gap-2 mb-2">
<p className="text-white font-medium text-sm truncate">{race.track}</p>
{isMyRace && (
<span className="flex-shrink-0 w-2 h-2 rounded-full bg-performance-green" title="Your league" />
)}
</div>
<p className="text-xs text-gray-500 truncate mb-2">{race.car}</p>
<div className="flex items-center justify-between text-xs">
<span className="text-gray-400">
{race.scheduledAt.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}
</span>
<span className="text-primary-blue font-medium">{timeUntil(race.scheduledAt)}</span>
</div>
</Link>
);
})}
</div>
) : (
<p className="text-gray-500 text-sm text-center py-4">No upcoming races</p>
)}
</Card>
{/* Friends */}
<Card>
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-white flex items-center gap-2">
<Users className="w-5 h-5 text-purple-400" />
Friends
</h3>
<span className="text-xs text-gray-500">{friends.length} friends</span>
</div>
{friends.length > 0 ? (
<div className="space-y-2">
{friends.slice(0, 6).map((friend) => (
<Link
key={friend.id}
href={`/drivers/${friend.id}`}
className="flex items-center gap-3 p-2 rounded-lg hover:bg-deep-graphite transition-colors"
>
<div className="w-9 h-9 rounded-full overflow-hidden bg-gradient-to-br from-primary-blue to-purple-600">
<Image
src={friend.avatarUrl}
alt={friend.name}
width={36}
height={36}
className="w-full h-full object-cover"
/>
</div>
<div className="flex-1 min-w-0">
<p className="text-white text-sm font-medium truncate">{friend.name}</p>
<p className="text-xs text-gray-500">{getCountryFlag(friend.country)}</p>
</div>
</Link>
))}
{friends.length > 6 && (
<Link
href="/profile"
className="block text-center py-2 text-sm text-primary-blue hover:underline"
>
+{friends.length - 6} more
</Link>
)}
</div>
) : (
<div className="text-center py-6">
<UserPlus className="w-10 h-10 text-gray-600 mx-auto mb-2" />
<p className="text-sm text-gray-400 mb-2">No friends yet</p>
<Link href="/drivers">
<Button variant="secondary" className="text-xs">
Find Drivers
</Button>
</Link>
</div>
)}
</Card>
</div>
</div>
</section>
</main>
);
}
// Feed Item Row Component
function FeedItemRow({ item }: { item: FeedItem }) {
const getActivityIcon = (type: string) => {
if (type.includes('win')) return { icon: Trophy, color: 'text-yellow-400 bg-yellow-400/10' };
if (type.includes('podium')) return { icon: Medal, color: 'text-warning-amber bg-warning-amber/10' };
if (type.includes('join')) return { icon: UserPlus, color: 'text-performance-green bg-performance-green/10' };
if (type.includes('friend')) return { icon: Heart, color: 'text-pink-400 bg-pink-400/10' };
if (type.includes('league')) return { icon: Flag, color: 'text-primary-blue bg-primary-blue/10' };
if (type.includes('race')) return { icon: Play, color: 'text-red-400 bg-red-400/10' };
return { icon: Activity, color: 'text-gray-400 bg-gray-400/10' };
};
const { icon: Icon, color } = getActivityIcon(item.type);
return (
<div className="flex gap-3 p-3 rounded-lg bg-deep-graphite/50 border border-charcoal-outline">
<div className={`flex h-10 w-10 items-center justify-center rounded-lg ${color} flex-shrink-0`}>
<Icon className="w-5 h-5" />
</div>
<div className="flex-1 min-w-0">
<p className="text-sm text-white">{item.headline}</p>
{item.body && (
<p className="text-xs text-gray-500 mt-1 line-clamp-2">{item.body}</p>
)}
<p className="text-xs text-gray-500 mt-1">{timeAgo(item.timestamp)}</p>
</div>
{item.ctaHref && (
<Link href={item.ctaHref} className="flex-shrink-0">
<Button variant="secondary" className="text-xs px-3 py-1.5">
{item.ctaLabel || 'View'}
</Button>
</Link>
)}
</div>
);
}