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

@@ -35,16 +35,21 @@ import {
Activity,
} from 'lucide-react';
import {
getDriverRepository,
getDriverStats,
getAllDriverRankings,
getGetDriverTeamUseCase,
getSocialRepository,
getGetProfileOverviewUseCase,
getImageService,
getGetAllTeamsUseCase,
getGetTeamMembersUseCase,
getUpdateDriverProfileUseCase,
} from '@/lib/di-container';
import { Driver, EntityMappers, type DriverDTO, type Team } from '@gridpilot/racing';
import type { DriverDTO } from '@gridpilot/racing/application/dto/DriverDTO';
import type {
ProfileOverviewViewModel,
ProfileOverviewDriverSummaryViewModel,
ProfileOverviewStatsViewModel,
ProfileOverviewTeamMembershipViewModel,
ProfileOverviewSocialSummaryViewModel,
ProfileOverviewExtendedProfileViewModel,
ProfileOverviewAchievementViewModel,
ProfileOverviewSocialHandleViewModel,
} from '@gridpilot/racing/application/presenters/IProfileOverviewPresenter';
import CreateDriverForm from '@/components/drivers/CreateDriverForm';
import Card from '@/components/ui/Card';
import Button from '@/components/ui/Button';
@@ -52,7 +57,6 @@ import Heading from '@/components/ui/Heading';
import ProfileRaceHistory from '@/components/drivers/ProfileRaceHistory';
import ProfileSettings from '@/components/drivers/ProfileSettings';
import { useEffectiveDriverId } from '@/lib/currentDriver';
import type { GetDriverTeamQueryResultDTO } from '@gridpilot/racing/application/dto/TeamCommandAndQueryDTO';
// ============================================================================
// TYPES
@@ -60,100 +64,6 @@ import type { GetDriverTeamQueryResultDTO } from '@gridpilot/racing/application/
type ProfileTab = 'overview' | 'history' | 'stats';
interface SocialHandle {
platform: 'twitter' | 'youtube' | 'twitch' | 'discord';
handle: string;
url: string;
}
interface Achievement {
id: string;
title: string;
description: string;
icon: 'trophy' | 'medal' | 'star' | 'crown' | 'target' | 'zap';
rarity: 'common' | 'rare' | 'epic' | 'legendary';
earnedAt: Date;
}
interface DriverExtendedProfile {
socialHandles: SocialHandle[];
achievements: Achievement[];
racingStyle: string;
favoriteTrack: string;
favoriteCar: string;
timezone: string;
availableHours: string;
lookingForTeam: boolean;
openToRequests: boolean;
}
interface TeamMembershipInfo {
team: Team;
role: string;
joinedAt: Date;
}
// ============================================================================
// DEMO DATA (Extended profile info not in domain yet)
// ============================================================================
function getDemoExtendedProfile(driverId: string): DriverExtendedProfile {
// Demo social handles based on driver id hash
const hash = driverId.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0);
const socialOptions: SocialHandle[][] = [
[
{ platform: 'twitter', handle: '@speedracer', url: 'https://twitter.com/speedracer' },
{ platform: 'youtube', handle: 'SpeedRacer Racing', url: 'https://youtube.com/@speedracer' },
{ platform: 'twitch', handle: 'speedracer_live', url: 'https://twitch.tv/speedracer_live' },
],
[
{ platform: 'twitter', handle: '@racingpro', url: 'https://twitter.com/racingpro' },
{ platform: 'discord', handle: 'RacingPro#1234', url: '#' },
],
[
{ platform: 'twitch', handle: 'simracer_elite', url: 'https://twitch.tv/simracer_elite' },
{ platform: 'youtube', handle: 'SimRacer Elite', url: 'https://youtube.com/@simracerelite' },
],
];
const achievementSets: Achievement[][] = [
[
{ id: '1', title: 'First Victory', description: 'Win your first race', icon: 'trophy', rarity: 'common', earnedAt: new Date(Date.now() - 90 * 24 * 60 * 60 * 1000) },
{ id: '2', title: 'Clean Racer', description: '10 races without incidents', icon: 'star', rarity: 'rare', earnedAt: new Date(Date.now() - 60 * 24 * 60 * 60 * 1000) },
{ id: '3', title: 'Podium Streak', description: '5 consecutive podium finishes', icon: 'medal', rarity: 'epic', earnedAt: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000) },
{ id: '4', title: 'Championship Glory', description: 'Win a league championship', icon: 'crown', rarity: 'legendary', earnedAt: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) },
],
[
{ id: '1', title: 'Rookie No More', description: 'Complete 25 races', icon: 'target', rarity: 'common', earnedAt: new Date(Date.now() - 120 * 24 * 60 * 60 * 1000) },
{ id: '2', title: 'Consistent Performer', description: 'Maintain 80%+ consistency rating', icon: 'zap', rarity: 'rare', earnedAt: new Date(Date.now() - 45 * 24 * 60 * 60 * 1000) },
{ id: '3', title: 'Endurance Master', description: 'Complete a 24-hour race', icon: 'star', rarity: 'epic', earnedAt: new Date(Date.now() - 15 * 24 * 60 * 60 * 1000) },
],
[
{ id: '1', title: 'Welcome Racer', description: 'Join GridPilot', icon: 'star', rarity: 'common', earnedAt: new Date(Date.now() - 180 * 24 * 60 * 60 * 1000) },
{ id: '2', title: 'Team Player', description: 'Join a racing team', icon: 'medal', rarity: 'rare', earnedAt: new Date(Date.now() - 80 * 24 * 60 * 60 * 1000) },
],
];
const tracks = ['Spa-Francorchamps', 'Nürburgring Nordschleife', 'Suzuka', 'Monza', 'Interlagos', 'Silverstone'];
const cars = ['Porsche 911 GT3 R', 'Ferrari 488 GT3', 'Mercedes-AMG GT3', 'BMW M4 GT3', 'Audi R8 LMS'];
const styles = ['Aggressive Overtaker', 'Consistent Pacer', 'Strategic Calculator', 'Late Braker', 'Smooth Operator'];
const timezones = ['EST (UTC-5)', 'CET (UTC+1)', 'PST (UTC-8)', 'GMT (UTC+0)', 'JST (UTC+9)'];
const hours = ['Evenings (18:00-23:00)', 'Weekends only', 'Late nights (22:00-02:00)', 'Flexible schedule'];
return {
socialHandles: socialOptions[hash % socialOptions.length],
achievements: achievementSets[hash % achievementSets.length],
racingStyle: styles[hash % styles.length],
favoriteTrack: tracks[hash % tracks.length],
favoriteCar: cars[hash % cars.length],
timezone: timezones[hash % timezones.length],
availableHours: hours[hash % hours.length],
lookingForTeam: hash % 3 === 0,
openToRequests: hash % 2 === 0,
};
}
// ============================================================================
// HELPER COMPONENTS
// ============================================================================
@@ -167,7 +77,7 @@ function getCountryFlag(countryCode: string): string {
return '🏁';
}
function getRarityColor(rarity: Achievement['rarity']) {
function getRarityColor(rarity: ProfileOverviewAchievementViewModel['rarity']) {
switch (rarity) {
case 'common':
return 'text-gray-400 bg-gray-400/10 border-gray-400/30';
@@ -180,7 +90,7 @@ function getRarityColor(rarity: Achievement['rarity']) {
}
}
function getAchievementIcon(icon: Achievement['icon']) {
function getAchievementIcon(icon: ProfileOverviewAchievementViewModel['icon']) {
switch (icon) {
case 'trophy':
return Trophy;
@@ -197,7 +107,7 @@ function getAchievementIcon(icon: Achievement['icon']) {
}
}
function getSocialIcon(platform: SocialHandle['platform']) {
function getSocialIcon(platform: ProfileOverviewSocialHandleViewModel['platform']) {
switch (platform) {
case 'twitter':
return Twitter;
@@ -210,7 +120,7 @@ function getSocialIcon(platform: SocialHandle['platform']) {
}
}
function getSocialColor(platform: SocialHandle['platform']) {
function getSocialColor(platform: ProfileOverviewSocialHandleViewModel['platform']) {
switch (platform) {
case 'twitter':
return 'hover:text-sky-400 hover:bg-sky-400/10';
@@ -361,9 +271,7 @@ export default function ProfilePage() {
const [loading, setLoading] = useState(true);
const [editMode, setEditMode] = useState(false);
const [activeTab, setActiveTab] = useState<ProfileTab>(tabParam || 'overview');
const [teamData, setTeamData] = useState<GetDriverTeamQueryResultDTO | null>(null);
const [allTeamMemberships, setAllTeamMemberships] = useState<TeamMembershipInfo[]>([]);
const [friends, setFriends] = useState<Driver[]>([]);
const [profileData, setProfileData] = useState<ProfileOverviewViewModel | null>(null);
const [friendRequestSent, setFriendRequestSent] = useState(false);
const effectiveDriverId = useEffectiveDriverId();
@@ -372,47 +280,25 @@ export default function ProfilePage() {
useEffect(() => {
const loadData = async () => {
try {
const driverRepo = getDriverRepository();
const currentDriverId = effectiveDriverId;
const currentDriver = await driverRepo.findById(currentDriverId);
if (currentDriver) {
const driverData = EntityMappers.toDriverDTO(currentDriver);
// Use GetProfileOverviewUseCase to load all profile data
const profileUseCase = getGetProfileOverviewUseCase();
await profileUseCase.execute({ driverId: currentDriverId });
const profileViewModel = profileUseCase.presenter.getViewModel();
if (profileViewModel && profileViewModel.currentDriver) {
// Set driver from ViewModel instead of direct repository access
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);
// Load primary team data
const teamUseCase = getGetDriverTeamUseCase();
await teamUseCase.execute({ driverId: currentDriverId });
const teamViewModel = teamUseCase.presenter.getViewModel();
setTeamData(teamViewModel.result);
// Load ALL team memberships
const allTeamsUseCase = getGetAllTeamsUseCase();
await allTeamsUseCase.execute();
const allTeamsViewModel = allTeamsUseCase.presenter.getViewModel();
const allTeams = allTeamsViewModel.teams;
const membershipsUseCase = getGetTeamMembersUseCase();
const memberships: TeamMembershipInfo[] = [];
for (const team of allTeams) {
await membershipsUseCase.execute({ teamId: team.id });
const membersViewModel = membershipsUseCase.presenter.getViewModel();
const members = membersViewModel.members;
const membership = members.find((m) => m.driverId === currentDriverId);
if (membership) {
memberships.push({
team,
role: membership.role,
joinedAt: membership.joinedAt,
});
}
}
setAllTeamMemberships(memberships);
// Load friends
const socialRepo = getSocialRepository();
const friendsList = await socialRepo.getFriends(currentDriverId);
setFriends(friendsList);
setProfileData(profileViewModel);
}
} catch (error) {
console.error('Failed to load profile:', error);
@@ -447,18 +333,20 @@ export default function ProfilePage() {
const handleSaveSettings = async (updates: Partial<DriverDTO>) => {
if (!driver) return;
const driverRepo = getDriverRepository();
const currentDriver = await driverRepo.findById(driver.id);
if (currentDriver) {
const updatedDriver: Driver = currentDriver.update({
bio: updates.bio ?? currentDriver.bio,
country: updates.country ?? currentDriver.country,
try {
const updateProfileUseCase = getUpdateDriverProfileUseCase();
const updatedDto = await updateProfileUseCase.execute({
driverId: driver.id,
bio: updates.bio,
country: updates.country,
});
const persistedDriver = await driverRepo.update(updatedDriver);
const updatedDto = EntityMappers.toDriverDTO(persistedDriver);
setDriver(updatedDto);
setEditMode(false);
if (updatedDto) {
setDriver(updatedDto);
setEditMode(false);
}
} catch (error) {
console.error('Failed to update profile:', error);
}
};
@@ -506,11 +394,14 @@ export default function ProfilePage() {
);
}
// Get extended profile data
const extendedProfile = getDemoExtendedProfile(driver.id);
const stats = getDriverStats(driver.id);
const allRankings = getAllDriverRankings();
const globalRank = stats?.overallRank ?? allRankings.findIndex(r => r.driverId === driver.id) + 1;
// Extract data from profileData ViewModel
const currentDriver = profileData?.currentDriver || null;
const stats = profileData?.stats || null;
const finishDistribution = profileData?.finishDistribution || null;
const teamMemberships = profileData?.teamMemberships || [];
const socialSummary = profileData?.socialSummary || { friendsCount: 0, friends: [] };
const extendedProfile = profileData?.extendedProfile;
const globalRank = currentDriver?.globalRank || null;
// Show edit mode
if (editMode) {
@@ -564,9 +455,9 @@ export default function ProfilePage() {
<span className="text-4xl" aria-label={`Country: ${driver.country}`}>
{getCountryFlag(driver.country)}
</span>
{teamData?.team.tag && (
{teamMemberships.length > 0 && teamMemberships[0] && (
<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}]
[{teamMemberships[0].teamTag || 'TEAM'}]
</span>
)}
</div>
@@ -587,13 +478,13 @@ export default function ProfilePage() {
</div>
</>
)}
{teamData && (
{teamMemberships.length > 0 && teamMemberships[0] && (
<Link
href={`/teams/${teamData.team.id}`}
href={`/teams/${teamMemberships[0].teamId}`}
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>
<span className="text-purple-400 font-medium">{teamMemberships[0].teamName}</span>
<ChevronRight className="w-3 h-3 text-purple-400" />
</Link>
)}
@@ -609,10 +500,12 @@ export default function ProfilePage() {
<Calendar className="w-4 h-4" />
Joined {new Date(driver.joinedAt).toLocaleDateString('en-US', { month: 'short', year: 'numeric' })}
</span>
<span className="flex items-center gap-1.5">
<Clock className="w-4 h-4" />
{extendedProfile.timezone}
</span>
{extendedProfile && (
<span className="flex items-center gap-1.5">
<Clock className="w-4 h-4" />
{extendedProfile.timezone}
</span>
)}
</div>
</div>
@@ -650,7 +543,7 @@ export default function ProfilePage() {
</div>
{/* Social Handles */}
{extendedProfile.socialHandles.length > 0 && (
{extendedProfile && extendedProfile.socialHandles.length > 0 && (
<div className="mt-6 pt-6 border-t border-charcoal-outline/50">
<div className="flex flex-wrap items-center gap-2">
<span className="text-sm text-gray-500 mr-2">Connect:</span>
@@ -688,18 +581,18 @@ export default function ProfilePage() {
)}
{/* Team Memberships */}
{allTeamMemberships.length > 0 && (
{teamMemberships.length > 0 && (
<Card>
<h2 className="text-lg font-semibold text-white mb-4 flex items-center gap-2">
<Shield className="w-5 h-5 text-purple-400" />
Team Memberships
<span className="text-sm text-gray-500 font-normal">({allTeamMemberships.length})</span>
<span className="text-sm text-gray-500 font-normal">({teamMemberships.length})</span>
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{allTeamMemberships.map((membership) => (
{teamMemberships.map((membership) => (
<Link
key={membership.team.id}
href={`/teams/${membership.team.id}`}
key={membership.teamId}
href={`/teams/${membership.teamId}`}
className="flex items-center gap-4 p-4 rounded-xl bg-iron-gray/30 border border-charcoal-outline hover:border-purple-400/30 hover:bg-iron-gray/50 transition-all group"
>
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-purple-600/20 border border-purple-600/30">
@@ -707,14 +600,14 @@ export default function ProfilePage() {
</div>
<div className="flex-1 min-w-0">
<p className="text-white font-semibold truncate group-hover:text-purple-400 transition-colors">
{membership.team.name}
{membership.teamName}
</p>
<div className="flex items-center gap-2 text-xs text-gray-400">
<span className="px-2 py-0.5 rounded-full bg-purple-600/20 text-purple-400 capitalize">
{membership.role}
</span>
<span>
Since {membership.joinedAt.toLocaleDateString('en-US', { month: 'short', year: 'numeric' })}
Since {new Date(membership.joinedAt).toLocaleDateString('en-US', { month: 'short', year: 'numeric' })}
</span>
</div>
</div>
@@ -889,82 +782,87 @@ export default function ProfilePage() {
</h2>
<div className="space-y-4 relative">
{/* Racing Style - Featured */}
<div className="p-4 rounded-xl bg-gradient-to-r from-neon-aqua/10 to-transparent border border-neon-aqua/20">
<div className="flex items-center gap-3">
<Zap className="w-5 h-5 text-neon-aqua" />
<div>
<span className="text-xs text-gray-500 uppercase tracking-wider block">Racing Style</span>
<p className="text-white font-semibold text-lg">{extendedProfile.racingStyle}</p>
{extendedProfile && (
<>
{/* Racing Style - Featured */}
<div className="p-4 rounded-xl bg-gradient-to-r from-neon-aqua/10 to-transparent border border-neon-aqua/20">
<div className="flex items-center gap-3">
<Zap className="w-5 h-5 text-neon-aqua" />
<div>
<span className="text-xs text-gray-500 uppercase tracking-wider block">Racing Style</span>
<p className="text-white font-semibold text-lg">{extendedProfile.racingStyle}</p>
</div>
</div>
</div>
</div>
</div>
{/* Track & Car Grid */}
<div className="grid grid-cols-2 gap-3">
<div className="p-3 rounded-lg bg-iron-gray/30 border border-charcoal-outline">
<div className="flex items-center gap-2 mb-1">
<Flag className="w-3.5 h-3.5 text-red-400" />
<span className="text-[10px] text-gray-500 uppercase tracking-wider">Track</span>
{/* Track & Car Grid */}
<div className="grid grid-cols-2 gap-3">
<div className="p-3 rounded-lg bg-iron-gray/30 border border-charcoal-outline">
<div className="flex items-center gap-2 mb-1">
<Flag className="w-3.5 h-3.5 text-red-400" />
<span className="text-[10px] text-gray-500 uppercase tracking-wider">Track</span>
</div>
<p className="text-white font-medium text-sm truncate">{extendedProfile.favoriteTrack}</p>
</div>
<div className="p-3 rounded-lg bg-iron-gray/30 border border-charcoal-outline">
<div className="flex items-center gap-2 mb-1">
<Target className="w-3.5 h-3.5 text-primary-blue" />
<span className="text-[10px] text-gray-500 uppercase tracking-wider">Car</span>
</div>
<p className="text-white font-medium text-sm truncate">{extendedProfile.favoriteCar}</p>
</div>
</div>
<p className="text-white font-medium text-sm truncate">{extendedProfile.favoriteTrack}</p>
</div>
<div className="p-3 rounded-lg bg-iron-gray/30 border border-charcoal-outline">
<div className="flex items-center gap-2 mb-1">
<Target className="w-3.5 h-3.5 text-primary-blue" />
<span className="text-[10px] text-gray-500 uppercase tracking-wider">Car</span>
{/* Availability */}
<div className="flex items-center gap-3 p-3 rounded-lg bg-iron-gray/30 border border-charcoal-outline">
<Clock className="w-4 h-4 text-warning-amber" />
<div>
<span className="text-[10px] text-gray-500 uppercase tracking-wider block">Available</span>
<p className="text-white font-medium text-sm">{extendedProfile.availableHours}</p>
</div>
</div>
<p className="text-white font-medium text-sm truncate">{extendedProfile.favoriteCar}</p>
</div>
</div>
{/* Availability */}
<div className="flex items-center gap-3 p-3 rounded-lg bg-iron-gray/30 border border-charcoal-outline">
<Clock className="w-4 h-4 text-warning-amber" />
<div>
<span className="text-[10px] text-gray-500 uppercase tracking-wider block">Available</span>
<p className="text-white font-medium text-sm">{extendedProfile.availableHours}</p>
</div>
</div>
{/* Status badges */}
<div className="pt-4 border-t border-charcoal-outline/50 space-y-2">
{extendedProfile.lookingForTeam && (
<div className="flex items-center gap-2 px-4 py-3 rounded-xl bg-gradient-to-r from-performance-green/20 to-performance-green/5 border border-performance-green/30">
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-performance-green/20">
<Users className="w-4 h-4 text-performance-green" />
</div>
<div>
<span className="text-sm text-performance-green font-semibold block">Looking for Team</span>
<span className="text-xs text-gray-500">Open to recruitment offers</span>
</div>
{/* Status badges */}
<div className="pt-4 border-t border-charcoal-outline/50 space-y-2">
{extendedProfile.lookingForTeam && (
<div className="flex items-center gap-2 px-4 py-3 rounded-xl bg-gradient-to-r from-performance-green/20 to-performance-green/5 border border-performance-green/30">
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-performance-green/20">
<Users className="w-4 h-4 text-performance-green" />
</div>
<div>
<span className="text-sm text-performance-green font-semibold block">Looking for Team</span>
<span className="text-xs text-gray-500">Open to recruitment offers</span>
</div>
</div>
)}
{extendedProfile.openToRequests && (
<div className="flex items-center gap-2 px-4 py-3 rounded-xl bg-gradient-to-r from-primary-blue/20 to-primary-blue/5 border border-primary-blue/30">
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-primary-blue/20">
<UserPlus className="w-4 h-4 text-primary-blue" />
</div>
<div>
<span className="text-sm text-primary-blue font-semibold block">Open to Requests</span>
<span className="text-xs text-gray-500">Accepting friend invites</span>
</div>
</div>
)}
</div>
)}
{extendedProfile.openToRequests && (
<div className="flex items-center gap-2 px-4 py-3 rounded-xl bg-gradient-to-r from-primary-blue/20 to-primary-blue/5 border border-primary-blue/30">
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-primary-blue/20">
<UserPlus className="w-4 h-4 text-primary-blue" />
</div>
<div>
<span className="text-sm text-primary-blue font-semibold block">Open to Requests</span>
<span className="text-xs text-gray-500">Accepting friend invites</span>
</div>
</div>
)}
</div>
</>
)}
</div>
</Card>
</div>
{/* Achievements */}
<Card>
<h2 className="text-lg font-semibold text-white mb-4 flex items-center gap-2">
<Award className="w-5 h-5 text-yellow-400" />
Achievements
<span className="ml-auto text-sm text-gray-500">{extendedProfile.achievements.length} earned</span>
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{extendedProfile.achievements.map((achievement) => {
{extendedProfile && extendedProfile.achievements.length > 0 && (
<Card>
<h2 className="text-lg font-semibold text-white mb-4 flex items-center gap-2">
<Award className="w-5 h-5 text-yellow-400" />
Achievements
<span className="ml-auto text-sm text-gray-500">{extendedProfile.achievements.length} earned</span>
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{extendedProfile.achievements.map((achievement) => {
const Icon = getAchievementIcon(achievement.icon);
const rarityClasses = getRarityColor(achievement.rarity);
return (
@@ -980,7 +878,7 @@ export default function ProfilePage() {
<p className="text-white font-semibold text-sm">{achievement.title}</p>
<p className="text-gray-400 text-xs mt-0.5">{achievement.description}</p>
<p className="text-gray-500 text-xs mt-1">
{achievement.earnedAt.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })}
{new Date(achievement.earnedAt).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })}
</p>
</div>
</div>
@@ -991,17 +889,17 @@ export default function ProfilePage() {
</Card>
{/* Friends Preview */}
{friends.length > 0 && (
{socialSummary && 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">({socialSummary.friendsCount})</span>
</h2>
</div>
<div className="flex flex-wrap gap-3">
{friends.slice(0, 8).map((friend) => (
{socialSummary.friends.slice(0, 8).map((friend) => (
<Link
key={friend.id}
href={`/drivers/${friend.id}`}
@@ -1020,9 +918,9 @@ export default function ProfilePage() {
<span className="text-lg">{getCountryFlag(friend.country)}</span>
</Link>
))}
{friends.length > 8 && (
{socialSummary.friendsCount > 8 && (
<div className="flex items-center px-3 py-2 text-sm text-gray-400">
+{friends.length - 8} more
+{socialSummary.friendsCount - 8} more
</div>
)}
</div>