Añade stepper de progreso y bloque "Qué pasa después" al chooser de canal

El chooser (solicitud/[id]) ahora muestra un stepper de 3 pasos (Tus datos ✓, Tu reforma actual, Render + presupuesto pendiente) y, debajo de las tarjetas de canal, una sección "Elijas lo que elijas, esto es lo que pasa después" con los 3 pasos del flujo (nos cuentas tu reforma, render + presupuesto en minutos, visita gratuita para el presupuesto final).

Las tarjetas de canal pasan a grid de 3 columnas en desktop, con iconos SV
This commit is contained in:
Carlos Narro
2026-06-11 17:42:09 +02:00
parent facf3cd79f
commit dbef9ef670
3 changed files with 284 additions and 38 deletions

View File

@@ -327,9 +327,15 @@ Somos los únicos en España que combinamos **captura instantánea del lead + ll
- **Título del paso:** ¿Cómo prefieres contarnos tu reforma, [Nombre]?
- **Subtitle:** Tú eliges. Por cualquiera de las tres nos das lo que necesitamos para preparar tu render y tu presupuesto.
- **Stepper de progreso (encima del título):**
- Paso 1 (completado): *Tus datos*
- Paso 2 (actual): *Tu reforma*
- Paso 3 (pendiente): *Render + presupuesto*
- **Tarjeta Llamada — título:** Que te llamemos
**Descripción:** Un asistente te llama y te hace unas preguntas rápidas. Lo más cómodo: no escribes nada.
**CTA:** Quiero que me llamen
**Badge:** La más rápida
- **Tarjeta WhatsApp — título:** Por WhatsApp
**Descripción:** Seguimos por chat a tu ritmo. Puedes mandar fotos y notas cuando quieras.
**CTA:** Seguir por WhatsApp
@@ -337,6 +343,19 @@ Somos los únicos en España que combinamos **captura instantánea del lead + ll
**Descripción:** Tú lo cuentas zona por zona y subes las fotos. Recibes el presupuesto al instante.
**CTA:** Rellenar el formulario
#### Bloque "Qué pasa después" (debajo de las tarjetas del chooser)
> Recuerda al lead lo que va a recibir elija el canal que elija: personalización, render con
> imágenes en minutos, y visita gratuita posterior para el presupuesto definitivo.
- **Título:** Elijas lo que elijas, esto es lo que pasa después
- **Paso 1 — título:** Nos cuentas tu reforma a tu manera
**Body:** Fotos, medidas, gustos. Cuanto más nos cuentes, más se parecerá el resultado a lo que tienes en la cabeza.
- **Paso 2 — título:** Render + presupuesto en minutos
**Body:** Ves tu espacio ya reformado en imágenes, con un presupuesto orientativo desglosado partida a partida.
- **Paso 3 — título:** Visita gratuita para el presupuesto final
**Body:** Si te convence, acuerdas una visita con [Reformista]: ve la obra, la mide con detalle y te confirma el precio definitivo. Sin compromiso.
### Paso 2 (canal llamada)
- **Título del paso:** Te llamamos cuando quieras

View File

