Files
fspm-web/src/components/site/Encabezado.tsx
T
Uriel JarethandClaude Opus 5 0e0331c1fc 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]>
2026-08-28 01:30:44 -06:00

242 lines
9.0 KiB
TypeScript

"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">
Recibir cotización
</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">
Recibir cotización
</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>
);
}