Añade personalización SEO/Quiénes somos y testimonios gestionables por reformista

- Panel/empresa: title y meta description SEO personalizables; foto, texto y
  años de experiencia para el bloque "Quiénes somos" (toggle on/off).
- Funnel por slug: metadata SEO desde el tenant, bloque "Quiénes somos" y
  testimonios servidos desde DB (sustituye los hardcodeados).
- Flujo de opiniones: el reformista solicita la opinión desde la ficha de un
  lead ganado; el cliente la deja en un funnel dedicado /opinion/[id] con
  estrellas + texto + fotos; entra como pendiente y el reformista la modera
  (publicar/ocultar/eliminar) en /panel/opiniones antes de mostrarla.
- Schema: columnas SEO/about en tenants, testimonioSolicitadoAt en leads,
  enum testimonio_estado, tablas testimonios + testimonio_fotos (migración 0006).
- Seed: opiniones demo (2 publicadas, 1 pendiente) y contenido "Quiénes somos".

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Carlos Narro
2026-06-01 12:26:13 +02:00
parent 1a1caaf0df
commit a91fe5ce2c
25 changed files with 2638 additions and 66 deletions

View File

@@ -0,0 +1,50 @@
import { notFound } from 'next/navigation';
import { getLeadForReview } from '@/lib/funnel/public-queries';
import { enviarOpinion } from '../actions';
import OpinionForm from '@/components/funnel/OpinionForm';
import TenantBrand from '@/components/funnel/TenantBrand';
export const dynamic = 'force-dynamic';
export default async function OpinionPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const data = await getLeadForReview(id);
if (!data || !data.tenant) notFound();
const { lead, tenant, yaEnviado } = data;
return (
<>
<TenantBrand
nombreEmpresa={tenant.nombreEmpresa}
logoUrl={tenant.logoUrl}
subtitle="Tu opinión"
/>
<div className="container py-10 max-w-2xl flex flex-col gap-6">
<div className="flex flex-col gap-2">
<h1 className="text-2xl font-black tracking-tight text-black">
{lead.nombre.split(' ')[0]}, ¿qué tal fue tu reforma con {tenant.nombreEmpresa}?
</h1>
<p className="text-sm text-gray-500 leading-relaxed">
Tu opinión nos ayuda muchísimo. Cuéntanos cómo fue y, si quieres, sube alguna foto del
resultado.
</p>
</div>
{yaEnviado ? (
<div className="bg-white border border-gray-200 rounded-xl p-8 text-center flex flex-col gap-3">
<span className="text-4xl" aria-hidden="true">
</span>
<h2 className="text-xl font-black text-black">Ya hemos recibido tu opinión</h2>
<p className="text-sm text-gray-500">¡Gracias por tomarte el tiempo!</p>
</div>
) : (
<div className="bg-white border border-gray-200 rounded-xl p-6 md:p-8 shadow-sm">
<OpinionForm action={enviarOpinion.bind(null, id)} nombreCliente={lead.nombre} />
</div>
)}
</div>
</>
);
}