refactor(onboarding): restructure onboarding routes and components

- Removed obsolete onboarding routes: complete, create-complex, index, verify-email, and verify.
- Introduced new setup stepper page for onboarding process.
- Created a multi-step setup component to handle complex creation with validation.
- Added new schemas for complex creation and plan features.
- Implemented detailed steps for complex name, location, plan selection, and court setup.
- Enhanced error handling and user feedback during the onboarding process.
This commit is contained in:
Jose Selesan
2026-06-02 10:02:28 -03:00
parent 2aaa91444d
commit 4544911f0e
51 changed files with 1006 additions and 2383 deletions

View File

@@ -1,5 +1,15 @@
import { z } from 'zod'
const dayOfWeekEnum = z.enum([
'MONDAY',
'TUESDAY',
'WEDNESDAY',
'THURSDAY',
'FRIDAY',
'SATURDAY',
'SUNDAY',
])
export const complexSchema = z.object({
id: z.uuid(),
complexName: z.string(),
@@ -14,39 +24,77 @@ export const complexSchema = z.object({
updatedAt: z.string().datetime(),
})
export 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(),
})
export 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(),
setupCourts: z.boolean().default(false),
courtSportId: z.string().uuid('El deporte seleccionado no es válido.').optional(),
courtStartTime: z.string().optional(),
courtEndTime: z.string().optional(),
courtDaysOfWeek: z.array(dayOfWeekEnum).optional(),
})
.superRefine((data, ctx) => {
if (data.setupCourts) {
if (!data.courtSportId) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ['courtSportId'],
message: 'Debes seleccionar un deporte.',
})
}
if (!data.courtStartTime || !/^\d{2}:\d{2}$/.test(data.courtStartTime)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ['courtStartTime'],
message: 'La hora de inicio debe tener formato HH:mm.',
})
}
if (!data.courtEndTime || !/^\d{2}:\d{2}$/.test(data.courtEndTime)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ['courtEndTime'],
message: 'La hora de fin debe tener formato HH:mm.',
})
}
if (!data.courtDaysOfWeek || data.courtDaysOfWeek.length === 0) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ['courtDaysOfWeek'],
message: 'Debes seleccionar al menos un día.',
})
}
}
})
export const updateComplexSchema = z
.object({

View File

@@ -4,28 +4,18 @@ export {
planRulesV1Schema,
planSchema,
planSummarySchema,
planWithFeaturesSchema,
planLimitsSchema,
} from './plan'
export type { Plan, PlanFeatureFlags, PlanRules, PlanRulesV1, PlanSummary } from './plan'
export {
onboardingCompleteResponseSchema,
onboardingCompleteSchema,
onboardingResendOtpResponseSchema,
onboardingResendOtpSchema,
onboardingStartResponseSchema,
onboardingStartSchema,
onboardingVerifyOtpResponseSchema,
onboardingVerifyOtpSchema,
} from './onboarding'
export type {
OnboardingCompleteInput,
OnboardingCompleteResponse,
OnboardingResendOtpInput,
OnboardingResendOtpResponse,
OnboardingStartInput,
OnboardingStartResponse,
OnboardingVerifyOtpInput,
OnboardingVerifyOtpResponse,
} from './onboarding'
Plan,
PlanFeatureFlags,
PlanRules,
PlanRulesV1,
PlanSummary,
PlanWithFeatures,
PlanLimits,
} from './plan'
export {
acceptComplexInvitationsSchema,
complexInvitationSchema,

View File

@@ -1,112 +0,0 @@
import { z } from 'zod'
const otpSchema = z
.string()
.trim()
.regex(/^\d{6}$/, 'El OTP debe tener 6 digitos numericos.')
export const onboardingStartSchema = z.object({
fullName: z
.string()
.trim()
.min(2, 'El nombre debe tener al menos 2 caracteres.')
.max(80, 'El nombre no puede superar los 80 caracteres.'),
email: z.email('Ingresa un email valido.'),
})
export const onboardingVerifyOtpSchema = z.object({
requestId: z.uuid('El requestId de onboarding no es valido.'),
otp: otpSchema,
})
export const onboardingResendOtpSchema = z.object({
requestId: z.uuid('El requestId de onboarding no es valido.'),
})
export const onboardingCompleteSchema = z.object({
onboardingRequestId: z.uuid('El onboardingRequestId no es valido.'),
password: z
.string()
.min(8, 'La contrasena debe tener al menos 8 caracteres.')
.max(72, 'La contrasena no puede superar los 72 caracteres.'),
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, 'Debes seleccionar un plan.')
.max(10, 'El plan seleccionado no es valido.'),
})
export const onboardingStartResponseSchema = z.object({
message: z.string(),
requestId: z.uuid(),
email: z.email(),
expiresAt: z.string().datetime(),
cooldownSeconds: z.int().nonnegative(),
})
export const onboardingVerifyOtpResponseSchema = z.object({
message: z.string(),
verified: z.boolean(),
requestId: z.uuid(),
email: z.email().nullable(),
expiresAt: z.string().datetime().nullable(),
remainingAttempts: z.int().nonnegative(),
cooldownSeconds: z.int().nonnegative(),
})
export const onboardingResendOtpResponseSchema = z.object({
message: z.string(),
requestId: z.uuid(),
email: z.email(),
expiresAt: z.string().datetime(),
cooldownSeconds: z.int().nonnegative(),
remainingAttempts: z.int().nonnegative(),
})
export const onboardingCompleteResponseSchema = z.object({
message: z.string(),
userId: z.uuid(),
complexId: z.uuid(),
complexSlug: z.string(),
})
export type OnboardingStartInput = z.infer<typeof onboardingStartSchema>
export type OnboardingVerifyOtpInput = z.infer<typeof onboardingVerifyOtpSchema>
export type OnboardingResendOtpInput = z.infer<typeof onboardingResendOtpSchema>
export type OnboardingCompleteInput = z.infer<typeof onboardingCompleteSchema>
export type OnboardingStartResponse = z.infer<typeof onboardingStartResponseSchema>
export type OnboardingVerifyOtpResponse = z.infer<
typeof onboardingVerifyOtpResponseSchema
>
export type OnboardingResendOtpResponse = z.infer<
typeof onboardingResendOtpResponseSchema
>
export type OnboardingCompleteResponse = z.infer<
typeof onboardingCompleteResponseSchema
>

View File

@@ -49,8 +49,25 @@ export const planSummarySchema = z.object({
price: z.number().nonnegative(),
})
export const planLimitsSchema = z.object({
maxCourts: positiveInt,
maxBookingsPerDay: positiveInt,
maxActiveUsers: positiveInt.optional(),
maxConcurrentBookingsPerSlot: positiveInt.optional(),
})
export const planWithFeaturesSchema = z.object({
code: z.string().trim().min(1).max(10),
name: z.string().trim().min(1).max(30),
price: z.number().nonnegative(),
features: planFeatureFlagsSchema,
limits: planLimitsSchema,
})
export type PlanFeatureFlags = z.infer<typeof planFeatureFlagsSchema>
export type PlanRulesV1 = z.infer<typeof planRulesV1Schema>
export type PlanRules = z.infer<typeof planRulesSchema>
export type Plan = z.infer<typeof planSchema>
export type PlanSummary = z.infer<typeof planSummarySchema>
export type PlanWithFeatures = z.infer<typeof planWithFeaturesSchema>
export type PlanLimits = z.infer<typeof planLimitsSchema>