feat: implement onboarding flow with complex creation and email verification, and add Zustand for state management
This commit is contained in:
222
apps/frontend/src/features/onboard/create-complex-page.tsx
Normal file
222
apps/frontend/src/features/onboard/create-complex-page.tsx
Normal file
@@ -0,0 +1,222 @@
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Field, FieldError, FieldLabel } from '@/components/ui/field';
|
||||
import { Input } from '@/components/ui/input';
|
||||
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 } from '@repo/api-contract';
|
||||
import { useNavigate } from '@tanstack/react-router';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
|
||||
const createComplexSchema = z.object({
|
||||
complexName: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(3, 'El nombre del complejo debe tener al menos 3 caracteres.')
|
||||
.max(120, 'El nombre del complejo no puede superar los 120 caracteres.'),
|
||||
physicalAddress: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(5, 'La direccion debe tener al menos 5 caracteres.')
|
||||
.max(200, 'La direccion no puede superar los 200 caracteres.'),
|
||||
city: z.string().trim().max(100, 'La ciudad no puede superar los 100 caracteres.').optional(),
|
||||
state: z
|
||||
.string()
|
||||
.trim()
|
||||
.max(100, 'La provincia/estado no puede superar los 100 caracteres.')
|
||||
.optional(),
|
||||
country: z.string().trim().max(100, 'El país no puede superar los 100 caracteres.').optional(),
|
||||
planCode: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, 'El planCode no puede estar vacío.')
|
||||
.max(10, 'El planCode no puede superar los 10 caracteres.')
|
||||
.optional(),
|
||||
});
|
||||
|
||||
type CreateComplexForm = z.infer<typeof createComplexSchema>;
|
||||
|
||||
export function CreateComplexPage() {
|
||||
const navigate = useNavigate();
|
||||
const { user } = useAuth();
|
||||
const [plans, setPlans] = useState<PlanSummary[]>([]);
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const form = useForm<CreateComplexForm>({
|
||||
resolver: zodResolver(createComplexSchema),
|
||||
mode: 'onChange',
|
||||
defaultValues: {
|
||||
complexName: '',
|
||||
physicalAddress: '',
|
||||
city: '',
|
||||
state: '',
|
||||
country: '',
|
||||
planCode: '',
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
void (async () => {
|
||||
try {
|
||||
const result = await apiClient.plans.list();
|
||||
setPlans(result);
|
||||
} catch {
|
||||
setPlans([]);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const onSubmit = async (values: CreateComplexForm) => {
|
||||
setErrorMessage(null);
|
||||
setIsSubmitting(true);
|
||||
|
||||
try {
|
||||
const pendingEmail = sessionStorage.getItem('pending-signup-email');
|
||||
const payload: Parameters<typeof apiClient.complexes.create>[0] = {
|
||||
complexName: values.complexName,
|
||||
physicalAddress: values.physicalAddress,
|
||||
city: values.city,
|
||||
state: values.state,
|
||||
country: values.country,
|
||||
planCode: values.planCode,
|
||||
};
|
||||
|
||||
if (pendingEmail) {
|
||||
payload.adminEmail = pendingEmail;
|
||||
}
|
||||
|
||||
const complex = await apiClient.complexes.create(payload);
|
||||
|
||||
sessionStorage.removeItem('pending-signup-email');
|
||||
sessionStorage.removeItem('pending-signup-userId');
|
||||
setCurrentComplexSlug(complex.complexSlug);
|
||||
await navigate({
|
||||
to: '/complex/$slug/edit',
|
||||
params: { slug: complex.complexSlug },
|
||||
});
|
||||
} catch {
|
||||
setErrorMessage('No pudimos crear el complejo. Intenta nuevamente.');
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<main className="flex min-h-screen w-full items-center justify-center px-6">
|
||||
<section className="w-full max-w-sm rounded-xl border bg-card p-6 text-card-foreground shadow-sm">
|
||||
<h1 className="mb-2 text-2xl font-semibold">Crear tu complejo</h1>
|
||||
<p className="mb-4 text-sm text-muted-foreground">
|
||||
Completá los datos de tu complejo para comenzar.
|
||||
</p>
|
||||
|
||||
<form className="space-y-3" onSubmit={form.handleSubmit(onSubmit)}>
|
||||
{user?.email && (
|
||||
<Field>
|
||||
<FieldLabel htmlFor="user-email">Email</FieldLabel>
|
||||
<Input id="user-email" type="email" value={user.email} disabled readOnly />
|
||||
</Field>
|
||||
)}
|
||||
|
||||
<Field data-invalid={Boolean(form.formState.errors.complexName)}>
|
||||
<FieldLabel htmlFor="complexName">Nombre del complejo</FieldLabel>
|
||||
<Input
|
||||
id="complexName"
|
||||
type="text"
|
||||
placeholder="Complejo Las Palmeras"
|
||||
aria-invalid={Boolean(form.formState.errors.complexName)}
|
||||
{...form.register('complexName')}
|
||||
/>
|
||||
<FieldError errors={[form.formState.errors.complexName]} />
|
||||
</Field>
|
||||
|
||||
<Field data-invalid={Boolean(form.formState.errors.physicalAddress)}>
|
||||
<FieldLabel htmlFor="physicalAddress">Dirección física</FieldLabel>
|
||||
<Input
|
||||
id="physicalAddress"
|
||||
type="text"
|
||||
placeholder="Av. San Martin 1234, Salta"
|
||||
aria-invalid={Boolean(form.formState.errors.physicalAddress)}
|
||||
{...form.register('physicalAddress')}
|
||||
/>
|
||||
<FieldError errors={[form.formState.errors.physicalAddress]} />
|
||||
</Field>
|
||||
|
||||
<Field data-invalid={Boolean(form.formState.errors.city)}>
|
||||
<FieldLabel htmlFor="city">Ciudad</FieldLabel>
|
||||
<Input
|
||||
id="city"
|
||||
type="text"
|
||||
placeholder="Salta"
|
||||
aria-invalid={Boolean(form.formState.errors.city)}
|
||||
{...form.register('city')}
|
||||
/>
|
||||
<FieldError errors={[form.formState.errors.city]} />
|
||||
</Field>
|
||||
|
||||
<Field data-invalid={Boolean(form.formState.errors.state)}>
|
||||
<FieldLabel htmlFor="state">Provincia/Estado</FieldLabel>
|
||||
<Input
|
||||
id="state"
|
||||
type="text"
|
||||
placeholder="Salta"
|
||||
aria-invalid={Boolean(form.formState.errors.state)}
|
||||
{...form.register('state')}
|
||||
/>
|
||||
<FieldError errors={[form.formState.errors.state]} />
|
||||
</Field>
|
||||
|
||||
<Field data-invalid={Boolean(form.formState.errors.country)}>
|
||||
<FieldLabel htmlFor="country">País</FieldLabel>
|
||||
<Input
|
||||
id="country"
|
||||
type="text"
|
||||
placeholder="Argentina"
|
||||
aria-invalid={Boolean(form.formState.errors.country)}
|
||||
{...form.register('country')}
|
||||
/>
|
||||
<FieldError errors={[form.formState.errors.country]} />
|
||||
</Field>
|
||||
|
||||
<Field data-invalid={Boolean(form.formState.errors.planCode)}>
|
||||
<FieldLabel htmlFor="planCode">Plan</FieldLabel>
|
||||
<select
|
||||
id="planCode"
|
||||
className="h-10 w-full rounded-md border bg-background px-3 text-sm"
|
||||
aria-invalid={Boolean(form.formState.errors.planCode)}
|
||||
{...form.register('planCode')}
|
||||
>
|
||||
<option value="">
|
||||
{plans.length > 0 ? 'Selecciona un plan' : 'No hay planes disponibles'}
|
||||
</option>
|
||||
{plans.map((plan) => (
|
||||
<option key={plan.code} value={plan.code}>
|
||||
{plan.name} - ${plan.price.toFixed(2)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{plans.length === 0 && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Carga planes en la base para continuar.
|
||||
</p>
|
||||
)}
|
||||
<FieldError errors={[form.formState.errors.planCode]} />
|
||||
</Field>
|
||||
|
||||
{errorMessage && <p className="text-sm text-destructive">{errorMessage}</p>}
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full"
|
||||
disabled={isSubmitting || !form.formState.isValid}
|
||||
>
|
||||
{isSubmitting ? 'Creando complejo...' : 'Crear complejo'}
|
||||
</Button>
|
||||
</form>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user