feat: refactor onboarding into a multi-step flow with dedicated routes and session state management
This commit is contained in:
@@ -368,6 +368,11 @@ export async function completeOnboarding(input: OnboardingCompleteInput) {
|
|||||||
const complexSlug = await buildUniqueComplexSlug(input.complexName);
|
const complexSlug = await buildUniqueComplexSlug(input.complexName);
|
||||||
|
|
||||||
const result = await db.$transaction(async (tx) => {
|
const result = await db.$transaction(async (tx) => {
|
||||||
|
await tx.user.update({
|
||||||
|
where: { id: user.id },
|
||||||
|
data: { emailVerified: true },
|
||||||
|
});
|
||||||
|
|
||||||
const complex = await tx.complex.create({
|
const complex = await tx.complex.create({
|
||||||
data: {
|
data: {
|
||||||
id: uuidv7(),
|
id: uuidv7(),
|
||||||
|
|||||||
119
apps/frontend/src/features/onboard/onboard-complete-page.tsx
Normal file
119
apps/frontend/src/features/onboard/onboard-complete-page.tsx
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
import { OnboardingCompleteStep } from '@/features/onboard/components/onboarding-complete-step';
|
||||||
|
import { OnboardingLayout } from '@/features/onboard/components/onboarding-layout';
|
||||||
|
import {
|
||||||
|
clearOnboardingSessionState,
|
||||||
|
getOnboardingSessionState,
|
||||||
|
updateOnboardingSessionState,
|
||||||
|
} from '@/features/onboard/onboarding-session';
|
||||||
|
import type { CompleteValues } from '@/features/onboard/onboarding.types';
|
||||||
|
import { apiClient } from '@/lib/api-client';
|
||||||
|
import { useAuth } from '@/lib/auth';
|
||||||
|
import { setCurrentComplexSlug } from '@/lib/current-complex';
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { type PlanSummary, onboardingCompleteSchema } from '@repo/api-contract';
|
||||||
|
import { useNavigate } from '@tanstack/react-router';
|
||||||
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
import { useForm, useWatch } from 'react-hook-form';
|
||||||
|
|
||||||
|
export function OnboardCompletePage() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { signInWithPassword } = useAuth();
|
||||||
|
const initialState = useMemo(() => getOnboardingSessionState(), []);
|
||||||
|
|
||||||
|
const [requestId] = useState<string | null>(initialState.requestId);
|
||||||
|
const [verifiedEmail] = useState<string | null>(initialState.verifiedEmail);
|
||||||
|
const [onboardingEmail] = useState<string | null>(initialState.onboardingEmail);
|
||||||
|
const [plans, setPlans] = useState<PlanSummary[]>([]);
|
||||||
|
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const form = useForm<CompleteValues>({
|
||||||
|
resolver: zodResolver(onboardingCompleteSchema.omit({ onboardingRequestId: true })),
|
||||||
|
mode: 'onChange',
|
||||||
|
defaultValues: initialState.completeForm,
|
||||||
|
});
|
||||||
|
const values = useWatch({ control: form.control });
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!requestId) {
|
||||||
|
void navigate({ to: '/onboard' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!verifiedEmail) {
|
||||||
|
void navigate({ to: '/onboard/verify' });
|
||||||
|
}
|
||||||
|
}, [navigate, requestId, verifiedEmail]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateOnboardingSessionState((current) => ({
|
||||||
|
...current,
|
||||||
|
completeForm: {
|
||||||
|
password: values.password ?? '',
|
||||||
|
complexName: values.complexName ?? '',
|
||||||
|
physicalAddress: values.physicalAddress ?? '',
|
||||||
|
planCode: values.planCode ?? '',
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}, [values.complexName, values.password, values.physicalAddress, values.planCode]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void (async () => {
|
||||||
|
try {
|
||||||
|
const result = await apiClient.plans.list();
|
||||||
|
setPlans(result);
|
||||||
|
} catch {
|
||||||
|
setPlans([]);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onSubmit = async (payload: CompleteValues) => {
|
||||||
|
if (!requestId) {
|
||||||
|
setErrorMessage('No encontramos una solicitud de onboarding activa.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setErrorMessage(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await apiClient.onboarding.complete({
|
||||||
|
onboardingRequestId: requestId,
|
||||||
|
password: payload.password,
|
||||||
|
complexName: payload.complexName,
|
||||||
|
physicalAddress: payload.physicalAddress,
|
||||||
|
planCode: payload.planCode,
|
||||||
|
});
|
||||||
|
|
||||||
|
setCurrentComplexSlug(result.complexSlug);
|
||||||
|
const emailForSignIn = verifiedEmail ?? onboardingEmail;
|
||||||
|
if (emailForSignIn) {
|
||||||
|
await signInWithPassword({ email: emailForSignIn, password: payload.password });
|
||||||
|
}
|
||||||
|
|
||||||
|
clearOnboardingSessionState();
|
||||||
|
await navigate({ to: '/complex/$slug/edit', params: { slug: result.complexSlug } });
|
||||||
|
} catch {
|
||||||
|
setErrorMessage('No pudimos completar el onboarding. Intenta nuevamente.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const planPlaceholder =
|
||||||
|
plans.length === 0 ? 'Codigo del plan (por ejemplo: BASIC)' : 'Selecciona un plan';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<OnboardingLayout
|
||||||
|
title="Completa tu onboarding"
|
||||||
|
description="Define tu contraseña y configura tu complejo."
|
||||||
|
>
|
||||||
|
<OnboardingCompleteStep
|
||||||
|
form={form}
|
||||||
|
plans={plans}
|
||||||
|
verifiedEmail={verifiedEmail}
|
||||||
|
errorMessage={errorMessage}
|
||||||
|
infoMessage={null}
|
||||||
|
planPlaceholder={planPlaceholder}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
/>
|
||||||
|
</OnboardingLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,365 +0,0 @@
|
|||||||
import { OnboardingCompleteStep } from '@/features/onboard/components/onboarding-complete-step';
|
|
||||||
import { OnboardingLayout } from '@/features/onboard/components/onboarding-layout';
|
|
||||||
import { OnboardingStartStep } from '@/features/onboard/components/onboarding-start-step';
|
|
||||||
import { OnboardingVerifyOtpStep } from '@/features/onboard/components/onboarding-verify-otp-step';
|
|
||||||
import type {
|
|
||||||
CompleteValues,
|
|
||||||
StartValues,
|
|
||||||
VerifyOtpValues,
|
|
||||||
} from '@/features/onboard/onboarding.types';
|
|
||||||
import { apiClient } from '@/lib/api-client';
|
|
||||||
import { useAuth } from '@/lib/auth';
|
|
||||||
import { setCurrentComplexSlug } from '@/lib/current-complex';
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
|
||||||
import {
|
|
||||||
type PlanSummary,
|
|
||||||
onboardingCompleteSchema,
|
|
||||||
onboardingStartSchema,
|
|
||||||
onboardingVerifyOtpSchema,
|
|
||||||
} from '@repo/api-contract';
|
|
||||||
import { useNavigate } from '@tanstack/react-router';
|
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
|
||||||
import { useForm, useWatch } from 'react-hook-form';
|
|
||||||
|
|
||||||
const OTP_MAX_ATTEMPTS = 5;
|
|
||||||
type OnboardStep = 'start' | 'verify-otp' | 'complete';
|
|
||||||
const ONBOARD_SESSION_KEY = 'playzer:onboarding-session';
|
|
||||||
|
|
||||||
type OnboardSessionState = {
|
|
||||||
step: OnboardStep;
|
|
||||||
requestId: string | null;
|
|
||||||
onboardingEmail: string | null;
|
|
||||||
verifiedEmail: string | null;
|
|
||||||
remainingAttempts: number;
|
|
||||||
resendCooldownSeconds: number;
|
|
||||||
startForm: StartValues;
|
|
||||||
verifyOtpForm: VerifyOtpValues;
|
|
||||||
completeForm: CompleteValues;
|
|
||||||
};
|
|
||||||
|
|
||||||
function isOnboardStep(value: unknown): value is OnboardStep {
|
|
||||||
return value === 'start' || value === 'verify-otp' || value === 'complete';
|
|
||||||
}
|
|
||||||
|
|
||||||
function readOnboardSession(): OnboardSessionState | null {
|
|
||||||
if (typeof window === 'undefined') return null;
|
|
||||||
|
|
||||||
const raw = window.sessionStorage.getItem(ONBOARD_SESSION_KEY);
|
|
||||||
if (!raw) return null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(raw) as Partial<OnboardSessionState>;
|
|
||||||
if (!isOnboardStep(parsed.step)) return null;
|
|
||||||
|
|
||||||
return {
|
|
||||||
step: parsed.step,
|
|
||||||
requestId: typeof parsed.requestId === 'string' ? parsed.requestId : null,
|
|
||||||
onboardingEmail: typeof parsed.onboardingEmail === 'string' ? parsed.onboardingEmail : null,
|
|
||||||
verifiedEmail: typeof parsed.verifiedEmail === 'string' ? parsed.verifiedEmail : null,
|
|
||||||
remainingAttempts:
|
|
||||||
typeof parsed.remainingAttempts === 'number'
|
|
||||||
? parsed.remainingAttempts
|
|
||||||
: OTP_MAX_ATTEMPTS,
|
|
||||||
resendCooldownSeconds:
|
|
||||||
typeof parsed.resendCooldownSeconds === 'number' ? parsed.resendCooldownSeconds : 0,
|
|
||||||
startForm: {
|
|
||||||
fullName: parsed.startForm?.fullName ?? '',
|
|
||||||
email: parsed.startForm?.email ?? '',
|
|
||||||
},
|
|
||||||
verifyOtpForm: {
|
|
||||||
otp: parsed.verifyOtpForm?.otp ?? '',
|
|
||||||
},
|
|
||||||
completeForm: {
|
|
||||||
password: parsed.completeForm?.password ?? '',
|
|
||||||
complexName: parsed.completeForm?.complexName ?? '',
|
|
||||||
physicalAddress: parsed.completeForm?.physicalAddress ?? '',
|
|
||||||
planCode: parsed.completeForm?.planCode ?? '',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearOnboardSession() {
|
|
||||||
if (typeof window === 'undefined') return;
|
|
||||||
window.sessionStorage.removeItem(ONBOARD_SESSION_KEY);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function OnboardPage() {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const { signInWithPassword } = useAuth();
|
|
||||||
|
|
||||||
const [step, setStep] = useState<OnboardStep>('start');
|
|
||||||
const [requestId, setRequestId] = useState<string | null>(null);
|
|
||||||
const [onboardingEmail, setOnboardingEmail] = useState<string | null>(null);
|
|
||||||
const [verifiedEmail, setVerifiedEmail] = useState<string | null>(null);
|
|
||||||
const [plans, setPlans] = useState<PlanSummary[]>([]);
|
|
||||||
const [remainingAttempts, setRemainingAttempts] = useState<number>(OTP_MAX_ATTEMPTS);
|
|
||||||
const [resendCooldownSeconds, setResendCooldownSeconds] = useState<number>(0);
|
|
||||||
const [isResending, setIsResending] = useState(false);
|
|
||||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
|
||||||
const [infoMessage, setInfoMessage] = useState<string | null>(null);
|
|
||||||
const [isSessionRestored, setIsSessionRestored] = useState(false);
|
|
||||||
|
|
||||||
const startForm = useForm<StartValues>({
|
|
||||||
resolver: zodResolver(onboardingStartSchema),
|
|
||||||
mode: 'onChange',
|
|
||||||
defaultValues: {
|
|
||||||
fullName: '',
|
|
||||||
email: '',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const completeForm = useForm<CompleteValues>({
|
|
||||||
resolver: zodResolver(onboardingCompleteSchema.omit({ onboardingRequestId: true })),
|
|
||||||
mode: 'onChange',
|
|
||||||
defaultValues: {
|
|
||||||
password: '',
|
|
||||||
complexName: '',
|
|
||||||
physicalAddress: '',
|
|
||||||
planCode: '',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const verifyOtpForm = useForm<VerifyOtpValues>({
|
|
||||||
resolver: zodResolver(onboardingVerifyOtpSchema.omit({ requestId: true })),
|
|
||||||
mode: 'onChange',
|
|
||||||
defaultValues: {
|
|
||||||
otp: '',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const startValues = useWatch({ control: startForm.control });
|
|
||||||
const verifyOtpValues = useWatch({ control: verifyOtpForm.control });
|
|
||||||
const completeValues = useWatch({ control: completeForm.control });
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const savedSession = readOnboardSession();
|
|
||||||
if (!savedSession) {
|
|
||||||
setIsSessionRestored(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setStep(savedSession.step);
|
|
||||||
setRequestId(savedSession.requestId);
|
|
||||||
setOnboardingEmail(savedSession.onboardingEmail);
|
|
||||||
setVerifiedEmail(savedSession.verifiedEmail);
|
|
||||||
setRemainingAttempts(savedSession.remainingAttempts);
|
|
||||||
setResendCooldownSeconds(savedSession.resendCooldownSeconds);
|
|
||||||
startForm.reset(savedSession.startForm);
|
|
||||||
verifyOtpForm.reset(savedSession.verifyOtpForm);
|
|
||||||
completeForm.reset(savedSession.completeForm);
|
|
||||||
setIsSessionRestored(true);
|
|
||||||
}, [startForm, verifyOtpForm, completeForm]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isSessionRestored || typeof window === 'undefined') return;
|
|
||||||
|
|
||||||
const sessionState: OnboardSessionState = {
|
|
||||||
step,
|
|
||||||
requestId,
|
|
||||||
onboardingEmail,
|
|
||||||
verifiedEmail,
|
|
||||||
remainingAttempts,
|
|
||||||
resendCooldownSeconds,
|
|
||||||
startForm: {
|
|
||||||
fullName: startValues.fullName ?? '',
|
|
||||||
email: startValues.email ?? '',
|
|
||||||
},
|
|
||||||
verifyOtpForm: {
|
|
||||||
otp: verifyOtpValues.otp ?? '',
|
|
||||||
},
|
|
||||||
completeForm: {
|
|
||||||
password: completeValues.password ?? '',
|
|
||||||
complexName: completeValues.complexName ?? '',
|
|
||||||
physicalAddress: completeValues.physicalAddress ?? '',
|
|
||||||
planCode: completeValues.planCode ?? '',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
window.sessionStorage.setItem(ONBOARD_SESSION_KEY, JSON.stringify(sessionState));
|
|
||||||
}, [
|
|
||||||
completeValues,
|
|
||||||
isSessionRestored,
|
|
||||||
onboardingEmail,
|
|
||||||
remainingAttempts,
|
|
||||||
requestId,
|
|
||||||
resendCooldownSeconds,
|
|
||||||
startValues,
|
|
||||||
step,
|
|
||||||
verifiedEmail,
|
|
||||||
verifyOtpValues,
|
|
||||||
]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (step !== 'complete') return;
|
|
||||||
|
|
||||||
void (async () => {
|
|
||||||
try {
|
|
||||||
const result = await apiClient.plans.list();
|
|
||||||
setPlans(result);
|
|
||||||
} catch {
|
|
||||||
setPlans([]);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
}, [step]);
|
|
||||||
|
|
||||||
const planPlaceholder = useMemo(() => {
|
|
||||||
if (plans.length === 0) return 'Codigo del plan (por ejemplo: BASIC)';
|
|
||||||
return 'Selecciona un plan';
|
|
||||||
}, [plans.length]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (resendCooldownSeconds <= 0) return;
|
|
||||||
|
|
||||||
const timeout = window.setTimeout(() => {
|
|
||||||
setResendCooldownSeconds((current) => Math.max(0, current - 1));
|
|
||||||
}, 1000);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.clearTimeout(timeout);
|
|
||||||
};
|
|
||||||
}, [resendCooldownSeconds]);
|
|
||||||
|
|
||||||
const onSubmitStart = async (values: StartValues) => {
|
|
||||||
setErrorMessage(null);
|
|
||||||
setInfoMessage(null);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await apiClient.onboarding.start(values);
|
|
||||||
setRequestId(result.requestId);
|
|
||||||
setOnboardingEmail(result.email);
|
|
||||||
setRemainingAttempts(OTP_MAX_ATTEMPTS);
|
|
||||||
setResendCooldownSeconds(result.cooldownSeconds);
|
|
||||||
verifyOtpForm.reset({ otp: '' });
|
|
||||||
setStep('verify-otp');
|
|
||||||
setInfoMessage(result.message);
|
|
||||||
} catch {
|
|
||||||
setErrorMessage('No pudimos iniciar el onboarding. Intenta nuevamente.');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSubmitVerifyOtp = async (values: VerifyOtpValues) => {
|
|
||||||
if (!requestId) {
|
|
||||||
setErrorMessage('No encontramos una solicitud de onboarding activa.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setErrorMessage(null);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await apiClient.onboarding.verifyOtp({
|
|
||||||
requestId,
|
|
||||||
otp: values.otp,
|
|
||||||
});
|
|
||||||
|
|
||||||
setRemainingAttempts(result.remainingAttempts);
|
|
||||||
setResendCooldownSeconds(result.cooldownSeconds);
|
|
||||||
setInfoMessage(result.message);
|
|
||||||
|
|
||||||
if (result.verified) {
|
|
||||||
setVerifiedEmail(result.email ?? onboardingEmail);
|
|
||||||
setStep('complete');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
setErrorMessage('No pudimos verificar el OTP. Intenta nuevamente.');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onResendOtp = async () => {
|
|
||||||
if (!requestId) {
|
|
||||||
setErrorMessage('No encontramos una solicitud de onboarding activa.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsResending(true);
|
|
||||||
setErrorMessage(null);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await apiClient.onboarding.resendOtp({ requestId });
|
|
||||||
setOnboardingEmail(result.email);
|
|
||||||
setRemainingAttempts(result.remainingAttempts);
|
|
||||||
setResendCooldownSeconds(result.cooldownSeconds);
|
|
||||||
verifyOtpForm.reset({ otp: '' });
|
|
||||||
setInfoMessage(result.message);
|
|
||||||
} catch {
|
|
||||||
setErrorMessage('No pudimos reenviar el OTP. Intenta nuevamente.');
|
|
||||||
} finally {
|
|
||||||
setIsResending(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSubmitComplete = async (values: CompleteValues) => {
|
|
||||||
if (!requestId) {
|
|
||||||
setErrorMessage('No encontramos una solicitud de onboarding activa.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setErrorMessage(null);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await apiClient.onboarding.complete({
|
|
||||||
onboardingRequestId: requestId,
|
|
||||||
password: values.password,
|
|
||||||
complexName: values.complexName,
|
|
||||||
physicalAddress: values.physicalAddress,
|
|
||||||
planCode: values.planCode,
|
|
||||||
});
|
|
||||||
setCurrentComplexSlug(result.complexSlug);
|
|
||||||
|
|
||||||
const emailForSignIn = verifiedEmail ?? onboardingEmail;
|
|
||||||
if (emailForSignIn) {
|
|
||||||
await signInWithPassword({ email: emailForSignIn, password: values.password });
|
|
||||||
}
|
|
||||||
|
|
||||||
clearOnboardSession();
|
|
||||||
await navigate({ to: '/complex/$slug/edit', params: { slug: result.complexSlug } });
|
|
||||||
} catch {
|
|
||||||
setErrorMessage('No pudimos completar el onboarding. Intenta nuevamente.');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<OnboardingLayout
|
|
||||||
title="Onboarding"
|
|
||||||
description="Crea tu cuenta y configura tu complejo de canchas."
|
|
||||||
>
|
|
||||||
{step === 'start' && (
|
|
||||||
<OnboardingStartStep
|
|
||||||
form={startForm}
|
|
||||||
errorMessage={errorMessage}
|
|
||||||
infoMessage={infoMessage}
|
|
||||||
onSubmit={onSubmitStart}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{step === 'verify-otp' && (
|
|
||||||
<OnboardingVerifyOtpStep
|
|
||||||
form={verifyOtpForm}
|
|
||||||
email={onboardingEmail}
|
|
||||||
errorMessage={errorMessage}
|
|
||||||
infoMessage={infoMessage}
|
|
||||||
remainingAttempts={remainingAttempts}
|
|
||||||
resendCooldownSeconds={resendCooldownSeconds}
|
|
||||||
isResending={isResending}
|
|
||||||
onSubmit={onSubmitVerifyOtp}
|
|
||||||
onResend={onResendOtp}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{step === 'complete' && (
|
|
||||||
<OnboardingCompleteStep
|
|
||||||
form={completeForm}
|
|
||||||
plans={plans}
|
|
||||||
verifiedEmail={verifiedEmail}
|
|
||||||
errorMessage={errorMessage}
|
|
||||||
infoMessage={infoMessage}
|
|
||||||
planPlaceholder={planPlaceholder}
|
|
||||||
onSubmit={onSubmitComplete}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</OnboardingLayout>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
79
apps/frontend/src/features/onboard/onboard-start-page.tsx
Normal file
79
apps/frontend/src/features/onboard/onboard-start-page.tsx
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import { OnboardingLayout } from '@/features/onboard/components/onboarding-layout';
|
||||||
|
import { OnboardingStartStep } from '@/features/onboard/components/onboarding-start-step';
|
||||||
|
import {
|
||||||
|
OTP_MAX_ATTEMPTS,
|
||||||
|
defaultCompleteFormValues,
|
||||||
|
defaultVerifyOtpFormValues,
|
||||||
|
getOnboardingSessionState,
|
||||||
|
updateOnboardingSessionState,
|
||||||
|
} from '@/features/onboard/onboarding-session';
|
||||||
|
import type { StartValues } from '@/features/onboard/onboarding.types';
|
||||||
|
import { apiClient } from '@/lib/api-client';
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { onboardingStartSchema } from '@repo/api-contract';
|
||||||
|
import { useNavigate } from '@tanstack/react-router';
|
||||||
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
import { useForm, useWatch } from 'react-hook-form';
|
||||||
|
|
||||||
|
export function OnboardStartPage() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||||
|
const [infoMessage, setInfoMessage] = useState<string | null>(null);
|
||||||
|
const initialState = useMemo(() => getOnboardingSessionState(), []);
|
||||||
|
|
||||||
|
const form = useForm<StartValues>({
|
||||||
|
resolver: zodResolver(onboardingStartSchema),
|
||||||
|
mode: 'onChange',
|
||||||
|
defaultValues: initialState.startForm,
|
||||||
|
});
|
||||||
|
const values = useWatch({ control: form.control });
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateOnboardingSessionState((current) => ({
|
||||||
|
...current,
|
||||||
|
startForm: {
|
||||||
|
fullName: values.fullName ?? '',
|
||||||
|
email: values.email ?? '',
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}, [values.email, values.fullName]);
|
||||||
|
|
||||||
|
const onSubmit = async (payload: StartValues) => {
|
||||||
|
setErrorMessage(null);
|
||||||
|
setInfoMessage(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await apiClient.onboarding.start(payload);
|
||||||
|
|
||||||
|
updateOnboardingSessionState((current) => ({
|
||||||
|
...current,
|
||||||
|
requestId: result.requestId,
|
||||||
|
onboardingEmail: result.email,
|
||||||
|
verifiedEmail: null,
|
||||||
|
remainingAttempts: OTP_MAX_ATTEMPTS,
|
||||||
|
resendCooldownSeconds: result.cooldownSeconds,
|
||||||
|
startForm: {
|
||||||
|
fullName: payload.fullName,
|
||||||
|
email: payload.email,
|
||||||
|
},
|
||||||
|
verifyOtpForm: defaultVerifyOtpFormValues,
|
||||||
|
completeForm: defaultCompleteFormValues,
|
||||||
|
}));
|
||||||
|
|
||||||
|
await navigate({ to: '/onboard/verify' });
|
||||||
|
} catch {
|
||||||
|
setErrorMessage('No pudimos iniciar el onboarding. Intenta nuevamente.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<OnboardingLayout title="Onboarding" description="Ingresa tu nombre y email para comenzar.">
|
||||||
|
<OnboardingStartStep
|
||||||
|
form={form}
|
||||||
|
errorMessage={errorMessage}
|
||||||
|
infoMessage={infoMessage}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
/>
|
||||||
|
</OnboardingLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
169
apps/frontend/src/features/onboard/onboard-verify-page.tsx
Normal file
169
apps/frontend/src/features/onboard/onboard-verify-page.tsx
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { OnboardingLayout } from '@/features/onboard/components/onboarding-layout';
|
||||||
|
import { OnboardingVerifyOtpStep } from '@/features/onboard/components/onboarding-verify-otp-step';
|
||||||
|
import {
|
||||||
|
OTP_MAX_ATTEMPTS,
|
||||||
|
getOnboardingSessionState,
|
||||||
|
updateOnboardingSessionState,
|
||||||
|
} from '@/features/onboard/onboarding-session';
|
||||||
|
import type { VerifyOtpValues } from '@/features/onboard/onboarding.types';
|
||||||
|
import { apiClient } from '@/lib/api-client';
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { onboardingVerifyOtpSchema } from '@repo/api-contract';
|
||||||
|
import { useNavigate } from '@tanstack/react-router';
|
||||||
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
import { useForm, useWatch } from 'react-hook-form';
|
||||||
|
|
||||||
|
export function OnboardVerifyPage() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const initialState = useMemo(() => getOnboardingSessionState(), []);
|
||||||
|
const [requestId] = useState<string | null>(initialState.requestId);
|
||||||
|
const [onboardingEmail, setOnboardingEmail] = useState<string | null>(
|
||||||
|
initialState.onboardingEmail
|
||||||
|
);
|
||||||
|
const [remainingAttempts, setRemainingAttempts] = useState<number>(
|
||||||
|
initialState.remainingAttempts || OTP_MAX_ATTEMPTS
|
||||||
|
);
|
||||||
|
const [resendCooldownSeconds, setResendCooldownSeconds] = useState<number>(
|
||||||
|
initialState.resendCooldownSeconds || 0
|
||||||
|
);
|
||||||
|
const [isResending, setIsResending] = useState(false);
|
||||||
|
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||||
|
const [infoMessage, setInfoMessage] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const form = useForm<VerifyOtpValues>({
|
||||||
|
resolver: zodResolver(onboardingVerifyOtpSchema.omit({ requestId: true })),
|
||||||
|
mode: 'onChange',
|
||||||
|
defaultValues: initialState.verifyOtpForm,
|
||||||
|
});
|
||||||
|
const values = useWatch({ control: form.control });
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (requestId && onboardingEmail) return;
|
||||||
|
void navigate({ to: '/onboard' });
|
||||||
|
}, [navigate, onboardingEmail, requestId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateOnboardingSessionState((current) => ({
|
||||||
|
...current,
|
||||||
|
onboardingEmail,
|
||||||
|
remainingAttempts,
|
||||||
|
resendCooldownSeconds,
|
||||||
|
verifyOtpForm: {
|
||||||
|
otp: values.otp ?? '',
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}, [onboardingEmail, remainingAttempts, resendCooldownSeconds, values.otp]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (resendCooldownSeconds <= 0) return;
|
||||||
|
|
||||||
|
const timeout = window.setTimeout(() => {
|
||||||
|
setResendCooldownSeconds((current) => Math.max(0, current - 1));
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.clearTimeout(timeout);
|
||||||
|
};
|
||||||
|
}, [resendCooldownSeconds]);
|
||||||
|
|
||||||
|
const onSubmit = async (payload: VerifyOtpValues) => {
|
||||||
|
if (!requestId) {
|
||||||
|
setErrorMessage('No encontramos una solicitud de onboarding activa.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setErrorMessage(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await apiClient.onboarding.verifyOtp({
|
||||||
|
requestId,
|
||||||
|
otp: payload.otp,
|
||||||
|
});
|
||||||
|
|
||||||
|
setRemainingAttempts(result.remainingAttempts);
|
||||||
|
setResendCooldownSeconds(result.cooldownSeconds);
|
||||||
|
setInfoMessage(result.message);
|
||||||
|
|
||||||
|
updateOnboardingSessionState((current) => ({
|
||||||
|
...current,
|
||||||
|
remainingAttempts: result.remainingAttempts,
|
||||||
|
resendCooldownSeconds: result.cooldownSeconds,
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (result.verified) {
|
||||||
|
const nextVerifiedEmail = result.email ?? onboardingEmail;
|
||||||
|
|
||||||
|
updateOnboardingSessionState((current) => ({
|
||||||
|
...current,
|
||||||
|
verifiedEmail: nextVerifiedEmail,
|
||||||
|
remainingAttempts: result.remainingAttempts,
|
||||||
|
resendCooldownSeconds: result.cooldownSeconds,
|
||||||
|
}));
|
||||||
|
|
||||||
|
await navigate({ to: '/onboard/complete' });
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
setErrorMessage('No pudimos verificar el OTP. Intenta nuevamente.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onResend = async () => {
|
||||||
|
if (!requestId) {
|
||||||
|
setErrorMessage('No encontramos una solicitud de onboarding activa.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsResending(true);
|
||||||
|
setErrorMessage(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await apiClient.onboarding.resendOtp({ requestId });
|
||||||
|
setOnboardingEmail(result.email);
|
||||||
|
setRemainingAttempts(result.remainingAttempts);
|
||||||
|
setResendCooldownSeconds(result.cooldownSeconds);
|
||||||
|
form.reset({ otp: '' });
|
||||||
|
setInfoMessage(result.message);
|
||||||
|
|
||||||
|
updateOnboardingSessionState((current) => ({
|
||||||
|
...current,
|
||||||
|
onboardingEmail: result.email,
|
||||||
|
remainingAttempts: result.remainingAttempts,
|
||||||
|
resendCooldownSeconds: result.cooldownSeconds,
|
||||||
|
verifyOtpForm: { otp: '' },
|
||||||
|
}));
|
||||||
|
} catch {
|
||||||
|
setErrorMessage('No pudimos reenviar el OTP. Intenta nuevamente.');
|
||||||
|
} finally {
|
||||||
|
setIsResending(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<OnboardingLayout
|
||||||
|
title="Verifica tu email"
|
||||||
|
description="Ingresa el OTP para validar tu cuenta."
|
||||||
|
>
|
||||||
|
<OnboardingVerifyOtpStep
|
||||||
|
form={form}
|
||||||
|
email={onboardingEmail}
|
||||||
|
errorMessage={errorMessage}
|
||||||
|
infoMessage={infoMessage}
|
||||||
|
remainingAttempts={remainingAttempts}
|
||||||
|
resendCooldownSeconds={resendCooldownSeconds}
|
||||||
|
isResending={isResending}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
onResend={onResend}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="mt-2 w-full"
|
||||||
|
onClick={() => {
|
||||||
|
void navigate({ to: '/onboard' });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Volver al paso anterior
|
||||||
|
</Button>
|
||||||
|
</OnboardingLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
140
apps/frontend/src/features/onboard/onboarding-session.ts
Normal file
140
apps/frontend/src/features/onboard/onboarding-session.ts
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
import type {
|
||||||
|
CompleteValues,
|
||||||
|
StartValues,
|
||||||
|
VerifyOtpValues,
|
||||||
|
} from '@/features/onboard/onboarding.types';
|
||||||
|
|
||||||
|
const ONBOARDING_SESSION_KEY = 'playzer:onboarding-session';
|
||||||
|
export const OTP_MAX_ATTEMPTS = 5;
|
||||||
|
|
||||||
|
export const defaultStartFormValues: StartValues = {
|
||||||
|
fullName: '',
|
||||||
|
email: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const defaultVerifyOtpFormValues: VerifyOtpValues = {
|
||||||
|
otp: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const defaultCompleteFormValues: CompleteValues = {
|
||||||
|
password: '',
|
||||||
|
complexName: '',
|
||||||
|
physicalAddress: '',
|
||||||
|
planCode: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
export type OnboardingSessionState = {
|
||||||
|
requestId: string | null;
|
||||||
|
onboardingEmail: string | null;
|
||||||
|
verifiedEmail: string | null;
|
||||||
|
remainingAttempts: number;
|
||||||
|
resendCooldownSeconds: number;
|
||||||
|
startForm: StartValues;
|
||||||
|
verifyOtpForm: VerifyOtpValues;
|
||||||
|
completeForm: CompleteValues;
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultOnboardingSessionState: OnboardingSessionState = {
|
||||||
|
requestId: null,
|
||||||
|
onboardingEmail: null,
|
||||||
|
verifiedEmail: null,
|
||||||
|
remainingAttempts: OTP_MAX_ATTEMPTS,
|
||||||
|
resendCooldownSeconds: 0,
|
||||||
|
startForm: defaultStartFormValues,
|
||||||
|
verifyOtpForm: defaultVerifyOtpFormValues,
|
||||||
|
completeForm: defaultCompleteFormValues,
|
||||||
|
};
|
||||||
|
|
||||||
|
function isObject(value: unknown): value is Record<string, unknown> {
|
||||||
|
return typeof value === 'object' && value !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeSessionState(value: unknown): OnboardingSessionState {
|
||||||
|
if (!isObject(value)) {
|
||||||
|
return defaultOnboardingSessionState;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
requestId: typeof value.requestId === 'string' ? value.requestId : null,
|
||||||
|
onboardingEmail: typeof value.onboardingEmail === 'string' ? value.onboardingEmail : null,
|
||||||
|
verifiedEmail: typeof value.verifiedEmail === 'string' ? value.verifiedEmail : null,
|
||||||
|
remainingAttempts:
|
||||||
|
typeof value.remainingAttempts === 'number'
|
||||||
|
? value.remainingAttempts
|
||||||
|
: defaultOnboardingSessionState.remainingAttempts,
|
||||||
|
resendCooldownSeconds:
|
||||||
|
typeof value.resendCooldownSeconds === 'number'
|
||||||
|
? value.resendCooldownSeconds
|
||||||
|
: defaultOnboardingSessionState.resendCooldownSeconds,
|
||||||
|
startForm: {
|
||||||
|
fullName:
|
||||||
|
isObject(value.startForm) && typeof value.startForm.fullName === 'string'
|
||||||
|
? value.startForm.fullName
|
||||||
|
: defaultStartFormValues.fullName,
|
||||||
|
email:
|
||||||
|
isObject(value.startForm) && typeof value.startForm.email === 'string'
|
||||||
|
? value.startForm.email
|
||||||
|
: defaultStartFormValues.email,
|
||||||
|
},
|
||||||
|
verifyOtpForm: {
|
||||||
|
otp:
|
||||||
|
isObject(value.verifyOtpForm) && typeof value.verifyOtpForm.otp === 'string'
|
||||||
|
? value.verifyOtpForm.otp
|
||||||
|
: defaultVerifyOtpFormValues.otp,
|
||||||
|
},
|
||||||
|
completeForm: {
|
||||||
|
password:
|
||||||
|
isObject(value.completeForm) && typeof value.completeForm.password === 'string'
|
||||||
|
? value.completeForm.password
|
||||||
|
: defaultCompleteFormValues.password,
|
||||||
|
complexName:
|
||||||
|
isObject(value.completeForm) && typeof value.completeForm.complexName === 'string'
|
||||||
|
? value.completeForm.complexName
|
||||||
|
: defaultCompleteFormValues.complexName,
|
||||||
|
physicalAddress:
|
||||||
|
isObject(value.completeForm) && typeof value.completeForm.physicalAddress === 'string'
|
||||||
|
? value.completeForm.physicalAddress
|
||||||
|
: defaultCompleteFormValues.physicalAddress,
|
||||||
|
planCode:
|
||||||
|
isObject(value.completeForm) && typeof value.completeForm.planCode === 'string'
|
||||||
|
? value.completeForm.planCode
|
||||||
|
: defaultCompleteFormValues.planCode,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getOnboardingSessionState(): OnboardingSessionState {
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
return defaultOnboardingSessionState;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawState = window.sessionStorage.getItem(ONBOARDING_SESSION_KEY);
|
||||||
|
if (!rawState) {
|
||||||
|
return defaultOnboardingSessionState;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return normalizeSessionState(JSON.parse(rawState));
|
||||||
|
} catch {
|
||||||
|
return defaultOnboardingSessionState;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setOnboardingSessionState(state: OnboardingSessionState) {
|
||||||
|
if (typeof window === 'undefined') return;
|
||||||
|
window.sessionStorage.setItem(ONBOARDING_SESSION_KEY, JSON.stringify(state));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateOnboardingSessionState(
|
||||||
|
updater: (current: OnboardingSessionState) => OnboardingSessionState
|
||||||
|
): OnboardingSessionState {
|
||||||
|
const currentState = getOnboardingSessionState();
|
||||||
|
const nextState = updater(currentState);
|
||||||
|
setOnboardingSessionState(nextState);
|
||||||
|
return nextState;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearOnboardingSessionState() {
|
||||||
|
if (typeof window === 'undefined') return;
|
||||||
|
window.sessionStorage.removeItem(ONBOARDING_SESSION_KEY);
|
||||||
|
}
|
||||||
@@ -52,9 +52,9 @@ function md5(string: string) {
|
|||||||
let c = 0x98badcfe;
|
let c = 0x98badcfe;
|
||||||
let d = 0x10325476;
|
let d = 0x10325476;
|
||||||
const s = [
|
const s = [
|
||||||
7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14,
|
7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9,
|
||||||
20, 5, 9, 14, 20, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 6, 10, 15, 21, 6, 10,
|
14, 20, 5, 9, 14, 20, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 6, 10, 15, 21,
|
||||||
15, 21, 6, 10, 15, 21, 6, 10, 15, 21,
|
6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21,
|
||||||
];
|
];
|
||||||
|
|
||||||
const str = unescape(encodeURIComponent(string));
|
const str = unescape(encodeURIComponent(string));
|
||||||
@@ -69,7 +69,7 @@ function md5(string: string) {
|
|||||||
words[wordLen * 16 - 2] = len * 8;
|
words[wordLen * 16 - 2] = len * 8;
|
||||||
|
|
||||||
for (let i = 0; i < words.length; i += 16) {
|
for (let i = 0; i < words.length; i += 16) {
|
||||||
let [aa, bb, cc, dd] = [a, b, c, d];
|
const [aa, bb, cc, dd] = [a, b, c, d];
|
||||||
for (let j = 0; j < 64; j++) {
|
for (let j = 0; j < 64; j++) {
|
||||||
let f: number;
|
let f: number;
|
||||||
let g: number;
|
let g: number;
|
||||||
@@ -91,7 +91,8 @@ function md5(string: string) {
|
|||||||
c = b;
|
c = b;
|
||||||
b =
|
b =
|
||||||
(b +
|
(b +
|
||||||
((a + f + k[j] + words[i + g]) << s[j] | (a + f + k[j] + words[i + g]) >>> (32 - s[j]))) |
|
(((a + f + k[j] + words[i + g]) << s[j]) |
|
||||||
|
((a + f + k[j] + words[i + g]) >>> (32 - s[j])))) |
|
||||||
0;
|
0;
|
||||||
a = temp;
|
a = temp;
|
||||||
}
|
}
|
||||||
@@ -103,9 +104,9 @@ function md5(string: string) {
|
|||||||
|
|
||||||
return [a, b, c, d]
|
return [a, b, c, d]
|
||||||
.map((v) =>
|
.map((v) =>
|
||||||
Array.from({ length: 4 }, (_, i) => ((v >> (i * 8)) & 0xff).toString(16).padStart(2, '0')).join(
|
Array.from({ length: 4 }, (_, i) =>
|
||||||
''
|
((v >> (i * 8)) & 0xff).toString(16).padStart(2, '0')
|
||||||
)
|
).join('')
|
||||||
)
|
)
|
||||||
.join('');
|
.join('');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ import { Route as ResetPasswordRouteImport } from './routes/reset-password'
|
|||||||
import { Route as OnboardRouteImport } from './routes/onboard'
|
import { Route as OnboardRouteImport } from './routes/onboard'
|
||||||
import { Route as LoginRouteImport } from './routes/login'
|
import { Route as LoginRouteImport } from './routes/login'
|
||||||
import { Route as AppRouteRouteImport } from './routes/_app/route'
|
import { Route as AppRouteRouteImport } from './routes/_app/route'
|
||||||
|
import { Route as OnboardIndexRouteImport } from './routes/onboard/index'
|
||||||
|
import { Route as OnboardVerifyRouteImport } from './routes/onboard/verify'
|
||||||
|
import { Route as OnboardCompleteRouteImport } from './routes/onboard/complete'
|
||||||
import { Route as AppProfileRouteImport } from './routes/_app/profile'
|
import { Route as AppProfileRouteImport } from './routes/_app/profile'
|
||||||
import { Route as AppAboutRouteImport } from './routes/_app/about'
|
import { Route as AppAboutRouteImport } from './routes/_app/about'
|
||||||
import { Route as ComplexSlugBookingRouteImport } from './routes/$complexSlug/booking'
|
import { Route as ComplexSlugBookingRouteImport } from './routes/$complexSlug/booking'
|
||||||
@@ -41,6 +44,21 @@ const AppRouteRoute = AppRouteRouteImport.update({
|
|||||||
id: '/_app',
|
id: '/_app',
|
||||||
getParentRoute: () => rootRouteImport,
|
getParentRoute: () => rootRouteImport,
|
||||||
} as any)
|
} as any)
|
||||||
|
const OnboardIndexRoute = OnboardIndexRouteImport.update({
|
||||||
|
id: '/',
|
||||||
|
path: '/',
|
||||||
|
getParentRoute: () => OnboardRoute,
|
||||||
|
} as any)
|
||||||
|
const OnboardVerifyRoute = OnboardVerifyRouteImport.update({
|
||||||
|
id: '/verify',
|
||||||
|
path: '/verify',
|
||||||
|
getParentRoute: () => OnboardRoute,
|
||||||
|
} as any)
|
||||||
|
const OnboardCompleteRoute = OnboardCompleteRouteImport.update({
|
||||||
|
id: '/complete',
|
||||||
|
path: '/complete',
|
||||||
|
getParentRoute: () => OnboardRoute,
|
||||||
|
} as any)
|
||||||
const AppProfileRoute = AppProfileRouteImport.update({
|
const AppProfileRoute = AppProfileRouteImport.update({
|
||||||
id: '/profile',
|
id: '/profile',
|
||||||
path: '/profile',
|
path: '/profile',
|
||||||
@@ -86,11 +104,14 @@ const AppAuthenticatedComplexSlugEditRoute =
|
|||||||
export interface FileRoutesByFullPath {
|
export interface FileRoutesByFullPath {
|
||||||
'/': typeof AppAuthenticatedIndexRoute
|
'/': typeof AppAuthenticatedIndexRoute
|
||||||
'/login': typeof LoginRoute
|
'/login': typeof LoginRoute
|
||||||
'/onboard': typeof OnboardRoute
|
'/onboard': typeof OnboardRouteWithChildren
|
||||||
'/reset-password': typeof ResetPasswordRoute
|
'/reset-password': typeof ResetPasswordRoute
|
||||||
'/$complexSlug/booking': typeof ComplexSlugBookingRouteWithChildren
|
'/$complexSlug/booking': typeof ComplexSlugBookingRouteWithChildren
|
||||||
'/about': typeof AppAboutRoute
|
'/about': typeof AppAboutRoute
|
||||||
'/profile': typeof AppProfileRoute
|
'/profile': typeof AppProfileRoute
|
||||||
|
'/onboard/complete': typeof OnboardCompleteRoute
|
||||||
|
'/onboard/verify': typeof OnboardVerifyRoute
|
||||||
|
'/onboard/': typeof OnboardIndexRoute
|
||||||
'/$complexSlug/booking/': typeof ComplexSlugBookingIndexRoute
|
'/$complexSlug/booking/': typeof ComplexSlugBookingIndexRoute
|
||||||
'/$complexSlug/booking/confirmed/$bookingCode': typeof ComplexSlugBookingConfirmedBookingCodeRoute
|
'/$complexSlug/booking/confirmed/$bookingCode': typeof ComplexSlugBookingConfirmedBookingCodeRoute
|
||||||
'/complex/$slug/edit': typeof AppAuthenticatedComplexSlugEditRoute
|
'/complex/$slug/edit': typeof AppAuthenticatedComplexSlugEditRoute
|
||||||
@@ -98,10 +119,12 @@ export interface FileRoutesByFullPath {
|
|||||||
export interface FileRoutesByTo {
|
export interface FileRoutesByTo {
|
||||||
'/': typeof AppAuthenticatedIndexRoute
|
'/': typeof AppAuthenticatedIndexRoute
|
||||||
'/login': typeof LoginRoute
|
'/login': typeof LoginRoute
|
||||||
'/onboard': typeof OnboardRoute
|
|
||||||
'/reset-password': typeof ResetPasswordRoute
|
'/reset-password': typeof ResetPasswordRoute
|
||||||
'/about': typeof AppAboutRoute
|
'/about': typeof AppAboutRoute
|
||||||
'/profile': typeof AppProfileRoute
|
'/profile': typeof AppProfileRoute
|
||||||
|
'/onboard/complete': typeof OnboardCompleteRoute
|
||||||
|
'/onboard/verify': typeof OnboardVerifyRoute
|
||||||
|
'/onboard': typeof OnboardIndexRoute
|
||||||
'/$complexSlug/booking': typeof ComplexSlugBookingIndexRoute
|
'/$complexSlug/booking': typeof ComplexSlugBookingIndexRoute
|
||||||
'/$complexSlug/booking/confirmed/$bookingCode': typeof ComplexSlugBookingConfirmedBookingCodeRoute
|
'/$complexSlug/booking/confirmed/$bookingCode': typeof ComplexSlugBookingConfirmedBookingCodeRoute
|
||||||
'/complex/$slug/edit': typeof AppAuthenticatedComplexSlugEditRoute
|
'/complex/$slug/edit': typeof AppAuthenticatedComplexSlugEditRoute
|
||||||
@@ -110,12 +133,15 @@ export interface FileRoutesById {
|
|||||||
__root__: typeof rootRouteImport
|
__root__: typeof rootRouteImport
|
||||||
'/_app': typeof AppRouteRouteWithChildren
|
'/_app': typeof AppRouteRouteWithChildren
|
||||||
'/login': typeof LoginRoute
|
'/login': typeof LoginRoute
|
||||||
'/onboard': typeof OnboardRoute
|
'/onboard': typeof OnboardRouteWithChildren
|
||||||
'/reset-password': typeof ResetPasswordRoute
|
'/reset-password': typeof ResetPasswordRoute
|
||||||
'/_app/_authenticated': typeof AppAuthenticatedRouteRouteWithChildren
|
'/_app/_authenticated': typeof AppAuthenticatedRouteRouteWithChildren
|
||||||
'/$complexSlug/booking': typeof ComplexSlugBookingRouteWithChildren
|
'/$complexSlug/booking': typeof ComplexSlugBookingRouteWithChildren
|
||||||
'/_app/about': typeof AppAboutRoute
|
'/_app/about': typeof AppAboutRoute
|
||||||
'/_app/profile': typeof AppProfileRoute
|
'/_app/profile': typeof AppProfileRoute
|
||||||
|
'/onboard/complete': typeof OnboardCompleteRoute
|
||||||
|
'/onboard/verify': typeof OnboardVerifyRoute
|
||||||
|
'/onboard/': typeof OnboardIndexRoute
|
||||||
'/$complexSlug/booking/': typeof ComplexSlugBookingIndexRoute
|
'/$complexSlug/booking/': typeof ComplexSlugBookingIndexRoute
|
||||||
'/_app/_authenticated/': typeof AppAuthenticatedIndexRoute
|
'/_app/_authenticated/': typeof AppAuthenticatedIndexRoute
|
||||||
'/$complexSlug/booking/confirmed/$bookingCode': typeof ComplexSlugBookingConfirmedBookingCodeRoute
|
'/$complexSlug/booking/confirmed/$bookingCode': typeof ComplexSlugBookingConfirmedBookingCodeRoute
|
||||||
@@ -131,6 +157,9 @@ export interface FileRouteTypes {
|
|||||||
| '/$complexSlug/booking'
|
| '/$complexSlug/booking'
|
||||||
| '/about'
|
| '/about'
|
||||||
| '/profile'
|
| '/profile'
|
||||||
|
| '/onboard/complete'
|
||||||
|
| '/onboard/verify'
|
||||||
|
| '/onboard/'
|
||||||
| '/$complexSlug/booking/'
|
| '/$complexSlug/booking/'
|
||||||
| '/$complexSlug/booking/confirmed/$bookingCode'
|
| '/$complexSlug/booking/confirmed/$bookingCode'
|
||||||
| '/complex/$slug/edit'
|
| '/complex/$slug/edit'
|
||||||
@@ -138,10 +167,12 @@ export interface FileRouteTypes {
|
|||||||
to:
|
to:
|
||||||
| '/'
|
| '/'
|
||||||
| '/login'
|
| '/login'
|
||||||
| '/onboard'
|
|
||||||
| '/reset-password'
|
| '/reset-password'
|
||||||
| '/about'
|
| '/about'
|
||||||
| '/profile'
|
| '/profile'
|
||||||
|
| '/onboard/complete'
|
||||||
|
| '/onboard/verify'
|
||||||
|
| '/onboard'
|
||||||
| '/$complexSlug/booking'
|
| '/$complexSlug/booking'
|
||||||
| '/$complexSlug/booking/confirmed/$bookingCode'
|
| '/$complexSlug/booking/confirmed/$bookingCode'
|
||||||
| '/complex/$slug/edit'
|
| '/complex/$slug/edit'
|
||||||
@@ -155,6 +186,9 @@ export interface FileRouteTypes {
|
|||||||
| '/$complexSlug/booking'
|
| '/$complexSlug/booking'
|
||||||
| '/_app/about'
|
| '/_app/about'
|
||||||
| '/_app/profile'
|
| '/_app/profile'
|
||||||
|
| '/onboard/complete'
|
||||||
|
| '/onboard/verify'
|
||||||
|
| '/onboard/'
|
||||||
| '/$complexSlug/booking/'
|
| '/$complexSlug/booking/'
|
||||||
| '/_app/_authenticated/'
|
| '/_app/_authenticated/'
|
||||||
| '/$complexSlug/booking/confirmed/$bookingCode'
|
| '/$complexSlug/booking/confirmed/$bookingCode'
|
||||||
@@ -164,7 +198,7 @@ export interface FileRouteTypes {
|
|||||||
export interface RootRouteChildren {
|
export interface RootRouteChildren {
|
||||||
AppRouteRoute: typeof AppRouteRouteWithChildren
|
AppRouteRoute: typeof AppRouteRouteWithChildren
|
||||||
LoginRoute: typeof LoginRoute
|
LoginRoute: typeof LoginRoute
|
||||||
OnboardRoute: typeof OnboardRoute
|
OnboardRoute: typeof OnboardRouteWithChildren
|
||||||
ResetPasswordRoute: typeof ResetPasswordRoute
|
ResetPasswordRoute: typeof ResetPasswordRoute
|
||||||
ComplexSlugBookingRoute: typeof ComplexSlugBookingRouteWithChildren
|
ComplexSlugBookingRoute: typeof ComplexSlugBookingRouteWithChildren
|
||||||
}
|
}
|
||||||
@@ -199,6 +233,27 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof AppRouteRouteImport
|
preLoaderRoute: typeof AppRouteRouteImport
|
||||||
parentRoute: typeof rootRouteImport
|
parentRoute: typeof rootRouteImport
|
||||||
}
|
}
|
||||||
|
'/onboard/': {
|
||||||
|
id: '/onboard/'
|
||||||
|
path: '/'
|
||||||
|
fullPath: '/onboard/'
|
||||||
|
preLoaderRoute: typeof OnboardIndexRouteImport
|
||||||
|
parentRoute: typeof OnboardRoute
|
||||||
|
}
|
||||||
|
'/onboard/verify': {
|
||||||
|
id: '/onboard/verify'
|
||||||
|
path: '/verify'
|
||||||
|
fullPath: '/onboard/verify'
|
||||||
|
preLoaderRoute: typeof OnboardVerifyRouteImport
|
||||||
|
parentRoute: typeof OnboardRoute
|
||||||
|
}
|
||||||
|
'/onboard/complete': {
|
||||||
|
id: '/onboard/complete'
|
||||||
|
path: '/complete'
|
||||||
|
fullPath: '/onboard/complete'
|
||||||
|
preLoaderRoute: typeof OnboardCompleteRouteImport
|
||||||
|
parentRoute: typeof OnboardRoute
|
||||||
|
}
|
||||||
'/_app/profile': {
|
'/_app/profile': {
|
||||||
id: '/_app/profile'
|
id: '/_app/profile'
|
||||||
path: '/profile'
|
path: '/profile'
|
||||||
@@ -289,6 +344,21 @@ const AppRouteRouteWithChildren = AppRouteRoute._addFileChildren(
|
|||||||
AppRouteRouteChildren,
|
AppRouteRouteChildren,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
interface OnboardRouteChildren {
|
||||||
|
OnboardCompleteRoute: typeof OnboardCompleteRoute
|
||||||
|
OnboardVerifyRoute: typeof OnboardVerifyRoute
|
||||||
|
OnboardIndexRoute: typeof OnboardIndexRoute
|
||||||
|
}
|
||||||
|
|
||||||
|
const OnboardRouteChildren: OnboardRouteChildren = {
|
||||||
|
OnboardCompleteRoute: OnboardCompleteRoute,
|
||||||
|
OnboardVerifyRoute: OnboardVerifyRoute,
|
||||||
|
OnboardIndexRoute: OnboardIndexRoute,
|
||||||
|
}
|
||||||
|
|
||||||
|
const OnboardRouteWithChildren =
|
||||||
|
OnboardRoute._addFileChildren(OnboardRouteChildren)
|
||||||
|
|
||||||
interface ComplexSlugBookingRouteChildren {
|
interface ComplexSlugBookingRouteChildren {
|
||||||
ComplexSlugBookingIndexRoute: typeof ComplexSlugBookingIndexRoute
|
ComplexSlugBookingIndexRoute: typeof ComplexSlugBookingIndexRoute
|
||||||
ComplexSlugBookingConfirmedBookingCodeRoute: typeof ComplexSlugBookingConfirmedBookingCodeRoute
|
ComplexSlugBookingConfirmedBookingCodeRoute: typeof ComplexSlugBookingConfirmedBookingCodeRoute
|
||||||
@@ -306,7 +376,7 @@ const ComplexSlugBookingRouteWithChildren =
|
|||||||
const rootRouteChildren: RootRouteChildren = {
|
const rootRouteChildren: RootRouteChildren = {
|
||||||
AppRouteRoute: AppRouteRouteWithChildren,
|
AppRouteRoute: AppRouteRouteWithChildren,
|
||||||
LoginRoute: LoginRoute,
|
LoginRoute: LoginRoute,
|
||||||
OnboardRoute: OnboardRoute,
|
OnboardRoute: OnboardRouteWithChildren,
|
||||||
ResetPasswordRoute: ResetPasswordRoute,
|
ResetPasswordRoute: ResetPasswordRoute,
|
||||||
ComplexSlugBookingRoute: ComplexSlugBookingRouteWithChildren,
|
ComplexSlugBookingRoute: ComplexSlugBookingRouteWithChildren,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { OnboardPage } from '@/features/onboard/onboard-page';
|
import { Outlet, createFileRoute } from '@tanstack/react-router';
|
||||||
import { createFileRoute } from '@tanstack/react-router';
|
|
||||||
|
|
||||||
export const Route = createFileRoute('/onboard')({
|
export const Route = createFileRoute('/onboard')({
|
||||||
component: OnboardPage,
|
component: Outlet,
|
||||||
});
|
});
|
||||||
|
|||||||
6
apps/frontend/src/routes/onboard/complete.tsx
Normal file
6
apps/frontend/src/routes/onboard/complete.tsx
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { OnboardCompletePage } from '@/features/onboard/onboard-complete-page';
|
||||||
|
import { createFileRoute } from '@tanstack/react-router';
|
||||||
|
|
||||||
|
export const Route = createFileRoute('/onboard/complete')({
|
||||||
|
component: OnboardCompletePage,
|
||||||
|
});
|
||||||
6
apps/frontend/src/routes/onboard/index.tsx
Normal file
6
apps/frontend/src/routes/onboard/index.tsx
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { OnboardStartPage } from '@/features/onboard/onboard-start-page';
|
||||||
|
import { createFileRoute } from '@tanstack/react-router';
|
||||||
|
|
||||||
|
export const Route = createFileRoute('/onboard/')({
|
||||||
|
component: OnboardStartPage,
|
||||||
|
});
|
||||||
6
apps/frontend/src/routes/onboard/verify.tsx
Normal file
6
apps/frontend/src/routes/onboard/verify.tsx
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { OnboardVerifyPage } from '@/features/onboard/onboard-verify-page';
|
||||||
|
import { createFileRoute } from '@tanstack/react-router';
|
||||||
|
|
||||||
|
export const Route = createFileRoute('/onboard/verify')({
|
||||||
|
component: OnboardVerifyPage,
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user