Conectar funnel B2C real sin claves: captura → fotos → presupuesto

El formulario de la landing ahora crea un lead real en BD y redirige a
/solicitud/[id]/fotos, donde el cliente sube fotos y datos de la reforma.
El orquestador simula los pasos de IA (pre-llamada, llamada, render) y
calcula el presupuesto DE VERDAD con el catálogo del reformista, dejando
el lead listo en el panel con render y desglose.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Carlos Narro
2026-05-31 14:29:21 +02:00
parent b95c588efe
commit b582f3ac33
10 changed files with 733 additions and 19 deletions

View File

@@ -0,0 +1,153 @@
'use client';
import { useState } from 'react';
import { useFormStatus } from 'react-dom';
import { TIPO_LABEL } from '@/lib/funnel';
const TIPOS = ['cocina', 'bano', 'salon', 'comedor', 'integral', 'otro'] as const;
const CALIDADES = [
{ value: 'basica', label: 'Básica' },
{ value: 'media', label: 'Media' },
{ value: 'premium', label: 'Premium' },
] as const;
const MAX_FOTOS = 4;
const inputClass =
'w-full px-4 py-3 text-base font-sans text-dark bg-white border-[1.5px] border-gray-200 rounded-lg transition-all duration-150 outline-none placeholder:text-gray-400 focus:border-black focus:shadow-[0_0_0_3px_rgba(0,0,0,0.06)]';
function SubmitButton({ disabled }: { disabled: boolean }) {
const { pending } = useFormStatus();
return (
<button
type="submit"
className="btn btn-primary btn-lg w-full justify-center mt-1 disabled:opacity-60 disabled:cursor-not-allowed disabled:transform-none"
disabled={pending || disabled}
aria-busy={pending}
>
{pending ? (
<>
<span
className="w-[18px] h-[18px] border-2 border-white/30 border-t-white rounded-full animate-[spin_0.7s_linear_infinite] shrink-0"
aria-hidden="true"
/>
Generando tu presupuesto...
</>
) : (
'Generar mi presupuesto'
)}
</button>
);
}
export default function FotosUploader({
action,
}: {
action: (formData: FormData) => void | Promise<void>;
}) {
const [previews, setPreviews] = useState<string[]>([]);
const handleFiles = (e: React.ChangeEvent<HTMLInputElement>) => {
const files = Array.from(e.target.files ?? []).slice(0, MAX_FOTOS);
previews.forEach((url) => URL.revokeObjectURL(url));
setPreviews(files.map((f) => URL.createObjectURL(f)));
};
return (
<form action={action} className="flex flex-col gap-5">
{/* Fotos */}
<div className="flex flex-col gap-2">
<label htmlFor="fotos" className="text-sm font-semibold text-dark">
Sube fotos del espacio <span className="text-gray-400 font-normal">(hasta {MAX_FOTOS})</span>
</label>
<input
id="fotos"
name="fotos"
type="file"
accept="image/*"
multiple
onChange={handleFiles}
className="block w-full text-sm text-gray-600 file:mr-4 file:py-2.5 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-semibold file:bg-black file:text-white hover:file:bg-gray-800 file:cursor-pointer cursor-pointer"
/>
{previews.length > 0 && (
<div className="flex flex-wrap gap-3 mt-2">
{previews.map((url, i) => (
// eslint-disable-next-line @next/next/no-img-element
<img
key={i}
src={url}
alt=""
className="w-20 h-20 object-cover rounded-lg border border-gray-200"
/>
))}
</div>
)}
</div>
{/* Tipo de reforma */}
<div className="flex flex-col gap-2">
<label htmlFor="tipoReforma" className="text-sm font-semibold text-dark">
¿Qué quieres reformar?
</label>
<select id="tipoReforma" name="tipoReforma" defaultValue="cocina" className={inputClass}>
{TIPOS.map((t) => (
<option key={t} value={t}>
{TIPO_LABEL[t]}
</option>
))}
</select>
</div>
{/* m2 + calidad */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="flex flex-col gap-2">
<label htmlFor="m2" className="text-sm font-semibold text-dark">
Metros cuadrados <span className="text-gray-400 font-normal">(aprox.)</span>
</label>
<input
id="m2"
name="m2"
type="number"
min="1"
step="1"
inputMode="numeric"
placeholder="12"
className={inputClass}
/>
</div>
<div className="flex flex-col gap-2">
<label htmlFor="calidad" className="text-sm font-semibold text-dark">
Nivel de acabado
</label>
<select id="calidad" name="calidad" defaultValue="media" className={inputClass}>
{CALIDADES.map((c) => (
<option key={c.value} value={c.value}>
{c.label}
</option>
))}
</select>
</div>
</div>
{/* Provincia */}
<div className="flex flex-col gap-2">
<label htmlFor="provincia" className="text-sm font-semibold text-dark">
Provincia
</label>
<input
id="provincia"
name="provincia"
type="text"
placeholder="Madrid"
autoComplete="address-level1"
className={inputClass}
/>
</div>
<SubmitButton disabled={false} />
<p className="text-xs text-gray-400 text-center">
Calculamos un presupuesto orientativo con tus datos. Sin compromiso.
</p>
</form>
);
}