- / y /b2b sirven la landing B2B estática (rewrites beforeFiles)
- /{slug} resuelve el funnel del reformista (app/[slug]/page.tsx) con
branding propio (TenantBrand) y atribución de leads por tenant
- crearLead(slug) y páginas /solicitud usan el tenant del lead
- Panel: edición del slug del funnel + URL pública en /panel/empresa
- Helper de slugs reservados para evitar colisiones con rutas reales
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
64 lines
1.9 KiB
TypeScript
64 lines
1.9 KiB
TypeScript
import { z } from 'zod';
|
|
|
|
export const signupSchema = z.object({
|
|
nombre: z.string().trim().min(1, 'Indica tu nombre.'),
|
|
email: z.string().trim().toLowerCase().email('Email no válido.'),
|
|
empresa: z.string().trim().min(1, 'Indica el nombre de tu empresa.'),
|
|
provincia: z.string().trim().min(1, 'Indica tu provincia.'),
|
|
password: z.string().min(8, 'La contraseña debe tener al menos 8 caracteres.'),
|
|
optInMarketing: z.union([z.literal('on'), z.literal('')]).optional(),
|
|
});
|
|
|
|
export type SignupInput = z.infer<typeof signupSchema>;
|
|
|
|
export function slugify(value: string): string {
|
|
return value
|
|
.normalize('NFD')
|
|
.replace(/[̀-ͯ]/g, '')
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9]+/g, '-')
|
|
.replace(/^-+|-+$/g, '');
|
|
}
|
|
|
|
// Rutas de la app que NO pueden ser slug de un reformista: colisionarían con
|
|
// páginas reales o con ficheros públicos servidos en la raíz del dominio.
|
|
export const RESERVED_SLUGS = new Set([
|
|
'panel',
|
|
'login',
|
|
'logout',
|
|
'signup',
|
|
'admin',
|
|
'solicitud',
|
|
'api',
|
|
'b2b',
|
|
'b2b-assets',
|
|
'assets',
|
|
'_next',
|
|
'favicon.ico',
|
|
'icon.svg',
|
|
'robots.txt',
|
|
'sitemap.xml',
|
|
]);
|
|
|
|
export function isReservedSlug(slug: string): boolean {
|
|
return RESERVED_SLUGS.has(slug);
|
|
}
|
|
|
|
export type SlugValidation = { ok: true; slug: string } | { ok: false; error: string };
|
|
|
|
// Normaliza y valida un slug propuesto para el funnel público de un reformista.
|
|
// No comprueba unicidad (eso requiere BD); solo formato y palabras reservadas.
|
|
export function validarSlug(raw: string): SlugValidation {
|
|
const slug = slugify(raw);
|
|
if (slug.length < 2) {
|
|
return { ok: false, error: 'El enlace debe tener al menos 2 caracteres.' };
|
|
}
|
|
if (slug.length > 40) {
|
|
return { ok: false, error: 'El enlace no puede superar los 40 caracteres.' };
|
|
}
|
|
if (isReservedSlug(slug)) {
|
|
return { ok: false, error: 'Ese enlace está reservado. Elige otro.' };
|
|
}
|
|
return { ok: true, slug };
|
|
}
|