feat: catálogo como centro del embudo y captura de baja fricción

Reestructuración pedida por el cliente tras revisar la primera versión.

Conversión:
- Formulario de 14 campos a 4: nombre, correo, teléfono y mensaje. La
  clasificación de riesgo ya no se pregunta, se deduce del contexto de la
  página y viaja oculta. Menos fricción, misma riqueza para el análisis.
- El inicio deja de bifurcar y empuja al catálogo: 36 llamados desembocan
  ahí, y las rutas por industria y escenario pasan a ser la forma de entrar
  al catálogo ya filtrado, en lugar de competir como destino.
- Catálogo rehecho como tienda: barra lateral con filtros, búsqueda que
  acepta expresiones regulares, títulos que abren la ficha, un solo botón
  primario por tarjeta y el comparador detrás de un desplegable.
- Ficha de producto orientada al cierre: bloque en lenguaje llano sobre si
  el equipo corresponde al caso, y "Cotizar este equipo" con el formulario
  precargado. Se retiraron los enlaces que sacaban del embudo a leer normas.
- Todos los llamados se unifican en "Recibir cotización".
- Widget de WhatsApp: nombre, teléfono, correo y mensaje libre. El texto que
  escribe la persona es el que viaja a la conversación.

Doble audiencia:
- 13 escenarios con la iconografía original de FSPM traducen un lugar
  reconocible a clases de fuego y productos, para quien no domina la
  terminología. La capa técnica se conserva para búsqueda.
- Héroe con fotografía en todas las landings.

Correcciones:
- El contenedor moría al arrancar porque el CLI de Prisma no podía escribir
  sus motores; el chown ahora incluye /app/prisma-cli.
- Iconos repetidos entre escenarios. El set original no tiene icono de
  cocina, así que ese escenario usa un marcador tipográfico explícito.
