refactor page to use services

This commit is contained in:
2025-12-18 15:58:09 +01:00
parent f54fa5de5b
commit fc386db06a
45 changed files with 2254 additions and 1292 deletions

View File

@@ -29,23 +29,15 @@ import {
MessageCircle,
ArrowLeft,
BarChart3,
History,
Shield,
Percent,
Activity,
Megaphone,
DollarSign,
} from 'lucide-react';
import { AllTeamsPresenter } from '@/lib/presenters/AllTeamsPresenter';
import { TeamMembersPresenter } from '@/lib/presenters/TeamMembersPresenter';
import { Driver, EntityMappers, type Team } from '@core/racing';
import type { DriverDTO } from '@core/racing';
import type { ProfileOverviewViewModel } from '@core/racing/application/presenters/IProfileOverviewPresenter';
import Button from '@/components/ui/Button';
import Card from '@/components/ui/Card';
import Breadcrumbs from '@/components/layout/Breadcrumbs';
import type { GetDriverTeamQueryResultDTO } from '@core/racing/application/dtos/GetDriverTeamQueryResultDTO';
import type { TeamMemberViewModel } from '@core/racing/application/presenters/ITeamMembersPresenter';
import { ServiceFactory } from '@/lib/services/ServiceFactory';
import { DriverProfileViewModel } from '@/lib/view-models/DriverProfileViewModel';
// ============================================================================
// TYPES
@@ -53,6 +45,16 @@ import type { TeamMemberViewModel } from '@core/racing/application/presenters/IT
type ProfileTab = 'overview' | 'stats';
interface Team {
id: string;
name: string;
tag: string;
description: string;
ownerId: string;
leagues: unknown[]; // TODO: define proper type
createdAt: Date;
}
interface SocialHandle {
platform: 'twitter' | 'youtube' | 'twitch' | 'discord';
handle: string;
@@ -306,59 +308,19 @@ function HorizontalBarChart({ data, maxValue }: BarChartProps) {
// MAIN PAGE
// ============================================================================
interface DriverProfileStatsViewModel {
rating: number;
wins: number;
podiums: number;
dnfs: number;
totalRaces: number;
avgFinish: number;
bestFinish: number;
worstFinish: number;
consistency: number;
percentile: number;
}
interface DriverProfileFriendViewModel {
id: string;
name: string;
country: string;
}
interface DriverProfileExtendedViewModel extends DriverExtendedProfile {}
interface DriverProfileViewModel {
currentDriver?: {
id: string;
name: string;
iracingId?: string | null;
country: string;
bio?: string | null;
joinedAt: string | Date;
globalRank?: number;
totalDrivers?: number;
};
stats?: DriverProfileStatsViewModel;
extendedProfile?: DriverProfileExtendedViewModel;
socialSummary?: {
friends: DriverProfileFriendViewModel[];
};
}
export default function DriverDetailPage() {
const router = useRouter();
const params = useParams();
const driverId = params.id as string;
const [driver, setDriver] = useState<DriverDTO | null>(null);
const [driverProfile, setDriverProfile] = useState<DriverProfileViewModel | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [activeTab, setActiveTab] = useState<ProfileTab>('overview');
const [teamData, setTeamData] = useState<GetDriverTeamQueryResultDTO | null>(null);
const [allTeamMemberships, setAllTeamMemberships] = useState<TeamMembershipInfo[]>([]);
const [friends, setFriends] = useState<Driver[]>([]);
const [friendRequestSent, setFriendRequestSent] = useState(false);
const [profileData, setProfileData] = useState<ProfileOverviewViewModel | null>(null);
const search =
typeof window !== 'undefined'
@@ -392,56 +354,39 @@ export default function DriverDetailPage() {
const loadDriver = async () => {
try {
// Use GetProfileOverviewUseCase to load all profile data
const profileUseCase = getGetProfileOverviewUseCase();
const profileViewModel = await profileUseCase.execute({ driverId });
// Initialize service factory
const serviceFactory = new ServiceFactory(process.env.NEXT_PUBLIC_API_BASE_URL || '');
const driverService = serviceFactory.createDriverService();
const teamService = serviceFactory.createTeamService();
if (!profileViewModel || !profileViewModel.currentDriver) {
// Get driver profile
const profileViewModel = await driverService.getDriverProfile(driverId);
if (!profileViewModel.currentDriver) {
setError('Driver not found');
setLoading(false);
return;
}
// Set driver from ViewModel
const driverData: DriverDTO = {
id: profileViewModel.currentDriver.id,
name: profileViewModel.currentDriver.name,
iracingId: profileViewModel.currentDriver.iracingId ?? '',
country: profileViewModel.currentDriver.country,
bio: profileViewModel.currentDriver.bio || '',
joinedAt: profileViewModel.currentDriver.joinedAt,
};
setDriver(driverData);
setProfileData(profileViewModel);
setDriverProfile(profileViewModel);
// Load ALL team memberships using caller-owned presenters
const allTeamsUseCase = getGetAllTeamsUseCase();
const allTeamsPresenter = new AllTeamsPresenter();
await allTeamsUseCase.execute(undefined as void, allTeamsPresenter);
const allTeamsViewModel = allTeamsPresenter.getViewModel();
const allTeams = allTeamsViewModel?.teams ?? [];
const membershipsUseCase = getGetTeamMembersUseCase();
// Load team memberships - get all teams and check memberships
const allTeams = await teamService.getAllTeams();
const memberships: TeamMembershipInfo[] = [];
for (const team of allTeams) {
const teamMembersPresenter = new TeamMembersPresenter();
await membershipsUseCase.execute({ teamId: team.id }, teamMembersPresenter);
const membersResult = teamMembersPresenter.getViewModel();
const members = membersResult?.members ?? [];
const membership = members.find(
(member: TeamMemberViewModel) => member.driverId === driverId,
);
const teamMembers = await teamService.getTeamMembers(team.id, driverId, ''); // ownerId not available in summary
const membership = teamMembers.find(member => member.driverId === driverId);
if (membership) {
memberships.push({
team: {
id: team.id,
name: team.name,
tag: team.tag,
description: team.description,
ownerId: '',
leagues: team.leagues,
createdAt: new Date(),
tag: '', // Not available in summary
description: '', // Not available in summary
ownerId: '', // Not available in summary
leagues: [], // TODO: populate if needed
createdAt: new Date(), // TODO: add to API
} as Team,
role: membership.role,
joinedAt: new Date(membership.joinedAt),
@@ -449,16 +394,6 @@ export default function DriverDetailPage() {
}
}
setAllTeamMemberships(memberships);
// Set friends from ViewModel
const friendsList = profileViewModel.socialSummary?.friends.map(f => {
return {
id: f.id,
name: f.name,
country: f.country,
} as Driver;
}) || [];
setFriends(friendsList);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to load driver');
} finally {
@@ -483,7 +418,7 @@ export default function DriverDetailPage() {
);
}
if (error || !driver) {
if (error || !driverProfile?.currentDriver) {
return (
<div className="max-w-4xl mx-auto px-4">
<Card className="text-center py-12">
@@ -497,12 +432,12 @@ export default function DriverDetailPage() {
);
}
const demoExtended = getDemoExtendedProfile(driver.id);
const demoExtended = getDemoExtendedProfile(driverProfile.currentDriver.id);
const extendedProfile: DriverExtendedProfile = {
socialHandles: profileData?.extendedProfile?.socialHandles ?? demoExtended.socialHandles,
socialHandles: driverProfile?.extendedProfile?.socialHandles ?? demoExtended.socialHandles,
achievements:
profileData?.extendedProfile?.achievements
? profileData.extendedProfile.achievements.map((achievement) => ({
driverProfile?.extendedProfile?.achievements
? driverProfile.extendedProfile.achievements.map((achievement) => ({
id: achievement.id,
title: achievement.title,
description: achievement.description,
@@ -511,25 +446,27 @@ export default function DriverDetailPage() {
earnedAt: new Date(achievement.earnedAt),
}))
: demoExtended.achievements,
racingStyle: profileData?.extendedProfile?.racingStyle ?? demoExtended.racingStyle,
favoriteTrack: profileData?.extendedProfile?.favoriteTrack ?? demoExtended.favoriteTrack,
favoriteCar: profileData?.extendedProfile?.favoriteCar ?? demoExtended.favoriteCar,
timezone: profileData?.extendedProfile?.timezone ?? demoExtended.timezone,
availableHours: profileData?.extendedProfile?.availableHours ?? demoExtended.availableHours,
racingStyle: driverProfile?.extendedProfile?.racingStyle ?? demoExtended.racingStyle,
favoriteTrack: driverProfile?.extendedProfile?.favoriteTrack ?? demoExtended.favoriteTrack,
favoriteCar: driverProfile?.extendedProfile?.favoriteCar ?? demoExtended.favoriteCar,
timezone: driverProfile?.extendedProfile?.timezone ?? demoExtended.timezone,
availableHours: driverProfile?.extendedProfile?.availableHours ?? demoExtended.availableHours,
lookingForTeam:
profileData?.extendedProfile?.lookingForTeam ?? demoExtended.lookingForTeam,
driverProfile?.extendedProfile?.lookingForTeam ?? demoExtended.lookingForTeam,
openToRequests:
profileData?.extendedProfile?.openToRequests ?? demoExtended.openToRequests,
driverProfile?.extendedProfile?.openToRequests ?? demoExtended.openToRequests,
};
const stats = profileData?.stats || null;
const globalRank = profileData?.currentDriver?.globalRank || 1;
const stats = driverProfile?.stats || null;
const globalRank = driverProfile?.currentDriver?.globalRank || 1;
const driver = driverProfile.currentDriver;
// Build sponsor insights for driver
const friendsCount = driverProfile?.socialSummary?.friends?.length ?? 0;
const driverMetrics = [
MetricBuilders.rating(stats?.rating ?? 0, 'Driver Rating'),
MetricBuilders.views((friends.length * 8) + 50),
MetricBuilders.views((friendsCount * 8) + 50),
MetricBuilders.engagement(stats?.consistency ?? 75),
MetricBuilders.reach((friends.length * 12) + 100),
MetricBuilders.reach((friendsCount * 12) + 100),
];
return (
@@ -596,7 +533,7 @@ export default function DriverDetailPage() {
<div className="w-28 h-28 md:w-36 md:h-36 rounded-2xl bg-gradient-to-br from-primary-blue to-purple-600 p-1 shadow-xl shadow-primary-blue/20">
<div className="w-full h-full rounded-xl overflow-hidden bg-iron-gray">
<Image
src={getImageService().getDriverAvatar(driver.id)}
src={driver.avatarUrl}
alt={driver.name}
width={144}
height={144}
@@ -613,11 +550,6 @@ export default function DriverDetailPage() {
<span className="text-4xl" aria-label={`Country: ${driver.country}`}>
{getCountryFlag(driver.country)}
</span>
{teamData?.team.tag && (
<span className="px-3 py-1 bg-purple-600/20 text-purple-400 rounded-full text-sm font-semibold border border-purple-600/30">
[{teamData.team.tag}]
</span>
)}
</div>
{/* Rating and Rank */}
@@ -636,16 +568,6 @@ export default function DriverDetailPage() {
</div>
</>
)}
{teamData && (
<Link
href={`/teams/${teamData.team.id}`}
className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-purple-600/10 border border-purple-600/30 hover:bg-purple-600/20 transition-colors"
>
<Users className="w-4 h-4 text-purple-400" />
<span className="text-purple-400 font-medium">{teamData.team.name}</span>
<ChevronRight className="w-3 h-3 text-purple-400" />
</Link>
)}
</div>
{/* Meta info */}
@@ -982,37 +904,37 @@ export default function DriverDetailPage() {
</Card>
{/* Friends Preview */}
{friends.length > 0 && (
{driverProfile.socialSummary.friends.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">
<Users className="w-5 h-5 text-purple-400" />
Friends
<span className="text-sm text-gray-500 font-normal">({friends.length})</span>
<span className="text-sm text-gray-500 font-normal">({driverProfile.socialSummary.friends.length})</span>
</h2>
</div>
<div className="flex flex-wrap gap-3">
{friends.slice(0, 8).map((friend) => (
{driverProfile.socialSummary.friends.slice(0, 8).map((friend) => (
<Link
key={friend.id}
href={`/drivers/${friend.id}`}
className="flex items-center gap-2 px-3 py-2 rounded-lg bg-iron-gray/50 border border-charcoal-outline hover:border-purple-400/30 hover:bg-iron-gray transition-all"
>
<div className="w-8 h-8 rounded-full overflow-hidden bg-gradient-to-br from-primary-blue to-purple-600">
<Image
src={getImageService().getDriverAvatar(friend.id)}
alt={friend.name}
width={32}
height={32}
className="w-full h-full object-cover"
/>
</div>
<Image
src={friend.avatarUrl || '/default-avatar.png'}
alt={friend.name}
width={32}
height={32}
className="w-full h-full object-cover"
/>
</div>
<span className="text-sm text-white">{friend.name}</span>
<span className="text-lg">{getCountryFlag(friend.country)}</span>
</Link>
))}
{friends.length > 8 && (
<div className="flex items-center px-3 py-2 text-sm text-gray-400">+{friends.length - 8} more</div>
{driverProfile.socialSummary.friends.length > 8 && (
<div className="flex items-center px-3 py-2 text-sm text-gray-400">+{driverProfile.socialSummary.friends.length - 8} more</div>
)}
</div>
</Card>