page wrapper
This commit is contained in:
@@ -1,339 +1,17 @@
|
||||
'use client';
|
||||
import { notFound } from 'next/navigation';
|
||||
import { PageWrapper } from '@/components/shared/state/PageWrapper';
|
||||
import { DashboardTemplate } from '@/templates/DashboardTemplate';
|
||||
import { PageDataFetcher } from '@/lib/page/PageDataFetcher';
|
||||
import { DASHBOARD_SERVICE_TOKEN } from '@/lib/di/tokens';
|
||||
import type { DashboardService } from '@/lib/services/dashboard/DashboardService';
|
||||
|
||||
import {
|
||||
Activity,
|
||||
Award,
|
||||
Calendar,
|
||||
ChevronRight,
|
||||
Clock,
|
||||
Flag,
|
||||
Medal,
|
||||
Play,
|
||||
Star,
|
||||
Target,
|
||||
Trophy,
|
||||
UserPlus,
|
||||
Users,
|
||||
} from 'lucide-react';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
|
||||
import { FeedItemRow } from '@/components/dashboard/FeedItemRow';
|
||||
import { FriendItem } from '@/components/dashboard/FriendItem';
|
||||
import { LeagueStandingItem } from '@/components/dashboard/LeagueStandingItem';
|
||||
import { StatCard } from '@/components/dashboard/StatCard';
|
||||
import { UpcomingRaceItem } from '@/components/dashboard/UpcomingRaceItem';
|
||||
import Button from '@/components/ui/Button';
|
||||
import Card from '@/components/ui/Card';
|
||||
|
||||
import { getCountryFlag } from '@/lib/utilities/country';
|
||||
import { getGreeting, timeUntil } from '@/lib/utilities/time';
|
||||
|
||||
import { StateContainer } from '@/components/shared/state/StateContainer';
|
||||
import { useDashboardOverview } from '@/hooks/dashboard/useDashboardOverview';
|
||||
import { DashboardOverviewViewModel } from '@/lib/view-models/DashboardOverviewViewModel';
|
||||
|
||||
export default function DashboardPage() {
|
||||
const { data: dashboardData, isLoading, error, retry } = useDashboardOverview();
|
||||
|
||||
return (
|
||||
<StateContainer
|
||||
data={dashboardData}
|
||||
isLoading={isLoading}
|
||||
error={error}
|
||||
retry={retry}
|
||||
config={{
|
||||
loading: { variant: 'full-screen', message: 'Loading dashboard...' },
|
||||
error: { variant: 'full-screen' },
|
||||
empty: {
|
||||
icon: Activity,
|
||||
title: 'No dashboard data',
|
||||
description: 'Try refreshing the page',
|
||||
action: { label: 'Refresh', onClick: retry }
|
||||
}
|
||||
}}
|
||||
>
|
||||
{(data: DashboardOverviewViewModel) => {
|
||||
// StateContainer ensures data is non-null when this renders
|
||||
const dashboardData = data!;
|
||||
const currentDriver = dashboardData.currentDriver;
|
||||
const nextRace = dashboardData.nextRace;
|
||||
const upcomingRaces = dashboardData.upcomingRaces;
|
||||
const leagueStandingsSummaries = dashboardData.leagueStandings;
|
||||
const feedSummary = { items: dashboardData.feedItems };
|
||||
const friends = dashboardData.friends;
|
||||
const activeLeaguesCount = dashboardData.activeLeaguesCount;
|
||||
|
||||
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">
|
||||
<StatCard icon={Trophy} value={wins} label="Wins" color="bg-performance-green/20 text-performance-green" />
|
||||
<StatCard icon={Medal} value={podiums} label="Podiums" color="bg-warning-amber/20 text-warning-amber" />
|
||||
<StatCard icon={Target} value={`${consistency}%`} label="Consistency" color="bg-primary-blue/20 text-primary-blue" />
|
||||
<StatCard icon={Users} value={activeLeaguesCount} label="Active Leagues" color="bg-purple-500/20 text-purple-400" />
|
||||
</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((summary: any) => (
|
||||
<LeagueStandingItem
|
||||
key={summary.leagueId}
|
||||
leagueId={summary.leagueId}
|
||||
leagueName={summary.leagueName}
|
||||
position={summary.position}
|
||||
points={summary.points}
|
||||
totalDrivers={summary.totalDrivers}
|
||||
/>
|
||||
))}
|
||||
</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: any) => (
|
||||
<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: any) => (
|
||||
<UpcomingRaceItem
|
||||
key={race.id}
|
||||
id={race.id}
|
||||
track={race.track}
|
||||
car={race.car}
|
||||
scheduledAt={race.scheduledAt}
|
||||
isMyLeague={race.isMyLeague}
|
||||
/>
|
||||
))}
|
||||
</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: any) => (
|
||||
<FriendItem
|
||||
key={friend.id}
|
||||
id={friend.id}
|
||||
name={friend.name}
|
||||
avatarUrl={friend.avatarUrl}
|
||||
country={friend.country}
|
||||
/>
|
||||
))}
|
||||
{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>
|
||||
);
|
||||
}}
|
||||
</StateContainer>
|
||||
export default async function DashboardPage() {
|
||||
const data = await PageDataFetcher.fetch<DashboardService, 'getDashboardOverview'>(
|
||||
DASHBOARD_SERVICE_TOKEN,
|
||||
'getDashboardOverview'
|
||||
);
|
||||
|
||||
if (!data) notFound();
|
||||
|
||||
return <PageWrapper data={data} Template={DashboardTemplate} />;
|
||||
}
|
||||
Reference in New Issue
Block a user