This commit is contained in:
2025-12-11 00:57:32 +01:00
parent 1303a14493
commit 6a427eab57
112 changed files with 6148 additions and 2272 deletions

View File

@@ -26,21 +26,11 @@ import {
import Card from '@/components/ui/Card';
import Button from '@/components/ui/Button';
import { getAuthService } from '@/lib/auth';
import {
getFeedRepository,
getRaceRepository,
getResultRepository,
getDriverRepository,
getLeagueRepository,
getStandingRepository,
getSocialRepository,
getDriverStats,
getImageService,
getLeagueMembershipRepository,
} from '@/lib/di-container';
import type { FeedItem } from '@gridpilot/social/domain/entities/FeedItem';
import type { Race } from '@gridpilot/racing/domain/entities/Race';
import type { Driver } from '@gridpilot/racing/domain/entities/Driver';
import { getGetDashboardOverviewUseCase } from '@/lib/di-container';
import type {
DashboardOverviewViewModel,
DashboardFeedItemSummaryViewModel,
} from '@gridpilot/racing/application/presenters/IDashboardOverviewPresenter';
export const dynamic = 'force-dynamic';
@@ -74,8 +64,9 @@ function timeUntil(date: Date): string {
return `${diffMinutes}m`;
}
function timeAgo(timestamp: Date): string {
const diffMs = Date.now() - timestamp.getTime();
function timeAgo(timestamp: Date | string): string {
const time = typeof timestamp === 'string' ? new Date(timestamp) : timestamp;
const diffMs = Date.now() - time.getTime();
const diffMinutes = Math.floor(diffMs / 60000);
if (diffMinutes < 1) return 'Just now';
if (diffMinutes < 60) return `${diffMinutes}m ago`;
@@ -100,73 +91,48 @@ export default async function DashboardPage() {
redirect('/auth/iracing?returnTo=/dashboard');
}
const feedRepository = getFeedRepository();
const raceRepository = getRaceRepository();
const resultRepository = getResultRepository();
const driverRepository = getDriverRepository();
const leagueRepository = getLeagueRepository();
const standingRepository = getStandingRepository();
const socialRepository = getSocialRepository();
const leagueMembershipRepository = getLeagueMembershipRepository();
const imageService = getImageService();
const currentDriverId = session.user.primaryDriverId ?? '';
const currentDriver = await driverRepository.findById(currentDriverId);
const [feedItems, allRaces, allResults, allLeagues, friends] = await Promise.all([
feedRepository.getFeedForDriver(currentDriverId),
raceRepository.findAll(),
resultRepository.findAll(),
leagueRepository.findAll(),
socialRepository.getFriends(currentDriverId),
]);
const useCase = getGetDashboardOverviewUseCase();
await useCase.execute({ driverId: currentDriverId });
const viewModel = useCase.presenter.getViewModel() as DashboardOverviewViewModel | null;
// Get driver's leagues by checking membership in each league
const driverLeagues: typeof allLeagues = [];
for (const league of allLeagues) {
const membership = await leagueMembershipRepository.getMembership(league.id, currentDriverId);
if (membership && membership.status === 'active') {
driverLeagues.push(league);
}
if (!viewModel) {
return null;
}
const driverLeagueIds = driverLeagues.map(l => l.id);
// Upcoming races (prioritize driver's leagues)
const upcomingRaces = allRaces
.filter((race) => race.status === 'scheduled')
.sort((a, b) => a.scheduledAt.getTime() - b.scheduledAt.getTime());
const myUpcomingRaces = upcomingRaces.filter(r => driverLeagueIds.includes(r.leagueId));
const otherUpcomingRaces = upcomingRaces.filter(r => !driverLeagueIds.includes(r.leagueId));
const nextRace = myUpcomingRaces[0] || otherUpcomingRaces[0];
const {
currentDriver,
myUpcomingRaces,
otherUpcomingRaces,
nextRace: nextRaceSummary,
recentResults,
leagueStandingsSummaries,
feedSummary,
friends,
upcomingRaces,
activeLeaguesCount,
} = viewModel;
// Recent results for driver
const driverResults = allResults.filter(r => r.driverId === currentDriverId);
const recentResults = driverResults.slice(0, 5);
const nextRace =
nextRaceSummary != null
? {
...nextRaceSummary,
scheduledAt: new Date(nextRaceSummary.scheduledAt),
}
: null;
// Get stats
const driverStats = getDriverStats(currentDriverId);
const upcomingRacesForDisplay = upcomingRaces.map(race => ({
...race,
scheduledAt: new Date(race.scheduledAt),
}));
// Get standings for driver's leagues
const leagueStandings = await Promise.all(
driverLeagues.slice(0, 3).map(async (league) => {
const standings = await standingRepository.findByLeagueId(league.id);
const driverStanding = standings.find(s => s.driverId === currentDriverId);
return {
league,
position: driverStanding?.position ?? 0,
points: driverStanding?.points ?? 0,
totalDrivers: standings.length,
};
})
);
// Calculate quick stats
const totalRaces = driverStats?.totalRaces ?? 0;
const wins = driverStats?.wins ?? 0;
const podiums = driverStats?.podiums ?? 0;
const rating = driverStats?.rating ?? 1500;
const globalRank = driverStats?.overallRank ?? 0;
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 (
<main className="min-h-screen bg-deep-graphite">
@@ -189,7 +155,7 @@ export default async function DashboardPage() {
<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={imageService.getDriverAvatar(currentDriverId)}
src={currentDriver.avatarUrl}
alt={currentDriver.name}
width={80}
height={80}
@@ -267,7 +233,7 @@ export default async function DashboardPage() {
<Target className="w-5 h-5 text-primary-blue" />
</div>
<div>
<p className="text-2xl font-bold text-white">{driverStats?.consistency ?? 0}%</p>
<p className="text-2xl font-bold text-white">{consistency}%</p>
<p className="text-xs text-gray-500">Consistency</p>
</div>
</div>
@@ -278,7 +244,7 @@ export default async function DashboardPage() {
<Users className="w-5 h-5 text-purple-400" />
</div>
<div>
<p className="text-2xl font-bold text-white">{driverLeagues.length}</p>
<p className="text-2xl font-bold text-white">{activeLeaguesCount}</p>
<p className="text-xs text-gray-500">Active Leagues</p>
</div>
</div>
@@ -302,7 +268,7 @@ export default async function DashboardPage() {
<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>
{myUpcomingRaces.includes(nextRace) && (
{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>
@@ -350,7 +316,7 @@ export default async function DashboardPage() {
)}
{/* League Standings Preview */}
{leagueStandings.length > 0 && (
{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">
@@ -362,10 +328,10 @@ export default async function DashboardPage() {
</Link>
</div>
<div className="space-y-3">
{leagueStandings.map(({ league, position, points, totalDrivers }) => (
{leagueStandingsSummaries.map(({ leagueId, leagueName, position, points, totalDrivers }) => (
<Link
key={league.id}
href={`/leagues/${league.id}/standings`}
key={leagueId}
href={`/leagues/${leagueId}/standings`}
className="flex items-center gap-4 p-4 rounded-xl bg-deep-graphite border border-charcoal-outline hover:border-primary-blue/30 transition-colors group"
>
<div className={`flex h-12 w-12 items-center justify-center rounded-xl font-bold text-xl ${
@@ -378,7 +344,7 @@ export default async function DashboardPage() {
</div>
<div className="flex-1 min-w-0">
<p className="text-white font-semibold truncate group-hover:text-primary-blue transition-colors">
{league.name}
{leagueName}
</p>
<p className="text-sm text-gray-500">
{points} points {totalDrivers} drivers
@@ -408,10 +374,10 @@ export default async function DashboardPage() {
Recent Activity
</h2>
</div>
{feedItems.length > 0 ? (
{feedSummary.items.length > 0 ? (
<div className="space-y-4">
{feedItems.slice(0, 5).map((item) => (
<FeedItemRow key={item.id} item={item} imageService={imageService} />
{feedSummary.items.slice(0, 5).map((item) => (
<FeedItemRow key={item.id} item={item} />
))}
</div>
) : (
@@ -437,10 +403,10 @@ export default async function DashboardPage() {
View all
</Link>
</div>
{upcomingRaces.length > 0 ? (
{upcomingRacesForDisplay.length > 0 ? (
<div className="space-y-3">
{upcomingRaces.slice(0, 5).map((race) => {
const isMyRace = driverLeagueIds.includes(race.leagueId);
{upcomingRacesForDisplay.slice(0, 5).map((race) => {
const isMyRace = race.isMyLeague;
return (
<Link
key={race.id}
@@ -488,7 +454,7 @@ export default async function DashboardPage() {
>
<div className="w-9 h-9 rounded-full overflow-hidden bg-gradient-to-br from-primary-blue to-purple-600">
<Image
src={imageService.getDriverAvatar(friend.id)}
src={friend.avatarUrl}
alt={friend.name}
width={36}
height={36}
@@ -530,7 +496,7 @@ export default async function DashboardPage() {
}
// Feed Item Row Component
function FeedItemRow({ item, imageService }: { item: FeedItem; imageService: any }) {
function FeedItemRow({ item }: { item: DashboardFeedItemSummaryViewModel }) {
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' };