website refactor

This commit is contained in:
2026-01-14 02:02:24 +01:00
parent 8d7c709e0c
commit 4522d41aef
291 changed files with 12763 additions and 9309 deletions

View File

@@ -0,0 +1,71 @@
'use client';
import { useRouter } from 'next/navigation';
import { OnboardingWizard } from '@/components/onboarding/OnboardingWizard';
import { routes } from '@/lib/routing/RouteConfig';
import { completeOnboardingAction, generateAvatarsAction } from './actions';
import { useAuth } from '@/lib/auth/AuthContext';
export function OnboardingWizardClient() {
const router = useRouter();
const { session } = useAuth();
const handleCompleteOnboarding = async (input: {
firstName: string;
lastName: string;
displayName: string;
country: string;
timezone?: string;
}) => {
try {
const result = await completeOnboardingAction(input);
if (result.isErr()) {
return { success: false, error: result.getError() };
}
router.push(routes.protected.dashboard);
router.refresh();
return { success: true };
} catch (error) {
return { success: false, error: 'Failed to complete onboarding' };
}
};
const handleGenerateAvatars = async (params: {
facePhotoData: string;
suitColor: string;
}) => {
if (!session?.user?.userId) {
return { success: false, error: 'Not authenticated' };
}
try {
const result = await generateAvatarsAction({
userId: session.user.userId,
facePhotoData: params.facePhotoData,
suitColor: params.suitColor,
});
if (result.isErr()) {
return { success: false, error: result.getError() };
}
const data = result.unwrap();
return { success: true, data };
} catch (error) {
return { success: false, error: 'Failed to generate avatars' };
}
};
return (
<OnboardingWizard
onCompleted={() => {
router.push(routes.protected.dashboard);
router.refresh();
}}
onCompleteOnboarding={handleCompleteOnboarding}
onGenerateAvatars={handleGenerateAvatars}
/>
);
}

View File

@@ -0,0 +1,53 @@
'use server';
import { Result } from '@/lib/contracts/Result';
import { CompleteOnboardingMutation } from '@/lib/mutations/onboarding/CompleteOnboardingMutation';
import { GenerateAvatarsMutation } from '@/lib/mutations/onboarding/GenerateAvatarsMutation';
import { CompleteOnboardingInputDTO } from '@/lib/types/generated/CompleteOnboardingInputDTO';
import { revalidatePath } from 'next/cache';
import { routes } from '@/lib/routing/RouteConfig';
/**
* Complete onboarding - thin wrapper around mutation
*
* Pattern: Server Action → Mutation → Service → API Client
*
* Authentication is handled automatically by the API via cookies.
* The BaseApiClient includes credentials: 'include', so cookies are sent automatically.
* If authentication fails, the API returns 401/403 which gets converted to domain errors.
*/
export async function completeOnboardingAction(
input: CompleteOnboardingInputDTO
): Promise<Result<{ success: boolean }, string>> {
const mutation = new CompleteOnboardingMutation();
const result = await mutation.execute(input);
if (result.isErr()) {
return Result.err(result.getError());
}
revalidatePath(routes.protected.dashboard);
return Result.ok({ success: true });
}
/**
* Generate avatars - thin wrapper around mutation
*
* Note: This action requires userId to be passed from the client.
* The client should get userId from session and pass it as a parameter.
*/
export async function generateAvatarsAction(params: {
userId: string;
facePhotoData: string;
suitColor: string;
}): Promise<Result<{ success: boolean; avatarUrls?: string[] }, string>> {
const mutation = new GenerateAvatarsMutation();
const result = await mutation.execute(params);
if (result.isErr()) {
return Result.err(result.getError());
}
const data = result.unwrap();
return Result.ok({ success: data.success, avatarUrls: data.avatarUrls });
}

View File