- FIREMIKS no aparecía en ningún escenario.
- Los ids de campo del formulario colisionaban con dos instancias por página.
- Se documentó el despliegue, que faltaba por completo.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
Uriel Jareth
2026-08-28 01:30:44 -06:00
co-authored by Claude Opus 5
parent 3c6bdcfc06
commit 0e0331c1fc
580 changed files with 7239 additions and 2683 deletions
+235 -81
View File
@@ -1,5 +1,8 @@
import type { Metadata } from "next";
import Link from "next/link";
import { FormularioContacto } from "@/components/formulario/FormularioContacto";
import { Migas } from "@/components/site/contenido/Migas";
import { FOTOS, HeroeLanding } from "@/components/site/HeroeLanding";
import { AVISO_DEMO, EMPRESA, SITIO_URL } from "@/data/empresa";
/**
@@ -7,18 +10,24 @@ import { AVISO_DEMO, EMPRESA, SITIO_URL } from "@/data/empresa";
*
* Reproduce, reordenado para poder leerse, el aviso publicado por FSPM en su
* sitio público, más una sección que declara qué recaba realmente el formulario
* de esta demo. Lo que no estaba en la fuente no se agregó, y la versión
* de esta demo —cuatro campos visibles y el contexto de la página— y un resumen
* en lenguaje llano arriba, para que alguien pueda enterarse sin leer nueve
* apartados. Lo que no estaba en la fuente no se agregó, y la versión
* definitiva es siempre la que publique la empresa.
*
* Va con `robots: { index: false }` a propósito: es la reproducción de un
* documento legal de un tercero dentro de una demo, y no queremos que compita
* en buscadores con el aviso que la empresa publica en su propio dominio.
*/
const TITULO = "Aviso de privacidad";
const DESCRIPCION = `Aviso de privacidad de ${EMPRESA.razonSocial}: responsable, finalidades del tratamiento, transferencias, medidas de seguridad y ejercicio de derechos ARCO.`;
const DESCRIPCION = `Aviso de privacidad de ${EMPRESA.razonSocial}: quién es el responsable, qué datos recaba este sitio, para qué se usan, a quién se transfieren y cómo ejercer tus derechos de acceso, rectificación, cancelación y oposición.`;
export const metadata: Metadata = {
title: TITULO,
description: DESCRIPCION,
alternates: { canonical: `${SITIO_URL}/aviso-de-privacidad` },
robots: { index: true, follow: true },
robots: { index: false, follow: true },
openGraph: {
title: `${TITULO} | ${EMPRESA.nombreCorto}`,
description: DESCRIPCION,
@@ -28,60 +37,101 @@ export const metadata: Metadata = {
const CORREO_ARCO = "[email protected]";
const CAMPOS_FORMULARIO = [
{ campo: "Nombre", uso: "Identificarte y dirigirnos a ti por tu nombre en el seguimiento." },
const MENSAJE_INICIAL = "Mi duda sobre este sitio es ";
/** El aviso en cinco frases, para quien no va a leer los nueve apartados. */
const EN_CORTO = [
"Tus datos los trata Enforcer Units Fire Service Plus México, S.A. de C.V.",
"Se usan para identificarte, contactarte y darle seguimiento a tu solicitud.",
"El aviso original contempla también envío de invitaciones, noticias y campañas.",
"Contempla transferencias a asociados y a terceros para esas mismas finalidades.",
`Puedes pedir acceso, corrección, cancelación u oposición escribiendo a ${CORREO_ARCO}.`,
];
/**
* Qué recaba el formulario de este sitio. Cuatro campos visibles y el contexto
* que se deduce de la página, sin preguntarlo.
*/
const CAMPOS_VISIBLES = [
{ campo: "Nombre completo", uso: "Identificarte y dirigirnos a ti por tu nombre en el seguimiento." },
{ campo: "Correo electrónico", uso: "Enviarte la respuesta técnica y la propuesta." },
{ campo: "Teléfono", uso: "Contactarte cuando la solicitud requiere una llamada." },
{ campo: "Empresa y puesto", uso: "Entender quién decide y con qué alcance, para no dar una respuesta genérica." },
{ campo: "Ciudad o estado", uso: "Ubicar la instalación y saber qué requisitos locales pueden aplicar." },
{ campo: "Tipo de operación y superficie", uso: "Interpretar el escenario y proponer una configuración." },
{ campo: "Clases de fuego declaradas", uso: "Clasificar el riesgo que tú mismo declaras en el selector." },
{ campo: "Urgencia y mensaje", uso: "Priorizar la atención y conocer el requerimiento en tus palabras." },
{
campo: "Datos de origen de la visita",
uso: "Saber por qué campaña o página llegaste, para medir qué contenido resuelve dudas reales.",
campo: "Mensaje",
uso: "Conocer el requerimiento en tus palabras. Lo que escribas aquí es lo único que decides compartir.",
},
{
campo: "Aceptación de este aviso",
uso: "Registrar que aceptaste el tratamiento antes de enviar. Sin esa casilla no se envía nada.",
},
];
const CAMPOS_CONTEXTO = [
{
campo: "Página desde la que escribiste",
uso: "Saber si llegaste desde una ficha de producto, un segmento o la página de cumplimiento, para responder sobre eso.",
},
{
campo: "Producto, categoría, industria o escenario de esa página",
uso: "Interpretar el escenario sin obligarte a llenar un cuestionario técnico.",
},
{
campo: "Clases de fuego asociadas a esa página",
uso: "Preparar la selección de equipo antes de la primera llamada.",
},
{
campo: "Origen de la visita (campaña, buscador o enlace)",
uso: "Medir qué contenido resuelve dudas reales. No identifica a nadie por sí solo.",
},
];
export default function PaginaAviso() {
return (
<>
{/* ---------- encabezado ---------- */}
<section className="bg-ink pt-12 pb-10 sm:pt-16 sm:pb-12">
<HeroeLanding
antes={<Migas actual="Aviso de privacidad" />}
eyebrow="Documento legal"
titulo="Aviso de privacidad"
entrada={`Reproducción del aviso publicado por ${EMPRESA.razonSocial}, reordenado para poder leerse. La versión vigente y definitiva es la que la empresa publique en su propio sitio.`}
foto={FOTOS.mapa}
enfoque="45% 45%"
prioridad
>
<div
role="note"
aria-label="Naturaleza de este sitio"
className="max-w-2xl rounded-[6px] border border-rule border-l-2 border-l-ember bg-ink-2/90 p-5"
>
<p className="font-mono text-[0.6875rem] uppercase tracking-[0.16em] text-ember">
Sobre este sitio
</p>
<p className="mt-3 text-sm leading-relaxed text-muted">{AVISO_DEMO}</p>
</div>
</HeroeLanding>
{/* ---------- el aviso en corto ---------- */}
<section className="border-t border-rule bg-ink-2 py-12 sm:py-14" aria-labelledby="en-corto">
<div className="shell">
<nav aria-label="Ruta de navegación" className="mb-8">
<ol className="flex flex-wrap items-center gap-2 font-mono text-[0.625rem] uppercase tracking-[0.14em] text-faint">
<li>
<Link href="/" className="hover:text-bright">
Inicio
</Link>
</li>
<li aria-hidden="true">/</li>
<li className="text-bright">Aviso de privacidad</li>
</ol>
</nav>
<div className="max-w-3xl">
<p className="eyebrow mb-5">Documento legal</p>
<h1 className="font-display text-[1.875rem] font-black uppercase leading-[1.04] tracking-tight sm:text-[2.5rem]">
Aviso de privacidad
</h1>
<p className="mt-5 text-sm leading-relaxed text-muted sm:text-base">
Reproducción del aviso publicado por {EMPRESA.razonSocial}, reordenado para poder
leerse. La versión vigente y definitiva es la que la empresa publique en su propio
sitio.
<p className="eyebrow eyebrow-muted mb-4">En corto</p>
<h2 id="en-corto" className="font-display text-xl font-extrabold sm:text-2xl">
Lo esencial en cinco frases
</h2>
<p className="mt-3 text-sm leading-relaxed text-muted">
Este resumen no sustituye al aviso: es una entrada para que puedas leerlo sabiendo qué
buscar. El texto que manda es el de los nueve apartados de abajo.
</p>
</div>
<div
role="note"
aria-label="Naturaleza de este sitio"
className="mt-8 max-w-3xl rounded-[6px] border border-rule border-l-2 border-l-ember bg-ink-2 p-5"
>
<p className="font-mono text-[0.6875rem] uppercase tracking-[0.16em] text-ember">
Sobre este sitio
</p>
<p className="mt-3 text-sm leading-relaxed text-muted">{AVISO_DEMO}</p>
<ul className="mt-6 space-y-3">
{EN_CORTO.map((punto) => (
<li key={punto} className="flex gap-3 text-sm leading-relaxed text-bright">
<span
aria-hidden="true"
className="mt-2 h-1.5 w-1.5 shrink-0 rounded-full bg-signal"
/>
{punto}
</li>
))}
</ul>
</div>
</div>
</section>
@@ -91,7 +141,10 @@ export default function PaginaAviso() {
<div className="shell">
<div className="max-w-3xl space-y-12">
<section aria-labelledby="responsable">
<h2 id="responsable" className="font-display text-xl font-extrabold text-deep sm:text-2xl">
<h2
id="responsable"
className="font-display text-xl font-extrabold text-deep sm:text-2xl"
>
1. Responsable del tratamiento
</h2>
<div className="mt-4 space-y-4 text-sm leading-relaxed text-deep-muted">
@@ -103,8 +156,8 @@ export default function PaginaAviso() {
</p>
<p>
{EMPRESA.nombreCorto} será el responsable del tratamiento que, con motivo de la
relación jurídica que haya sido celebrada y para el cumplimiento de la misma, se le
dé a tus datos personales.
relación jurídica que haya sido celebrada y para el cumplimiento de la misma, se
le dé a tus datos personales.
</p>
<p>
En cumplimiento de la Ley Federal de Protección de Datos Personales en Posesión de
@@ -124,38 +177,82 @@ export default function PaginaAviso() {
2. Qué datos recaba este sitio
</h2>
<p className="mt-4 text-sm leading-relaxed text-deep-muted">
Esta sección no viene del aviso original: describe exactamente lo que pide el
formulario de este sitio, para que sepas qué estás enviando antes de enviarlo.
Esta sección no viene del aviso original: describe exactamente lo que recoge el
formulario de este sitio, para que sepas qué estás enviando antes de enviarlo. Son
cuatro campos visibles, más el contexto de la página en la que estabas.
</p>
<h3 className="mt-8 font-mono text-[0.6875rem] uppercase tracking-[0.16em] text-deep-muted">
Lo que escribes tú
</h3>
<div
className="mt-6 overflow-x-auto rounded-[6px] border border-rule-lite bg-white [-webkit-overflow-scrolling:touch]"
className="mt-4 overflow-x-auto rounded-[6px] border border-rule-lite bg-white [-webkit-overflow-scrolling:touch]"
tabIndex={0}
role="region"
aria-label="Datos que recaba el formulario, tabla desplazable en horizontal"
aria-label="Campos que llena la persona, tabla desplazable en horizontal"
>
<table className="w-full min-w-[36rem] border-collapse text-left">
<table className="w-full min-w-[34rem] border-collapse text-left">
<caption className="sr-only">
Campos del formulario de contacto y para qué se usa cada uno.
Campos visibles del formulario de contacto y para qué se usa cada uno.
</caption>
<thead>
<tr className="border-b border-rule-lite bg-paper-2">
<th
scope="col"
className="whitespace-nowrap px-4 py-3 font-mono text-[0.625rem] font-medium uppercase tracking-[0.14em] text-deep-muted"
>
Dato
</th>
<th
scope="col"
className="whitespace-nowrap px-4 py-3 font-mono text-[0.625rem] font-medium uppercase tracking-[0.14em] text-deep-muted"
>
Para qué se usa
</th>
{["Dato", "Para qué se usa"].map((c) => (
<th
key={c}
scope="col"
className="whitespace-nowrap px-4 py-3 font-mono text-[0.625rem] font-medium uppercase tracking-[0.14em] text-deep-muted"
>
{c}
</th>
))}
</tr>
</thead>
<tbody>
{CAMPOS_FORMULARIO.map((fila) => (
{CAMPOS_VISIBLES.map((fila) => (
<tr key={fila.campo} className="border-b border-rule-lite last:border-b-0">
<th scope="row" className="px-4 py-3.5 text-left align-top">
<span className="font-sans text-sm font-semibold text-deep">
{fila.campo}
</span>
</th>
<td className="px-4 py-3.5 align-top text-sm leading-relaxed text-deep-muted">
{fila.uso}
</td>
</tr>
))}
</tbody>
</table>
</div>
<h3 className="mt-8 font-mono text-[0.6875rem] uppercase tracking-[0.16em] text-deep-muted">
Lo que se deduce de la página, sin preguntártelo
</h3>
<div
className="mt-4 overflow-x-auto rounded-[6px] border border-rule-lite bg-white [-webkit-overflow-scrolling:touch]"
tabIndex={0}
role="region"
aria-label="Contexto que registra el formulario, tabla desplazable en horizontal"
>
<table className="w-full min-w-[34rem] border-collapse text-left">
<caption className="sr-only">
Contexto que el formulario registra a partir de la página de origen.
</caption>
<thead>
<tr className="border-b border-rule-lite bg-paper-2">
{["Dato", "Para qué se usa"].map((c) => (
<th
key={c}
scope="col"
className="whitespace-nowrap px-4 py-3 font-mono text-[0.625rem] font-medium uppercase tracking-[0.14em] text-deep-muted"
>
{c}
</th>
))}
</tr>
</thead>
<tbody>
{CAMPOS_CONTEXTO.map((fila) => (
<tr key={fila.campo} className="border-b border-rule-lite last:border-b-0">
<th scope="row" className="px-4 py-3.5 text-left align-top">
<span className="font-sans text-sm font-semibold text-deep">
@@ -181,7 +278,10 @@ export default function PaginaAviso() {
<hr className="hair-lite" />
<section aria-labelledby="finalidades">
<h2 id="finalidades" className="font-display text-xl font-extrabold text-deep sm:text-2xl">
<h2
id="finalidades"
className="font-display text-xl font-extrabold text-deep sm:text-2xl"
>
3. Finalidades del tratamiento
</h2>
<div className="mt-4 space-y-4 text-sm leading-relaxed text-deep-muted">
@@ -193,10 +293,10 @@ export default function PaginaAviso() {
establecidos.
</p>
<p>
Esto incluye enviar, por cualquier medio físico, electrónico o magnético, de manera
enunciativa mas no limitativa: invitación a reuniones de trabajo, minutas, noticias
y anuncios en general relacionados con la institución, campañas publicitarias
afines e invitación a eventos.
Esto incluye enviar, por cualquier medio físico, electrónico o magnético, de
manera enunciativa mas no limitativa: invitación a reuniones de trabajo, minutas,
noticias y anuncios en general relacionados con la institución, campañas
publicitarias afines e invitación a eventos.
</p>
</div>
</section>
@@ -204,7 +304,10 @@ export default function PaginaAviso() {
<hr className="hair-lite" />
<section aria-labelledby="transferencias">
<h2 id="transferencias" className="font-display text-xl font-extrabold text-deep sm:text-2xl">
<h2
id="transferencias"
className="font-display text-xl font-extrabold text-deep sm:text-2xl"
>
4. Transferencias
</h2>
<p className="mt-4 text-sm leading-relaxed text-deep-muted">
@@ -217,7 +320,10 @@ export default function PaginaAviso() {
<hr className="hair-lite" />
<section aria-labelledby="seguridad">
<h2 id="seguridad" className="font-display text-xl font-extrabold text-deep sm:text-2xl">
<h2
id="seguridad"
className="font-display text-xl font-extrabold text-deep sm:text-2xl"
>
5. Medidas de seguridad
</h2>
<p className="mt-4 text-sm leading-relaxed text-deep-muted">
@@ -230,7 +336,10 @@ export default function PaginaAviso() {
<hr className="hair-lite" />
<section aria-labelledby="consentimiento">
<h2 id="consentimiento" className="font-display text-xl font-extrabold text-deep sm:text-2xl">
<h2
id="consentimiento"
className="font-display text-xl font-extrabold text-deep sm:text-2xl"
>
6. Excepciones al consentimiento
</h2>
<p className="mt-4 text-sm leading-relaxed text-deep-muted">
@@ -249,8 +358,8 @@ export default function PaginaAviso() {
<div className="mt-4 space-y-4 text-sm leading-relaxed text-deep-muted">
<p>
El ejercicio de los derechos de Acceso, Rectificación, Cancelación y Oposición
respecto de tus datos personales, así como la revocación del consentimiento para su
tratamiento, podrá realizarse a través de los procedimientos implementados o
respecto de tus datos personales, así como la revocación del consentimiento para
su tratamiento, podrá realizarse a través de los procedimientos implementados o
solicitándolo al siguiente correo electrónico:
</p>
<p>
@@ -268,16 +377,19 @@ export default function PaginaAviso() {
8. Cambios al aviso
</h2>
<p className="mt-4 text-sm leading-relaxed text-deep-muted">
{EMPRESA.nombreCorto} podrá enmendar o modificar el presente aviso de privacidad como
lo estime conveniente, e informará y pondrá a tu disposición el aviso actualizado en
su sitio web.
{EMPRESA.nombreCorto} podrá enmendar o modificar el presente aviso de privacidad
como lo estime conveniente, e informará y pondrá a tu disposición el aviso
actualizado en su sitio web.
</p>
</section>
<hr className="hair-lite" />
<section aria-labelledby="contacto-aviso">
<h2 id="contacto-aviso" className="font-display text-xl font-extrabold text-deep sm:text-2xl">
<h2
id="contacto-aviso"
className="font-display text-xl font-extrabold text-deep sm:text-2xl"
>
9. Contacto
</h2>
<dl className="spec mt-5 sm:grid-cols-2">
@@ -311,6 +423,48 @@ export default function PaginaAviso() {
</div>
</div>
</article>
{/* ---------- salida ---------- */}
<section
className="border-t border-rule bg-ink py-14 sm:py-20"
aria-labelledby="dudas-titulo"
>
<div className="shell">
<div className="grid gap-10 lg:grid-cols-[1fr_1.05fr] lg:items-start lg:gap-16">
<div>
<p className="eyebrow mb-4">Si te quedó una duda</p>
<h2 id="dudas-titulo" className="font-display text-2xl font-extrabold sm:text-3xl">
Pregunta antes de enviar tus datos, no después
</h2>
<p className="mt-4 text-sm leading-relaxed text-muted sm:text-base">
Para ejercer tus derechos de acceso, rectificación, cancelación u oposición, el canal
es el correo del apartado 7:{" "}
<a href={`mailto:${CORREO_ARCO}`} className="link break-all font-mono text-xs">
{CORREO_ARCO}
</a>
. Este formulario es para lo demás: dudas sobre el sitio, sobre qué se registra o
sobre lo que necesitas proteger.
</p>
<p className="mt-4 text-sm leading-relaxed text-muted">
Y si lo que buscabas era información técnica,{" "}
<Link href="/cumplimiento-nom-002" className="link">
la página de cumplimiento
</Link>{" "}
es un mejor punto de partida.
</p>
</div>
<FormularioContacto
id="formulario-aviso"
canal="catalogo"
titulo="Escribirnos"
descripcion="Completa la frase del mensaje con tu duda. Aplica el mismo aviso de privacidad que acabas de leer."
mensajeInicial={MENSAJE_INICIAL}
textoBoton="Enviar mi duda"
/>
</div>
</div>
</section>
</>
);
}