@@ -58,6 +58,20 @@
--transition-fast: 150ms ease;
--transition-base: 250ms ease;
--transition-slow: 400ms ease;
/* Animations (usar con motion-safe: para respetar prefers-reduced-motion) */
--animate-fade-up: fade-up 0.7s cubic-bezier(0.16, 1, 0.3, 1) both;
@keyframes fade-up {
from {
opacity: 0;
transform: translateY(14px);
}
to {
opacity: 1;
transform: none;
}
}
}
@layer base {

View File

@@ -1,82 +1,295 @@
import Link from 'next/link';
import { notFound } from 'next/navigation';
import { getPublicLead } from '@/lib/funnel/public-queries';
import { resolveTheme, themeStyle } from '@/lib/funnel/themes';
import TenantBrand from '@/components/funnel/TenantBrand';
export const dynamic = 'force-dynamic';
const BRAND = 'var(--brand, #0a0a0a)';
const BRAND_CONTRAST = 'var(--brand-contrast, #ffffff)';
function IconLlamada() {
return (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path
d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
function IconWhatsapp() {
return (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path
d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
function IconFormulario() {
return (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path
d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M14 2v6h6M16 13H8M16 17H8M10 9H8"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
const CANALES = [
{
slug: 'llamada',
icon: '📞',
icon: <IconLlamada />,
titulo: 'Que te llamemos',
descripcion:
'Un asistente te llama y te hace unas preguntas rápidas. Lo más cómodo: no escribes nada.',
cta: 'Quiero que me llamen',
badge: 'La más rápida',
},
{
slug: 'whatsapp',
icon: '💬',
icon: <IconWhatsapp />,
titulo: 'Por WhatsApp',
descripcion:
'Seguimos por chat a tu ritmo. Puedes mandar fotos y notas cuando quieras.',
descripcion: 'Seguimos por chat a tu ritmo. Puedes mandar fotos y notas cuando quieras.',
cta: 'Seguir por WhatsApp',
badge: null,
},
{
slug: 'formulario',
icon: '📝',
icon: <IconFormulario />,
titulo: 'Rellenar un formulario',
descripcion:
'Tú lo cuentas zona por zona y subes las fotos. Recibes el presupuesto al instante.',
cta: 'Rellenar el formulario',
badge: null,
},
] as const;
const PASOS_DESPUES = [
{
titulo: 'Nos cuentas tu reforma a tu manera',
body: 'Fotos, medidas, gustos. Cuanto más nos cuentes, más se parecerá el resultado a lo que tienes en la cabeza.',
},
{
titulo: 'Render + presupuesto en minutos',
body: 'Ves tu espacio ya reformado en imágenes, con un presupuesto orientativo desglosado partida a partida.',
},
// El tercer paso interpola el nombre del reformista; se monta en el componente.
] as const;
function Stepper() {
const conectores = 'h-px flex-1 min-w-4';
return (
<ol className="flex items-center gap-2.5 sm:gap-3" aria-label="Progreso de tu solicitud">
<li className="flex items-center gap-2 shrink-0">
<span
className="w-6 h-6 rounded-full flex items-center justify-center shrink-0"
style={{ backgroundColor: BRAND, color: BRAND_CONTRAST }}
>
<svg width="12" height="12" viewBox="0 0 32 32" fill="none" aria-hidden="true">
<path
d="M6 16l7 7L26 9"
stroke="currentColor"
strokeWidth="4"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</span>
<span className="hidden sm:inline text-xs font-semibold text-gray-600">Tus datos</span>
</li>
<li aria-hidden="true" className={conectores} style={{ backgroundColor: BRAND }} />
<li className="flex items-center gap-2 shrink-0" aria-current="step">
<span
className="w-6 h-6 rounded-full text-[11px] font-black flex items-center justify-center shrink-0"
style={{ backgroundColor: BRAND, color: BRAND_CONTRAST }}
>
2
</span>
<span className="text-xs font-bold text-black">Tu reforma</span>
</li>
<li aria-hidden="true" className={`${conectores} bg-gray-200`} />
<li className="flex items-center gap-2 shrink-0">
<span className="w-6 h-6 rounded-full bg-white border border-gray-300 text-[11px] font-bold text-gray-400 flex items-center justify-center shrink-0">
3
</span>
<span className="hidden sm:inline text-xs font-semibold text-gray-400">
Render + presupuesto
</span>
</li>
</ol>
);
}
export default async function ChooserPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const data = await getPublicLead(id);
if (!data) notFound();
const { lead, tenant } = data;
const theme = resolveTheme(tenant?.themePreset, tenant?.themeColor);
const nombrePila = lead.nombre.split(' ')[0];
const nombreReformista = tenant?.nombreEmpresa ?? 'el reformista';
const pasosDespues = [
...PASOS_DESPUES,
{
titulo: 'Visita gratuita para el presupuesto final',
body: `Si te convence, acuerdas una visita con ${nombreReformista}: ve la obra, la mide con detalle y te confirma el precio definitivo. Sin compromiso.`,
},
];
return (
<>
<div
className={theme.heading === 'serif' ? 'theme-serif' : undefined}
style={themeStyle(tenant?.themePreset, tenant?.themeColor)}
>
{tenant && <TenantBrand nombreEmpresa={tenant.nombreEmpresa} logoUrl={tenant.logoUrl} />}
<div className="container py-10 max-w-2xl flex flex-col gap-6">
<div className="flex flex-col gap-2">
<span className="text-xs font-semibold uppercase tracking-widest text-gray-400">
Elige cómo seguir
</span>
<h1 className="text-2xl font-black tracking-tight text-black">
¿Cómo prefieres contarnos tu reforma, {lead.nombre.split(' ')[0]}?
</h1>
<p className="text-sm text-gray-500 leading-relaxed">
eliges. Por cualquiera de las tres nos das lo que necesitamos para preparar tu render
y tu presupuesto.
</p>
</div>
<div className="flex flex-col gap-3">
{CANALES.map((c) => (
<Link
key={c.slug}
href={`/solicitud/${id}/${c.slug}`}
className="group bg-white border border-gray-200 rounded-xl p-5 shadow-sm flex items-center gap-4 transition-all hover:border-black hover:shadow-md"
>
<span className="text-3xl shrink-0" aria-hidden="true">
{c.icon}
</span>
<div className="flex flex-col gap-0.5 min-w-0">
<span className="text-base font-bold text-black">{c.titulo}</span>
<span className="text-sm text-gray-500 leading-snug">{c.descripcion}</span>
<span className="text-sm font-semibold text-[color:var(--brand,#0a0a0a)] mt-1">
{c.cta}
<div className="relative overflow-hidden">
{/* Halo sutil con el color de marca del reformista */}
<div
aria-hidden="true"
className="absolute inset-x-0 top-0 h-80 pointer-events-none"
style={{
background: `radial-gradient(60% 100% at 50% 0%, color-mix(in srgb, ${BRAND} 8%, transparent), transparent 70%)`,
}}
/>
<div className="container relative max-w-4xl py-10 md:py-14 flex flex-col gap-8 md:gap-10">
<div className="motion-safe:animate-fade-up">
<Stepper />
</div>
<header
className="flex flex-col gap-3 max-w-2xl motion-safe:animate-fade-up"
style={{ animationDelay: '80ms' }}
>
<span className="text-xs font-semibold uppercase tracking-widest text-gray-400">
Elige cómo seguir
</span>
<h1 className="text-3xl md:text-4xl font-black tracking-tight text-black leading-[1.1] text-balance">
¿Cómo prefieres contarnos tu reforma, {nombrePila}?
</h1>
<p className="text-sm md:text-base text-gray-500 leading-relaxed">
eliges. Por cualquiera de las tres nos das lo que necesitamos para preparar tu
render y tu presupuesto.
</p>
</header>
<div className="grid gap-3 md:grid-cols-3 md:gap-4">
{CANALES.map((c, i) => (
<Link
key={c.slug}
href={`/solicitud/${id}/${c.slug}`}
className="group relative flex items-start gap-4 md:flex-col md:gap-5 bg-white border border-gray-200 rounded-2xl p-5 md:p-6 shadow-sm transition-all duration-250 hover:-translate-y-0.5 hover:border-[color:var(--brand,#0a0a0a)] hover:shadow-[0_16px_40px_-12px_rgba(0,0,0,0.18)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--brand,#0a0a0a)] focus-visible:ring-offset-2 motion-safe:animate-fade-up"
style={{ animationDelay: `${160 + i * 80}ms` }}
>
{c.badge && (
<span
className="absolute top-0 right-5 -translate-y-1/2 px-2.5 py-1 rounded-full text-[10px] font-bold uppercase tracking-widest"
style={{ backgroundColor: BRAND, color: BRAND_CONTRAST }}
>
{c.badge}
</span>
)}
<span
className="w-12 h-12 rounded-xl flex items-center justify-center shrink-0 transition-transform duration-250 group-hover:scale-105"
style={{ backgroundColor: BRAND, color: BRAND_CONTRAST }}
aria-hidden="true"
>
{c.icon}
</span>
</div>
</Link>
))}
<span className="flex flex-col gap-1.5 min-w-0 flex-1">
<h2 className="text-lg font-black tracking-tight text-black leading-snug">
{c.titulo}
</h2>
<span className="text-sm text-gray-500 leading-relaxed">{c.descripcion}</span>
<span
className="flex items-center gap-1.5 text-sm font-bold mt-2 md:mt-auto md:pt-4"
style={{ color: BRAND }}
>
{c.cta}
<svg
width="15"
height="15"
viewBox="0 0 16 16"
fill="none"
aria-hidden="true"
className="transition-transform duration-250 group-hover:translate-x-1"
>
<path
d="M2 8h12M10 4l4 4-4 4"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</span>
</span>
</Link>
))}
</div>
<section
className="bg-white border border-gray-200 rounded-2xl p-6 md:p-8 shadow-sm motion-safe:animate-fade-up"
style={{ animationDelay: '420ms' }}
aria-labelledby="que-pasa-despues"
>
<h2
id="que-pasa-despues"
className="text-base md:text-lg font-black tracking-tight text-black"
>
Elijas lo que elijas, esto es lo que pasa después
</h2>
<ol className="relative mt-6 grid gap-6 md:grid-cols-3 md:gap-8">
{/* Línea que conecta los pasos en desktop */}
<div
aria-hidden="true"
className="hidden md:block absolute top-[15px] left-8 right-8 h-px bg-gray-200"
/>
{pasosDespues.map((paso, i) => (
<li key={paso.titulo} className="relative flex items-start gap-4 md:flex-col">
<span
className="relative w-8 h-8 rounded-full text-[13px] font-black flex items-center justify-center shrink-0 ring-4 ring-white"
style={{ backgroundColor: BRAND, color: BRAND_CONTRAST }}
aria-hidden="true"
>
{i + 1}
</span>
<div className="flex flex-col gap-1 min-w-0">
<h3 className="text-sm font-bold text-black leading-snug">{paso.titulo}</h3>
<p className="text-sm text-gray-500 leading-relaxed">{paso.body}</p>
</div>
</li>
))}
</ol>
</section>
</div>
</div>
</>
</div>
);
}