auth rework

This commit is contained in:
2025-12-17 15:42:53 +01:00
parent 75eaa1aa9f
commit bab55955e1
2 changed files with 203 additions and 87 deletions

View File

@@ -1,4 +1,5 @@
import { redirect } from 'next/navigation';
'use client';
import Image from 'next/image';
import Link from 'next/link';
import {
@@ -25,12 +26,127 @@ import {
import Card from '@/components/ui/Card';
import Button from '@/components/ui/Button';
import type {
DashboardOverviewViewModel,
DashboardFeedItemSummaryViewModel,
} from '@core/racing/application/presenters/IDashboardOverviewPresenter';
export const dynamic = 'force-dynamic';
// 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 {
@@ -81,58 +197,29 @@ function getGreeting(): string {
return 'Good evening';
}
export default async function DashboardPage() {
const authService = getAuthService();
const session = await authService.getCurrentSession();
interface FeedItem {
id: string;
type: string;
headline: string;
body: string | null;
timestamp: Date;
ctaHref?: string;
ctaLabel?: string;
}
if (!session) {
redirect('/auth/iracing?returnTo=/dashboard');
}
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 currentDriverId = session.user.primaryDriverId ?? '';
const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/races/dashboard/overview?driverId=${currentDriverId}`);
if (!response.ok) {
throw new Error('Failed to fetch dashboard overview');
}
const viewModel: DashboardOverviewViewModel = await response.json();
if (!viewModel) {
return null;
}
const {
currentDriver,
myUpcomingRaces,
otherUpcomingRaces,
nextRace: nextRaceSummary,
recentResults,
leagueStandingsSummaries,
feedSummary,
friends,
upcomingRaces,
activeLeaguesCount,
} = viewModel;
const nextRace =
nextRaceSummary != null
? {
...nextRaceSummary,
scheduledAt: new Date(nextRaceSummary.scheduledAt),
}
: null;
const upcomingRacesForDisplay = upcomingRaces.map(race => ({
...race,
scheduledAt: new Date(race.scheduledAt),
}));
const totalRaces = currentDriver?.totalRaces ?? 0;
const wins = currentDriver?.wins ?? 0;
const podiums = currentDriver?.podiums ?? 0;
const rating = currentDriver?.rating ?? 1500;
const globalRank = currentDriver?.globalRank ?? 0;
const consistency = currentDriver?.consistency ?? 0;
const { totalRaces, wins, podiums, rating, globalRank, consistency } = currentDriver;
return (
<main className="min-h-screen bg-deep-graphite">
@@ -150,27 +237,25 @@ export default async function DashboardPage() {
<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">
{currentDriver && (
<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 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 className="absolute -bottom-1 -right-1 w-5 h-5 rounded-full bg-performance-green border-3 border-deep-graphite" />
</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 ?? 'Racer'}
<span className="ml-3 text-2xl">{currentDriver ? getCountryFlag(currentDriver.country) : '🏁'}</span>
{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">
@@ -370,7 +455,7 @@ export default async function DashboardPage() {
<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-neon-aqua" />
<Activity className="w-5 h-5 text-cyan-400" />
Recent Activity
</h2>
</div>
@@ -403,9 +488,9 @@ export default async function DashboardPage() {
View all
</Link>
</div>
{upcomingRacesForDisplay.length > 0 ? (
{upcomingRaces.length > 0 ? (
<div className="space-y-3">
{upcomingRacesForDisplay.slice(0, 5).map((race) => {
{upcomingRaces.slice(0, 5).map((race) => {
const isMyRace = race.isMyLeague;
return (
<Link
@@ -496,7 +581,7 @@ export default async function DashboardPage() {
}
// Feed Item Row Component
function FeedItemRow({ item }: { item: DashboardFeedItemSummaryViewModel }) {
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' };

View File

@@ -1,20 +1,51 @@
import { redirect } from 'next/navigation';
'use client';
import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import OnboardingWizard from '@/components/onboarding/OnboardingWizard';
import { Loader2 } from 'lucide-react';
export const dynamic = 'force-dynamic';
// TODO: Re-enable API integration once backend is ready
// import { redirect } from 'next/navigation';
export default async function OnboardingPage() {
const authService = getAuthService();
const session = await authService.getCurrentSession();
export default function OnboardingPage() {
const router = useRouter();
const [checking, setChecking] = useState(true);
if (!session) {
redirect('/auth/iracing?returnTo=/onboarding');
}
useEffect(() => {
// TODO: Re-enable auth check once backend is ready
// For now, just show onboarding after a brief check
const checkDemoMode = () => {
// Check if user has demo mode cookie
const cookies = document.cookie.split(';');
const demoModeCookie = cookies.find(c => c.trim().startsWith('gridpilot_demo_mode='));
if (!demoModeCookie) {
// Not logged in, redirect to auth
router.push('/auth/login?returnTo=/onboarding');
return;
}
// For demo, skip onboarding and go to dashboard
// In production, this would check if onboarding is complete
router.push('/dashboard');
};
const primaryDriverId = session.user.primaryDriverId ?? '';
// Brief delay to prevent flash
const timer = setTimeout(() => {
checkDemoMode();
}, 500);
if (primaryDriverId) {
redirect('/dashboard');
return () => clearTimeout(timer);
}, [router]);
// Show loading while checking
if (checking) {
return (
<main className="min-h-screen bg-deep-graphite flex items-center justify-center">
<Loader2 className="w-8 h-8 text-primary-blue animate-spin" />
</main>
);
}
return (