auth rework
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import { redirect } from 'next/navigation';
|
'use client';
|
||||||
|
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import {
|
import {
|
||||||
@@ -25,12 +26,127 @@ import {
|
|||||||
|
|
||||||
import Card from '@/components/ui/Card';
|
import Card from '@/components/ui/Card';
|
||||||
import Button from '@/components/ui/Button';
|
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
|
// Helper functions
|
||||||
function getCountryFlag(countryCode: string): string {
|
function getCountryFlag(countryCode: string): string {
|
||||||
@@ -81,58 +197,29 @@ function getGreeting(): string {
|
|||||||
return 'Good evening';
|
return 'Good evening';
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function DashboardPage() {
|
interface FeedItem {
|
||||||
const authService = getAuthService();
|
id: string;
|
||||||
const session = await authService.getCurrentSession();
|
type: string;
|
||||||
|
headline: string;
|
||||||
|
body: string | null;
|
||||||
|
timestamp: Date;
|
||||||
|
ctaHref?: string;
|
||||||
|
ctaLabel?: string;
|
||||||
|
}
|
||||||
|
|
||||||
if (!session) {
|
export default function DashboardPage() {
|
||||||
redirect('/auth/iracing?returnTo=/dashboard');
|
// 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 { totalRaces, wins, podiums, rating, globalRank, consistency } = currentDriver;
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="min-h-screen bg-deep-graphite">
|
<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">
|
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-8">
|
||||||
{/* Welcome Message */}
|
{/* Welcome Message */}
|
||||||
<div className="flex items-start gap-5">
|
<div className="flex items-start gap-5">
|
||||||
{currentDriver && (
|
<div className="relative">
|
||||||
<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-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">
|
||||||
<div className="w-full h-full rounded-xl overflow-hidden bg-iron-gray">
|
<Image
|
||||||
<Image
|
src={currentDriver.avatarUrl}
|
||||||
src={currentDriver.avatarUrl}
|
alt={currentDriver.name}
|
||||||
alt={currentDriver.name}
|
width={80}
|
||||||
width={80}
|
height={80}
|
||||||
height={80}
|
className="w-full h-full object-cover"
|
||||||
className="w-full h-full object-cover"
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</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>
|
||||||
)}
|
<div className="absolute -bottom-1 -right-1 w-5 h-5 rounded-full bg-performance-green border-3 border-deep-graphite" />
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-gray-400 text-sm mb-1">{getGreeting()},</p>
|
<p className="text-gray-400 text-sm mb-1">{getGreeting()},</p>
|
||||||
<h1 className="text-3xl md:text-4xl font-bold text-white mb-2">
|
<h1 className="text-3xl md:text-4xl font-bold text-white mb-2">
|
||||||
{currentDriver?.name ?? 'Racer'}
|
{currentDriver.name}
|
||||||
<span className="ml-3 text-2xl">{currentDriver ? getCountryFlag(currentDriver.country) : '🏁'}</span>
|
<span className="ml-3 text-2xl">{getCountryFlag(currentDriver.country)}</span>
|
||||||
</h1>
|
</h1>
|
||||||
<div className="flex flex-wrap items-center gap-3">
|
<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">
|
<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>
|
<Card>
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<h2 className="text-lg font-semibold text-white flex items-center gap-2">
|
<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
|
Recent Activity
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
@@ -403,9 +488,9 @@ export default async function DashboardPage() {
|
|||||||
View all
|
View all
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
{upcomingRacesForDisplay.length > 0 ? (
|
{upcomingRaces.length > 0 ? (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{upcomingRacesForDisplay.slice(0, 5).map((race) => {
|
{upcomingRaces.slice(0, 5).map((race) => {
|
||||||
const isMyRace = race.isMyLeague;
|
const isMyRace = race.isMyLeague;
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
@@ -496,7 +581,7 @@ export default async function DashboardPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Feed Item Row Component
|
// Feed Item Row Component
|
||||||
function FeedItemRow({ item }: { item: DashboardFeedItemSummaryViewModel }) {
|
function FeedItemRow({ item }: { item: FeedItem }) {
|
||||||
const getActivityIcon = (type: string) => {
|
const getActivityIcon = (type: string) => {
|
||||||
if (type.includes('win')) return { icon: Trophy, color: 'text-yellow-400 bg-yellow-400/10' };
|
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('podium')) return { icon: Medal, color: 'text-warning-amber bg-warning-amber/10' };
|
||||||
|
|||||||
@@ -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 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() {
|
export default function OnboardingPage() {
|
||||||
const authService = getAuthService();
|
const router = useRouter();
|
||||||
const session = await authService.getCurrentSession();
|
const [checking, setChecking] = useState(true);
|
||||||
|
|
||||||
if (!session) {
|
useEffect(() => {
|
||||||
redirect('/auth/iracing?returnTo=/onboarding');
|
// 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) {
|
return () => clearTimeout(timer);
|
||||||
redirect('/dashboard');
|
}, [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 (
|
return (
|
||||||
|
|||||||
Reference in New Issue
Block a user