wip
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user