feat: sitio comercial FSPM con micro CRM interno
Sitio público orientado a venta consultiva e implementación de las recomendaciones de la auditoría de marca y mercado: - Inicio con tres rutas (industria, riesgo, cumplimiento) y clasificador de riesgo en el héroe, contra el hallazgo de que el visitante tenía que inferir qué solución le correspondía. - Cinco páginas por segmento prioritario y catálogo comparable por clase de fuego, capacidad y agente. - Los claims sin evidencia por modelo quedan aislados y se presentan como compromiso de documentación, nunca como característica del producto. - SEO técnico completo: metadatos por página, sitemap derivado de los datos, JSON-LD, iconos y tarjeta social generadas. Micro CRM: - Captura por formulario embebido y por widget de WhatsApp que registra antes de abrir la conversación. - Atribución de primer toque que sobrevive a la navegación. - Deduplicación de contacto; un mismo contacto acumula oportunidades. - Motor de asignación por reglas con reparto por turno de respaldo. - Panel con dos roles de alcance distinto, tablero analítico, bitácora, pedidos y reglas. No enlazado desde el sitio público y con noindex. Empaquetado en Docker multietapa con la base de demostración horneada en la imagen y el volumen de datos separado, para que los leads capturados sobrevivan a los redespliegues. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
122e06a4bb
commit
01b817d098
@@ -0,0 +1,240 @@
|
||||
import type { Metadata } from "next";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { FormularioContacto } from "@/components/formulario/FormularioContacto";
|
||||
import { Revelar } from "@/components/site/Revelar";
|
||||
import { ExploradorCatalogo } from "@/components/site/catalogo/ExploradorCatalogo";
|
||||
import { CATEGORIAS, PRODUCTOS, productosDeCategoria } from "@/data/catalogo";
|
||||
import { CLASES_ORDENADAS } from "@/data/clases-fuego";
|
||||
import { SITIO_URL } from "@/data/empresa";
|
||||
|
||||
/**
|
||||
* /catalogo — la corrección directa al hallazgo 5.1 del estudio: ocho
|
||||
* categorías sin rutas por riesgo ni aplicación.
|
||||
*
|
||||
* No es una galería de producto. Es un comparador: se entra por clase de fuego
|
||||
* y por aplicación, y se sale con dos o tres equipos que descartar o cotizar.
|
||||
*/
|
||||
|
||||
const TITULO = "Catálogo de equipos y agentes contra incendio";
|
||||
const DESCRIPCION =
|
||||
"Compara los 22 equipos y agentes de FSPM por clase de fuego, capacidad, agente y alcance declarado. Filtra por el riesgo que tienes, no por el nombre del producto.";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: TITULO,
|
||||
description: DESCRIPCION,
|
||||
alternates: { canonical: `${SITIO_URL}/catalogo` },
|
||||
openGraph: {
|
||||
title: TITULO,
|
||||
description: DESCRIPCION,
|
||||
url: `${SITIO_URL}/catalogo`,
|
||||
},
|
||||
};
|
||||
|
||||
export default function PaginaCatalogo() {
|
||||
const resumen = [
|
||||
{ clave: "Categorías", valor: String(CATEGORIAS.length) },
|
||||
{ clave: "Equipos y agentes", valor: String(PRODUCTOS.length) },
|
||||
{ clave: "Clases de fuego", valor: `${CLASES_ORDENADAS.length} · A B C D K` },
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* ---------- encabezado: la lógica de lectura del catálogo ---------- */}
|
||||
<section className="grid-bg relative overflow-hidden bg-ink pt-14 pb-16 sm:pt-20 sm:pb-20">
|
||||
<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">Catálogo</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<div className="grid gap-12 lg:grid-cols-[1.15fr_1fr] lg:items-start">
|
||||
<div className="anim-rise">
|
||||
<p className="eyebrow mb-5">Catálogo técnico</p>
|
||||
<h1 className="font-display text-[2.125rem] font-black uppercase leading-[1.02] tracking-tight sm:text-5xl lg:text-[3.5rem]">
|
||||
Un catálogo se lee por clase de fuego, no por nombre de producto
|
||||
</h1>
|
||||
<p className="mt-6 max-w-xl text-base leading-relaxed text-muted sm:text-lg">
|
||||
Nadie compra «un extintor». Se compra la respuesta a un escenario: qué
|
||||
se está quemando, dónde, quién va a operar el equipo y qué documentación tiene que
|
||||
quedar en el expediente. Este catálogo está ordenado en ese sentido.
|
||||
</p>
|
||||
|
||||
<dl className="spec mt-9 sm:grid-cols-3">
|
||||
{resumen.map((r) => (
|
||||
<div key={r.clave} className="spec-cell">
|
||||
<dt className="spec-key">{r.clave}</dt>
|
||||
<dd className="spec-val">{r.valor}</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
|
||||
<div className="mt-9 flex flex-wrap gap-3">
|
||||
<Link href="#cotizar" className="btn btn-signal">
|
||||
Solicitar diagnóstico
|
||||
</Link>
|
||||
<Link href="/evidencia" className="btn btn-ghost">
|
||||
Ver qué documentación aplica
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* cómo elegir: el criterio, no el producto */}
|
||||
<aside className="panel p-6 sm:p-7" aria-labelledby="criterio-titulo">
|
||||
<p className="eyebrow eyebrow-muted mb-4">Cómo elegir</p>
|
||||
<h2 id="criterio-titulo" className="font-display text-lg font-extrabold sm:text-xl">
|
||||
Tres preguntas antes de mirar un solo equipo
|
||||
</h2>
|
||||
<ol className="mt-6 space-y-5">
|
||||
{[
|
||||
{
|
||||
n: "01",
|
||||
t: "¿Qué se quema?",
|
||||
d: "Define la clase de fuego. El agente equivocado no solo falla: puede empeorar el incidente.",
|
||||
},
|
||||
{
|
||||
n: "02",
|
||||
t: "¿Quién responde y en cuántos segundos?",
|
||||
d: "Determina si el equipo debe ser portátil, móvil, fijo o automático.",
|
||||
},
|
||||
{
|
||||
n: "03",
|
||||
t: "¿Qué te van a pedir por escrito?",
|
||||
d: "Aseguradora, auditoría y Protección Civil piden documentos distintos. Eso define el expediente.",
|
||||
},
|
||||
].map((paso) => (
|
||||
<li key={paso.n} className="flex gap-4">
|
||||
<span className="font-mono text-[0.625rem] font-semibold tracking-[0.08em] text-signal">
|
||||
{paso.n}
|
||||
</span>
|
||||
<div>
|
||||
<h3 className="font-display text-sm font-bold uppercase tracking-wide text-bright">
|
||||
{paso.t}
|
||||
</h3>
|
||||
<p className="mt-1.5 text-sm leading-relaxed text-muted">{paso.d}</p>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
|
||||
<hr className="hair my-6" />
|
||||
|
||||
<p className="text-xs leading-relaxed text-faint">
|
||||
Si la tercera pregunta no tiene respuesta todavía, empieza por{" "}
|
||||
<Link href="/cumplimiento-nom-002" className="link">
|
||||
cumplimiento NOM-002
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ---------- filtros + tabla comparativa ---------- */}
|
||||
<ExploradorCatalogo />
|
||||
|
||||
{/* ---------- las 8 categorías con su tagline ---------- */}
|
||||
<section className="border-t border-rule bg-ink py-14 sm:py-20" aria-labelledby="categorias-titulo">
|
||||
<div className="shell">
|
||||
<Revelar>
|
||||
<p className="eyebrow mb-4">Ocho categorías</p>
|
||||
<h2 id="categorias-titulo" className="font-display text-2xl font-extrabold sm:text-3xl">
|
||||
Cada categoría responde a una forma distinta de llegar al fuego
|
||||
</h2>
|
||||
<p className="mt-3 max-w-2xl text-sm leading-relaxed text-muted sm:text-base">
|
||||
Portátil, móvil, fijo, automático o el agente por separado. La diferencia no es de
|
||||
gama: es de quién opera, en cuánto tiempo y con qué instalación.
|
||||
</p>
|
||||
</Revelar>
|
||||
|
||||
<ul className="mt-10 grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{CATEGORIAS.map((categoria) => {
|
||||
const cuantos = productosDeCategoria(categoria.slug).length;
|
||||
return (
|
||||
<li key={categoria.slug}>
|
||||
<Link
|
||||
href={`/catalogo/${categoria.slug}`}
|
||||
className="group flex h-full flex-col overflow-hidden rounded-[6px] border border-rule bg-ink-2 transition-colors duration-200 hover:border-rule-2 [-webkit-tap-highlight-color:transparent]"
|
||||
>
|
||||
<div className="relative aspect-[16/9] w-full overflow-hidden border-b border-rule bg-ink-3">
|
||||
<Image
|
||||
src={categoria.imagen}
|
||||
alt={categoria.nombre}
|
||||
fill
|
||||
sizes="(max-width: 640px) 92vw, (max-width: 1024px) 44vw, 22rem"
|
||||
className="object-contain p-6 transition-transform duration-300 group-hover:scale-[1.04]"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-1 flex-col p-5">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<h3 className="font-display text-lg font-extrabold leading-tight text-bright">
|
||||
{categoria.nombre}
|
||||
</h3>
|
||||
<span className="badge shrink-0">
|
||||
{cuantos} {cuantos === 1 ? "equipo" : "equipos"}
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-3 text-sm leading-relaxed text-muted">{categoria.tagline}</p>
|
||||
<span className="mt-auto pt-5 font-mono text-[0.625rem] uppercase tracking-[0.14em] text-signal transition-colors group-hover:text-signal-hi">
|
||||
Ver la categoría →
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ---------- formulario embebido: requisito del cliente ---------- */}
|
||||
<section id="cotizar" className="grid-bg-lite border-t border-rule bg-paper py-14 sm:py-20">
|
||||
<div className="shell">
|
||||
<div className="grid gap-10 lg:grid-cols-[1fr_1.05fr] lg:items-start lg:gap-16">
|
||||
<div>
|
||||
<Revelar>
|
||||
<p className="eyebrow mb-4">Siguiente paso</p>
|
||||
<h2 className="font-display text-2xl font-extrabold text-deep sm:text-3xl">
|
||||
Dinos qué hay que proteger y te devolvemos la selección, no un folleto
|
||||
</h2>
|
||||
<p className="mt-4 text-sm leading-relaxed text-deep-muted sm:text-base">
|
||||
Con la clase de fuego, la superficie y quién opera el equipo alcanza para proponer
|
||||
una configuración y decirte qué documentación te corresponde exigir. Sin costo y sin
|
||||
compromiso de compra.
|
||||
</p>
|
||||
</Revelar>
|
||||
|
||||
<ul className="mt-8 space-y-3.5">
|
||||
{[
|
||||
"Selección de equipo y agente por escenario, no por catálogo completo.",
|
||||
"Índice de la documentación que debe acompañar la compra.",
|
||||
"Referencia de lo que la NOM-002 exige según tu nivel de riesgo.",
|
||||
].map((punto) => (
|
||||
<li key={punto} className="flex gap-3 text-sm leading-relaxed text-deep-muted">
|
||||
<span aria-hidden="true" className="mt-2 h-1 w-1 shrink-0 rounded-full bg-signal" />
|
||||
{punto}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<FormularioContacto
|
||||
canal="catalogo"
|
||||
variante="claro"
|
||||
titulo="Solicitar diagnóstico"
|
||||
descripcion="Respondemos con el escenario interpretado, el equipo que aplica y la lista de documentos que deberíamos entregarte."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user