wip
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useEffectiveDriverId } from '@/lib/currentDriver';
|
||||
import { getSendNotificationUseCase } from '@/lib/di-container';
|
||||
import { getSendNotificationUseCase, getRaceRepository, getLeagueRepository } from '@/lib/di-container';
|
||||
import type { NotificationUrgency } from '@gridpilot/notifications/application';
|
||||
import {
|
||||
Bell,
|
||||
@@ -170,38 +170,64 @@ export default function DevToolbar() {
|
||||
setSending(true);
|
||||
try {
|
||||
const sendNotification = getSendNotificationUseCase();
|
||||
|
||||
|
||||
const raceRepository = getRaceRepository();
|
||||
const leagueRepository = getLeagueRepository();
|
||||
|
||||
const [allRaces, allLeagues] = await Promise.all([
|
||||
raceRepository.findAll(),
|
||||
leagueRepository.findAll(),
|
||||
]);
|
||||
|
||||
const completedRaces = allRaces.filter((race: any) => race.status === 'completed');
|
||||
const scheduledRaces = allRaces.filter((race: any) => race.status === 'scheduled');
|
||||
|
||||
const primaryRace = completedRaces[0] ?? allRaces[0];
|
||||
const secondaryRace = scheduledRaces[0] ?? allRaces[1] ?? primaryRace;
|
||||
const primaryLeague = allLeagues[0];
|
||||
|
||||
let title: string;
|
||||
let body: string;
|
||||
let notificationType: 'protest_filed' | 'protest_defense_requested' | 'protest_vote_required';
|
||||
let actionUrl: string;
|
||||
|
||||
switch (selectedType) {
|
||||
case 'protest_filed':
|
||||
case 'protest_filed': {
|
||||
const raceId = primaryRace?.id;
|
||||
title = '🚨 Protest Filed Against You';
|
||||
body = 'Max Verstappen has filed a protest against you for unsafe rejoining at Turn 3, Lap 12 during the Spa-Francorchamps race.';
|
||||
body =
|
||||
'A protest has been filed against you for unsafe rejoining during a recent race. Please review the incident details.';
|
||||
notificationType = 'protest_filed';
|
||||
actionUrl = '/races/race-1/stewarding';
|
||||
actionUrl = raceId ? `/races/${raceId}/stewarding` : '/races';
|
||||
break;
|
||||
case 'defense_requested':
|
||||
}
|
||||
case 'defense_requested': {
|
||||
const raceId = secondaryRace?.id ?? primaryRace?.id;
|
||||
title = '⚖️ Defense Requested';
|
||||
body = 'A steward has requested your defense regarding the incident at Turn 1 in the Monza race. Please provide your side of the story within 48 hours.';
|
||||
body =
|
||||
'A steward has requested your defense regarding a recent incident. Please provide your side of the story within 48 hours.';
|
||||
notificationType = 'protest_defense_requested';
|
||||
actionUrl = '/races/race-2/stewarding';
|
||||
actionUrl = raceId ? `/races/${raceId}/stewarding` : '/races';
|
||||
break;
|
||||
case 'vote_required':
|
||||
}
|
||||
case 'vote_required': {
|
||||
const leagueId = primaryLeague?.id;
|
||||
title = '🗳️ Your Vote Required';
|
||||
body = 'As a league steward, you are required to vote on the protest: Driver A vs Driver B - Causing a collision at Eau Rouge.';
|
||||
body =
|
||||
'As a league steward, you are required to vote on an open protest. Please review the case and cast your vote.';
|
||||
notificationType = 'protest_vote_required';
|
||||
actionUrl = '/leagues/league-1/stewarding';
|
||||
actionUrl = leagueId ? `/leagues/${leagueId}/stewarding` : '/leagues';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// For modal urgency, add actions
|
||||
const actions = selectedUrgency === 'modal' ? [
|
||||
{ label: 'View Protest', type: 'primary' as const, href: actionUrl, actionId: 'view' },
|
||||
{ label: 'Dismiss', type: 'secondary' as const, actionId: 'dismiss' },
|
||||
] : undefined;
|
||||
const actions =
|
||||
selectedUrgency === 'modal'
|
||||
? [
|
||||
{ label: 'View Protest', type: 'primary' as const, href: actionUrl, actionId: 'view' },
|
||||
{ label: 'Dismiss', type: 'secondary' as const, actionId: 'dismiss' },
|
||||
]
|
||||
: undefined;
|
||||
|
||||
await sendNotification.execute({
|
||||
recipientId: currentDriverId,
|
||||
@@ -214,9 +240,12 @@ export default function DevToolbar() {
|
||||
actions,
|
||||
data: {
|
||||
protestId: `demo-protest-${Date.now()}`,
|
||||
raceId: 'race-1',
|
||||
leagueId: 'league-1',
|
||||
deadline: selectedUrgency === 'modal' ? new Date(Date.now() + 48 * 60 * 60 * 1000) : undefined,
|
||||
raceId: primaryRace?.id,
|
||||
leagueId: primaryLeague?.id,
|
||||
deadline:
|
||||
selectedUrgency === 'modal'
|
||||
? new Date(Date.now() + 48 * 60 * 60 * 1000)
|
||||
: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import DriverRankings from './DriverRankings';
|
||||
import PerformanceMetrics from './PerformanceMetrics';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { getDriverStats, getLeagueRankings, getGetDriverTeamQuery, getAllDriverRankings } from '@/lib/di-container';
|
||||
import { getPrimaryLeagueIdForDriver } from '@/lib/leagueMembership';
|
||||
import type { GetDriverTeamQueryResultDTO } from '@gridpilot/racing/application/dto/TeamCommandAndQueryDTO';
|
||||
|
||||
interface DriverProfileProps {
|
||||
@@ -19,7 +20,10 @@ interface DriverProfileProps {
|
||||
|
||||
export default function DriverProfile({ driver, isOwnProfile = false, onEditClick }: DriverProfileProps) {
|
||||
const driverStats = getDriverStats(driver.id);
|
||||
const leagueRank = getLeagueRankings(driver.id, 'league-1');
|
||||
const primaryLeagueId = getPrimaryLeagueIdForDriver(driver.id);
|
||||
const leagueRank = primaryLeagueId
|
||||
? getLeagueRankings(driver.id, primaryLeagueId)
|
||||
: { rank: 0, totalDrivers: 0, percentile: 0 };
|
||||
const allRankings = getAllDriverRankings();
|
||||
const [teamData, setTeamData] = useState<GetDriverTeamQueryResultDTO | null>(null);
|
||||
|
||||
@@ -53,7 +57,7 @@ export default function DriverProfile({ driver, isOwnProfile = false, onEditClic
|
||||
},
|
||||
{
|
||||
type: 'league' as const,
|
||||
name: 'European GT Championship',
|
||||
name: 'Primary League',
|
||||
rank: leagueRank.rank,
|
||||
totalDrivers: leagueRank.totalDrivers,
|
||||
percentile: leagueRank.percentile,
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import Card from '../ui/Card';
|
||||
import RankBadge from './RankBadge';
|
||||
import { getDriverStats, getAllDriverRankings, getLeagueRankings } from '@/lib/di-container';
|
||||
import { getPrimaryLeagueIdForDriver } from '@/lib/leagueMembership';
|
||||
|
||||
interface ProfileStatsProps {
|
||||
driverId?: string;
|
||||
@@ -19,7 +20,9 @@ interface ProfileStatsProps {
|
||||
export default function ProfileStats({ driverId, stats }: ProfileStatsProps) {
|
||||
const driverStats = driverId ? getDriverStats(driverId) : null;
|
||||
const allRankings = getAllDriverRankings();
|
||||
const leagueRank = driverId ? getLeagueRankings(driverId, 'league-1') : null;
|
||||
const primaryLeagueId = driverId ? getPrimaryLeagueIdForDriver(driverId) : null;
|
||||
const leagueRank =
|
||||
driverId && primaryLeagueId ? getLeagueRankings(driverId, primaryLeagueId) : null;
|
||||
|
||||
const defaultStats = stats || (driverStats
|
||||
? {
|
||||
@@ -115,7 +118,7 @@ export default function ProfileStats({ driverId, stats }: ProfileStatsProps) {
|
||||
<div className="flex items-center gap-3">
|
||||
<RankBadge rank={leagueRank.rank} size="md" />
|
||||
<div>
|
||||
<div className="text-white font-medium">European GT Championship</div>
|
||||
<div className="text-white font-medium">Primary League</div>
|
||||
<div className="text-sm text-gray-400">
|
||||
{leagueRank.rank} of {leagueRank.totalDrivers} drivers
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@ import Card from '@/components/ui/Card';
|
||||
import Button from '@/components/ui/Button';
|
||||
import Image from 'next/image';
|
||||
import type { FeedItem } from '@gridpilot/social/domain/entities/FeedItem';
|
||||
import { friends } from '@gridpilot/testing-support';
|
||||
import { getDriverRepository, getImageService, getSocialRepository } from '@/lib/di-container';
|
||||
|
||||
function timeAgo(timestamp: Date): string {
|
||||
const diffMs = Date.now() - timestamp.getTime();
|
||||
@@ -15,16 +15,39 @@ function timeAgo(timestamp: Date): string {
|
||||
return `${diffDays} d ago`;
|
||||
}
|
||||
|
||||
function getActor(item: FeedItem) {
|
||||
async function resolveActor(item: FeedItem) {
|
||||
const driverRepo = getDriverRepository();
|
||||
const imageService = getImageService();
|
||||
const socialRepo = getSocialRepository();
|
||||
|
||||
if (item.actorFriendId) {
|
||||
const friend = friends.find(f => f.driverId === item.actorFriendId);
|
||||
if (friend) {
|
||||
return {
|
||||
name: friend.displayName,
|
||||
avatarUrl: friend.avatarUrl
|
||||
};
|
||||
// Try social graph first (friend display name/avatar)
|
||||
try {
|
||||
const friend = await socialRepo.getFriendByDriverId?.(item.actorFriendId);
|
||||
if (friend) {
|
||||
return {
|
||||
name: friend.displayName ?? friend.driverName ?? `Driver ${item.actorFriendId}`,
|
||||
avatarUrl: friend.avatarUrl ?? imageService.getDriverAvatar(item.actorFriendId),
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
// fall through to driver lookup
|
||||
}
|
||||
|
||||
// Fallback to driver entity + image service
|
||||
try {
|
||||
const driver = await driverRepo.findById(item.actorFriendId);
|
||||
if (driver) {
|
||||
return {
|
||||
name: driver.name,
|
||||
avatarUrl: imageService.getDriverAvatar(driver.id),
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
// ignore and return null below
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -33,7 +56,22 @@ interface FeedItemCardProps {
|
||||
}
|
||||
|
||||
export default function FeedItemCard({ item }: FeedItemCardProps) {
|
||||
const actor = getActor(item);
|
||||
const [actor, setActor] = useState<{ name: string; avatarUrl: string } | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
void (async () => {
|
||||
const resolved = await resolveActor(item);
|
||||
if (!cancelled) {
|
||||
setActor(resolved);
|
||||
}
|
||||
})();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [item]);
|
||||
|
||||
return (
|
||||
<div className="flex gap-4">
|
||||
@@ -68,7 +106,7 @@ export default function FeedItemCard({ item }: FeedItemCardProps) {
|
||||
{timeAgo(item.timestamp)}
|
||||
</span>
|
||||
</div>
|
||||
{(item.ctaHref && item.ctaLabel) && (
|
||||
{item.ctaHref && item.ctaLabel && (
|
||||
<div className="mt-3">
|
||||
<Button
|
||||
as="a"
|
||||
|
||||
Reference in New Issue
Block a user