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,241 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import { EMPRESA, NAV_PRINCIPAL } from "@/data/empresa";
|
||||
import { INDUSTRIAS } from "@/data/industrias";
|
||||
|
||||
export function Encabezado() {
|
||||
const ruta = usePathname();
|
||||
const [abierto, setAbierto] = useState(false);
|
||||
const [desplazado, setDesplazado] = useState(false);
|
||||
const [solucionesAbierto, setSolucionesAbierto] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const alScroll = () => setDesplazado(window.scrollY > 12);
|
||||
alScroll();
|
||||
window.addEventListener("scroll", alScroll, { passive: true });
|
||||
return () => window.removeEventListener("scroll", alScroll);
|
||||
}, []);
|
||||
|
||||
// cerrar el menú al navegar
|
||||
useEffect(() => {
|
||||
setAbierto(false);
|
||||
setSolucionesAbierto(false);
|
||||
}, [ruta]);
|
||||
|
||||
// bloquear el scroll del cuerpo con el menú móvil abierto
|
||||
useEffect(() => {
|
||||
document.body.style.overflow = abierto ? "hidden" : "";
|
||||
return () => {
|
||||
document.body.style.overflow = "";
|
||||
};
|
||||
}, [abierto]);
|
||||
|
||||
const activa = (href: string) => ruta === href || ruta.startsWith(`${href}/`);
|
||||
|
||||
return (
|
||||
<header
|
||||
className={[
|
||||
"sticky top-0 z-50 border-b transition-colors duration-200",
|
||||
desplazado ? "border-rule bg-ink/95 backdrop-blur-md" : "border-transparent bg-ink",
|
||||
].join(" ")}
|
||||
>
|
||||
{/* barra de contacto: el teléfono de un proveedor de emergencia siempre visible */}
|
||||
<div className="hidden border-b border-rule/60 lg:block">
|
||||
<div className="shell flex h-9 items-center justify-between">
|
||||
<p className="font-mono text-[0.625rem] uppercase tracking-[0.16em] text-faint">
|
||||
Atención a operaciones críticas · Ciudad de México y a distancia
|
||||
</p>
|
||||
<div className="flex items-center gap-5">
|
||||
<a
|
||||
href={`tel:${EMPRESA.telefonoTel}`}
|
||||
className="font-mono text-[0.6875rem] tracking-[0.08em] text-muted transition-colors hover:text-bright"
|
||||
>
|
||||
{EMPRESA.telefono}
|
||||
</a>
|
||||
<a
|
||||
href={`mailto:${EMPRESA.correo}`}
|
||||
className="font-mono text-[0.6875rem] tracking-[0.08em] text-muted transition-colors hover:text-bright"
|
||||
>
|
||||
{EMPRESA.correo}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="shell flex h-[4.5rem] items-center justify-between gap-6">
|
||||
<Link href="/" className="flex shrink-0 items-center gap-3" aria-label={`${EMPRESA.nombreCorto} — inicio`}>
|
||||
<Image
|
||||
src="/media/marca/fspm-logo-blanco.png"
|
||||
alt={EMPRESA.nombre}
|
||||
width={148}
|
||||
height={40}
|
||||
priority
|
||||
className="h-8 w-auto"
|
||||
/>
|
||||
</Link>
|
||||
|
||||
{/* navegación de escritorio */}
|
||||
<nav className="hidden items-center gap-1 lg:flex" aria-label="Navegación principal">
|
||||
{/* Soluciones abre las cinco rutas por industria */}
|
||||
<div
|
||||
className="relative"
|
||||
onMouseEnter={() => setSolucionesAbierto(true)}
|
||||
onMouseLeave={() => setSolucionesAbierto(false)}
|
||||
>
|
||||
<Link
|
||||
href="/soluciones"
|
||||
aria-expanded={solucionesAbierto}
|
||||
className={[
|
||||
"flex items-center gap-1.5 px-3 py-2 font-mono text-[0.75rem] uppercase tracking-[0.1em] transition-colors",
|
||||
activa("/soluciones") ? "text-signal-hi" : "text-muted hover:text-bright",
|
||||
].join(" ")}
|
||||
>
|
||||
Soluciones
|
||||
<svg width="9" height="6" viewBox="0 0 9 6" aria-hidden="true" className="opacity-60">
|
||||
<path d="M1 1l3.5 3.5L8 1" stroke="currentColor" strokeWidth="1.4" fill="none" />
|
||||
</svg>
|
||||
</Link>
|
||||
|
||||
{solucionesAbierto ? (
|
||||
<div className="absolute left-0 top-full w-[22rem] pt-2">
|
||||
<div className="panel overflow-hidden p-1.5 shadow-2xl shadow-black/50">
|
||||
{INDUSTRIAS.map((ind) => (
|
||||
<Link
|
||||
key={ind.slug}
|
||||
href={`/soluciones/${ind.slug}`}
|
||||
className="group flex items-start gap-3 rounded-[3px] px-3 py-2.5 transition-colors hover:bg-ink-3"
|
||||
>
|
||||
<span className="mt-0.5 font-mono text-[0.625rem] text-signal">
|
||||
{String(ind.prioridad).padStart(2, "0")}
|
||||
</span>
|
||||
<span className="min-w-0">
|
||||
<span className="block text-sm font-semibold text-bright">{ind.nombre}</span>
|
||||
<span className="mt-0.5 block truncate text-xs text-faint">{ind.dolor}</span>
|
||||
</span>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{NAV_PRINCIPAL.filter((n) => n.href !== "/soluciones").map((n) => (
|
||||
<Link
|
||||
key={n.href}
|
||||
href={n.href}
|
||||
className={[
|
||||
"px-3 py-2 font-mono text-[0.75rem] uppercase tracking-[0.1em] transition-colors",
|
||||
activa(n.href) ? "text-signal-hi" : "text-muted hover:text-bright",
|
||||
].join(" ")}
|
||||
>
|
||||
{n.label}
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<div className="hidden shrink-0 items-center gap-2.5 lg:flex">
|
||||
<Link href="/contacto" className="btn btn-signal btn-sm">
|
||||
Solicitar diagnóstico
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* disparador móvil */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setAbierto((v) => !v)}
|
||||
aria-expanded={abierto}
|
||||
aria-controls="menu-movil"
|
||||
aria-label={abierto ? "Cerrar menú" : "Abrir menú"}
|
||||
className="flex h-10 w-10 shrink-0 items-center justify-center rounded-[3px] border border-rule-2 text-bright lg:hidden"
|
||||
>
|
||||
<span className="relative block h-3 w-4">
|
||||
<span
|
||||
className={[
|
||||
"absolute left-0 block h-[1.5px] w-4 bg-current transition-transform duration-200",
|
||||
abierto ? "top-[5px] rotate-45" : "top-0",
|
||||
].join(" ")}
|
||||
/>
|
||||
<span
|
||||
className={[
|
||||
"absolute left-0 top-[5px] block h-[1.5px] w-4 bg-current transition-opacity duration-200",
|
||||
abierto ? "opacity-0" : "opacity-100",
|
||||
].join(" ")}
|
||||
/>
|
||||
<span
|
||||
className={[
|
||||
"absolute left-0 block h-[1.5px] w-4 bg-current transition-transform duration-200",
|
||||
abierto ? "top-[5px] -rotate-45" : "top-[10px]",
|
||||
].join(" ")}
|
||||
/>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* panel móvil */}
|
||||
{abierto ? (
|
||||
<div
|
||||
id="menu-movil"
|
||||
className="fixed inset-x-0 bottom-0 top-[4.5rem] z-40 overflow-y-auto border-t border-rule bg-ink lg:hidden"
|
||||
>
|
||||
<div className="shell py-6">
|
||||
<p className="eyebrow eyebrow-muted mb-3">Rutas por industria</p>
|
||||
<ul className="mb-7 divide-y divide-rule/70">
|
||||
{INDUSTRIAS.map((ind) => (
|
||||
<li key={ind.slug}>
|
||||
<Link
|
||||
href={`/soluciones/${ind.slug}`}
|
||||
className="flex items-baseline gap-3 py-3.5"
|
||||
>
|
||||
<span className="font-mono text-[0.625rem] text-signal">
|
||||
{String(ind.prioridad).padStart(2, "0")}
|
||||
</span>
|
||||
<span>
|
||||
<span className="block font-display text-lg font-bold text-bright">
|
||||
{ind.nombre}
|
||||
</span>
|
||||
<span className="block text-xs text-faint">{ind.dolor}</span>
|
||||
</span>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<p className="eyebrow eyebrow-muted mb-3">Secciones</p>
|
||||
<ul className="mb-8 divide-y divide-rule/70">
|
||||
{NAV_PRINCIPAL.filter((n) => n.href !== "/soluciones").map((n) => (
|
||||
<li key={n.href}>
|
||||
<Link
|
||||
href={n.href}
|
||||
className="block py-3.5 font-mono text-sm uppercase tracking-[0.1em] text-bright"
|
||||
>
|
||||
{n.label}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<Link href="/contacto" className="btn btn-signal w-full">
|
||||
Solicitar diagnóstico
|
||||
</Link>
|
||||
|
||||
<div className="mt-6 space-y-2">
|
||||
<a
|
||||
href={`tel:${EMPRESA.telefonoTel}`}
|
||||
className="block font-mono text-sm text-muted"
|
||||
>
|
||||
{EMPRESA.telefono}
|
||||
</a>
|
||||
<a href={`mailto:${EMPRESA.correo}`} className="block font-mono text-sm text-muted">
|
||||
{EMPRESA.correo}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</header>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user