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]>
242 lines
9.0 KiB
TypeScript
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>
|
|
);
|
|
}
|