wip
This commit is contained in:
@@ -6,8 +6,11 @@ import Link from 'next/link';
|
||||
import Card from '@/components/ui/Card';
|
||||
import Button from '@/components/ui/Button';
|
||||
import Heading from '@/components/ui/Heading';
|
||||
import { Race, RaceStatus } from '@gridpilot/racing/domain/entities/Race';
|
||||
import { getGetRacesPageDataUseCase } from '@/lib/di-container';
|
||||
import type {
|
||||
RacesPageViewModel,
|
||||
RaceListItemViewModel,
|
||||
} from '@gridpilot/racing/application/presenters/IRacesPagePresenter';
|
||||
import {
|
||||
Calendar,
|
||||
Clock,
|
||||
@@ -27,21 +30,16 @@ import {
|
||||
} from 'lucide-react';
|
||||
|
||||
type TimeFilter = 'all' | 'upcoming' | 'live' | 'past';
|
||||
type RaceStatusFilter = RaceListItemViewModel['status'];
|
||||
|
||||
export default function RacesPage() {
|
||||
const router = useRouter();
|
||||
|
||||
const [pageData, setPageData] = useState<{
|
||||
races: Array<{ race: Race; leagueName: string }>;
|
||||
stats: { total: number; scheduled: number; running: number; completed: number };
|
||||
liveRaces: Array<{ race: Race; leagueName: string }>;
|
||||
upcomingRaces: Array<{ race: Race; leagueName: string }>;
|
||||
recentResults: Array<{ race: Race; leagueName: string }>;
|
||||
} | null>(null);
|
||||
const [pageData, setPageData] = useState<RacesPageViewModel | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
// Filters
|
||||
const [statusFilter, setStatusFilter] = useState<RaceStatus | 'all'>('all');
|
||||
const [statusFilter, setStatusFilter] = useState<RaceStatusFilter | 'all'>('all');
|
||||
const [leagueFilter, setLeagueFilter] = useState<string>('all');
|
||||
const [timeFilter, setTimeFilter] = useState<TimeFilter>('upcoming');
|
||||
|
||||
@@ -50,71 +48,7 @@ export default function RacesPage() {
|
||||
const useCase = getGetRacesPageDataUseCase();
|
||||
await useCase.execute();
|
||||
const data = useCase.presenter.getViewModel();
|
||||
|
||||
// Transform ViewModel back to page format
|
||||
setPageData({
|
||||
races: data.races.map(r => ({
|
||||
race: {
|
||||
id: r.id,
|
||||
track: r.track,
|
||||
car: r.car,
|
||||
scheduledAt: new Date(r.scheduledAt),
|
||||
status: r.status,
|
||||
leagueId: r.leagueId,
|
||||
strengthOfField: r.strengthOfField,
|
||||
isUpcoming: () => r.isUpcoming,
|
||||
isLive: () => r.isLive,
|
||||
isPast: () => r.isPast,
|
||||
} as Race,
|
||||
leagueName: r.leagueName,
|
||||
})),
|
||||
stats: data.stats,
|
||||
liveRaces: data.liveRaces.map(r => ({
|
||||
race: {
|
||||
id: r.id,
|
||||
track: r.track,
|
||||
car: r.car,
|
||||
scheduledAt: new Date(r.scheduledAt),
|
||||
status: r.status,
|
||||
leagueId: r.leagueId,
|
||||
strengthOfField: r.strengthOfField,
|
||||
isUpcoming: () => r.isUpcoming,
|
||||
isLive: () => r.isLive,
|
||||
isPast: () => r.isPast,
|
||||
} as Race,
|
||||
leagueName: r.leagueName,
|
||||
})),
|
||||
upcomingRaces: data.upcomingThisWeek.map(r => ({
|
||||
race: {
|
||||
id: r.id,
|
||||
track: r.track,
|
||||
car: r.car,
|
||||
scheduledAt: new Date(r.scheduledAt),
|
||||
status: r.status,
|
||||
leagueId: r.leagueId,
|
||||
strengthOfField: r.strengthOfField,
|
||||
isUpcoming: () => r.isUpcoming,
|
||||
isLive: () => r.isLive,
|
||||
isPast: () => r.isPast,
|
||||
} as Race,
|
||||
leagueName: r.leagueName,
|
||||
})),
|
||||
recentResults: data.recentResults.map(r => ({
|
||||
race: {
|
||||
id: r.id,
|
||||
track: r.track,
|
||||
car: r.car,
|
||||
scheduledAt: new Date(r.scheduledAt),
|
||||
status: r.status,
|
||||
leagueId: r.leagueId,
|
||||
strengthOfField: r.strengthOfField,
|
||||
isUpcoming: () => r.isUpcoming,
|
||||
isLive: () => r.isLive,
|
||||
isPast: () => r.isPast,
|
||||
} as Race,
|
||||
leagueName: r.leagueName,
|
||||
})),
|
||||
});
|
||||
setPageData(data);
|
||||
} catch (err) {
|
||||
console.error('Failed to load races:', err);
|
||||
} finally {
|
||||
@@ -130,7 +64,7 @@ export default function RacesPage() {
|
||||
const filteredRaces = useMemo(() => {
|
||||
if (!pageData) return [];
|
||||
|
||||
return pageData.races.filter(({ race }) => {
|
||||
return pageData.races.filter((race) => {
|
||||
// Status filter
|
||||
if (statusFilter !== 'all' && race.status !== statusFilter) {
|
||||
return false;
|
||||
@@ -142,13 +76,13 @@ export default function RacesPage() {
|
||||
}
|
||||
|
||||
// Time filter
|
||||
if (timeFilter === 'upcoming' && !race.isUpcoming()) {
|
||||
if (timeFilter === 'upcoming' && !race.isUpcoming) {
|
||||
return false;
|
||||
}
|
||||
if (timeFilter === 'live' && !race.isLive()) {
|
||||
if (timeFilter === 'live' && !race.isLive) {
|
||||
return false;
|
||||
}
|
||||
if (timeFilter === 'past' && !race.isPast()) {
|
||||
if (timeFilter === 'past' && !race.isPast) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -158,18 +92,18 @@ export default function RacesPage() {
|
||||
|
||||
// Group races by date for calendar view
|
||||
const racesByDate = useMemo(() => {
|
||||
const grouped = new Map<string, Array<{ race: Race; leagueName: string }>>();
|
||||
filteredRaces.forEach(item => {
|
||||
const dateKey = item.race.scheduledAt.toISOString().split('T')[0];
|
||||
const grouped = new Map<string, RaceListItemViewModel[]>();
|
||||
filteredRaces.forEach(race => {
|
||||
const dateKey = new Date(race.scheduledAt).toISOString().split('T')[0];
|
||||
if (!grouped.has(dateKey)) {
|
||||
grouped.set(dateKey, []);
|
||||
}
|
||||
grouped.get(dateKey)!.push(item);
|
||||
grouped.get(dateKey)!.push(race);
|
||||
});
|
||||
return grouped;
|
||||
}, [filteredRaces]);
|
||||
|
||||
const upcomingRaces = pageData?.upcomingRaces ?? [];
|
||||
|
||||
const upcomingRaces = pageData?.upcomingThisWeek ?? [];
|
||||
const liveRaces = pageData?.liveRaces ?? [];
|
||||
const recentResults = pageData?.recentResults ?? [];
|
||||
const stats = pageData?.stats ?? { total: 0, scheduled: 0, running: 0, completed: 0 };
|
||||
@@ -331,7 +265,7 @@ export default function RacesPage() {
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{liveRaces.map(({ race, leagueName }) => (
|
||||
{liveRaces.map((race) => (
|
||||
<div
|
||||
key={race.id}
|
||||
onClick={() => router.push(`/races/${race.id}`)}
|
||||
@@ -343,7 +277,7 @@ export default function RacesPage() {
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-white">{race.track}</h3>
|
||||
<p className="text-sm text-gray-400">{leagueName}</p>
|
||||
<p className="text-sm text-gray-400">{race.leagueName}</p>
|
||||
</div>
|
||||
</div>
|
||||
<ChevronRight className="w-5 h-5 text-gray-400" />
|
||||
@@ -385,8 +319,8 @@ export default function RacesPage() {
|
||||
className="px-4 py-2 bg-deep-graphite border border-charcoal-outline rounded-lg text-white text-sm focus:outline-none focus:ring-2 focus:ring-primary-blue"
|
||||
>
|
||||
<option value="all">All Leagues</option>
|
||||
{pageData && [...new Set(pageData.races.map(r => r.race.leagueId))].map(leagueId => {
|
||||
const item = pageData.races.find(r => r.race.leagueId === leagueId);
|
||||
{pageData && [...new Set(pageData.races.map(r => r.leagueId))].map(leagueId => {
|
||||
const item = pageData.races.find(r => r.leagueId === leagueId);
|
||||
return item ? (
|
||||
<option key={leagueId} value={leagueId}>
|
||||
{item.leagueName}
|
||||
@@ -415,106 +349,106 @@ export default function RacesPage() {
|
||||
</div>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{Array.from(racesByDate.entries()).map(([dateKey, dayRaces]) => (
|
||||
<div key={dateKey} className="space-y-3">
|
||||
{/* Date Header */}
|
||||
<div className="flex items-center gap-3 px-2">
|
||||
<div className="p-2 bg-primary-blue/10 rounded-lg">
|
||||
<Calendar className="w-4 h-4 text-primary-blue" />
|
||||
</div>
|
||||
<span className="text-sm font-semibold text-white">
|
||||
{formatFullDate(new Date(dateKey))}
|
||||
</span>
|
||||
<span className="text-xs text-gray-500">
|
||||
{dayRaces.length} race{dayRaces.length !== 1 ? 's' : ''}
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
{Array.from(racesByDate.entries()).map(([dateKey, dayRaces]) => (
|
||||
<div key={dateKey} className="space-y-3">
|
||||
{/* Date Header */}
|
||||
<div className="flex items-center gap-3 px-2">
|
||||
<div className="p-2 bg-primary-blue/10 rounded-lg">
|
||||
<Calendar className="w-4 h-4 text-primary-blue" />
|
||||
</div>
|
||||
<span className="text-sm font-semibold text-white">
|
||||
{formatFullDate(new Date(dateKey))}
|
||||
</span>
|
||||
<span className="text-xs text-gray-500">
|
||||
{dayRaces.length} race{dayRaces.length !== 1 ? 's' : ''}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Races for this date */}
|
||||
<div className="space-y-2">
|
||||
{dayRaces.map(({ race, leagueName }) => {
|
||||
const config = statusConfig[race.status];
|
||||
const StatusIcon = config.icon;
|
||||
{/* Races for this date */}
|
||||
<div className="space-y-2">
|
||||
{dayRaces.map((race) => {
|
||||
const config = statusConfig[race.status];
|
||||
const StatusIcon = config.icon;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={race.id}
|
||||
onClick={() => router.push(`/races/${race.id}`)}
|
||||
className={`group relative overflow-hidden rounded-xl bg-iron-gray border ${config.border} p-4 cursor-pointer transition-all duration-200 hover:scale-[1.01] hover:border-primary-blue`}
|
||||
>
|
||||
{/* Live indicator */}
|
||||
{race.status === 'running' && (
|
||||
<div className="absolute top-0 left-0 right-0 h-1 bg-gradient-to-r from-performance-green via-performance-green/50 to-performance-green animate-pulse" />
|
||||
)}
|
||||
return (
|
||||
<div
|
||||
key={race.id}
|
||||
onClick={() => router.push(`/races/${race.id}`)}
|
||||
className={`group relative overflow-hidden rounded-xl bg-iron-gray border ${config.border} p-4 cursor-pointer transition-all duration-200 hover:scale-[1.01] hover:border-primary-blue`}
|
||||
>
|
||||
{/* Live indicator */}
|
||||
{race.status === 'running' && (
|
||||
<div className="absolute top-0 left-0 right-0 h-1 bg-gradient-to-r from-performance-green via-performance-green/50 to-performance-green animate-pulse" />
|
||||
)}
|
||||
|
||||
<div className="flex items-start gap-4">
|
||||
{/* Time Column */}
|
||||
<div className="flex-shrink-0 text-center min-w-[60px]">
|
||||
<p className="text-lg font-bold text-white">{formatTime(race.scheduledAt)}</p>
|
||||
<p className={`text-xs ${config.color}`}>
|
||||
{race.status === 'running' ? 'LIVE' : getRelativeTime(race.scheduledAt)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-start gap-4">
|
||||
{/* Time Column */}
|
||||
<div className="flex-shrink-0 text-center min-w-[60px]">
|
||||
<p className="text-lg font-bold text-white">{formatTime(new Date(race.scheduledAt))}</p>
|
||||
<p className={`text-xs ${config.color}`}>
|
||||
{race.status === 'running' ? 'LIVE' : getRelativeTime(new Date(race.scheduledAt))}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Divider */}
|
||||
<div className={`w-px self-stretch ${config.bg}`} />
|
||||
{/* Divider */}
|
||||
<div className={`w-px self-stretch ${config.bg}`} />
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="min-w-0">
|
||||
<h3 className="font-semibold text-white truncate group-hover:text-primary-blue transition-colors">
|
||||
{race.track}
|
||||
</h3>
|
||||
<div className="flex items-center gap-3 mt-1">
|
||||
<span className="flex items-center gap-1 text-sm text-gray-400">
|
||||
<Car className="w-3.5 h-3.5" />
|
||||
{race.car}
|
||||
</span>
|
||||
{race.strengthOfField && (
|
||||
<span className="flex items-center gap-1 text-sm text-gray-400">
|
||||
<Zap className="w-3.5 h-3.5 text-warning-amber" />
|
||||
SOF {race.strengthOfField}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{/* Main Content */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="min-w-0">
|
||||
<h3 className="font-semibold text-white truncate group-hover:text-primary-blue transition-colors">
|
||||
{race.track}
|
||||
</h3>
|
||||
<div className="flex items-center gap-3 mt-1">
|
||||
<span className="flex items-center gap-1 text-sm text-gray-400">
|
||||
<Car className="w-3.5 h-3.5" />
|
||||
{race.car}
|
||||
</span>
|
||||
{race.strengthOfField && (
|
||||
<span className="flex items-center gap-1 text-sm text-gray-400">
|
||||
<Zap className="w-3.5 h-3.5 text-warning-amber" />
|
||||
SOF {race.strengthOfField}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Status Badge */}
|
||||
<div className={`flex items-center gap-1.5 px-2.5 py-1 rounded-full ${config.bg} ${config.border} border`}>
|
||||
<StatusIcon className={`w-3.5 h-3.5 ${config.color}`} />
|
||||
<span className={`text-xs font-medium ${config.color}`}>
|
||||
{config.label}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{/* Status Badge */}
|
||||
<div className={`flex items-center gap-1.5 px-2.5 py-1 rounded-full ${config.bg} ${config.border} border`}>
|
||||
<StatusIcon className={`w-3.5 h-3.5 ${config.color}`} />
|
||||
<span className={`text-xs font-medium ${config.color}`}>
|
||||
{config.label}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* League Link */}
|
||||
<div className="mt-3 pt-3 border-t border-charcoal-outline/50">
|
||||
<Link
|
||||
href={`/leagues/${race.leagueId}`}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="inline-flex items-center gap-2 text-sm text-primary-blue hover:underline"
|
||||
>
|
||||
<Trophy className="w-3.5 h-3.5" />
|
||||
{leagueName}
|
||||
<ArrowRight className="w-3 h-3" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
{/* League Link */}
|
||||
<div className="mt-3 pt-3 border-t border-charcoal-outline/50">
|
||||
<Link
|
||||
href={`/leagues/${race.leagueId}`}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="inline-flex items-center gap-2 text-sm text-primary-blue hover:underline"
|
||||
>
|
||||
<Trophy className="w-3.5 h-3.5" />
|
||||
{race.leagueName}
|
||||
<ArrowRight className="w-3 h-3" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Arrow */}
|
||||
<ChevronRight className="w-5 h-5 text-gray-500 group-hover:text-primary-blue transition-colors flex-shrink-0" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{/* Arrow */}
|
||||
<ChevronRight className="w-5 h-5 text-gray-500 group-hover:text-primary-blue transition-colors flex-shrink-0" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* View All Link */}
|
||||
{filteredRaces.length > 0 && (
|
||||
@@ -548,7 +482,7 @@ export default function RacesPage() {
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{upcomingRaces.map(({ race }) => (
|
||||
{upcomingRaces.map((race) => (
|
||||
<div
|
||||
key={race.id}
|
||||
onClick={() => router.push(`/races/${race.id}`)}
|
||||
@@ -561,7 +495,7 @@ export default function RacesPage() {
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-sm font-medium text-white truncate">{race.track}</p>
|
||||
<p className="text-xs text-gray-500">{formatTime(race.scheduledAt)}</p>
|
||||
<p className="text-xs text-gray-500">{formatTime(new Date(race.scheduledAt))}</p>
|
||||
</div>
|
||||
<ChevronRight className="w-4 h-4 text-gray-500" />
|
||||
</div>
|
||||
@@ -585,7 +519,7 @@ export default function RacesPage() {
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{recentResults.map(({ race }) => (
|
||||
{recentResults.map((race) => (
|
||||
<div
|
||||
key={race.id}
|
||||
onClick={() => router.push(`/races/${race.id}/results`)}
|
||||
@@ -596,7 +530,7 @@ export default function RacesPage() {
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-sm font-medium text-white truncate">{race.track}</p>
|
||||
<p className="text-xs text-gray-500">{formatDate(race.scheduledAt)}</p>
|
||||
<p className="text-xs text-gray-500">{formatDate(new Date(race.scheduledAt))}</p>
|
||||
</div>
|
||||
<ChevronRight className="w-4 h-4 text-gray-500" />
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user