wip
This commit is contained in:
@@ -9,9 +9,8 @@ export default function AlphaFooter() {
|
||||
<div className="flex flex-col md:flex-row items-center justify-between gap-4">
|
||||
<div className="flex items-center gap-2 text-sm text-gray-400">
|
||||
<span className="px-2 py-1 bg-warning-amber/10 text-warning-amber rounded border border-warning-amber/20 font-medium">
|
||||
Alpha v0.1
|
||||
Alpha
|
||||
</span>
|
||||
<span>In-memory prototype</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-6 text-sm">
|
||||
@@ -29,12 +28,6 @@ export default function AlphaFooter() {
|
||||
>
|
||||
Roadmap
|
||||
</a>
|
||||
<Link
|
||||
href="/"
|
||||
className="text-gray-400 hover:text-primary-blue transition-colors"
|
||||
>
|
||||
← Back to Landing
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,26 +1,38 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
const navLinks = [
|
||||
{ href: '/', label: 'Dashboard' },
|
||||
type AlphaNavProps = {
|
||||
isAuthenticated?: boolean;
|
||||
};
|
||||
|
||||
const nonHomeLinks = [
|
||||
{ href: '/profile', label: 'Profile' },
|
||||
{ href: '/leagues', label: 'Leagues' },
|
||||
{ href: '/teams', label: 'Teams' },
|
||||
{ href: '/drivers', label: 'Drivers' },
|
||||
{ href: '/social', label: 'Social' },
|
||||
] as const;
|
||||
|
||||
export function AlphaNav() {
|
||||
export function AlphaNav({ isAuthenticated }: AlphaNavProps) {
|
||||
const pathname = usePathname();
|
||||
|
||||
const navLinks = isAuthenticated
|
||||
? ([{ href: '/dashboard', label: 'Dashboard' } as const, ...nonHomeLinks] as const)
|
||||
: ([{ href: '/', label: 'Home' } as const, ...nonHomeLinks] as const);
|
||||
|
||||
const loginHref = '/auth/iracing/start?returnTo=/dashboard';
|
||||
|
||||
return (
|
||||
<nav className="sticky top-0 z-40 bg-deep-graphite/95 backdrop-blur-md border-b border-white/5">
|
||||
<div className="max-w-7xl mx-auto px-6">
|
||||
<div className="flex items-center justify-between h-14">
|
||||
<div className="flex items-baseline space-x-3">
|
||||
<Link href="/" className="text-xl font-semibold text-white hover:text-primary-blue transition-colors">
|
||||
<Link
|
||||
href="/"
|
||||
className="text-xl font-semibold text-white hover:text-primary-blue transition-colors"
|
||||
>
|
||||
GridPilot
|
||||
</Link>
|
||||
<span className="text-xs text-gray-500 font-light">ALPHA</span>
|
||||
@@ -35,9 +47,10 @@ export function AlphaNav() {
|
||||
href={link.href}
|
||||
className={`
|
||||
relative px-4 py-2 text-sm font-medium transition-all duration-200
|
||||
${isActive
|
||||
? 'text-primary-blue'
|
||||
: 'text-gray-400 hover:text-white'
|
||||
${
|
||||
isActive
|
||||
? 'text-primary-blue'
|
||||
: 'text-gray-400 hover:text-white'
|
||||
}
|
||||
`}
|
||||
>
|
||||
@@ -50,9 +63,29 @@ export function AlphaNav() {
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="hidden md:flex items-center space-x-3">
|
||||
{!isAuthenticated && (
|
||||
<Link
|
||||
href={loginHref}
|
||||
className="inline-flex items-center justify-center px-3 py-1.5 rounded-md bg-primary-blue text-xs font-medium text-white hover:bg-primary-blue/90 transition-colors"
|
||||
>
|
||||
Authenticate with iRacing
|
||||
</Link>
|
||||
)}
|
||||
{isAuthenticated && (
|
||||
<form action="/auth/logout" method="POST">
|
||||
<button
|
||||
type="submit"
|
||||
className="inline-flex items-center justify-center px-3 py-1.5 rounded-md border border-gray-600 text-xs font-medium text-gray-200 hover:bg-gray-800 transition-colors"
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="md:hidden w-8" />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
export interface BreadcrumbItem {
|
||||
label: string;
|
||||
href?: string;
|
||||
}
|
||||
|
||||
interface BreadcrumbsProps {
|
||||
items: BreadcrumbItem[];
|
||||
}
|
||||
|
||||
export default function Breadcrumbs({ items }: BreadcrumbsProps) {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<nav className="flex items-center gap-2 text-sm mb-6">
|
||||
{items.map((item, index) => {
|
||||
const isLast = index === items.length - 1;
|
||||
|
||||
return (
|
||||
<div key={index} className="flex items-center gap-2">
|
||||
{item.href && !isLast ? (
|
||||
<button
|
||||
onClick={() => router.push(item.href!)}
|
||||
className="text-gray-400 hover:text-primary-blue transition-colors"
|
||||
>
|
||||
{item.label}
|
||||
</button>
|
||||
) : (
|
||||
<span className={isLast ? 'text-white font-medium' : 'text-gray-400'}>
|
||||
{item.label}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{!isLast && (
|
||||
<svg
|
||||
className="w-4 h-4 text-gray-600"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M9 5l7 7-7 7"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
import { useState } from 'react';
|
||||
import Card from '../ui/Card';
|
||||
import Button from '../ui/Button';
|
||||
import { Race } from '@gridpilot/racing-domain/entities/Race';
|
||||
import { Race } from '@gridpilot/racing/domain/entities/Race';
|
||||
|
||||
interface CompanionInstructionsProps {
|
||||
race: Race;
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
interface DataWarningProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function DataWarning({ className }: DataWarningProps) {
|
||||
const [isDismissed, setIsDismissed] = useState(false);
|
||||
const [isMounted, setIsMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setIsMounted(true);
|
||||
const dismissed = sessionStorage.getItem('data-warning-dismissed');
|
||||
if (dismissed === 'true') {
|
||||
setIsDismissed(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleDismiss = () => {
|
||||
sessionStorage.setItem('data-warning-dismissed', 'true');
|
||||
setIsDismissed(true);
|
||||
};
|
||||
|
||||
if (!isMounted) return null;
|
||||
if (isDismissed) return null;
|
||||
|
||||
return (
|
||||
<div className={`${className ?? 'mb-6'} bg-iron-gray border border-charcoal-outline rounded-lg p-4`}>
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="w-10 h-10 rounded-lg bg-primary-blue/10 flex items-center justify-center flex-shrink-0">
|
||||
<svg className="w-5 h-5 text-primary-blue" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-gray-300">
|
||||
Your data will be lost when you refresh the page. Alpha uses in-memory storage only.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleDismiss}
|
||||
className="text-gray-400 hover:text-white transition-colors p-1 flex-shrink-0"
|
||||
aria-label="Dismiss warning"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import Card from '../ui/Card';
|
||||
|
||||
interface DriverCardProps {
|
||||
id: string;
|
||||
name: string;
|
||||
avatar?: string;
|
||||
rating: number;
|
||||
skillLevel: 'beginner' | 'intermediate' | 'advanced' | 'pro';
|
||||
nationality?: string;
|
||||
racesCompleted: number;
|
||||
wins: number;
|
||||
isActive?: boolean;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
export default function DriverCard({
|
||||
id,
|
||||
name,
|
||||
avatar,
|
||||
rating,
|
||||
skillLevel,
|
||||
nationality,
|
||||
racesCompleted,
|
||||
wins,
|
||||
isActive = true,
|
||||
onClick,
|
||||
}: DriverCardProps) {
|
||||
const skillBadgeColors = {
|
||||
beginner: 'bg-green-500/20 text-green-400',
|
||||
intermediate: 'bg-blue-500/20 text-blue-400',
|
||||
advanced: 'bg-purple-500/20 text-purple-400',
|
||||
pro: 'bg-red-500/20 text-red-400',
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="cursor-pointer hover:scale-[1.03] transition-transform duration-150"
|
||||
onClick={onClick}
|
||||
>
|
||||
<Card>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="relative">
|
||||
<div className="w-16 h-16 bg-charcoal-outline rounded-full flex items-center justify-center flex-shrink-0">
|
||||
{avatar ? (
|
||||
<img
|
||||
src={avatar}
|
||||
alt={name}
|
||||
className="w-full h-full object-cover rounded-full"
|
||||
/>
|
||||
) : (
|
||||
<span className="text-2xl font-bold text-gray-500">
|
||||
{name.charAt(0)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{isActive && (
|
||||
<div className="absolute bottom-0 right-0 w-4 h-4 bg-green-500 border-2 border-iron-gray rounded-full" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="text-lg font-semibold text-white truncate">
|
||||
{name}
|
||||
</h3>
|
||||
{nationality && (
|
||||
<p className="text-sm text-gray-400">{nationality}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex-1">
|
||||
<div className="text-2xl font-bold text-white">{rating}</div>
|
||||
<div className="text-xs text-gray-400">Rating</div>
|
||||
</div>
|
||||
<div className="flex-1 text-center">
|
||||
<div className="text-2xl font-bold text-white">{wins}</div>
|
||||
<div className="text-xs text-gray-400">Wins</div>
|
||||
</div>
|
||||
<div className="flex-1 text-right">
|
||||
<div className="text-2xl font-bold text-white">{racesCompleted}</div>
|
||||
<div className="text-xs text-gray-400">Races</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span
|
||||
className={`inline-block px-3 py-1 rounded-full text-xs font-medium ${
|
||||
skillBadgeColors[skillLevel]
|
||||
}`}
|
||||
>
|
||||
{skillLevel.charAt(0).toUpperCase() + skillLevel.slice(1)}
|
||||
</span>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import Card from '../ui/Card';
|
||||
import RankBadge from './RankBadge';
|
||||
|
||||
interface RankingData {
|
||||
type: 'overall' | 'league' | 'class';
|
||||
name: string;
|
||||
rank: number;
|
||||
totalDrivers: number;
|
||||
percentile: number;
|
||||
rating: number;
|
||||
}
|
||||
|
||||
interface DriverRankingsProps {
|
||||
rankings: RankingData[];
|
||||
}
|
||||
|
||||
export default function DriverRankings({ rankings }: DriverRankingsProps) {
|
||||
const getPercentileColor = (percentile: number) => {
|
||||
if (percentile >= 90) return 'text-green-400';
|
||||
if (percentile >= 75) return 'text-primary-blue';
|
||||
if (percentile >= 50) return 'text-warning-amber';
|
||||
return 'text-gray-400';
|
||||
};
|
||||
|
||||
const getPercentileLabel = (percentile: number) => {
|
||||
if (percentile >= 90) return 'Top 10%';
|
||||
if (percentile >= 75) return 'Top 25%';
|
||||
if (percentile >= 50) return 'Top 50%';
|
||||
return `${(100 - percentile).toFixed(0)}th percentile`;
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<h3 className="text-xl font-semibold text-white mb-6">Rankings</h3>
|
||||
|
||||
<div className="space-y-4">
|
||||
{rankings.map((ranking, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="p-4 rounded-lg bg-deep-graphite border border-charcoal-outline"
|
||||
>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<RankBadge rank={ranking.rank} size="md" />
|
||||
<div>
|
||||
<div className="text-white font-medium">{ranking.name}</div>
|
||||
<div className="text-sm text-gray-400">
|
||||
{ranking.rank} of {ranking.totalDrivers} drivers
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-2xl font-bold text-primary-blue">{ranking.rating}</div>
|
||||
<div className="text-xs text-gray-400">Rating</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-gray-400">Percentile</span>
|
||||
<span className={`font-medium ${getPercentileColor(ranking.percentile)}`}>
|
||||
{getPercentileLabel(ranking.percentile)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{rankings.length === 0 && (
|
||||
<div className="text-center py-8 text-gray-400">
|
||||
No ranking data available yet.
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -4,8 +4,7 @@ import { useState, FormEvent } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import Input from '../ui/Input';
|
||||
import Button from '../ui/Button';
|
||||
import DataWarning from './DataWarning';
|
||||
import { Driver } from '@gridpilot/racing-domain/entities/Driver';
|
||||
import { Driver } from '@gridpilot/racing/domain/entities/Driver';
|
||||
import { getDriverRepository } from '../../lib/di-container';
|
||||
|
||||
interface FormErrors {
|
||||
@@ -93,7 +92,6 @@ export default function CreateDriverForm() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<DataWarning />
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<div>
|
||||
<label htmlFor="name" className="block text-sm font-medium text-gray-300 mb-2">
|
||||
@@ -106,7 +104,7 @@ export default function CreateDriverForm() {
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
error={!!errors.name}
|
||||
errorMessage={errors.name}
|
||||
placeholder="Max Verstappen"
|
||||
placeholder="Alex Vermeer"
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
73
apps/website/components/drivers/DriverCard.tsx
Normal file
73
apps/website/components/drivers/DriverCard.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import Card from '@/components/ui/Card';
|
||||
import RankBadge from '@/components/drivers/RankBadge';
|
||||
|
||||
export interface DriverCardProps {
|
||||
id: string;
|
||||
name: string;
|
||||
rating: number;
|
||||
skillLevel: 'beginner' | 'intermediate' | 'advanced' | 'pro';
|
||||
nationality: string;
|
||||
racesCompleted: number;
|
||||
wins: number;
|
||||
podiums: number;
|
||||
rank: number;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
export default function DriverCard(props: DriverCardProps) {
|
||||
const {
|
||||
name,
|
||||
rating,
|
||||
nationality,
|
||||
racesCompleted,
|
||||
wins,
|
||||
podiums,
|
||||
rank,
|
||||
onClick,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Card
|
||||
className="hover:border-charcoal-outline/60 transition-colors cursor-pointer"
|
||||
onClick={onClick}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4 flex-1">
|
||||
<RankBadge rank={rank} size="lg" />
|
||||
|
||||
<div className="w-16 h-16 rounded-full bg-primary-blue/20 flex items-center justify-center text-2xl font-bold text-white">
|
||||
{name.charAt(0)}
|
||||
</div>
|
||||
|
||||
<div className="flex-1">
|
||||
<h3 className="text-xl font-semibold text-white mb-1">{name}</h3>
|
||||
<p className="text-sm text-gray-400">
|
||||
{nationality} • {racesCompleted} races
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-8 text-center">
|
||||
<div>
|
||||
<div className="text-2xl font-bold text-primary-blue">{rating}</div>
|
||||
<div className="text-xs text-gray-400">Rating</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-2xl font-bold text-green-400">{wins}</div>
|
||||
<div className="text-xs text-gray-400">Wins</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-2xl font-bold text-warning-amber">{podiums}</div>
|
||||
<div className="text-xs text-gray-400">Podiums</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-gray-400">
|
||||
{racesCompleted > 0 ? ((wins / racesCompleted) * 100).toFixed(0) : '0'}%
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">Win Rate</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
'use client';
|
||||
|
||||
import { DriverDTO } from '@gridpilot/racing-application/mappers/EntityMappers';
|
||||
import { DriverDTO } from '@gridpilot/racing/application/mappers/EntityMappers';
|
||||
import Card from '../ui/Card';
|
||||
import ProfileHeader from './ProfileHeader';
|
||||
import ProfileHeader from '../profile/ProfileHeader';
|
||||
import ProfileStats from './ProfileStats';
|
||||
import CareerHighlights from './CareerHighlights';
|
||||
import DriverRankings from './DriverRankings';
|
||||
import PerformanceMetrics from './PerformanceMetrics';
|
||||
import { getDriverTeam } from '@/lib/team-data';
|
||||
import { getDriverTeam } from '@gridpilot/racing/application';
|
||||
import { getDriverStats, getLeagueRankings } from '@/lib/di-container';
|
||||
|
||||
interface DriverProfileProps {
|
||||
81
apps/website/components/drivers/DriverRankings.tsx
Normal file
81
apps/website/components/drivers/DriverRankings.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import Card from '@/components/ui/Card';
|
||||
|
||||
export interface DriverRanking {
|
||||
type: 'overall' | 'league';
|
||||
name: string;
|
||||
rank: number;
|
||||
totalDrivers: number;
|
||||
percentile: number;
|
||||
rating: number;
|
||||
}
|
||||
|
||||
interface DriverRankingsProps {
|
||||
rankings: DriverRanking[];
|
||||
}
|
||||
|
||||
export default function DriverRankings({ rankings }: DriverRankingsProps) {
|
||||
if (!rankings || rankings.length === 0) {
|
||||
return (
|
||||
<Card className="bg-iron-gray/60 border-charcoal-outline/80">
|
||||
<div className="p-4">
|
||||
<h3 className="text-lg font-semibold text-white mb-2">Rankings</h3>
|
||||
<p className="text-sm text-gray-400">
|
||||
No ranking data available yet. Compete in leagues to earn your first results.
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="bg-iron-gray/60 border-charcoal-outline/80">
|
||||
<div className="p-4">
|
||||
<h3 className="text-lg font-semibold text-white mb-4">Rankings</h3>
|
||||
<div className="space-y-3">
|
||||
{rankings.map((ranking, index) => (
|
||||
<div
|
||||
key={`${ranking.type}-${ranking.name}-${index}`}
|
||||
className="flex items-center justify-between py-2 px-3 rounded-lg bg-deep-graphite/60"
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-sm font-medium text-white">
|
||||
{ranking.name}
|
||||
</span>
|
||||
<span className="text-xs text-gray-400">
|
||||
{ranking.type === 'overall' ? 'Overall' : 'League'} ranking
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-6 text-right text-xs">
|
||||
<div>
|
||||
<div className="text-primary-blue text-base font-semibold">
|
||||
#{ranking.rank}
|
||||
</div>
|
||||
<div className="text-gray-500">Position</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-white text-sm font-semibold">
|
||||
{ranking.totalDrivers}
|
||||
</div>
|
||||
<div className="text-gray-500">Drivers</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-green-400 text-sm font-semibold">
|
||||
{ranking.percentile.toFixed(1)}%
|
||||
</div>
|
||||
<div className="text-gray-500">Percentile</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-warning-amber text-sm font-semibold">
|
||||
{ranking.rating}
|
||||
</div>
|
||||
<div className="text-gray-500">Rating</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { DriverDTO } from '@gridpilot/racing-application/mappers/EntityMappers';
|
||||
import { DriverDTO } from '@gridpilot/racing/application/mappers/EntityMappers';
|
||||
import Card from '../ui/Card';
|
||||
import Button from '../ui/Button';
|
||||
import Input from '../ui/Input';
|
||||
25
apps/website/components/feed/FeedEmptyState.tsx
Normal file
25
apps/website/components/feed/FeedEmptyState.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import Card from '@/components/ui/Card';
|
||||
import Button from '@/components/ui/Button';
|
||||
|
||||
export default function FeedEmptyState() {
|
||||
return (
|
||||
<Card className="bg-iron-gray/80 border-dashed border-charcoal-outline text-center py-10">
|
||||
<div className="text-3xl mb-3">🏁</div>
|
||||
<h3 className="text-lg font-semibold text-white mb-2">
|
||||
Your feed is warming up
|
||||
</h3>
|
||||
<p className="text-sm text-gray-400 mb-4 max-w-md mx-auto">
|
||||
As leagues, teams, and friends start racing, this feed will show their latest results,
|
||||
signups, and highlights.
|
||||
</p>
|
||||
<Button
|
||||
as="a"
|
||||
href="/leagues"
|
||||
variant="secondary"
|
||||
className="text-xs px-4 py-2"
|
||||
>
|
||||
Explore leagues
|
||||
</Button>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
83
apps/website/components/feed/FeedItemCard.tsx
Normal file
83
apps/website/components/feed/FeedItemCard.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
import Card from '@/components/ui/Card';
|
||||
import Button from '@/components/ui/Button';
|
||||
import type { FeedItem } from '@gridpilot/social/domain/entities/FeedItem';
|
||||
import { friends } from '@gridpilot/testing-support';
|
||||
|
||||
function timeAgo(timestamp: Date): string {
|
||||
const diffMs = Date.now() - timestamp.getTime();
|
||||
const diffMinutes = Math.floor(diffMs / 60000);
|
||||
if (diffMinutes < 1) return 'Just now';
|
||||
if (diffMinutes < 60) return `${diffMinutes} min 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 getActor(item: FeedItem) {
|
||||
if (item.actorFriendId) {
|
||||
const friend = friends.find(f => f.driverId === item.actorFriendId);
|
||||
if (friend) {
|
||||
return {
|
||||
name: friend.displayName,
|
||||
avatarUrl: friend.avatarUrl
|
||||
};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
interface FeedItemCardProps {
|
||||
item: FeedItem;
|
||||
}
|
||||
|
||||
export default function FeedItemCard({ item }: FeedItemCardProps) {
|
||||
const actor = getActor(item);
|
||||
|
||||
return (
|
||||
<div className="flex gap-4">
|
||||
<div className="flex-shrink-0">
|
||||
{actor ? (
|
||||
<div className="w-10 h-10 rounded-full overflow-hidden bg-charcoal-outline">
|
||||
<img
|
||||
src={actor.avatarUrl}
|
||||
alt={actor.name}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<Card className="w-10 h-10 flex items-center justify-center rounded-full bg-primary-blue/10 border-primary-blue/40 p-0">
|
||||
<span className="text-xs text-primary-blue font-semibold">
|
||||
{item.type.startsWith('friend') ? 'FR' : 'LG'}
|
||||
</span>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div>
|
||||
<p className="text-sm text-white">{item.headline}</p>
|
||||
{item.body && (
|
||||
<p className="text-xs text-gray-400 mt-1">{item.body}</p>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-[11px] text-gray-500 whitespace-nowrap">
|
||||
{timeAgo(item.timestamp)}
|
||||
</span>
|
||||
</div>
|
||||
{(item.ctaHref && item.ctaLabel) && (
|
||||
<div className="mt-3">
|
||||
<Button
|
||||
as="a"
|
||||
href={item.ctaHref}
|
||||
variant="secondary"
|
||||
className="text-xs px-4 py-2"
|
||||
>
|
||||
{item.ctaLabel}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
43
apps/website/components/feed/FeedLayout.tsx
Normal file
43
apps/website/components/feed/FeedLayout.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import Card from '@/components/ui/Card';
|
||||
import type { FeedItem } from '@gridpilot/social/domain/entities/FeedItem';
|
||||
import type { Race } from '@gridpilot/racing/domain/entities/Race';
|
||||
import type { RaceWithResultsDTO } from '@gridpilot/testing-support';
|
||||
import FeedList from '@/components/feed/FeedList';
|
||||
import UpcomingRacesSidebar from '@/components/races/UpcomingRacesSidebar';
|
||||
import LatestResultsSidebar from '@/components/races/LatestResultsSidebar';
|
||||
|
||||
interface FeedLayoutProps {
|
||||
feedItems: FeedItem[];
|
||||
upcomingRaces: Race[];
|
||||
latestResults: RaceWithResultsDTO[];
|
||||
}
|
||||
|
||||
export default function FeedLayout({
|
||||
feedItems,
|
||||
upcomingRaces,
|
||||
latestResults
|
||||
}: FeedLayoutProps) {
|
||||
return (
|
||||
<section className="max-w-7xl mx-auto mt-16 mb-20">
|
||||
<div className="flex flex-col gap-8 lg:grid lg:grid-cols-3">
|
||||
<div className="lg:col-span-2 space-y-4">
|
||||
<div className="flex items-baseline justify-between gap-4">
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold text-white">Activity</h2>
|
||||
<p className="text-sm text-gray-400">
|
||||
See what your friends and leagues are doing right now.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Card className="bg-iron-gray/80">
|
||||
<FeedList items={feedItems} />
|
||||
</Card>
|
||||
</div>
|
||||
<aside className="space-y-6">
|
||||
<UpcomingRacesSidebar races={upcomingRaces} />
|
||||
<LatestResultsSidebar results={latestResults} />
|
||||
</aside>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
21
apps/website/components/feed/FeedList.tsx
Normal file
21
apps/website/components/feed/FeedList.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import FeedEmptyState from '@/components/feed/FeedEmptyState';
|
||||
import FeedItemCard from '@/components/feed/FeedItemCard';
|
||||
import type { FeedItem } from '@gridpilot/social/domain/entities/FeedItem';
|
||||
|
||||
interface FeedListProps {
|
||||
items: FeedItem[];
|
||||
}
|
||||
|
||||
export default function FeedList({ items }: FeedListProps) {
|
||||
if (!items.length) {
|
||||
return <FeedEmptyState />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{items.map(item => (
|
||||
<FeedItemCard key={item.id} item={item} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
53
apps/website/components/layout/Breadcrumbs.tsx
Normal file
53
apps/website/components/layout/Breadcrumbs.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
|
||||
export type BreadcrumbItem = {
|
||||
label: string;
|
||||
href?: string;
|
||||
};
|
||||
|
||||
interface BreadcrumbsProps {
|
||||
items: BreadcrumbItem[];
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function Breadcrumbs({ items, className }: BreadcrumbsProps) {
|
||||
if (!items || items.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const lastIndex = items.length - 1;
|
||||
|
||||
return (
|
||||
<nav
|
||||
aria-label="Breadcrumb"
|
||||
className={className ?? 'text-sm text-gray-400 mb-4'}
|
||||
>
|
||||
<ol className="flex items-center gap-2 flex-wrap">
|
||||
{items.map((item, index) => {
|
||||
const isLast = index === lastIndex;
|
||||
const content = item.href && !isLast ? (
|
||||
<Link
|
||||
href={item.href}
|
||||
className="hover:text-primary-blue transition-colors"
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
) : (
|
||||
<span className={isLast ? 'text-white' : ''}>{item.label}</span>
|
||||
);
|
||||
|
||||
return (
|
||||
<li key={`${item.label}-${index}`} className="flex items-center gap-2">
|
||||
{index > 0 && (
|
||||
<span className="text-gray-600">/</span>
|
||||
)}
|
||||
{content}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ol>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
@@ -4,8 +4,7 @@ import { useState, FormEvent } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import Input from '../ui/Input';
|
||||
import Button from '../ui/Button';
|
||||
import DataWarning from './DataWarning';
|
||||
import { League } from '@gridpilot/racing-domain/entities/League';
|
||||
import { League } from '@gridpilot/racing/domain/entities/League';
|
||||
import { getLeagueRepository, getDriverRepository } from '../../lib/di-container';
|
||||
|
||||
interface FormErrors {
|
||||
@@ -98,7 +97,6 @@ export default function CreateLeagueForm() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<DataWarning />
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<div>
|
||||
<label htmlFor="name" className="block text-sm font-medium text-gray-300 mb-2">
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
requestToJoin,
|
||||
getCurrentDriverId,
|
||||
type MembershipStatus,
|
||||
} from '@/lib/membership-data';
|
||||
} from '@gridpilot/racing/application';
|
||||
|
||||
interface JoinLeagueButtonProps {
|
||||
leagueId: string;
|
||||
@@ -5,8 +5,7 @@ import { useRouter } from 'next/navigation';
|
||||
import Button from '../ui/Button';
|
||||
import Card from '../ui/Card';
|
||||
import LeagueMembers from './LeagueMembers';
|
||||
import DataWarning from './DataWarning';
|
||||
import { League } from '@gridpilot/racing-domain/entities/League';
|
||||
import { League } from '@gridpilot/racing/domain/entities/League';
|
||||
import {
|
||||
getJoinRequests,
|
||||
approveJoinRequest,
|
||||
@@ -16,9 +15,9 @@ import {
|
||||
getCurrentDriverId,
|
||||
type JoinRequest,
|
||||
type MembershipRole,
|
||||
} from '@/lib/membership-data';
|
||||
} from '@gridpilot/racing/application';
|
||||
import { getDriverRepository } from '@/lib/di-container';
|
||||
import { Driver } from '@gridpilot/racing-domain/entities/Driver';
|
||||
import { Driver } from '@gridpilot/racing/domain/entities/Driver';
|
||||
|
||||
interface LeagueAdminProps {
|
||||
league: League;
|
||||
@@ -104,8 +103,6 @@ export default function LeagueAdmin({ league, onLeagueUpdate }: LeagueAdminProps
|
||||
|
||||
return (
|
||||
<div>
|
||||
<DataWarning />
|
||||
|
||||
{error && (
|
||||
<div className="mb-6 p-4 rounded-lg bg-red-500/10 border border-red-500/30 text-red-400">
|
||||
{error}
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { League } from '@gridpilot/racing-domain/entities/League';
|
||||
import { League } from '@gridpilot/racing/domain/entities/League';
|
||||
import Card from '../ui/Card';
|
||||
|
||||
interface LeagueCardProps {
|
||||
@@ -1,9 +1,14 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Driver } from '@gridpilot/racing-domain/entities/Driver';
|
||||
import { Driver } from '@gridpilot/racing/domain/entities/Driver';
|
||||
import { getDriverRepository, getDriverStats } from '@/lib/di-container';
|
||||
import { getLeagueMembers, getCurrentDriverId, type LeagueMembership, type MembershipRole } from '@/lib/membership-data';
|
||||
import {
|
||||
getLeagueMembers,
|
||||
getCurrentDriverId,
|
||||
type LeagueMembership,
|
||||
type MembershipRole,
|
||||
} from '@gridpilot/racing/application';
|
||||
|
||||
interface LeagueMembersProps {
|
||||
leagueId: string;
|
||||
@@ -2,14 +2,14 @@
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Race } from '@gridpilot/racing-domain/entities/Race';
|
||||
import { Race } from '@gridpilot/racing/domain/entities/Race';
|
||||
import { getRaceRepository } from '@/lib/di-container';
|
||||
import { getCurrentDriverId } from '@/lib/membership-data';
|
||||
import {
|
||||
getCurrentDriverId,
|
||||
isRegistered,
|
||||
registerForRace,
|
||||
withdrawFromRace
|
||||
} from '@/lib/registration-data';
|
||||
withdrawFromRace,
|
||||
} from '@gridpilot/racing/application';
|
||||
|
||||
interface LeagueScheduleProps {
|
||||
leagueId: string;
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { getMembership, getCurrentDriverId, type MembershipRole } from '@/lib/membership-data';
|
||||
import { getMembership, getCurrentDriverId, type MembershipRole } from '@gridpilot/racing/application';
|
||||
|
||||
interface MembershipStatusProps {
|
||||
leagueId: string;
|
||||
@@ -4,12 +4,11 @@ import { useState, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import Button from '../ui/Button';
|
||||
import Input from '../ui/Input';
|
||||
import DataWarning from './DataWarning';
|
||||
import { Race } from '@gridpilot/racing-domain/entities/Race';
|
||||
import { League } from '@gridpilot/racing-domain/entities/League';
|
||||
import { SessionType } from '@gridpilot/racing-domain/entities/Race';
|
||||
import { Race } from '@gridpilot/racing/domain/entities/Race';
|
||||
import { League } from '@gridpilot/racing/domain/entities/League';
|
||||
import { SessionType } from '@gridpilot/racing/domain/entities/Race';
|
||||
import { getRaceRepository, getLeagueRepository } from '../../lib/di-container';
|
||||
import { InMemoryRaceRepository } from '@gridpilot/racing-infrastructure/repositories/InMemoryRaceRepository';
|
||||
import { InMemoryRaceRepository } from '@gridpilot/racing/infrastructure/repositories/InMemoryRaceRepository';
|
||||
|
||||
interface ScheduleRaceFormProps {
|
||||
preSelectedLeagueId?: string;
|
||||
@@ -136,7 +135,6 @@ export default function ScheduleRaceForm({
|
||||
|
||||
return (
|
||||
<>
|
||||
<DataWarning />
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
{error && (
|
||||
<div className="p-4 rounded-lg bg-red-500/10 border border-red-500/30 text-red-400">
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { Standing } from '@gridpilot/racing-domain/entities/Standing';
|
||||
import { Driver } from '@gridpilot/racing-domain/entities/Driver';
|
||||
import { Standing } from '@gridpilot/racing/domain/entities/Standing';
|
||||
import { Driver } from '@gridpilot/racing/domain/entities/Driver';
|
||||
|
||||
interface StandingsTableProps {
|
||||
standings: Standing[];
|
||||
@@ -1,8 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { DriverDTO } from '@gridpilot/racing-application/mappers/EntityMappers';
|
||||
import { DriverDTO } from '@gridpilot/racing/application/mappers/EntityMappers';
|
||||
import Button from '../ui/Button';
|
||||
import { getDriverTeam } from '@/lib/team-data';
|
||||
import { getDriverTeam } from '@gridpilot/racing/application';
|
||||
|
||||
interface ProfileHeaderProps {
|
||||
driver: DriverDTO;
|
||||
@@ -2,8 +2,7 @@
|
||||
|
||||
import { useState } from 'react';
|
||||
import Button from '../ui/Button';
|
||||
import DataWarning from './DataWarning';
|
||||
import { Result } from '@gridpilot/racing-domain/entities/Result';
|
||||
import { Result } from '@gridpilot/racing/domain/entities/Result';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
interface ImportResultsFormProps {
|
||||
@@ -143,7 +142,6 @@ export default function ImportResultsForm({ raceId, onSuccess, onError }: Import
|
||||
|
||||
return (
|
||||
<>
|
||||
<DataWarning />
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-400 mb-2">
|
||||
36
apps/website/components/races/LatestResultsSidebar.tsx
Normal file
36
apps/website/components/races/LatestResultsSidebar.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import Card from '@/components/ui/Card';
|
||||
import type { RaceWithResultsDTO } from '@gridpilot/testing-support';
|
||||
|
||||
interface LatestResultsSidebarProps {
|
||||
results: RaceWithResultsDTO[];
|
||||
}
|
||||
|
||||
export default function LatestResultsSidebar({ results }: LatestResultsSidebarProps) {
|
||||
if (!results.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="bg-iron-gray/80">
|
||||
<h3 className="text-sm font-semibold text-white mb-3">Latest results</h3>
|
||||
<ul className="space-y-3">
|
||||
{results.slice(0, 4).map(result => (
|
||||
<li key={result.raceId} className="flex items-start justify-between gap-3 text-xs">
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-white truncate">{result.track}</p>
|
||||
<p className="text-gray-400 truncate">
|
||||
{result.winnerName} • {result.car}
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-right text-gray-500 whitespace-nowrap">
|
||||
{result.scheduledAt.toLocaleDateString(undefined, {
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
})}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { Race } from '@gridpilot/racing-domain/entities/Race';
|
||||
import { Race } from '@gridpilot/racing/domain/entities/Race';
|
||||
|
||||
interface RaceCardProps {
|
||||
race: Race;
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { Result } from '@gridpilot/racing-domain/entities/Result';
|
||||
import { Driver } from '@gridpilot/racing-domain/entities/Driver';
|
||||
import { Result } from '@gridpilot/racing/domain/entities/Result';
|
||||
import { Driver } from '@gridpilot/racing/domain/entities/Driver';
|
||||
|
||||
interface ResultsTableProps {
|
||||
results: Result[];
|
||||
45
apps/website/components/races/UpcomingRacesSidebar.tsx
Normal file
45
apps/website/components/races/UpcomingRacesSidebar.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import type { Race } from '@gridpilot/racing/domain/entities/Race';
|
||||
import Card from '@/components/ui/Card';
|
||||
import Button from '@/components/ui/Button';
|
||||
|
||||
interface UpcomingRacesSidebarProps {
|
||||
races: Race[];
|
||||
}
|
||||
|
||||
export default function UpcomingRacesSidebar({ races }: UpcomingRacesSidebarProps) {
|
||||
if (!races.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="bg-iron-gray/80">
|
||||
<div className="flex items-baseline justify-between mb-3">
|
||||
<h3 className="text-sm font-semibold text-white">Upcoming races</h3>
|
||||
<Button
|
||||
as="a"
|
||||
href="/races"
|
||||
variant="secondary"
|
||||
className="text-[11px] px-3 py-1.5"
|
||||
>
|
||||
View all
|
||||
</Button>
|
||||
</div>
|
||||
<ul className="space-y-3">
|
||||
{races.slice(0, 4).map(race => (
|
||||
<li key={race.id} className="flex items-start justify-between gap-3 text-xs">
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-white truncate">{race.track}</p>
|
||||
<p className="text-gray-400 truncate">{race.car}</p>
|
||||
</div>
|
||||
<div className="text-right text-gray-500 whitespace-nowrap">
|
||||
{race.scheduledAt.toLocaleDateString(undefined, {
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
})}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import { useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import Button from '@/components/ui/Button';
|
||||
import Input from '@/components/ui/Input';
|
||||
import { createTeam, getCurrentDriverId } from '@/lib/team-data';
|
||||
import { createTeam, getCurrentDriverId } from '@gridpilot/racing/application';
|
||||
|
||||
interface CreateTeamFormProps {
|
||||
onCancel?: () => void;
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
joinTeam,
|
||||
requestToJoinTeam,
|
||||
leaveTeam,
|
||||
} from '@/lib/team-data';
|
||||
} from '@gridpilot/racing/application';
|
||||
|
||||
interface JoinTeamButtonProps {
|
||||
teamId: string;
|
||||
@@ -5,7 +5,7 @@ import Card from '@/components/ui/Card';
|
||||
import Button from '@/components/ui/Button';
|
||||
import Input from '@/components/ui/Input';
|
||||
import { getDriverRepository } from '@/lib/di-container';
|
||||
import { EntityMappers, DriverDTO } from '@gridpilot/racing-application/mappers/EntityMappers';
|
||||
import { EntityMappers, DriverDTO } from '@gridpilot/racing/application/mappers/EntityMappers';
|
||||
import {
|
||||
Team,
|
||||
TeamJoinRequest,
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
approveTeamJoinRequest,
|
||||
rejectTeamJoinRequest,
|
||||
updateTeam,
|
||||
} from '@/lib/team-data';
|
||||
} from '@gridpilot/racing/application';
|
||||
|
||||
interface TeamAdminProps {
|
||||
team: Team;
|
||||
@@ -3,8 +3,8 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import Card from '@/components/ui/Card';
|
||||
import { getDriverRepository, getDriverStats } from '@/lib/di-container';
|
||||
import { EntityMappers, DriverDTO } from '@gridpilot/racing-application/mappers/EntityMappers';
|
||||
import { TeamMembership, TeamRole } from '@/lib/team-data';
|
||||
import { EntityMappers, DriverDTO } from '@gridpilot/racing/application/mappers/EntityMappers';
|
||||
import { TeamMembership, TeamRole } from '@gridpilot/racing/application';
|
||||
|
||||
interface TeamRosterProps {
|
||||
teamId: string;
|
||||
@@ -3,8 +3,8 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import Card from '@/components/ui/Card';
|
||||
import { getStandingRepository, getLeagueRepository } from '@/lib/di-container';
|
||||
import { EntityMappers, LeagueDTO } from '@gridpilot/racing-application/mappers/EntityMappers';
|
||||
import { getTeamMembers } from '@/lib/team-data';
|
||||
import { EntityMappers, LeagueDTO } from '@gridpilot/racing/application/mappers/EntityMappers';
|
||||
import { getTeamMembers } from '@gridpilot/racing/application';
|
||||
|
||||
interface TeamStandingsProps {
|
||||
teamId: string;
|
||||
Reference in New Issue
Block a user