refactor page to use services
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user