page wrapper

This commit is contained in:
2026-01-07 12:40:52 +01:00
parent e589c30bf8
commit 0db80fa98d
128 changed files with 7386 additions and 8096 deletions

View File

@@ -17,8 +17,10 @@ import type {
} from '@/lib/view-models/DriverProfileViewModel';
import { getMediaUrl } from '@/lib/utilities/media';
// Shared state components
import { StateContainer } from '@/components/shared/state/StateContainer';
// New architecture components
import { StatefulPageWrapper } from '@/components/shared/state/StatefulPageWrapper';
// Icons
import {
Activity,
Award,
@@ -255,30 +257,60 @@ function FinishDistributionChart({ wins, podiums, topTen, total }: FinishDistrib
}
// ============================================================================
// MAIN PAGE COMPONENT
// TEMPLATE COMPONENT
// ============================================================================
export default function ProfilePage() {
interface ProfileTemplateProps {
data: DriverProfileViewModel;
onEdit: () => void;
onAddFriend: () => void;
activeTab: ProfileTab;
setActiveTab: (tab: ProfileTab) => void;
friendRequestSent: boolean;
isOwnProfile: boolean;
handleSaveSettings: (updates: { bio?: string; country?: string }) => Promise<void>;
editMode: boolean;
setEditMode: (edit: boolean) => void;
}
function ProfileTemplate({
data,
onEdit,
onAddFriend,
activeTab,
setActiveTab,
friendRequestSent,
isOwnProfile,
handleSaveSettings,
editMode,
setEditMode
}: ProfileTemplateProps) {
const router = useRouter();
const searchParams = useSearchParams();
const tabParam = searchParams.get('tab') as ProfileTab | null;
const driverService = useInject(DRIVER_SERVICE_TOKEN);
const mediaService = useInject(MEDIA_SERVICE_TOKEN);
// Extract data from ViewModel
const currentDriver = data.currentDriver;
if (!currentDriver) {
return (
<div className="max-w-4xl mx-auto px-4">
<Card className="text-center py-12">
<User className="w-16 h-16 text-gray-600 mx-auto mb-4" />
<p className="text-gray-400 mb-2">No driver profile found</p>
<p className="text-sm text-gray-500">Please create a driver profile to continue</p>
</Card>
</div>
);
}
const effectiveDriverId = useEffectiveDriverId();
const isOwnProfile = true; // This page is always your own profile
// Use React-Query hook for profile data
const { data: profileData, isLoading: loading, error, retry } = useDriverProfile(effectiveDriverId || '');
const [editMode, setEditMode] = useState(false);
const [activeTab, setActiveTab] = useState<ProfileTab>(tabParam || 'overview');
const [friendRequestSent, setFriendRequestSent] = useState(false);
const stats = data.stats;
const teamMemberships = data.teamMemberships;
const socialSummary = data.socialSummary;
const extendedProfile = data.extendedProfile;
const globalRank = currentDriver.globalRank || null;
// Update URL when tab changes
useEffect(() => {
if (tabParam !== activeTab) {
if (searchParams.get('tab') !== activeTab) {
const params = new URLSearchParams(searchParams.toString());
if (activeTab === 'overview') {
params.delete('tab');
@@ -288,62 +320,18 @@ export default function ProfilePage() {
const query = params.toString();
router.replace(`/profile${query ? `?${query}` : ''}`, { scroll: false });
}
}, [activeTab, tabParam, searchParams, router]);
}, [activeTab, searchParams, router]);
// Sync tab from URL on mount and param change
useEffect(() => {
const tabParam = searchParams.get('tab') as ProfileTab | null;
if (tabParam && tabParam !== activeTab) {
setActiveTab(tabParam);
}
}, [tabParam]);
const handleSaveSettings = async (updates: { bio?: string; country?: string }) => {
if (!profileData?.currentDriver) return;
try {
const updatedProfile = await driverService.updateProfile(updates);
// Update local state
retry();
setEditMode(false);
} catch (error) {
console.error('Failed to update profile:', error);
}
};
const handleAddFriend = () => {
setFriendRequestSent(true);
// In production, this would call a use case
};
// Show create form if no profile exists
if (!loading && !profileData?.currentDriver && !error) {
return (
<div className="max-w-4xl mx-auto px-4">
<div className="text-center mb-8">
<div className="flex h-16 w-16 items-center justify-center rounded-2xl bg-gradient-to-br from-primary-blue/20 to-purple-600/10 border border-primary-blue/30 mx-auto mb-4">
<User className="w-8 h-8 text-primary-blue" />
</div>
<Heading level={1} className="mb-2">Create Your Driver Profile</Heading>
<p className="text-gray-400">
Join the GridPilot community and start your racing journey
</p>
</div>
<Card className="max-w-2xl mx-auto">
<div className="mb-6">
<h2 className="text-xl font-semibold text-white mb-2">Get Started</h2>
<p className="text-gray-400 text-sm">
Create your driver profile to join leagues, compete in races, and connect with other drivers.
</p>
</div>
<CreateDriverForm />
</Card>
</div>
);
}
}, [searchParams]);
// Show edit mode
if (editMode && profileData?.currentDriver) {
if (editMode && currentDriver) {
return (
<div className="max-w-4xl mx-auto px-4 space-y-6">
<div className="flex items-center justify-between mb-4">
@@ -352,55 +340,13 @@ export default function ProfilePage() {
Cancel
</Button>
</div>
<ProfileSettings driver={profileData.currentDriver} onSave={handleSaveSettings} />
<ProfileSettings driver={currentDriver} onSave={handleSaveSettings} />
</div>
);
}
return (
<StateContainer
data={profileData}
isLoading={loading}
error={error}
retry={retry}
config={{
loading: { variant: 'full-screen', message: 'Loading profile...' },
error: { variant: 'full-screen' },
empty: {
icon: User,
title: 'No profile data',
description: 'Unable to load your profile information',
action: { label: 'Retry', onClick: retry }
}
}}
>
{(profileData) => {
// Extract data from profileData ViewModel
// At this point, we know profileData exists and currentDriver should exist
// (otherwise we would have shown the create form above)
const currentDriver = profileData.currentDriver;
// If currentDriver is null despite our checks, show empty state
if (!currentDriver) {
return (
<div className="max-w-4xl mx-auto px-4">
<Card className="text-center py-12">
<User className="w-16 h-16 text-gray-600 mx-auto mb-4" />
<p className="text-gray-400 mb-2">No driver profile found</p>
<p className="text-sm text-gray-500">Please create a driver profile to continue</p>
</Card>
</div>
);
}
const stats = profileData.stats;
const teamMemberships = profileData.teamMemberships;
const socialSummary = profileData.socialSummary;
const extendedProfile = profileData.extendedProfile;
const globalRank = currentDriver.globalRank || null;
return (
<div className="max-w-7xl mx-auto px-4 pb-12 space-y-6">
<div className="max-w-7xl mx-auto px-4 pb-12 space-y-6">
{/* Hero Header Section */}
<div className="relative rounded-2xl overflow-hidden bg-gradient-to-br from-iron-gray/80 via-iron-gray/60 to-deep-graphite border border-charcoal-outline">
{/* Background Pattern */}
@@ -497,7 +443,7 @@ export default function ProfilePage() {
{isOwnProfile ? (
<Button
variant="primary"
onClick={() => setEditMode(true)}
onClick={onEdit}
className="flex items-center gap-2"
>
<Edit3 className="w-4 h-4" />
@@ -507,7 +453,7 @@ export default function ProfilePage() {
<>
<Button
variant="primary"
onClick={handleAddFriend}
onClick={onAddFriend}
disabled={friendRequestSent}
className="flex items-center gap-2"
>
@@ -1055,16 +1001,112 @@ export default function ProfilePage() {
</div>
)}
{activeTab === 'stats' && !stats && (
<Card className="text-center py-12">
<BarChart3 className="w-16 h-16 text-gray-600 mx-auto mb-4" />
<p className="text-gray-400 mb-2">No statistics available yet</p>
<p className="text-sm text-gray-500">Join a league and complete races to see detailed stats</p>
</Card>
)}
{activeTab === 'stats' && !stats && (
<Card className="text-center py-12">
<BarChart3 className="w-16 h-16 text-gray-600 mx-auto mb-4" />
<p className="text-gray-400 mb-2">No statistics available yet</p>
<p className="text-sm text-gray-500">Join a league and complete races to see detailed stats</p>
</Card>
)}
</div>
);
}
// ============================================================================
// MAIN PAGE COMPONENT
// ============================================================================
export default function ProfilePage() {
const router = useRouter();
const searchParams = useSearchParams();
const tabParam = searchParams.get('tab') as ProfileTab | null;
const driverService = useInject(DRIVER_SERVICE_TOKEN);
const mediaService = useInject(MEDIA_SERVICE_TOKEN);
const effectiveDriverId = useEffectiveDriverId();
const isOwnProfile = true; // This page is always your own profile
// Use React-Query hook for profile data
const { data: profileData, isLoading: loading, error, refetch } = useDriverProfile(effectiveDriverId || '');
const [editMode, setEditMode] = useState(false);
const [activeTab, setActiveTab] = useState<ProfileTab>(tabParam || 'overview');
const [friendRequestSent, setFriendRequestSent] = useState(false);
const handleSaveSettings = async (updates: { bio?: string; country?: string }) => {
if (!profileData?.currentDriver) return;
try {
const updatedProfile = await driverService.updateProfile(updates);
// Update local state
refetch();
setEditMode(false);
} catch (error) {
console.error('Failed to update profile:', error);
}
};
const handleAddFriend = () => {
setFriendRequestSent(true);
// In production, this would call a use case
};
// Show create form if no profile exists
if (!loading && !profileData?.currentDriver && !error) {
return (
<div className="max-w-4xl mx-auto px-4">
<div className="text-center mb-8">
<div className="flex h-16 w-16 items-center justify-center rounded-2xl bg-gradient-to-br from-primary-blue/20 to-purple-600/10 border border-primary-blue/30 mx-auto mb-4">
<User className="w-8 h-8 text-primary-blue" />
</div>
<Heading level={1} className="mb-2">Create Your Driver Profile</Heading>
<p className="text-gray-400">
Join the GridPilot community and start your racing journey
</p>
</div>
);
}}
</StateContainer>
);
<Card className="max-w-2xl mx-auto">
<div className="mb-6">
<h2 className="text-xl font-semibold text-white mb-2">Get Started</h2>
<p className="text-gray-400 text-sm">
Create your driver profile to join leagues, compete in races, and connect with other drivers.
</p>
</div>
<CreateDriverForm />
</Card>
</div>
);
}
return (
<StatefulPageWrapper
data={profileData}
isLoading={loading}
error={error}
retry={refetch}
Template={({ data }) => (
<ProfileTemplate
data={data}
onEdit={() => setEditMode(true)}
onAddFriend={handleAddFriend}
activeTab={activeTab}
setActiveTab={setActiveTab}
friendRequestSent={friendRequestSent}
isOwnProfile={isOwnProfile}
handleSaveSettings={handleSaveSettings}
editMode={editMode}
setEditMode={setEditMode}
/>
)}
loading={{ variant: 'full-screen', message: 'Loading profile...' }}
errorConfig={{ variant: 'full-screen' }}
empty={{
icon: User,
title: 'No profile data',
description: 'Unable to load your profile information',
action: { label: 'Retry', onClick: refetch }
}}
/>
);
}