website refactor
This commit is contained in:
@@ -2,23 +2,21 @@
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { ProfileTemplate, type ProfileTab } from '@/templates/ProfileTemplate';
|
||||
import { ProfileTemplate } from '@/templates/ProfileTemplate';
|
||||
import { type ProfileTab } from '@/components/profile/ProfileNavTabs';
|
||||
import type { ProfileViewData } from '@/lib/view-data/ProfileViewData';
|
||||
import type { Result } from '@/lib/contracts/Result';
|
||||
|
||||
interface ProfilePageClientProps {
|
||||
viewData: ProfileViewData;
|
||||
mode: 'profile-exists' | 'needs-profile';
|
||||
onSaveSettings: (updates: { bio?: string; country?: string }) => Promise<Result<void, string>>;
|
||||
}
|
||||
|
||||
export function ProfilePageClient({ viewData, mode, onSaveSettings }: ProfilePageClientProps) {
|
||||
export function ProfilePageClient({ viewData, mode }: ProfilePageClientProps) {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const tabParam = searchParams.get('tab') as ProfileTab | null;
|
||||
|
||||
const [activeTab, setActiveTab] = useState<ProfileTab>(tabParam || 'overview');
|
||||
const [editMode, setEditMode] = useState(false);
|
||||
const [friendRequestSent, setFriendRequestSent] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -49,19 +47,8 @@ export function ProfilePageClient({ viewData, mode, onSaveSettings }: ProfilePag
|
||||
mode={mode}
|
||||
activeTab={activeTab}
|
||||
onTabChange={setActiveTab}
|
||||
editMode={editMode}
|
||||
onEditModeChange={setEditMode}
|
||||
friendRequestSent={friendRequestSent}
|
||||
onFriendRequestSend={() => setFriendRequestSent(true)}
|
||||
onSaveSettings={async (updates) => {
|
||||
const result = await onSaveSettings(updates);
|
||||
if (result.isErr()) {
|
||||
// In a real app, we'd show a toast or error message.
|
||||
// For now, we just throw to let the UI handle it if needed,
|
||||
// or we could add an error state to this client component.
|
||||
throw new Error(result.getError());
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
'use server';
|
||||
|
||||
import { revalidatePath } from 'next/cache';
|
||||
import { Result } from '@/lib/contracts/Result';
|
||||
import { routes } from '@/lib/routing/RouteConfig';
|
||||
import { UpdateDriverProfileMutation } from '@/lib/mutations/drivers/UpdateDriverProfileMutation';
|
||||
|
||||
export async function updateProfileAction(
|
||||
updates: { bio?: string; country?: string },
|
||||
): Promise<Result<void, string>> {
|
||||
const mutation = new UpdateDriverProfileMutation();
|
||||
const result = await mutation.execute({ bio: updates.bio, country: updates.country });
|
||||
|
||||
if (result.isErr()) {
|
||||
return Result.err(result.getError());
|
||||
}
|
||||
|
||||
revalidatePath(routes.protected.profile);
|
||||
return Result.ok(undefined);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { headers } from 'next/headers';
|
||||
import { redirect } from 'next/navigation';
|
||||
import { ProfileLayoutShell } from '@/ui/ProfileLayoutShell';
|
||||
import { ProfileLayoutShellTemplate } from '@/templates/ProfileLayoutShellTemplate';
|
||||
import { createRouteGuard } from '@/lib/auth/createRouteGuard';
|
||||
|
||||
interface ProfileLayoutProps {
|
||||
@@ -18,5 +18,5 @@ export default async function ProfileLayout({ children }: ProfileLayoutProps) {
|
||||
redirect(result.to);
|
||||
}
|
||||
|
||||
return <ProfileLayoutShell>{children}</ProfileLayoutShell>;
|
||||
return <ProfileLayoutShellTemplate viewData={{}}>{children}</ProfileLayoutShellTemplate>;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,4 @@
|
||||
import Link from 'next/link';
|
||||
import { Button } from '@/ui/Button';
|
||||
import { Container } from '@/ui/Container';
|
||||
import { Heading } from '@/ui/Heading';
|
||||
import { Grid } from '@/ui/Grid';
|
||||
import { routes } from '@/lib/routing/RouteConfig';
|
||||
import { LiveryCard } from '@/ui/LiveryCard';
|
||||
import { Stack } from '@/ui/Stack';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { ProfileLiveriesTemplate } from '@/templates/ProfileLiveriesTemplate';
|
||||
|
||||
export default async function ProfileLiveriesPage() {
|
||||
const mockLiveries = [
|
||||
@@ -29,29 +20,5 @@ export default async function ProfileLiveriesPage() {
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<Container size="lg" py={8}>
|
||||
<Stack direction="row" align="center" justify="between" mb={8}>
|
||||
<Box>
|
||||
<Heading level={1}>My Liveries</Heading>
|
||||
<Text color="text-gray-400" mt={1} block>Manage your custom car liveries</Text>
|
||||
</Box>
|
||||
<Link href={routes.protected.profileLiveryUpload}>
|
||||
<Button variant="primary">Upload livery</Button>
|
||||
</Link>
|
||||
</Stack>
|
||||
|
||||
<Grid cols={3} gap={6}>
|
||||
{mockLiveries.map((livery) => (
|
||||
<LiveryCard key={livery.id} livery={livery} />
|
||||
))}
|
||||
</Grid>
|
||||
|
||||
<Box mt={12}>
|
||||
<Link href={routes.protected.profile}>
|
||||
<Button variant="secondary">Back to profile</Button>
|
||||
</Link>
|
||||
</Box>
|
||||
</Container>
|
||||
);
|
||||
return <ProfileLiveriesTemplate viewData={{ liveries: mockLiveries }} />;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { Button } from '@/ui/Button';
|
||||
import { Card } from '@/ui/Card';
|
||||
import { Container } from '@/ui/Container';
|
||||
import { Heading } from '@/ui/Heading';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { Box } from '@/ui/Box';
|
||||
import { routes } from '@/lib/routing/RouteConfig';
|
||||
import { UploadDropzone } from '@/components/media/UploadDropzone';
|
||||
import { MediaPreviewCard } from '@/ui/MediaPreviewCard';
|
||||
import { MediaMetaPanel, mapMediaMetadata } from '@/ui/MediaMetaPanel';
|
||||
|
||||
export function ProfileLiveryUploadPageClient() {
|
||||
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
||||
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
|
||||
const handleFilesSelected = (files: File[]) => {
|
||||
if (files.length > 0) {
|
||||
const file = files[0];
|
||||
setSelectedFile(file);
|
||||
const url = URL.createObjectURL(file);
|
||||
setPreviewUrl(url);
|
||||
} else {
|
||||
setSelectedFile(null);
|
||||
setPreviewUrl(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpload = async () => {
|
||||
if (!selectedFile) return;
|
||||
setIsUploading(true);
|
||||
// Mock upload delay
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
setIsUploading(false);
|
||||
alert('Livery uploaded successfully! (Mock)');
|
||||
};
|
||||
|
||||
return (
|
||||
<Container size="md">
|
||||
<Box mb={6}>
|
||||
<Heading level={1}>Upload livery</Heading>
|
||||
<Text color="text-gray-500">
|
||||
Upload your custom car livery. Supported formats: .png, .jpg, .tga
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Box display="grid" responsiveGridCols={{ base: 1, md: 2 }} gap={6}>
|
||||
<Box>
|
||||
<Card>
|
||||
<UploadDropzone
|
||||
onFilesSelected={handleFilesSelected}
|
||||
accept=".png,.jpg,.jpeg,.tga"
|
||||
maxSize={10 * 1024 * 1024} // 10MB
|
||||
isLoading={isUploading}
|
||||
/>
|
||||
|
||||
<Box mt={6} display="flex" justifyContent="end" gap={3}>
|
||||
<Link href={routes.protected.profileLiveries}>
|
||||
<Button variant="ghost">Cancel</Button>
|
||||
</Link>
|
||||
<Button
|
||||
variant="primary"
|
||||
disabled={!selectedFile || isUploading}
|
||||
onClick={handleUpload}
|
||||
isLoading={isUploading}
|
||||
>
|
||||
Upload Livery
|
||||
</Button>
|
||||
</Box>
|
||||
</Card>
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
{previewUrl ? (
|
||||
<Box display="flex" flexDirection="col" gap={6}>
|
||||
<MediaPreviewCard
|
||||
src={previewUrl}
|
||||
title={selectedFile?.name}
|
||||
subtitle="Preview"
|
||||
aspectRatio="16/9"
|
||||
/>
|
||||
|
||||
<MediaMetaPanel
|
||||
items={mapMediaMetadata({
|
||||
filename: selectedFile?.name,
|
||||
size: selectedFile?.size,
|
||||
contentType: selectedFile?.type || 'image/tga',
|
||||
createdAt: new Date(),
|
||||
})}
|
||||
/>
|
||||
</Box>
|
||||
) : (
|
||||
<Card center p={12}>
|
||||
<Text color="text-gray-500" align="center">
|
||||
Select a file to see preview and details
|
||||
</Text>
|
||||
</Card>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@@ -1,21 +1,6 @@
|
||||
import Link from 'next/link';
|
||||
import { Button } from '@/ui/Button';
|
||||
import { Card } from '@/ui/Card';
|
||||
import { Container } from '@/ui/Container';
|
||||
import { Heading } from '@/ui/Heading';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { routes } from '@/lib/routing/RouteConfig';
|
||||
import React from 'react';
|
||||
import { ProfileLiveryUploadPageClient } from './ProfileLiveryUploadPageClient';
|
||||
|
||||
export default async function ProfileLiveryUploadPage() {
|
||||
return (
|
||||
<Container size="md">
|
||||
<Heading level={1}>Upload livery</Heading>
|
||||
<Card>
|
||||
<Text block mb={4}>Livery upload is currently unavailable.</Text>
|
||||
<Link href={routes.protected.profileLiveries}>
|
||||
<Button variant="secondary">Back to liveries</Button>
|
||||
</Link>
|
||||
</Card>
|
||||
</Container>
|
||||
);
|
||||
return <ProfileLiveryUploadPageClient />;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { ProfilePageQuery } from '@/lib/page-queries/ProfilePageQuery';
|
||||
import { notFound } from 'next/navigation';
|
||||
import { updateProfileAction } from './actions';
|
||||
import { ProfilePageClient } from './ProfilePageClient';
|
||||
|
||||
export default async function ProfilePage() {
|
||||
@@ -18,7 +17,6 @@ export default async function ProfilePage() {
|
||||
<ProfilePageClient
|
||||
viewData={viewData}
|
||||
mode={mode}
|
||||
onSaveSettings={updateProfileAction}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { ProfileSettingsTemplate } from '@/templates/ProfileSettingsTemplate';
|
||||
import type { ProfileViewData } from '@/lib/view-data/ProfileViewData';
|
||||
import type { Result } from '@/lib/contracts/Result';
|
||||
import { ProgressLine } from '@/components/shared/ux/ProgressLine';
|
||||
import { InlineNotice } from '@/components/shared/ux/InlineNotice';
|
||||
import { Box } from '@/ui/Box';
|
||||
|
||||
interface ProfileSettingsPageClientProps {
|
||||
viewData: ProfileViewData;
|
||||
onSave: (updates: { bio?: string; country?: string }) => Promise<Result<void, string>>;
|
||||
}
|
||||
|
||||
export function ProfileSettingsPageClient({ viewData, onSave }: ProfileSettingsPageClientProps) {
|
||||
const router = useRouter();
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const [bio, setBio] = useState(viewData.driver.bio || '');
|
||||
const [country, setCountry] = useState(viewData.driver.countryCode);
|
||||
|
||||
const handleSave = async () => {
|
||||
setIsSaving(true);
|
||||
setError(null);
|
||||
try {
|
||||
const result = await onSave({ bio, country });
|
||||
if (result.isErr()) {
|
||||
setError(result.getError());
|
||||
} else {
|
||||
router.refresh();
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to save settings');
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ProgressLine isLoading={isSaving} />
|
||||
{error && (
|
||||
<Box position="fixed" top={4} right={4} zIndex={50} maxWidth="md">
|
||||
<InlineNotice
|
||||
variant="error"
|
||||
title="Update Failed"
|
||||
message={error}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
<ProfileSettingsTemplate
|
||||
viewData={viewData}
|
||||
bio={bio}
|
||||
country={country}
|
||||
onBioChange={setBio}
|
||||
onCountryChange={setCountry}
|
||||
onSave={handleSave}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,21 +1,22 @@
|
||||
import Link from 'next/link';
|
||||
import { Button } from '@/ui/Button';
|
||||
import { Card } from '@/ui/Card';
|
||||
import { Container } from '@/ui/Container';
|
||||
import { Heading } from '@/ui/Heading';
|
||||
import { Text } from '@/ui/Text';
|
||||
import { routes } from '@/lib/routing/RouteConfig';
|
||||
import { ProfilePageQuery } from '@/lib/page-queries/ProfilePageQuery';
|
||||
import { notFound } from 'next/navigation';
|
||||
import { updateProfileAction } from '@/app/actions/profileActions';
|
||||
import { ProfileSettingsPageClient } from './ProfileSettingsPageClient';
|
||||
|
||||
export default async function ProfileSettingsPage() {
|
||||
const query = new ProfilePageQuery();
|
||||
const result = await query.execute();
|
||||
|
||||
if (result.isErr()) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const viewData = result.unwrap();
|
||||
|
||||
return (
|
||||
<Container size="md">
|
||||
<Heading level={1}>Settings</Heading>
|
||||
<Card>
|
||||
<Text block mb={4}>Settings are currently unavailable.</Text>
|
||||
<Link href={routes.protected.profile}>
|
||||
<Button variant="secondary">Back to profile</Button>
|
||||
</Link>
|
||||
</Card>
|
||||
</Container>
|
||||
<ProfileSettingsPageClient
|
||||
viewData={viewData}
|
||||
onSave={updateProfileAction}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import type { Result } from '@/lib/contracts/Result';
|
||||
import type { SponsorshipRequestsViewData } from '@/lib/view-data/SponsorshipRequestsViewData';
|
||||
import { SponsorshipRequestsTemplate } from '@/templates/SponsorshipRequestsTemplate';
|
||||
import { InlineNotice } from '@/components/shared/ux/InlineNotice';
|
||||
import { ProgressLine } from '@/components/shared/ux/ProgressLine';
|
||||
import { Box } from '@/ui/Box';
|
||||
|
||||
interface SponsorshipRequestsClientProps {
|
||||
viewData: SponsorshipRequestsViewData;
|
||||
@@ -11,25 +16,54 @@ interface SponsorshipRequestsClientProps {
|
||||
}
|
||||
|
||||
export function SponsorshipRequestsClient({ viewData, onAccept, onReject }: SponsorshipRequestsClientProps) {
|
||||
const router = useRouter();
|
||||
const [isProcessing, setIsProcessing] = useState<string | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const handleAccept = async (requestId: string) => {
|
||||
setIsProcessing(requestId);
|
||||
setError(null);
|
||||
const result = await onAccept(requestId);
|
||||
if (result.isErr()) {
|
||||
console.error('Failed to accept request:', result.getError());
|
||||
setError(result.getError());
|
||||
setIsProcessing(null);
|
||||
} else {
|
||||
router.refresh();
|
||||
setIsProcessing(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleReject = async (requestId: string, reason?: string) => {
|
||||
setIsProcessing(requestId);
|
||||
setError(null);
|
||||
const result = await onReject(requestId, reason);
|
||||
if (result.isErr()) {
|
||||
console.error('Failed to reject request:', result.getError());
|
||||
setError(result.getError());
|
||||
setIsProcessing(null);
|
||||
} else {
|
||||
router.refresh();
|
||||
setIsProcessing(null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<SponsorshipRequestsTemplate
|
||||
viewData={viewData}
|
||||
onAccept={handleAccept}
|
||||
onReject={handleReject}
|
||||
/>
|
||||
<>
|
||||
<ProgressLine isLoading={!!isProcessing} />
|
||||
{error && (
|
||||
<Box position="fixed" top={4} right={4} zIndex={50} maxWidth="md">
|
||||
<InlineNotice
|
||||
variant="error"
|
||||
title="Action Failed"
|
||||
message={error}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
<SponsorshipRequestsTemplate
|
||||
viewData={viewData}
|
||||
onAccept={handleAccept}
|
||||
onReject={handleReject}
|
||||
processingId={isProcessing}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
'use server';
|
||||
|
||||
import { AcceptSponsorshipRequestMutation } from '@/lib/mutations/sponsors/AcceptSponsorshipRequestMutation';
|
||||
import { RejectSponsorshipRequestMutation } from '@/lib/mutations/sponsors/RejectSponsorshipRequestMutation';
|
||||
import { SessionGateway } from '@/lib/gateways/SessionGateway';
|
||||
import { revalidatePath } from 'next/cache';
|
||||
import { Result } from '@/lib/contracts/Result';
|
||||
import { routes } from '@/lib/routing/RouteConfig';
|
||||
|
||||
export async function acceptSponsorshipRequest(
|
||||
requestId: string,
|
||||
): Promise<Result<void, string>> {
|
||||
// Get session for actorDriverId
|
||||
const sessionGateway = new SessionGateway();
|
||||
const session = await sessionGateway.getSession();
|
||||
const actorDriverId = session?.user?.primaryDriverId;
|
||||
|
||||
if (!actorDriverId) {
|
||||
return Result.err('Not authenticated');
|
||||
}
|
||||
|
||||
const mutation = new AcceptSponsorshipRequestMutation();
|
||||
const result = await mutation.execute({ requestId, actorDriverId });
|
||||
|
||||
if (result.isErr()) {
|
||||
console.error('Failed to accept sponsorship request:', result.getError());
|
||||
return Result.err(result.getError());
|
||||
}
|
||||
|
||||
revalidatePath(routes.protected.profileSponsorshipRequests);
|
||||
return Result.ok(undefined);
|
||||
}
|
||||
|
||||
export async function rejectSponsorshipRequest(
|
||||
requestId: string,
|
||||
reason?: string,
|
||||
): Promise<Result<void, string>> {
|
||||
// Get session for actorDriverId
|
||||
const sessionGateway = new SessionGateway();
|
||||
const session = await sessionGateway.getSession();
|
||||
const actorDriverId = session?.user?.primaryDriverId;
|
||||
|
||||
if (!actorDriverId) {
|
||||
return Result.err('Not authenticated');
|
||||
}
|
||||
|
||||
const mutation = new RejectSponsorshipRequestMutation();
|
||||
const result = await mutation.execute({ requestId, actorDriverId, reason: reason || null });
|
||||
|
||||
if (result.isErr()) {
|
||||
console.error('Failed to reject sponsorship request:', result.getError());
|
||||
return Result.err(result.getError());
|
||||
}
|
||||
|
||||
revalidatePath(routes.protected.profileSponsorshipRequests);
|
||||
return Result.ok(undefined);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { notFound } from 'next/navigation';
|
||||
import { SponsorshipRequestsPageQuery } from '@/lib/page-queries/SponsorshipRequestsPageQuery';
|
||||
import { SponsorshipRequestsClient } from './SponsorshipRequestsClient';
|
||||
import { acceptSponsorshipRequest, rejectSponsorshipRequest } from './actions';
|
||||
import { acceptSponsorshipRequest, rejectSponsorshipRequest } from '@/app/actions/sponsorshipActions';
|
||||
|
||||
export default async function SponsorshipRequestsPage() {
|
||||
// Execute PageQuery
|
||||
|
||||
Reference in New Issue
Block a user