@@ -1,6 +1,3 @@
import { headers } from 'next/headers';
import { createRouteGuard } from '@/lib/auth/createRouteGuard';
interface OnboardingLayoutProps {
children: React.ReactNode;
}
@@ -8,19 +5,22 @@ interface OnboardingLayoutProps {
/**
* Onboarding Layout
*
* Provides authentication protection for the onboarding flow.
* Uses RouteGuard to enforce access control server-side.
* Provides basic layout structure for onboarding pages.
* Authentication is handled at the layout boundary.
*/
import { headers } from 'next/headers';
import { redirect } from 'next/navigation';
import { createRouteGuard } from '@/lib/auth/createRouteGuard';
export default async function OnboardingLayout({ children }: OnboardingLayoutProps) {
const headerStore = await headers();
const pathname = headerStore.get('x-pathname') || '/';
const guard = createRouteGuard();
await guard.enforce({ pathname });
return (
<div className="min-h-screen bg-deep-graphite">
{children}
</div>
);
}
const result = await guard.enforce({ pathname });
if (result.type === 'redirect') {
redirect(result.to);
}
return <>{children}</>;
}

View File

@@ -1,66 +1,36 @@
'use client';
import { redirect } from 'next/navigation';
import { OnboardingWizardClient } from './OnboardingWizardClient';
import { OnboardingPageQuery } from '@/lib/page-queries/page-queries/OnboardingPageQuery';
import { SearchParamBuilder } from '@/lib/routing/search-params/SearchParamBuilder';
import { routes } from '@/lib/routing/RouteConfig';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';
import OnboardingWizard from '@/components/onboarding/OnboardingWizard';
import { useAuth } from '@/lib/auth/AuthContext';
// Shared state components
import { useCurrentDriver } from "@/lib/hooks/driver/useCurrentDriver";
import { StatefulPageWrapper } from '@/components/shared/state/StatefulPageWrapper';
// Template component that accepts data
function OnboardingTemplate({ data }: { data: any }) {
return <OnboardingWizard />;
}
export default function OnboardingPage() {
const router = useRouter();
const { session } = useAuth();
// Check if user is logged in
const shouldRedirectToLogin = !session;
// Fetch current driver data using DI + React-Query
const { data: driver, isLoading, error, refetch } = useCurrentDriver({
enabled: !!session,
});
const shouldRedirectToDashboard = !isLoading && Boolean(driver);
useEffect(() => {
if (shouldRedirectToLogin) {
router.replace('/auth/login?returnTo=/onboarding');
return;
/**
* Onboarding Page
*
* Server Component that handles authentication and authorization.
* Redirects to login if not authenticated.
* Redirects to dashboard if already onboarded.
*/
export default async function OnboardingPage() {
// Use PageQuery to check if user is already onboarded
const result = await OnboardingPageQuery.execute();
if (result.isErr()) {
const error = result.getError();
if (error === 'unauthorized') {
redirect(`${routes.auth.login}${SearchParamBuilder.auth(routes.protected.onboarding)}`);
} else if (error === 'serverError' || error === 'networkError') {
// Show error page or let them proceed with a warning
// For now, we'll let them proceed
}
if (shouldRedirectToDashboard) {
router.replace('/dashboard');
} else {
const viewData = result.unwrap();
if (viewData.isAlreadyOnboarded) {
redirect(routes.protected.dashboard);
}
}, [router, shouldRedirectToLogin, shouldRedirectToDashboard]);
if (shouldRedirectToLogin) {
return null;
}
if (shouldRedirectToDashboard) {
return null;
}
// For the StatefulPageWrapper, we need to provide data even if it's empty
// The page is workflow-driven, not data-driven
const wrapperData = driver || {};
return (
<main className="min-h-screen bg-deep-graphite">
<StatefulPageWrapper
data={wrapperData}
isLoading={isLoading}
error={error}
retry={refetch}
Template={OnboardingTemplate}
loading={{ variant: 'full-screen', message: 'Loading onboarding...' }}
/>
</main>
);
return <OnboardingWizardClient />;
}