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:
co-authored by
Claude Opus 5
parent
3c6bdcfc06
commit
0e0331c1fc
@@ -7,3 +7,5 @@ node_modules/
|
|||||||
.env.coolify
|
.env.coolify
|
||||||
tests/reporte/
|
tests/reporte/
|
||||||
tsconfig.tsbuildinfo
|
tsconfig.tsbuildinfo
|
||||||
|
test-results/
|
||||||
|
playwright-report/
|
||||||
|
|||||||
+6
-1
@@ -161,8 +161,13 @@ RUN chmod +x /usr/local/bin/docker-entrypoint.sh
|
|||||||
|
|
||||||
# El directorio de datos nace con dueño nextjs: si el volumen se crea vacío,
|
# El directorio de datos nace con dueño nextjs: si el volumen se crea vacío,
|
||||||
# Docker hereda estos permisos y el proceso no root puede escribir la base.
|
# Docker hereda estos permisos y el proceso no root puede escribir la base.
|
||||||
|
# /app/prisma-cli entra en el chown a propósito: al arrancar, `prisma db push`
|
||||||
|
# verifica y a veces reescribe sus motores dentro de su propio node_modules. Sin
|
||||||
|
# permiso de escritura el contenedor muere con
|
||||||
|
# "Can't write to /app/prisma-cli/node_modules/@prisma/engines" y el arranque
|
||||||
|
# nunca llega a Next. Se detectó con el contenedor ya construido, no en el build.
|
||||||
RUN mkdir -p /app/data \
|
RUN mkdir -p /app/data \
|
||||||
&& chown -R nextjs:nodejs /app/data /app/semilla /app/.next
|
&& chown -R nextjs:nodejs /app/data /app/semilla /app/.next /app/prisma-cli
|
||||||
|
|
||||||
USER nextjs
|
USER nextjs
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,149 @@
|
|||||||
|
# Despliegue
|
||||||
|
|
||||||
|
Sitio de FSPM con micro CRM. Un solo contenedor: Next.js en modo `standalone` y
|
||||||
|
SQLite en un volumen. No hay base de datos externa.
|
||||||
|
|
||||||
|
## Variables de entorno
|
||||||
|
|
||||||
|
| Variable | Requerida | Qué pasa si falta |
|
||||||
|
|---|---|---|
|
||||||
|
| `DATABASE_URL` | Sí | Prisma no arranca. En el contenedor ya viene fijada a `file:/app/data/fspm.db`; **no la cambies** salvo que muevas el volumen. |
|
||||||
|
| `AUTH_SECRET` | Sí | El arranque falla con un error explícito. Firma las cookies de sesión del panel. |
|
||||||
|
| `NEXT_PUBLIC_SITE_URL` | Sí, **en construcción** | Las URL canónicas, el sitemap y Open Graph apuntan al dominio equivocado. Ver la advertencia de abajo. |
|
||||||
|
| `NEXT_PUBLIC_WHATSAPP` | Sí, **en construcción** | El widget de WhatsApp abre una conversación con un número vacío. |
|
||||||
|
| `DEMO_ACCESO_RAPIDO` | No | Con cualquier valor distinto de `false`, el acceso de un clic por perfil está activo en `/admin/login`. |
|
||||||
|
| `NODE_ENV` | No | Ya viene en `production` dentro de la imagen. |
|
||||||
|
|
||||||
|
### Advertencia sobre las variables `NEXT_PUBLIC_*`
|
||||||
|
|
||||||
|
Next las sustituye **literalmente durante `next build`**, no las lee al arrancar.
|
||||||
|
Cambiar `NEXT_PUBLIC_SITE_URL` o `NEXT_PUBLIC_WHATSAPP` en el entorno del
|
||||||
|
contenedor **no tiene ningún efecto**: hay que reconstruir la imagen pasándolas
|
||||||
|
como `--build-arg`. Es el error más fácil de cometer aquí, y se manifiesta como
|
||||||
|
un sitemap con el dominio incorrecto y un widget de WhatsApp que no marca a nadie.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build \
|
||||||
|
--build-arg NEXT_PUBLIC_SITE_URL=https://fspm-demo.urieljareth.org \
|
||||||
|
--build-arg NEXT_PUBLIC_WHATSAPP=525540348395 \
|
||||||
|
-t fspm-web:demo .
|
||||||
|
```
|
||||||
|
|
||||||
|
### Generar un `AUTH_SECRET`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node -e "console.log(require('crypto').randomBytes(48).toString('base64url'))"
|
||||||
|
```
|
||||||
|
|
||||||
|
Uno por entorno. No reutilices el de desarrollo: cualquiera que lo tenga puede
|
||||||
|
firmar una cookie de sesión válida y entrar al panel como administrador.
|
||||||
|
|
||||||
|
## El volumen — la parte frágil
|
||||||
|
|
||||||
|
**Si no montas `/app/data`, cada redespliegue borra los leads capturados.**
|
||||||
|
|
||||||
|
La base de demostración se hornea dentro de la imagen durante la construcción.
|
||||||
|
Al arrancar, el punto de entrada la copia al volumen **solo si el volumen está
|
||||||
|
vacío**; si ya hay una base, no la toca. Ese es el mecanismo que permite que los
|
||||||
|
leads que llegan por el formulario y por el widget sobrevivan a una nueva versión
|
||||||
|
de la imagen.
|
||||||
|
|
||||||
|
Sin volumen, el contenedor sigue funcionando —arranca con los datos sembrados—,
|
||||||
|
así que la falla es silenciosa: nadie se entera hasta que un lead real desaparece.
|
||||||
|
|
||||||
|
En Coolify: **Storages → Add → Volume Mount**, con destino `/app/data`.
|
||||||
|
Verifícalo después del primer despliegue con:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker inspect <contenedor> --format '{{json .Mounts}}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Local con Docker
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -t fspm-web:demo .
|
||||||
|
|
||||||
|
docker run -d --name fspm-demo -p 3100:3000 \
|
||||||
|
-v fspm-datos:/app/data \
|
||||||
|
-e AUTH_SECRET="<secreto generado>" \
|
||||||
|
fspm-web:demo
|
||||||
|
|
||||||
|
curl -s http://127.0.0.1:3100/api/salud
|
||||||
|
```
|
||||||
|
|
||||||
|
`/api/salud` consulta la base, así que un volumen no escribible se reporta como
|
||||||
|
contenedor enfermo en lugar de pasar por sano.
|
||||||
|
|
||||||
|
## Coolify
|
||||||
|
|
||||||
|
Los endpoints `POST /applications/*` de la API devuelven 404 en esta instancia
|
||||||
|
(verificado en 4.1.2 y de nuevo en 4.3.12), así que la aplicación **se crea desde
|
||||||
|
la interfaz web** y solo el despliegue se dispara por API.
|
||||||
|
|
||||||
|
1. Crea la aplicación apuntando al repositorio de Gitea, rama `main`,
|
||||||
|
tipo **Dockerfile**.
|
||||||
|
2. Fija el dominio, para que Coolify no asigne uno aleatorio y devuelva 503.
|
||||||
|
3. Carga las variables de entorno. Las `NEXT_PUBLIC_*` van también como
|
||||||
|
argumentos de construcción.
|
||||||
|
4. Monta el volumen en `/app/data`.
|
||||||
|
5. Despliega y vigila el registro.
|
||||||
|
|
||||||
|
Despliegues posteriores:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -H "Authorization: Bearer $COOLIFY_TOKEN" \
|
||||||
|
"$COOLIFY_API_URL/deploy?uuid=<uuid de la aplicación>"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verificación posterior al despliegue
|
||||||
|
|
||||||
|
Hazla en este orden; cada punto descarta una clase distinta de falla.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
BASE=https://fspm-demo.urieljareth.org
|
||||||
|
|
||||||
|
curl -s $BASE/api/salud # {"ok":true,...}
|
||||||
|
curl -s $BASE/robots.txt | grep admin # debe prohibir /admin
|
||||||
|
curl -s $BASE/sitemap.xml | grep -c admin # debe dar 0
|
||||||
|
curl -sI $BASE/icon | grep -i content-type # image/*
|
||||||
|
curl -sI $BASE/opengraph-image | grep -i content-type
|
||||||
|
curl -s $BASE/sitemap.xml | grep -o 'https://[^<]*' | head -3 # ¿el dominio correcto?
|
||||||
|
```
|
||||||
|
|
||||||
|
Después, a mano:
|
||||||
|
|
||||||
|
1. Envía el formulario del inicio y confirma que devuelve folio.
|
||||||
|
2. Abre el widget de WhatsApp, envíalo y revisa que el enlace lleve el prefijo
|
||||||
|
`[DEMO]`, el folio y el mensaje que escribiste.
|
||||||
|
3. Entra a `/admin/login` —**no está enlazado desde el sitio**, es intencional— y
|
||||||
|
usa el acceso rápido para ver el panel desde los dos roles.
|
||||||
|
4. Confirma que la oportunidad de los pasos 1 y 2 aparece en el panel con su
|
||||||
|
campaña y su producto de interés.
|
||||||
|
5. Redespliega y vuelve a entrar al panel: los leads de los pasos 1 y 2 **deben
|
||||||
|
seguir ahí**. Si desaparecieron, el volumen no está montado.
|
||||||
|
|
||||||
|
## Antes de enseñarlo a un cliente
|
||||||
|
|
||||||
|
- `AUTH_SECRET` distinto del de desarrollo.
|
||||||
|
- `DEMO_ACCESO_RAPIDO=false` si la demo queda publicada sin acompañamiento: con
|
||||||
|
el acceso rápido activo, cualquiera que llegue a `/admin/login` entra como
|
||||||
|
administrador.
|
||||||
|
- Los datos del panel son ficticios y los precios de los pedidos son inventados.
|
||||||
|
Conviene decirlo en voz alta antes de abrir la pantalla de pedidos.
|
||||||
|
|
||||||
|
## Notas del entorno de desarrollo
|
||||||
|
|
||||||
|
- **El repositorio vive en una carpeta sincronizada a la nube.** El cliente de
|
||||||
|
sincronización mueve archivos de `.next` a media compilación y `npm run build`
|
||||||
|
falla con `Cannot find module './NNNN.js'` aunque el código esté correcto.
|
||||||
|
Está reproducido. La construcción dentro de Docker no se ve afectada, porque
|
||||||
|
copia el contexto y compila en el contenedor. Para verificar en local, usa la
|
||||||
|
imagen, no `next build`.
|
||||||
|
- `next.config.ts` acepta `NEXT_DIST_DIR` para mover el directorio de salida,
|
||||||
|
pero Next lo resuelve relativo a la raíz del proyecto y no acepta rutas
|
||||||
|
absolutas, así que no resuelve el problema anterior por sí solo.
|
||||||
|
- El `chown` del Dockerfile incluye `/app/prisma-cli` a propósito: al arrancar,
|
||||||
|
`prisma db push` reescribe sus motores dentro de su propio `node_modules`, y
|
||||||
|
sin permiso el contenedor muere con
|
||||||
|
`Can't write to /app/prisma-cli/node_modules/@prisma/engines`. Se detectó con
|
||||||
|
el contenedor ya construido, nunca durante el build.
|
||||||
Generated
+15
-15
@@ -17,7 +17,7 @@
|
|||||||
"zod": "4.1.11"
|
"zod": "4.1.11"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "1.56.0",
|
"@playwright/test": "^1.62.1",
|
||||||
"@tailwindcss/postcss": "4.1.13",
|
"@tailwindcss/postcss": "4.1.13",
|
||||||
"@types/node": "22.18.6",
|
"@types/node": "22.18.6",
|
||||||
"@types/react": "19.1.13",
|
"@types/react": "19.1.13",
|
||||||
@@ -1217,19 +1217,19 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@playwright/test": {
|
"node_modules/@playwright/test": {
|
||||||
"version": "1.56.0",
|
"version": "1.62.1",
|
||||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.56.0.tgz",
|
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.62.1.tgz",
|
||||||
"integrity": "sha512-Tzh95Twig7hUwwNe381/K3PggZBZblKUe2wv25oIpzWLr6Z0m4KgV1ZVIjnR6GM9ANEqjZD7XsZEa6JL/7YEgg==",
|
"integrity": "sha512-DTcUc8qii+cpHvtOwggMtBRMjKZHXYWdw8syRYu2vtzuq4Wxphqq4NfCs5Zt44L6mA8rfDfj+PHnxFc/FeK6mQ==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright": "1.56.0"
|
"playwright": "1.62.1"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright": "cli.js"
|
"playwright": "cli.js"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=20"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/client": {
|
"node_modules/@prisma/client": {
|
||||||
@@ -2456,35 +2456,35 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/playwright": {
|
"node_modules/playwright": {
|
||||||
"version": "1.56.0",
|
"version": "1.62.1",
|
||||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.56.0.tgz",
|
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.1.tgz",
|
||||||
"integrity": "sha512-X5Q1b8lOdWIE4KAoHpW3SE8HvUB+ZZsUoN64ZhjnN8dOb1UpujxBtENGiZFE+9F/yhzJwYa+ca3u43FeLbboHA==",
|
"integrity": "sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"playwright-core": "1.56.0"
|
"playwright-core": "1.62.1"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright": "cli.js"
|
"playwright": "cli.js"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=20"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"fsevents": "2.3.2"
|
"fsevents": "2.3.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/playwright-core": {
|
"node_modules/playwright-core": {
|
||||||
"version": "1.56.0",
|
"version": "1.62.1",
|
||||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.56.0.tgz",
|
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.1.tgz",
|
||||||
"integrity": "sha512-1SXl7pMfemAMSDn5rkPeZljxOCYAmQnYLBTExuh6E8USHXGSX3dx6lYZN/xPpTz1vimXmPA9CDnILvmJaB8aSQ==",
|
"integrity": "sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
"playwright-core": "cli.js"
|
"playwright-core": "cli.js"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=20"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/playwright/node_modules/fsevents": {
|
"node_modules/playwright/node_modules/fsevents": {
|
||||||
|
|||||||
+2
-2
@@ -25,6 +25,7 @@
|
|||||||
"zod": "4.1.11"
|
"zod": "4.1.11"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@playwright/test": "^1.62.1",
|
||||||
"@tailwindcss/postcss": "4.1.13",
|
"@tailwindcss/postcss": "4.1.13",
|
||||||
"@types/node": "22.18.6",
|
"@types/node": "22.18.6",
|
||||||
"@types/react": "19.1.13",
|
"@types/react": "19.1.13",
|
||||||
@@ -32,8 +33,7 @@
|
|||||||
"prisma": "6.16.2",
|
"prisma": "6.16.2",
|
||||||
"tailwindcss": "4.1.13",
|
"tailwindcss": "4.1.13",
|
||||||
"tsx": "4.20.5",
|
"tsx": "4.20.5",
|
||||||
"typescript": "5.9.2",
|
"typescript": "5.9.2"
|
||||||
"@playwright/test": "1.56.0"
|
|
||||||
},
|
},
|
||||||
"prisma": {
|
"prisma": {
|
||||||
"seed": "tsx prisma/seed.ts"
|
"seed": "tsx prisma/seed.ts"
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import Link from "next/link";
|
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";
|
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
|
* 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
|
* 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.
|
* 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 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 = {
|
export const metadata: Metadata = {
|
||||||
title: TITULO,
|
title: TITULO,
|
||||||
description: DESCRIPCION,
|
description: DESCRIPCION,
|
||||||
alternates: { canonical: `${SITIO_URL}/aviso-de-privacidad` },
|
alternates: { canonical: `${SITIO_URL}/aviso-de-privacidad` },
|
||||||
robots: { index: true, follow: true },
|
robots: { index: false, follow: true },
|
||||||
openGraph: {
|
openGraph: {
|
||||||
title: `${TITULO} | ${EMPRESA.nombreCorto}`,
|
title: `${TITULO} | ${EMPRESA.nombreCorto}`,
|
||||||
description: DESCRIPCION,
|
description: DESCRIPCION,
|
||||||
@@ -28,60 +37,101 @@ export const metadata: Metadata = {
|
|||||||
|
|
||||||
const CORREO_ARCO = "[email protected]";
|
const CORREO_ARCO = "[email protected]";
|
||||||
|
|
||||||
const CAMPOS_FORMULARIO = [
|
const MENSAJE_INICIAL = "Mi duda sobre este sitio es ";
|
||||||
{ campo: "Nombre", uso: "Identificarte y dirigirnos a ti por tu nombre en el seguimiento." },
|
|
||||||
|
/** 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: "Correo electrónico", uso: "Enviarte la respuesta técnica y la propuesta." },
|
||||||
{ campo: "Teléfono", uso: "Contactarte cuando la solicitud requiere una llamada." },
|
{ 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",
|
campo: "Mensaje",
|
||||||
uso: "Saber por qué campaña o página llegaste, para medir qué contenido resuelve dudas reales.",
|
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() {
|
export default function PaginaAviso() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* ---------- encabezado ---------- */}
|
<HeroeLanding
|
||||||
<section className="bg-ink pt-12 pb-10 sm:pt-16 sm:pb-12">
|
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">
|
<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">
|
<div className="max-w-3xl">
|
||||||
<p className="eyebrow mb-5">Documento legal</p>
|
<p className="eyebrow eyebrow-muted mb-4">En corto</p>
|
||||||
<h1 className="font-display text-[1.875rem] font-black uppercase leading-[1.04] tracking-tight sm:text-[2.5rem]">
|
<h2 id="en-corto" className="font-display text-xl font-extrabold sm:text-2xl">
|
||||||
Aviso de privacidad
|
Lo esencial en cinco frases
|
||||||
</h1>
|
</h2>
|
||||||
<p className="mt-5 text-sm leading-relaxed text-muted sm:text-base">
|
<p className="mt-3 text-sm leading-relaxed text-muted">
|
||||||
Reproducción del aviso publicado por {EMPRESA.razonSocial}, reordenado para poder
|
Este resumen no sustituye al aviso: es una entrada para que puedas leerlo sabiendo qué
|
||||||
leerse. La versión vigente y definitiva es la que la empresa publique en su propio
|
buscar. El texto que manda es el de los nueve apartados de abajo.
|
||||||
sitio.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
<ul className="mt-6 space-y-3">
|
||||||
|
{EN_CORTO.map((punto) => (
|
||||||
<div
|
<li key={punto} className="flex gap-3 text-sm leading-relaxed text-bright">
|
||||||
role="note"
|
<span
|
||||||
aria-label="Naturaleza de este sitio"
|
aria-hidden="true"
|
||||||
className="mt-8 max-w-3xl rounded-[6px] border border-rule border-l-2 border-l-ember bg-ink-2 p-5"
|
className="mt-2 h-1.5 w-1.5 shrink-0 rounded-full bg-signal"
|
||||||
>
|
/>
|
||||||
<p className="font-mono text-[0.6875rem] uppercase tracking-[0.16em] text-ember">
|
{punto}
|
||||||
Sobre este sitio
|
</li>
|
||||||
</p>
|
))}
|
||||||
<p className="mt-3 text-sm leading-relaxed text-muted">{AVISO_DEMO}</p>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -91,7 +141,10 @@ export default function PaginaAviso() {
|
|||||||
<div className="shell">
|
<div className="shell">
|
||||||
<div className="max-w-3xl space-y-12">
|
<div className="max-w-3xl space-y-12">
|
||||||
<section aria-labelledby="responsable">
|
<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
|
1. Responsable del tratamiento
|
||||||
</h2>
|
</h2>
|
||||||
<div className="mt-4 space-y-4 text-sm leading-relaxed text-deep-muted">
|
<div className="mt-4 space-y-4 text-sm leading-relaxed text-deep-muted">
|
||||||
@@ -103,8 +156,8 @@ export default function PaginaAviso() {
|
|||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
{EMPRESA.nombreCorto} será el responsable del tratamiento que, con motivo de la
|
{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
|
relación jurídica que haya sido celebrada y para el cumplimiento de la misma, se
|
||||||
dé a tus datos personales.
|
le dé a tus datos personales.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
En cumplimiento de la Ley Federal de Protección de Datos Personales en Posesión de
|
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
|
2. Qué datos recaba este sitio
|
||||||
</h2>
|
</h2>
|
||||||
<p className="mt-4 text-sm leading-relaxed text-deep-muted">
|
<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
|
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.
|
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>
|
</p>
|
||||||
|
|
||||||
|
<h3 className="mt-8 font-mono text-[0.6875rem] uppercase tracking-[0.16em] text-deep-muted">
|
||||||
|
Lo que escribes tú
|
||||||
|
</h3>
|
||||||
<div
|
<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}
|
tabIndex={0}
|
||||||
role="region"
|
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">
|
<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>
|
</caption>
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="border-b border-rule-lite bg-paper-2">
|
<tr className="border-b border-rule-lite bg-paper-2">
|
||||||
<th
|
{["Dato", "Para qué se usa"].map((c) => (
|
||||||
scope="col"
|
<th
|
||||||
className="whitespace-nowrap px-4 py-3 font-mono text-[0.625rem] font-medium uppercase tracking-[0.14em] text-deep-muted"
|
key={c}
|
||||||
>
|
scope="col"
|
||||||
Dato
|
className="whitespace-nowrap px-4 py-3 font-mono text-[0.625rem] font-medium uppercase tracking-[0.14em] text-deep-muted"
|
||||||
</th>
|
>
|
||||||
<th
|
{c}
|
||||||
scope="col"
|
</th>
|
||||||
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>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<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">
|
<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">
|
<th scope="row" className="px-4 py-3.5 text-left align-top">
|
||||||
<span className="font-sans text-sm font-semibold text-deep">
|
<span className="font-sans text-sm font-semibold text-deep">
|
||||||
@@ -181,7 +278,10 @@ export default function PaginaAviso() {
|
|||||||
<hr className="hair-lite" />
|
<hr className="hair-lite" />
|
||||||
|
|
||||||
<section aria-labelledby="finalidades">
|
<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
|
3. Finalidades del tratamiento
|
||||||
</h2>
|
</h2>
|
||||||
<div className="mt-4 space-y-4 text-sm leading-relaxed text-deep-muted">
|
<div className="mt-4 space-y-4 text-sm leading-relaxed text-deep-muted">
|
||||||
@@ -193,10 +293,10 @@ export default function PaginaAviso() {
|
|||||||
establecidos.
|
establecidos.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Esto incluye enviar, por cualquier medio físico, electrónico o magnético, de manera
|
Esto incluye enviar, por cualquier medio físico, electrónico o magnético, de
|
||||||
enunciativa mas no limitativa: invitación a reuniones de trabajo, minutas, noticias
|
manera enunciativa mas no limitativa: invitación a reuniones de trabajo, minutas,
|
||||||
y anuncios en general relacionados con la institución, campañas publicitarias
|
noticias y anuncios en general relacionados con la institución, campañas
|
||||||
afines e invitación a eventos.
|
publicitarias afines e invitación a eventos.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -204,7 +304,10 @@ export default function PaginaAviso() {
|
|||||||
<hr className="hair-lite" />
|
<hr className="hair-lite" />
|
||||||
|
|
||||||
<section aria-labelledby="transferencias">
|
<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
|
4. Transferencias
|
||||||
</h2>
|
</h2>
|
||||||
<p className="mt-4 text-sm leading-relaxed text-deep-muted">
|
<p className="mt-4 text-sm leading-relaxed text-deep-muted">
|
||||||
@@ -217,7 +320,10 @@ export default function PaginaAviso() {
|
|||||||
<hr className="hair-lite" />
|
<hr className="hair-lite" />
|
||||||
|
|
||||||
<section aria-labelledby="seguridad">
|
<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
|
5. Medidas de seguridad
|
||||||
</h2>
|
</h2>
|
||||||
<p className="mt-4 text-sm leading-relaxed text-deep-muted">
|
<p className="mt-4 text-sm leading-relaxed text-deep-muted">
|
||||||
@@ -230,7 +336,10 @@ export default function PaginaAviso() {
|
|||||||
<hr className="hair-lite" />
|
<hr className="hair-lite" />
|
||||||
|
|
||||||
<section aria-labelledby="consentimiento">
|
<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
|
6. Excepciones al consentimiento
|
||||||
</h2>
|
</h2>
|
||||||
<p className="mt-4 text-sm leading-relaxed text-deep-muted">
|
<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">
|
<div className="mt-4 space-y-4 text-sm leading-relaxed text-deep-muted">
|
||||||
<p>
|
<p>
|
||||||
El ejercicio de los derechos de Acceso, Rectificación, Cancelación y Oposición
|
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
|
respecto de tus datos personales, así como la revocación del consentimiento para
|
||||||
tratamiento, podrá realizarse a través de los procedimientos implementados o
|
su tratamiento, podrá realizarse a través de los procedimientos implementados o
|
||||||
solicitándolo al siguiente correo electrónico:
|
solicitándolo al siguiente correo electrónico:
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
@@ -268,16 +377,19 @@ export default function PaginaAviso() {
|
|||||||
8. Cambios al aviso
|
8. Cambios al aviso
|
||||||
</h2>
|
</h2>
|
||||||
<p className="mt-4 text-sm leading-relaxed text-deep-muted">
|
<p className="mt-4 text-sm leading-relaxed text-deep-muted">
|
||||||
{EMPRESA.nombreCorto} podrá enmendar o modificar el presente aviso de privacidad como
|
{EMPRESA.nombreCorto} podrá enmendar o modificar el presente aviso de privacidad
|
||||||
lo estime conveniente, e informará y pondrá a tu disposición el aviso actualizado en
|
como lo estime conveniente, e informará y pondrá a tu disposición el aviso
|
||||||
su sitio web.
|
actualizado en su sitio web.
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<hr className="hair-lite" />
|
<hr className="hair-lite" />
|
||||||
|
|
||||||
<section aria-labelledby="contacto-aviso">
|
<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
|
9. Contacto
|
||||||
</h2>
|
</h2>
|
||||||
<dl className="spec mt-5 sm:grid-cols-2">
|
<dl className="spec mt-5 sm:grid-cols-2">
|
||||||
@@ -311,6 +423,48 @@ export default function PaginaAviso() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+122
-65
@@ -1,6 +1,9 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { FormularioContacto } from "@/components/formulario/FormularioContacto";
|
||||||
import { RielClases } from "@/components/marca/RielClases";
|
import { RielClases } from "@/components/marca/RielClases";
|
||||||
|
import { Migas } from "@/components/site/contenido/Migas";
|
||||||
|
import { FOTOS, HeroeLanding } from "@/components/site/HeroeLanding";
|
||||||
import { Revelar } from "@/components/site/Revelar";
|
import { Revelar } from "@/components/site/Revelar";
|
||||||
import { getCategoria } from "@/data/catalogo";
|
import { getCategoria } from "@/data/catalogo";
|
||||||
import type { ClaseFuego } from "@/data/clases-fuego";
|
import type { ClaseFuego } from "@/data/clases-fuego";
|
||||||
@@ -13,12 +16,16 @@ import { getIndustria } from "@/data/industrias";
|
|||||||
*
|
*
|
||||||
* REGLA DURA: son escenarios construidos con fines de demostración. No
|
* REGLA DURA: son escenarios construidos con fines de demostración. No
|
||||||
* corresponden a clientes reales, no llevan nombres de empresa y no presentan
|
* corresponden a clientes reales, no llevan nombres de empresa y no presentan
|
||||||
* cifras como si fueran mediciones. El aviso va arriba, no en una nota al pie.
|
* cifras como si fueran mediciones. El rótulo va en el héroe y se repite arriba
|
||||||
|
* del contenido, nunca en una nota al pie.
|
||||||
|
*
|
||||||
|
* Cada caso abre con un resumen en una frase, para quien no va a leer los seis
|
||||||
|
* pasos, y después desarrolla el detalle técnico para quien sí.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const TITULO = "Casos de aplicación";
|
const TITULO = "Casos de aplicación: cómo se resuelve un escenario real";
|
||||||
const DESCRIPCION =
|
const DESCRIPCION =
|
||||||
"Cuatro escenarios ilustrativos de protección contra incendios con la estructura problema, riesgo, solución, prueba, resultado y documentación. Construidos con fines de demostración.";
|
"Cuatro escenarios ilustrativos de protección contra incendios —planta química, bodega, gasolinera y cocina de hotel— con la estructura problema, riesgo, solución, prueba, resultado y documentación. Construidos con fines de demostración.";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: TITULO,
|
title: TITULO,
|
||||||
@@ -31,11 +38,15 @@ export const metadata: Metadata = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const MENSAJE_INICIAL = "Mi escenario se parece al de ";
|
||||||
|
|
||||||
type Caso = {
|
type Caso = {
|
||||||
id: string;
|
id: string;
|
||||||
industriaSlug: string;
|
industriaSlug: string;
|
||||||
titulo: string;
|
titulo: string;
|
||||||
escenario: string;
|
escenario: string;
|
||||||
|
/** El caso en una frase, para quien no va a leer los seis pasos. */
|
||||||
|
resumen: string;
|
||||||
clases: ClaseFuego[];
|
clases: ClaseFuego[];
|
||||||
categorias: string[];
|
categorias: string[];
|
||||||
problema: string;
|
problema: string;
|
||||||
@@ -51,7 +62,10 @@ const CASOS: Caso[] = [
|
|||||||
id: "trasvase",
|
id: "trasvase",
|
||||||
industriaSlug: "industria-energia",
|
industriaSlug: "industria-energia",
|
||||||
titulo: "Patio de trasvase de solventes en una planta química",
|
titulo: "Patio de trasvase de solventes en una planta química",
|
||||||
escenario: "Escenario ilustrativo · Planta química, patio de trasvase con contención secundaria",
|
escenario:
|
||||||
|
"Escenario ilustrativo · Planta química, patio de trasvase con contención secundaria",
|
||||||
|
resumen:
|
||||||
|
"El equipo estaba a cien metros de donde puede arder. Se cronometró el recorrido y se movió el equipo, no se compró más.",
|
||||||
clases: ["A", "B", "C"],
|
clases: ["A", "B", "C"],
|
||||||
categorias: ["agentes-extinguidores", "productos-moviles"],
|
categorias: ["agentes-extinguidores", "productos-moviles"],
|
||||||
problema:
|
problema:
|
||||||
@@ -83,6 +97,8 @@ const CASOS: Caso[] = [
|
|||||||
industriaSlug: "logistica-bodegas",
|
industriaSlug: "logistica-bodegas",
|
||||||
titulo: "Nave de almacenamiento con carga combustible y montacargas eléctricos",
|
titulo: "Nave de almacenamiento con carga combustible y montacargas eléctricos",
|
||||||
escenario: "Escenario ilustrativo · Centro logístico, nave única con racks de gran altura",
|
escenario: "Escenario ilustrativo · Centro logístico, nave única con racks de gran altura",
|
||||||
|
resumen:
|
||||||
|
"Había extintores suficientes en el conteo, pero no los correctos en los pasillos correctos.",
|
||||||
clases: ["A", "B", "C"],
|
clases: ["A", "B", "C"],
|
||||||
categorias: ["extintores-portatiles", "rociadores"],
|
categorias: ["extintores-portatiles", "rociadores"],
|
||||||
problema:
|
problema:
|
||||||
@@ -114,6 +130,8 @@ const CASOS: Caso[] = [
|
|||||||
industriaSlug: "gasolineras-flotas",
|
industriaSlug: "gasolineras-flotas",
|
||||||
titulo: "Estación de servicio con público en isla y autotanques en patio",
|
titulo: "Estación de servicio con público en isla y autotanques en patio",
|
||||||
escenario: "Escenario ilustrativo · Estación de servicio urbana con seis posiciones de carga",
|
escenario: "Escenario ilustrativo · Estación de servicio urbana con seis posiciones de carga",
|
||||||
|
resumen:
|
||||||
|
"Quien responde primero es el despachador. Se entrenó a los tres turnos y se dejó constancia de quién puede operar el equipo.",
|
||||||
clases: ["A", "B", "C"],
|
clases: ["A", "B", "C"],
|
||||||
categorias: ["productos-moviles", "extintores-portatiles"],
|
categorias: ["productos-moviles", "extintores-portatiles"],
|
||||||
problema:
|
problema:
|
||||||
@@ -145,6 +163,8 @@ const CASOS: Caso[] = [
|
|||||||
industriaSlug: "cocinas-hoteles",
|
industriaSlug: "cocinas-hoteles",
|
||||||
titulo: "Cocina de hotel con freidoras bajo una sola campana",
|
titulo: "Cocina de hotel con freidoras bajo una sola campana",
|
||||||
escenario: "Escenario ilustrativo · Hotel de servicio completo, cocina central y comedor",
|
escenario: "Escenario ilustrativo · Hotel de servicio completo, cocina central y comedor",
|
||||||
|
resumen:
|
||||||
|
"Lo que estaba colgado en la pared no sirve para aceite hirviendo. Se cambió por equipo apto para clase K y se entrenó al turno de cierre.",
|
||||||
clases: ["A", "B", "K"],
|
clases: ["A", "B", "K"],
|
||||||
categorias: ["rociadores", "extintores-portatiles"],
|
categorias: ["rociadores", "extintores-portatiles"],
|
||||||
problema:
|
problema:
|
||||||
@@ -176,32 +196,38 @@ const CASOS: Caso[] = [
|
|||||||
export default function PaginaCasos() {
|
export default function PaginaCasos() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* ---------- encabezado con el aviso destacado arriba ---------- */}
|
<HeroeLanding
|
||||||
<section className="grid-bg bg-ink pt-12 pb-14 sm:pt-16 sm:pb-20">
|
antes={<Migas actual="Casos de aplicación" />}
|
||||||
<div className="shell">
|
eyebrow="Escenarios ilustrativos"
|
||||||
<nav aria-label="Ruta de navegación" className="mb-8">
|
titulo="Cuatro escenarios y cómo se resuelve cada uno"
|
||||||
<ol className="flex flex-wrap items-center gap-2 font-mono text-[0.625rem] uppercase tracking-[0.14em] text-faint">
|
entrada="Planta química, bodega, gasolinera y cocina de hotel. Cada caso sigue seis pasos: problema, riesgo, solución, prueba, resultado y documentación."
|
||||||
<li>
|
foto={FOTOS.planta}
|
||||||
<Link href="/" className="hover:text-bright">
|
enfoque="50% 62%"
|
||||||
Inicio
|
altura="alto"
|
||||||
</Link>
|
prioridad
|
||||||
</li>
|
>
|
||||||
<li aria-hidden="true">/</li>
|
<div className="flex flex-col gap-4 sm:flex-row sm:items-center">
|
||||||
<li className="text-bright">Casos de aplicación</li>
|
<Link href="#plantear" className="btn btn-signal w-full sm:w-auto">
|
||||||
</ol>
|
Plantear mi escenario
|
||||||
</nav>
|
</Link>
|
||||||
|
<p className="max-w-md font-mono text-[0.625rem] uppercase leading-relaxed tracking-[0.1em] text-ember">
|
||||||
|
Construidos para demostración · No corresponden a clientes reales
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</HeroeLanding>
|
||||||
|
|
||||||
{/* el aviso va antes del contenido, no al final */}
|
{/* ---------- el rótulo, arriba del contenido ---------- */}
|
||||||
<div
|
<section className="border-t border-rule bg-ink-2 py-10 sm:py-12" aria-labelledby="rotulo">
|
||||||
role="note"
|
<div className="shell">
|
||||||
aria-label="Naturaleza de estos casos"
|
<div className="rounded-[6px] border border-rule bg-ink p-0">
|
||||||
className="mb-10 rounded-[6px] border border-rule bg-ink-2 p-0 sm:mb-12"
|
|
||||||
>
|
|
||||||
<div className="hazard h-1.5 w-full rounded-t-[6px]" aria-hidden="true" />
|
<div className="hazard h-1.5 w-full rounded-t-[6px]" aria-hidden="true" />
|
||||||
<div className="p-5 sm:p-6">
|
<div className="p-5 sm:p-6">
|
||||||
<p className="font-mono text-[0.6875rem] font-medium uppercase tracking-[0.16em] text-ember">
|
<h2
|
||||||
Escenarios ilustrativos
|
id="rotulo"
|
||||||
</p>
|
className="font-mono text-[0.6875rem] font-medium uppercase tracking-[0.16em] text-ember"
|
||||||
|
>
|
||||||
|
Qué son y qué no son estos casos
|
||||||
|
</h2>
|
||||||
<p className="mt-3 max-w-3xl text-sm leading-relaxed text-muted">
|
<p className="mt-3 max-w-3xl text-sm leading-relaxed text-muted">
|
||||||
Los cuatro casos de esta página están{" "}
|
Los cuatro casos de esta página están{" "}
|
||||||
<span className="text-bright">
|
<span className="text-bright">
|
||||||
@@ -211,21 +237,13 @@ export default function PaginaCasos() {
|
|||||||
cualitativa: ninguna cifra de esta página debe leerse como una medición. Están
|
cualitativa: ninguna cifra de esta página debe leerse como una medición. Están
|
||||||
escritos para mostrar cómo se estructura un caso, no para acreditar uno.
|
escritos para mostrar cómo se estructura un caso, no para acreditar uno.
|
||||||
</p>
|
</p>
|
||||||
|
<p className="mt-3 max-w-3xl text-sm leading-relaxed text-muted">
|
||||||
|
Aun así, la estructura es la real. Cuando la prueba se hace antes de la compra, la
|
||||||
|
documentación deja de ser un trámite y se vuelve la constancia de algo que sí
|
||||||
|
ocurrió.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="max-w-3xl anim-rise">
|
|
||||||
<p className="eyebrow mb-5">Casos de aplicación</p>
|
|
||||||
<h1 className="font-display text-[2rem] font-black uppercase leading-[1.03] tracking-tight sm:text-[2.75rem] lg:text-5xl">
|
|
||||||
Seis pasos que convierten una compra en un expediente
|
|
||||||
</h1>
|
|
||||||
<p className="mt-6 text-base leading-relaxed text-muted sm:text-lg">
|
|
||||||
Problema, riesgo, solución, prueba, resultado y documentación. Es la estructura que
|
|
||||||
recomienda el estudio de marca y también el orden en el que conviene trabajar: cuando
|
|
||||||
la prueba se hace antes de la compra, la documentación deja de ser un trámite y se
|
|
||||||
vuelve la constancia de algo que sí ocurrió.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -267,6 +285,15 @@ export default function PaginaCasos() {
|
|||||||
{caso.titulo}
|
{caso.titulo}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
|
<p
|
||||||
|
className={[
|
||||||
|
"mt-4 text-base leading-relaxed",
|
||||||
|
claro ? "text-deep" : "text-bright",
|
||||||
|
].join(" ")}
|
||||||
|
>
|
||||||
|
{caso.resumen}
|
||||||
|
</p>
|
||||||
|
|
||||||
<p
|
<p
|
||||||
className={[
|
className={[
|
||||||
"mt-4 font-mono text-[0.6875rem] uppercase leading-relaxed tracking-[0.1em]",
|
"mt-4 font-mono text-[0.6875rem] uppercase leading-relaxed tracking-[0.1em]",
|
||||||
@@ -351,6 +378,15 @@ export default function PaginaCasos() {
|
|||||||
<Paso claro={claro} n="06" titulo="Documentación">
|
<Paso claro={claro} n="06" titulo="Documentación">
|
||||||
<Lista claro={claro} elementos={caso.documentacion} />
|
<Lista claro={claro} elementos={caso.documentacion} />
|
||||||
</Paso>
|
</Paso>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Link
|
||||||
|
href="#plantear"
|
||||||
|
className={["btn btn-sm", claro ? "btn-deep" : "btn-signal"].join(" ")}
|
||||||
|
>
|
||||||
|
Mi caso se parece a este
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -358,30 +394,52 @@ export default function PaginaCasos() {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
{/* ---------- cierre ---------- */}
|
{/* ---------- conversión ---------- */}
|
||||||
<section className="border-t border-rule bg-ink-2 py-14 sm:py-20">
|
<section
|
||||||
|
id="plantear"
|
||||||
|
className="grid-bg-lite border-t border-rule bg-paper py-14 sm:py-20"
|
||||||
|
aria-labelledby="plantear-titulo"
|
||||||
|
>
|
||||||
<div className="shell">
|
<div className="shell">
|
||||||
<div className="max-w-3xl">
|
<div className="grid gap-10 lg:grid-cols-[1fr_1.05fr] lg:items-start lg:gap-16">
|
||||||
<Revelar>
|
<div>
|
||||||
<p className="eyebrow mb-4">Sobre los casos reales</p>
|
<Revelar>
|
||||||
<h2 className="font-display text-2xl font-extrabold sm:text-3xl">
|
<p className="eyebrow mb-4">Siguiente paso</p>
|
||||||
|
<h2
|
||||||
|
id="plantear-titulo"
|
||||||
|
className="font-display text-2xl font-extrabold text-deep sm:text-3xl"
|
||||||
|
>
|
||||||
|
Cuéntanos tu escenario y lo estructuramos igual
|
||||||
|
</h2>
|
||||||
|
<p className="mt-4 text-sm leading-relaxed text-deep-muted sm:text-base">
|
||||||
|
Los seis pasos no son un formato de presentación: son el orden en el que conviene
|
||||||
|
trabajar. Empieza describiendo el lugar y nosotros hacemos las preguntas que
|
||||||
|
faltan.
|
||||||
|
</p>
|
||||||
|
</Revelar>
|
||||||
|
|
||||||
|
<hr className="hair-lite my-8" />
|
||||||
|
|
||||||
|
<h3 className="font-display text-lg font-extrabold text-deep">
|
||||||
Por qué todavía no publicamos casos con nombre
|
Por qué todavía no publicamos casos con nombre
|
||||||
</h2>
|
</h3>
|
||||||
</Revelar>
|
<p className="mt-3 text-sm leading-relaxed text-deep-muted">
|
||||||
<p className="mt-4 text-sm leading-relaxed text-muted sm:text-base">
|
Un caso de estudio requiere autorización del cliente sobre qué se puede contar, qué
|
||||||
Un caso de estudio requiere autorización del cliente sobre qué se puede contar, qué
|
periodo cubre y qué cifras se pueden publicar. Mientras eso no exista por escrito,
|
||||||
periodo cubre y qué cifras se pueden publicar. Mientras eso no exista por escrito,
|
preferimos escenarios rotulados como estos antes que atribuir un resultado a una
|
||||||
preferimos escenarios rotulados como estos antes que atribuir un resultado a una marca
|
marca que no lo ha confirmado.
|
||||||
que no lo ha confirmado.
|
</p>
|
||||||
</p>
|
|
||||||
<div className="mt-8 flex flex-wrap gap-3">
|
|
||||||
<Link href="/contacto" className="btn btn-signal">
|
|
||||||
Plantear tu escenario
|
|
||||||
</Link>
|
|
||||||
<Link href="/evidencia" className="btn btn-ghost">
|
|
||||||
Ver el centro de evidencia
|
|
||||||
</Link>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<FormularioContacto
|
||||||
|
id="formulario-casos"
|
||||||
|
canal="catalogo"
|
||||||
|
variante="claro"
|
||||||
|
titulo="Plantear mi escenario"
|
||||||
|
descripcion="Completa la frase del mensaje con el caso que más se parezca al tuyo, o descríbelo con tus palabras. Respondemos con el escenario estructurado."
|
||||||
|
mensajeInicial={MENSAJE_INICIAL}
|
||||||
|
textoBoton="Enviar mi escenario"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -390,10 +448,9 @@ export default function PaginaCasos() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function parrafo(claro: boolean): string {
|
function parrafo(claro: boolean): string {
|
||||||
return [
|
return ["text-sm leading-relaxed sm:text-base", claro ? "text-deep-muted" : "text-muted"].join(
|
||||||
"text-sm leading-relaxed sm:text-base",
|
" ",
|
||||||
claro ? "text-deep-muted" : "text-muted",
|
);
|
||||||
].join(" ");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function Paso({
|
function Paso({
|
||||||
|
|||||||
@@ -3,24 +3,52 @@ import Image from "next/image";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import { FormularioContacto } from "@/components/formulario/FormularioContacto";
|
import { FormularioContacto } from "@/components/formulario/FormularioContacto";
|
||||||
import { Revelar } from "@/components/site/Revelar";
|
import { PastillaEscenario } from "@/components/marca/IconoEscenario";
|
||||||
|
import { RielClases } from "@/components/marca/RielClases";
|
||||||
|
import { HeroeLanding, FOTOS } from "@/components/site/HeroeLanding";
|
||||||
|
import { TablaComparativa } from "@/components/site/catalogo/TablaComparativa";
|
||||||
import { TarjetaProducto } from "@/components/site/catalogo/TarjetaProducto";
|
import { TarjetaProducto } from "@/components/site/catalogo/TarjetaProducto";
|
||||||
import { iconoDeAplicacion } from "@/components/site/catalogo/iconos";
|
import { iconoDeAplicacion } from "@/components/site/catalogo/iconos";
|
||||||
import { CATEGORIAS, getCategoria, productosDeCategoria } from "@/data/catalogo";
|
import { CATEGORIAS, getCategoria, productosDeCategoria } from "@/data/catalogo";
|
||||||
import type { ClaseFuego } from "@/data/clases-fuego";
|
import { CLASES, type ClaseFuego } from "@/data/clases-fuego";
|
||||||
import { SITIO_URL } from "@/data/empresa";
|
import { SITIO_URL } from "@/data/empresa";
|
||||||
import { INDUSTRIAS } from "@/data/industrias";
|
import { escenariosDeCategoria } from "@/data/escenarios";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* /catalogo/[categoria] — las ocho rutas que el sitio original no tenía.
|
* /catalogo/[categoria] — la estantería de una categoría.
|
||||||
*
|
*
|
||||||
* Además de los productos, la página cruza con los segmentos prioritarios del
|
* Misma corrección que el catálogo: la página explicaba y ahora muestra. Se
|
||||||
* estudio: si esta categoría aparece en el paquete recomendado de una
|
* fueron la rejilla de tarjetas de escenario con su riesgo desarrollado, los
|
||||||
* industria, se enlaza. Es lo que convierte un catálogo en una ruta de compra.
|
* párrafos que introducían cada sección y el discurso de «elige por tu lugar»,
|
||||||
|
* que ya vive en el inicio, en /soluciones y en /cumplimiento-nom-002.
|
||||||
|
*
|
||||||
|
* Lo que se conserva es el dato: la descripción de la fuente, las clases
|
||||||
|
* declaradas por los equipos de la categoría, las aplicaciones que la propia
|
||||||
|
* fuente le atribuye y los lugares donde entra —esos, como pastillas que
|
||||||
|
* enlazan al catálogo ya filtrado, no como tarjetas—. Es la densidad técnica
|
||||||
|
* que posiciona la página.
|
||||||
|
*
|
||||||
|
* Cada categoría estrena fotografía y recorte, para que ocho páginas con la
|
||||||
|
* misma plantilla no se lean como ocho copias.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
type Props = { params: Promise<{ categoria: string }> };
|
type Props = { params: Promise<{ categoria: string }> };
|
||||||
|
|
||||||
|
type Portada = { foto: string; enfoque: string };
|
||||||
|
|
||||||
|
const PORTADAS: Record<string, Portada> = {
|
||||||
|
"extintores-portatiles": { foto: FOTOS.practica, enfoque: "center 42%" },
|
||||||
|
"extintores-fijos": { foto: FOTOS.planta, enfoque: "center 32%" },
|
||||||
|
"productos-moviles": { foto: FOTOS.bombero, enfoque: "center 24%" },
|
||||||
|
rociadores: { foto: FOTOS.galeria, enfoque: "center 46%" },
|
||||||
|
"agentes-extinguidores": { foto: FOTOS.portadaGaleria, enfoque: "center 52%" },
|
||||||
|
firemiks: { foto: FOTOS.planta, enfoque: "right 66%" },
|
||||||
|
"proteccion-civil": { foto: FOTOS.proteccionCivil, enfoque: "center 30%" },
|
||||||
|
robots: { foto: FOTOS.galeria, enfoque: "left 28%" },
|
||||||
|
};
|
||||||
|
|
||||||
|
const PORTADA_RESPALDO: Portada = { foto: FOTOS.practica, enfoque: "center" };
|
||||||
|
|
||||||
export function generateStaticParams(): Array<{ categoria: string }> {
|
export function generateStaticParams(): Array<{ categoria: string }> {
|
||||||
return CATEGORIAS.map((c) => ({ categoria: c.slug }));
|
return CATEGORIAS.map((c) => ({ categoria: c.slug }));
|
||||||
}
|
}
|
||||||
@@ -34,7 +62,13 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const url = `${SITIO_URL}/catalogo/${categoria.slug}`;
|
const url = `${SITIO_URL}/catalogo/${categoria.slug}`;
|
||||||
const descripcion = `${categoria.tagline} ${productosDeCategoria(categoria.slug).length} equipos con clases de fuego, capacidad y agente declarados.`;
|
const cuantos = productosDeCategoria(categoria.slug).length;
|
||||||
|
const lugares = escenariosDeCategoria(categoria.slug)
|
||||||
|
.slice(0, 4)
|
||||||
|
.map((e) => e.nombre.toLowerCase())
|
||||||
|
.join(", ");
|
||||||
|
|
||||||
|
const descripcion = `${categoria.tagline} ${cuantos} ${cuantos === 1 ? "equipo" : "equipos"} con clases de fuego, capacidad y agente declarados${lugares ? `, para ${lugares}` : ""}. Cotiza directo.`;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: categoria.nombre,
|
title: categoria.nombre,
|
||||||
@@ -56,21 +90,28 @@ export default async function PaginaCategoria({ params }: Props) {
|
|||||||
if (!categoria) notFound();
|
if (!categoria) notFound();
|
||||||
|
|
||||||
const productos = productosDeCategoria(categoria.slug);
|
const productos = productosDeCategoria(categoria.slug);
|
||||||
const industrias = INDUSTRIAS.filter((i) => i.categorias.includes(categoria.slug));
|
const escenarios = escenariosDeCategoria(categoria.slug);
|
||||||
|
|
||||||
// clases que realmente cubre algún producto de la categoría
|
|
||||||
const clases = Array.from(
|
|
||||||
new Set(productos.flatMap((p) => p.clases)),
|
|
||||||
) as ClaseFuego[];
|
|
||||||
|
|
||||||
const otras = CATEGORIAS.filter((c) => c.slug !== categoria.slug);
|
const otras = CATEGORIAS.filter((c) => c.slug !== categoria.slug);
|
||||||
|
|
||||||
|
// clases que realmente declara algún producto de la categoría
|
||||||
|
const declaradas = new Set(productos.flatMap((p) => p.clases));
|
||||||
|
const clases: ClaseFuego[] = CLASES.filter((c) => declaradas.has(c));
|
||||||
|
|
||||||
|
const portada = PORTADAS[categoria.slug] ?? PORTADA_RESPALDO;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* ---------- encabezado de categoría ---------- */}
|
{/* ================= HÉROE CORTO ================= */}
|
||||||
<section className="grid-bg bg-ink pt-12 pb-14 sm:pt-16 sm:pb-16">
|
<HeroeLanding
|
||||||
<div className="shell">
|
foto={portada.foto}
|
||||||
<nav aria-label="Ruta de navegación" className="mb-8">
|
enfoque={portada.enfoque}
|
||||||
|
prioridad
|
||||||
|
altura="medio"
|
||||||
|
eyebrow={`Categoría ${String(categoria.orden).padStart(2, "0")} de ${CATEGORIAS.length}`}
|
||||||
|
titulo={categoria.nombre}
|
||||||
|
entrada={categoria.tagline}
|
||||||
|
antes={
|
||||||
|
<nav aria-label="Ruta de navegación">
|
||||||
<ol className="flex flex-wrap items-center gap-2 font-mono text-[0.625rem] uppercase tracking-[0.14em] text-faint">
|
<ol className="flex flex-wrap items-center gap-2 font-mono text-[0.625rem] uppercase tracking-[0.14em] text-faint">
|
||||||
<li>
|
<li>
|
||||||
<Link href="/" className="hover:text-bright">
|
<Link href="/" className="hover:text-bright">
|
||||||
@@ -87,185 +128,203 @@ export default async function PaginaCategoria({ params }: Props) {
|
|||||||
<li className="text-bright">{categoria.nombre}</li>
|
<li className="text-bright">{categoria.nombre}</li>
|
||||||
</ol>
|
</ol>
|
||||||
</nav>
|
</nav>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{clases.length > 0 ? (
|
||||||
|
<div className="mb-6">
|
||||||
|
<RielClases activas={clases} tamano="md" etiqueta="Clases declaradas" />
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<div className="grid gap-10 lg:grid-cols-[1.2fr_1fr] lg:items-center">
|
<Link href="/catalogo" className="btn btn-ghost btn-sm">
|
||||||
<div className="anim-rise">
|
← Volver al catálogo completo
|
||||||
<p className="eyebrow mb-5">
|
</Link>
|
||||||
Categoría {String(categoria.orden).padStart(2, "0")} de {CATEGORIAS.length}
|
</HeroeLanding>
|
||||||
</p>
|
|
||||||
<h1 className="font-display text-[2rem] font-black uppercase leading-[1.04] tracking-tight sm:text-[2.75rem] lg:text-5xl">
|
|
||||||
{categoria.nombre}
|
|
||||||
</h1>
|
|
||||||
<p className="mt-5 max-w-xl text-base leading-relaxed text-bright/85 sm:text-lg">
|
|
||||||
{categoria.tagline}
|
|
||||||
</p>
|
|
||||||
<p className="mt-4 max-w-xl text-sm leading-relaxed text-muted">
|
|
||||||
{categoria.descripcion}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<dl className="spec mt-8 sm:grid-cols-2">
|
{/* ================= LOS EQUIPOS ================= */}
|
||||||
|
<section
|
||||||
|
id="equipos"
|
||||||
|
className="border-t border-rule bg-ink-2 py-10 scroll-mt-24 sm:py-12"
|
||||||
|
aria-labelledby="equipos-titulo"
|
||||||
|
>
|
||||||
|
<div className="shell">
|
||||||
|
<div className="flex flex-wrap items-center justify-between gap-3 border-b border-rule pb-4">
|
||||||
|
<h2
|
||||||
|
id="equipos-titulo"
|
||||||
|
className="font-mono text-xs uppercase tracking-[0.12em] text-bright"
|
||||||
|
>
|
||||||
|
{productos.length} {productos.length === 1 ? "equipo" : "equipos"}
|
||||||
|
<span className="text-faint"> en {categoria.nombre.toLowerCase()}</span>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
href={`/catalogo?categoria=${encodeURIComponent(categoria.slug)}`}
|
||||||
|
className="font-mono text-[0.625rem] uppercase tracking-[0.14em] text-signal hover:text-signal-hi"
|
||||||
|
>
|
||||||
|
Filtrar en el catálogo →
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{productos.map((producto) => (
|
||||||
|
<TarjetaProducto key={producto.slug} producto={producto} sinCategoria />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Comparación cerrada por omisión: sirve al comprador técnico, pero
|
||||||
|
no es lo primero que hay que ver. */}
|
||||||
|
{productos.length > 1 ? (
|
||||||
|
<details className="mt-8 rounded-[6px] border border-rule bg-ink">
|
||||||
|
<summary
|
||||||
|
className="cursor-pointer list-none px-4 py-3.5 font-mono text-[0.6875rem] uppercase tracking-[0.14em] text-bright transition-colors hover:text-signal-hi [&::-webkit-details-marker]:hidden"
|
||||||
|
style={{ WebkitTapHighlightColor: "transparent" }}
|
||||||
|
>
|
||||||
|
Comparar especificaciones ({productos.length}) +
|
||||||
|
</summary>
|
||||||
|
<div className="border-t border-rule p-4">
|
||||||
|
<TablaComparativa productos={productos} claro={false} />
|
||||||
|
<p className="mt-4 text-xs leading-relaxed text-faint">
|
||||||
|
Donde la fuente no declara un dato, la celda lo dice: no lo rellenamos con una
|
||||||
|
estimación. La ficha técnica y el certificado por número de modelo se entregan
|
||||||
|
antes de cotizar.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* ================= EL DATO DE LA CATEGORÍA ================= */}
|
||||||
|
<section
|
||||||
|
className="border-t border-rule bg-ink py-10 sm:py-12"
|
||||||
|
aria-labelledby="contexto-titulo"
|
||||||
|
>
|
||||||
|
<div className="shell">
|
||||||
|
<div className="grid gap-8 lg:grid-cols-[1.15fr_1fr] lg:gap-12">
|
||||||
|
<div className="min-w-0">
|
||||||
|
<h2
|
||||||
|
id="contexto-titulo"
|
||||||
|
className="font-display text-lg font-extrabold uppercase tracking-wide sm:text-xl"
|
||||||
|
>
|
||||||
|
Qué resuelve esta categoría
|
||||||
|
</h2>
|
||||||
|
<p className="mt-3 text-sm leading-relaxed text-muted">{categoria.descripcion}</p>
|
||||||
|
|
||||||
|
<dl className="spec mt-6 sm:grid-cols-2">
|
||||||
<div className="spec-cell">
|
<div className="spec-cell">
|
||||||
<dt className="spec-key">Equipos en la categoría</dt>
|
<dt className="spec-key">Equipos en la categoría</dt>
|
||||||
<dd className="spec-val">{productos.length}</dd>
|
<dd className="spec-val">{productos.length}</dd>
|
||||||
</div>
|
</div>
|
||||||
<div className="spec-cell">
|
<div className="spec-cell">
|
||||||
<dt className="spec-key">Clases declaradas</dt>
|
<dt className="spec-key">Clases declaradas</dt>
|
||||||
<dd className="spec-val">{clases.length > 0 ? clases.join(" · ") : "No aplica"}</dd>
|
<dd className="spec-val">
|
||||||
|
{clases.length > 0 ? clases.join(" · ") : "No aplica"}
|
||||||
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="relative aspect-[4/3] w-full overflow-hidden rounded-[6px] border border-rule bg-ink-2">
|
<div className="min-w-0">
|
||||||
<Image
|
<h3 className="font-mono text-[0.6875rem] uppercase tracking-[0.16em] text-faint">
|
||||||
src={categoria.imagen}
|
Dónde la ubica la fuente
|
||||||
alt={categoria.nombre}
|
</h3>
|
||||||
fill
|
<ul className="mt-3 flex flex-wrap gap-2">
|
||||||
priority
|
{categoria.aplicaciones.map((aplicacion) => {
|
||||||
sizes="(max-width: 1024px) 92vw, 34rem"
|
const icono = iconoDeAplicacion(aplicacion);
|
||||||
className="object-contain p-8"
|
return (
|
||||||
/>
|
<li
|
||||||
|
key={aplicacion}
|
||||||
|
className="flex items-center gap-2 rounded-[3px] border border-rule-2 bg-ink-2 px-2.5 py-1.5"
|
||||||
|
>
|
||||||
|
{icono ? (
|
||||||
|
<Image
|
||||||
|
src={icono}
|
||||||
|
alt=""
|
||||||
|
aria-hidden="true"
|
||||||
|
width={18}
|
||||||
|
height={18}
|
||||||
|
className="h-[18px] w-[18px] shrink-0 object-contain opacity-80"
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
<span className="font-mono text-[0.6875rem] uppercase tracking-[0.1em] text-bright">
|
||||||
|
{aplicacion}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
{escenarios.length > 0 ? (
|
||||||
|
<>
|
||||||
|
<h3 className="mt-7 font-mono text-[0.6875rem] uppercase tracking-[0.16em] text-faint">
|
||||||
|
Lugares donde entra en la configuración
|
||||||
|
</h3>
|
||||||
|
<ul className="mt-3 flex flex-wrap gap-2">
|
||||||
|
{escenarios.map((escenario) => (
|
||||||
|
<li key={escenario.slug} className="min-w-0">
|
||||||
|
<Link
|
||||||
|
href={`/catalogo?escenario=${encodeURIComponent(escenario.slug)}`}
|
||||||
|
className="inline-block rounded-[3px] transition-opacity hover:opacity-80"
|
||||||
|
>
|
||||||
|
<PastillaEscenario escenario={escenario} />
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* ---------- aplicaciones ---------- */}
|
{/* ================= FORMULARIO Y OTRAS CATEGORÍAS ================= */}
|
||||||
<section className="border-t border-rule bg-ink-2 py-12 sm:py-14" aria-labelledby="aplicaciones-titulo">
|
<section
|
||||||
<div className="shell">
|
id="cotizar"
|
||||||
<h2
|
className="grid-bg-lite border-t border-rule bg-paper py-14 scroll-mt-20 sm:py-20"
|
||||||
id="aplicaciones-titulo"
|
aria-labelledby="cotizar-titulo"
|
||||||
className="font-display text-lg font-extrabold uppercase tracking-wide sm:text-xl"
|
>
|
||||||
>
|
|
||||||
Dónde la ubica la fuente
|
|
||||||
</h2>
|
|
||||||
<ul className="mt-6 flex flex-wrap gap-2.5">
|
|
||||||
{categoria.aplicaciones.map((aplicacion) => {
|
|
||||||
const icono = iconoDeAplicacion(aplicacion);
|
|
||||||
return (
|
|
||||||
<li
|
|
||||||
key={aplicacion}
|
|
||||||
className="flex items-center gap-2 rounded-[3px] border border-rule-2 bg-ink px-3 py-2"
|
|
||||||
>
|
|
||||||
{icono ? (
|
|
||||||
<Image
|
|
||||||
src={icono}
|
|
||||||
alt=""
|
|
||||||
width={20}
|
|
||||||
height={20}
|
|
||||||
sizes="20px"
|
|
||||||
className="h-5 w-5 shrink-0 object-contain opacity-80"
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
<span className="font-mono text-[0.6875rem] uppercase tracking-[0.1em] text-bright">
|
|
||||||
{aplicacion}
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
<p className="mt-5 max-w-2xl text-xs leading-relaxed text-faint">
|
|
||||||
Las aplicaciones provienen de la información pública de FSPM. La selección final depende
|
|
||||||
del escenario real: material, superficie, personal y nivel de riesgo del centro de
|
|
||||||
trabajo.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* ---------- productos de la categoría ---------- */}
|
|
||||||
<section className="grid-bg-lite bg-paper py-14 sm:py-20" aria-labelledby="productos-titulo">
|
|
||||||
<div className="shell">
|
|
||||||
<Revelar>
|
|
||||||
<p className="eyebrow mb-4">Equipos</p>
|
|
||||||
<h2 id="productos-titulo" className="font-display text-2xl font-extrabold text-deep sm:text-3xl">
|
|
||||||
{productos.length === 1
|
|
||||||
? "El equipo de esta categoría"
|
|
||||||
: `Los ${productos.length} equipos de esta categoría`}
|
|
||||||
</h2>
|
|
||||||
</Revelar>
|
|
||||||
|
|
||||||
<div className="mt-9 grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
|
|
||||||
{productos.map((producto) => (
|
|
||||||
<TarjetaProducto key={producto.slug} producto={producto} claro sinCategoria />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* ---------- cruce con industrias ---------- */}
|
|
||||||
{industrias.length > 0 ? (
|
|
||||||
<section className="border-t border-rule bg-ink py-14 sm:py-20" aria-labelledby="industrias-titulo">
|
|
||||||
<div className="shell">
|
|
||||||
<Revelar>
|
|
||||||
<p className="eyebrow mb-4">Dónde aplica</p>
|
|
||||||
<h2 id="industrias-titulo" className="font-display text-2xl font-extrabold sm:text-3xl">
|
|
||||||
Segmentos donde esta categoría forma parte del paquete
|
|
||||||
</h2>
|
|
||||||
<p className="mt-3 max-w-2xl text-sm leading-relaxed text-muted sm:text-base">
|
|
||||||
Un equipo aislado rara vez resuelve una instalación. Estas son las operaciones donde
|
|
||||||
{" "}
|
|
||||||
{categoria.nombre.toLowerCase()} entra dentro de una configuración más amplia.
|
|
||||||
</p>
|
|
||||||
</Revelar>
|
|
||||||
|
|
||||||
<ul className="mt-9 grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
||||||
{industrias.map((industria) => (
|
|
||||||
<li key={industria.slug}>
|
|
||||||
<Link
|
|
||||||
href={`/soluciones/${industria.slug}`}
|
|
||||||
className="group flex h-full flex-col rounded-[6px] border border-rule bg-ink-2 p-5 transition-colors duration-200 hover:border-rule-2 [-webkit-tap-highlight-color:transparent]"
|
|
||||||
>
|
|
||||||
<h3 className="font-display text-base font-extrabold leading-tight text-bright">
|
|
||||||
{industria.nombre}
|
|
||||||
</h3>
|
|
||||||
<p className="mt-2.5 text-sm leading-relaxed text-muted">{industria.dolor}</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 solución →
|
|
||||||
</span>
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{/* ---------- formulario ---------- */}
|
|
||||||
<section id="cotizar" className="border-t border-rule bg-ink-2 py-14 sm:py-20">
|
|
||||||
<div className="shell">
|
<div className="shell">
|
||||||
<div className="grid gap-10 lg:grid-cols-[1fr_1.05fr] lg:items-start lg:gap-16">
|
<div className="grid gap-10 lg:grid-cols-[1fr_1.05fr] lg:items-start lg:gap-16">
|
||||||
<div>
|
<div>
|
||||||
<Revelar>
|
<p className="eyebrow mb-4">Cotizar esta categoría</p>
|
||||||
<p className="eyebrow mb-4">Cotizar esta categoría</p>
|
<h2
|
||||||
<h2 className="font-display text-2xl font-extrabold sm:text-3xl">
|
id="cotizar-titulo"
|
||||||
Cuéntanos la instalación y te decimos cuál de estos equipos aplica
|
className="font-display text-2xl font-extrabold text-deep sm:text-3xl"
|
||||||
</h2>
|
>
|
||||||
<p className="mt-4 text-sm leading-relaxed text-muted sm:text-base">
|
Cuéntanos la instalación y te decimos cuál de estos equipos aplica
|
||||||
Y también cuál no. Descartar un equipo que no corresponde ahorra más dinero que un
|
</h2>
|
||||||
descuento.
|
<p className="mt-4 text-sm leading-relaxed text-deep-muted sm:text-base">
|
||||||
</p>
|
Y también cuál no. Descartar un equipo que no corresponde ahorra más que un
|
||||||
</Revelar>
|
descuento. Cuatro campos, sin costo y sin compromiso de compra.
|
||||||
|
</p>
|
||||||
|
|
||||||
<hr className="hair my-8" />
|
<hr className="hair-lite my-8" />
|
||||||
|
|
||||||
<h3 className="font-mono text-[0.6875rem] uppercase tracking-[0.16em] text-faint">
|
<nav aria-label="Otras categorías">
|
||||||
Otras categorías
|
<h3 className="font-mono text-[0.6875rem] uppercase tracking-[0.16em] text-deep-muted">
|
||||||
</h3>
|
Otras categorías
|
||||||
<ul className="mt-4 flex flex-wrap gap-2">
|
</h3>
|
||||||
{otras.map((c) => (
|
<ul className="mt-4 flex flex-wrap gap-2">
|
||||||
<li key={c.slug}>
|
{otras.map((c) => (
|
||||||
<Link
|
<li key={c.slug}>
|
||||||
href={`/catalogo/${c.slug}`}
|
<Link
|
||||||
className="badge hover:border-signal hover:text-bright [-webkit-tap-highlight-color:transparent]"
|
href={`/catalogo/${c.slug}`}
|
||||||
>
|
className="badge text-deep-muted hover:border-signal hover:text-deep"
|
||||||
{c.nombre}
|
>
|
||||||
</Link>
|
{c.nombre}
|
||||||
</li>
|
</Link>
|
||||||
))}
|
</li>
|
||||||
</ul>
|
))}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<FormularioContacto
|
<FormularioContacto
|
||||||
canal="catalogo"
|
canal="catalogo"
|
||||||
|
variante="claro"
|
||||||
|
clases={clases}
|
||||||
|
mensajeInicial={`Me interesa la categoría: ${categoria.nombre}. `}
|
||||||
titulo={`Cotizar ${categoria.nombre.toLowerCase()}`}
|
titulo={`Cotizar ${categoria.nombre.toLowerCase()}`}
|
||||||
descripcion="Respondemos con la configuración que corresponde al escenario y la lista de documentos que debe acompañarla."
|
descripcion="Respondemos con la configuración que corresponde al escenario y la lista de documentos que debe acompañarla."
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,24 +1,33 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import Image from "next/image";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { FormularioContacto } from "@/components/formulario/FormularioContacto";
|
import { HeroeLanding, FOTOS } from "@/components/site/HeroeLanding";
|
||||||
import { Revelar } from "@/components/site/Revelar";
|
|
||||||
import { ExploradorCatalogo } from "@/components/site/catalogo/ExploradorCatalogo";
|
import { ExploradorCatalogo } from "@/components/site/catalogo/ExploradorCatalogo";
|
||||||
import { CATEGORIAS, PRODUCTOS, productosDeCategoria } from "@/data/catalogo";
|
import { construirConsulta, leerEstado } from "@/components/site/catalogo/estado";
|
||||||
import { CLASES_ORDENADAS } from "@/data/clases-fuego";
|
import { CATEGORIAS, PRODUCTOS } from "@/data/catalogo";
|
||||||
import { SITIO_URL } from "@/data/empresa";
|
import { SITIO_URL } from "@/data/empresa";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* /catalogo — la corrección directa al hallazgo 5.1 del estudio: ocho
|
* /catalogo — la tienda.
|
||||||
* 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
|
* Corrección de rumbo pedida por el cliente: la versión anterior explicaba
|
||||||
* y por aplicación, y se sale con dos o tres equipos que descartar o cotizar.
|
* cómo elegir antes de mostrar un solo equipo —tres preguntas, dos «puertas»,
|
||||||
|
* trece tarjetas de escenario con su riesgo, párrafos de contexto entre
|
||||||
|
* secciones—. Todo eso ya vive en el inicio, en /soluciones y en
|
||||||
|
* /cumplimiento-nom-002, y aquí estorbaba.
|
||||||
|
*
|
||||||
|
* Esta página quedó en tres piezas: héroe corto, barra lateral con filtros y
|
||||||
|
* rejilla de producto. Nada más. La mecánica de selección —clase de fuego,
|
||||||
|
* escenario, categoría, agente, búsqueda con expresiones regulares— se conservó
|
||||||
|
* completa, porque es lo que el cliente aprobó; solo dejó de explicarse.
|
||||||
|
*
|
||||||
|
* El estado sigue viviendo en la cadena de consulta, así que
|
||||||
|
* `/catalogo?escenario=gasolineras` —el enlace que ya usan otras páginas y que
|
||||||
|
* el vendedor manda por WhatsApp— abre el catálogo filtrado.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const TITULO = "Catálogo de equipos y agentes contra incendio";
|
const TITULO = "Catálogo de equipos y agentes contra incendio";
|
||||||
const DESCRIPCION =
|
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.";
|
"22 equipos filtrables por clase de fuego A B C D K, categoría, agente y lugar de instalación: extintores portátiles y fijos, equipos móviles, rociadores automáticos, agentes FireAde y dosificadores FireMiks. Especificación declarada y cotización directa.";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: TITULO,
|
title: TITULO,
|
||||||
@@ -28,22 +37,29 @@ export const metadata: Metadata = {
|
|||||||
title: TITULO,
|
title: TITULO,
|
||||||
description: DESCRIPCION,
|
description: DESCRIPCION,
|
||||||
url: `${SITIO_URL}/catalogo`,
|
url: `${SITIO_URL}/catalogo`,
|
||||||
|
images: [{ url: FOTOS.practica }],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function PaginaCatalogo() {
|
type Props = {
|
||||||
const resumen = [
|
searchParams: Promise<Record<string, string | string[] | undefined>>;
|
||||||
{ 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` },
|
export default async function PaginaCatalogo({ searchParams }: Props) {
|
||||||
];
|
const estado = leerEstado(await searchParams);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* ---------- encabezado: la lógica de lectura del catálogo ---------- */}
|
<HeroeLanding
|
||||||
<section className="grid-bg relative overflow-hidden bg-ink pt-14 pb-16 sm:pt-20 sm:pb-20">
|
foto={FOTOS.practica}
|
||||||
<div className="shell">
|
enfoque="center 38%"
|
||||||
<nav aria-label="Ruta de navegación" className="mb-8">
|
prioridad
|
||||||
|
altura="medio"
|
||||||
|
eyebrow="Catálogo"
|
||||||
|
titulo="Equipos y agentes contra incendio"
|
||||||
|
entrada={`${PRODUCTOS.length} equipos en ${CATEGORIAS.length} categorías. Filtra por clase de fuego, por categoría, por agente o por el lugar que proteges; el clic en el nombre abre la ficha técnica.`}
|
||||||
|
antes={
|
||||||
|
<nav aria-label="Ruta de navegación">
|
||||||
<ol className="flex flex-wrap items-center gap-2 font-mono text-[0.625rem] uppercase tracking-[0.14em] text-faint">
|
<ol className="flex flex-wrap items-center gap-2 font-mono text-[0.625rem] uppercase tracking-[0.14em] text-faint">
|
||||||
<li>
|
<li>
|
||||||
<Link href="/" className="hover:text-bright">
|
<Link href="/" className="hover:text-bright">
|
||||||
@@ -54,187 +70,14 @@ export default function PaginaCatalogo() {
|
|||||||
<li className="text-bright">Catálogo</li>
|
<li className="text-bright">Catálogo</li>
|
||||||
</ol>
|
</ol>
|
||||||
</nav>
|
</nav>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="grid gap-12 lg:grid-cols-[1.15fr_1fr] lg:items-start">
|
{/* El explorador guarda su propio estado y reescribe la URL sin navegar.
|
||||||
<div className="anim-rise">
|
Si alguien llega desde otra página a `/catalogo?escenario=…` estando
|
||||||
<p className="eyebrow mb-5">Catálogo técnico</p>
|
ya en `/catalogo`, la navegación no lo remontaría por sí sola: esta
|
||||||
<h1 className="font-display text-[2.125rem] font-black uppercase leading-[1.02] tracking-tight sm:text-5xl lg:text-[3.5rem]">
|
llave lo reinicia con el estado que trae la URL. */}
|
||||||
Un catálogo se lee por clase de fuego, no por nombre de producto
|
<ExploradorCatalogo key={construirConsulta(estado)} inicial={estado} />
|
||||||
</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>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { FormularioContacto } from "@/components/formulario/FormularioContacto";
|
import { FormularioContacto } from "@/components/formulario/FormularioContacto";
|
||||||
|
import { Migas } from "@/components/site/contenido/Migas";
|
||||||
|
import { FOTOS, HeroeLanding } from "@/components/site/HeroeLanding";
|
||||||
import { Revelar } from "@/components/site/Revelar";
|
import { Revelar } from "@/components/site/Revelar";
|
||||||
import { EMPRESA, SITIO_URL, WHATSAPP } from "@/data/empresa";
|
import { EMPRESA, SITIO_URL, WHATSAPP } from "@/data/empresa";
|
||||||
|
|
||||||
@@ -11,10 +13,14 @@ import { EMPRESA, SITIO_URL, WHATSAPP } from "@/data/empresa";
|
|||||||
* atención, ni domicilio con número, ni mapa: nada de eso está confirmado, y
|
* atención, ni domicilio con número, ni mapa: nada de eso está confirmado, y
|
||||||
* una dirección equivocada en un sitio de protección contra incendios es un
|
* una dirección equivocada en un sitio de protección contra incendios es un
|
||||||
* problema real, no un detalle.
|
* problema real, no un detalle.
|
||||||
|
*
|
||||||
|
* El formulario pide cuatro campos, así que la página no puede prometer que la
|
||||||
|
* respuesta llega con la clase de fuego y la superficie ya declaradas: lo que
|
||||||
|
* promete es que preguntamos nosotros lo que falte.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const TITULO = "Contacto";
|
const TITULO = "Contacto: habla con un especialista de FSPM";
|
||||||
const DESCRIPCION = `Habla con ${EMPRESA.nombre}. Teléfono, correo y solicitud de diagnóstico para operaciones que necesitan protección contra incendios documentada.`;
|
const DESCRIPCION = `Teléfono, correo y WhatsApp de ${EMPRESA.nombre}, en ${EMPRESA.ciudad}. Solicita un diagnóstico de protección contra incendios con cuatro campos y te contesta una persona con nombre, no un buzón compartido.`;
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: TITULO,
|
title: TITULO,
|
||||||
@@ -31,37 +37,33 @@ const MENSAJE_WHATSAPP = encodeURIComponent(
|
|||||||
"Hola, me gustaría información sobre protección contra incendios.",
|
"Hola, me gustaría información sobre protección contra incendios.",
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const MENSAJE_INICIAL = "Necesito proteger ";
|
||||||
|
|
||||||
export default function PaginaContacto() {
|
export default function PaginaContacto() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* ---------- encabezado ---------- */}
|
<HeroeLanding
|
||||||
<section className="grid-bg bg-ink pt-12 pb-14 sm:pt-16 sm:pb-16">
|
antes={<Migas actual="Contacto" />}
|
||||||
<div className="shell">
|
eyebrow="Contacto"
|
||||||
<nav aria-label="Ruta de navegación" className="mb-8">
|
titulo="Si tu situación no puede esperar, marca"
|
||||||
<ol className="flex flex-wrap items-center gap-2 font-mono text-[0.625rem] uppercase tracking-[0.14em] text-faint">
|
entrada="Para todo lo demás, el formulario funciona mejor: son cuatro campos, queda con folio y lo recibe una persona con nombre. Lo que falte lo preguntamos nosotros."
|
||||||
<li>
|
foto={FOTOS.contacto}
|
||||||
<Link href="/" className="hover:text-bright">
|
enfoque="50% 38%"
|
||||||
Inicio
|
altura="alto"
|
||||||
</Link>
|
prioridad
|
||||||
</li>
|
>
|
||||||
<li aria-hidden="true">/</li>
|
<div className="flex flex-col gap-4 sm:flex-row sm:items-center">
|
||||||
<li className="text-bright">Contacto</li>
|
<a href={`tel:${EMPRESA.telefonoTel}`} className="btn btn-signal w-full sm:w-auto">
|
||||||
</ol>
|
Marcar {EMPRESA.telefono}
|
||||||
</nav>
|
</a>
|
||||||
|
<Link
|
||||||
<div className="max-w-3xl anim-rise">
|
href="#formulario"
|
||||||
<p className="eyebrow mb-5">Contacto</p>
|
className="font-mono text-[0.6875rem] uppercase tracking-[0.12em] text-bright underline decoration-rule-2 underline-offset-4 hover:decoration-signal [-webkit-tap-highlight-color:transparent]"
|
||||||
<h1 className="font-display text-[2rem] font-black uppercase leading-[1.03] tracking-tight sm:text-[2.75rem] lg:text-5xl">
|
>
|
||||||
Si tu situación no puede esperar, marca
|
O envía el formulario
|
||||||
</h1>
|
</Link>
|
||||||
<p className="mt-6 text-base leading-relaxed text-muted sm:text-lg">
|
|
||||||
Para todo lo demás, el formulario sirve mejor: llega con la clase de fuego, la
|
|
||||||
superficie y el tipo de operación, así que la primera respuesta puede ser técnica en
|
|
||||||
lugar de una pregunta de vuelta.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</HeroeLanding>
|
||||||
|
|
||||||
{/* ---------- datos verificados ---------- */}
|
{/* ---------- datos verificados ---------- */}
|
||||||
<section className="border-t border-rule bg-ink-2 py-12 sm:py-14" aria-labelledby="datos-titulo">
|
<section className="border-t border-rule bg-ink-2 py-12 sm:py-14" aria-labelledby="datos-titulo">
|
||||||
@@ -73,16 +75,16 @@ export default function PaginaContacto() {
|
|||||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||||
<a
|
<a
|
||||||
href={`tel:${EMPRESA.telefonoTel}`}
|
href={`tel:${EMPRESA.telefonoTel}`}
|
||||||
className="group flex flex-col rounded-[6px] border border-rule bg-ink p-5 transition-colors duration-200 hover:border-signal [-webkit-tap-highlight-color:transparent]"
|
className="group flex flex-col rounded-[6px] border border-signal/60 bg-ink p-5 transition-colors duration-200 hover:border-signal [-webkit-tap-highlight-color:transparent]"
|
||||||
>
|
>
|
||||||
<span className="font-mono text-[0.625rem] uppercase tracking-[0.14em] text-faint">
|
<span className="font-mono text-[0.625rem] uppercase tracking-[0.14em] text-signal">
|
||||||
Teléfono
|
Teléfono
|
||||||
</span>
|
</span>
|
||||||
<span className="mt-2.5 font-mono text-lg font-semibold text-bright transition-colors group-hover:text-signal-hi">
|
<span className="mt-2.5 font-mono text-lg font-semibold text-bright transition-colors group-hover:text-signal-hi">
|
||||||
{EMPRESA.telefono}
|
{EMPRESA.telefono}
|
||||||
</span>
|
</span>
|
||||||
<span className="mt-2 text-xs leading-relaxed text-muted">
|
<span className="mt-2 text-xs leading-relaxed text-muted">
|
||||||
Marca directo desde el móvil.
|
La vía más rápida. Marca directo desde el móvil.
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
@@ -144,15 +146,26 @@ export default function PaginaContacto() {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* ---------- formulario completo ---------- */}
|
{/* ---------- formulario completo ---------- */}
|
||||||
<section id="formulario" className="grid-bg-lite border-t border-rule bg-paper py-14 sm:py-20">
|
<section
|
||||||
|
id="formulario"
|
||||||
|
className="grid-bg-lite border-t border-rule bg-paper py-14 sm:py-20"
|
||||||
|
aria-labelledby="solicitud-titulo"
|
||||||
|
>
|
||||||
<div className="shell">
|
<div className="shell">
|
||||||
<div className="grid gap-10 lg:grid-cols-[1fr_1.1fr] lg:items-start lg:gap-16">
|
<div className="grid gap-10 lg:grid-cols-[1fr_1.1fr] lg:items-start lg:gap-16">
|
||||||
<div>
|
<div>
|
||||||
<Revelar>
|
<Revelar>
|
||||||
<p className="eyebrow mb-4">Solicitud</p>
|
<p className="eyebrow mb-4">Solicitud</p>
|
||||||
<h2 className="font-display text-2xl font-extrabold text-deep sm:text-3xl">
|
<h2
|
||||||
|
id="solicitud-titulo"
|
||||||
|
className="font-display text-2xl font-extrabold text-deep sm:text-3xl"
|
||||||
|
>
|
||||||
Qué pasa cuando envías el formulario
|
Qué pasa cuando envías el formulario
|
||||||
</h2>
|
</h2>
|
||||||
|
<p className="mt-4 text-sm leading-relaxed text-deep-muted sm:text-base">
|
||||||
|
Nombre, correo, teléfono y una línea de contexto. Nada más: los cuestionarios
|
||||||
|
técnicos de catorce campos son la razón por la que la gente cierra la pestaña.
|
||||||
|
</p>
|
||||||
</Revelar>
|
</Revelar>
|
||||||
|
|
||||||
<ol className="mt-8 space-y-6">
|
<ol className="mt-8 space-y-6">
|
||||||
@@ -164,8 +177,8 @@ export default function PaginaContacto() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
n: "02",
|
n: "02",
|
||||||
t: "Se interpreta el escenario",
|
t: "Preguntamos lo que falta",
|
||||||
d: "Con la clase de fuego, la superficie y el tipo de operación que declaraste, alguien revisa qué configuración aplica y qué habría que descartar.",
|
d: "Con la página desde la que escribiste y lo que dejaste en el mensaje ya se intuye el escenario. Lo que no se intuye —qué se puede quemar, quién opera, qué superficie— lo preguntamos nosotros en la primera respuesta.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
n: "03",
|
n: "03",
|
||||||
@@ -196,7 +209,7 @@ export default function PaginaContacto() {
|
|||||||
{[
|
{[
|
||||||
{ href: "/preguntas-frecuentes", label: "Preguntas frecuentes" },
|
{ href: "/preguntas-frecuentes", label: "Preguntas frecuentes" },
|
||||||
{ href: "/cumplimiento-nom-002", label: "Cumplimiento NOM-002" },
|
{ href: "/cumplimiento-nom-002", label: "Cumplimiento NOM-002" },
|
||||||
{ href: "/evidencia", label: "Centro de evidencia" },
|
{ href: "/evidencia", label: "Documentación técnica" },
|
||||||
{ href: "/catalogo", label: "Catálogo" },
|
{ href: "/catalogo", label: "Catálogo" },
|
||||||
].map((enlace) => (
|
].map((enlace) => (
|
||||||
<li key={enlace.href}>
|
<li key={enlace.href}>
|
||||||
@@ -234,11 +247,13 @@ export default function PaginaContacto() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<FormularioContacto
|
<FormularioContacto
|
||||||
|
id="formulario-contacto"
|
||||||
canal="contacto"
|
canal="contacto"
|
||||||
variante="claro"
|
variante="claro"
|
||||||
compacto={false}
|
titulo="Recibir cotización"
|
||||||
titulo="Solicitar diagnóstico"
|
descripcion="Completa la frase del mensaje con lo que hay que proteger. Te respondemos con el escenario, el equipo que aplica y qué documentación te corresponde."
|
||||||
descripcion="Cuéntanos qué operación hay que proteger. Te respondemos con el escenario, el equipo que aplica y qué documentación te corresponde."
|
mensajeInicial={MENSAJE_INICIAL}
|
||||||
|
textoBoton="Recibir cotización"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,21 +1,30 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { FormularioContacto } from "@/components/formulario/FormularioContacto";
|
import { FormularioContacto } from "@/components/formulario/FormularioContacto";
|
||||||
|
import { TarjetaEscenario } from "@/components/marca/IconoEscenario";
|
||||||
|
import { Migas } from "@/components/site/contenido/Migas";
|
||||||
|
import { FOTOS, HeroeLanding } from "@/components/site/HeroeLanding";
|
||||||
import { Revelar } from "@/components/site/Revelar";
|
import { Revelar } from "@/components/site/Revelar";
|
||||||
import { EMPRESA, NORMAS, SITIO_URL } from "@/data/empresa";
|
import { EMPRESA, NORMAS, SITIO_URL } from "@/data/empresa";
|
||||||
|
import { ESCENARIOS, type Escenario } from "@/data/escenarios";
|
||||||
import { INDUSTRIAS } from "@/data/industrias";
|
import { INDUSTRIAS } from "@/data/industrias";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* /cumplimiento-nom-002 — página de intención de búsqueda alta.
|
* /cumplimiento-nom-002 — la página de mayor intención de compra del sitio.
|
||||||
*
|
*
|
||||||
* Regla del contenido: no se citan artículos, umbrales ni plazos que no estén
|
* Orden deliberado: primero las tres preguntas que hace el dueño de un negocio
|
||||||
* en src/data/empresa.ts o en la fuente pública. Lo que no está verificado se
|
* —me aplica, qué me pueden multar, qué tengo que tener— y solo después la
|
||||||
* declara como no reproducido, no se estima.
|
* precisión normativa. Cada bloque está escrito en dos niveles: la primera
|
||||||
|
* frase la entiende cualquiera, la segunda le da al experto el dato.
|
||||||
|
*
|
||||||
|
* Regla del contenido: no se citan artículos, umbrales, plazos ni montos de
|
||||||
|
* sanción que no estén en src/data/empresa.ts o en la fuente pública. Lo que no
|
||||||
|
* está verificado se declara como no reproducido; no se estima.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const TITULO = "Cumplimiento NOM-002-STPS-2010";
|
const TITULO = "Cumplimiento NOM-002-STPS-2010: qué te van a pedir";
|
||||||
const DESCRIPCION =
|
const DESCRIPCION =
|
||||||
"Qué exige la NOM-002-STPS-2010 a un centro de trabajo, qué documentación se acumula y cómo se ve un expediente completo. Referencia orientativa, no dictamen técnico ni jurídico.";
|
"¿La NOM-002-STPS-2010 te aplica y qué te van a pedir en una verificación? Explicado sin tecnicismos y con la precisión normativa después: clasificación de riesgo, equipo, plan de emergencia, brigadas, simulacros y los ocho documentos del expediente.";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: TITULO,
|
title: TITULO,
|
||||||
@@ -31,43 +40,83 @@ export const metadata: Metadata = {
|
|||||||
const AVISO_ALCANCE =
|
const AVISO_ALCANCE =
|
||||||
"Esta página es una referencia orientativa construida con información pública. La normativa puede actualizarse y complementarse por estado, municipio, tipo de instalación y autoridad competente. No sustituye un dictamen técnico ni una opinión jurídica.";
|
"Esta página es una referencia orientativa construida con información pública. La normativa puede actualizarse y complementarse por estado, municipio, tipo de instalación y autoridad competente. No sustituye un dictamen técnico ni una opinión jurídica.";
|
||||||
|
|
||||||
|
const MENSAJE_INICIAL = "Necesito saber si cumplo con la NOM-002. Mi instalación es un ";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Las tres preguntas reales, en el orden en el que llegan. Cada respuesta abre
|
||||||
|
* en lenguaje llano y cierra con el dato normativo.
|
||||||
|
*/
|
||||||
|
const PREGUNTAS_REALES = [
|
||||||
|
{
|
||||||
|
id: "me-aplica",
|
||||||
|
pregunta: "¿Esto me aplica a mí?",
|
||||||
|
llano:
|
||||||
|
"Si tienes gente trabajando, sí. No hay tamaño mínimo: aplica igual a una oficina de quince personas que a una planta de trescientas.",
|
||||||
|
tecnico:
|
||||||
|
"La NOM-002-STPS-2010 rige para todo centro de trabajo en México. Lo que cambia entre una oficina y una planta no es la norma: es el nivel de riesgo de incendio con el que se clasifica el centro, y de ese nivel dependen el equipo, los sistemas y el plan que te corresponden.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "que-me-multan",
|
||||||
|
pregunta: "¿Qué me pueden multar?",
|
||||||
|
llano:
|
||||||
|
"No te vamos a dar una cifra. Quien te promete un monto exacto en una página web se lo está inventando, y con eso no se toma una decisión.",
|
||||||
|
tecnico:
|
||||||
|
"El monto de una sanción lo determina la autoridad competente según lo que encuentre en la verificación, y no lo publicamos porque no podemos sostenerlo con una fuente verificable. Lo que sí es concreto —y es lo que conviene preparar— son los ocho documentos del expediente que aparece más abajo.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "que-necesito",
|
||||||
|
pregunta: "¿Qué tengo que tener?",
|
||||||
|
llano:
|
||||||
|
"Equipo que sirva para lo que se puede quemar en cada área, un plan escrito, personal que sepa qué hacer y registro de todo lo anterior. No se cumple solo comprando extintores.",
|
||||||
|
tecnico:
|
||||||
|
"En el orden que sigue la norma: clasificación del riesgo de incendio, equipo contra incendio, sistemas fijos y medios de detección y alarma cuando el nivel lo exige, plan de atención a emergencias por escrito, brigadas con capacitación acreditada y simulacros ejecutados y documentados.",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
/** Elementos que la norma contempla, según la referencia de NORMAS. */
|
/** Elementos que la norma contempla, según la referencia de NORMAS. */
|
||||||
const ELEMENTOS = [
|
const ELEMENTOS = [
|
||||||
{
|
{
|
||||||
n: "01",
|
n: "01",
|
||||||
titulo: "Clasificación del riesgo de incendio",
|
titulo: "Clasificación del riesgo de incendio",
|
||||||
|
llano:
|
||||||
|
"Es el primer paso y define todo lo demás. Si se hace mal, todo lo que se compre encima queda mal.",
|
||||||
detalle:
|
detalle:
|
||||||
"Es el primer paso y condiciona todo lo demás. De la clasificación depende qué equipo, qué sistemas y qué plan te corresponden. Se determina con el procedimiento de la propia norma; los umbrales no se reproducen aquí porque dependen del material y de la superficie de cada centro de trabajo.",
|
"De la clasificación depende qué equipo, qué sistemas y qué plan te corresponden. Se determina con el procedimiento de la propia norma; los umbrales no se reproducen aquí porque dependen del material y de la superficie de cada centro de trabajo.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
n: "02",
|
n: "02",
|
||||||
titulo: "Equipo contra incendio",
|
titulo: "Equipo contra incendio",
|
||||||
|
llano: "Extintores que se vean, se alcancen y sirvan para lo que hay en esa área concreta.",
|
||||||
detalle:
|
detalle:
|
||||||
"Extintores disponibles, accesibles y señalizados, seleccionados según la clase de fuego presente en cada área. El equipo correcto en el lugar equivocado no cumple.",
|
"Disponibles, accesibles y señalizados, seleccionados según la clase de fuego presente en cada zona. El equipo correcto en el lugar equivocado no cumple, y el conteo total no sustituye a la distancia de recorrido.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
n: "03",
|
n: "03",
|
||||||
titulo: "Sistemas fijos y de alarma",
|
titulo: "Sistemas fijos y de alarma",
|
||||||
|
llano: "Si tu riesgo es alto, con equipo portátil no alcanza.",
|
||||||
detalle:
|
detalle:
|
||||||
"Según el nivel de riesgo, el centro de trabajo puede requerir sistemas fijos de supresión y medios de detección y alarma, no solo equipo portátil.",
|
"Según el nivel de riesgo, el centro de trabajo puede requerir sistemas fijos de supresión y medios de detección y alarma, además del equipo portátil. Es el punto donde la conversación deja de ser una compra y pasa a ser un proyecto.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
n: "04",
|
n: "04",
|
||||||
titulo: "Plan de atención a emergencias",
|
titulo: "Plan de atención a emergencias",
|
||||||
|
llano: "Un documento que diga quién hace qué el día del incidente. Por escrito, no de palabra.",
|
||||||
detalle:
|
detalle:
|
||||||
"Documento por escrito. Es lo primero que se pide en una verificación y lo que más frecuentemente no existe o está desactualizado.",
|
"Es lo primero que se pide en una verificación y lo que con más frecuencia no existe o quedó desactualizado tras un cambio de instalación, de proceso o de personal.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
n: "05",
|
n: "05",
|
||||||
titulo: "Brigadas y capacitación",
|
titulo: "Brigadas y capacitación",
|
||||||
|
llano: "Personas con nombre y apellido que sepan operar el equipo, y un papel que lo compruebe.",
|
||||||
detalle:
|
detalle:
|
||||||
"Personal designado, entrenado y con constancia. La capacitación sin registro documental no es demostrable ante una autoridad.",
|
"Personal designado, entrenado y con constancia por persona y fecha. La capacitación sin registro documental no es demostrable ante una autoridad, aunque haya ocurrido.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
n: "06",
|
n: "06",
|
||||||
titulo: "Simulacros",
|
titulo: "Simulacros",
|
||||||
|
llano: "Practicarlo antes, y dejar constancia de que se practicó.",
|
||||||
detalle:
|
detalle:
|
||||||
"Ejecutados y documentados. El registro del simulacro es evidencia tanto del plan como de la capacitación.",
|
"Ejecutados y documentados. El registro del simulacro es evidencia doble, del plan y de la capacitación, y es donde se detectan los huecos que un plano no muestra.",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -114,171 +163,215 @@ const EXPEDIENTE = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/** Los ocho escenarios más frecuentes en una conversación de cumplimiento. */
|
||||||
|
const SLUGS_CUMPLIMIENTO = [
|
||||||
|
"industria",
|
||||||
|
"gasolineras",
|
||||||
|
"bodegas",
|
||||||
|
"cocinas-hoteles",
|
||||||
|
"oficinas",
|
||||||
|
"auditorios-afluencia",
|
||||||
|
"flotas-maquinaria",
|
||||||
|
"minas",
|
||||||
|
];
|
||||||
|
|
||||||
|
const ESCENARIOS_CUMPLIMIENTO = ESCENARIOS.filter((e) => SLUGS_CUMPLIMIENTO.includes(e.slug));
|
||||||
|
|
||||||
|
function hrefEscenario(escenario: Escenario): string {
|
||||||
|
if (escenario.industria) return `/soluciones/${escenario.industria}`;
|
||||||
|
const categoria = escenario.categorias[0];
|
||||||
|
return categoria ? `/catalogo/${categoria}` : "/catalogo";
|
||||||
|
}
|
||||||
|
|
||||||
export default function PaginaCumplimiento() {
|
export default function PaginaCumplimiento() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* ---------- encabezado con la advertencia de alcance ---------- */}
|
<HeroeLanding
|
||||||
<section className="grid-bg bg-ink pt-12 pb-14 sm:pt-16 sm:pb-20">
|
antes={<Migas actual="Cumplimiento NOM-002" />}
|
||||||
<div className="shell">
|
eyebrow="Cumplimiento NOM-002-STPS-2010"
|
||||||
<nav aria-label="Ruta de navegación" className="mb-8">
|
titulo="¿La NOM-002 te aplica? Sí. Esto es lo que te van a pedir"
|
||||||
<ol className="flex flex-wrap items-center gap-2 font-mono text-[0.625rem] uppercase tracking-[0.14em] text-faint">
|
entrada="Primero en español llano y después con la precisión normativa. Si solo quieres saber por dónde empezar, empieza por la revisión: te decimos qué te falta antes de venderte nada."
|
||||||
<li>
|
foto={FOTOS.normas}
|
||||||
<Link href="/" className="hover:text-bright">
|
enfoque="62% 42%"
|
||||||
Inicio
|
altura="alto"
|
||||||
</Link>
|
prioridad
|
||||||
</li>
|
>
|
||||||
<li aria-hidden="true">/</li>
|
<div className="flex flex-col gap-4 sm:flex-row sm:items-center">
|
||||||
<li className="text-bright">Cumplimiento</li>
|
<Link href="#validar" className="btn btn-signal w-full sm:w-auto">
|
||||||
</ol>
|
Revisar mi cumplimiento
|
||||||
</nav>
|
</Link>
|
||||||
|
<p className="font-mono text-[0.625rem] uppercase leading-relaxed tracking-[0.1em] text-faint">
|
||||||
|
Cuatro campos · Sin costo · Sin compromiso de compra
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</HeroeLanding>
|
||||||
|
|
||||||
<div className="max-w-3xl anim-rise">
|
{/* ---------- las tres preguntas reales ---------- */}
|
||||||
<p className="eyebrow mb-5">Referencia normativa</p>
|
<section className="grid-bg-lite bg-paper py-14 sm:py-20" aria-labelledby="preguntas-titulo">
|
||||||
<h1 className="font-display text-[2rem] font-black uppercase leading-[1.03] tracking-tight sm:text-[2.75rem] lg:text-5xl">
|
<div className="shell">
|
||||||
Cumplimiento NOM-002: qué te van a pedir y cómo se ve un expediente completo
|
<Revelar>
|
||||||
</h1>
|
<p className="eyebrow mb-4">Empecemos por lo básico</p>
|
||||||
<p className="mt-6 text-base leading-relaxed text-muted sm:text-lg">
|
<h2
|
||||||
La norma no se cumple comprando extintores. Se cumple demostrando, por escrito, que el
|
id="preguntas-titulo"
|
||||||
riesgo está clasificado, el equipo corresponde a ese riesgo y el personal sabe qué
|
className="max-w-2xl font-display text-2xl font-extrabold text-deep sm:text-3xl"
|
||||||
hacer. Esta página explica esa lógica y qué documentos la sostienen.
|
>
|
||||||
|
Las tres preguntas que hace todo el mundo, respondidas sin rodeos
|
||||||
|
</h2>
|
||||||
|
<p className="mt-4 max-w-2xl text-sm leading-relaxed text-deep-muted sm:text-base">
|
||||||
|
En cada respuesta, lo primero es lo que necesitas para decidir. Lo segundo es el
|
||||||
|
detalle normativo, para quien tiene que sostenerlo ante una auditoría.
|
||||||
</p>
|
</p>
|
||||||
|
</Revelar>
|
||||||
|
|
||||||
|
<div className="mt-10 grid gap-px bg-rule-lite lg:grid-cols-3">
|
||||||
|
{PREGUNTAS_REALES.map((item) => (
|
||||||
|
<article key={item.id} id={item.id} className="bg-white p-6 sm:p-7">
|
||||||
|
<h3 className="font-display text-lg font-extrabold leading-tight text-deep sm:text-xl">
|
||||||
|
{item.pregunta}
|
||||||
|
</h3>
|
||||||
|
<p className="mt-4 text-base leading-relaxed text-deep">{item.llano}</p>
|
||||||
|
<hr className="hair-lite my-5" />
|
||||||
|
<p className="font-mono text-[0.625rem] uppercase tracking-[0.14em] text-deep-muted">
|
||||||
|
El dato normativo
|
||||||
|
</p>
|
||||||
|
<p className="mt-2.5 text-sm leading-relaxed text-deep-muted">{item.tecnico}</p>
|
||||||
|
</article>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
role="note"
|
role="note"
|
||||||
aria-label="Alcance de esta página"
|
aria-label="Alcance de esta página"
|
||||||
className="mt-10 max-w-3xl rounded-[6px] border border-rule border-l-2 border-l-ember bg-ink-2 p-5 sm:p-6"
|
className="mt-10 max-w-3xl rounded-[6px] border border-rule-lite border-l-2 border-l-ember bg-white p-5 sm:p-6"
|
||||||
>
|
>
|
||||||
<p className="font-mono text-[0.6875rem] uppercase tracking-[0.16em] text-ember">
|
<p className="font-mono text-[0.6875rem] uppercase tracking-[0.16em] text-deep-muted">
|
||||||
Alcance de esta página
|
Alcance de esta página
|
||||||
</p>
|
</p>
|
||||||
<p className="mt-3 text-sm leading-relaxed text-muted">{AVISO_ALCANCE}</p>
|
<p className="mt-3 text-sm leading-relaxed text-deep-muted">{AVISO_ALCANCE}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* ---------- a quién aplica ---------- */}
|
{/* ---------- identifica tu caso con la iconografía de escenarios ---------- */}
|
||||||
<section className="grid-bg-lite bg-paper py-14 sm:py-20" aria-labelledby="aplica-titulo">
|
<section
|
||||||
|
className="border-t border-rule bg-ink py-14 sm:py-20"
|
||||||
|
aria-labelledby="escenarios-titulo"
|
||||||
|
>
|
||||||
<div className="shell">
|
<div className="shell">
|
||||||
<div className="grid gap-10 lg:grid-cols-[1fr_1.1fr] lg:gap-16">
|
<Revelar>
|
||||||
<div>
|
<p className="eyebrow mb-4">Encuentra tu caso</p>
|
||||||
<Revelar>
|
<h2
|
||||||
<p className="eyebrow mb-4">A quién aplica</p>
|
id="escenarios-titulo"
|
||||||
<h2 id="aplica-titulo" className="font-display text-2xl font-extrabold text-deep sm:text-3xl">
|
className="max-w-2xl font-display text-2xl font-extrabold sm:text-3xl"
|
||||||
A todo centro de trabajo, sin mínimo de tamaño
|
>
|
||||||
</h2>
|
Busca el lugar que se parece al tuyo
|
||||||
<p className="mt-4 text-sm leading-relaxed text-deep-muted sm:text-base">
|
</h2>
|
||||||
{NORMAS[0].aplica}
|
<p className="mt-4 max-w-2xl text-sm leading-relaxed text-muted sm:text-base">
|
||||||
</p>
|
No hace falta que sepas qué clase de fuego tienes. Si administras una gasolinera, un
|
||||||
<p className="mt-4 text-sm leading-relaxed text-deep-muted">
|
hotel o una bodega, eso ya define el perfil de riesgo y, con él, las obligaciones que
|
||||||
Es la razón por la que una oficina de veinte personas y una planta petroquímica
|
se te aplican con más peso.
|
||||||
están bajo la misma norma con obligaciones muy distintas: lo que cambia no es la
|
</p>
|
||||||
norma, es la clasificación del riesgo.
|
</Revelar>
|
||||||
</p>
|
|
||||||
</Revelar>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="panel-lite p-6 sm:p-8">
|
<ul className="mt-10 grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||||
<h3 className="font-display text-lg font-extrabold text-deep sm:text-xl">
|
{ESCENARIOS_CUMPLIMIENTO.map((escenario) => (
|
||||||
Dos niveles, dos conversaciones distintas
|
<li key={escenario.slug} className="grid">
|
||||||
</h3>
|
<TarjetaEscenario
|
||||||
|
escenario={escenario}
|
||||||
|
href={hrefEscenario(escenario)}
|
||||||
|
conRiesgo={false}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
|
||||||
<div className="mt-6 space-y-6">
|
<p className="mt-8 max-w-3xl text-sm leading-relaxed text-muted">
|
||||||
<div className="border-l-2 border-l-rule-lite pl-4">
|
Si el tuyo no está en la lista, escríbelo tal cual en la revisión: lo clasificamos
|
||||||
<p className="font-mono text-[0.625rem] uppercase tracking-[0.14em] text-deep-muted">
|
nosotros.{" "}
|
||||||
Riesgo ordinario
|
<Link href="#validar" className="link">
|
||||||
</p>
|
Ir a la revisión
|
||||||
<p className="mt-2 text-sm leading-relaxed text-deep-muted">
|
</Link>
|
||||||
El foco está en tener el equipo correcto, señalizado y revisado, con un plan por
|
.
|
||||||
escrito y personal capacitado. La mayoría de las PyMEs y oficinas queda aquí y
|
</p>
|
||||||
el error caro es comprar de más por miedo.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="border-l-2 border-l-signal pl-4">
|
|
||||||
<p className="font-mono text-[0.625rem] uppercase tracking-[0.14em] text-signal">
|
|
||||||
Riesgo alto
|
|
||||||
</p>
|
|
||||||
<p className="mt-2 text-sm leading-relaxed text-deep-muted">
|
|
||||||
La norma establece requerimientos mayores para los centros clasificados como de
|
|
||||||
riesgo alto de incendio: la conversación pasa de equipo portátil a sistemas
|
|
||||||
fijos, detección, brigadas formales y evidencia sostenida en el tiempo.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr className="hair-lite my-6" />
|
|
||||||
|
|
||||||
<p className="text-xs leading-relaxed text-deep-muted">
|
|
||||||
No reproducimos aquí los umbrales de clasificación: dependen del material y de la
|
|
||||||
superficie de cada centro de trabajo, y determinarlos mal invalida todo lo que se
|
|
||||||
construya encima. Ese es el primer punto de un diagnóstico.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* ---------- qué contempla la norma ---------- */}
|
{/* ---------- qué contempla la norma ---------- */}
|
||||||
<section className="border-t border-rule bg-ink py-14 sm:py-20" aria-labelledby="elementos-titulo">
|
<section
|
||||||
|
className="grid-bg-lite border-t border-rule bg-paper py-14 sm:py-20"
|
||||||
|
aria-labelledby="elementos-titulo"
|
||||||
|
>
|
||||||
<div className="shell">
|
<div className="shell">
|
||||||
<Revelar>
|
<Revelar>
|
||||||
<p className="eyebrow mb-4">Qué contempla</p>
|
<p className="eyebrow mb-4">Qué contempla la norma</p>
|
||||||
<h2 id="elementos-titulo" className="font-display text-2xl font-extrabold sm:text-3xl">
|
<h2
|
||||||
|
id="elementos-titulo"
|
||||||
|
className="max-w-2xl font-display text-2xl font-extrabold text-deep sm:text-3xl"
|
||||||
|
>
|
||||||
Seis frentes, y ninguno se cierra con una compra
|
Seis frentes, y ninguno se cierra con una compra
|
||||||
</h2>
|
</h2>
|
||||||
<p className="mt-3 max-w-2xl text-sm leading-relaxed text-muted sm:text-base">
|
<p className="mt-4 max-w-2xl text-sm leading-relaxed text-deep-muted sm:text-base">
|
||||||
Cada uno genera un documento distinto. Un expediente incompleto suele fallar en los
|
Cada frente genera un documento distinto. Los expedientes incompletos casi siempre
|
||||||
frentes que no se compran: el plan, la capacitación y el registro.
|
fallan en los tres que no se compran: el plan, la capacitación y el registro.
|
||||||
</p>
|
</p>
|
||||||
</Revelar>
|
</Revelar>
|
||||||
|
|
||||||
<ol className="mt-10 grid gap-px bg-rule sm:grid-cols-2 lg:grid-cols-3">
|
<ol className="mt-10 grid gap-px bg-rule-lite sm:grid-cols-2 lg:grid-cols-3">
|
||||||
{ELEMENTOS.map((elemento) => (
|
{ELEMENTOS.map((elemento) => (
|
||||||
<li key={elemento.n} className="bg-ink-2 p-6">
|
<li key={elemento.n} className="bg-white p-6 sm:p-7">
|
||||||
<p className="font-mono text-[0.625rem] font-semibold tracking-[0.1em] text-signal">
|
<p className="font-mono text-[0.625rem] font-semibold tracking-[0.1em] text-signal">
|
||||||
{elemento.n}
|
{elemento.n}
|
||||||
</p>
|
</p>
|
||||||
<h3 className="mt-3 font-display text-base font-extrabold leading-tight text-bright">
|
<h3 className="mt-3 font-display text-base font-extrabold leading-tight text-deep">
|
||||||
{elemento.titulo}
|
{elemento.titulo}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="mt-3 text-sm leading-relaxed text-muted">{elemento.detalle}</p>
|
<p className="mt-3 text-sm leading-relaxed text-deep">{elemento.llano}</p>
|
||||||
|
<p className="mt-3 text-sm leading-relaxed text-deep-muted">{elemento.detalle}</p>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ol>
|
</ol>
|
||||||
|
|
||||||
<div className="mt-10 rounded-[6px] border border-rule bg-ink-2 p-5 sm:p-6">
|
<div className="mt-10 rounded-[6px] border border-rule-lite bg-white p-5 sm:p-6">
|
||||||
<p className="font-mono text-[0.6875rem] uppercase tracking-[0.16em] text-faint">
|
<p className="font-mono text-[0.6875rem] uppercase tracking-[0.16em] text-deep-muted">
|
||||||
Mantenimiento de extintores
|
La obligación que más se pierde de vista
|
||||||
</p>
|
</p>
|
||||||
<p className="mt-3 max-w-3xl text-sm leading-relaxed text-muted">
|
<h3 className="mt-3 font-display text-lg font-extrabold text-deep">
|
||||||
{NORMAS[1].aplica} Es la obligación recurrente que más seguido se pierde: no por
|
Los extintores se revisan cada mes
|
||||||
desconocerla, sino porque nadie quedó a cargo de la bitácora.
|
</h3>
|
||||||
|
<p className="mt-3 max-w-3xl text-sm leading-relaxed text-deep-muted">
|
||||||
|
{NORMAS[1].aplica} Casi nunca se pierde por desconocerla: se pierde porque nadie
|
||||||
|
quedó a cargo de la bitácora.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* ---------- el expediente ---------- */}
|
{/* ---------- el expediente ---------- */}
|
||||||
<section className="grid-bg-lite bg-paper py-14 sm:py-20" aria-labelledby="expediente-titulo">
|
<section
|
||||||
|
className="border-t border-rule bg-ink py-14 sm:py-20"
|
||||||
|
aria-labelledby="expediente-titulo"
|
||||||
|
>
|
||||||
<div className="shell-wide">
|
<div className="shell-wide">
|
||||||
<Revelar>
|
<Revelar>
|
||||||
<p className="eyebrow mb-4">Expediente</p>
|
<p className="eyebrow mb-4">Expediente</p>
|
||||||
<h2 id="expediente-titulo" className="font-display text-2xl font-extrabold text-deep sm:text-3xl">
|
<h2
|
||||||
Cómo se ve un expediente que aguanta una verificación
|
id="expediente-titulo"
|
||||||
|
className="max-w-2xl font-display text-2xl font-extrabold sm:text-3xl"
|
||||||
|
>
|
||||||
|
Los ocho documentos que piden en una verificación
|
||||||
</h2>
|
</h2>
|
||||||
<p className="mt-3 max-w-2xl text-sm leading-relaxed text-deep-muted sm:text-base">
|
<p className="mt-4 max-w-2xl text-sm leading-relaxed text-muted sm:text-base">
|
||||||
Ocho documentos. Ninguno es opcional cuando alguien pregunta, y cada uno prueba algo
|
Esta es la lista con la que conviene compararse antes de que alguien llegue a
|
||||||
distinto.
|
pedirla. Ninguno es opcional cuando se pregunta, y cada uno prueba algo distinto.
|
||||||
</p>
|
</p>
|
||||||
</Revelar>
|
</Revelar>
|
||||||
|
|
||||||
<p className="mt-6 font-mono text-[0.6875rem] uppercase tracking-[0.14em] text-deep-muted sm:hidden">
|
<p className="mt-6 font-mono text-[0.6875rem] uppercase tracking-[0.14em] text-faint sm:hidden">
|
||||||
Desliza la tabla para ver todas las columnas
|
Desliza la tabla para ver todas las columnas
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="mt-4 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 bg-ink-2 [-webkit-overflow-scrolling:touch]"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
role="region"
|
role="region"
|
||||||
aria-label="Documentos del expediente, tabla desplazable en horizontal"
|
aria-label="Documentos del expediente, tabla desplazable en horizontal"
|
||||||
@@ -289,12 +382,12 @@ export default function PaginaCumplimiento() {
|
|||||||
lo solicita.
|
lo solicita.
|
||||||
</caption>
|
</caption>
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="border-b border-rule-lite bg-paper-2">
|
<tr className="border-b border-rule bg-ink-3">
|
||||||
{["Documento", "Qué prueba", "Quién lo pide"].map((c) => (
|
{["Documento", "Qué prueba", "Quién lo pide"].map((c) => (
|
||||||
<th
|
<th
|
||||||
key={c}
|
key={c}
|
||||||
scope="col"
|
scope="col"
|
||||||
className="whitespace-nowrap px-4 py-3 font-mono text-[0.625rem] font-medium uppercase tracking-[0.14em] text-deep-muted"
|
className="whitespace-nowrap px-4 py-3 font-mono text-[0.625rem] font-medium uppercase tracking-[0.14em] text-faint"
|
||||||
>
|
>
|
||||||
{c}
|
{c}
|
||||||
</th>
|
</th>
|
||||||
@@ -303,16 +396,16 @@ export default function PaginaCumplimiento() {
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{EXPEDIENTE.map((fila) => (
|
{EXPEDIENTE.map((fila) => (
|
||||||
<tr key={fila.documento} className="border-b border-rule-lite last:border-b-0">
|
<tr key={fila.documento} className="border-b border-rule last:border-b-0">
|
||||||
<th scope="row" className="px-4 py-4 text-left align-top">
|
<th scope="row" className="px-4 py-4 text-left align-top">
|
||||||
<span className="font-sans text-sm font-semibold text-deep">
|
<span className="font-sans text-sm font-semibold text-bright">
|
||||||
{fila.documento}
|
{fila.documento}
|
||||||
</span>
|
</span>
|
||||||
</th>
|
</th>
|
||||||
<td className="max-w-[24rem] px-4 py-4 align-top text-sm leading-relaxed text-deep-muted">
|
<td className="max-w-[24rem] px-4 py-4 align-top text-sm leading-relaxed text-muted">
|
||||||
{fila.quePrueba}
|
{fila.quePrueba}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-4 align-top font-mono text-xs leading-relaxed text-deep-muted">
|
<td className="px-4 py-4 align-top font-mono text-xs leading-relaxed text-muted">
|
||||||
{fila.quienLoPide}
|
{fila.quienLoPide}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -321,22 +414,85 @@ export default function PaginaCumplimiento() {
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="mt-6 max-w-3xl text-xs leading-relaxed text-deep-muted">
|
<div className="mt-8 flex flex-col gap-4 sm:flex-row sm:items-center">
|
||||||
En una compra con nosotros, los documentos que dependen del equipo se entregan por
|
<Link href="#validar" className="btn btn-signal w-full sm:w-auto">
|
||||||
número de modelo.{" "}
|
Ver qué me falta del expediente
|
||||||
<Link href="/evidencia" className="link">
|
|
||||||
Ver el índice de documentación por categoría
|
|
||||||
</Link>
|
</Link>
|
||||||
.
|
<p className="max-w-lg text-xs leading-relaxed text-faint">
|
||||||
</p>
|
En una compra con nosotros, los documentos que dependen del equipo se entregan por
|
||||||
|
número de modelo.{" "}
|
||||||
|
<Link href="/evidencia" className="link">
|
||||||
|
Ver el índice de documentación por categoría
|
||||||
|
</Link>
|
||||||
|
.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* ---------- referencias ---------- */}
|
{/* ---------- lo que cambia por operación ---------- */}
|
||||||
<section className="border-t border-rule bg-ink py-14 sm:py-16" aria-labelledby="referencias-titulo">
|
<section
|
||||||
|
className="grid-bg-lite border-t border-rule bg-paper py-14 sm:py-20"
|
||||||
|
aria-labelledby="segmentos-titulo"
|
||||||
|
>
|
||||||
<div className="shell">
|
<div className="shell">
|
||||||
<Revelar>
|
<Revelar>
|
||||||
<p className="eyebrow mb-4">Referencias</p>
|
<p className="eyebrow mb-4">Por operación</p>
|
||||||
|
<h2
|
||||||
|
id="segmentos-titulo"
|
||||||
|
className="max-w-2xl font-display text-2xl font-extrabold text-deep sm:text-3xl"
|
||||||
|
>
|
||||||
|
Lo que además se te pide según dónde trabajas
|
||||||
|
</h2>
|
||||||
|
<p className="mt-4 max-w-2xl text-sm leading-relaxed text-deep-muted sm:text-base">
|
||||||
|
La NOM-002 es el piso común. Encima de ella, cada sector acumula requisitos propios
|
||||||
|
de su autoridad, de su aseguradora o de su cliente corporativo.
|
||||||
|
</p>
|
||||||
|
</Revelar>
|
||||||
|
|
||||||
|
<ul className="mt-10 grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{INDUSTRIAS.map((industria) => (
|
||||||
|
<li key={industria.slug} className="flex">
|
||||||
|
<Link
|
||||||
|
href={`/soluciones/${industria.slug}`}
|
||||||
|
className="group flex h-full w-full flex-col rounded-[6px] border border-rule-lite bg-white p-5 transition-colors duration-200 hover:border-signal [-webkit-tap-highlight-color:transparent]"
|
||||||
|
>
|
||||||
|
<h3 className="font-display text-base font-extrabold leading-tight text-deep">
|
||||||
|
{industria.nombre}
|
||||||
|
</h3>
|
||||||
|
<p className="mt-2 text-sm leading-relaxed text-deep">{industria.dolor}</p>
|
||||||
|
<ul className="mt-4 space-y-2">
|
||||||
|
{industria.cumplimiento.map((obligacion) => (
|
||||||
|
<li
|
||||||
|
key={obligacion}
|
||||||
|
className="flex gap-2.5 text-xs leading-relaxed text-deep-muted"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
className="mt-1.5 h-1 w-1 shrink-0 rounded-full bg-signal"
|
||||||
|
/>
|
||||||
|
{obligacion}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<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 el segmento →
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* ---------- referencias normativas ---------- */}
|
||||||
|
<section
|
||||||
|
className="border-t border-rule bg-ink-2 py-14 sm:py-16"
|
||||||
|
aria-labelledby="referencias-titulo"
|
||||||
|
>
|
||||||
|
<div className="shell">
|
||||||
|
<Revelar>
|
||||||
|
<p className="eyebrow eyebrow-muted mb-4">Referencias</p>
|
||||||
<h2 id="referencias-titulo" className="font-display text-xl font-extrabold sm:text-2xl">
|
<h2 id="referencias-titulo" className="font-display text-xl font-extrabold sm:text-2xl">
|
||||||
Normas citadas en esta página
|
Normas citadas en esta página
|
||||||
</h2>
|
</h2>
|
||||||
@@ -371,65 +527,48 @@ export default function PaginaCumplimiento() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* ---------- por industria ---------- */}
|
{/* ---------- conversión ---------- */}
|
||||||
<section className="border-t border-rule bg-ink-2 py-14 sm:py-16" aria-labelledby="segmentos-titulo">
|
<section
|
||||||
<div className="shell">
|
id="validar"
|
||||||
<Revelar>
|
className="grid-bg-lite border-t border-rule bg-paper py-14 sm:py-20"
|
||||||
<p className="eyebrow eyebrow-muted mb-4">Por operación</p>
|
aria-labelledby="validar-titulo"
|
||||||
<h2 id="segmentos-titulo" className="font-display text-xl font-extrabold sm:text-2xl">
|
>
|
||||||
Lo que cambia según dónde trabajas
|
|
||||||
</h2>
|
|
||||||
</Revelar>
|
|
||||||
<ul className="mt-8 grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
||||||
{INDUSTRIAS.map((industria) => (
|
|
||||||
<li key={industria.slug}>
|
|
||||||
<Link
|
|
||||||
href={`/soluciones/${industria.slug}`}
|
|
||||||
className="group flex h-full flex-col rounded-[6px] border border-rule bg-ink p-5 transition-colors duration-200 hover:border-rule-2 [-webkit-tap-highlight-color:transparent]"
|
|
||||||
>
|
|
||||||
<h3 className="font-display text-base font-extrabold leading-tight text-bright">
|
|
||||||
{industria.nombre}
|
|
||||||
</h3>
|
|
||||||
<ul className="mt-3 space-y-2">
|
|
||||||
{industria.cumplimiento.map((obligacion) => (
|
|
||||||
<li key={obligacion} className="flex gap-2.5 text-xs leading-relaxed text-muted">
|
|
||||||
<span
|
|
||||||
aria-hidden="true"
|
|
||||||
className="mt-1.5 h-1 w-1 shrink-0 rounded-full bg-signal"
|
|
||||||
/>
|
|
||||||
{obligacion}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
<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 el segmento →
|
|
||||||
</span>
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* ---------- formulario y cierre con la advertencia ---------- */}
|
|
||||||
<section id="validar" className="grid-bg-lite border-t border-rule bg-paper py-14 sm:py-20">
|
|
||||||
<div className="shell">
|
<div className="shell">
|
||||||
<div className="grid gap-10 lg:grid-cols-[1fr_1.05fr] lg:items-start lg:gap-16">
|
<div className="grid gap-10 lg:grid-cols-[1fr_1.05fr] lg:items-start lg:gap-16">
|
||||||
<div>
|
<div>
|
||||||
<Revelar>
|
<Revelar>
|
||||||
<p className="eyebrow mb-4">Siguiente paso</p>
|
<p className="eyebrow mb-4">Siguiente paso</p>
|
||||||
<h2 className="font-display text-2xl font-extrabold text-deep sm:text-3xl">
|
<h2
|
||||||
Validar cumplimiento NOM-002
|
id="validar-titulo"
|
||||||
|
className="font-display text-2xl font-extrabold text-deep sm:text-3xl"
|
||||||
|
>
|
||||||
|
Te decimos qué te falta antes de venderte nada
|
||||||
</h2>
|
</h2>
|
||||||
<p className="mt-4 text-sm leading-relaxed text-deep-muted sm:text-base">
|
<p className="mt-4 text-sm leading-relaxed text-deep-muted sm:text-base">
|
||||||
Revisamos qué tienes, qué te falta y en qué orden conviene resolverlo. Si algo de tu
|
Revisamos qué tienes, qué te falta y en qué orden conviene resolverlo. Si tu
|
||||||
situación requiere un dictamen formal, te lo decimos en lugar de venderte equipo.
|
situación requiere un dictamen formal, te lo decimos en lugar de venderte equipo.
|
||||||
</p>
|
</p>
|
||||||
</Revelar>
|
</Revelar>
|
||||||
|
|
||||||
|
<ul className="mt-8 space-y-3.5">
|
||||||
|
{[
|
||||||
|
"No necesitas saber tu nivel de riesgo para escribirnos: eso es justo lo que revisamos.",
|
||||||
|
"Si ya te levantaron una observación, mándala tal cual y respondemos sobre ella.",
|
||||||
|
"Si lo que te falta no es equipo sino documentación, lo decimos así.",
|
||||||
|
].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
|
<div
|
||||||
role="note"
|
role="note"
|
||||||
aria-label="Alcance de esta página"
|
aria-label="Recordatorio de alcance"
|
||||||
className="mt-8 rounded-[6px] border border-rule-lite border-l-2 border-l-ember bg-white p-5"
|
className="mt-8 rounded-[6px] border border-rule-lite border-l-2 border-l-ember bg-white p-5"
|
||||||
>
|
>
|
||||||
<p className="font-mono text-[0.6875rem] uppercase tracking-[0.16em] text-deep-muted">
|
<p className="font-mono text-[0.6875rem] uppercase tracking-[0.16em] text-deep-muted">
|
||||||
@@ -440,10 +579,13 @@ export default function PaginaCumplimiento() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<FormularioContacto
|
<FormularioContacto
|
||||||
|
id="formulario-cumplimiento"
|
||||||
canal="cumplimiento"
|
canal="cumplimiento"
|
||||||
variante="claro"
|
variante="claro"
|
||||||
titulo="Validar mi cumplimiento"
|
titulo="Revisar mi cumplimiento NOM-002"
|
||||||
descripcion="Dinos qué tipo de instalación es y qué te pidieron. Respondemos con el hueco documental que vemos y por dónde empezar."
|
descripcion="Completa la frase del mensaje con tu tipo de instalación y envía. Respondemos con el hueco documental que vemos y por dónde empezar."
|
||||||
|
mensajeInicial={MENSAJE_INICIAL}
|
||||||
|
textoBoton="Solicitar revisión"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+193
-136
@@ -1,6 +1,8 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { FormularioContacto } from "@/components/formulario/FormularioContacto";
|
import { FormularioContacto } from "@/components/formulario/FormularioContacto";
|
||||||
|
import { Migas } from "@/components/site/contenido/Migas";
|
||||||
|
import { FOTOS, HeroeLanding } from "@/components/site/HeroeLanding";
|
||||||
import { Revelar } from "@/components/site/Revelar";
|
import { Revelar } from "@/components/site/Revelar";
|
||||||
import { CATEGORIAS, PRODUCTOS, productosDeCategoria } from "@/data/catalogo";
|
import { CATEGORIAS, PRODUCTOS, productosDeCategoria } from "@/data/catalogo";
|
||||||
import { EMPRESA, NORMAS, SITIO_URL } from "@/data/empresa";
|
import { EMPRESA, NORMAS, SITIO_URL } from "@/data/empresa";
|
||||||
@@ -9,15 +11,18 @@ import { EMPRESA, NORMAS, SITIO_URL } from "@/data/empresa";
|
|||||||
* /evidencia — el centro de evidencia de la recomendación 10.3 del estudio.
|
* /evidencia — el centro de evidencia de la recomendación 10.3 del estudio.
|
||||||
*
|
*
|
||||||
* Regla que gobierna esta página: en la extracción pública del sitio de FSPM no
|
* Regla que gobierna esta página: en la extracción pública del sitio de FSPM no
|
||||||
* apareció un índice verificable de documentos por modelo. Así que aquí no hay
|
* apareció un índice verificable de documentos por número de modelo. Así que
|
||||||
* un solo botón de descarga. Un botón falso en una demo destruye la
|
* aquí no hay un solo botón de descarga. Un botón falso en una demo destruye la
|
||||||
* credibilidad de todo lo demás; decir la verdad sobre el estado del acervo es
|
* credibilidad de todo lo demás; decir la verdad sobre el estado del acervo es
|
||||||
* precisamente el argumento comercial.
|
* precisamente el argumento comercial.
|
||||||
|
*
|
||||||
|
* Cada bloque abre en lenguaje llano —qué papel es y para qué te sirve— y
|
||||||
|
* cierra con la precisión que necesita quien va a defender el expediente.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const TITULO = "Centro de evidencia técnica";
|
const TITULO = "Documentación técnica: qué papeles debe traer tu equipo";
|
||||||
const DESCRIPCION =
|
const DESCRIPCION =
|
||||||
"Qué documentación corresponde a cada compra de equipo contra incendio, qué aplica por categoría de producto y qué afirmaciones documentamos por número de modelo antes de cotizar.";
|
"Qué documentación corresponde a cada compra de equipo contra incendio: ficha técnica, hoja de seguridad, certificado, garantía, manual, bitácora y constancias. Qué aplica por categoría y qué documentamos por número de modelo antes de cotizar.";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: TITULO,
|
title: TITULO,
|
||||||
@@ -30,6 +35,9 @@ export const metadata: Metadata = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const MENSAJE_INICIAL =
|
||||||
|
"Necesito la documentación técnica de un equipo. Lo que me están pidiendo es ";
|
||||||
|
|
||||||
type DocumentoId =
|
type DocumentoId =
|
||||||
| "ficha"
|
| "ficha"
|
||||||
| "sds"
|
| "sds"
|
||||||
@@ -40,18 +48,26 @@ type DocumentoId =
|
|||||||
| "instalacion"
|
| "instalacion"
|
||||||
| "capacitacion";
|
| "capacitacion";
|
||||||
|
|
||||||
const DOCUMENTOS: Array<{ id: DocumentoId; nombre: string; corto: string; queEs: string }> = [
|
const DOCUMENTOS: Array<{
|
||||||
|
id: DocumentoId;
|
||||||
|
nombre: string;
|
||||||
|
corto: string;
|
||||||
|
llano: string;
|
||||||
|
queEs: string;
|
||||||
|
}> = [
|
||||||
{
|
{
|
||||||
id: "ficha",
|
id: "ficha",
|
||||||
nombre: "Ficha técnica del modelo",
|
nombre: "Ficha técnica del modelo",
|
||||||
corto: "Ficha técnica",
|
corto: "Ficha técnica",
|
||||||
|
llano: "El papel que dice qué es y qué hace el equipo que compraste.",
|
||||||
queEs:
|
queEs:
|
||||||
"El documento que sostiene cada número que aparece en una página de producto: capacidad, agente, clases de fuego con sus condiciones y alcance con las condiciones de la prueba. Si un dato no está en la ficha, no debería estar en la propuesta.",
|
"Sostiene cada número que aparece en una página de producto: capacidad, agente, clases de fuego con sus condiciones y alcance con las condiciones de la prueba. Si un dato no está en la ficha, no debería estar en la propuesta.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "sds",
|
id: "sds",
|
||||||
nombre: "Hoja de datos de seguridad del agente",
|
nombre: "Hoja de datos de seguridad del agente",
|
||||||
corto: "Hoja de seguridad",
|
corto: "Hoja de seguridad",
|
||||||
|
llano: "Qué pasa si alguien lo respira, lo toca o lo derrama.",
|
||||||
queEs:
|
queEs:
|
||||||
"Manejo, almacenamiento, incompatibilidades y respuesta ante exposición. Es el documento que pide seguridad industrial y el área ambiental, y el que permite responder si el agente puede estar donde va a estar.",
|
"Manejo, almacenamiento, incompatibilidades y respuesta ante exposición. Es el documento que pide seguridad industrial y el área ambiental, y el que permite responder si el agente puede estar donde va a estar.",
|
||||||
},
|
},
|
||||||
@@ -59,6 +75,7 @@ const DOCUMENTOS: Array<{ id: DocumentoId; nombre: string; corto: string; queEs:
|
|||||||
id: "certificado",
|
id: "certificado",
|
||||||
nombre: "Certificado o listado por modelo",
|
nombre: "Certificado o listado por modelo",
|
||||||
corto: "Certificado",
|
corto: "Certificado",
|
||||||
|
llano: "La prueba de que un laboratorio externo lo revisó, no solo el fabricante.",
|
||||||
queEs:
|
queEs:
|
||||||
"El certificado del laboratorio u organismo, con alcance, número de expediente y fecha. Una marca de certificación sin documento asociado a un modelo no es evidencia: es un logotipo.",
|
"El certificado del laboratorio u organismo, con alcance, número de expediente y fecha. Una marca de certificación sin documento asociado a un modelo no es evidencia: es un logotipo.",
|
||||||
},
|
},
|
||||||
@@ -66,6 +83,7 @@ const DOCUMENTOS: Array<{ id: DocumentoId; nombre: string; corto: string; queEs:
|
|||||||
id: "garantia",
|
id: "garantia",
|
||||||
nombre: "Garantía con condiciones por escrito",
|
nombre: "Garantía con condiciones por escrito",
|
||||||
corto: "Garantía",
|
corto: "Garantía",
|
||||||
|
llano: "Cuánto dura, qué cubre y qué la cancela.",
|
||||||
queEs:
|
queEs:
|
||||||
"Plazo, cobertura, exclusiones y qué obligaciones de mantenimiento la conservan vigente. Un plazo sin condiciones es una cifra publicitaria.",
|
"Plazo, cobertura, exclusiones y qué obligaciones de mantenimiento la conservan vigente. Un plazo sin condiciones es una cifra publicitaria.",
|
||||||
},
|
},
|
||||||
@@ -73,29 +91,33 @@ const DOCUMENTOS: Array<{ id: DocumentoId; nombre: string; corto: string; queEs:
|
|||||||
id: "manual",
|
id: "manual",
|
||||||
nombre: "Manual de operación y mantenimiento",
|
nombre: "Manual de operación y mantenimiento",
|
||||||
corto: "Manual",
|
corto: "Manual",
|
||||||
|
llano: "Cómo se usa y cada cuánto hay que revisarlo.",
|
||||||
queEs:
|
queEs:
|
||||||
"Cómo se opera, cada cuánto se revisa y qué se considera una falla. Sin manual, la capacitación se vuelve tradición oral y la garantía queda expuesta.",
|
"Operación, periodicidad de revisión y qué se considera una falla. Sin manual, la capacitación se vuelve tradición oral y la garantía queda expuesta.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "bitacora",
|
id: "bitacora",
|
||||||
nombre: "Bitácora de revisión",
|
nombre: "Bitácora de revisión",
|
||||||
corto: "Bitácora",
|
corto: "Bitácora",
|
||||||
|
llano: "La hoja donde se firma cada revisión. Es la que piden primero.",
|
||||||
queEs:
|
queEs:
|
||||||
"El formato donde queda constancia de cada revisión, con fecha y responsable. Es la evidencia recurrente que una verificación pide primero.",
|
"Constancia de cada revisión con fecha y responsable. Es la evidencia recurrente que una verificación solicita antes que cualquier otra, porque delata si el programa existe o solo se describió.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "instalacion",
|
id: "instalacion",
|
||||||
nombre: "Constancia de instalación y puesta en marcha",
|
nombre: "Constancia de instalación y puesta en marcha",
|
||||||
corto: "Instalación",
|
corto: "Instalación",
|
||||||
|
llano: "Para lo que se instala: quién lo instaló y que se probó funcionando.",
|
||||||
queEs:
|
queEs:
|
||||||
"Para sistemas que se instalan: ubicación, configuración instalada, pruebas de arranque y quién las ejecutó. Es lo que permite reclamar si el sistema no actúa.",
|
"Ubicación, configuración instalada, pruebas de arranque y quién las ejecutó. Es lo que permite reclamar si el sistema no actúa cuando debía.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "capacitacion",
|
id: "capacitacion",
|
||||||
nombre: "Constancia de capacitación",
|
nombre: "Constancia de capacitación",
|
||||||
corto: "Capacitación",
|
corto: "Capacitación",
|
||||||
|
llano: "El papel con los nombres de quienes ya saben usarlo.",
|
||||||
queEs:
|
queEs:
|
||||||
"Nombre, fecha, contenido y quién capacitó. La capacitación sin constancia no es demostrable ante una autoridad.",
|
"Nombre, fecha, contenido y quién capacitó. La capacitación sin constancia no es demostrable ante una autoridad, aunque haya ocurrido.",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -152,71 +174,59 @@ export default function PaginaEvidencia() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* ---------- encabezado ---------- */}
|
<HeroeLanding
|
||||||
<section className="grid-bg bg-ink pt-12 pb-14 sm:pt-16 sm:pb-20">
|
antes={<Migas actual="Evidencia técnica" />}
|
||||||
<div className="shell">
|
eyebrow="Centro de evidencia"
|
||||||
<nav aria-label="Ruta de navegación" className="mb-8">
|
titulo="Los papeles que tiene que traer tu equipo"
|
||||||
<ol className="flex flex-wrap items-center gap-2 font-mono text-[0.625rem] uppercase tracking-[0.14em] text-faint">
|
entrada="Un equipo contra incendio se compra dos veces: cuando se instala y cuando alguien pide los documentos. Aquí está la lista de qué exigir, para que la segunda vez no sea un problema."
|
||||||
<li>
|
foto={FOTOS.publicaciones}
|
||||||
<Link href="/" className="hover:text-bright">
|
enfoque="65% 45%"
|
||||||
Inicio
|
altura="alto"
|
||||||
</Link>
|
prioridad
|
||||||
</li>
|
>
|
||||||
<li aria-hidden="true">/</li>
|
<div className="flex flex-col gap-4 sm:flex-row sm:items-center">
|
||||||
<li className="text-bright">Evidencia técnica</li>
|
<Link href="#solicitar" className="btn btn-signal w-full sm:w-auto">
|
||||||
</ol>
|
Pedir la documentación de un equipo
|
||||||
</nav>
|
</Link>
|
||||||
|
<p className="font-mono text-[0.625rem] uppercase leading-relaxed tracking-[0.1em] text-faint">
|
||||||
<div className="max-w-3xl anim-rise">
|
{DOCUMENTOS.length} documentos · {CATEGORIAS.length} categorías · {totalClaims} puntos
|
||||||
<p className="eyebrow mb-5">Centro de evidencia</p>
|
por documentar
|
||||||
<h1 className="font-display text-[2rem] font-black uppercase leading-[1.03] tracking-tight sm:text-[2.75rem] lg:text-5xl">
|
</p>
|
||||||
La documentación es el producto que casi nadie entrega
|
|
||||||
</h1>
|
|
||||||
<p className="mt-6 text-base leading-relaxed text-muted sm:text-lg">
|
|
||||||
Un equipo contra incendio se compra dos veces: cuando se instala y cuando alguien pide
|
|
||||||
los papeles. Esta página dice exactamente qué documentos deberían acompañar una
|
|
||||||
compra, qué aplica según la categoría y qué afirmaciones documentamos por número de
|
|
||||||
modelo antes de cotizar.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<dl className="spec mt-9 sm:grid-cols-3">
|
|
||||||
<div className="spec-cell">
|
|
||||||
<dt className="spec-key">Documentos del acervo</dt>
|
|
||||||
<dd className="spec-val">{DOCUMENTOS.length}</dd>
|
|
||||||
</div>
|
|
||||||
<div className="spec-cell">
|
|
||||||
<dt className="spec-key">Categorías indexadas</dt>
|
|
||||||
<dd className="spec-val">{CATEGORIAS.length}</dd>
|
|
||||||
</div>
|
|
||||||
<div className="spec-cell">
|
|
||||||
<dt className="spec-key">Puntos por documentar</dt>
|
|
||||||
<dd className="spec-val">{totalClaims}</dd>
|
|
||||||
</div>
|
|
||||||
</dl>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</HeroeLanding>
|
||||||
|
|
||||||
{/* ---------- el estado real del acervo: sin descargas falsas ---------- */}
|
{/* ---------- el estado real del acervo: sin descargas falsas ---------- */}
|
||||||
<section className="border-t border-rule bg-ink-2 py-12 sm:py-16" aria-labelledby="estado-titulo">
|
<section
|
||||||
|
className="grid-bg-lite bg-paper py-12 sm:py-16"
|
||||||
|
aria-labelledby="estado-titulo"
|
||||||
|
>
|
||||||
<div className="shell">
|
<div className="shell">
|
||||||
<div className="rounded-[6px] border border-rule border-l-2 border-l-ember bg-ink p-6 sm:p-8">
|
<div className="rounded-[6px] border border-rule-lite border-l-2 border-l-ember bg-white p-6 sm:p-8">
|
||||||
<p className="font-mono text-[0.6875rem] uppercase tracking-[0.16em] text-ember">
|
<p className="font-mono text-[0.6875rem] uppercase tracking-[0.16em] text-deep-muted">
|
||||||
Estado del acervo
|
Estado del acervo
|
||||||
</p>
|
</p>
|
||||||
<h2 id="estado-titulo" className="mt-4 font-display text-xl font-extrabold sm:text-2xl">
|
<h2
|
||||||
|
id="estado-titulo"
|
||||||
|
className="mt-4 font-display text-xl font-extrabold text-deep sm:text-2xl"
|
||||||
|
>
|
||||||
Aquí no hay botones de descarga, y es a propósito
|
Aquí no hay botones de descarga, y es a propósito
|
||||||
</h2>
|
</h2>
|
||||||
<div className="mt-4 max-w-3xl space-y-4 text-sm leading-relaxed text-muted">
|
<div className="mt-4 max-w-3xl space-y-4 text-sm leading-relaxed text-deep-muted sm:text-base">
|
||||||
<p>
|
<p>
|
||||||
En la extracción pública del sitio de FSPM no apareció un índice verificable de
|
Podríamos haber puesto una fila de botones «Descargar ficha» que no
|
||||||
documentos por número de modelo. Podríamos haber puesto una fila de botones
|
llevan a ningún archivo. No lo hicimos: un botón falso destruye la credibilidad de
|
||||||
«Descargar ficha» que no llevan a ningún archivo. No lo hicimos: un
|
todo lo demás que dice esta página.
|
||||||
botón falso destruye la credibilidad de todo lo demás que dice esta página.
|
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Lo que sí hacemos es esto:{" "}
|
En la extracción pública del sitio de FSPM no apareció un índice de documentos
|
||||||
<span className="text-bright">
|
asociado a números de modelo. Lo que sí aparece es un puñado de certificados
|
||||||
|
referidos al concentrado espumógeno y al agente humectante, no a un equipo
|
||||||
|
concreto: sirven como antecedente, no como la evidencia que una auditoría te va a
|
||||||
|
pedir sobre el extintor que tienes colgado en la pared.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Así que esto es lo que hacemos:{" "}
|
||||||
|
<span className="font-semibold text-deep">
|
||||||
los documentos se entregan por número de modelo, a solicitud, junto con la
|
los documentos se entregan por número de modelo, a solicitud, junto con la
|
||||||
propuesta técnica
|
propuesta técnica
|
||||||
</span>
|
</span>
|
||||||
@@ -225,35 +235,39 @@ export default function PaginaEvidencia() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-7 flex flex-wrap gap-3">
|
<div className="mt-7">
|
||||||
<Link href="#solicitar" className="btn btn-signal btn-sm">
|
<Link href="#solicitar" className="btn btn-signal btn-sm">
|
||||||
Solicitar documentación
|
Solicitar documentación
|
||||||
</Link>
|
</Link>
|
||||||
<Link href="/catalogo" className="btn btn-ghost btn-sm">
|
|
||||||
Ver el catálogo
|
|
||||||
</Link>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* ---------- qué documentación corresponde a cada compra ---------- */}
|
{/* ---------- qué documentación corresponde a cada compra ---------- */}
|
||||||
<section className="grid-bg-lite bg-paper py-14 sm:py-20" aria-labelledby="documentos-titulo">
|
<section
|
||||||
|
className="border-t border-rule bg-ink py-14 sm:py-20"
|
||||||
|
aria-labelledby="documentos-titulo"
|
||||||
|
>
|
||||||
<div className="shell">
|
<div className="shell">
|
||||||
<Revelar>
|
<Revelar>
|
||||||
<p className="eyebrow mb-4">El acervo</p>
|
<p className="eyebrow mb-4">El acervo</p>
|
||||||
<h2 id="documentos-titulo" className="font-display text-2xl font-extrabold text-deep sm:text-3xl">
|
<h2
|
||||||
|
id="documentos-titulo"
|
||||||
|
className="max-w-2xl font-display text-2xl font-extrabold sm:text-3xl"
|
||||||
|
>
|
||||||
Los {DOCUMENTOS.length} documentos que corresponden a una compra
|
Los {DOCUMENTOS.length} documentos que corresponden a una compra
|
||||||
</h2>
|
</h2>
|
||||||
<p className="mt-3 max-w-2xl text-sm leading-relaxed text-deep-muted sm:text-base">
|
<p className="mt-4 max-w-2xl text-sm leading-relaxed text-muted sm:text-base">
|
||||||
No todos aplican a todos los productos. Los dos últimos aparecen solo cuando hay
|
Cada uno arranca con para qué te sirve, en una línea, y sigue con lo que tiene que
|
||||||
instalación o personal que operar.
|
decir por dentro. Los dos últimos aplican solo cuando hay algo que instalar o
|
||||||
|
personal que operar.
|
||||||
</p>
|
</p>
|
||||||
</Revelar>
|
</Revelar>
|
||||||
|
|
||||||
<ol className="mt-10 grid gap-px bg-rule-lite sm:grid-cols-2">
|
<ol className="mt-10 grid gap-px bg-rule sm:grid-cols-2">
|
||||||
{DOCUMENTOS.map((documento, i) => (
|
{DOCUMENTOS.map((documento, i) => (
|
||||||
<li key={documento.id} className="bg-white p-6 sm:p-7">
|
<li key={documento.id} className="bg-ink-2 p-6 sm:p-7">
|
||||||
<div className="flex items-baseline gap-3">
|
<div className="flex items-baseline gap-3">
|
||||||
<span
|
<span
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
@@ -261,11 +275,12 @@ export default function PaginaEvidencia() {
|
|||||||
>
|
>
|
||||||
{String(i + 1).padStart(2, "0")}
|
{String(i + 1).padStart(2, "0")}
|
||||||
</span>
|
</span>
|
||||||
<h3 className="font-display text-base font-extrabold leading-tight text-deep">
|
<h3 className="font-display text-base font-extrabold leading-tight text-bright">
|
||||||
{documento.nombre}
|
{documento.nombre}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-3 text-sm leading-relaxed text-deep-muted">{documento.queEs}</p>
|
<p className="mt-3 text-base leading-relaxed text-bright">{documento.llano}</p>
|
||||||
|
<p className="mt-3 text-sm leading-relaxed text-muted">{documento.queEs}</p>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ol>
|
</ol>
|
||||||
@@ -273,25 +288,32 @@ export default function PaginaEvidencia() {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* ---------- índice por categoría ---------- */}
|
{/* ---------- índice por categoría ---------- */}
|
||||||
<section className="border-t border-rule bg-ink py-14 sm:py-20" aria-labelledby="indice-titulo">
|
<section
|
||||||
|
className="grid-bg-lite border-t border-rule bg-paper py-14 sm:py-20"
|
||||||
|
aria-labelledby="indice-titulo"
|
||||||
|
>
|
||||||
<div className="shell-wide">
|
<div className="shell-wide">
|
||||||
<Revelar>
|
<Revelar>
|
||||||
<p className="eyebrow mb-4">Índice</p>
|
<p className="eyebrow mb-4">Índice</p>
|
||||||
<h2 id="indice-titulo" className="font-display text-2xl font-extrabold sm:text-3xl">
|
<h2
|
||||||
Qué aplica según la categoría de producto
|
id="indice-titulo"
|
||||||
|
className="max-w-2xl font-display text-2xl font-extrabold text-deep sm:text-3xl"
|
||||||
|
>
|
||||||
|
Qué exigir según lo que estés comprando
|
||||||
</h2>
|
</h2>
|
||||||
<p className="mt-3 max-w-2xl text-sm leading-relaxed text-muted sm:text-base">
|
<p className="mt-4 max-w-2xl text-sm leading-relaxed text-deep-muted sm:text-base">
|
||||||
Es la lista de lo que conviene exigir en cada compra, no una afirmación de qué está ya
|
Busca tu categoría y lleva esta lista a la conversación con cualquier proveedor,
|
||||||
publicado.
|
nosotros incluidos. Es lo que conviene exigir en cada compra, no una afirmación de
|
||||||
|
qué está ya publicado.
|
||||||
</p>
|
</p>
|
||||||
</Revelar>
|
</Revelar>
|
||||||
|
|
||||||
<p className="mt-6 font-mono text-[0.6875rem] uppercase tracking-[0.14em] text-faint sm:hidden">
|
<p className="mt-6 font-mono text-[0.6875rem] uppercase tracking-[0.14em] text-deep-muted sm:hidden">
|
||||||
Desliza la tabla para ver todas las columnas
|
Desliza la tabla para ver todas las columnas
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="mt-4 overflow-x-auto rounded-[6px] border border-rule bg-ink-2 [-webkit-overflow-scrolling:touch]"
|
className="mt-4 overflow-x-auto rounded-[6px] border border-rule-lite bg-white [-webkit-overflow-scrolling:touch]"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
role="region"
|
role="region"
|
||||||
aria-label="Índice de documentación por categoría, tabla desplazable en horizontal"
|
aria-label="Índice de documentación por categoría, tabla desplazable en horizontal"
|
||||||
@@ -301,10 +323,10 @@ export default function PaginaEvidencia() {
|
|||||||
Documentos que corresponden a cada categoría de producto.
|
Documentos que corresponden a cada categoría de producto.
|
||||||
</caption>
|
</caption>
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="border-b border-rule bg-ink-3">
|
<tr className="border-b border-rule-lite bg-paper-2">
|
||||||
<th
|
<th
|
||||||
scope="col"
|
scope="col"
|
||||||
className="whitespace-nowrap px-4 py-3 font-mono text-[0.625rem] font-medium uppercase tracking-[0.14em] text-faint"
|
className="whitespace-nowrap px-4 py-3 font-mono text-[0.625rem] font-medium uppercase tracking-[0.14em] text-deep-muted"
|
||||||
>
|
>
|
||||||
Categoría
|
Categoría
|
||||||
</th>
|
</th>
|
||||||
@@ -312,7 +334,7 @@ export default function PaginaEvidencia() {
|
|||||||
<th
|
<th
|
||||||
key={d.id}
|
key={d.id}
|
||||||
scope="col"
|
scope="col"
|
||||||
className="whitespace-nowrap px-3 py-3 font-mono text-[0.625rem] font-medium uppercase tracking-[0.12em] text-faint"
|
className="whitespace-nowrap px-3 py-3 font-mono text-[0.625rem] font-medium uppercase tracking-[0.12em] text-deep-muted"
|
||||||
>
|
>
|
||||||
{d.corto}
|
{d.corto}
|
||||||
</th>
|
</th>
|
||||||
@@ -323,11 +345,11 @@ export default function PaginaEvidencia() {
|
|||||||
{CATEGORIAS.map((categoria) => {
|
{CATEGORIAS.map((categoria) => {
|
||||||
const aplica = APLICA_POR_CATEGORIA[categoria.slug] ?? [];
|
const aplica = APLICA_POR_CATEGORIA[categoria.slug] ?? [];
|
||||||
return (
|
return (
|
||||||
<tr key={categoria.slug} className="border-b border-rule last:border-b-0">
|
<tr key={categoria.slug} className="border-b border-rule-lite last:border-b-0">
|
||||||
<th scope="row" className="px-4 py-4 text-left align-middle">
|
<th scope="row" className="px-4 py-4 text-left align-middle">
|
||||||
<Link
|
<Link
|
||||||
href={`/catalogo/${categoria.slug}`}
|
href={`/catalogo/${categoria.slug}`}
|
||||||
className="link font-sans text-sm font-semibold text-bright [-webkit-tap-highlight-color:transparent]"
|
className="link font-sans text-sm font-semibold text-deep [-webkit-tap-highlight-color:transparent]"
|
||||||
>
|
>
|
||||||
{categoria.nombre}
|
{categoria.nombre}
|
||||||
</Link>
|
</Link>
|
||||||
@@ -352,13 +374,13 @@ export default function PaginaEvidencia() {
|
|||||||
</svg>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="flex h-5 w-5 items-center justify-center rounded-[2px] border border-rule-2">
|
<span className="flex h-5 w-5 items-center justify-center rounded-[2px] border border-rule-lite">
|
||||||
<span className="sr-only">
|
<span className="sr-only">
|
||||||
{d.nombre} no aplica a {categoria.nombre}
|
{d.nombre} no aplica a {categoria.nombre}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
className="h-[1px] w-2 bg-rule-2"
|
className="h-[1px] w-2 bg-rule-lite"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -371,25 +393,38 @@ export default function PaginaEvidencia() {
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-8">
|
||||||
|
<Link href="#solicitar" className="btn btn-deep btn-sm">
|
||||||
|
Pedir este índice para mi caso
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* ---------- consolidado de claims por verificar ---------- */}
|
{/* ---------- consolidado de claims por verificar ---------- */}
|
||||||
<section className="grid-bg-lite bg-paper py-14 sm:py-20" aria-labelledby="claims-titulo">
|
<section
|
||||||
|
className="border-t border-rule bg-ink py-14 sm:py-20"
|
||||||
|
aria-labelledby="claims-titulo"
|
||||||
|
>
|
||||||
<div className="shell">
|
<div className="shell">
|
||||||
<p className="font-mono text-[0.6875rem] font-medium uppercase tracking-[0.16em] text-ember-dim">
|
<p className="font-mono text-[0.6875rem] font-medium uppercase tracking-[0.16em] text-ember">
|
||||||
Compromiso de documentación
|
Compromiso de documentación
|
||||||
</p>
|
</p>
|
||||||
<h2 id="claims-titulo" className="mt-4 font-display text-2xl font-extrabold text-deep sm:text-3xl">
|
<h2
|
||||||
|
id="claims-titulo"
|
||||||
|
className="mt-4 max-w-2xl font-display text-2xl font-extrabold sm:text-3xl"
|
||||||
|
>
|
||||||
Los {totalClaims} puntos que documentamos antes de cotizar
|
Los {totalClaims} puntos que documentamos antes de cotizar
|
||||||
</h2>
|
</h2>
|
||||||
<div className="mt-4 max-w-3xl space-y-4 text-sm leading-relaxed text-deep-muted sm:text-base">
|
<div className="mt-4 max-w-3xl space-y-4 text-sm leading-relaxed text-muted sm:text-base">
|
||||||
<p>
|
<p>
|
||||||
El sitio anterior afirmaba varias cosas como características del producto:{" "}
|
El sitio anterior presentaba varias cosas como características del producto:{" "}
|
||||||
<span className="text-deep">no caduca</span>,{" "}
|
<span className="text-bright">no caduca</span>,{" "}
|
||||||
<span className="text-deep">20 años de garantía</span>,{" "}
|
<span className="text-bright">20 años de garantía</span>,{" "}
|
||||||
<span className="text-deep">no requiere recarga</span>, cobertura de todas las clases
|
<span className="text-bright">no requiere recarga</span>, cobertura de todas las
|
||||||
de fuego. El estudio de marca concluyó que no pueden presentarse como universales.
|
clases de fuego. El estudio de marca concluyó que no pueden presentarse como
|
||||||
|
universales.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Aquí no se repiten. Se convierten en una lista de trabajo: cada punto es un documento
|
Aquí no se repiten. Se convierten en una lista de trabajo: cada punto es un documento
|
||||||
@@ -402,41 +437,41 @@ export default function PaginaEvidencia() {
|
|||||||
{porCategoria.map(({ categoria, productos, claims }) => (
|
{porCategoria.map(({ categoria, productos, claims }) => (
|
||||||
<details
|
<details
|
||||||
key={categoria.slug}
|
key={categoria.slug}
|
||||||
className="group rounded-[6px] border border-rule-lite bg-white [-webkit-tap-highlight-color:transparent]"
|
className="group rounded-[6px] border border-rule bg-ink-2 [-webkit-tap-highlight-color:transparent]"
|
||||||
>
|
>
|
||||||
<summary className="flex cursor-pointer list-none items-center justify-between gap-4 px-5 py-4 sm:px-6">
|
<summary className="flex cursor-pointer list-none [&::-webkit-details-marker]:hidden [-webkit-tap-highlight-color:transparent] items-center justify-between gap-4 px-5 py-4 sm:px-6">
|
||||||
<span>
|
<span>
|
||||||
<span className="block font-display text-base font-extrabold text-deep">
|
<span className="block font-display text-base font-extrabold text-bright">
|
||||||
{categoria.nombre}
|
{categoria.nombre}
|
||||||
</span>
|
</span>
|
||||||
<span className="mt-1 block font-mono text-[0.625rem] uppercase tracking-[0.12em] text-deep-muted">
|
<span className="mt-1 block font-mono text-[0.625rem] uppercase tracking-[0.12em] text-faint">
|
||||||
{productos.length} {productos.length === 1 ? "equipo" : "equipos"} ·{" "}
|
{productos.length} {productos.length === 1 ? "equipo" : "equipos"} ·{" "}
|
||||||
{claims.length} {claims.length === 1 ? "punto" : "puntos"} por documentar
|
{claims.length} {claims.length === 1 ? "punto" : "puntos"} por documentar
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
className="flex h-7 w-7 shrink-0 items-center justify-center rounded-[2px] border border-rule-lite font-mono text-sm text-deep-muted transition-transform duration-200 group-open:rotate-45"
|
className="flex h-7 w-7 shrink-0 items-center justify-center rounded-[2px] border border-rule-2 font-mono text-sm text-muted transition-transform duration-200 group-open:rotate-45"
|
||||||
>
|
>
|
||||||
+
|
+
|
||||||
</span>
|
</span>
|
||||||
</summary>
|
</summary>
|
||||||
|
|
||||||
<div className="border-t border-rule-lite px-5 pb-6 pt-5 sm:px-6">
|
<div className="border-t border-rule px-5 pb-6 pt-5 sm:px-6">
|
||||||
{claims.length > 0 ? (
|
{claims.length > 0 ? (
|
||||||
<ul className="space-y-3.5">
|
<ul className="space-y-3.5">
|
||||||
{claims.map((claim) => (
|
{claims.map((claim) => (
|
||||||
<li key={claim} className="flex gap-3">
|
<li key={claim} className="flex gap-3">
|
||||||
<span
|
<span
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
className="mt-1.5 h-1.5 w-1.5 shrink-0 rounded-full bg-ember-dim"
|
className="mt-1.5 h-1.5 w-1.5 shrink-0 rounded-full bg-ember"
|
||||||
/>
|
/>
|
||||||
<p className="text-sm leading-relaxed text-deep-muted">{claim}</p>
|
<p className="text-sm leading-relaxed text-muted">{claim}</p>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-sm leading-relaxed text-deep-muted">
|
<p className="text-sm leading-relaxed text-muted">
|
||||||
Sin puntos pendientes registrados para esta categoría.
|
Sin puntos pendientes registrados para esta categoría.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@@ -455,30 +490,36 @@ export default function PaginaEvidencia() {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* ---------- referencias normativas y certificaciones enumeradas ---------- */}
|
{/* ---------- referencias normativas y certificaciones enumeradas ---------- */}
|
||||||
<section className="border-t border-rule bg-ink py-14 sm:py-20" aria-labelledby="normas-titulo">
|
<section
|
||||||
|
className="grid-bg-lite border-t border-rule bg-paper py-14 sm:py-20"
|
||||||
|
aria-labelledby="normas-titulo"
|
||||||
|
>
|
||||||
<div className="shell">
|
<div className="shell">
|
||||||
<Revelar>
|
<Revelar>
|
||||||
<p className="eyebrow mb-4">Referencias normativas</p>
|
<p className="eyebrow mb-4">Referencias normativas</p>
|
||||||
<h2 id="normas-titulo" className="font-display text-2xl font-extrabold sm:text-3xl">
|
<h2
|
||||||
|
id="normas-titulo"
|
||||||
|
className="max-w-2xl font-display text-2xl font-extrabold text-deep sm:text-3xl"
|
||||||
|
>
|
||||||
Las normas contra las que se revisa un expediente
|
Las normas contra las que se revisa un expediente
|
||||||
</h2>
|
</h2>
|
||||||
</Revelar>
|
</Revelar>
|
||||||
|
|
||||||
<ul className="mt-9 grid gap-4 lg:grid-cols-2">
|
<ul className="mt-9 grid gap-4 lg:grid-cols-2">
|
||||||
{NORMAS.map((norma) => (
|
{NORMAS.map((norma) => (
|
||||||
<li key={norma.clave} className="panel p-5 sm:p-6">
|
<li key={norma.clave} className="panel-lite p-5 sm:p-6">
|
||||||
<p className="font-mono text-xs font-semibold uppercase tracking-[0.12em] text-signal">
|
<p className="font-mono text-xs font-semibold uppercase tracking-[0.12em] text-signal">
|
||||||
{norma.clave}
|
{norma.clave}
|
||||||
</p>
|
</p>
|
||||||
<h3 className="mt-3 font-display text-base font-extrabold leading-tight text-bright">
|
<h3 className="mt-3 font-display text-base font-extrabold leading-tight text-deep">
|
||||||
{norma.titulo}
|
{norma.titulo}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="mt-3 text-sm leading-relaxed text-muted">{norma.aplica}</p>
|
<p className="mt-3 text-sm leading-relaxed text-deep-muted">{norma.aplica}</p>
|
||||||
<a
|
<a
|
||||||
href={norma.fuente}
|
href={norma.fuente}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="mt-4 inline-block font-mono text-[0.625rem] uppercase tracking-[0.14em] text-bright underline decoration-rule-2 underline-offset-4 hover:decoration-signal"
|
className="mt-4 inline-block font-mono text-[0.625rem] uppercase tracking-[0.14em] text-deep underline decoration-rule-lite underline-offset-4 hover:decoration-signal"
|
||||||
>
|
>
|
||||||
Ver la fuente oficial
|
Ver la fuente oficial
|
||||||
</a>
|
</a>
|
||||||
@@ -486,14 +527,14 @@ export default function PaginaEvidencia() {
|
|||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<div className="mt-10 rounded-[6px] border border-rule border-l-2 border-l-ember bg-ink-2 p-6 sm:p-7">
|
<div className="mt-10 rounded-[6px] border border-rule-lite border-l-2 border-l-ember bg-white p-6 sm:p-7">
|
||||||
<p className="font-mono text-[0.6875rem] uppercase tracking-[0.16em] text-ember">
|
<p className="font-mono text-[0.6875rem] uppercase tracking-[0.16em] text-deep-muted">
|
||||||
Enumerado por la fuente, pendiente de verificar
|
Enumerado por la fuente, pendiente de verificar
|
||||||
</p>
|
</p>
|
||||||
<h3 className="mt-4 font-display text-lg font-extrabold sm:text-xl">
|
<h3 className="mt-4 font-display text-lg font-extrabold text-deep sm:text-xl">
|
||||||
Lo que el sitio anterior lista bajo «certificaciones»
|
Lo que el sitio anterior lista bajo «certificaciones»
|
||||||
</h3>
|
</h3>
|
||||||
<p className="mt-3 max-w-3xl text-sm leading-relaxed text-muted">
|
<p className="mt-3 max-w-3xl text-sm leading-relaxed text-deep-muted">
|
||||||
Estas referencias aparecen enumeradas en la información pública de FSPM. Las
|
Estas referencias aparecen enumeradas en la información pública de FSPM. Las
|
||||||
reproducimos como inventario de trabajo, no como certificaciones vigentes: una
|
reproducimos como inventario de trabajo, no como certificaciones vigentes: una
|
||||||
referencia solo se convierte en evidencia cuando existe el documento con laboratorio,
|
referencia solo se convierte en evidencia cuando existe el documento con laboratorio,
|
||||||
@@ -502,34 +543,45 @@ export default function PaginaEvidencia() {
|
|||||||
|
|
||||||
<ul className="mt-6 flex flex-wrap gap-2">
|
<ul className="mt-6 flex flex-wrap gap-2">
|
||||||
{CERTIFICACIONES_ENUMERADAS.map((certificacion) => (
|
{CERTIFICACIONES_ENUMERADAS.map((certificacion) => (
|
||||||
<li key={certificacion} className="badge">
|
<li
|
||||||
|
key={certificacion}
|
||||||
|
className="badge !border-rule-lite !text-deep-muted"
|
||||||
|
>
|
||||||
{certificacion}
|
{certificacion}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<p className="mt-6 max-w-3xl text-xs leading-relaxed text-faint">
|
<p className="mt-6 max-w-3xl text-xs leading-relaxed text-deep-muted">
|
||||||
Ninguna norma sustituye a otra. Un listado UL no equivale a una NOM, y una prueba
|
Ninguna norma sustituye a otra. Un listado UL no equivale a una NOM, y una prueba
|
||||||
ambiental no acredita desempeño de extinción. Construir ese mapa de equivalencias, con
|
ambiental no acredita desempeño de extinción. Construir ese mapa de equivalencias,
|
||||||
el documento en la mano, es parte del trabajo previo a una propuesta.
|
con el documento en la mano, es parte del trabajo previo a una propuesta.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* ---------- solicitud ---------- */}
|
{/* ---------- solicitud ---------- */}
|
||||||
<section id="solicitar" className="grid-bg-lite border-t border-rule bg-paper py-14 sm:py-20">
|
<section
|
||||||
|
id="solicitar"
|
||||||
|
className="border-t border-rule bg-ink py-14 sm:py-20"
|
||||||
|
aria-labelledby="solicitar-titulo"
|
||||||
|
>
|
||||||
<div className="shell">
|
<div className="shell">
|
||||||
<div className="grid gap-10 lg:grid-cols-[1fr_1.05fr] lg:items-start lg:gap-16">
|
<div className="grid gap-10 lg:grid-cols-[1fr_1.05fr] lg:items-start lg:gap-16">
|
||||||
<div>
|
<div>
|
||||||
<Revelar>
|
<Revelar>
|
||||||
<p className="eyebrow mb-4">Solicitar documentación</p>
|
<p className="eyebrow mb-4">Solicitar documentación</p>
|
||||||
<h2 className="font-display text-2xl font-extrabold text-deep sm:text-3xl">
|
<h2
|
||||||
Pide los documentos por número de modelo
|
id="solicitar-titulo"
|
||||||
|
className="font-display text-2xl font-extrabold sm:text-3xl"
|
||||||
|
>
|
||||||
|
Dinos qué te están pidiendo y te decimos qué existe
|
||||||
</h2>
|
</h2>
|
||||||
<p className="mt-4 text-sm leading-relaxed text-deep-muted sm:text-base">
|
<p className="mt-4 text-sm leading-relaxed text-muted sm:text-base">
|
||||||
Dinos qué equipo te interesa o qué te está pidiendo tu auditoría. Respondemos con lo
|
No hace falta que sepas el nombre del documento. Con el equipo que te interesa o
|
||||||
que existe, con su alcance, y con lo que no podemos sostener por escrito.
|
con la observación que te levantaron es suficiente: respondemos con lo que existe,
|
||||||
|
con su alcance, y con lo que no podemos sostener por escrito.
|
||||||
</p>
|
</p>
|
||||||
</Revelar>
|
</Revelar>
|
||||||
|
|
||||||
@@ -539,8 +591,11 @@ export default function PaginaEvidencia() {
|
|||||||
"Si un plazo de garantía tiene condiciones, van en la propuesta, no en la letra chica.",
|
"Si un plazo de garantía tiene condiciones, van en la propuesta, no en la letra chica.",
|
||||||
"Si no tenemos un documento, lo decimos. Es más barato que descubrirlo en la auditoría.",
|
"Si no tenemos un documento, lo decimos. Es más barato que descubrirlo en la auditoría.",
|
||||||
].map((punto) => (
|
].map((punto) => (
|
||||||
<li key={punto} className="flex gap-3 text-sm leading-relaxed text-deep-muted">
|
<li key={punto} className="flex gap-3 text-sm leading-relaxed text-muted">
|
||||||
<span aria-hidden="true" className="mt-2 h-1 w-1 shrink-0 rounded-full bg-signal" />
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
className="mt-2 h-1 w-1 shrink-0 rounded-full bg-signal"
|
||||||
|
/>
|
||||||
{punto}
|
{punto}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
@@ -548,10 +603,12 @@ export default function PaginaEvidencia() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<FormularioContacto
|
<FormularioContacto
|
||||||
canal="cumplimiento"
|
id="formulario-evidencia"
|
||||||
variante="claro"
|
canal="catalogo"
|
||||||
titulo="Solicitar documentación técnica"
|
titulo="Pedir documentación técnica"
|
||||||
descripcion="Indica el equipo o la instalación y qué documento te están pidiendo. Respondemos con el índice aplicable a tu caso."
|
descripcion="Completa la frase del mensaje con el documento o la observación que tienes enfrente. Respondemos con el índice aplicable a tu caso."
|
||||||
|
mensajeInicial={MENSAJE_INICIAL}
|
||||||
|
textoBoton="Solicitar documentación"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,21 +1,27 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
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 { Revelar } from "@/components/site/Revelar";
|
import { Revelar } from "@/components/site/Revelar";
|
||||||
import { CATEGORIAS, PRODUCTOS } from "@/data/catalogo";
|
import { CATEGORIAS, PRODUCTOS } from "@/data/catalogo";
|
||||||
import { EMPRESA, PRENSA, SITIO_URL } from "@/data/empresa";
|
import { CLIENTES, EMPRESA, PRENSA, SITIO_URL } from "@/data/empresa";
|
||||||
import { INDUSTRIAS } from "@/data/industrias";
|
import { INDUSTRIAS } from "@/data/industrias";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* /nosotros — origen de la tecnología, posicionamiento y postura editorial.
|
* /nosotros — origen de la tecnología, posicionamiento y postura editorial.
|
||||||
*
|
*
|
||||||
* La parte más importante de esta página es la última sección: por qué la
|
* La parte más importante de esta página es la sección de postura: por qué la
|
||||||
* empresa documenta en lugar de prometer. Es la traducción del hallazgo central
|
* empresa documenta en lugar de prometer. Es la traducción del hallazgo central
|
||||||
* del estudio de marca y la razón de ser del resto del sitio.
|
* del estudio de marca y la razón de ser del resto del sitio.
|
||||||
|
*
|
||||||
|
* Los logotipos de clientes se muestran sin atribuirles testimonio ni
|
||||||
|
* resultado: el estudio pide confirmar la relación antes de usarlos como caso.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const TITULO = "Quiénes somos";
|
const TITULO = "Quiénes somos: protección contra incendios documentada";
|
||||||
const DESCRIPCION = `${EMPRESA.nombre}: origen de la tecnología FireAde, posicionamiento en operaciones críticas y por qué documentamos en lugar de prometer.`;
|
const DESCRIPCION = `${EMPRESA.nombre}: de dónde viene la tecnología FireAde, en qué operaciones trabajamos y por qué documentamos en lugar de prometer. La postura editorial que gobierna todo el sitio, explicada en cinco reglas.`;
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: TITULO,
|
title: TITULO,
|
||||||
@@ -28,34 +34,41 @@ export const metadata: Metadata = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const MENSAJE_INICIAL = "Quiero hablar con alguien sobre ";
|
||||||
|
|
||||||
const POSTURA = [
|
const POSTURA = [
|
||||||
{
|
{
|
||||||
n: "01",
|
n: "01",
|
||||||
titulo: "Una afirmación sin documento es publicidad",
|
titulo: "Una afirmación sin documento es publicidad",
|
||||||
|
llano: "Si te decimos algo del equipo, te damos el papel que lo dice.",
|
||||||
texto:
|
texto:
|
||||||
"El sitio anterior presentaba vida útil ilimitada, veinte años de garantía y ausencia de recarga como características del producto. Eran, en el mejor de los casos, afirmaciones con condiciones que nadie escribió. Ahora viven en un bloque aparte de cada ficha, junto con lo que hay que entregar para poder sostenerlas.",
|
"El sitio anterior presentaba vida útil ilimitada, veinte años de garantía y ausencia de recarga como características del producto. Eran, en el mejor de los casos, afirmaciones con condiciones que nadie escribió. Ahora viven en un bloque aparte de cada ficha, junto con lo que hay que entregar para poder sostenerlas.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
n: "02",
|
n: "02",
|
||||||
titulo: "Los campos vacíos se quedan vacíos",
|
titulo: "Los campos vacíos se quedan vacíos",
|
||||||
|
llano: "Si no sabemos un dato, la ficha dice que no lo sabemos.",
|
||||||
texto:
|
texto:
|
||||||
"Cuando no hay dato de alcance, de capacidad o de caudal, la ficha lo dice. No ponemos un guion ni una estimación redondeada: un número inventado en una ficha técnica reaparece en una auditoría con el nombre de quien lo firmó.",
|
"Cuando no hay dato de alcance, de capacidad o de caudal, la ficha lo dice. No ponemos un guion ni una estimación redondeada: un número inventado en una ficha técnica reaparece en una auditoría con el nombre de quien lo firmó.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
n: "03",
|
n: "03",
|
||||||
titulo: "No hay botones de descarga que no descarguen nada",
|
titulo: "No hay botones de descarga que no descarguen nada",
|
||||||
|
llano: "Ningún botón de este sitio te va a llevar a un archivo que no existe.",
|
||||||
texto:
|
texto:
|
||||||
"El centro de evidencia explica qué documentación corresponde a cada compra y cómo se solicita por número de modelo. Lo que no hace es simular un acervo. Un botón falso destruye la credibilidad de todo lo demás.",
|
"El centro de evidencia explica qué documentación corresponde a cada compra y cómo se solicita por número de modelo. Lo que no hace es simular un acervo. Un botón falso destruye la credibilidad de todo lo demás.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
n: "04",
|
n: "04",
|
||||||
titulo: "Los casos van rotulados",
|
titulo: "Los casos van rotulados",
|
||||||
|
llano: "Los ejemplos de aplicación dicen arriba que son ejemplos.",
|
||||||
texto:
|
texto:
|
||||||
"Los escenarios de aplicación están construidos con fines de demostración y lo dicen arriba, no en una nota al pie. Un caso con nombre de cliente requiere autorización por escrito sobre qué se puede contar y qué cifras se pueden publicar.",
|
"Los escenarios están construidos con fines de demostración y lo declaran en el encabezado, no en una nota al pie. Un caso con nombre de cliente requiere autorización por escrito sobre qué se puede contar y qué cifras se pueden publicar.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
n: "05",
|
n: "05",
|
||||||
titulo: "Preferimos la respuesta incómoda",
|
titulo: "Preferimos la respuesta incómoda",
|
||||||
|
llano: "Si algo no nos conviene decirte pero te conviene saberlo, te lo decimos.",
|
||||||
texto:
|
texto:
|
||||||
"Si un certificado no cubre tu caso de uso, si un plazo trae condiciones o si simplemente no tenemos un documento, va en la propuesta. Sale más barato ahí que en la verificación.",
|
"Si un certificado no cubre tu caso de uso, si un plazo trae condiciones o si simplemente no tenemos un documento, va en la propuesta. Sale más barato ahí que en la verificación.",
|
||||||
},
|
},
|
||||||
@@ -64,64 +77,27 @@ const POSTURA = [
|
|||||||
export default function PaginaNosotros() {
|
export default function PaginaNosotros() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* ---------- encabezado ---------- */}
|
<HeroeLanding
|
||||||
<section className="grid-bg relative overflow-hidden bg-ink pt-12 pb-14 sm:pt-16 sm:pb-20">
|
antes={<Migas actual="Nosotros" />}
|
||||||
<div className="shell">
|
eyebrow="Quiénes somos"
|
||||||
<nav aria-label="Ruta de navegación" className="mb-8">
|
titulo={EMPRESA.promesa}
|
||||||
<ol className="flex flex-wrap items-center gap-2 font-mono text-[0.625rem] uppercase tracking-[0.14em] text-faint">
|
entrada={EMPRESA.descripcion}
|
||||||
<li>
|
foto={FOTOS.bombero}
|
||||||
<Link href="/" className="hover:text-bright">
|
enfoque="58% 32%"
|
||||||
Inicio
|
alt="Personal de respuesta a incendios trabajando en una plataforma industrial"
|
||||||
</Link>
|
altura="alto"
|
||||||
</li>
|
prioridad
|
||||||
<li aria-hidden="true">/</li>
|
>
|
||||||
<li className="text-bright">Nosotros</li>
|
<div className="flex flex-col gap-4 sm:flex-row sm:items-center">
|
||||||
</ol>
|
<Link href="#hablar" className="btn btn-signal w-full sm:w-auto">
|
||||||
</nav>
|
Hablar con un especialista
|
||||||
|
</Link>
|
||||||
<div className="grid gap-12 lg:grid-cols-[1.15fr_1fr] lg:items-center">
|
<p className="font-mono text-[0.625rem] uppercase leading-relaxed tracking-[0.1em] text-faint">
|
||||||
<div className="anim-rise">
|
Tecnología de origen {EMPRESA.fundacionOrigen} · {CATEGORIAS.length} categorías ·{" "}
|
||||||
<p className="eyebrow mb-5">Quiénes somos</p>
|
{PRODUCTOS.length} equipos y agentes
|
||||||
<h1 className="font-display text-[2rem] font-black uppercase leading-[1.03] tracking-tight sm:text-[2.75rem] lg:text-5xl">
|
</p>
|
||||||
{EMPRESA.promesa}
|
|
||||||
</h1>
|
|
||||||
<p className="mt-6 max-w-xl text-base leading-relaxed text-muted sm:text-lg">
|
|
||||||
{EMPRESA.descripcion}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<dl className="spec mt-9 sm:grid-cols-3">
|
|
||||||
<div className="spec-cell">
|
|
||||||
<dt className="spec-key">Origen de la tecnología</dt>
|
|
||||||
<dd className="spec-val">{EMPRESA.fundacionOrigen}</dd>
|
|
||||||
</div>
|
|
||||||
<div className="spec-cell">
|
|
||||||
<dt className="spec-key">Categorías</dt>
|
|
||||||
<dd className="spec-val">{CATEGORIAS.length}</dd>
|
|
||||||
</div>
|
|
||||||
<div className="spec-cell">
|
|
||||||
<dt className="spec-key">Equipos y agentes</dt>
|
|
||||||
<dd className="spec-val">{PRODUCTOS.length}</dd>
|
|
||||||
</div>
|
|
||||||
</dl>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="relative aspect-[4/3] w-full overflow-hidden rounded-[6px] border border-rule">
|
|
||||||
<Image
|
|
||||||
src="/media/foto/bombero-plataforma.jpg"
|
|
||||||
alt="Personal de respuesta a incendios trabajando en una plataforma industrial"
|
|
||||||
fill
|
|
||||||
priority
|
|
||||||
sizes="(max-width: 1024px) 92vw, 34rem"
|
|
||||||
className="object-cover"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
aria-hidden="true"
|
|
||||||
className="absolute inset-0 bg-gradient-to-t from-ink/70 via-transparent to-transparent"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</HeroeLanding>
|
||||||
|
|
||||||
{/* ---------- origen de la tecnología ---------- */}
|
{/* ---------- origen de la tecnología ---------- */}
|
||||||
<section className="grid-bg-lite bg-paper py-14 sm:py-20" aria-labelledby="origen-titulo">
|
<section className="grid-bg-lite bg-paper py-14 sm:py-20" aria-labelledby="origen-titulo">
|
||||||
@@ -130,11 +106,18 @@ export default function PaginaNosotros() {
|
|||||||
<div>
|
<div>
|
||||||
<Revelar>
|
<Revelar>
|
||||||
<p className="eyebrow mb-4">Origen</p>
|
<p className="eyebrow mb-4">Origen</p>
|
||||||
<h2 id="origen-titulo" className="font-display text-2xl font-extrabold text-deep sm:text-3xl">
|
<h2
|
||||||
|
id="origen-titulo"
|
||||||
|
className="font-display text-2xl font-extrabold text-deep sm:text-3xl"
|
||||||
|
>
|
||||||
La tecnología viene de {EMPRESA.empresaOrigen}
|
La tecnología viene de {EMPRESA.empresaOrigen}
|
||||||
</h2>
|
</h2>
|
||||||
</Revelar>
|
</Revelar>
|
||||||
<div className="mt-5 space-y-4 text-sm leading-relaxed text-deep-muted sm:text-base">
|
<div className="mt-5 space-y-4 text-sm leading-relaxed text-deep-muted sm:text-base">
|
||||||
|
<p className="text-base text-deep">
|
||||||
|
No fabricamos un extintor más barato. Traemos una tecnología distinta y
|
||||||
|
respondemos por su documentación.
|
||||||
|
</p>
|
||||||
<p>
|
<p>
|
||||||
{EMPRESA.empresaOrigen} se presenta como fundada en {EMPRESA.fundacionOrigen} por{" "}
|
{EMPRESA.empresaOrigen} se presenta como fundada en {EMPRESA.fundacionOrigen} por{" "}
|
||||||
{EMPRESA.fundadorOrigen}. {EMPRESA.nombreCorto} opera en México como{" "}
|
{EMPRESA.fundadorOrigen}. {EMPRESA.nombreCorto} opera en México como{" "}
|
||||||
@@ -149,7 +132,8 @@ export default function PaginaNosotros() {
|
|||||||
<p>
|
<p>
|
||||||
Ese es también el límite honesto de lo que podemos afirmar aquí. Los datos de
|
Ese es también el límite honesto de lo que podemos afirmar aquí. Los datos de
|
||||||
origen provienen de la comunicación pública de la empresa; el desempeño de cada
|
origen provienen de la comunicación pública de la empresa; el desempeño de cada
|
||||||
formulación y de cada modelo se demuestra con su documentación, no con su historia.
|
formulación y de cada modelo se demuestra con su documentación, no con su
|
||||||
|
historia.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -177,7 +161,10 @@ export default function PaginaNosotros() {
|
|||||||
{String(industria.prioridad).padStart(2, "0")}
|
{String(industria.prioridad).padStart(2, "0")}
|
||||||
</span>
|
</span>
|
||||||
<p className="text-sm leading-relaxed text-deep-muted">
|
<p className="text-sm leading-relaxed text-deep-muted">
|
||||||
<Link href={`/soluciones/${industria.slug}`} className="link font-semibold text-deep">
|
<Link
|
||||||
|
href={`/soluciones/${industria.slug}`}
|
||||||
|
className="link font-semibold text-deep"
|
||||||
|
>
|
||||||
{industria.nombre}
|
{industria.nombre}
|
||||||
</Link>
|
</Link>
|
||||||
{" — "}
|
{" — "}
|
||||||
@@ -192,13 +179,19 @@ export default function PaginaNosotros() {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* ---------- postura editorial ---------- */}
|
{/* ---------- postura editorial ---------- */}
|
||||||
<section className="border-t border-rule bg-ink py-14 sm:py-20" aria-labelledby="postura-titulo">
|
<section
|
||||||
|
className="border-t border-rule bg-ink py-14 sm:py-20"
|
||||||
|
aria-labelledby="postura-titulo"
|
||||||
|
>
|
||||||
<div className="shell">
|
<div className="shell">
|
||||||
<div className="max-w-3xl">
|
<div className="max-w-3xl">
|
||||||
<p className="font-mono text-[0.6875rem] font-medium uppercase tracking-[0.16em] text-ember">
|
<p className="font-mono text-[0.6875rem] font-medium uppercase tracking-[0.16em] text-ember">
|
||||||
Postura editorial
|
Postura editorial
|
||||||
</p>
|
</p>
|
||||||
<h2 id="postura-titulo" className="mt-4 font-display text-2xl font-extrabold sm:text-3xl">
|
<h2
|
||||||
|
id="postura-titulo"
|
||||||
|
className="mt-4 font-display text-2xl font-extrabold sm:text-3xl"
|
||||||
|
>
|
||||||
Por qué documentamos en lugar de prometer
|
Por qué documentamos en lugar de prometer
|
||||||
</h2>
|
</h2>
|
||||||
<p className="mt-5 text-base leading-relaxed text-muted">
|
<p className="mt-5 text-base leading-relaxed text-muted">
|
||||||
@@ -218,6 +211,7 @@ export default function PaginaNosotros() {
|
|||||||
<h3 className="mt-3 font-display text-base font-extrabold leading-tight text-bright">
|
<h3 className="mt-3 font-display text-base font-extrabold leading-tight text-bright">
|
||||||
{regla.titulo}
|
{regla.titulo}
|
||||||
</h3>
|
</h3>
|
||||||
|
<p className="mt-3 text-base leading-relaxed text-bright">{regla.llano}</p>
|
||||||
<p className="mt-3 text-sm leading-relaxed text-muted">{regla.texto}</p>
|
<p className="mt-3 text-sm leading-relaxed text-muted">{regla.texto}</p>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
@@ -225,23 +219,73 @@ export default function PaginaNosotros() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{/* ---------- logotipos de clientes, sin atribuirles resultado ---------- */}
|
||||||
|
<section
|
||||||
|
className="grid-bg-lite border-t border-rule bg-paper py-14 sm:py-16"
|
||||||
|
aria-labelledby="clientes-titulo"
|
||||||
|
>
|
||||||
|
<div className="shell">
|
||||||
|
<Revelar>
|
||||||
|
<p className="eyebrow mb-4">Logotipos publicados por la empresa</p>
|
||||||
|
<h2
|
||||||
|
id="clientes-titulo"
|
||||||
|
className="max-w-2xl font-display text-2xl font-extrabold text-deep sm:text-3xl"
|
||||||
|
>
|
||||||
|
Marcas que aparecen en la comunicación de FSPM
|
||||||
|
</h2>
|
||||||
|
<p className="mt-4 max-w-2xl text-sm leading-relaxed text-deep-muted sm:text-base">
|
||||||
|
Se muestran como logotipos y nada más. No les atribuimos testimonio, resultado ni
|
||||||
|
periodo: mientras la relación no esté confirmada por escrito con cada empresa,
|
||||||
|
presentarlas como caso de éxito sería exactamente lo que este sitio no hace.
|
||||||
|
</p>
|
||||||
|
</Revelar>
|
||||||
|
|
||||||
|
<ul className="mt-9 grid grid-cols-2 gap-3 sm:grid-cols-4 lg:grid-cols-6">
|
||||||
|
{CLIENTES.map((cliente) => (
|
||||||
|
<li
|
||||||
|
key={cliente.nombre}
|
||||||
|
className="flex h-20 items-center justify-center rounded-[3px] border border-rule-lite bg-white px-4"
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src={cliente.logo}
|
||||||
|
alt={cliente.nombre}
|
||||||
|
width={104}
|
||||||
|
height={36}
|
||||||
|
sizes="104px"
|
||||||
|
className="max-h-9 w-auto object-contain opacity-70"
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
{/* ---------- Christmas Hero, sin exagerarlo ---------- */}
|
{/* ---------- Christmas Hero, sin exagerarlo ---------- */}
|
||||||
<section className="grid-bg-lite border-t border-rule bg-paper py-14 sm:py-20" aria-labelledby="prensa-titulo">
|
<section
|
||||||
|
className="border-t border-rule bg-ink py-14 sm:py-20"
|
||||||
|
aria-labelledby="prensa-titulo"
|
||||||
|
>
|
||||||
<div className="shell">
|
<div className="shell">
|
||||||
<div className="grid gap-10 lg:grid-cols-[1fr_1fr] lg:gap-16">
|
<div className="grid gap-10 lg:grid-cols-[1fr_1fr] lg:gap-16">
|
||||||
<div>
|
<div>
|
||||||
<Revelar>
|
<Revelar>
|
||||||
<p className="eyebrow mb-4">Antecedente de comunicación</p>
|
<p className="eyebrow mb-4">Antecedente de comunicación</p>
|
||||||
<h2 id="prensa-titulo" className="font-display text-2xl font-extrabold text-deep sm:text-3xl">
|
<h2
|
||||||
|
id="prensa-titulo"
|
||||||
|
className="font-display text-2xl font-extrabold sm:text-3xl"
|
||||||
|
>
|
||||||
«Christmas Hero»
|
«Christmas Hero»
|
||||||
</h2>
|
</h2>
|
||||||
</Revelar>
|
</Revelar>
|
||||||
<div className="mt-5 space-y-4 text-sm leading-relaxed text-deep-muted sm:text-base">
|
<div className="mt-5 space-y-4 text-sm leading-relaxed text-muted sm:text-base">
|
||||||
|
<p className="text-base text-bright">
|
||||||
|
Un adorno navideño que apagaba el fuego de un árbol de Navidad. Salió en prensa y
|
||||||
|
demuestra algo que importa aquí: la empresa sabe explicar lo técnico.
|
||||||
|
</p>
|
||||||
<p>
|
<p>
|
||||||
En 2019, {EMPRESA.nombreCorto} desarrolló una pieza de comunicación llamada
|
En 2019, {EMPRESA.nombreCorto} desarrolló esa pieza de comunicación. Fue publicada
|
||||||
Christmas Hero: un adorno navideño que funcionaba como dispositivo de extinción
|
por LatinSpots en 2019 y obtuvo una mención como segundo finalista de El Ojo Local
|
||||||
para árboles de Navidad. Fue publicada por LatinSpots en 2019 y obtuvo una mención
|
México.
|
||||||
como segundo finalista de El Ojo Local México.
|
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Lo decimos con esa medida, sin convertirlo en más de lo que fue. Importa por dos
|
Lo decimos con esa medida, sin convertirlo en más de lo que fue. Importa por dos
|
||||||
@@ -257,7 +301,7 @@ export default function PaginaNosotros() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-mono text-[0.6875rem] uppercase tracking-[0.16em] text-deep-muted">
|
<h3 className="font-mono text-[0.6875rem] uppercase tracking-[0.16em] text-faint">
|
||||||
Publicaciones que han cubierto a FSPM
|
Publicaciones que han cubierto a FSPM
|
||||||
</h3>
|
</h3>
|
||||||
<ul className="mt-6 grid grid-cols-2 gap-3 sm:grid-cols-3">
|
<ul className="mt-6 grid grid-cols-2 gap-3 sm:grid-cols-3">
|
||||||
@@ -267,7 +311,7 @@ export default function PaginaNosotros() {
|
|||||||
href={medio.url}
|
href={medio.url}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="flex h-20 items-center justify-center rounded-[3px] border border-rule-lite bg-white px-4 transition-colors duration-200 hover:border-signal/60 [-webkit-tap-highlight-color:transparent]"
|
className="flex h-20 items-center justify-center rounded-[3px] border border-rule bg-ink-2 px-4 transition-colors duration-200 hover:border-signal/60 [-webkit-tap-highlight-color:transparent]"
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
src={medio.logo}
|
src={medio.logo}
|
||||||
@@ -281,37 +325,73 @@ export default function PaginaNosotros() {
|
|||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
<p className="mt-5 text-xs leading-relaxed text-deep-muted">
|
<p className="mt-5 text-xs leading-relaxed text-faint">
|
||||||
Los enlaces llevan a la nota original de cada medio. Varias corresponden a la cobertura
|
Los enlaces llevan a la nota original de cada medio. Varias corresponden a la
|
||||||
de Christmas Hero; aparecen como cobertura periodística, no como respaldo del
|
cobertura de Christmas Hero; aparecen como cobertura periodística, no como respaldo
|
||||||
desempeño de un producto.
|
del desempeño de un producto.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* ---------- salidas ---------- */}
|
{/* ---------- conversión ---------- */}
|
||||||
<section className="border-t border-rule bg-ink-2 py-14 sm:py-16">
|
<section
|
||||||
|
id="hablar"
|
||||||
|
className="grid-bg-lite border-t border-rule bg-paper py-14 sm:py-20"
|
||||||
|
aria-labelledby="hablar-titulo"
|
||||||
|
>
|
||||||
<div className="shell">
|
<div className="shell">
|
||||||
<div className="max-w-3xl">
|
<div className="grid gap-10 lg:grid-cols-[1fr_1.05fr] lg:items-start lg:gap-16">
|
||||||
<Revelar>
|
<div>
|
||||||
<p className="eyebrow mb-4">Siguiente paso</p>
|
<Revelar>
|
||||||
<h2 className="font-display text-2xl font-extrabold sm:text-3xl">
|
<p className="eyebrow mb-4">Siguiente paso</p>
|
||||||
Lo que sigue depende de qué tan urgente sea tu caso
|
<h2
|
||||||
</h2>
|
id="hablar-titulo"
|
||||||
</Revelar>
|
className="font-display text-2xl font-extrabold text-deep sm:text-3xl"
|
||||||
<div className="mt-8 flex flex-wrap gap-3">
|
>
|
||||||
<Link href="/contacto" className="btn btn-signal">
|
Cuéntanos qué hay que proteger
|
||||||
Hablar con nosotros
|
</h2>
|
||||||
</Link>
|
<p className="mt-4 text-sm leading-relaxed text-deep-muted sm:text-base">
|
||||||
<Link href="/catalogo" className="btn btn-ghost">
|
No necesitas llegar con un requerimiento técnico redactado. Con el tipo de
|
||||||
Ver el catálogo
|
instalación y lo que te preocupa es suficiente para empezar: la primera respuesta
|
||||||
</Link>
|
ya trae la selección propuesta y qué documentación le corresponde.
|
||||||
<Link href="/evidencia" className="btn btn-ghost">
|
</p>
|
||||||
Centro de evidencia
|
</Revelar>
|
||||||
</Link>
|
|
||||||
|
<hr className="hair-lite my-8" />
|
||||||
|
|
||||||
|
<h3 className="font-mono text-[0.6875rem] uppercase tracking-[0.16em] text-deep-muted">
|
||||||
|
O empieza por donde te sirva más
|
||||||
|
</h3>
|
||||||
|
<ul className="mt-4 flex flex-wrap gap-2">
|
||||||
|
{[
|
||||||
|
{ href: "/catalogo", label: "Catálogo" },
|
||||||
|
{ href: "/cumplimiento-nom-002", label: "Cumplimiento NOM-002" },
|
||||||
|
{ href: "/evidencia", label: "Documentación técnica" },
|
||||||
|
{ href: "/preguntas-frecuentes", label: "Preguntas frecuentes" },
|
||||||
|
].map((enlace) => (
|
||||||
|
<li key={enlace.href}>
|
||||||
|
<Link
|
||||||
|
href={enlace.href}
|
||||||
|
className="badge !border-rule-lite !text-deep-muted hover:!border-signal hover:!text-deep [-webkit-tap-highlight-color:transparent]"
|
||||||
|
>
|
||||||
|
{enlace.label}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<FormularioContacto
|
||||||
|
id="formulario-nosotros"
|
||||||
|
canal="catalogo"
|
||||||
|
variante="claro"
|
||||||
|
titulo="Hablar con un especialista"
|
||||||
|
descripcion="Completa la frase del mensaje con lo que necesitas resolver. Te contesta una persona con nombre, no un buzón."
|
||||||
|
mensajeInicial={MENSAJE_INICIAL}
|
||||||
|
textoBoton="Solicitar contacto"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
+117
-104
@@ -1,5 +1,7 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { FormularioContacto } from "@/components/formulario/FormularioContacto";
|
||||||
|
import { FOTOS, HeroeLanding } from "@/components/site/HeroeLanding";
|
||||||
import { CATEGORIAS } from "@/data/catalogo";
|
import { CATEGORIAS } from "@/data/catalogo";
|
||||||
import { EMPRESA } from "@/data/empresa";
|
import { EMPRESA } from "@/data/empresa";
|
||||||
import { INDUSTRIAS } from "@/data/industrias";
|
import { INDUSTRIAS } from "@/data/industrias";
|
||||||
@@ -7,126 +9,137 @@ import { INDUSTRIAS } from "@/data/industrias";
|
|||||||
/**
|
/**
|
||||||
* 404 con la voz del sitio: dice qué pasó y ofrece salidas reales.
|
* 404 con la voz del sitio: dice qué pasó y ofrece salidas reales.
|
||||||
* Sin humor, sin disculpas y sin ilustración de astronauta perdido.
|
* Sin humor, sin disculpas y sin ilustración de astronauta perdido.
|
||||||
|
*
|
||||||
|
* Lleva héroe con fotografía como el resto de las páginas, y cierra con el
|
||||||
|
* formulario: si alguien llegó aquí buscando algo, decir qué buscaba es una
|
||||||
|
* salida tan válida como el índice.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Página no encontrada",
|
title: "Página no encontrada",
|
||||||
description:
|
description:
|
||||||
"La dirección solicitada no existe en este sitio. Rutas disponibles: catálogo, soluciones por industria, cumplimiento y contacto.",
|
"La dirección solicitada no existe en este sitio de FSPM. Estas son las rutas que sí funcionan: catálogo de equipos contra incendio, soluciones por industria, cumplimiento NOM-002, documentación técnica y contacto.",
|
||||||
robots: { index: false, follow: true },
|
robots: { index: false, follow: true },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const MENSAJE_INICIAL = "Llegué a una página que no existe. Lo que estaba buscando es ";
|
||||||
|
|
||||||
|
const RECURSOS = [
|
||||||
|
{ href: "/cumplimiento-nom-002", label: "Cumplimiento NOM-002" },
|
||||||
|
{ href: "/evidencia", label: "Documentación técnica" },
|
||||||
|
{ href: "/preguntas-frecuentes", label: "Preguntas frecuentes" },
|
||||||
|
{ href: "/casos", label: "Casos de aplicación" },
|
||||||
|
{ href: "/nosotros", label: "Quiénes somos" },
|
||||||
|
{ href: "/aviso-de-privacidad", label: "Aviso de privacidad" },
|
||||||
|
];
|
||||||
|
|
||||||
export default function NoEncontrado() {
|
export default function NoEncontrado() {
|
||||||
return (
|
return (
|
||||||
<section
|
<>
|
||||||
className="grid-bg flex min-h-[70vh] flex-col justify-center bg-ink py-16 sm:py-24"
|
<HeroeLanding
|
||||||
// 100dvh-style respaldo: si el navegador no entiende dvh, descarta esta
|
eyebrow="Error 404"
|
||||||
// declaración y se queda con el 70vh de la clase.
|
titulo="Esta dirección no existe en el sitio"
|
||||||
style={{ minHeight: "70dvh" }}
|
entrada="Puede que la página se haya movido, que el enlace venga incompleto o que la dirección nunca haya existido. Abajo están las rutas que sí funcionan."
|
||||||
>
|
foto={FOTOS.portadaGaleria}
|
||||||
<div className="shell">
|
enfoque="50% 40%"
|
||||||
<div className="grid gap-12 lg:grid-cols-[1fr_1fr] lg:gap-16">
|
altura="alto"
|
||||||
<div>
|
prioridad
|
||||||
<p className="font-mono text-[0.6875rem] font-medium uppercase tracking-[0.18em] text-signal">
|
>
|
||||||
Error 404
|
<div className="flex flex-col gap-4 sm:flex-row sm:items-center">
|
||||||
</p>
|
<Link href="/catalogo" className="btn btn-signal w-full sm:w-auto">
|
||||||
|
Ir al catálogo
|
||||||
|
</Link>
|
||||||
|
<p className="text-sm leading-relaxed text-muted">
|
||||||
|
Si necesitas una respuesta ahora, marca al{" "}
|
||||||
|
<a href={`tel:${EMPRESA.telefonoTel}`} className="link">
|
||||||
|
{EMPRESA.telefono}
|
||||||
|
</a>
|
||||||
|
.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</HeroeLanding>
|
||||||
|
|
||||||
<h1 className="mt-5 font-display text-[2rem] font-black uppercase leading-[1.03] tracking-tight sm:text-[2.75rem] lg:text-5xl">
|
{/* ---------- índice de rutas reales ---------- */}
|
||||||
Esta dirección no existe en el sitio
|
<section
|
||||||
</h1>
|
className="grid-bg-lite border-t border-rule bg-paper py-14 sm:py-20"
|
||||||
|
aria-labelledby="indice-titulo"
|
||||||
|
>
|
||||||
|
<div className="shell">
|
||||||
|
<div className="grid gap-10 lg:grid-cols-[1.1fr_1fr] lg:gap-16">
|
||||||
|
<div>
|
||||||
|
<p className="eyebrow mb-4">Índice del sitio</p>
|
||||||
|
<h2
|
||||||
|
id="indice-titulo"
|
||||||
|
className="font-display text-2xl font-extrabold text-deep sm:text-3xl"
|
||||||
|
>
|
||||||
|
Rutas que sí funcionan
|
||||||
|
</h2>
|
||||||
|
|
||||||
<p className="mt-6 max-w-xl text-base leading-relaxed text-muted sm:text-lg">
|
<h3 className="mt-8 font-mono text-[0.6875rem] uppercase tracking-[0.16em] text-deep-muted">
|
||||||
Puede que la página se haya movido, que el enlace venga incompleto o que la dirección
|
Categorías del catálogo
|
||||||
nunca haya existido. Estas son las rutas que sí funcionan.
|
</h3>
|
||||||
</p>
|
<ul className="mt-4 grid gap-2 sm:grid-cols-2">
|
||||||
|
{CATEGORIAS.map((categoria) => (
|
||||||
|
<li key={categoria.slug}>
|
||||||
|
<Link
|
||||||
|
href={`/catalogo/${categoria.slug}`}
|
||||||
|
className="block text-sm leading-snug text-deep underline decoration-rule-lite underline-offset-4 transition-colors hover:decoration-signal [-webkit-tap-highlight-color:transparent]"
|
||||||
|
>
|
||||||
|
{categoria.nombre}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
|
||||||
<div className="mt-9 flex flex-wrap gap-3">
|
<hr className="hair-lite my-7" />
|
||||||
<Link href="/catalogo" className="btn btn-signal">
|
|
||||||
Ir al catálogo
|
<h3 className="font-mono text-[0.6875rem] uppercase tracking-[0.16em] text-deep-muted">
|
||||||
</Link>
|
Soluciones por operación
|
||||||
<Link href="/soluciones" className="btn btn-ghost">
|
</h3>
|
||||||
Ver soluciones
|
<ul className="mt-4 grid gap-2 sm:grid-cols-2">
|
||||||
</Link>
|
{INDUSTRIAS.map((industria) => (
|
||||||
<Link href="/contacto" className="btn btn-ghost">
|
<li key={industria.slug}>
|
||||||
Contacto
|
<Link
|
||||||
</Link>
|
href={`/soluciones/${industria.slug}`}
|
||||||
|
className="block text-sm leading-snug text-deep underline decoration-rule-lite underline-offset-4 transition-colors hover:decoration-signal [-webkit-tap-highlight-color:transparent]"
|
||||||
|
>
|
||||||
|
{industria.nombre}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<hr className="hair-lite my-7" />
|
||||||
|
|
||||||
|
<h3 className="font-mono text-[0.6875rem] uppercase tracking-[0.16em] text-deep-muted">
|
||||||
|
Recursos
|
||||||
|
</h3>
|
||||||
|
<ul className="mt-4 grid gap-2 sm:grid-cols-2">
|
||||||
|
{RECURSOS.map((enlace) => (
|
||||||
|
<li key={enlace.href}>
|
||||||
|
<Link
|
||||||
|
href={enlace.href}
|
||||||
|
className="block text-sm leading-snug text-deep underline decoration-rule-lite underline-offset-4 transition-colors hover:decoration-signal [-webkit-tap-highlight-color:transparent]"
|
||||||
|
>
|
||||||
|
{enlace.label}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr className="hair my-9" />
|
<FormularioContacto
|
||||||
|
id="formulario-404"
|
||||||
<p className="text-sm leading-relaxed text-muted">
|
canal="catalogo"
|
||||||
Si necesitas una respuesta ahora, marca al{" "}
|
variante="claro"
|
||||||
<a href={`tel:${EMPRESA.telefonoTel}`} className="link">
|
titulo="Dinos qué estabas buscando"
|
||||||
{EMPRESA.telefono}
|
descripcion="Si el enlace venía de un correo, de una cotización o de un buscador, completa la frase del mensaje y te lo mandamos directo."
|
||||||
</a>
|
mensajeInicial={MENSAJE_INICIAL}
|
||||||
.
|
textoBoton="Enviar"
|
||||||
</p>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="panel p-6 sm:p-7">
|
|
||||||
<p className="eyebrow eyebrow-muted mb-5">Índice rápido</p>
|
|
||||||
|
|
||||||
<h2 className="font-mono text-[0.6875rem] uppercase tracking-[0.16em] text-faint">
|
|
||||||
Categorías del catálogo
|
|
||||||
</h2>
|
|
||||||
<ul className="mt-4 grid gap-2 sm:grid-cols-2">
|
|
||||||
{CATEGORIAS.map((categoria) => (
|
|
||||||
<li key={categoria.slug}>
|
|
||||||
<Link
|
|
||||||
href={`/catalogo/${categoria.slug}`}
|
|
||||||
className="block text-sm leading-snug text-bright underline decoration-rule-2 underline-offset-4 transition-colors hover:decoration-signal [-webkit-tap-highlight-color:transparent]"
|
|
||||||
>
|
|
||||||
{categoria.nombre}
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<hr className="hair my-6" />
|
|
||||||
|
|
||||||
<h2 className="font-mono text-[0.6875rem] uppercase tracking-[0.16em] text-faint">
|
|
||||||
Soluciones por operación
|
|
||||||
</h2>
|
|
||||||
<ul className="mt-4 grid gap-2 sm:grid-cols-2">
|
|
||||||
{INDUSTRIAS.map((industria) => (
|
|
||||||
<li key={industria.slug}>
|
|
||||||
<Link
|
|
||||||
href={`/soluciones/${industria.slug}`}
|
|
||||||
className="block text-sm leading-snug text-bright underline decoration-rule-2 underline-offset-4 transition-colors hover:decoration-signal [-webkit-tap-highlight-color:transparent]"
|
|
||||||
>
|
|
||||||
{industria.nombre}
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<hr className="hair my-6" />
|
|
||||||
|
|
||||||
<h2 className="font-mono text-[0.6875rem] uppercase tracking-[0.16em] text-faint">
|
|
||||||
Recursos
|
|
||||||
</h2>
|
|
||||||
<ul className="mt-4 grid gap-2 sm:grid-cols-2">
|
|
||||||
{[
|
|
||||||
{ href: "/cumplimiento-nom-002", label: "Cumplimiento NOM-002" },
|
|
||||||
{ href: "/evidencia", label: "Centro de evidencia" },
|
|
||||||
{ href: "/preguntas-frecuentes", label: "Preguntas frecuentes" },
|
|
||||||
{ href: "/casos", label: "Casos de aplicación" },
|
|
||||||
{ href: "/nosotros", label: "Quiénes somos" },
|
|
||||||
{ href: "/aviso-de-privacidad", label: "Aviso de privacidad" },
|
|
||||||
].map((enlace) => (
|
|
||||||
<li key={enlace.href}>
|
|
||||||
<Link
|
|
||||||
href={enlace.href}
|
|
||||||
className="block text-sm leading-snug text-bright underline decoration-rule-2 underline-offset-4 transition-colors hover:decoration-signal [-webkit-tap-highlight-color:transparent]"
|
|
||||||
>
|
|
||||||
{enlace.label}
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
</section>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+51
-17
@@ -2,38 +2,71 @@ import type { Metadata } from "next";
|
|||||||
import { BloqueDiagnostico } from "@/components/site/home/BloqueDiagnostico";
|
import { BloqueDiagnostico } from "@/components/site/home/BloqueDiagnostico";
|
||||||
import { BloqueNorma } from "@/components/site/home/BloqueNorma";
|
import { BloqueNorma } from "@/components/site/home/BloqueNorma";
|
||||||
import { CatalogoDestacado } from "@/components/site/home/CatalogoDestacado";
|
import { CatalogoDestacado } from "@/components/site/home/CatalogoDestacado";
|
||||||
|
import { CategoriasCatalogo } from "@/components/site/home/CategoriasCatalogo";
|
||||||
import { ClasesFuego } from "@/components/site/home/ClasesFuego";
|
import { ClasesFuego } from "@/components/site/home/ClasesFuego";
|
||||||
import { Clientes } from "@/components/site/home/Clientes";
|
import { Clientes } from "@/components/site/home/Clientes";
|
||||||
|
import { EntradaEscenario } from "@/components/site/home/EntradaEscenario";
|
||||||
import { Heroe } from "@/components/site/home/Heroe";
|
import { Heroe } from "@/components/site/home/Heroe";
|
||||||
import { Prensa } from "@/components/site/home/Prensa";
|
import { Prensa } from "@/components/site/home/Prensa";
|
||||||
import { RutasIndustria } from "@/components/site/home/RutasIndustria";
|
import { RutasIndustria } from "@/components/site/home/RutasIndustria";
|
||||||
import { TresRutas } from "@/components/site/home/TresRutas";
|
|
||||||
import { EMPRESA, SITIO_URL } from "@/data/empresa";
|
import { EMPRESA, SITIO_URL } from "@/data/empresa";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PÁGINA DE INICIO.
|
* PÁGINA DE INICIO — su trabajo es llevar al catálogo.
|
||||||
*
|
*
|
||||||
* Es la implementación de la recomendación 10.1 del estudio de marca: rehacer
|
* Encargo literal del cliente: "la página está enfocada en que vean el
|
||||||
* el inicio con tres rutas — industria, tipo de riesgo y cumplimiento — en
|
* catálogo. Optimiza el home para que vean el catálogo, con botones y
|
||||||
* lugar de un escaparate de ocho categorías que obliga al visitante a inferir
|
* optimizaciones de funnel para ver el catálogo. La navegación por áreas es
|
||||||
* qué solución le corresponde.
|
* secundaria."
|
||||||
*
|
*
|
||||||
* El orden de las secciones no es estético, es el recorrido de un comprador:
|
* Eso choca de frente con la recomendación 10.1 del estudio de marca, que pidió
|
||||||
* clasificar el riesgo → elegir la ruta → entender el criterio técnico → ver
|
* rutas por industria justamente para que el sitio no fuera "un catálogo grande
|
||||||
* el equipo → entender la obligación normativa → pedir el diagnóstico → juzgar
|
* y ya". La síntesis no es elegir: es que la navegación por áreas deje de ser un
|
||||||
* a quién le está hablando.
|
* destino y pase a ser **la puerta al catálogo ya filtrado**. Escenarios,
|
||||||
|
* industrias y clases de fuego siguen cualificando al visitante, siguen
|
||||||
|
* cargando la densidad técnica que posiciona en búsqueda, y ahora todas
|
||||||
|
* desembocan en `/catalogo?escenario=…`, `?clases=…` o `?categoria=…`. El
|
||||||
|
* catálogo sabe leer esos parámetros; la traducción vive en
|
||||||
|
* `src/components/site/home/enlaces.ts`.
|
||||||
|
*
|
||||||
|
* ---- El orden, y por qué ----
|
||||||
|
*
|
||||||
|
* 1 Heroe un botón rojo al catálogo con la cifra de equipos, y
|
||||||
|
* el clasificador como atajo al catálogo filtrado
|
||||||
|
* 2 CategoriasCatalogo las 8 categorías como filtro. Ocupa la posición que
|
||||||
|
* tenían las rutas por industria: el equipo aparece
|
||||||
|
* antes de cualquier página de lectura
|
||||||
|
* 3 CatalogoDestacado 6 equipos con la misma tarjeta que usa /catalogo
|
||||||
|
* 4 EntradaEscenario 13 lugares reconocibles → catálogo filtrado
|
||||||
|
* 5 ClasesFuego el criterio técnico; cada clase → catálogo filtrado
|
||||||
|
* 6 RutasIndustria compacta, y con el único enlace a /soluciones
|
||||||
|
* 7 BloqueNorma la obligación normativa, resuelta ahí mismo
|
||||||
|
* 8 BloqueDiagnostico el formulario, para quien no quiere navegar catálogo
|
||||||
|
* 9 Clientes · Prensa a quién le está hablando
|
||||||
|
*
|
||||||
|
* Disciplina de embudo: un solo llamado primario por sección, y en las
|
||||||
|
* secciones de producto ese llamado va al catálogo o a una ficha, nunca a una
|
||||||
|
* página de lectura. Las salidas secundarias son enlaces de texto, no botones.
|
||||||
|
* Ninguna sección de venta enlaza a `/cumplimiento-nom-002`, `/evidencia` ni
|
||||||
|
* `/preguntas-frecuentes`: el argumento normativo se cierra en dos frases donde
|
||||||
|
* hace falta.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
const TITULO = "Extintores y protección contra incendios en México";
|
||||||
|
|
||||||
const DESCRIPCION =
|
const DESCRIPCION =
|
||||||
"Extintores, agentes espumógenos y sistemas fijos, móviles y automáticos para industria, energía, logística, gasolineras, cocinas y PyMEs. Clasificamos el riesgo, seleccionamos el agente y entregamos la evidencia que exige la NOM-002-STPS-2010.";
|
"Extintores, espumas y sistemas fijos, móviles y automáticos para gasolineras, bodegas, cocinas industriales, plantas, minas y oficinas. Entra por el lugar que operas o por clase de fuego (A, B, C, D, K): te decimos qué equipo te toca, cuánto y qué documentar para la NOM-002-STPS-2010.";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: `${EMPRESA.promesa} | ${EMPRESA.nombreCorto}`,
|
title: { absolute: `${TITULO} | ${EMPRESA.nombreCorto}` },
|
||||||
description: DESCRIPCION,
|
description: DESCRIPCION,
|
||||||
alternates: { canonical: "/" },
|
alternates: { canonical: "/" },
|
||||||
keywords: [
|
keywords: [
|
||||||
"protección contra incendios",
|
"protección contra incendios",
|
||||||
"extintores industriales",
|
"extintores industriales",
|
||||||
|
"extintor para gasolinera",
|
||||||
|
"extintor para cocina industrial",
|
||||||
|
"protección contra incendios en bodega",
|
||||||
"NOM-002-STPS-2010",
|
"NOM-002-STPS-2010",
|
||||||
"agente espumógeno",
|
"agente espumógeno",
|
||||||
"sistema contra incendio",
|
"sistema contra incendio",
|
||||||
@@ -44,7 +77,7 @@ export const metadata: Metadata = {
|
|||||||
locale: "es_MX",
|
locale: "es_MX",
|
||||||
url: SITIO_URL,
|
url: SITIO_URL,
|
||||||
siteName: EMPRESA.nombre,
|
siteName: EMPRESA.nombre,
|
||||||
title: `${EMPRESA.promesa} | ${EMPRESA.nombreCorto}`,
|
title: `${TITULO} | ${EMPRESA.nombreCorto}`,
|
||||||
description: DESCRIPCION,
|
description: DESCRIPCION,
|
||||||
images: [
|
images: [
|
||||||
{
|
{
|
||||||
@@ -57,7 +90,7 @@ export const metadata: Metadata = {
|
|||||||
},
|
},
|
||||||
twitter: {
|
twitter: {
|
||||||
card: "summary_large_image",
|
card: "summary_large_image",
|
||||||
title: `${EMPRESA.promesa} | ${EMPRESA.nombreCorto}`,
|
title: `${TITULO} | ${EMPRESA.nombreCorto}`,
|
||||||
description: DESCRIPCION,
|
description: DESCRIPCION,
|
||||||
images: ["/media/foto/bombero-plataforma.jpg"],
|
images: ["/media/foto/bombero-plataforma.jpg"],
|
||||||
},
|
},
|
||||||
@@ -106,10 +139,11 @@ export default function PaginaInicio() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Heroe />
|
<Heroe />
|
||||||
<TresRutas />
|
<CategoriasCatalogo />
|
||||||
<RutasIndustria />
|
|
||||||
<ClasesFuego />
|
|
||||||
<CatalogoDestacado />
|
<CatalogoDestacado />
|
||||||
|
<EntradaEscenario />
|
||||||
|
<ClasesFuego />
|
||||||
|
<RutasIndustria />
|
||||||
<BloqueNorma />
|
<BloqueNorma />
|
||||||
<BloqueDiagnostico />
|
<BloqueDiagnostico />
|
||||||
<Clientes />
|
<Clientes />
|
||||||
|
|||||||
@@ -1,23 +1,32 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { FormularioContacto } from "@/components/formulario/FormularioContacto";
|
||||||
|
import { TarjetaEscenario } from "@/components/marca/IconoEscenario";
|
||||||
|
import { Migas } from "@/components/site/contenido/Migas";
|
||||||
|
import { FOTOS, HeroeLanding } from "@/components/site/HeroeLanding";
|
||||||
import { Revelar } from "@/components/site/Revelar";
|
import { Revelar } from "@/components/site/Revelar";
|
||||||
import { EMPRESA, NORMAS, SITIO_URL } from "@/data/empresa";
|
import { EMPRESA, NORMAS, SITIO_URL } from "@/data/empresa";
|
||||||
|
import { ESCENARIOS, type Escenario } from "@/data/escenarios";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* /preguntas-frecuentes
|
* /preguntas-frecuentes
|
||||||
*
|
*
|
||||||
|
* Aquí llega quien tiene miedo de preguntar. Así que cada respuesta abre con
|
||||||
|
* una frase directa —la respuesta corta, sin condescendencia— y después
|
||||||
|
* desarrolla el detalle para quien lo necesita.
|
||||||
|
*
|
||||||
* Las preguntas vienen de la página real de FSPM. Las respuestas no se copian:
|
* Las preguntas vienen de la página real de FSPM. Las respuestas no se copian:
|
||||||
* donde la fuente afirma "no caduca", "20 años de garantía" o "no requiere
|
* donde la fuente afirma "no caduca", "20 años de garantía" o "no requiere
|
||||||
* recarga", la respuesta explica qué documento respalda eso y bajo qué
|
* recarga", la respuesta explica qué documento respalda eso y bajo qué
|
||||||
* condiciones. Es el mismo criterio del resto del sitio.
|
* condiciones. Es el mismo criterio del resto del sitio.
|
||||||
*
|
*
|
||||||
* El JSON-LD FAQPage se construye desde el mismo arreglo que se renderiza, así
|
* El JSON-LD FAQPage se construye desde el mismo arreglo que se renderiza —
|
||||||
* que no puede desincronizarse del contenido visible.
|
* respuesta corta incluida—, así que no puede desincronizarse de lo visible.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const TITULO = "Preguntas frecuentes";
|
const TITULO = "Preguntas frecuentes sobre extintores y FireAde 2000";
|
||||||
const DESCRIPCION =
|
const DESCRIPCION =
|
||||||
"Respuestas verificables sobre FireAde 2000, vida útil, garantía, recarga, clases de fuego, certificaciones y documentación. Qué documento respalda cada afirmación y bajo qué condiciones.";
|
"Respuestas directas sobre vida útil, garantía, recarga, clases de fuego, certificaciones UL y NFPA, precio y documentación. Cada respuesta dice qué documento la respalda y bajo qué condiciones.";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: TITULO,
|
title: TITULO,
|
||||||
@@ -30,9 +39,13 @@ export const metadata: Metadata = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const MENSAJE_INICIAL = "Tengo una duda que no está en las preguntas frecuentes: ";
|
||||||
|
|
||||||
type Pregunta = {
|
type Pregunta = {
|
||||||
id: string;
|
id: string;
|
||||||
pregunta: string;
|
pregunta: string;
|
||||||
|
/** La respuesta en una frase. Lo primero que se lee al abrir. */
|
||||||
|
corta: string;
|
||||||
/** Párrafos en texto plano: alimentan la página y el JSON-LD. */
|
/** Párrafos en texto plano: alimentan la página y el JSON-LD. */
|
||||||
respuesta: string[];
|
respuesta: string[];
|
||||||
};
|
};
|
||||||
@@ -54,26 +67,32 @@ const GRUPOS: Grupo[] = [
|
|||||||
{
|
{
|
||||||
id: "caduca",
|
id: "caduca",
|
||||||
pregunta: "¿Es cierto que el equipo no caduca?",
|
pregunta: "¿Es cierto que el equipo no caduca?",
|
||||||
|
corta:
|
||||||
|
"El agente puede durar muchísimo; el recipiente que lo contiene sigue necesitando revisión. Son dos cosas distintas y conviene no mezclarlas.",
|
||||||
respuesta: [
|
respuesta: [
|
||||||
"La información pública de FSPM afirma que FireAde 2000 tiene vida de anaquel ilimitada y que conserva sus propiedades incluso mezclado con agua. No repetimos esa afirmación como característica del producto porque una vida útil se demuestra con un documento, no con una frase.",
|
"La información pública de FSPM afirma que FireAde 2000 tiene vida de anaquel ilimitada y que conserva sus propiedades incluso mezclado con agua. No repetimos esa afirmación como característica del producto porque una vida útil se demuestra con un documento, no con una frase.",
|
||||||
"Lo que respalda una respuesta seria es la carta técnica del fabricante referida al número de modelo, con las condiciones de almacenamiento y las circunstancias que la invalidan. Eso lo entregamos antes de cotizar.",
|
"Lo que respalda una respuesta seria es la carta técnica del fabricante referida al número de modelo, con las condiciones de almacenamiento y las circunstancias que la invalidan. Eso lo entregamos antes de cotizar.",
|
||||||
"Además, conviene separar dos cosas que suelen mezclarse: la vida útil del agente y el mantenimiento del equipo que lo contiene. Aunque el agente conservara sus propiedades, el recipiente, la válvula y el manómetro siguen sujetos a revisión.",
|
"Y aunque el agente conservara intactas sus propiedades, el recipiente, la válvula y el manómetro siguen sujetos a revisión periódica. Un equipo con agente vigente y válvula vencida no pasa una verificación.",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "recarga",
|
id: "recarga",
|
||||||
pregunta: "¿Entonces no requiere recarga?",
|
pregunta: "¿Entonces no requiere recarga?",
|
||||||
|
corta:
|
||||||
|
"Puede que no necesites recargarlo cada año, pero sí revisarlo. La revisión no desaparece nunca.",
|
||||||
respuesta: [
|
respuesta: [
|
||||||
"La fuente lo presenta así, pero no especifica si aplica al agente, al extintor completo, ni bajo qué condiciones de mantenimiento. Esa distinción es justamente la que decide si tu expediente aguanta una verificación.",
|
"La fuente presenta la ausencia de recarga como característica en varias páginas de producto, y en la de rociadores lo matiza ella misma: dice que al usarse con FireAde 2000 no necesita recarga anual porque el agente no tiene fecha de caducidad, pero que igual se recomienda una revisión anual.",
|
||||||
"Nuestra respuesta es documental: confirmamos por escrito a qué se refiere exactamente la ausencia de recarga en tu modelo y qué obligaciones de mantenimiento subsisten.",
|
"Ese matiz es la respuesta. Confirmamos por escrito a qué se refiere exactamente la ausencia de recarga en tu modelo —al agente, al equipo completo, bajo qué condiciones— y qué obligaciones de mantenimiento subsisten.",
|
||||||
"En todo caso, la revisión periódica del equipo no desaparece. La NOM-154-SCFI-2005, referida por la NOM-002, establece que la revisión de los extintores debe hacerse cada mes.",
|
"En todo caso, la revisión periódica no desaparece. La NOM-154-SCFI-2005, referida por la NOM-002, establece que la revisión de los extintores debe hacerse cada mes.",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "garantia",
|
id: "garantia",
|
||||||
pregunta: "¿La garantía es de 20 años o de 15?",
|
pregunta: "¿La garantía es de 20 años o de 15?",
|
||||||
|
corta:
|
||||||
|
"En la información pública aparecen las dos cifras, así que no publicamos ninguna: te entregamos el documento de garantía del modelo.",
|
||||||
respuesta: [
|
respuesta: [
|
||||||
"Las dos cifras aparecen en la información pública de FSPM: las páginas de producto mencionan veinte años y la sección de preguntas frecuentes menciona quince para los dispositivos de aplicación. Esa inconsistencia es la razón por la que no publicamos ninguna de las dos como dato.",
|
"Las páginas de producto mencionan veinte años y la sección de preguntas frecuentes menciona quince para los dispositivos de aplicación. Esa inconsistencia es exactamente la razón por la que no convertimos ninguna de las dos en un dato de venta.",
|
||||||
"Lo que sí entregamos es el documento de garantía del modelo, con plazo, cobertura, exclusiones y las obligaciones de mantenimiento que la conservan vigente. Un plazo sin condiciones es una cifra publicitaria, no una garantía.",
|
"Lo que sí entregamos es el documento de garantía del modelo, con plazo, cobertura, exclusiones y las obligaciones de mantenimiento que la conservan vigente. Un plazo sin condiciones es una cifra publicitaria, no una garantía.",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -88,16 +107,20 @@ const GRUPOS: Grupo[] = [
|
|||||||
{
|
{
|
||||||
id: "afff",
|
id: "afff",
|
||||||
pregunta: "¿FireAde 2000 es un espumógeno AFFF?",
|
pregunta: "¿FireAde 2000 es un espumógeno AFFF?",
|
||||||
|
corta:
|
||||||
|
"Está clasificado en esa familia, pero la etiqueta importa menos que la formulación concreta que te ofrecen.",
|
||||||
respuesta: [
|
respuesta: [
|
||||||
"La fuente responde \"sí y no\": lo describe como clasificado en la familia AFFF y, al mismo tiempo, como una evolución tecnológica frente al AFFF tradicional basado en flúor. En el catálogo aparece como FireAde 2000 AR-AFFF, con una solución al 6% para los equipos Enforcer.",
|
"La fuente responde \"sí y no\": lo describe como clasificado en la familia AFFF y, al mismo tiempo, como una evolución tecnológica frente al AFFF tradicional basado en flúor. En el catálogo aparece como FireAde 2000 AR-AFFF, con una solución al 6% para los equipos Enforcer.",
|
||||||
"Para una decisión de compra la categoría importa menos que la formulación concreta. Lo que resuelve la pregunta es la ficha técnica de la variante que se te ofrece y su hoja de datos de seguridad vigente, con la composición declarada.",
|
"Para una decisión de compra, lo que resuelve la pregunta es la ficha técnica de la variante que se te ofrece y su hoja de datos de seguridad vigente, con la composición declarada. La categoría comercial no dice qué tan bien apaga lo que tú tienes.",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "clases",
|
id: "clases",
|
||||||
pregunta: "¿Sirve para las cinco clases de fuego, A, B, C, D y K?",
|
pregunta: "¿Sirve para las cinco clases de fuego, A, B, C, D y K?",
|
||||||
|
corta:
|
||||||
|
"La fuente lo afirma de forma general, pero pone una condición en clase C. Cuando una afirmación trae condición, la condición es parte de la afirmación.",
|
||||||
respuesta: [
|
respuesta: [
|
||||||
"La fuente lo afirma de forma general, pero ella misma pone una condición: para fuegos de clase C la eficacia se declara con agua desmineralizada. Cuando una afirmación trae condición, la condición es parte de la afirmación.",
|
"Para fuegos de clase C la propia fuente declara la eficacia con agua desmineralizada. Ese detalle cambia la conversación: no es lo mismo comprar un equipo que funciona en tu tablero eléctrico que uno que funciona en un tablero con una condición de agua que tu operación no cumple.",
|
||||||
"En cada ficha de producto mostramos el riel de clases con lo que la fuente declara, no con lo que convendría declarar, y anotamos que las condiciones por clase se confirman en la ficha técnica del modelo: concentración, equipo y tipo de agua.",
|
"En cada ficha de producto mostramos el riel de clases con lo que la fuente declara, no con lo que convendría declarar, y anotamos que las condiciones por clase se confirman en la ficha técnica del modelo: concentración, equipo y tipo de agua.",
|
||||||
"Hay productos del catálogo a los que deliberadamente no les atribuimos clases de fuego, como TrakAde y VaporAde Plus, porque la fuente no los presenta como agentes de extinción.",
|
"Hay productos del catálogo a los que deliberadamente no les atribuimos clases de fuego, como TrakAde y VaporAde Plus, porque la fuente no los presenta como agentes de extinción.",
|
||||||
],
|
],
|
||||||
@@ -105,42 +128,50 @@ const GRUPOS: Grupo[] = [
|
|||||||
{
|
{
|
||||||
id: "ambiental",
|
id: "ambiental",
|
||||||
pregunta: "¿Es cierto que no es tóxico ni contaminante?",
|
pregunta: "¿Es cierto que no es tóxico ni contaminante?",
|
||||||
|
corta:
|
||||||
|
"Son afirmaciones fuertes y verificables. Por eso pedimos ver los reportes antes de repetirlas.",
|
||||||
respuesta: [
|
respuesta: [
|
||||||
"La fuente afirma que no contiene materiales tóxicos, que no es contaminante, que superó pruebas con organismos acuáticos y que su biodegradabilidad es prácticamente total en veinticinco días. Son afirmaciones fuertes y verificables, y precisamente por eso pedimos verlas antes de repetirlas.",
|
"La fuente afirma que no contiene materiales tóxicos, que no es contaminante, que superó pruebas con organismos acuáticos y que su biodegradabilidad es prácticamente total en veinticinco días.",
|
||||||
"Lo que las sostiene es la hoja de datos de seguridad vigente y los reportes de laboratorio con método, alcance y fecha, referidos a la formulación exacta. Un resultado obtenido sobre una variante no se traslada automáticamente a otra.",
|
"Lo que las sostiene es la hoja de datos de seguridad vigente y los reportes de laboratorio con método, alcance y fecha, referidos a la formulación exacta. Un resultado obtenido sobre una variante no se traslada automáticamente a otra.",
|
||||||
"Lo que no vamos a hacer es prometer que una formulación seguirá conforme a la regulación ambiental futura. Nadie puede sostener eso, y el estudio de marca lo señala como un riesgo reputacional.",
|
"Lo que no vamos a hacer es prometer que una formulación seguirá conforme a la regulación ambiental futura, como afirma la fuente original. Nadie puede sostener eso, y el estudio de marca lo señala como un riesgo reputacional.",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "enfriamiento",
|
id: "enfriamiento",
|
||||||
pregunta: "¿Se puede aplicar de forma preventiva para enfriar equipo que no está ardiendo?",
|
pregunta: "¿Se puede aplicar de forma preventiva para enfriar equipo que no está ardiendo?",
|
||||||
|
corta: "La fuente describe ese uso, pero lo evaluamos caso por caso y no lo ofrecemos como capacidad general.",
|
||||||
respuesta: [
|
respuesta: [
|
||||||
"La fuente describe ese uso, por ejemplo en transformadores y en maquinaria con carga térmica de riesgo, y menciona también instalaciones para las que no existe documentación en la extracción pública que revisamos.",
|
"La fuente menciona ese uso en transformadores y en maquinaria con carga térmica de riesgo, y también en instalaciones para las que no existe documentación en la extracción pública que revisamos.",
|
||||||
"Por eso limitamos la respuesta a lo que puede sostenerse: la aplicación preventiva de enfriamiento se evalúa caso por caso, con la ficha técnica del agente y con el criterio del responsable del equipo que se va a rociar. No la ofrecemos como una capacidad general.",
|
"Por eso limitamos la respuesta a lo que puede sostenerse: la aplicación preventiva de enfriamiento se evalúa con la ficha técnica del agente y con el criterio del responsable del equipo que se va a rociar. Si tu caso es uno de esos, dinos exactamente cuál y respondemos sobre él.",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "compatibilidad",
|
id: "compatibilidad",
|
||||||
pregunta: "¿Es compatible con mi equipo instalado y con agua salada?",
|
pregunta: "¿Es compatible con mi equipo instalado y con agua salada?",
|
||||||
|
corta:
|
||||||
|
"La fuente afirma compatibilidad con cualquier equipamiento y con todo tipo de aguas. La pregunta útil es más estrecha que eso.",
|
||||||
respuesta: [
|
respuesta: [
|
||||||
"La fuente afirma compatibilidad con cualquier equipamiento, incluidos sistemas de espuma de aire comprimido, y con todo tipo de aguas.",
|
"Lo que hay que responder no es si es compatible en general, sino si es compatible con tu equipo, del fabricante que sea, en la concentración que tu sistema dosifica.",
|
||||||
"En la práctica, la pregunta que hay que responder es más estrecha: si es compatible con tu equipo, del fabricante que sea, en la concentración que tu sistema dosifica. Eso se confirma cruzando la ficha técnica del agente con el manual de tu equipo, y es parte del trabajo previo a una propuesta.",
|
"Eso se confirma cruzando la ficha técnica del agente con el manual de tu equipo, y es parte del trabajo previo a una propuesta. Si tienes el modelo del sistema a mano, es el dato que más acelera la respuesta.",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "mezcla",
|
id: "mezcla",
|
||||||
pregunta: "¿Puedo mezclarlo con el espumógeno que ya tengo?",
|
pregunta: "¿Puedo mezclarlo con el espumógeno que ya tengo?",
|
||||||
|
corta: "No. Y la propia fuente lo desaconseja.",
|
||||||
respuesta: [
|
respuesta: [
|
||||||
"No. La propia fuente lo desaconseja, y sin un dato de compatibilidad documentado la mezcla compromete el desempeño y muy probablemente la garantía.",
|
"Sin un dato de compatibilidad documentado, la mezcla compromete el desempeño y muy probablemente la garantía.",
|
||||||
"Si tienes concentrado de otra marca en el tanque, lo correcto es planear la sustitución y el manejo del residuo, no la mezcla. La disposición del concentrado que sale es un tema técnico y de costo que conviene resolver antes de la compra, no después.",
|
"Si tienes concentrado de otra marca en el tanque, lo correcto es planear la sustitución y el manejo del residuo, no la mezcla. La disposición del concentrado que sale es un tema técnico y de costo que conviene resolver antes de la compra, no después.",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "limpieza",
|
id: "limpieza",
|
||||||
pregunta: "¿Qué implica la limpieza después de usarlo?",
|
pregunta: "¿Qué implica la limpieza después de usarlo?",
|
||||||
|
corta:
|
||||||
|
"La fuente dice que basta agua. Ten en cuenta que el residuo no es solo agente: es agente más lo que se estaba quemando.",
|
||||||
respuesta: [
|
respuesta: [
|
||||||
"La fuente indica que el agente no es corrosivo, que su pH es prácticamente neutro y que la limpieza se resuelve con agua.",
|
"La fuente indica que el agente no es corrosivo, que su pH es prácticamente neutro y que la limpieza se resuelve con agua.",
|
||||||
"El documento que respalda eso es la hoja de datos de seguridad, y es también el que dice cómo manejar el residuo según el material sobre el que se aplicó. Después de un incendio, el residuo no es solo agente: es agente más lo que se estaba quemando.",
|
"El documento que respalda eso es la hoja de datos de seguridad, y es también el que dice cómo manejar el residuo según el material sobre el que se aplicó. Después de un incendio esa distinción decide si el residuo se va al drenaje o se maneja como residuo peligroso.",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -153,35 +184,42 @@ const GRUPOS: Grupo[] = [
|
|||||||
{
|
{
|
||||||
id: "alcance",
|
id: "alcance",
|
||||||
pregunta: "¿Cuál es el alcance real de un extintor Enforcer?",
|
pregunta: "¿Cuál es el alcance real de un extintor Enforcer?",
|
||||||
|
corta:
|
||||||
|
"La fuente declara más de 10 metros. Lo publicamos con esa etiqueta y no lo convertimos en una cifra exacta.",
|
||||||
respuesta: [
|
respuesta: [
|
||||||
"En el catálogo aparece como \"más de 10 m\", que es el dato declarado por la fuente. Lo publicamos con esa etiqueta y no lo convertimos en una cifra exacta.",
|
"Un alcance solo significa algo junto con las condiciones de la medición: presión, boquilla, ángulo y agente.",
|
||||||
"Un alcance solo significa algo junto con las condiciones de la medición: presión, boquilla, ángulo y agente. Eso viene en la ficha técnica del modelo, y es lo que pedimos antes de proponer una distribución de equipo.",
|
"Eso viene en la ficha técnica del modelo, y es lo que pedimos antes de proponer una distribución de equipo. Distribuir con un alcance sin condiciones es como distribuir con un plano sin escala.",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "certificaciones",
|
id: "certificaciones",
|
||||||
pregunta: "¿Los equipos tienen certificación UL o cumplen NFPA?",
|
pregunta: "¿Los equipos tienen certificación UL o cumplen NFPA?",
|
||||||
|
corta:
|
||||||
|
"Hay referencias enumeradas, y una referencia no es un certificado. Te entregamos el que corresponde al modelo que se te ofrece.",
|
||||||
respuesta: [
|
respuesta: [
|
||||||
"La información pública de FSPM enumera referencias como UL 162, UL 711, NFPA 11, NFPA 16, NFPA 18, DIN EN 1568-3 y LASTFIRE, entre otras.",
|
"La información pública de FSPM enumera referencias como UL 162, UL 711, NFPA 11, NFPA 16, NFPA 18, DIN EN 1568-3 y LASTFIRE, entre otras. También publica algunos certificados en PDF, pero referidos al concentrado espumógeno y al agente humectante, no a un número de modelo de equipo.",
|
||||||
"Una referencia no es un certificado. Un listado aplica a una formulación o a un modelo concreto, con expediente, alcance y fecha, y no se traslada al resto del catálogo. Lo que entregamos es el certificado que corresponde al modelo que se te está ofreciendo.",
|
"Un listado aplica a una formulación o a un modelo concreto, con expediente, alcance y fecha, y no se traslada al resto del catálogo. Lo que entregamos es el certificado que corresponde al modelo que se te está ofreciendo.",
|
||||||
"También conviene decirlo con claridad: ninguna norma sustituye a otra. Un listado UL no equivale a una NOM mexicana, y una prueba ambiental no acredita desempeño de extinción.",
|
"También conviene decirlo con claridad: ninguna norma sustituye a otra. Un listado UL no equivale a una NOM mexicana, y una prueba ambiental no acredita desempeño de extinción.",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "elegir",
|
id: "elegir",
|
||||||
pregunta: "¿Cómo sé qué equipo me corresponde?",
|
pregunta: "¿Cómo sé qué equipo me corresponde?",
|
||||||
|
corta:
|
||||||
|
"Con tres respuestas: qué se puede quemar en cada área, quién va a operar el equipo y qué documentación te van a pedir.",
|
||||||
respuesta: [
|
respuesta: [
|
||||||
"Por tres respuestas, en este orden: qué se quema en cada área, quién va a operar el equipo y en cuántos segundos, y qué documentación te van a pedir.",
|
|
||||||
"La primera define la clase de fuego y por tanto el agente. La segunda define si el equipo debe ser portátil, móvil, fijo o de activación automática. La tercera define el expediente.",
|
"La primera define la clase de fuego y por tanto el agente. La segunda define si el equipo debe ser portátil, móvil, fijo o de activación automática. La tercera define el expediente.",
|
||||||
"El catálogo está construido para responder eso: se filtra por clase de fuego y por aplicación, no por nombre de producto.",
|
"El catálogo está construido para responder eso: se filtra por clase de fuego y por aplicación, no por nombre de producto. Si no sabes ninguna de las tres, el escenario de tu instalación es suficiente para empezar.",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "precio",
|
id: "precio",
|
||||||
pregunta: "¿Cuánto cuesta?",
|
pregunta: "¿Cuánto cuesta?",
|
||||||
|
corta:
|
||||||
|
"No publicamos precios. Dependen de la configuración, la cantidad, la instalación y el servicio, y preferimos decírtelo antes de que llenes un formulario.",
|
||||||
respuesta: [
|
respuesta: [
|
||||||
"No publicamos precios porque dependen de la configuración, la cantidad, la instalación y el servicio asociado. Preferimos decirlo antes de que llenes un formulario, no después.",
|
"Lo que sí podemos anticipar es el criterio de costo: el error caro casi nunca es el precio unitario. Es comprar equipo que no corresponde a la clase de fuego del área, o distribuirlo por metros cuadrados en lugar de por distancia de recorrido.",
|
||||||
"Lo que sí podemos anticipar es el criterio: el error caro casi nunca es el precio unitario, es comprar equipo que no corresponde a la clase de fuego del área o distribuirlo por metros cuadrados en lugar de por distancia de recorrido.",
|
"En la respuesta a una solicitud va la configuración propuesta y qué se puede recortar sin perder cumplimiento. Eso es más útil que una lista de precios.",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -194,15 +232,18 @@ const GRUPOS: Grupo[] = [
|
|||||||
{
|
{
|
||||||
id: "nom-002",
|
id: "nom-002",
|
||||||
pregunta: "¿Qué me exige la NOM-002-STPS-2010?",
|
pregunta: "¿Qué me exige la NOM-002-STPS-2010?",
|
||||||
|
corta:
|
||||||
|
"Si tienes gente trabajando, te aplica. Lo que cambia entre una oficina y una planta es el nivel de riesgo con el que se clasifica el centro.",
|
||||||
respuesta: [
|
respuesta: [
|
||||||
NORMAS[0].aplica,
|
NORMAS[0].aplica,
|
||||||
"En términos prácticos: la clasificación del riesgo va primero y condiciona todo lo demás. Los umbrales de esa clasificación dependen del material y de la superficie de cada centro de trabajo, así que no los reproducimos aquí.",
|
"En términos prácticos, la clasificación del riesgo va primero y condiciona todo lo demás. Los umbrales de esa clasificación dependen del material y de la superficie de cada centro de trabajo, así que no los reproducimos aquí.",
|
||||||
"Es una referencia orientativa. La normativa puede complementarse por estado y municipio, y esta página no sustituye un dictamen técnico ni jurídico.",
|
"Es una referencia orientativa. La normativa puede complementarse por estado y municipio, y esta página no sustituye un dictamen técnico ni jurídico.",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "revision",
|
id: "revision",
|
||||||
pregunta: "¿Cada cuánto hay que revisar los extintores?",
|
pregunta: "¿Cada cuánto hay que revisar los extintores?",
|
||||||
|
corta: "Cada mes.",
|
||||||
respuesta: [
|
respuesta: [
|
||||||
NORMAS[1].aplica,
|
NORMAS[1].aplica,
|
||||||
"Es la obligación recurrente que más seguido se pierde, y casi nunca por desconocerla: se pierde porque nadie quedó a cargo de la bitácora. Por eso, en una entrega, el formato de bitácora y el responsable nombrado son parte del alcance.",
|
"Es la obligación recurrente que más seguido se pierde, y casi nunca por desconocerla: se pierde porque nadie quedó a cargo de la bitácora. Por eso, en una entrega, el formato de bitácora y el responsable nombrado son parte del alcance.",
|
||||||
@@ -211,23 +252,45 @@ const GRUPOS: Grupo[] = [
|
|||||||
{
|
{
|
||||||
id: "documentos",
|
id: "documentos",
|
||||||
pregunta: "¿Qué documentos debo exigir en una compra?",
|
pregunta: "¿Qué documentos debo exigir en una compra?",
|
||||||
|
corta:
|
||||||
|
"Ficha técnica, hoja de seguridad del agente, certificado por modelo, garantía con condiciones, manual y bitácora. Y dos más si hay instalación o personal que operar.",
|
||||||
respuesta: [
|
respuesta: [
|
||||||
"Ficha técnica del modelo, hoja de datos de seguridad del agente, certificado o listado por modelo, garantía con condiciones por escrito, manual de operación y mantenimiento, y bitácora de revisión. Cuando hay instalación, además la constancia de puesta en marcha; cuando hay personal que operar, la constancia de capacitación.",
|
"Cuando hay algo que instalar, se agrega la constancia de instalación y puesta en marcha; cuando hay personal que operar, la constancia de capacitación.",
|
||||||
"No todos aplican a todos los productos. El centro de evidencia tiene el índice de qué corresponde según la categoría.",
|
"No todos aplican a todos los productos. El centro de evidencia tiene el índice de qué corresponde según la categoría, en una tabla que puedes llevarte a la conversación con cualquier proveedor.",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "descargas",
|
id: "descargas",
|
||||||
pregunta: "¿Por qué no hay documentos para descargar en este sitio?",
|
pregunta: "¿Por qué no hay documentos para descargar en este sitio?",
|
||||||
|
corta:
|
||||||
|
"Porque no vamos a poner botones que no llevan a ningún archivo. Los documentos se entregan por número de modelo, a solicitud.",
|
||||||
respuesta: [
|
respuesta: [
|
||||||
"Porque en la extracción pública del sitio de FSPM no apareció un índice verificable de documentos por número de modelo, y poner botones de descarga que no llevan a ningún archivo sería peor que no tenerlos.",
|
"En la extracción pública del sitio de FSPM no apareció un índice de documentos asociado a números de modelo, y simular uno sería peor que no tenerlo.",
|
||||||
"Los documentos se entregan por número de modelo, a solicitud, junto con la propuesta técnica. Y cuando un documento no exista o no cubra lo que se afirma, lo decimos en la propuesta.",
|
"Los documentos se entregan junto con la propuesta técnica. Y cuando un documento no exista o no cubra lo que se afirma, lo decimos en la propuesta en lugar de omitirlo.",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/** Escenarios como entrada alternativa para quien no sabe qué preguntar. */
|
||||||
|
const SLUGS_ENTRADA = [
|
||||||
|
"oficinas",
|
||||||
|
"cocinas-hoteles",
|
||||||
|
"gasolineras",
|
||||||
|
"bodegas",
|
||||||
|
"industria",
|
||||||
|
"hogar",
|
||||||
|
];
|
||||||
|
|
||||||
|
const ESCENARIOS_ENTRADA = ESCENARIOS.filter((e) => SLUGS_ENTRADA.includes(e.slug));
|
||||||
|
|
||||||
|
function hrefEscenario(escenario: Escenario): string {
|
||||||
|
if (escenario.industria) return `/soluciones/${escenario.industria}`;
|
||||||
|
const categoria = escenario.categorias[0];
|
||||||
|
return categoria ? `/catalogo/${categoria}` : "/catalogo";
|
||||||
|
}
|
||||||
|
|
||||||
export default function PaginaPreguntas() {
|
export default function PaginaPreguntas() {
|
||||||
const todas = GRUPOS.flatMap((g) => g.preguntas);
|
const todas = GRUPOS.flatMap((g) => g.preguntas);
|
||||||
|
|
||||||
@@ -239,7 +302,7 @@ export default function PaginaPreguntas() {
|
|||||||
name: p.pregunta,
|
name: p.pregunta,
|
||||||
acceptedAnswer: {
|
acceptedAnswer: {
|
||||||
"@type": "Answer",
|
"@type": "Answer",
|
||||||
text: p.respuesta.join(" "),
|
text: [p.corta, ...p.respuesta].join(" "),
|
||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
@@ -251,50 +314,50 @@ export default function PaginaPreguntas() {
|
|||||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* ---------- encabezado ---------- */}
|
<HeroeLanding
|
||||||
<section className="grid-bg bg-ink pt-12 pb-14 sm:pt-16 sm:pb-20">
|
antes={<Migas actual="Preguntas frecuentes" />}
|
||||||
|
eyebrow="Preguntas frecuentes"
|
||||||
|
titulo="Pregunta lo que quieras preguntar"
|
||||||
|
entrada="Cada respuesta abre con una frase directa y sigue con el detalle. No hay pregunta obvia: la mayoría de los errores caros de esta industria empiezan por no preguntar."
|
||||||
|
foto={FOTOS.practica}
|
||||||
|
enfoque="72% 45%"
|
||||||
|
altura="alto"
|
||||||
|
prioridad
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-4 sm:flex-row sm:items-center">
|
||||||
|
<Link href="#preguntar" className="btn btn-signal w-full sm:w-auto">
|
||||||
|
Hacer mi pregunta
|
||||||
|
</Link>
|
||||||
|
<p className="font-mono text-[0.625rem] uppercase leading-relaxed tracking-[0.1em] text-faint">
|
||||||
|
{todas.length} preguntas · {GRUPOS.length} temas
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</HeroeLanding>
|
||||||
|
|
||||||
|
{/* ---------- por qué las respuestas no son las de la competencia ---------- */}
|
||||||
|
<section className="grid-bg-lite bg-paper py-12 sm:py-16" aria-labelledby="criterio-titulo">
|
||||||
<div className="shell">
|
<div className="shell">
|
||||||
<nav aria-label="Ruta de navegación" className="mb-8">
|
<div className="max-w-3xl">
|
||||||
<ol className="flex flex-wrap items-center gap-2 font-mono text-[0.625rem] uppercase tracking-[0.14em] text-faint">
|
<p className="eyebrow mb-4">Cómo leer estas respuestas</p>
|
||||||
<li>
|
<h2
|
||||||
<Link href="/" className="hover:text-bright">
|
id="criterio-titulo"
|
||||||
Inicio
|
className="font-display text-2xl font-extrabold text-deep sm:text-3xl"
|
||||||
</Link>
|
>
|
||||||
</li>
|
Las preguntas son las del sitio original. Las respuestas, no
|
||||||
<li aria-hidden="true">/</li>
|
</h2>
|
||||||
<li className="text-bright">Preguntas frecuentes</li>
|
<p className="mt-4 text-sm leading-relaxed text-deep-muted sm:text-base">
|
||||||
</ol>
|
Donde la fuente afirmaba algo sin condiciones —que no caduca, que no requiere recarga,
|
||||||
</nav>
|
que tiene veinte años de garantía— aquí se explica qué documento lo respalda y qué
|
||||||
|
queda pendiente de confirmar por número de modelo. Es menos vistoso y sirve mucho más
|
||||||
<div className="max-w-3xl anim-rise">
|
el día que llega la auditoría.
|
||||||
<p className="eyebrow mb-5">Preguntas frecuentes</p>
|
|
||||||
<h1 className="font-display text-[2rem] font-black uppercase leading-[1.03] tracking-tight sm:text-[2.75rem] lg:text-5xl">
|
|
||||||
Las mismas preguntas, respondidas con lo que se puede documentar
|
|
||||||
</h1>
|
|
||||||
<p className="mt-6 text-base leading-relaxed text-muted sm:text-lg">
|
|
||||||
Estas preguntas vienen de la página real de FSPM. Las respuestas no. Donde la fuente
|
|
||||||
afirmaba algo sin condiciones, aquí se explica qué documento lo respalda y qué queda
|
|
||||||
pendiente de confirmar por número de modelo. Es menos vistoso y sirve mucho más
|
|
||||||
cuando llega la auditoría.
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<dl className="spec mt-9 sm:grid-cols-2">
|
|
||||||
<div className="spec-cell">
|
|
||||||
<dt className="spec-key">Preguntas</dt>
|
|
||||||
<dd className="spec-val">{todas.length}</dd>
|
|
||||||
</div>
|
|
||||||
<div className="spec-cell">
|
|
||||||
<dt className="spec-key">Temas</dt>
|
|
||||||
<dd className="spec-val">{GRUPOS.length}</dd>
|
|
||||||
</div>
|
|
||||||
</dl>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* ---------- los grupos ---------- */}
|
{/* ---------- los grupos ---------- */}
|
||||||
{GRUPOS.map((grupo, indice) => {
|
{GRUPOS.map((grupo, indice) => {
|
||||||
const claro = indice % 2 === 0;
|
const claro = indice % 2 !== 0;
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
key={grupo.id}
|
key={grupo.id}
|
||||||
@@ -341,11 +404,11 @@ export default function PaginaPreguntas() {
|
|||||||
key={pregunta.id}
|
key={pregunta.id}
|
||||||
id={pregunta.id}
|
id={pregunta.id}
|
||||||
className={[
|
className={[
|
||||||
"group rounded-[6px] border [-webkit-tap-highlight-color:transparent]",
|
"group scroll-mt-28 rounded-[6px] border [-webkit-tap-highlight-color:transparent]",
|
||||||
claro ? "border-rule-lite bg-white" : "border-rule bg-ink-2",
|
claro ? "border-rule-lite bg-white" : "border-rule bg-ink-2",
|
||||||
].join(" ")}
|
].join(" ")}
|
||||||
>
|
>
|
||||||
<summary className="flex cursor-pointer list-none items-start justify-between gap-4 px-5 py-4 sm:px-6 sm:py-5">
|
<summary className="flex cursor-pointer list-none [&::-webkit-details-marker]:hidden [-webkit-tap-highlight-color:transparent] items-start justify-between gap-4 px-5 py-4 sm:px-6 sm:py-5">
|
||||||
<span
|
<span
|
||||||
className={[
|
className={[
|
||||||
"font-display text-base font-bold leading-snug sm:text-lg",
|
"font-display text-base font-bold leading-snug sm:text-lg",
|
||||||
@@ -358,9 +421,7 @@ export default function PaginaPreguntas() {
|
|||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
className={[
|
className={[
|
||||||
"mt-0.5 flex h-7 w-7 shrink-0 items-center justify-center rounded-[2px] border font-mono text-sm transition-transform duration-200 group-open:rotate-45",
|
"mt-0.5 flex h-7 w-7 shrink-0 items-center justify-center rounded-[2px] border font-mono text-sm transition-transform duration-200 group-open:rotate-45",
|
||||||
claro
|
claro ? "border-rule-lite text-deep-muted" : "border-rule-2 text-muted",
|
||||||
? "border-rule-lite text-deep-muted"
|
|
||||||
: "border-rule-2 text-muted",
|
|
||||||
].join(" ")}
|
].join(" ")}
|
||||||
>
|
>
|
||||||
+
|
+
|
||||||
@@ -369,21 +430,32 @@ export default function PaginaPreguntas() {
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
className={[
|
className={[
|
||||||
"space-y-4 border-t px-5 pb-6 pt-5 sm:px-6",
|
"border-t px-5 pb-6 pt-5 sm:px-6",
|
||||||
claro ? "border-rule-lite" : "border-rule",
|
claro ? "border-rule-lite" : "border-rule",
|
||||||
].join(" ")}
|
].join(" ")}
|
||||||
>
|
>
|
||||||
{pregunta.respuesta.map((parrafo) => (
|
<p
|
||||||
<p
|
className={[
|
||||||
key={parrafo.slice(0, 48)}
|
"border-l-2 border-l-signal pl-4 text-base leading-relaxed",
|
||||||
className={[
|
claro ? "text-deep" : "text-bright",
|
||||||
"text-sm leading-relaxed",
|
].join(" ")}
|
||||||
claro ? "text-deep-muted" : "text-muted",
|
>
|
||||||
].join(" ")}
|
{pregunta.corta}
|
||||||
>
|
</p>
|
||||||
{parrafo}
|
|
||||||
</p>
|
<div className="mt-5 space-y-4">
|
||||||
))}
|
{pregunta.respuesta.map((parrafo) => (
|
||||||
|
<p
|
||||||
|
key={parrafo.slice(0, 48)}
|
||||||
|
className={[
|
||||||
|
"text-sm leading-relaxed",
|
||||||
|
claro ? "text-deep-muted" : "text-muted",
|
||||||
|
].join(" ")}
|
||||||
|
>
|
||||||
|
{parrafo}
|
||||||
|
</p>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
))}
|
))}
|
||||||
@@ -394,29 +466,92 @@ export default function PaginaPreguntas() {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
{/* ---------- salidas ---------- */}
|
{/* ---------- entrada por escenario, para quien no sabe qué preguntar ---------- */}
|
||||||
<section className="border-t border-rule bg-ink-2 py-14 sm:py-16">
|
<section
|
||||||
|
className="border-t border-rule bg-ink-2 py-14 sm:py-20"
|
||||||
|
aria-labelledby="escenarios-titulo"
|
||||||
|
>
|
||||||
<div className="shell">
|
<div className="shell">
|
||||||
<div className="max-w-3xl">
|
<Revelar>
|
||||||
<p className="eyebrow mb-4">Si tu pregunta no está aquí</p>
|
<p className="eyebrow eyebrow-muted mb-4">Otra forma de empezar</p>
|
||||||
<h2 className="font-display text-2xl font-extrabold sm:text-3xl">
|
<h2
|
||||||
Pregúntala directo, con el escenario incluido
|
id="escenarios-titulo"
|
||||||
|
className="max-w-2xl font-display text-2xl font-extrabold sm:text-3xl"
|
||||||
|
>
|
||||||
|
Si no sabes qué preguntar, empieza por tu lugar
|
||||||
</h2>
|
</h2>
|
||||||
<p className="mt-4 text-sm leading-relaxed text-muted sm:text-base">
|
<p className="mt-4 max-w-2xl text-sm leading-relaxed text-muted sm:text-base">
|
||||||
Si la respuesta depende de un documento que no tenemos, te lo decimos. Es más útil que
|
No necesitas conocer las clases de fuego para saber qué te corresponde. Elige el lugar
|
||||||
una respuesta cómoda.
|
que se parece al tuyo y de ahí sale el resto.
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-8 flex flex-wrap gap-3">
|
</Revelar>
|
||||||
<Link href="/contacto" className="btn btn-signal">
|
|
||||||
Escribirnos
|
<ul className="mt-10 grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
</Link>
|
{ESCENARIOS_ENTRADA.map((escenario) => (
|
||||||
<Link href="/evidencia" className="btn btn-ghost">
|
<li key={escenario.slug} className="grid">
|
||||||
Centro de evidencia
|
<TarjetaEscenario escenario={escenario} href={hrefEscenario(escenario)} />
|
||||||
</Link>
|
</li>
|
||||||
<Link href="/cumplimiento-nom-002" className="btn btn-ghost">
|
))}
|
||||||
Cumplimiento NOM-002
|
</ul>
|
||||||
</Link>
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* ---------- conversión ---------- */}
|
||||||
|
<section
|
||||||
|
id="preguntar"
|
||||||
|
className="grid-bg-lite border-t border-rule bg-paper py-14 sm:py-20"
|
||||||
|
aria-labelledby="preguntar-titulo"
|
||||||
|
>
|
||||||
|
<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">Si tu pregunta no está aquí</p>
|
||||||
|
<h2
|
||||||
|
id="preguntar-titulo"
|
||||||
|
className="font-display text-2xl font-extrabold text-deep sm:text-3xl"
|
||||||
|
>
|
||||||
|
Pregúntala tal cual, con tus palabras
|
||||||
|
</h2>
|
||||||
|
<p className="mt-4 text-sm leading-relaxed text-deep-muted sm:text-base">
|
||||||
|
No hace falta que uses el término técnico correcto. Si la respuesta depende de un
|
||||||
|
documento que no tenemos, te lo decimos: es más útil que una respuesta cómoda.
|
||||||
|
</p>
|
||||||
|
</Revelar>
|
||||||
|
|
||||||
|
<hr className="hair-lite my-8" />
|
||||||
|
|
||||||
|
<h3 className="font-mono text-[0.6875rem] uppercase tracking-[0.16em] text-deep-muted">
|
||||||
|
Antes de escribir, quizá ya está aquí
|
||||||
|
</h3>
|
||||||
|
<ul className="mt-4 flex flex-wrap gap-2">
|
||||||
|
{[
|
||||||
|
{ href: "/cumplimiento-nom-002", label: "Cumplimiento NOM-002" },
|
||||||
|
{ href: "/evidencia", label: "Documentación técnica" },
|
||||||
|
{ href: "/catalogo", label: "Catálogo" },
|
||||||
|
{ href: "/casos", label: "Casos de aplicación" },
|
||||||
|
].map((enlace) => (
|
||||||
|
<li key={enlace.href}>
|
||||||
|
<Link
|
||||||
|
href={enlace.href}
|
||||||
|
className="badge !border-rule-lite !text-deep-muted hover:!border-signal hover:!text-deep [-webkit-tap-highlight-color:transparent]"
|
||||||
|
>
|
||||||
|
{enlace.label}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<FormularioContacto
|
||||||
|
id="formulario-preguntas"
|
||||||
|
canal="catalogo"
|
||||||
|
variante="claro"
|
||||||
|
titulo="Hacer mi pregunta"
|
||||||
|
descripcion="Completa la frase del mensaje con tu duda, como la dirías en voz alta. Respondemos con el dato y con el documento que lo respalda."
|
||||||
|
mensajeInicial={MENSAJE_INICIAL}
|
||||||
|
textoBoton="Enviar mi pregunta"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -1,58 +1,101 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import Image from "next/image";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import { FormularioContacto } from "@/components/formulario/FormularioContacto";
|
|
||||||
import { RielClases } from "@/components/marca/RielClases";
|
import { RielClases } from "@/components/marca/RielClases";
|
||||||
|
import { HeroeLanding } from "@/components/site/HeroeLanding";
|
||||||
import { Revelar } from "@/components/site/Revelar";
|
import { Revelar } from "@/components/site/Revelar";
|
||||||
import { BloqueDocumentacion } from "@/components/site/catalogo/BloqueDocumentacion";
|
import { BloqueAdecuacion } from "@/components/site/producto/BloqueAdecuacion";
|
||||||
import { TarjetaProducto } from "@/components/site/catalogo/TarjetaProducto";
|
import { BotonCotizar } from "@/components/site/producto/BotonCotizar";
|
||||||
import { iconoDeAplicacion } from "@/components/site/catalogo/iconos";
|
import { Caracteristicas } from "@/components/site/producto/Caracteristicas";
|
||||||
|
import { EscenariosProducto } from "@/components/site/producto/EscenariosProducto";
|
||||||
|
import { EspecificacionProducto } from "@/components/site/producto/EspecificacionProducto";
|
||||||
|
import { FormularioCotizar } from "@/components/site/producto/FormularioCotizar";
|
||||||
|
import { NormaYDocumentacion } from "@/components/site/producto/NormaYDocumentacion";
|
||||||
|
import { RiesgoAgente } from "@/components/site/producto/RiesgoAgente";
|
||||||
|
import { TarjetaRelacionada } from "@/components/site/producto/TarjetaRelacionada";
|
||||||
|
import { mensajeInicialDe } from "@/components/site/producto/guia";
|
||||||
|
import { encabezadoDe, industriaDeCategoria } from "@/components/site/producto/presentacion";
|
||||||
import { PRODUCTOS, getCategoria, getProducto, productosDeCategoria } from "@/data/catalogo";
|
import { PRODUCTOS, getCategoria, getProducto, productosDeCategoria } from "@/data/catalogo";
|
||||||
import { CLASES_INFO } from "@/data/clases-fuego";
|
|
||||||
import { EMPRESA, SITIO_URL } from "@/data/empresa";
|
import { EMPRESA, SITIO_URL } from "@/data/empresa";
|
||||||
|
import { ESCENARIOS } from "@/data/escenarios";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* /producto/[slug] — la página que decide la venta.
|
* /producto/[slug] — la página que cierra la venta.
|
||||||
*
|
*
|
||||||
* Lleva tres cosas que el sitio original no tenía juntas: la especificación sin
|
* El encargo del cliente fue explícito: la ficha estaba optimizada para
|
||||||
* huecos rellenados a mano, el compromiso de documentación por número de modelo
|
* informar y tiene que quedar optimizada para convertir. Tres decisiones
|
||||||
* y un formulario que ya sabe qué producto se estaba viendo.
|
* salen de ahí:
|
||||||
*
|
*
|
||||||
* El JSON-LD declara Product sin offers, price ni aggregateRating: no hay
|
* 1. NO HAY ENLACES QUE SAQUEN DEL EMBUDO. Antes la ficha mandaba a
|
||||||
* precios públicos ni reseñas, e inventarlos en datos estructurados es una
|
* /evidencia a leer el índice de documentación. Ahora el argumento
|
||||||
* violación de las políticas de Google además de una mentira.
|
* normativo se resuelve dentro de la propia página, en dos párrafos, y
|
||||||
|
* termina en el formulario. La navegación y el pie siguen teniendo esas
|
||||||
|
* rutas para quien las busque.
|
||||||
|
*
|
||||||
|
* 2. "COTIZAR ESTE EQUIPO" APARECE SEIS VECES y siempre es un ancla a
|
||||||
|
* `#cotizar` —sin JavaScript, sin estado—, con el formulario ya montado con
|
||||||
|
* el producto, la categoría, las clases de fuego y un mensaje redactado que
|
||||||
|
* el visitante completa en cinco segundos.
|
||||||
|
*
|
||||||
|
* 3. LA FICHA ES HÍBRIDA. Abre en lenguaje llano —"¿es esto lo que
|
||||||
|
* necesito?", con el caso honesto de cuándo conviene otro equipo— y sigue
|
||||||
|
* con la especificación y el riesgo del agente incorrecto, que es lo que
|
||||||
|
* valida al comprador técnico. El texto llano no compite con el SEO: captura
|
||||||
|
* la búsqueda de cola larga que la ficha técnica sola nunca alcanza.
|
||||||
|
*
|
||||||
|
* El JSON-LD declara `Product` sin `offers`, `price` ni `aggregateRating`: no
|
||||||
|
* hay precios públicos ni reseñas, e inventarlos en datos estructurados viola
|
||||||
|
* las políticas de Google además de ser mentira.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/** Se calcula una vez: el formulario valida contra esta lista el `?escenario=` de la URL. */
|
||||||
|
const SLUGS_ESCENARIO = ESCENARIOS.map((e) => e.slug);
|
||||||
|
|
||||||
type Props = { params: Promise<{ slug: string }> };
|
type Props = { params: Promise<{ slug: string }> };
|
||||||
|
|
||||||
export function generateStaticParams(): Array<{ slug: string }> {
|
export function generateStaticParams(): Array<{ slug: string }> {
|
||||||
return PRODUCTOS.map((p) => ({ slug: p.slug }));
|
return PRODUCTOS.map((p) => ({ slug: p.slug }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Recorta en frontera de palabra: una descripción cortada a la mitad se ve mal en el resultado. */
|
||||||
|
function recortar(texto: string, max = 300): string {
|
||||||
|
const limpio = texto.replace(/\s+/g, " ").trim();
|
||||||
|
if (limpio.length <= max) return limpio;
|
||||||
|
const corte = limpio.slice(0, max - 1);
|
||||||
|
const espacio = corte.lastIndexOf(" ");
|
||||||
|
return `${(espacio > 0 ? corte.slice(0, espacio) : corte).replace(/[.,;:]$/, "")}…`;
|
||||||
|
}
|
||||||
|
|
||||||
export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
||||||
const { slug } = await params;
|
const { slug } = await params;
|
||||||
const producto = getProducto(slug);
|
const producto = getProducto(slug);
|
||||||
|
|
||||||
if (!producto) {
|
if (!producto) {
|
||||||
return { title: "Producto no encontrado" };
|
return { title: "Producto no encontrado", robots: { index: false, follow: false } };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const categoria = getCategoria(producto.categoria);
|
||||||
const url = `${SITIO_URL}/producto/${producto.slug}`;
|
const url = `${SITIO_URL}/producto/${producto.slug}`;
|
||||||
const partes = [
|
|
||||||
producto.capacidad ? `Capacidad ${producto.capacidad}.` : null,
|
const descripcion = recortar(
|
||||||
producto.agente ? `Agente ${producto.agente}.` : null,
|
[
|
||||||
producto.clases.length > 0 ? `Clases ${producto.clases.join(", ")}.` : null,
|
producto.resumen,
|
||||||
].filter(Boolean);
|
producto.capacidad ? `Capacidad ${producto.capacidad}.` : null,
|
||||||
|
producto.agente ? `Agente ${producto.agente}.` : null,
|
||||||
|
producto.clases.length > 0 ? `Clases de fuego ${producto.clases.join(", ")}.` : null,
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(" "),
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: producto.nombre,
|
title: categoria ? `${producto.nombre} — ${categoria.nombre}` : producto.nombre,
|
||||||
description: `${producto.resumen} ${partes.join(" ")}`.trim().slice(0, 300),
|
description: descripcion,
|
||||||
alternates: { canonical: url },
|
alternates: { canonical: url },
|
||||||
openGraph: {
|
openGraph: {
|
||||||
type: "website",
|
type: "website",
|
||||||
title: `${producto.nombre} | ${EMPRESA.nombreCorto}`,
|
title: `${producto.nombre} | ${EMPRESA.nombreCorto}`,
|
||||||
description: producto.resumen,
|
description: recortar(producto.resumen, 200),
|
||||||
url,
|
url,
|
||||||
images: [{ url: producto.imagen, alt: producto.nombre }],
|
images: [{ url: producto.imagen, alt: producto.nombre }],
|
||||||
},
|
},
|
||||||
@@ -70,21 +113,18 @@ export default async function PaginaProducto({ params }: Props) {
|
|||||||
(p) => p.slug !== producto.slug,
|
(p) => p.slug !== producto.slug,
|
||||||
);
|
);
|
||||||
|
|
||||||
// la tira de especificación: solo lo que la fuente declara
|
const rutaCategoria = categoria ? `/catalogo/${categoria.slug}` : "/catalogo";
|
||||||
const especificacion = [
|
const { foto, enfoque } = encabezadoDe(producto);
|
||||||
{ clave: "Capacidad", valor: producto.capacidad },
|
|
||||||
{ clave: "Agente", valor: producto.agente },
|
|
||||||
{ clave: "Alcance declarado", valor: producto.alcance },
|
|
||||||
{ clave: "Descarga", valor: producto.descarga },
|
|
||||||
{ clave: "Modelo", valor: producto.modelo },
|
|
||||||
].filter((campo): campo is { clave: string; valor: string } => Boolean(campo.valor));
|
|
||||||
|
|
||||||
const rutaBase = categoria ? `/catalogo/${categoria.slug}` : "/catalogo";
|
// contexto que el formulario no pregunta pero sí registra
|
||||||
|
const industria = industriaDeCategoria(producto.categoria);
|
||||||
|
const clases = producto.clases.length > 0 ? producto.clases : undefined;
|
||||||
|
const mensajeInicial = mensajeInicialDe(producto);
|
||||||
|
|
||||||
const migas = [
|
const migas = [
|
||||||
{ nombre: "Catálogo", url: `${SITIO_URL}/catalogo` },
|
{ nombre: "Catálogo", ruta: "/catalogo" },
|
||||||
...(categoria ? [{ nombre: categoria.nombre, url: `${SITIO_URL}${rutaBase}` }] : []),
|
...(categoria ? [{ nombre: categoria.nombre, ruta: rutaCategoria }] : []),
|
||||||
{ nombre: producto.nombre, url: `${SITIO_URL}/producto/${producto.slug}` },
|
{ nombre: producto.nombre, ruta: `/producto/${producto.slug}` },
|
||||||
];
|
];
|
||||||
|
|
||||||
const jsonLdMigas = {
|
const jsonLdMigas = {
|
||||||
@@ -94,7 +134,7 @@ export default async function PaginaProducto({ params }: Props) {
|
|||||||
"@type": "ListItem",
|
"@type": "ListItem",
|
||||||
position: i + 1,
|
position: i + 1,
|
||||||
name: miga.nombre,
|
name: miga.nombre,
|
||||||
item: miga.url,
|
item: `${SITIO_URL}${miga.ruta}`,
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -122,242 +162,113 @@ export default async function PaginaProducto({ params }: Props) {
|
|||||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLdProducto) }}
|
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLdProducto) }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* ---------- 1 y 2. migas, imagen e identificación ---------- */}
|
{/* ---------- héroe con fotografía y el llamado a la acción primario ---------- */}
|
||||||
<section className="grid-bg bg-ink pt-12 pb-14 sm:pt-16 sm:pb-20">
|
<HeroeLanding
|
||||||
<div className="shell">
|
foto={foto}
|
||||||
<nav aria-label="Ruta de navegación" className="mb-8">
|
enfoque={enfoque}
|
||||||
<ol className="flex flex-wrap items-center gap-2 font-mono text-[0.625rem] uppercase tracking-[0.14em] text-faint">
|
prioridad
|
||||||
<li>
|
altura="medio"
|
||||||
<Link href="/catalogo" className="hover:text-bright">
|
eyebrow={categoria ? categoria.nombre : "Catálogo"}
|
||||||
Catálogo
|
titulo={producto.nombre}
|
||||||
</Link>
|
entrada={producto.resumen}
|
||||||
</li>
|
antes={
|
||||||
{categoria ? (
|
<nav aria-label="Ruta de navegación">
|
||||||
<>
|
<ol className="flex flex-wrap items-center gap-x-2 gap-y-1 font-mono text-[0.625rem] uppercase tracking-[0.14em] text-faint">
|
||||||
<li aria-hidden="true">/</li>
|
{migas.map((miga, i) => {
|
||||||
<li>
|
const ultima = i === migas.length - 1;
|
||||||
<Link href={rutaBase} className="hover:text-bright">
|
return (
|
||||||
{categoria.nombre}
|
<li key={miga.ruta} className="flex items-center gap-2">
|
||||||
</Link>
|
{i > 0 ? <span aria-hidden="true">/</span> : null}
|
||||||
|
{ultima ? (
|
||||||
|
<span className="text-bright">{miga.nombre}</span>
|
||||||
|
) : (
|
||||||
|
<Link
|
||||||
|
href={miga.ruta}
|
||||||
|
className="transition-colors hover:text-bright [-webkit-tap-highlight-color:transparent]"
|
||||||
|
>
|
||||||
|
{miga.nombre}
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
</li>
|
</li>
|
||||||
</>
|
);
|
||||||
) : null}
|
})}
|
||||||
<li aria-hidden="true">/</li>
|
|
||||||
<li className="text-bright">{producto.nombre}</li>
|
|
||||||
</ol>
|
</ol>
|
||||||
</nav>
|
</nav>
|
||||||
|
}
|
||||||
<div className="grid gap-10 lg:grid-cols-[1fr_1.1fr] lg:gap-14">
|
>
|
||||||
<div className="relative aspect-square w-full overflow-hidden rounded-[6px] border border-rule bg-ink-2">
|
<div className="flex flex-col gap-7">
|
||||||
<Image
|
{producto.clases.length > 0 ? (
|
||||||
src={producto.imagen}
|
|
||||||
alt={producto.nombre}
|
|
||||||
fill
|
|
||||||
priority
|
|
||||||
sizes="(max-width: 1024px) 92vw, 32rem"
|
|
||||||
className="object-contain p-8 sm:p-12"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="anim-rise">
|
|
||||||
{categoria ? (
|
|
||||||
<p className="eyebrow mb-5">{categoria.nombre}</p>
|
|
||||||
) : (
|
|
||||||
<p className="eyebrow mb-5">Catálogo</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<h1 className="font-display text-[1.875rem] font-black uppercase leading-[1.05] tracking-tight sm:text-[2.5rem] lg:text-[2.75rem]">
|
|
||||||
{producto.nombre}
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
{producto.modelo ? (
|
|
||||||
<p className="mt-4 font-mono text-xs uppercase tracking-[0.14em] text-faint">
|
|
||||||
Modelo <span className="text-bright">{producto.modelo}</span>
|
|
||||||
</p>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<p className="mt-6 max-w-xl text-base leading-relaxed text-muted sm:text-lg">
|
|
||||||
{producto.resumen}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="mt-8">
|
|
||||||
{producto.clases.length > 0 ? (
|
|
||||||
<>
|
|
||||||
<p className="field-label">Clases de fuego declaradas</p>
|
|
||||||
<RielClases activas={producto.clases} tamano="lg" />
|
|
||||||
<p className="mt-3.5 max-w-lg text-xs leading-relaxed text-faint">
|
|
||||||
Las condiciones de aplicación por clase se confirman en la ficha técnica del
|
|
||||||
modelo. Para clase C, la fuente condiciona el desempeño al uso de agua
|
|
||||||
desmineralizada.
|
|
||||||
</p>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<div className="rounded-[3px] border border-rule-2 bg-ink-2 px-4 py-3.5">
|
|
||||||
<p className="font-mono text-[0.625rem] uppercase tracking-[0.14em] text-ember">
|
|
||||||
Sin clase de fuego declarada
|
|
||||||
</p>
|
|
||||||
<p className="mt-2 max-w-lg text-xs leading-relaxed text-muted">
|
|
||||||
Este producto no se presenta como agente extintor, así que no le atribuimos
|
|
||||||
clases de fuego. Consulta su ficha técnica para la aplicación correcta.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 3. tira de especificación, sin campos vacíos */}
|
|
||||||
{especificacion.length > 0 ? (
|
|
||||||
<dl className="spec mt-9 sm:grid-cols-2">
|
|
||||||
{especificacion.map((campo) => (
|
|
||||||
<div key={campo.clave} className="spec-cell">
|
|
||||||
<dt className="spec-key">{campo.clave}</dt>
|
|
||||||
<dd className="spec-val whitespace-normal leading-relaxed">{campo.valor}</dd>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</dl>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<div className="mt-9 flex flex-wrap gap-3">
|
|
||||||
<Link href="#cotizar" className="btn btn-signal">
|
|
||||||
Cotizar este equipo
|
|
||||||
</Link>
|
|
||||||
<Link href="/evidencia" className="btn btn-ghost">
|
|
||||||
Qué documentos incluye
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* ---------- 4 y 5. características y compromiso de documentación ---------- */}
|
|
||||||
<section className="grid-bg-lite bg-paper py-14 sm:py-20">
|
|
||||||
<div className="shell">
|
|
||||||
<div className="grid gap-10 lg:grid-cols-[1fr_1.15fr] lg:gap-14">
|
|
||||||
<div>
|
<div>
|
||||||
<Revelar>
|
<p className="field-label">Clases de fuego declaradas</p>
|
||||||
<p className="eyebrow mb-4">Características</p>
|
<RielClases activas={producto.clases} tamano="lg" animar />
|
||||||
<h2 className="font-display text-2xl font-extrabold text-deep sm:text-3xl">
|
|
||||||
Lo que la fuente declara del equipo
|
|
||||||
</h2>
|
|
||||||
</Revelar>
|
|
||||||
<ul className="mt-8 space-y-4">
|
|
||||||
{producto.caracteristicas.map((caracteristica) => (
|
|
||||||
<li key={caracteristica} className="flex gap-3.5">
|
|
||||||
<span
|
|
||||||
aria-hidden="true"
|
|
||||||
className="mt-2 h-1.5 w-1.5 shrink-0 rounded-full bg-signal"
|
|
||||||
/>
|
|
||||||
<p className="text-sm leading-relaxed text-deep-muted">{caracteristica}</p>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
{producto.clases.length > 0 ? (
|
|
||||||
<div className="panel-lite mt-10 p-5">
|
|
||||||
<h3 className="font-mono text-[0.6875rem] uppercase tracking-[0.16em] text-deep-muted">
|
|
||||||
Qué significa cada clase encendida
|
|
||||||
</h3>
|
|
||||||
<dl className="mt-4 space-y-3">
|
|
||||||
{producto.clases.map((clase) => (
|
|
||||||
<div key={clase} className="flex gap-3">
|
|
||||||
<dt className="flex h-5 w-5 shrink-0 items-center justify-center rounded-[2px] bg-signal font-mono text-[0.625rem] font-semibold text-white">
|
|
||||||
{clase}
|
|
||||||
</dt>
|
|
||||||
<dd className="text-xs leading-relaxed text-deep-muted">
|
|
||||||
<span className="font-semibold text-deep">
|
|
||||||
{CLASES_INFO[clase].nombre}.
|
|
||||||
</span>{" "}
|
|
||||||
{CLASES_INFO[clase].combustible}.
|
|
||||||
</dd>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</dl>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
<div className="lg:pt-2">
|
<div className="max-w-lg rounded-[3px] border border-rule-2 bg-ink-2/85 px-4 py-3.5">
|
||||||
<BloqueDocumentacion claims={producto.claimsPorVerificar} claro />
|
<p className="font-mono text-[0.625rem] uppercase tracking-[0.14em] text-ember">
|
||||||
|
Sin clase de fuego declarada
|
||||||
{producto.claimsPorVerificar.length === 0 ? (
|
</p>
|
||||||
<div className="panel-lite p-6">
|
<p className="mt-2 text-xs leading-relaxed text-muted">
|
||||||
<p className="font-mono text-[0.6875rem] uppercase tracking-[0.16em] text-deep-muted">
|
La fuente no presenta este producto como agente extintor, así que no le atribuimos
|
||||||
Documentación
|
clases de fuego.
|
||||||
</p>
|
</p>
|
||||||
<p className="mt-3 text-sm leading-relaxed text-deep-muted">
|
|
||||||
Este producto no tiene afirmaciones pendientes de verificación registradas. La
|
|
||||||
entrega sigue incluyendo ficha técnica, hoja de datos de seguridad cuando
|
|
||||||
aplica, garantía con condiciones y manual.{" "}
|
|
||||||
<Link href="/evidencia" className="link">
|
|
||||||
Ver el índice de documentación
|
|
||||||
</Link>
|
|
||||||
.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-3 min-[420px]:flex-row min-[420px]:flex-wrap min-[420px]:items-center">
|
||||||
|
<BotonCotizar bloque>Cotizar este equipo</BotonCotizar>
|
||||||
|
<a
|
||||||
|
href="#especificacion"
|
||||||
|
className="btn btn-ghost w-full min-[420px]:w-auto [-webkit-tap-highlight-color:transparent]"
|
||||||
|
>
|
||||||
|
Ver especificación
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<p className="max-w-lg font-mono text-[0.625rem] uppercase leading-relaxed tracking-[0.1em] text-faint">
|
||||||
|
{producto.modelo ? (
|
||||||
|
<>
|
||||||
|
Modelo <span className="text-bright">{producto.modelo}</span> ·{" "}
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
Sin precio público: depende de configuración, cantidad e instalación
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</HeroeLanding>
|
||||||
|
|
||||||
{/* ---------- 6. aplicaciones ---------- */}
|
{/* ---------- 1. ¿es esto lo que necesito? ---------- */}
|
||||||
<section className="border-t border-rule bg-ink py-14 sm:py-16" aria-labelledby="aplicaciones-titulo">
|
<BloqueAdecuacion producto={producto} />
|
||||||
<div className="shell">
|
|
||||||
<Revelar>
|
|
||||||
<p className="eyebrow mb-4">Aplicaciones</p>
|
|
||||||
<h2 id="aplicaciones-titulo" className="font-display text-2xl font-extrabold sm:text-3xl">
|
|
||||||
Dónde lo ubica la fuente
|
|
||||||
</h2>
|
|
||||||
</Revelar>
|
|
||||||
|
|
||||||
<ul className="mt-9 grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-6">
|
{/* ---------- 2. escenarios donde aplica ---------- */}
|
||||||
{producto.aplicaciones.map((aplicacion) => {
|
<EscenariosProducto producto={producto} />
|
||||||
const icono = iconoDeAplicacion(aplicacion);
|
|
||||||
return (
|
|
||||||
<li
|
|
||||||
key={aplicacion}
|
|
||||||
className="flex flex-col items-center gap-3 rounded-[6px] border border-rule bg-ink-2 px-3 py-5 text-center"
|
|
||||||
>
|
|
||||||
{icono ? (
|
|
||||||
<Image
|
|
||||||
src={icono}
|
|
||||||
alt=""
|
|
||||||
width={40}
|
|
||||||
height={40}
|
|
||||||
sizes="40px"
|
|
||||||
className="h-10 w-10 object-contain opacity-85"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<span
|
|
||||||
aria-hidden="true"
|
|
||||||
className="flex h-10 w-10 items-center justify-center rounded-[3px] border border-rule-2 font-mono text-sm text-faint"
|
|
||||||
>
|
|
||||||
{aplicacion.slice(0, 2).toUpperCase()}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
<span className="font-mono text-[0.625rem] uppercase leading-tight tracking-[0.1em] text-bright">
|
|
||||||
{aplicacion}
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* ---------- 7. formulario con el producto resuelto ---------- */}
|
{/* ---------- 3. especificación técnica ---------- */}
|
||||||
<section id="cotizar" className="border-t border-rule bg-ink-2 py-14 sm:py-20">
|
<EspecificacionProducto producto={producto} />
|
||||||
|
|
||||||
|
{/* ---------- 4. características ---------- */}
|
||||||
|
<Caracteristicas producto={producto} />
|
||||||
|
|
||||||
|
{/* ---------- 5. riesgo de usar el agente equivocado ---------- */}
|
||||||
|
<RiesgoAgente producto={producto} />
|
||||||
|
|
||||||
|
{/* ---------- 6. cumplimiento resuelto aquí + compromiso de documentación ---------- */}
|
||||||
|
<NormaYDocumentacion producto={producto} />
|
||||||
|
|
||||||
|
{/* ---------- 7. formulario, con el producto ya resuelto ---------- */}
|
||||||
|
<section
|
||||||
|
className="border-t border-rule bg-ink py-14 sm:py-20"
|
||||||
|
aria-labelledby="cotizar-titulo"
|
||||||
|
>
|
||||||
<div className="shell">
|
<div className="shell">
|
||||||
<div className="grid gap-10 lg:grid-cols-[1fr_1.05fr] lg:items-start lg:gap-16">
|
<div className="grid gap-10 lg:grid-cols-[1fr_1.05fr] lg:items-start lg:gap-16">
|
||||||
<div>
|
<div>
|
||||||
<Revelar>
|
<Revelar>
|
||||||
<p className="eyebrow mb-4">Cotizar</p>
|
<p className="eyebrow mb-4">Cotizar</p>
|
||||||
<h2 className="font-display text-2xl font-extrabold sm:text-3xl">
|
<h2 id="cotizar-titulo" className="font-display text-2xl font-extrabold sm:text-3xl">
|
||||||
Cotizamos con la instalación a la vista, no por pieza
|
Cotizamos con la instalación a la vista, no por pieza
|
||||||
</h2>
|
</h2>
|
||||||
<p className="mt-4 text-sm leading-relaxed text-muted sm:text-base">
|
<p className="mt-4 text-sm leading-relaxed text-muted sm:text-base">
|
||||||
Tu solicitud llega con el equipo que estabas viendo ya registrado, así que la
|
El mensaje ya viene empezado. Cámbialo por lo que sí es tu caso, deja tus datos y
|
||||||
primera respuesta puede ser técnica en lugar de una pregunta de vuelta.
|
la solicitud llega con el equipo, la categoría y las clases de fuego registradas.
|
||||||
</p>
|
</p>
|
||||||
</Revelar>
|
</Revelar>
|
||||||
|
|
||||||
@@ -372,32 +283,50 @@ export default async function PaginaProducto({ params }: Props) {
|
|||||||
<dd className="spec-val whitespace-normal">{categoria.nombre}</dd>
|
<dd className="spec-val whitespace-normal">{categoria.nombre}</dd>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
{producto.clases.length > 0 ? (
|
||||||
|
<div className="spec-cell">
|
||||||
|
<dt className="spec-key">Clases declaradas</dt>
|
||||||
|
<dd className="spec-val">{producto.clases.join(" · ")}</dd>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</dl>
|
</dl>
|
||||||
|
|
||||||
<p className="mt-6 text-xs leading-relaxed text-faint">
|
<ul className="mt-7 space-y-2.5">
|
||||||
No publicamos precios: dependen de la configuración, la cantidad y la instalación.
|
{[
|
||||||
Preferimos decirlo antes de que llenes el formulario.
|
"Cuatro campos: nombre, correo, teléfono y mensaje.",
|
||||||
</p>
|
"Sin costo y sin compromiso de compra.",
|
||||||
|
"Si el equipo correcto es otro, te lo decimos en la respuesta.",
|
||||||
|
].map((linea) => (
|
||||||
|
<li key={linea} className="flex gap-3">
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
className="mt-[0.45rem] h-1.5 w-1.5 shrink-0 rounded-full bg-signal"
|
||||||
|
/>
|
||||||
|
<span className="text-sm leading-relaxed text-muted">{linea}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<FormularioContacto
|
<FormularioCotizar
|
||||||
canal="producto"
|
|
||||||
producto={{
|
producto={{
|
||||||
slug: producto.slug,
|
slug: producto.slug,
|
||||||
nombre: producto.nombre,
|
nombre: producto.nombre,
|
||||||
categoriaSlug: producto.categoria,
|
categoriaSlug: producto.categoria,
|
||||||
}}
|
}}
|
||||||
titulo="Solicitar cotización"
|
industria={industria}
|
||||||
descripcion="Cuéntanos dónde va instalado y qué material se maneja. Respondemos con configuración, cantidad y los documentos que acompañan la entrega."
|
clases={clases}
|
||||||
|
mensajeInicial={mensajeInicial}
|
||||||
|
escenariosValidos={SLUGS_ESCENARIO}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* ---------- 8. productos relacionados ---------- */}
|
{/* ---------- 8. productos relacionados, cada uno con su botón de cotizar ---------- */}
|
||||||
{relacionados.length > 0 && categoria ? (
|
{relacionados.length > 0 && categoria ? (
|
||||||
<section
|
<section
|
||||||
className="grid-bg-lite border-t border-rule bg-paper py-14 sm:py-20"
|
className="grid-bg-lite border-t border-rule-lite bg-paper py-14 sm:py-20"
|
||||||
aria-labelledby="relacionados-titulo"
|
aria-labelledby="relacionados-titulo"
|
||||||
>
|
>
|
||||||
<div className="shell">
|
<div className="shell">
|
||||||
@@ -410,18 +339,25 @@ export default async function PaginaProducto({ params }: Props) {
|
|||||||
Otros equipos de {categoria.nombre.toLowerCase()}
|
Otros equipos de {categoria.nombre.toLowerCase()}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="mt-3 max-w-2xl text-sm leading-relaxed text-deep-muted">
|
<p className="mt-3 max-w-2xl text-sm leading-relaxed text-deep-muted">
|
||||||
Comparten agente y clases declaradas; cambian la capacidad y el formato de respuesta.
|
Comparten agente y clases declaradas; cambian la capacidad y el formato de
|
||||||
|
respuesta. Si ya sabes cuál es el tuyo, cotízalo directo desde aquí.
|
||||||
</p>
|
</p>
|
||||||
</Revelar>
|
</Revelar>
|
||||||
|
|
||||||
<div className="mt-9 grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
|
<div className="mt-9 grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
{relacionados.slice(0, 6).map((p) => (
|
{relacionados.slice(0, 6).map((p) => (
|
||||||
<TarjetaProducto key={p.slug} producto={p} claro sinCategoria />
|
<TarjetaRelacionada key={p.slug} producto={p} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-10">
|
<div className="mt-10 flex flex-col gap-3 min-[420px]:flex-row min-[420px]:flex-wrap min-[420px]:items-center">
|
||||||
<Link href={rutaBase} className="btn btn-deep">
|
<BotonCotizar variante="deep" bloque>
|
||||||
|
Cotizar este equipo
|
||||||
|
</BotonCotizar>
|
||||||
|
<Link
|
||||||
|
href={rutaCategoria}
|
||||||
|
className="btn btn-sm w-full border-rule-lite text-deep min-[420px]:w-auto [-webkit-tap-highlight-color:transparent] hover:border-deep"
|
||||||
|
>
|
||||||
Ver toda la categoría
|
Ver toda la categoría
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,34 +1,109 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import Image from "next/image";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import { FormularioContacto } from "@/components/formulario/FormularioContacto";
|
import { FormularioContacto } from "@/components/formulario/FormularioContacto";
|
||||||
import { RielClases } from "@/components/marca/RielClases";
|
import { RielClases } from "@/components/marca/RielClases";
|
||||||
|
import { FOTOS, HeroeLanding } from "@/components/site/HeroeLanding";
|
||||||
import { Revelar } from "@/components/site/Revelar";
|
import { Revelar } from "@/components/site/Revelar";
|
||||||
|
import { BandaEscenarios } from "@/components/site/soluciones/BandaEscenarios";
|
||||||
import { TarjetaProducto } from "@/components/site/soluciones/TarjetaProducto";
|
import { TarjetaProducto } from "@/components/site/soluciones/TarjetaProducto";
|
||||||
import { getCategoria, productosDeCategoria } from "@/data/catalogo";
|
import { getCategoria, productosDeCategoria } from "@/data/catalogo";
|
||||||
import { CLASES_INFO, parseClases, type ClaseFuego } from "@/data/clases-fuego";
|
import { CLASES_INFO, parseClases, type ClaseFuego } from "@/data/clases-fuego";
|
||||||
import { EMPRESA } from "@/data/empresa";
|
import { EMPRESA, SITIO_URL } from "@/data/empresa";
|
||||||
|
import { ESCENARIOS } from "@/data/escenarios";
|
||||||
import { getIndustria, INDUSTRIAS } from "@/data/industrias";
|
import { getIndustria, INDUSTRIAS } from "@/data/industrias";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PÁGINA DE SOLUCIÓN POR INDUSTRIA.
|
* PÁGINA DE SOLUCIÓN POR INDUSTRIA.
|
||||||
*
|
*
|
||||||
* Es una de las cinco landing pages que pide la recomendación 6 del estudio.
|
* Es una de las cinco landing pages que pide la recomendación 6 del estudio.
|
||||||
* Usa todos los campos del segmento: el dolor como encabezado, el contexto, las
|
* Abre con `HeroeLanding` —fotografía de fondo, requisito explícito del
|
||||||
* preguntas del diagnóstico presentadas como la secuencia real de una visita
|
* cliente— con foto y recorte distintos por segmento, para que las cinco no se
|
||||||
* técnica, el paquete —la traducción de "no cotices solo un extintor"—, el
|
* lean como la misma página con otro título.
|
||||||
* cumplimiento aplicable sobre fondo claro, y el equipo de las categorías que
|
*
|
||||||
* corresponden a esa ruta.
|
* El `<h1>` lleva la frase que se busca: "protección contra incendios para
|
||||||
|
* cocinas industriales, hoteles y alimentos". El dolor del comprador, que antes
|
||||||
|
* era el `<h1>`, pasó a la entrada: sigue siendo lo primero que se lee, pero
|
||||||
|
* ya no ocupa el encabezado que tiene que competir en buscadores.
|
||||||
|
*
|
||||||
|
* Las dos capas conviven: la banda de escenarios traduce el segmento a lugares
|
||||||
|
* reconocibles y las preguntas del diagnóstico dan el nivel técnico. Los
|
||||||
|
* llamados primarios de todas las secciones terminan en `#diagnostico`; los
|
||||||
|
* enlaces a `/evidencia` y `/cumplimiento-nom-002` se retiraron porque sacaban
|
||||||
|
* al visitante del embudo hacia páginas de lectura, y su argumento se resolvió
|
||||||
|
* con dos frases en cada sección.
|
||||||
*
|
*
|
||||||
* El riesgo declarado viaja por query string desde el clasificador del inicio:
|
* El riesgo declarado viaja por query string desde el clasificador del inicio:
|
||||||
* `?clases=B,C` gana sobre las clases dominantes del segmento y llega
|
* `?clases=B,C` gana sobre las clases dominantes del segmento y llega al
|
||||||
* preseleccionado al formulario.
|
* formulario como campo oculto.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
type Params = { industria: string };
|
type Params = { industria: string };
|
||||||
type Busqueda = { [clave: string]: string | string[] | undefined };
|
type Busqueda = { [clave: string]: string | string[] | undefined };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Foto, recorte y encabezados por segmento.
|
||||||
|
*
|
||||||
|
* `foto` y `enfoque`: las imágenes comparten archivo con otras páginas del
|
||||||
|
* sitio, así que el recorte es lo que evita que dos landings se vean
|
||||||
|
* idénticas.
|
||||||
|
*
|
||||||
|
* `h1` y `titulo`: el encabezado de la página y el título de buscador se
|
||||||
|
* escriben a mano por segmento en lugar de componerse con `nombreLargo`.
|
||||||
|
* Concatenar daba encabezados de ochenta y ocho caracteres —"plantas
|
||||||
|
* industriales, petróleo, gas, química y energía"— que se comen cuatro líneas
|
||||||
|
* en el héroe y se truncan en el resultado de búsqueda. Cada par lleva la
|
||||||
|
* frase que realmente se busca; `nombreLargo` sigue completo en el eyebrow y
|
||||||
|
* en la descripción.
|
||||||
|
*/
|
||||||
|
const PORTADA: Record<
|
||||||
|
string,
|
||||||
|
{ foto: string; enfoque: string; h1: string; titulo: string }
|
||||||
|
> = {
|
||||||
|
"industria-energia": {
|
||||||
|
foto: FOTOS.bombero,
|
||||||
|
enfoque: "72% 38%",
|
||||||
|
h1: "Protección contra incendios para plantas industriales y energía",
|
||||||
|
titulo: "Protección contra incendios industrial y de energía",
|
||||||
|
},
|
||||||
|
"logistica-bodegas": {
|
||||||
|
foto: FOTOS.planta,
|
||||||
|
enfoque: "45% 62%",
|
||||||
|
h1: "Protección contra incendios para bodegas y centros de distribución",
|
||||||
|
titulo: "Protección contra incendios para bodegas y logística",
|
||||||
|
},
|
||||||
|
"gasolineras-flotas": {
|
||||||
|
foto: FOTOS.practica,
|
||||||
|
enfoque: "62% 46%",
|
||||||
|
h1: "Protección contra incendios para gasolineras y flotas",
|
||||||
|
titulo: "Extintores para gasolineras y flotas",
|
||||||
|
},
|
||||||
|
"cocinas-hoteles": {
|
||||||
|
foto: FOTOS.galeria,
|
||||||
|
enfoque: "38% 30%",
|
||||||
|
h1: "Protección contra incendios para cocinas industriales y hoteles",
|
||||||
|
titulo: "Extintores clase K para cocinas industriales y hoteles",
|
||||||
|
},
|
||||||
|
"pymes-oficinas": {
|
||||||
|
foto: FOTOS.contacto,
|
||||||
|
enfoque: "66% 26%",
|
||||||
|
h1: "Protección contra incendios para PyMEs, oficinas y comercios",
|
||||||
|
titulo: "Extintores para PyMEs, oficinas y comercios",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Respaldo si algún día se agrega un segmento sin copia propia. */
|
||||||
|
function portadaDe(slug: string, nombreLargo: string, imagen: string) {
|
||||||
|
return (
|
||||||
|
PORTADA[slug] ?? {
|
||||||
|
foto: imagen,
|
||||||
|
enfoque: "center",
|
||||||
|
h1: `Protección contra incendios para ${nombreLargo.toLowerCase()}`,
|
||||||
|
titulo: `Protección contra incendios para ${nombreLargo.toLowerCase()}`,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function generateStaticParams(): Params[] {
|
export function generateStaticParams(): Params[] {
|
||||||
return INDUSTRIAS.map((i) => ({ industria: i.slug }));
|
return INDUSTRIAS.map((i) => ({ industria: i.slug }));
|
||||||
}
|
}
|
||||||
@@ -45,27 +120,38 @@ export async function generateMetadata({
|
|||||||
return { title: "Solución no encontrada", robots: { index: false, follow: false } };
|
return { title: "Solución no encontrada", robots: { index: false, follow: false } };
|
||||||
}
|
}
|
||||||
|
|
||||||
const titulo = `Protección contra incendios para ${industria.nombreLargo.toLowerCase()}`;
|
const portada = portadaDe(industria.slug, industria.nombreLargo, industria.imagen);
|
||||||
const descripcion = `${industria.dolor} ${industria.contexto.split(". ")[0]}. Clases de fuego ${industria.clases.join(", ")}, equipo, capacitación y la evidencia que exige la NOM-002-STPS-2010.`;
|
|
||||||
|
const lugares = ESCENARIOS.filter((e) => e.industria === industria.slug)
|
||||||
|
.map((e) => e.nombre.toLowerCase())
|
||||||
|
.join(", ");
|
||||||
|
|
||||||
|
const descripcion = `${industria.dolor} Equipo, agente y capacidad para ${
|
||||||
|
lugares || industria.nombreLargo.toLowerCase()
|
||||||
|
}; clases de fuego ${industria.clases.join(
|
||||||
|
", ",
|
||||||
|
)}. Incluye la evidencia que exige la NOM-002-STPS-2010. Diagnóstico sin costo.`;
|
||||||
|
|
||||||
|
const url = `${SITIO_URL}/soluciones/${industria.slug}`;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: titulo,
|
title: portada.titulo,
|
||||||
description: descripcion,
|
description: descripcion,
|
||||||
alternates: { canonical: `/soluciones/${industria.slug}` },
|
alternates: { canonical: `/soluciones/${industria.slug}` },
|
||||||
openGraph: {
|
openGraph: {
|
||||||
type: "article",
|
type: "article",
|
||||||
locale: "es_MX",
|
locale: "es_MX",
|
||||||
siteName: EMPRESA.nombre,
|
siteName: EMPRESA.nombre,
|
||||||
url: `/soluciones/${industria.slug}`,
|
url,
|
||||||
title: `${titulo} | ${EMPRESA.nombreCorto}`,
|
title: `${portada.titulo} | ${EMPRESA.nombreCorto}`,
|
||||||
description: descripcion,
|
description: descripcion,
|
||||||
images: [{ url: industria.imagen, width: 1200, height: 630, alt: industria.nombreLargo }],
|
images: [{ url: portada.foto, width: 1200, height: 630, alt: industria.nombreLargo }],
|
||||||
},
|
},
|
||||||
twitter: {
|
twitter: {
|
||||||
card: "summary_large_image",
|
card: "summary_large_image",
|
||||||
title: `${titulo} | ${EMPRESA.nombreCorto}`,
|
title: `${portada.titulo} | ${EMPRESA.nombreCorto}`,
|
||||||
description: descripcion,
|
description: descripcion,
|
||||||
images: [industria.imagen],
|
images: [portada.foto],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -96,6 +182,9 @@ export default async function PaginaSolucion({
|
|||||||
declaradas.length > 0 ? declaradas : (industria.clases as ClaseFuego[]);
|
declaradas.length > 0 ? declaradas : (industria.clases as ClaseFuego[]);
|
||||||
const vieneDelClasificador = declaradas.length > 0;
|
const vieneDelClasificador = declaradas.length > 0;
|
||||||
|
|
||||||
|
const escenarios = ESCENARIOS.filter((e) => e.industria === industria.slug);
|
||||||
|
const portada = portadaDe(industria.slug, industria.nombreLargo, industria.imagen);
|
||||||
|
|
||||||
const bloquesCategoria = industria.categorias
|
const bloquesCategoria = industria.categorias
|
||||||
.map((slugCategoria) => {
|
.map((slugCategoria) => {
|
||||||
const categoria = getCategoria(slugCategoria);
|
const categoria = getCategoria(slugCategoria);
|
||||||
@@ -106,41 +195,40 @@ export default async function PaginaSolucion({
|
|||||||
})
|
})
|
||||||
.filter((b): b is NonNullable<typeof b> => b !== null);
|
.filter((b): b is NonNullable<typeof b> => b !== null);
|
||||||
|
|
||||||
|
/** Migas de pan también como dato estructurado: solo rutas que existen. */
|
||||||
|
const migas = {
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "BreadcrumbList",
|
||||||
|
itemListElement: [
|
||||||
|
{ "@type": "ListItem", position: 1, name: "Inicio", item: SITIO_URL },
|
||||||
|
{ "@type": "ListItem", position: 2, name: "Soluciones", item: `${SITIO_URL}/soluciones` },
|
||||||
|
{
|
||||||
|
"@type": "ListItem",
|
||||||
|
position: 3,
|
||||||
|
name: industria.nombreLargo,
|
||||||
|
item: `${SITIO_URL}/soluciones/${industria.slug}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* ---------- 1. Encabezado ---------- */}
|
<script
|
||||||
<section className="relative isolate overflow-hidden bg-ink" aria-labelledby="solucion-titulo">
|
type="application/ld+json"
|
||||||
<div className="absolute inset-0 -z-20">
|
dangerouslySetInnerHTML={{ __html: JSON.stringify(migas) }}
|
||||||
<Image
|
/>
|
||||||
src={industria.imagen}
|
|
||||||
alt=""
|
|
||||||
aria-hidden="true"
|
|
||||||
fill
|
|
||||||
priority
|
|
||||||
sizes="100vw"
|
|
||||||
className="object-cover object-center"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="absolute inset-0 -z-10"
|
|
||||||
aria-hidden="true"
|
|
||||||
style={{
|
|
||||||
background:
|
|
||||||
"linear-gradient(100deg, var(--color-ink) 0%, rgba(12,17,22,0.97) 40%, rgba(12,17,22,0.82) 66%, rgba(12,17,22,0.52) 100%)",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="absolute inset-0 -z-10"
|
|
||||||
aria-hidden="true"
|
|
||||||
style={{
|
|
||||||
background:
|
|
||||||
"linear-gradient(to top, var(--color-ink) 0%, rgba(12,17,22,0.3) 34%, rgba(12,17,22,0) 70%)",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div className="grid-bg absolute inset-0 -z-10" aria-hidden="true" />
|
|
||||||
|
|
||||||
<div className="shell relative py-14 sm:py-20">
|
{/* ---------- 1. Encabezado ---------- */}
|
||||||
<nav aria-label="Ruta de navegación" className="mb-7">
|
<HeroeLanding
|
||||||
|
foto={portada.foto}
|
||||||
|
enfoque={portada.enfoque}
|
||||||
|
altura="alto"
|
||||||
|
prioridad
|
||||||
|
eyebrow={industria.nombreLargo}
|
||||||
|
titulo={portada.h1}
|
||||||
|
entrada={industria.dolor}
|
||||||
|
antes={
|
||||||
|
<nav aria-label="Ruta de navegación">
|
||||||
<ol className="flex flex-wrap items-center gap-x-2 gap-y-1 font-mono text-[0.625rem] uppercase tracking-[0.14em] text-faint">
|
<ol className="flex flex-wrap items-center gap-x-2 gap-y-1 font-mono text-[0.625rem] uppercase tracking-[0.14em] text-faint">
|
||||||
<li>
|
<li>
|
||||||
<Link href="/" className="transition-colors hover:text-bright">
|
<Link href="/" className="transition-colors hover:text-bright">
|
||||||
@@ -157,57 +245,74 @@ export default async function PaginaSolucion({
|
|||||||
<li className="text-muted">{industria.nombre}</li>
|
<li className="text-muted">{industria.nombre}</li>
|
||||||
</ol>
|
</ol>
|
||||||
</nav>
|
</nav>
|
||||||
|
}
|
||||||
<div className="max-w-[44rem]">
|
>
|
||||||
<p className="eyebrow">
|
<div className="max-w-[44rem]">
|
||||||
{/* prioridad del estudio de mercado, no índice de lista */}
|
<div className="panel flex flex-col gap-4 p-4 sm:flex-row sm:items-center sm:justify-between">
|
||||||
Prioridad {String(industria.prioridad).padStart(2, "0")} ·{" "}
|
<div>
|
||||||
{industria.nombreLargo}
|
<p className="font-mono text-[0.625rem] uppercase tracking-[0.14em] text-faint">
|
||||||
</p>
|
{vieneDelClasificador ? "Riesgo que declaraste" : "Clases habituales del segmento"}
|
||||||
|
</p>
|
||||||
<h1
|
<RielClases activas={clasesActivas} tamano="md" className="mt-2.5" />
|
||||||
id="solucion-titulo"
|
|
||||||
className="anim-rise mt-5 font-display font-black uppercase leading-[0.96] tracking-[-0.03em] text-bright"
|
|
||||||
style={{ fontSize: "clamp(1.85rem, 5.4vw, 3.5rem)" }}
|
|
||||||
>
|
|
||||||
{industria.dolor}
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<p className="mt-5 font-mono text-xs uppercase tracking-[0.12em] text-signal">
|
|
||||||
Protección contra incendios para {industria.nombreLargo.toLowerCase()}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p className="mt-6 text-base leading-relaxed text-muted sm:text-lg">
|
|
||||||
{industria.contexto}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="panel mt-8 flex flex-col gap-4 p-4 sm:flex-row sm:items-center sm:justify-between">
|
|
||||||
<div>
|
|
||||||
<p className="font-mono text-[0.625rem] uppercase tracking-[0.14em] text-faint">
|
|
||||||
{vieneDelClasificador
|
|
||||||
? "Riesgo que declaraste"
|
|
||||||
: "Clases habituales del segmento"}
|
|
||||||
</p>
|
|
||||||
<RielClases activas={clasesActivas} tamano="md" className="mt-2.5" />
|
|
||||||
</div>
|
|
||||||
<a href="#diagnostico" className="btn btn-signal btn-sm shrink-0">
|
|
||||||
Solicitar diagnóstico
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
|
<a href="#diagnostico" className="btn btn-signal btn-sm shrink-0" style={{ WebkitTapHighlightColor: "transparent" }}>
|
||||||
|
Recibir cotización
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<ul className="mt-4 flex flex-wrap gap-1.5">
|
<ul className="mt-4 flex flex-wrap gap-1.5">
|
||||||
{clasesActivas.map((clase) => (
|
{clasesActivas.map((clase) => (
|
||||||
<li key={clase} className="badge badge-signal">
|
<li key={clase} className="badge badge-signal">
|
||||||
Clase {clase} · {CLASES_INFO[clase].nombre}
|
Clase {clase} · {CLASES_INFO[clase].nombre}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
</div>
|
||||||
|
</HeroeLanding>
|
||||||
|
|
||||||
|
{/* ---------- 2. El escenario, en llano ----------
|
||||||
|
Va sobre papel: es la traducción del segmento a lugares que el
|
||||||
|
visitante reconoce, y con la banda de escenarios inmediatamente
|
||||||
|
después forma un solo bloque de lectura clara entre dos secciones
|
||||||
|
oscuras. */}
|
||||||
|
<section
|
||||||
|
className="border-y border-rule-lite bg-paper text-deep"
|
||||||
|
aria-labelledby="escenario-titulo"
|
||||||
|
>
|
||||||
|
<div className="grid-bg-lite">
|
||||||
|
<div className="shell py-14 sm:py-16">
|
||||||
|
<Revelar className="max-w-[46rem]">
|
||||||
|
<p className="eyebrow">El escenario</p>
|
||||||
|
<h2
|
||||||
|
id="escenario-titulo"
|
||||||
|
className="mt-4 font-display uppercase tracking-[-0.02em] text-deep"
|
||||||
|
style={{ fontSize: "clamp(1.5rem, 3.4vw, 2.2rem)" }}
|
||||||
|
>
|
||||||
|
Qué se está protegiendo aquí
|
||||||
|
</h2>
|
||||||
|
<p className="mt-4 text-base leading-relaxed text-deep-muted sm:text-lg">
|
||||||
|
{industria.contexto}
|
||||||
|
</p>
|
||||||
|
</Revelar>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* ---------- 2. Las preguntas del diagnóstico ---------- */}
|
{escenarios.length > 0 ? (
|
||||||
<section className="border-y border-rule bg-ink-2" aria-labelledby="visita-titulo">
|
<BandaEscenarios
|
||||||
|
id="lugares"
|
||||||
|
escenarios={escenarios}
|
||||||
|
eyebrow="Lugares de esta ruta"
|
||||||
|
titulo={`Dónde empieza el fuego en ${industria.nombre.toLowerCase()}`}
|
||||||
|
descripcion="Los lugares concretos que cubre esta ruta, con el riesgo de cada uno. Cada tarjeta abre el catálogo filtrado por ese escenario, ya cruzado con sus clases de fuego."
|
||||||
|
conRiesgo
|
||||||
|
columnas={3}
|
||||||
|
claro
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{/* ---------- 3. Las preguntas del diagnóstico ---------- */}
|
||||||
|
<section className="border-b border-rule bg-ink-2" aria-labelledby="visita-titulo">
|
||||||
<div className="shell py-16 sm:py-20">
|
<div className="shell py-16 sm:py-20">
|
||||||
<Revelar className="max-w-[44rem]">
|
<Revelar className="max-w-[44rem]">
|
||||||
<p className="eyebrow eyebrow-muted">La visita técnica</p>
|
<p className="eyebrow eyebrow-muted">La visita técnica</p>
|
||||||
@@ -221,7 +326,7 @@ export default async function PaginaSolucion({
|
|||||||
<p className="mt-4 text-muted">
|
<p className="mt-4 text-muted">
|
||||||
Es el orden real de una visita en este tipo de operación. Si puedes responder estas
|
Es el orden real de una visita en este tipo de operación. Si puedes responder estas
|
||||||
preguntas por adelantado, la primera propuesta llega mucho más cerca de lo que
|
preguntas por adelantado, la primera propuesta llega mucho más cerca de lo que
|
||||||
necesitas.
|
necesitas y te ahorra una vuelta de correos.
|
||||||
</p>
|
</p>
|
||||||
</Revelar>
|
</Revelar>
|
||||||
|
|
||||||
@@ -237,10 +342,16 @@ export default async function PaginaSolucion({
|
|||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ol>
|
</ol>
|
||||||
|
|
||||||
|
<Revelar className="mt-8">
|
||||||
|
<a href="#diagnostico" className="btn btn-ghost btn-sm" style={{ WebkitTapHighlightColor: "transparent" }}>
|
||||||
|
Recibir cotización
|
||||||
|
</a>
|
||||||
|
</Revelar>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* ---------- 3. Qué se empaqueta ---------- */}
|
{/* ---------- 4. Qué se empaqueta ---------- */}
|
||||||
<section className="bg-ink" aria-labelledby="paquete-titulo">
|
<section className="bg-ink" aria-labelledby="paquete-titulo">
|
||||||
<div className="shell py-16 sm:py-20">
|
<div className="shell py-16 sm:py-20">
|
||||||
<div className="grid gap-10 lg:grid-cols-12 lg:gap-12">
|
<div className="grid gap-10 lg:grid-cols-12 lg:gap-12">
|
||||||
@@ -254,13 +365,21 @@ export default async function PaginaSolucion({
|
|||||||
No cotices solo un extintor
|
No cotices solo un extintor
|
||||||
</h2>
|
</h2>
|
||||||
<p className="mt-4 text-muted">
|
<p className="mt-4 text-muted">
|
||||||
Un equipo suelto resuelve una compra, no un riesgo. Para {" "}
|
Un equipo suelto resuelve una compra, no un riesgo. Para{" "}
|
||||||
{industria.nombreLargo.toLowerCase()} esto es lo que conviene dejar cerrado en el
|
{industria.nombreLargo.toLowerCase()} esto es lo que conviene dejar cerrado en el
|
||||||
mismo proyecto.
|
mismo proyecto, en el orden en que se ejecuta.
|
||||||
</p>
|
</p>
|
||||||
<Link href="/evidencia" className="btn btn-ghost btn-sm mt-6">
|
<p className="mt-4 text-sm leading-relaxed text-faint">
|
||||||
Ver qué documentamos
|
Cada punto de la lista deja un documento: bitácora, constancia de capacitación,
|
||||||
</Link>
|
ficha técnica y hoja de seguridad del agente. Es el expediente que se revisa
|
||||||
|
después, y va incluido.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="mt-7">
|
||||||
|
<a href="#diagnostico" className="btn btn-signal" style={{ WebkitTapHighlightColor: "transparent" }}>
|
||||||
|
Recibir cotización
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</Revelar>
|
</Revelar>
|
||||||
|
|
||||||
<div className="lg:col-span-7">
|
<div className="lg:col-span-7">
|
||||||
@@ -284,14 +403,14 @@ export default async function PaginaSolucion({
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* ---------- 4. Cumplimiento aplicable ---------- */}
|
{/* ---------- 5. Cumplimiento aplicable ---------- */}
|
||||||
<section
|
<section
|
||||||
className="border-y border-rule-lite bg-paper text-deep"
|
className="border-y border-rule-lite bg-paper text-deep"
|
||||||
aria-labelledby="cumplimiento-titulo"
|
aria-labelledby="cumplimiento-titulo"
|
||||||
>
|
>
|
||||||
<div className="grid-bg-lite">
|
<div className="grid-bg-lite">
|
||||||
<div className="shell py-16 sm:py-20">
|
<div className="shell py-16 sm:py-20">
|
||||||
<Revelar className="max-w-[44rem]">
|
<Revelar className="max-w-[46rem]">
|
||||||
<p className="eyebrow">Cumplimiento</p>
|
<p className="eyebrow">Cumplimiento</p>
|
||||||
<h2
|
<h2
|
||||||
id="cumplimiento-titulo"
|
id="cumplimiento-titulo"
|
||||||
@@ -300,6 +419,13 @@ export default async function PaginaSolucion({
|
|||||||
>
|
>
|
||||||
Qué te van a revisar en esta operación
|
Qué te van a revisar en esta operación
|
||||||
</h2>
|
</h2>
|
||||||
|
<p className="mt-4 text-deep-muted">
|
||||||
|
Lo que se revisa no es solo el equipo colgado en la pared: es el papel que prueba
|
||||||
|
que corresponde a tu nivel de riesgo y que alguien lo inspeccionó. La
|
||||||
|
NOM-002-STPS-2010 aplica a todo centro de trabajo en México y obliga a clasificar
|
||||||
|
el riesgo de incendio, sostener un plan por escrito y documentar la revisión
|
||||||
|
mensual de extintores conforme a la NOM-154-SCFI-2005.
|
||||||
|
</p>
|
||||||
</Revelar>
|
</Revelar>
|
||||||
|
|
||||||
<Revelar className="mt-8">
|
<Revelar className="mt-8">
|
||||||
@@ -318,26 +444,30 @@ export default async function PaginaSolucion({
|
|||||||
definitivo depende de la clasificación de riesgo de tu centro de trabajo y de la
|
definitivo depende de la clasificación de riesgo de tu centro de trabajo y de la
|
||||||
autoridad estatal o municipal que te verifique.
|
autoridad estatal o municipal que te verifique.
|
||||||
</p>
|
</p>
|
||||||
<Link href="/cumplimiento-nom-002" className="btn btn-deep btn-sm shrink-0">
|
<a href="#diagnostico" className="btn btn-deep btn-sm shrink-0" style={{ WebkitTapHighlightColor: "transparent" }}>
|
||||||
Cómo funciona la NOM-002
|
Recibir cotización
|
||||||
</Link>
|
</a>
|
||||||
</Revelar>
|
</Revelar>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* ---------- 5. Productos relevantes ---------- */}
|
{/* ---------- 6. Productos relevantes ---------- */}
|
||||||
<section className="bg-ink" aria-labelledby="equipo-titulo">
|
<section className="bg-ink" aria-labelledby="equipo-titulo">
|
||||||
<div className="shell py-16 sm:py-20">
|
<div className="shell py-16 sm:py-20">
|
||||||
<Revelar className="max-w-[44rem]">
|
<Revelar className="max-w-[46rem]">
|
||||||
<p className="eyebrow">Equipo de esta ruta</p>
|
<p className="eyebrow">Equipo de esta ruta</p>
|
||||||
<h2
|
<h2
|
||||||
id="equipo-titulo"
|
id="equipo-titulo"
|
||||||
className="mt-4 font-display uppercase tracking-[-0.02em] text-bright"
|
className="mt-4 font-display uppercase tracking-[-0.02em] text-bright"
|
||||||
style={{ fontSize: "clamp(1.5rem, 3.4vw, 2.2rem)" }}
|
style={{ fontSize: "clamp(1.5rem, 3.4vw, 2.2rem)" }}
|
||||||
>
|
>
|
||||||
Categorías que se usan en {industria.nombre.toLowerCase()}
|
Equipo que se usa en {industria.nombre.toLowerCase()}
|
||||||
</h2>
|
</h2>
|
||||||
|
<p className="mt-4 text-muted">
|
||||||
|
Lo que cambia de un equipo a otro es el agente, la capacidad y el alcance. Esos tres
|
||||||
|
datos vienen en cada tarjeta, junto con las clases de fuego que cubre.
|
||||||
|
</p>
|
||||||
</Revelar>
|
</Revelar>
|
||||||
|
|
||||||
<div className="mt-10 space-y-12">
|
<div className="mt-10 space-y-12">
|
||||||
@@ -380,10 +510,10 @@ export default async function PaginaSolucion({
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* ---------- 6. Formulario ---------- */}
|
{/* ---------- 7. Formulario ---------- */}
|
||||||
<section
|
<section
|
||||||
id="diagnostico"
|
id="diagnostico"
|
||||||
className="scroll-mt-20 border-t border-rule bg-ink-2"
|
className="scroll-mt-24 border-t border-rule bg-ink-2"
|
||||||
aria-labelledby="diagnostico-titulo"
|
aria-labelledby="diagnostico-titulo"
|
||||||
>
|
>
|
||||||
<div className="shell py-16 sm:py-20">
|
<div className="shell py-16 sm:py-20">
|
||||||
@@ -395,11 +525,12 @@ export default async function PaginaSolucion({
|
|||||||
className="mt-4 font-display uppercase tracking-[-0.02em] text-bright"
|
className="mt-4 font-display uppercase tracking-[-0.02em] text-bright"
|
||||||
style={{ fontSize: "clamp(1.5rem, 3.4vw, 2.2rem)" }}
|
style={{ fontSize: "clamp(1.5rem, 3.4vw, 2.2rem)" }}
|
||||||
>
|
>
|
||||||
Diagnóstico para {industria.nombre.toLowerCase()}
|
Recibir cotización para {industria.nombre.toLowerCase()}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="mt-4 text-muted">
|
<p className="mt-4 text-muted">
|
||||||
Tu segmento y tus clases de fuego ya vienen cargados en el formulario. Solo
|
Ya sabemos de qué operación vienes y con qué clases de fuego llegaste: eso viaja con
|
||||||
necesitamos saber dónde estás y qué tienes instalado hoy.
|
tu solicitud y no te lo volvemos a preguntar. Solo necesitamos saber cómo
|
||||||
|
contactarte y qué tienes instalado hoy.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<hr className="hair my-7" />
|
<hr className="hair my-7" />
|
||||||
@@ -432,10 +563,10 @@ export default async function PaginaSolucion({
|
|||||||
<Revelar className="lg:col-span-7" delay={90}>
|
<Revelar className="lg:col-span-7" delay={90}>
|
||||||
<FormularioContacto
|
<FormularioContacto
|
||||||
canal="solucion"
|
canal="solucion"
|
||||||
titulo={`Diagnóstico · ${industria.nombre}`}
|
titulo="Recibir cotización"
|
||||||
descripcion="Con el escenario descrito podemos responder qué agente aplica, en qué formato y qué te toca documentar, sin una segunda vuelta de correos."
|
descripcion="Cuatro campos. Con el escenario descrito podemos responder qué agente aplica, en qué formato y qué te toca documentar, sin una segunda vuelta de correos."
|
||||||
industriaPreseleccionada={industria.slug}
|
industria={industria.slug}
|
||||||
clasesPreseleccionadas={clasesActivas}
|
clases={clasesActivas}
|
||||||
/>
|
/>
|
||||||
</Revelar>
|
</Revelar>
|
||||||
</div>
|
</div>
|
||||||
@@ -449,7 +580,7 @@ export default async function PaginaSolucion({
|
|||||||
id="otras-titulo"
|
id="otras-titulo"
|
||||||
className="font-display text-base uppercase tracking-[0.01em] text-bright"
|
className="font-display text-base uppercase tracking-[0.01em] text-bright"
|
||||||
>
|
>
|
||||||
Otras rutas
|
Otras rutas por industria
|
||||||
</h2>
|
</h2>
|
||||||
<ul className="mt-5 flex flex-wrap gap-2">
|
<ul className="mt-5 flex flex-wrap gap-2">
|
||||||
{INDUSTRIAS.filter((i) => i.slug !== industria.slug).map((otra) => (
|
{INDUSTRIAS.filter((i) => i.slug !== industria.slug).map((otra) => (
|
||||||
|
|||||||
@@ -1,72 +1,130 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import Link from "next/link";
|
import { FormularioContacto } from "@/components/formulario/FormularioContacto";
|
||||||
|
import { HeroeLanding, FOTOS } from "@/components/site/HeroeLanding";
|
||||||
import { Revelar } from "@/components/site/Revelar";
|
import { Revelar } from "@/components/site/Revelar";
|
||||||
|
import { BandaEscenarios } from "@/components/site/soluciones/BandaEscenarios";
|
||||||
import { FilaRuta } from "@/components/site/soluciones/FilaRuta";
|
import { FilaRuta } from "@/components/site/soluciones/FilaRuta";
|
||||||
|
import { CATEGORIAS, PRODUCTOS } from "@/data/catalogo";
|
||||||
import { CLASES } from "@/data/clases-fuego";
|
import { CLASES } from "@/data/clases-fuego";
|
||||||
import { EMPRESA } from "@/data/empresa";
|
import { EMPRESA, SITIO_URL } from "@/data/empresa";
|
||||||
|
import { ESCENARIOS } from "@/data/escenarios";
|
||||||
import { INDUSTRIAS } from "@/data/industrias";
|
import { INDUSTRIAS } from "@/data/industrias";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ÍNDICE DE SOLUCIONES.
|
* ÍNDICE DE SOLUCIONES.
|
||||||
*
|
*
|
||||||
* Página breve y comparativa: los cinco segmentos prioritarios del estudio con
|
* Abre con fotografía, como todas las landings: requisito explícito del
|
||||||
* el contexto suficiente para decidir por dónde entrar. No repite el catálogo,
|
* cliente. Después, las dos capas del sitio en el orden en que sirven aquí:
|
||||||
* lo enruta.
|
* primero las cinco rutas por industria con su contexto completo, para
|
||||||
|
* comparar; después los trece escenarios concretos, que dan entrada a los
|
||||||
|
* cuatro casos —museo, auditorio, agricultura, bosque— que no caben en ninguna
|
||||||
|
* de las cinco rutas y que de otro modo se quedarían sin puerta.
|
||||||
|
*
|
||||||
|
* La página cierra en el formulario, no en un enlace a `/contacto`: el botón
|
||||||
|
* que el visitante ve arriba y el título del formulario abajo dicen lo mismo,
|
||||||
|
* "Recibir cotización".
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
const SIN_INDUSTRIA = ESCENARIOS.filter((e) => e.industria === null);
|
||||||
|
|
||||||
|
const TITULO_SEO = "Protección contra incendios por industria";
|
||||||
|
|
||||||
const DESCRIPCION =
|
const DESCRIPCION =
|
||||||
"Cinco rutas de protección contra incendios por tipo de operación: industria y energía, logística y bodegas, gasolineras y flotas, cocinas y hoteles, PyMEs y oficinas. Cada una con su escenario, sus clases de fuego y lo que hay que documentar.";
|
"Cinco rutas de protección contra incendios según lo que operas: plantas y energía, bodegas y logística, gasolineras y flotas, cocinas y hoteles, PyMEs y oficinas. Cada ruta con su escenario, sus clases de fuego dominantes, el equipo que aplica y lo que hay que documentar para la NOM-002.";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Soluciones por industria",
|
title: TITULO_SEO,
|
||||||
description: DESCRIPCION,
|
description: DESCRIPCION,
|
||||||
alternates: { canonical: "/soluciones" },
|
alternates: { canonical: "/soluciones" },
|
||||||
|
keywords: [
|
||||||
|
"protección contra incendios industrial",
|
||||||
|
"extintor para gasolinera",
|
||||||
|
"protección contra incendios en bodega",
|
||||||
|
"sistema contra incendio para cocina industrial",
|
||||||
|
"extintores para oficina",
|
||||||
|
"NOM-002-STPS-2010",
|
||||||
|
],
|
||||||
openGraph: {
|
openGraph: {
|
||||||
type: "website",
|
type: "website",
|
||||||
locale: "es_MX",
|
locale: "es_MX",
|
||||||
siteName: EMPRESA.nombre,
|
siteName: EMPRESA.nombre,
|
||||||
url: "/soluciones",
|
url: `${SITIO_URL}/soluciones`,
|
||||||
title: `Soluciones por industria | ${EMPRESA.nombreCorto}`,
|
title: `${TITULO_SEO} | ${EMPRESA.nombreCorto}`,
|
||||||
description: DESCRIPCION,
|
description: DESCRIPCION,
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
url: FOTOS.portadaGaleria,
|
||||||
|
width: 1200,
|
||||||
|
height: 630,
|
||||||
|
alt: "Equipo de respuesta contra incendio de FSPM en operación",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
card: "summary_large_image",
|
||||||
|
title: `${TITULO_SEO} | ${EMPRESA.nombreCorto}`,
|
||||||
|
description: DESCRIPCION,
|
||||||
|
images: [FOTOS.portadaGaleria],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const DATOS = [
|
||||||
|
{ clave: "Rutas por industria", valor: String(INDUSTRIAS.length) },
|
||||||
|
{ clave: "Escenarios mapeados", valor: String(ESCENARIOS.length) },
|
||||||
|
{ clave: "Categorías de equipo", valor: String(CATEGORIAS.length).padStart(2, "0") },
|
||||||
|
{ clave: "Equipos en catálogo", valor: String(PRODUCTOS.length) },
|
||||||
|
];
|
||||||
|
|
||||||
export default function PaginaSoluciones() {
|
export default function PaginaSoluciones() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<section className="grid-bg border-b border-rule bg-ink" aria-labelledby="soluciones-titulo">
|
<HeroeLanding
|
||||||
<div className="shell py-14 sm:py-18">
|
eyebrow="Soluciones por industria"
|
||||||
<Revelar className="max-w-[46rem]">
|
titulo="El riesgo no es igual en una bodega que en una cocina"
|
||||||
<p className="eyebrow">Rutas por industria</p>
|
entrada="Lo que se quema, quién responde y qué autoridad te revisa cambia con cada operación. Elige la ruta que se parece a la tuya y verás el escenario, las clases de fuego dominantes, el equipo que aplica y el expediente que se exige."
|
||||||
<h1
|
foto={FOTOS.portadaGaleria}
|
||||||
id="soluciones-titulo"
|
enfoque="58% 34%"
|
||||||
className="mt-5 font-display font-black uppercase leading-[0.96] tracking-[-0.03em] text-bright"
|
altura="alto"
|
||||||
style={{ fontSize: "clamp(1.9rem, 5.2vw, 3.4rem)" }}
|
prioridad
|
||||||
>
|
>
|
||||||
El riesgo no es igual en una bodega que en una cocina
|
<div className="flex flex-wrap items-center gap-x-6 gap-y-3">
|
||||||
</h1>
|
<a href="#diagnostico" className="btn btn-signal" style={{ WebkitTapHighlightColor: "transparent" }}>
|
||||||
<p className="mt-6 text-base leading-relaxed text-muted sm:text-lg">
|
Recibir cotización
|
||||||
Cada operación quema cosas distintas, responde con personas distintas y le rinde
|
</a>
|
||||||
cuentas a autoridades distintas. Estas son las {INDUSTRIAS.length} rutas por las que
|
<a href="#por-escenario" className="link text-sm text-muted">
|
||||||
entra la mayoría de nuestros proyectos, ordenadas por prioridad del estudio de
|
Prefiero elegir por el lugar que opero
|
||||||
mercado.
|
</a>
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="mt-7 flex flex-wrap gap-3">
|
|
||||||
<Link href="/#clases-de-fuego" className="btn btn-ghost btn-sm">
|
|
||||||
Prefiero entrar por clase de fuego
|
|
||||||
</Link>
|
|
||||||
<Link href="/cumplimiento-nom-002" className="btn btn-ghost btn-sm">
|
|
||||||
Prefiero entrar por cumplimiento
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</Revelar>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="bg-ink" aria-label="Listado de rutas por industria">
|
<dl className="spec mt-9 grid-cols-2 sm:grid-cols-4">
|
||||||
|
{DATOS.map((d) => (
|
||||||
|
<div key={d.clave} className="spec-cell">
|
||||||
|
<dt className="spec-key">{d.clave}</dt>
|
||||||
|
<dd className="spec-val">{d.valor}</dd>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</dl>
|
||||||
|
</HeroeLanding>
|
||||||
|
|
||||||
|
<section className="bg-ink" aria-labelledby="rutas-titulo">
|
||||||
<div className="shell py-14 sm:py-16">
|
<div className="shell py-14 sm:py-16">
|
||||||
<ul>
|
<Revelar className="max-w-[46rem]">
|
||||||
|
<p className="eyebrow">Las cinco rutas</p>
|
||||||
|
<h2
|
||||||
|
id="rutas-titulo"
|
||||||
|
className="mt-4 font-display uppercase tracking-[-0.02em] text-bright"
|
||||||
|
style={{ fontSize: "clamp(1.5rem, 3.4vw, 2.2rem)" }}
|
||||||
|
>
|
||||||
|
Cinco operaciones, cinco formas de arder
|
||||||
|
</h2>
|
||||||
|
<p className="mt-4 text-muted">
|
||||||
|
Son los segmentos por los que entra la mayor parte de nuestros proyectos. Cada fila
|
||||||
|
trae el escenario en lenguaje llano y, al lado, las clases de fuego y las categorías de
|
||||||
|
equipo que le corresponden, para que puedas comparar las cinco sin abrir ninguna.
|
||||||
|
</p>
|
||||||
|
</Revelar>
|
||||||
|
|
||||||
|
<ul className="mt-10">
|
||||||
{INDUSTRIAS.map((industria) => (
|
{INDUSTRIAS.map((industria) => (
|
||||||
<FilaRuta key={industria.slug} industria={industria} />
|
<FilaRuta key={industria.slug} industria={industria} />
|
||||||
))}
|
))}
|
||||||
@@ -74,22 +132,66 @@ export default function PaginaSoluciones() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="border-t border-rule-lite bg-paper text-deep">
|
<BandaEscenarios
|
||||||
<div className="shell py-12 sm:py-14">
|
id="por-escenario"
|
||||||
<Revelar className="flex flex-col gap-6 lg:flex-row lg:items-center lg:justify-between">
|
escenarios={ESCENARIOS}
|
||||||
<div className="max-w-[42rem]">
|
eyebrow="Entrada por escenario"
|
||||||
<h2 className="font-display text-lg uppercase tracking-[-0.01em] text-deep sm:text-xl">
|
titulo="Elige por el lugar que operas"
|
||||||
¿Tu operación no encaja limpio en ninguna?
|
descripcion={`Trece lugares concretos, del patio de maniobras al comedor del hotel, cada uno con el equipo que ahí se usa. ${SIN_INDUSTRIA.length} de ellos —museo, auditorio, campo agrícola y área forestal— no encajan en ninguna de las cinco rutas de arriba, y el catálogo sí los cubre.`}
|
||||||
</h2>
|
conRiesgo={false}
|
||||||
<p className="mt-3 text-sm leading-relaxed text-deep-muted">
|
/>
|
||||||
Pasa seguido: un sitio mixto puede tener clase {CLASES.join(", ")} en el mismo
|
|
||||||
predio. Descríbenos el escenario y lo clasificamos contigo antes de proponer equipo.
|
<section
|
||||||
</p>
|
id="diagnostico"
|
||||||
|
className="scroll-mt-24 border-t border-rule-lite bg-paper text-deep"
|
||||||
|
aria-labelledby="diagnostico-titulo"
|
||||||
|
>
|
||||||
|
<div className="grid-bg-lite">
|
||||||
|
<div className="shell py-16 sm:py-20">
|
||||||
|
<div className="grid gap-10 lg:grid-cols-12 lg:gap-14">
|
||||||
|
<Revelar className="lg:col-span-5">
|
||||||
|
<p className="eyebrow">Siguiente paso</p>
|
||||||
|
<h2
|
||||||
|
id="diagnostico-titulo"
|
||||||
|
className="mt-4 font-display uppercase tracking-[-0.02em] text-deep"
|
||||||
|
style={{ fontSize: "clamp(1.5rem, 3.4vw, 2.2rem)" }}
|
||||||
|
>
|
||||||
|
¿Tu operación no encaja limpio en ninguna?
|
||||||
|
</h2>
|
||||||
|
<p className="mt-4 text-deep-muted">
|
||||||
|
Pasa seguido, y no es un problema: un mismo predio puede tener nave, taller,
|
||||||
|
cuarto eléctrico y comedor, con clase {CLASES.join(", ")} conviviendo en la misma
|
||||||
|
dirección. Describe el sitio y lo clasificamos contigo antes de proponer equipo.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<hr className="hair-lite my-7" />
|
||||||
|
|
||||||
|
<p className="text-sm leading-relaxed text-deep-muted">
|
||||||
|
Un sitio mixto se resuelve por zonas, no con un solo equipo repetido en toda la
|
||||||
|
planta. Eso es lo primero que se define en la visita.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p className="mt-5 text-sm text-deep-muted">
|
||||||
|
Si prefieres hablar antes de escribir:{" "}
|
||||||
|
<a
|
||||||
|
href={`tel:${EMPRESA.telefonoTel}`}
|
||||||
|
className="link font-mono text-sm text-deep"
|
||||||
|
>
|
||||||
|
{EMPRESA.telefono}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</Revelar>
|
||||||
|
|
||||||
|
<Revelar className="lg:col-span-7" delay={90}>
|
||||||
|
<FormularioContacto
|
||||||
|
canal="solucion"
|
||||||
|
variante="claro"
|
||||||
|
titulo="Recibir cotización"
|
||||||
|
descripcion="Cuatro campos. Cuéntanos en el mensaje qué opera el sitio y qué tienes instalado hoy: con eso la primera respuesta ya trae el equipo que aplica y la lista de documentos."
|
||||||
|
/>
|
||||||
|
</Revelar>
|
||||||
</div>
|
</div>
|
||||||
<Link href="/contacto" className="btn btn-deep shrink-0">
|
</div>
|
||||||
Describir mi escenario
|
|
||||||
</Link>
|
|
||||||
</Revelar>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -197,6 +197,9 @@
|
|||||||
line-height: 1;
|
line-height: 1;
|
||||||
padding: 0.9375rem 1.5rem;
|
padding: 0.9375rem 1.5rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
/* Safari e iOS pintan un recuadro gris al tocar. Se apaga aquí una vez
|
||||||
|
en lugar de repetirlo en línea en cada botón del sitio. */
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
transition:
|
transition:
|
||||||
background-color 160ms ease,
|
background-color 160ms ease,
|
||||||
border-color 160ms ease,
|
border-color 160ms ease,
|
||||||
@@ -431,6 +434,12 @@
|
|||||||
text-decoration-color: var(--color-signal);
|
text-decoration-color: var(--color-signal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.link,
|
||||||
|
.field,
|
||||||
|
.badge {
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
/* --- salto de accesibilidad --- */
|
/* --- salto de accesibilidad --- */
|
||||||
.skip {
|
.skip {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|||||||
@@ -1,20 +1,27 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { RielClasesSelector } from "@/components/marca/RielClases";
|
|
||||||
import type { ClaseFuego } from "@/data/clases-fuego";
|
import type { ClaseFuego } from "@/data/clases-fuego";
|
||||||
import { OPCIONES_INDUSTRIA } from "@/data/industrias";
|
|
||||||
import type { Canal } from "@/lib/crm/constantes";
|
import type { Canal } from "@/lib/crm/constantes";
|
||||||
import type { RespuestaLead } from "@/lib/leads/esquema";
|
import type { RespuestaLead } from "@/lib/leads/esquema";
|
||||||
import { capturarAtribucion, type Atribucion } from "@/lib/utm";
|
import { capturarAtribucion, type Atribucion } from "@/lib/utm";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Formulario de captación embebido. Va en el inicio, en el catálogo y en cada
|
* FORMULARIO DE CAPTACIÓN — cuatro campos, no catorce.
|
||||||
* ficha de producto, y siempre viaja con su contexto: qué producto se estaba
|
|
||||||
* viendo, en qué URL y con qué campaña llegó el visitante.
|
|
||||||
*
|
*
|
||||||
* No pide "solicita información": pide los datos que un ingeniero necesita para
|
* La versión anterior preguntaba industria, clases de fuego, superficie,
|
||||||
* cotizar sin una segunda vuelta de correos.
|
* urgencia, empresa, ciudad y puesto. Buena información, mala conversión: cada
|
||||||
|
* campo adicional es una razón más para cerrar la pestaña, y quien está
|
||||||
|
* comparando proveedores no llena un cuestionario técnico en la primera visita.
|
||||||
|
*
|
||||||
|
* Ahora se piden nombre, correo, teléfono y mensaje. Todo lo demás —producto de
|
||||||
|
* interés, categoría, industria, escenario, clases de fuego, campaña— se deduce
|
||||||
|
* del contexto de la página y viaja en campos ocultos. El vendedor recibe la
|
||||||
|
* misma ficha completa; el visitante solo escribió cuatro cosas.
|
||||||
|
*
|
||||||
|
* `mensajeInicial` permite que un botón como "Cotizar este equipo" deje el
|
||||||
|
* formulario listo con el texto ya redactado, para que el visitante solo ponga
|
||||||
|
* sus datos y envíe.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export type ProductoInteres = {
|
export type ProductoInteres = {
|
||||||
@@ -27,44 +34,49 @@ type Props = {
|
|||||||
canal: Canal;
|
canal: Canal;
|
||||||
titulo?: string;
|
titulo?: string;
|
||||||
descripcion?: string;
|
descripcion?: string;
|
||||||
|
/** Ancla para que los botones de cotización lleguen aquí. */
|
||||||
|
id?: string;
|
||||||
/** Cuando el formulario vive en una ficha, el producto ya viene resuelto. */
|
/** Cuando el formulario vive en una ficha, el producto ya viene resuelto. */
|
||||||
producto?: ProductoInteres;
|
producto?: ProductoInteres;
|
||||||
industriaPreseleccionada?: string;
|
/** Contexto que el formulario no pregunta pero sí registra. */
|
||||||
clasesPreseleccionadas?: ClaseFuego[];
|
industria?: string;
|
||||||
|
escenario?: string;
|
||||||
|
clases?: ClaseFuego[];
|
||||||
|
/** Texto con el que arranca el campo de mensaje. */
|
||||||
|
mensajeInicial?: string;
|
||||||
|
/** Texto del botón. Por omisión se ajusta según haya producto o no. */
|
||||||
|
textoBoton?: string;
|
||||||
/** "claro" para las secciones sobre papel. */
|
/** "claro" para las secciones sobre papel. */
|
||||||
variante?: "oscuro" | "claro";
|
variante?: "oscuro" | "claro";
|
||||||
/** Solo nombre, correo, teléfono y mensaje. Para espacios reducidos. */
|
|
||||||
compacto?: boolean;
|
|
||||||
className?: string;
|
className?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Estado = "listo" | "enviando" | "hecho" | "error";
|
type Estado = "listo" | "enviando" | "hecho" | "error";
|
||||||
|
|
||||||
const URGENCIAS = [
|
|
||||||
{ value: "inmediata", label: "Inmediata — tengo un hallazgo abierto" },
|
|
||||||
{ value: "trimestre", label: "Este trimestre" },
|
|
||||||
{ value: "presupuesto", label: "Estoy armando presupuesto" },
|
|
||||||
{ value: "explorando", label: "Explorando opciones" },
|
|
||||||
];
|
|
||||||
|
|
||||||
export function FormularioContacto({
|
export function FormularioContacto({
|
||||||
canal,
|
canal,
|
||||||
titulo = "Solicitar diagnóstico",
|
titulo = "Recibe tu cotización",
|
||||||
descripcion = "Cuéntanos qué operación hay que proteger. Te respondemos con el escenario, el agente adecuado y qué documentación te corresponde.",
|
descripcion = "Déjanos tus datos y un especialista te contacta con la solución que corresponde a tu operación y la documentación que te van a pedir.",
|
||||||
|
id,
|
||||||
producto,
|
producto,
|
||||||
industriaPreseleccionada,
|
industria,
|
||||||
clasesPreseleccionadas = [],
|
escenario,
|
||||||
|
clases,
|
||||||
|
mensajeInicial,
|
||||||
|
textoBoton,
|
||||||
variante = "oscuro",
|
variante = "oscuro",
|
||||||
compacto = false,
|
|
||||||
className = "",
|
className = "",
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const claro = variante === "claro";
|
const claro = variante === "claro";
|
||||||
|
// Los identificadores de campo mezclan canal e `id`: dos formularios del
|
||||||
|
// mismo canal en una misma página generaban ids duplicados, y una etiqueta
|
||||||
|
// apuntando a dos campos rompe el foco y el lector de pantalla.
|
||||||
|
const ref = id ? `${canal}-${id}` : canal;
|
||||||
const [estado, setEstado] = useState<Estado>("listo");
|
const [estado, setEstado] = useState<Estado>("listo");
|
||||||
const [errores, setErrores] = useState<Record<string, string>>({});
|
const [errores, setErrores] = useState<Record<string, string>>({});
|
||||||
const [errorGeneral, setErrorGeneral] = useState<string | null>(null);
|
const [errorGeneral, setErrorGeneral] = useState<string | null>(null);
|
||||||
const [folio, setFolio] = useState<string | null>(null);
|
const [folio, setFolio] = useState<string | null>(null);
|
||||||
const [atiende, setAtiende] = useState<string | null>(null);
|
const [atiende, setAtiende] = useState<string | null>(null);
|
||||||
const [clases, setClases] = useState<ClaseFuego[]>(clasesPreseleccionadas);
|
|
||||||
const atribucion = useRef<Atribucion>({});
|
const atribucion = useRef<Atribucion>({});
|
||||||
const regionEstado = useRef<HTMLDivElement>(null);
|
const regionEstado = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
@@ -72,7 +84,6 @@ export function FormularioContacto({
|
|||||||
atribucion.current = capturarAtribucion();
|
atribucion.current = capturarAtribucion();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// llevar el foco al aviso de resultado, para lector de pantalla
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (estado === "hecho" || estado === "error") regionEstado.current?.focus();
|
if (estado === "hecho" || estado === "error") regionEstado.current?.focus();
|
||||||
}, [estado]);
|
}, [estado]);
|
||||||
@@ -90,14 +101,11 @@ export function FormularioContacto({
|
|||||||
nombre: String(datos.get("nombre") ?? ""),
|
nombre: String(datos.get("nombre") ?? ""),
|
||||||
email: String(datos.get("email") ?? ""),
|
email: String(datos.get("email") ?? ""),
|
||||||
telefono: String(datos.get("telefono") ?? ""),
|
telefono: String(datos.get("telefono") ?? ""),
|
||||||
empresa: String(datos.get("empresa") ?? "") || undefined,
|
|
||||||
puesto: String(datos.get("puesto") ?? "") || undefined,
|
|
||||||
ciudad: String(datos.get("ciudad") ?? "") || undefined,
|
|
||||||
industria: String(datos.get("industria") ?? "") || undefined,
|
|
||||||
superficie: String(datos.get("superficie") ?? "") || undefined,
|
|
||||||
urgencia: String(datos.get("urgencia") ?? "") || undefined,
|
|
||||||
mensaje: String(datos.get("mensaje") ?? "") || undefined,
|
mensaje: String(datos.get("mensaje") ?? "") || undefined,
|
||||||
clases: clases.length ? clases : undefined,
|
// contexto: no se pregunta, se deduce de dónde ocurrió el envío
|
||||||
|
industria,
|
||||||
|
escenario,
|
||||||
|
clases: clases && clases.length ? clases : undefined,
|
||||||
origen: "FORMULARIO" as const,
|
origen: "FORMULARIO" as const,
|
||||||
canal,
|
canal,
|
||||||
productoSlug: producto?.slug,
|
productoSlug: producto?.slug,
|
||||||
@@ -137,14 +145,11 @@ export function FormularioContacto({
|
|||||||
if (estado === "hecho") {
|
if (estado === "hecho") {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
id={id}
|
||||||
ref={regionEstado}
|
ref={regionEstado}
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
role="status"
|
role="status"
|
||||||
className={[
|
className={[claro ? "panel-lite" : "panel", "p-7 outline-none sm:p-9", className].join(" ")}
|
||||||
claro ? "panel-lite" : "panel",
|
|
||||||
"p-7 outline-none sm:p-9",
|
|
||||||
className,
|
|
||||||
].join(" ")}
|
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2.5">
|
<div className="flex items-center gap-2.5">
|
||||||
<span className="flex h-7 w-7 items-center justify-center rounded-full bg-ok/15">
|
<span className="flex h-7 w-7 items-center justify-center rounded-full bg-ok/15">
|
||||||
@@ -169,21 +174,26 @@ export function FormularioContacto({
|
|||||||
claro ? "text-deep" : "text-bright",
|
claro ? "text-deep" : "text-bright",
|
||||||
].join(" ")}
|
].join(" ")}
|
||||||
>
|
>
|
||||||
Recibimos tu solicitud
|
Listo, ya tenemos tu caso
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<p className={["mt-3 text-sm leading-relaxed", claro ? "text-deep-muted" : "text-muted"].join(" ")}>
|
<p
|
||||||
|
className={[
|
||||||
|
"mt-3 text-sm leading-relaxed",
|
||||||
|
claro ? "text-deep-muted" : "text-muted",
|
||||||
|
].join(" ")}
|
||||||
|
>
|
||||||
{atiende
|
{atiende
|
||||||
? `${atiende} tiene tu caso asignado y da seguimiento.`
|
? `${atiende} tiene tu solicitud asignada y te contacta.`
|
||||||
: "Un especialista tiene tu caso asignado y da seguimiento."}{" "}
|
: "Un especialista tiene tu solicitud asignada y te contacta."}{" "}
|
||||||
Si tu situación no puede esperar, marca directo al{" "}
|
Si no puede esperar, marca directo al{" "}
|
||||||
<a href="tel:+525540348395" className="link">
|
<a href="tel:+525540348395" className="link">
|
||||||
55 4034 8395
|
55 4034 8395
|
||||||
</a>
|
</a>
|
||||||
.
|
.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<dl className={["spec mt-6", claro ? "" : ""].join(" ")} style={{ gridTemplateColumns: "1fr 1fr" }}>
|
<dl className="spec mt-6" style={{ gridTemplateColumns: "1fr 1fr" }}>
|
||||||
<div className="spec-cell">
|
<div className="spec-cell">
|
||||||
<dt className="spec-key">Folio</dt>
|
<dt className="spec-key">Folio</dt>
|
||||||
<dd className="spec-val">{folio}</dd>
|
<dd className="spec-val">{folio}</dd>
|
||||||
@@ -207,11 +217,16 @@ export function FormularioContacto({
|
|||||||
// ---------- formulario ----------
|
// ---------- formulario ----------
|
||||||
const claseCampo = claro ? "field field-lite" : "field";
|
const claseCampo = claro ? "field field-lite" : "field";
|
||||||
const claseEtiqueta = claro ? "field-label !text-deep-muted" : "field-label";
|
const claseEtiqueta = claro ? "field-label !text-deep-muted" : "field-label";
|
||||||
|
const etiquetaBoton =
|
||||||
|
textoBoton ?? "Recibir cotización";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={[claro ? "panel-lite" : "panel", "p-6 sm:p-8", className].join(" ")}>
|
<div
|
||||||
|
id={id}
|
||||||
|
className={[claro ? "panel-lite" : "panel", "scroll-mt-28 p-6 sm:p-8", className].join(" ")}
|
||||||
|
>
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<p className="eyebrow mb-3">{producto ? "Cotizar este equipo" : "Diagnóstico sin costo"}</p>
|
<p className="eyebrow mb-3">{producto ? "Cotizar este equipo" : "Cotización sin costo"}</p>
|
||||||
<h3
|
<h3
|
||||||
className={[
|
className={[
|
||||||
"font-display text-2xl font-extrabold leading-tight sm:text-[1.75rem]",
|
"font-display text-2xl font-extrabold leading-tight sm:text-[1.75rem]",
|
||||||
@@ -220,14 +235,19 @@ export function FormularioContacto({
|
|||||||
>
|
>
|
||||||
{titulo}
|
{titulo}
|
||||||
</h3>
|
</h3>
|
||||||
<p className={["mt-2.5 text-sm leading-relaxed", claro ? "text-deep-muted" : "text-muted"].join(" ")}>
|
<p
|
||||||
|
className={[
|
||||||
|
"mt-2.5 text-sm leading-relaxed",
|
||||||
|
claro ? "text-deep-muted" : "text-muted",
|
||||||
|
].join(" ")}
|
||||||
|
>
|
||||||
{descripcion}
|
{descripcion}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{producto ? (
|
{producto ? (
|
||||||
<p className="mt-4 inline-flex items-center gap-2 rounded-[3px] border border-signal/40 bg-signal/10 px-2.5 py-1.5">
|
<p className="mt-4 inline-flex flex-wrap items-center gap-2 rounded-[3px] border border-signal/40 bg-signal/10 px-2.5 py-1.5">
|
||||||
<span className="font-mono text-[0.625rem] uppercase tracking-[0.12em] text-signal-hi">
|
<span className="font-mono text-[0.625rem] uppercase tracking-[0.12em] text-signal-hi">
|
||||||
Interés
|
Cotizando
|
||||||
</span>
|
</span>
|
||||||
<span className={["font-mono text-xs", claro ? "text-deep" : "text-bright"].join(" ")}>
|
<span className={["font-mono text-xs", claro ? "text-deep" : "text-bright"].join(" ")}>
|
||||||
{producto.nombre}
|
{producto.nombre}
|
||||||
@@ -239,17 +259,23 @@ export function FormularioContacto({
|
|||||||
<form onSubmit={enviar} noValidate>
|
<form onSubmit={enviar} noValidate>
|
||||||
{/* señuelo antispam: invisible y fuera del orden de tabulación */}
|
{/* señuelo antispam: invisible y fuera del orden de tabulación */}
|
||||||
<div aria-hidden="true" className="absolute h-0 w-0 overflow-hidden opacity-0">
|
<div aria-hidden="true" className="absolute h-0 w-0 overflow-hidden opacity-0">
|
||||||
<label htmlFor={`sitio-${canal}`}>No llenar</label>
|
<label htmlFor={`sitio-${ref}`}>No llenar</label>
|
||||||
<input id={`sitio-${canal}`} name="_sitio" type="text" tabIndex={-1} autoComplete="off" />
|
<input
|
||||||
|
id={`sitio-${ref}`}
|
||||||
|
name="_sitio"
|
||||||
|
type="text"
|
||||||
|
tabIndex={-1}
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-4 sm:grid-cols-2">
|
<div className="grid gap-4">
|
||||||
<div className="sm:col-span-2">
|
<div>
|
||||||
<label htmlFor={`nombre-${canal}`} className={claseEtiqueta}>
|
<label htmlFor={`nombre-${ref}`} className={claseEtiqueta}>
|
||||||
Nombre
|
Nombre completo
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
id={`nombre-${canal}`}
|
id={`nombre-${ref}`}
|
||||||
name="nombre"
|
name="nombre"
|
||||||
type="text"
|
type="text"
|
||||||
required
|
required
|
||||||
@@ -257,190 +283,73 @@ export function FormularioContacto({
|
|||||||
placeholder="Nombre y apellido"
|
placeholder="Nombre y apellido"
|
||||||
className={claseCampo}
|
className={claseCampo}
|
||||||
aria-invalid={Boolean(errores.nombre)}
|
aria-invalid={Boolean(errores.nombre)}
|
||||||
aria-describedby={errores.nombre ? `err-nombre-${canal}` : undefined}
|
aria-describedby={errores.nombre ? `err-nombre-${ref}` : undefined}
|
||||||
/>
|
/>
|
||||||
{errores.nombre ? (
|
{errores.nombre ? (
|
||||||
<span id={`err-nombre-${canal}`} className="field-error">
|
<span id={`err-nombre-${ref}`} className="field-error">
|
||||||
{errores.nombre}
|
{errores.nombre}
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div className="grid gap-4 sm:grid-cols-2">
|
||||||
<label htmlFor={`email-${canal}`} className={claseEtiqueta}>
|
<div>
|
||||||
Correo
|
<label htmlFor={`email-${ref}`} className={claseEtiqueta}>
|
||||||
</label>
|
Correo
|
||||||
<input
|
</label>
|
||||||
id={`email-${canal}`}
|
<input
|
||||||
name="email"
|
id={`email-${ref}`}
|
||||||
type="email"
|
name="email"
|
||||||
required
|
type="email"
|
||||||
autoComplete="email"
|
required
|
||||||
inputMode="email"
|
autoComplete="email"
|
||||||
placeholder="[email protected]"
|
inputMode="email"
|
||||||
className={claseCampo}
|
placeholder="[email protected]"
|
||||||
aria-invalid={Boolean(errores.email)}
|
className={claseCampo}
|
||||||
aria-describedby={errores.email ? `err-email-${canal}` : undefined}
|
aria-invalid={Boolean(errores.email)}
|
||||||
/>
|
aria-describedby={errores.email ? `err-email-${ref}` : undefined}
|
||||||
{errores.email ? (
|
/>
|
||||||
<span id={`err-email-${canal}`} className="field-error">
|
{errores.email ? (
|
||||||
{errores.email}
|
<span id={`err-email-${ref}`} className="field-error">
|
||||||
</span>
|
{errores.email}
|
||||||
) : null}
|
</span>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor={`telefono-${ref}`} className={claseEtiqueta}>
|
||||||
|
Teléfono
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id={`telefono-${ref}`}
|
||||||
|
name="telefono"
|
||||||
|
type="tel"
|
||||||
|
required
|
||||||
|
autoComplete="tel"
|
||||||
|
inputMode="tel"
|
||||||
|
placeholder="55 1234 5678"
|
||||||
|
className={claseCampo}
|
||||||
|
aria-invalid={Boolean(errores.telefono)}
|
||||||
|
aria-describedby={errores.telefono ? `err-telefono-${ref}` : undefined}
|
||||||
|
/>
|
||||||
|
{errores.telefono ? (
|
||||||
|
<span id={`err-telefono-${ref}`} className="field-error">
|
||||||
|
{errores.telefono}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor={`telefono-${canal}`} className={claseEtiqueta}>
|
<label htmlFor={`mensaje-${ref}`} className={claseEtiqueta}>
|
||||||
Teléfono
|
Mensaje
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id={`telefono-${canal}`}
|
|
||||||
name="telefono"
|
|
||||||
type="tel"
|
|
||||||
required
|
|
||||||
autoComplete="tel"
|
|
||||||
inputMode="tel"
|
|
||||||
placeholder="55 1234 5678"
|
|
||||||
className={claseCampo}
|
|
||||||
aria-invalid={Boolean(errores.telefono)}
|
|
||||||
aria-describedby={errores.telefono ? `err-telefono-${canal}` : undefined}
|
|
||||||
/>
|
|
||||||
{errores.telefono ? (
|
|
||||||
<span id={`err-telefono-${canal}`} className="field-error">
|
|
||||||
{errores.telefono}
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{!compacto ? (
|
|
||||||
<>
|
|
||||||
<div>
|
|
||||||
<label htmlFor={`empresa-${canal}`} className={claseEtiqueta}>
|
|
||||||
Empresa
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id={`empresa-${canal}`}
|
|
||||||
name="empresa"
|
|
||||||
type="text"
|
|
||||||
autoComplete="organization"
|
|
||||||
placeholder="Razón social o nombre comercial"
|
|
||||||
className={claseCampo}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label htmlFor={`ciudad-${canal}`} className={claseEtiqueta}>
|
|
||||||
Ciudad o estado
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id={`ciudad-${canal}`}
|
|
||||||
name="ciudad"
|
|
||||||
type="text"
|
|
||||||
autoComplete="address-level2"
|
|
||||||
placeholder="Dónde está la instalación"
|
|
||||||
className={claseCampo}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label htmlFor={`industria-${canal}`} className={claseEtiqueta}>
|
|
||||||
Tipo de operación
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
id={`industria-${canal}`}
|
|
||||||
name="industria"
|
|
||||||
defaultValue={industriaPreseleccionada ?? ""}
|
|
||||||
className={`${claseCampo} appearance-none [-webkit-appearance:none]`}
|
|
||||||
>
|
|
||||||
<option value="">Selecciona una</option>
|
|
||||||
{OPCIONES_INDUSTRIA.map((o) => (
|
|
||||||
<option key={o.value} value={o.value}>
|
|
||||||
{o.label}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label htmlFor={`urgencia-${canal}`} className={claseEtiqueta}>
|
|
||||||
Cuándo lo necesitas
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
id={`urgencia-${canal}`}
|
|
||||||
name="urgencia"
|
|
||||||
defaultValue=""
|
|
||||||
className={`${claseCampo} appearance-none [-webkit-appearance:none]`}
|
|
||||||
>
|
|
||||||
<option value="">Selecciona una</option>
|
|
||||||
{URGENCIAS.map((u) => (
|
|
||||||
<option key={u.value} value={u.value}>
|
|
||||||
{u.label}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label htmlFor={`superficie-${canal}`} className={claseEtiqueta}>
|
|
||||||
Superficie a proteger
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id={`superficie-${canal}`}
|
|
||||||
name="superficie"
|
|
||||||
type="text"
|
|
||||||
placeholder="Ej. 4 500 m² en dos naves"
|
|
||||||
className={claseCampo}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label htmlFor={`puesto-${canal}`} className={claseEtiqueta}>
|
|
||||||
Tu puesto
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id={`puesto-${canal}`}
|
|
||||||
name="puesto"
|
|
||||||
type="text"
|
|
||||||
autoComplete="organization-title"
|
|
||||||
placeholder="Ej. Jefe de seguridad industrial"
|
|
||||||
className={claseCampo}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* el riel: el visitante declara su propio riesgo */}
|
|
||||||
<div className="sm:col-span-2">
|
|
||||||
<span className={claseEtiqueta}>Clases de fuego presentes</span>
|
|
||||||
<div
|
|
||||||
className={[
|
|
||||||
"flex flex-wrap items-center gap-4 rounded-[3px] border px-3.5 py-3",
|
|
||||||
claro ? "border-rule-lite bg-paper-2/60" : "border-rule-2 bg-ink/60",
|
|
||||||
].join(" ")}
|
|
||||||
>
|
|
||||||
<RielClasesSelector valor={clases} onChange={setClases} tamano="md" />
|
|
||||||
<p
|
|
||||||
className={[
|
|
||||||
"font-mono text-[0.625rem] leading-relaxed tracking-[0.04em]",
|
|
||||||
claro ? "text-deep-muted" : "text-faint",
|
|
||||||
].join(" ")}
|
|
||||||
>
|
|
||||||
{clases.length === 0
|
|
||||||
? "Si no las conoces, déjalo vacío y lo clasificamos nosotros"
|
|
||||||
: `Declaraste ${clases.length} ${clases.length === 1 ? "clase" : "clases"}`}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<div className="sm:col-span-2">
|
|
||||||
<label htmlFor={`mensaje-${canal}`} className={claseEtiqueta}>
|
|
||||||
Qué hay que proteger
|
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
id={`mensaje-${canal}`}
|
id={`mensaje-${ref}`}
|
||||||
name="mensaje"
|
name="mensaje"
|
||||||
rows={compacto ? 3 : 4}
|
rows={4}
|
||||||
placeholder="Describe la instalación, el material que se maneja y qué te pidieron cumplir."
|
defaultValue={mensajeInicial}
|
||||||
|
placeholder="Cuéntanos brevemente qué necesitas proteger. Si no sabes por dónde empezar, escríbelo así y nosotros preguntamos lo demás."
|
||||||
className={`${claseCampo} resize-y`}
|
className={`${claseCampo} resize-y`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -456,10 +365,9 @@ export function FormularioContacto({
|
|||||||
aria-invalid={Boolean(errores.privacidad)}
|
aria-invalid={Boolean(errores.privacidad)}
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
className={[
|
className={["text-xs leading-relaxed", claro ? "text-deep-muted" : "text-muted"].join(
|
||||||
"text-xs leading-relaxed",
|
" ",
|
||||||
claro ? "text-deep-muted" : "text-muted",
|
)}
|
||||||
].join(" ")}
|
|
||||||
>
|
>
|
||||||
Acepto que FSPM use mis datos para responder esta solicitud, conforme al{" "}
|
Acepto que FSPM use mis datos para responder esta solicitud, conforme al{" "}
|
||||||
<a href="/aviso-de-privacidad" className="link">
|
<a href="/aviso-de-privacidad" className="link">
|
||||||
@@ -482,8 +390,12 @@ export function FormularioContacto({
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<button type="submit" disabled={estado === "enviando"} className="btn btn-signal mt-6 w-full">
|
<button
|
||||||
{estado === "enviando" ? "Enviando…" : producto ? "Solicitar cotización" : "Solicitar diagnóstico"}
|
type="submit"
|
||||||
|
disabled={estado === "enviando"}
|
||||||
|
className="btn btn-signal mt-6 w-full"
|
||||||
|
>
|
||||||
|
{estado === "enviando" ? "Enviando…" : etiquetaBoton}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<p
|
<p
|
||||||
@@ -492,7 +404,7 @@ export function FormularioContacto({
|
|||||||
claro ? "text-deep-muted" : "text-faint",
|
claro ? "text-deep-muted" : "text-faint",
|
||||||
].join(" ")}
|
].join(" ")}
|
||||||
>
|
>
|
||||||
Respuesta en horario hábil. Sin costo y sin compromiso de compra.
|
Sin costo y sin compromiso de compra · Cuatro campos, nada más
|
||||||
</p>
|
</p>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -14,14 +14,6 @@ import { capturarAtribucion, type Atribucion } from "@/lib/utm";
|
|||||||
* mensaje, y la campaña que lo trajo queda medida.
|
* mensaje, y la campaña que lo trajo queda medida.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const MOTIVOS = [
|
|
||||||
{ value: "diagnostico", label: "Quiero un diagnóstico de mi instalación" },
|
|
||||||
{ value: "cotizacion", label: "Necesito cotizar equipo" },
|
|
||||||
{ value: "cumplimiento", label: "Tengo una verificación o auditoría encima" },
|
|
||||||
{ value: "urgencia", label: "Es urgente, tuve un incidente" },
|
|
||||||
{ value: "otro", label: "Otro tema" },
|
|
||||||
];
|
|
||||||
|
|
||||||
type Estado = "listo" | "enviando" | "hecho" | "error";
|
type Estado = "listo" | "enviando" | "hecho" | "error";
|
||||||
|
|
||||||
export function WidgetWhatsApp() {
|
export function WidgetWhatsApp() {
|
||||||
@@ -98,9 +90,6 @@ export function WidgetWhatsApp() {
|
|||||||
if (estado === "enviando") return;
|
if (estado === "enviando") return;
|
||||||
|
|
||||||
const datos = new FormData(evento.currentTarget);
|
const datos = new FormData(evento.currentTarget);
|
||||||
const motivo = String(datos.get("motivo") ?? "");
|
|
||||||
const etiquetaMotivo = MOTIVOS.find((m) => m.value === motivo)?.label ?? motivo;
|
|
||||||
|
|
||||||
setEstado("enviando");
|
setEstado("enviando");
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
@@ -112,9 +101,7 @@ export function WidgetWhatsApp() {
|
|||||||
nombre: String(datos.get("nombre") ?? ""),
|
nombre: String(datos.get("nombre") ?? ""),
|
||||||
email: String(datos.get("email") ?? ""),
|
email: String(datos.get("email") ?? ""),
|
||||||
telefono: String(datos.get("telefono") ?? ""),
|
telefono: String(datos.get("telefono") ?? ""),
|
||||||
empresa: String(datos.get("empresa") ?? "") || undefined,
|
mensaje: String(datos.get("mensaje") ?? "") || undefined,
|
||||||
mensaje: etiquetaMotivo,
|
|
||||||
urgencia: motivo === "urgencia" ? "inmediata" : undefined,
|
|
||||||
origen: "WHATSAPP" as const,
|
origen: "WHATSAPP" as const,
|
||||||
canal: "widget" as const,
|
canal: "widget" as const,
|
||||||
productoUrl:
|
productoUrl:
|
||||||
@@ -308,37 +295,17 @@ export function WidgetWhatsApp() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="wa-empresa" className="field-label">
|
<label htmlFor="wa-mensaje" className="field-label">
|
||||||
Empresa
|
Mensaje
|
||||||
</label>
|
</label>
|
||||||
<input
|
<textarea
|
||||||
id="wa-empresa"
|
id="wa-mensaje"
|
||||||
name="empresa"
|
name="mensaje"
|
||||||
type="text"
|
rows={3}
|
||||||
autoComplete="organization"
|
placeholder="Cuéntanos qué necesitas."
|
||||||
placeholder="Opcional"
|
className="field resize-y"
|
||||||
className="field"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
|
||||||
<label htmlFor="wa-motivo" className="field-label">
|
|
||||||
Motivo
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
id="wa-motivo"
|
|
||||||
name="motivo"
|
|
||||||
required
|
|
||||||
defaultValue="diagnostico"
|
|
||||||
className="field appearance-none [-webkit-appearance:none]"
|
|
||||||
>
|
|
||||||
{MOTIVOS.map((m) => (
|
|
||||||
<option key={m.value} value={m.value}>
|
|
||||||
{m.label}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<label className="mt-4 flex cursor-pointer items-start gap-2.5">
|
<label className="mt-4 flex cursor-pointer items-start gap-2.5">
|
||||||
@@ -368,7 +335,7 @@ export function WidgetWhatsApp() {
|
|||||||
disabled={estado === "enviando"}
|
disabled={estado === "enviando"}
|
||||||
className="btn btn-signal mt-4 w-full"
|
className="btn btn-signal mt-4 w-full"
|
||||||
>
|
>
|
||||||
{estado === "enviando" ? "Registrando…" : "Abrir WhatsApp"}
|
{estado === "enviando" ? "Enviando…" : "Enviar mensaje"}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -0,0 +1,169 @@
|
|||||||
|
import Image from "next/image";
|
||||||
|
import Link from "next/link";
|
||||||
|
import type { Escenario } from "@/data/escenarios";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Iconografía de escenario, tomada del sitio original de FSPM.
|
||||||
|
*
|
||||||
|
* Los iconos son círculos naranjas con glifo blanco, así que se sostienen
|
||||||
|
* sobre el fondo oscuro sin marco adicional. Su función no es decorar: son la
|
||||||
|
* entrada para quien no sabe qué clase de fuego tiene pero sí sabe que
|
||||||
|
* administra una gasolinera.
|
||||||
|
*
|
||||||
|
* Tres presentaciones, según el espacio:
|
||||||
|
* ficha tarjeta completa con riesgo explicado, para elegir
|
||||||
|
* pastilla insignia compacta, para listar dónde aplica un producto
|
||||||
|
* marca solo el icono con su nombre accesible
|
||||||
|
*/
|
||||||
|
|
||||||
|
const TAMANO = { sm: 26, md: 34, lg: 46 } as const;
|
||||||
|
|
||||||
|
export function IconoEscenario({
|
||||||
|
escenario,
|
||||||
|
tamano = "md",
|
||||||
|
className = "",
|
||||||
|
}: {
|
||||||
|
escenario: Escenario;
|
||||||
|
tamano?: keyof typeof TAMANO;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
const px = TAMANO[tamano];
|
||||||
|
|
||||||
|
// El set original de FSPM no cubre los trece escenarios. Donde falta el
|
||||||
|
// archivo se dibuja un marcador tipográfico con la inicial, en el naranja de
|
||||||
|
// los iconos, para que la rejilla mantenga su ritmo. Un icono prestado de
|
||||||
|
// otro escenario sería peor: en una rejilla que existe para reconocer tu
|
||||||
|
// lugar de un vistazo, dos tarjetas con el mismo símbolo se leen como error.
|
||||||
|
if (!escenario.icono) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
className={`flex shrink-0 items-center justify-center rounded-full font-mono font-semibold leading-none text-white ${className}`}
|
||||||
|
style={{
|
||||||
|
width: px,
|
||||||
|
height: px,
|
||||||
|
backgroundColor: "#f0842a",
|
||||||
|
fontSize: Math.round(px * 0.42),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{escenario.nombre.charAt(0)}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Image
|
||||||
|
src={escenario.icono}
|
||||||
|
alt=""
|
||||||
|
aria-hidden="true"
|
||||||
|
width={px}
|
||||||
|
height={px}
|
||||||
|
className={`shrink-0 ${className}`}
|
||||||
|
style={{ width: px, height: px }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Insignia compacta: icono más nombre. Para listas de aplicación. */
|
||||||
|
export function PastillaEscenario({
|
||||||
|
escenario,
|
||||||
|
claro = false,
|
||||||
|
}: {
|
||||||
|
escenario: Escenario;
|
||||||
|
claro?: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={[
|
||||||
|
"inline-flex items-center gap-2 rounded-[3px] border px-2.5 py-1.5",
|
||||||
|
claro ? "border-rule-lite bg-white" : "border-rule bg-ink-2",
|
||||||
|
].join(" ")}
|
||||||
|
title={escenario.enLlano}
|
||||||
|
>
|
||||||
|
<IconoEscenario escenario={escenario} tamano="sm" />
|
||||||
|
<span
|
||||||
|
className={[
|
||||||
|
"font-mono text-[0.6875rem] leading-none tracking-[0.04em]",
|
||||||
|
claro ? "text-deep" : "text-bright",
|
||||||
|
].join(" ")}
|
||||||
|
>
|
||||||
|
{escenario.nombre}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tarjeta de escenario para elegir. Enlaza a donde el escenario deba llevar
|
||||||
|
* —normalmente el catálogo filtrado— y explica el riesgo en lenguaje llano.
|
||||||
|
*/
|
||||||
|
export function TarjetaEscenario({
|
||||||
|
escenario,
|
||||||
|
href,
|
||||||
|
claro = false,
|
||||||
|
conRiesgo = true,
|
||||||
|
}: {
|
||||||
|
escenario: Escenario;
|
||||||
|
href: string;
|
||||||
|
claro?: boolean;
|
||||||
|
conRiesgo?: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
href={href}
|
||||||
|
className={[
|
||||||
|
"group flex flex-col gap-3 rounded-[4px] border p-4 transition-colors duration-150",
|
||||||
|
claro
|
||||||
|
? "border-rule-lite bg-white hover:border-signal"
|
||||||
|
: "border-rule bg-ink-2 hover:border-signal",
|
||||||
|
].join(" ")}
|
||||||
|
style={{ WebkitTapHighlightColor: "transparent" }}
|
||||||
|
>
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<IconoEscenario escenario={escenario} tamano="lg" />
|
||||||
|
<span className="min-w-0">
|
||||||
|
<span
|
||||||
|
className={[
|
||||||
|
"block font-display text-base font-bold leading-tight",
|
||||||
|
claro ? "text-deep" : "text-bright",
|
||||||
|
].join(" ")}
|
||||||
|
>
|
||||||
|
{escenario.nombre}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className={[
|
||||||
|
"mt-1 block text-xs leading-snug",
|
||||||
|
claro ? "text-deep-muted" : "text-muted",
|
||||||
|
].join(" ")}
|
||||||
|
>
|
||||||
|
{escenario.enLlano}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{conRiesgo ? (
|
||||||
|
<p
|
||||||
|
className={[
|
||||||
|
"border-l-2 border-ember/50 pl-3 text-xs leading-relaxed",
|
||||||
|
claro ? "text-deep-muted" : "text-faint",
|
||||||
|
].join(" ")}
|
||||||
|
>
|
||||||
|
{escenario.riesgo}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<span className="mt-auto flex items-center gap-1.5 font-mono text-[0.625rem] uppercase tracking-[0.14em] text-signal transition-colors group-hover:text-signal-hi">
|
||||||
|
Ver qué necesita
|
||||||
|
<svg width="11" height="8" viewBox="0 0 11 8" aria-hidden="true">
|
||||||
|
<path
|
||||||
|
d="M0 4h9M6 1l3 3-3 3"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.3"
|
||||||
|
fill="none"
|
||||||
|
strokeLinecap="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -139,7 +139,7 @@ export function Encabezado() {
|
|||||||
|
|
||||||
<div className="hidden shrink-0 items-center gap-2.5 lg:flex">
|
<div className="hidden shrink-0 items-center gap-2.5 lg:flex">
|
||||||
<Link href="/contacto" className="btn btn-signal btn-sm">
|
<Link href="/contacto" className="btn btn-signal btn-sm">
|
||||||
Solicitar diagnóstico
|
Recibir cotización
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -219,7 +219,7 @@ export function Encabezado() {
|
|||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<Link href="/contacto" className="btn btn-signal w-full">
|
<Link href="/contacto" className="btn btn-signal w-full">
|
||||||
Solicitar diagnóstico
|
Recibir cotización
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<div className="mt-6 space-y-2">
|
<div className="mt-6 space-y-2">
|
||||||
|
|||||||
@@ -0,0 +1,135 @@
|
|||||||
|
import Image from "next/image";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HÉROE DE LANDING — encabezado con fotografía de fondo.
|
||||||
|
*
|
||||||
|
* Toda página pública abre con imagen. El tratamiento no es un velo gris
|
||||||
|
* uniforme: es un degradado direccional desde `--color-ink` que deja opaco el
|
||||||
|
* lado donde vive el texto y translúcido el lado de la imagen, para que la
|
||||||
|
* fotografía se vea y el texto siga siendo legible sobre cualquier zona clara.
|
||||||
|
*
|
||||||
|
* Las fotografías son las del propio archivo de FSPM, en blanco y negro. Hay
|
||||||
|
* diez y hay más páginas que eso, así que varias se reutilizan: el `enfoque`
|
||||||
|
* permite recortar distinto la misma imagen y que dos páginas no se vean
|
||||||
|
* idénticas.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const FOTOS = {
|
||||||
|
bombero: "/media/foto/bombero-plataforma.jpg",
|
||||||
|
practica: "/media/foto/fspm-productos-portada.jpg",
|
||||||
|
planta: "/media/foto/bg-02.jpg",
|
||||||
|
galeria: "/media/foto/fspm-galeria.jpg",
|
||||||
|
portadaGaleria: "/media/foto/portada-fspm-galeria.jpg",
|
||||||
|
normas: "/media/foto/fspm-normas-y-certificaciones.jpg",
|
||||||
|
contacto: "/media/foto/fspm-contacto.jpg",
|
||||||
|
publicaciones: "/media/foto/back-publicaciones.jpg",
|
||||||
|
proteccionCivil: "/media/foto/libro-proteccion-civil.jpg",
|
||||||
|
clientes: "/media/foto/fspm-fondo-clientes.jpg",
|
||||||
|
mapa: "/media/foto/mapa-mexico.jpg",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
/** Etiqueta corta en mono, arriba del título. */
|
||||||
|
eyebrow?: string;
|
||||||
|
/** El `<h1>` de la página. Solo uno por página. */
|
||||||
|
titulo: string;
|
||||||
|
/** Bajada. Dos líneas máximo; si necesitas más, va en el cuerpo. */
|
||||||
|
entrada?: string;
|
||||||
|
/** Ruta de la fotografía. Usa `FOTOS`. */
|
||||||
|
foto: string;
|
||||||
|
/** Posición del recorte, para variar entre páginas que comparten imagen. */
|
||||||
|
enfoque?: string;
|
||||||
|
/** Contenido bajo la entrada: llamados a la acción, insignias, riel de clases. */
|
||||||
|
children?: React.ReactNode;
|
||||||
|
/** Migas de pan u otro elemento que va arriba del eyebrow. */
|
||||||
|
antes?: React.ReactNode;
|
||||||
|
/** "alto" para el inicio y las rutas por industria; "medio" para el resto. */
|
||||||
|
altura?: "medio" | "alto";
|
||||||
|
/** Texto alternativo. Vacío cuando la imagen es puramente ambiental. */
|
||||||
|
alt?: string;
|
||||||
|
/** Solo la primera imagen de la página debe llevar prioridad de carga. */
|
||||||
|
prioridad?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ALTURA = {
|
||||||
|
medio: "py-16 sm:py-20 lg:py-24",
|
||||||
|
alto: "py-20 sm:py-28 lg:py-32",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export function HeroeLanding({
|
||||||
|
eyebrow,
|
||||||
|
titulo,
|
||||||
|
entrada,
|
||||||
|
foto,
|
||||||
|
enfoque = "center",
|
||||||
|
children,
|
||||||
|
antes,
|
||||||
|
altura = "medio",
|
||||||
|
alt = "",
|
||||||
|
prioridad = false,
|
||||||
|
}: Props) {
|
||||||
|
return (
|
||||||
|
<section className="relative isolate overflow-hidden bg-ink">
|
||||||
|
<div className="absolute inset-0 -z-20">
|
||||||
|
<Image
|
||||||
|
src={foto}
|
||||||
|
alt={alt}
|
||||||
|
aria-hidden={alt === "" ? "true" : undefined}
|
||||||
|
fill
|
||||||
|
priority={prioridad}
|
||||||
|
sizes="100vw"
|
||||||
|
className="object-cover"
|
||||||
|
style={{ objectPosition: enfoque }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Degradado direccional: opaco donde va el texto. */}
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 -z-10"
|
||||||
|
aria-hidden="true"
|
||||||
|
style={{
|
||||||
|
background:
|
||||||
|
"linear-gradient(97deg, var(--color-ink) 0%, rgba(12,17,22,0.97) 32%, rgba(12,17,22,0.85) 55%, rgba(12,17,22,0.5) 80%, rgba(12,17,22,0.34) 100%)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/* Cierre inferior, para que la sección siguiente entre sin costura. */}
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 -z-10"
|
||||||
|
aria-hidden="true"
|
||||||
|
style={{
|
||||||
|
background:
|
||||||
|
"linear-gradient(to top, var(--color-ink) 0%, rgba(12,17,22,0.4) 22%, rgba(12,17,22,0) 58%)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="grid-bg absolute inset-0 -z-10" aria-hidden="true" />
|
||||||
|
|
||||||
|
<div className={`shell relative ${ALTURA[altura]}`}>
|
||||||
|
{antes ? <div className="mb-5">{antes}</div> : null}
|
||||||
|
|
||||||
|
{eyebrow ? <p className="eyebrow anim-rise">{eyebrow}</p> : null}
|
||||||
|
|
||||||
|
<h1
|
||||||
|
className="anim-rise mt-4 max-w-[44rem] font-display font-black uppercase leading-[0.96] tracking-[-0.028em] text-bright"
|
||||||
|
style={{ animationDelay: "70ms", fontSize: "clamp(1.9rem, 5vw, 3.4rem)" }}
|
||||||
|
>
|
||||||
|
{titulo}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
{entrada ? (
|
||||||
|
<p
|
||||||
|
className="anim-rise mt-5 max-w-[38rem] text-base leading-relaxed text-muted sm:text-lg"
|
||||||
|
style={{ animationDelay: "140ms" }}
|
||||||
|
>
|
||||||
|
{entrada}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{children ? (
|
||||||
|
<div className="anim-rise mt-8" style={{ animationDelay: "210ms" }}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,288 +1,455 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { RielClases, RielClasesSelector } from "@/components/marca/RielClases";
|
import { FormularioContacto } from "@/components/formulario/FormularioContacto";
|
||||||
import { Revelar } from "@/components/site/Revelar";
|
import { FiltrosCatalogo, PanelFiltrosMovil } from "@/components/site/catalogo/FiltrosCatalogo";
|
||||||
|
import { TablaComparativa } from "@/components/site/catalogo/TablaComparativa";
|
||||||
import { TarjetaProducto } from "@/components/site/catalogo/TarjetaProducto";
|
import { TarjetaProducto } from "@/components/site/catalogo/TarjetaProducto";
|
||||||
|
import {
|
||||||
|
construirBuscador,
|
||||||
|
familiasDeAgente,
|
||||||
|
familiasDisponibles,
|
||||||
|
type Buscador,
|
||||||
|
type FamiliaAgente,
|
||||||
|
} from "@/components/site/catalogo/busqueda";
|
||||||
|
import {
|
||||||
|
construirConsulta,
|
||||||
|
cuantosFiltros,
|
||||||
|
hayFiltroActivo,
|
||||||
|
type EstadoCatalogo,
|
||||||
|
} from "@/components/site/catalogo/estado";
|
||||||
import { CATEGORIAS, PRODUCTOS, type Producto } from "@/data/catalogo";
|
import { CATEGORIAS, PRODUCTOS, type Producto } from "@/data/catalogo";
|
||||||
import type { ClaseFuego } from "@/data/clases-fuego";
|
import type { ClaseFuego } from "@/data/clases-fuego";
|
||||||
|
import { ESCENARIOS, getEscenario } from "@/data/escenarios";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* El catálogo como comparador, no como galería.
|
* EXPLORADOR DEL CATÁLOGO — tienda, no lección.
|
||||||
*
|
*
|
||||||
* Tres controles: clase de fuego (el riel, que es el vocabulario del oficio),
|
* La versión anterior explicaba antes de mostrar: dos «puertas» rotuladas, una
|
||||||
* categoría y aplicación. Filtran en el cliente, sin recarga. El resultado se
|
* rejilla de trece tarjetas de escenario con su riesgo desarrollado y párrafos
|
||||||
* ve dos veces a propósito: como tarjetas para reconocer el equipo y como hoja
|
* de contexto entre secciones. El cliente la rechazó por compleja y tiene
|
||||||
* de comparación para descartarlo. Es la recomendación 10.2 del estudio.
|
* razón: eso ya vive en el inicio, en /soluciones y en /cumplimiento-nom-002.
|
||||||
|
*
|
||||||
|
* Lo que queda es la mecánica de selección, que sí le gustó, montada como un
|
||||||
|
* comercio electrónico corriente:
|
||||||
|
*
|
||||||
|
* escritorio barra lateral fija con desplazamiento propio + rejilla de 3
|
||||||
|
* tableta rejilla de 2, filtros detrás del botón «Filtros (N)»
|
||||||
|
* móvil rejilla de 1, filtros en panel a pantalla completa
|
||||||
|
*
|
||||||
|
* ---- Lo que se conservó a propósito ----
|
||||||
|
*
|
||||||
|
* · El estado en la cadena de consulta. `/catalogo?escenario=gasolineras` se
|
||||||
|
* sigue abriendo filtrado, porque otras páginas ya enlazan así.
|
||||||
|
* · El motor de búsqueda con expresiones regulares y su `try/catch`, ahora
|
||||||
|
* presentado como un campo de búsqueda normal.
|
||||||
|
* · El estado vacío que invita a pedir diagnóstico en lugar de decir «0».
|
||||||
|
* · El formulario al cierre, con el escenario activo como contexto. Vive aquí
|
||||||
|
* y no en la página porque el escenario ahora se cambia sin navegar: si el
|
||||||
|
* formulario fuera del servidor, mandaría al CRM un escenario viejo.
|
||||||
|
* · La hoja de comparación, detrás de un desplegable cerrado. Sirve al
|
||||||
|
* comprador técnico, pero no es lo primero que hay que ver.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const CATEGORIA_NOMBRE = new Map(CATEGORIAS.map((c) => [c.slug, c.nombre]));
|
type Orden = "recomendado" | "nombre" | "clases";
|
||||||
|
|
||||||
const APLICACIONES: string[] = Array.from(
|
const ORDENES: Array<{ valor: Orden; etiqueta: string }> = [
|
||||||
new Set(PRODUCTOS.flatMap((p) => p.aplicaciones)),
|
{ valor: "recomendado", etiqueta: "Orden del catálogo" },
|
||||||
).sort((a, b) => a.localeCompare(b, "es-MX"));
|
{ valor: "nombre", etiqueta: "Nombre A–Z" },
|
||||||
|
{ valor: "clases", etiqueta: "Más clases cubiertas" },
|
||||||
|
];
|
||||||
|
|
||||||
const COLUMNAS = [
|
const FAMILIAS = familiasDisponibles(PRODUCTOS);
|
||||||
"Producto",
|
|
||||||
"Categoría",
|
|
||||||
"Clases",
|
|
||||||
"Capacidad",
|
|
||||||
"Agente",
|
|
||||||
"Alcance declarado",
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
|
/** Qué filtro se ignora al contar una faceta. */
|
||||||
|
type Omitir = "escenario" | "clases" | "categoria" | "agente" | null;
|
||||||
|
|
||||||
|
/** El filtro técnico es exigente a propósito: cubre TODAS las clases marcadas. */
|
||||||
function cubreClases(producto: Producto, clases: ClaseFuego[]): boolean {
|
function cubreClases(producto: Producto, clases: ClaseFuego[]): boolean {
|
||||||
return clases.every((c) => producto.clases.includes(c));
|
return clases.every((clase) => producto.clases.includes(clase));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ExploradorCatalogo() {
|
function esFamilia(valor: string): valor is FamiliaAgente {
|
||||||
const [clases, setClases] = useState<ClaseFuego[]>([]);
|
return (FAMILIAS as readonly string[]).includes(valor);
|
||||||
const [categoria, setCategoria] = useState("");
|
}
|
||||||
const [aplicacion, setAplicacion] = useState("");
|
|
||||||
|
/**
|
||||||
|
* Un solo predicado para los resultados y para los conteos por faceta. Con
|
||||||
|
* `omitir` se descuenta el filtro de la propia faceta, que es lo que hace que
|
||||||
|
* el número junto a cada categoría sea «cuántos habría si eligieras esta».
|
||||||
|
*/
|
||||||
|
function coincide(
|
||||||
|
producto: Producto,
|
||||||
|
estado: EstadoCatalogo,
|
||||||
|
buscador: Buscador,
|
||||||
|
omitir: Omitir = null,
|
||||||
|
): boolean {
|
||||||
|
if (omitir !== "escenario" && estado.escenario !== null) {
|
||||||
|
const escenario = getEscenario(estado.escenario);
|
||||||
|
if (escenario && !escenario.categorias.includes(producto.categoria)) return false;
|
||||||
|
}
|
||||||
|
if (omitir !== "clases" && !cubreClases(producto, estado.clases)) return false;
|
||||||
|
if (omitir !== "categoria" && estado.categoria !== "" && producto.categoria !== estado.categoria) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (omitir !== "agente" && estado.agente !== "" && esFamilia(estado.agente)) {
|
||||||
|
if (!familiasDeAgente(producto.agente).includes(estado.agente)) return false;
|
||||||
|
}
|
||||||
|
return buscador.coincide(producto);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ordenar(productos: Producto[], orden: Orden): Producto[] {
|
||||||
|
if (orden === "recomendado") return productos;
|
||||||
|
const copia = productos.slice();
|
||||||
|
if (orden === "nombre") {
|
||||||
|
return copia.sort((a, b) => a.nombre.localeCompare(b.nombre, "es"));
|
||||||
|
}
|
||||||
|
return copia.sort(
|
||||||
|
(a, b) => b.clases.length - a.clases.length || a.nombre.localeCompare(b.nombre, "es"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ExploradorCatalogo({ inicial }: { inicial: EstadoCatalogo }) {
|
||||||
|
const [estado, setEstado] = useState<EstadoCatalogo>(inicial);
|
||||||
|
const [orden, setOrden] = useState<Orden>("recomendado");
|
||||||
|
const [panelAbierto, setPanelAbierto] = useState(false);
|
||||||
|
const primeraVez = useRef(true);
|
||||||
|
|
||||||
|
const escenario = estado.escenario ? getEscenario(estado.escenario) : undefined;
|
||||||
|
|
||||||
|
// ---- la URL como estado compartible, sin viaje al servidor ----
|
||||||
|
useEffect(() => {
|
||||||
|
if (primeraVez.current) {
|
||||||
|
primeraVez.current = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const temporizador = window.setTimeout(() => {
|
||||||
|
const destino = `${window.location.pathname}${construirConsulta(estado)}`;
|
||||||
|
window.history.replaceState(window.history.state, "", destino);
|
||||||
|
}, 260);
|
||||||
|
return () => window.clearTimeout(temporizador);
|
||||||
|
}, [estado]);
|
||||||
|
|
||||||
|
const buscador = useMemo(() => construirBuscador(estado.q), [estado.q]);
|
||||||
|
|
||||||
const resultados = useMemo(
|
const resultados = useMemo(
|
||||||
() =>
|
() => ordenar(PRODUCTOS.filter((p) => coincide(p, estado, buscador)), orden),
|
||||||
PRODUCTOS.filter(
|
[estado, buscador, orden],
|
||||||
(p) =>
|
|
||||||
cubreClases(p, clases) &&
|
|
||||||
(categoria === "" || p.categoria === categoria) &&
|
|
||||||
(aplicacion === "" || p.aplicaciones.includes(aplicacion)),
|
|
||||||
),
|
|
||||||
[clases, categoria, aplicacion],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const hayFiltro = clases.length > 0 || categoria !== "" || aplicacion !== "";
|
// ---- conteos por faceta: el número que va junto a cada opción ----
|
||||||
|
const conteoPorCategoria = useMemo(() => {
|
||||||
|
const mapa = new Map<string, number>();
|
||||||
|
for (const categoria of CATEGORIAS) {
|
||||||
|
const cuantos = PRODUCTOS.filter(
|
||||||
|
(p) => p.categoria === categoria.slug && coincide(p, estado, buscador, "categoria"),
|
||||||
|
).length;
|
||||||
|
mapa.set(categoria.slug, cuantos);
|
||||||
|
}
|
||||||
|
return mapa;
|
||||||
|
}, [estado, buscador]);
|
||||||
|
|
||||||
function limpiar() {
|
const conteoPorEscenario = useMemo(() => {
|
||||||
setClases([]);
|
const mapa = new Map<string, number>();
|
||||||
setCategoria("");
|
for (const item of ESCENARIOS) {
|
||||||
setAplicacion("");
|
const cuantos = PRODUCTOS.filter(
|
||||||
}
|
(p) => item.categorias.includes(p.categoria) && coincide(p, estado, buscador, "escenario"),
|
||||||
|
).length;
|
||||||
|
mapa.set(item.slug, cuantos);
|
||||||
|
}
|
||||||
|
return mapa;
|
||||||
|
}, [estado, buscador]);
|
||||||
|
|
||||||
|
const conteoPorAgente = useMemo(() => {
|
||||||
|
const mapa = new Map<string, number>();
|
||||||
|
for (const familia of FAMILIAS) {
|
||||||
|
const cuantos = PRODUCTOS.filter(
|
||||||
|
(p) =>
|
||||||
|
familiasDeAgente(p.agente).includes(familia) &&
|
||||||
|
coincide(p, estado, buscador, "agente"),
|
||||||
|
).length;
|
||||||
|
mapa.set(familia, cuantos);
|
||||||
|
}
|
||||||
|
return mapa;
|
||||||
|
}, [estado, buscador]);
|
||||||
|
|
||||||
|
const hayFiltro = hayFiltroActivo(estado);
|
||||||
|
const activos = cuantosFiltros(estado);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Un cambio de escenario acota las categorías disponibles. Si la que estaba
|
||||||
|
* elegida no pertenece al escenario nuevo, el resultado sería cero equipos
|
||||||
|
* sin explicación: se suelta la categoría en el mismo movimiento.
|
||||||
|
*/
|
||||||
|
const cambiar = useCallback((parcial: Partial<EstadoCatalogo>) => {
|
||||||
|
setEstado((previo) => {
|
||||||
|
const siguiente = { ...previo, ...parcial };
|
||||||
|
if (parcial.escenario !== undefined && siguiente.escenario !== null) {
|
||||||
|
const nuevo = getEscenario(siguiente.escenario);
|
||||||
|
if (nuevo && siguiente.categoria !== "" && !nuevo.categorias.includes(siguiente.categoria)) {
|
||||||
|
siguiente.categoria = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return siguiente;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const limpiar = useCallback(() => {
|
||||||
|
setEstado({ escenario: null, clases: [], categoria: "", agente: "", q: "" });
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const filtros = (
|
||||||
|
<FiltrosCatalogo
|
||||||
|
estado={estado}
|
||||||
|
onCambio={cambiar}
|
||||||
|
conteoPorCategoria={conteoPorCategoria}
|
||||||
|
conteoPorEscenario={conteoPorEscenario}
|
||||||
|
conteoPorAgente={conteoPorAgente}
|
||||||
|
familias={FAMILIAS}
|
||||||
|
modoBusqueda={buscador.modo}
|
||||||
|
avisoBusqueda={buscador.aviso}
|
||||||
|
hayFiltro={hayFiltro}
|
||||||
|
onLimpiar={limpiar}
|
||||||
|
ambito="lateral"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
const conteo =
|
const conteo =
|
||||||
resultados.length === 1 ? "1 equipo coincide" : `${resultados.length} equipos coinciden`;
|
resultados.length === 1 ? "1 equipo" : `${resultados.length} equipos`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* ---------- filtros y resultado: el mundo oscuro, la operación ---------- */}
|
|
||||||
<section
|
<section
|
||||||
className="grid-bg border-t border-rule bg-ink py-14 sm:py-20"
|
id="resultados"
|
||||||
aria-labelledby="filtros-titulo"
|
className="border-t border-rule bg-ink-2 py-10 scroll-mt-24 sm:py-12"
|
||||||
|
aria-labelledby="catalogo-titulo"
|
||||||
>
|
>
|
||||||
<div className="shell">
|
<div className="shell">
|
||||||
<Revelar>
|
<h2 id="catalogo-titulo" className="sr-only">
|
||||||
<p className="eyebrow mb-4">Selector técnico</p>
|
Selección de equipos
|
||||||
<h2 id="filtros-titulo" className="font-display text-2xl font-extrabold sm:text-3xl">
|
</h2>
|
||||||
Filtra por lo que tienes, no por el nombre del equipo
|
|
||||||
</h2>
|
|
||||||
<p className="mt-3 max-w-2xl text-sm leading-relaxed text-muted sm:text-base">
|
|
||||||
Enciende las clases de fuego presentes en tu operación. El filtro deja únicamente los
|
|
||||||
equipos que cubren todas las que marcaste, según lo declarado en la información pública
|
|
||||||
de FSPM.
|
|
||||||
</p>
|
|
||||||
</Revelar>
|
|
||||||
|
|
||||||
<div className="panel mt-8 p-5 sm:p-7">
|
<div className="lg:grid lg:grid-cols-[15rem_minmax(0,1fr)] lg:gap-8 xl:grid-cols-[16.5rem_minmax(0,1fr)]">
|
||||||
<div className="grid gap-6 lg:grid-cols-[auto_1fr_1fr] lg:items-end">
|
{/* ---------------- barra lateral, fija y con scroll propio ---------------- */}
|
||||||
<div>
|
<aside className="hidden lg:block" aria-labelledby="filtros-titulo">
|
||||||
<span className="field-label">Clases de fuego</span>
|
<div
|
||||||
<div className="rounded-[3px] border border-rule-2 bg-ink/60 px-3 py-2.5">
|
className="sticky top-[5.25rem] max-h-[calc(100vh-6.5rem)] overflow-y-auto overflow-x-hidden pr-1 supports-[height:100dvh]:max-h-[calc(100dvh-6.5rem)]"
|
||||||
<RielClasesSelector valor={clases} onChange={setClases} tamano="md" />
|
style={{ WebkitOverflowScrolling: "touch" }}
|
||||||
|
>
|
||||||
|
<h2 id="filtros-titulo" className="sr-only">
|
||||||
|
Filtros del catálogo
|
||||||
|
</h2>
|
||||||
|
{filtros}
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
{/* ---------------- rejilla ---------------- */}
|
||||||
|
<div className="min-w-0">
|
||||||
|
<div className="flex flex-wrap items-center justify-between gap-3 border-b border-rule pb-4">
|
||||||
|
<div className="flex min-w-0 items-center gap-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setPanelAbierto(true)}
|
||||||
|
className="btn btn-ghost btn-sm lg:hidden"
|
||||||
|
aria-expanded={panelAbierto}
|
||||||
|
aria-haspopup="dialog"
|
||||||
|
>
|
||||||
|
Filtros
|
||||||
|
{activos > 0 ? (
|
||||||
|
<span className="font-mono tabular-nums text-signal-hi">({activos})</span>
|
||||||
|
) : null}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<p
|
||||||
|
aria-live="polite"
|
||||||
|
className="min-w-0 font-mono text-xs uppercase tracking-[0.12em] text-bright"
|
||||||
|
>
|
||||||
|
{conteo}
|
||||||
|
<span className="text-faint"> de {PRODUCTOS.length} en el catálogo</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<label
|
||||||
|
htmlFor="catalogo-orden"
|
||||||
|
className="font-mono text-[0.625rem] uppercase tracking-[0.14em] text-faint"
|
||||||
|
>
|
||||||
|
Orden
|
||||||
|
</label>
|
||||||
|
{/* `appearance-none` apaga el estilo nativo en WebKit, que
|
||||||
|
ignora el borde y el radio del sistema de diseño. A cambio
|
||||||
|
desaparece la flecha, así que se dibuja aquí. */}
|
||||||
|
<div className="relative">
|
||||||
|
<select
|
||||||
|
id="catalogo-orden"
|
||||||
|
value={orden}
|
||||||
|
onChange={(evento) => setOrden(evento.target.value as Orden)}
|
||||||
|
className="field w-auto appearance-none py-2 pr-8 text-[0.8125rem] [-webkit-appearance:none]"
|
||||||
|
>
|
||||||
|
{ORDENES.map((opcion) => (
|
||||||
|
<option key={opcion.valor} value={opcion.valor}>
|
||||||
|
{opcion.etiqueta}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
viewBox="0 0 10 6"
|
||||||
|
width="10"
|
||||||
|
height="6"
|
||||||
|
className="pointer-events-none absolute right-2.5 top-1/2 -translate-y-1/2 text-faint"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M1 1l4 4 4-4"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.4"
|
||||||
|
strokeLinecap="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
{escenario ? (
|
||||||
<label htmlFor="filtro-categoria" className="field-label">
|
<p className="mt-4 border-l-2 border-l-signal pl-3 text-sm leading-relaxed text-muted">
|
||||||
Categoría
|
{`Filtrado para ${escenario.nombre}. Clases típicas de ese lugar: ${escenario.clases.join(" · ")}.`}
|
||||||
</label>
|
</p>
|
||||||
<select
|
|
||||||
id="filtro-categoria"
|
|
||||||
value={categoria}
|
|
||||||
onChange={(e) => setCategoria(e.target.value)}
|
|
||||||
className="field appearance-none [-webkit-appearance:none]"
|
|
||||||
>
|
|
||||||
<option value="">Todas las categorías</option>
|
|
||||||
{CATEGORIAS.map((c) => (
|
|
||||||
<option key={c.slug} value={c.slug}>
|
|
||||||
{c.nombre}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label htmlFor="filtro-aplicacion" className="field-label">
|
|
||||||
Aplicación
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
id="filtro-aplicacion"
|
|
||||||
value={aplicacion}
|
|
||||||
onChange={(e) => setAplicacion(e.target.value)}
|
|
||||||
className="field appearance-none [-webkit-appearance:none]"
|
|
||||||
>
|
|
||||||
<option value="">Cualquier aplicación</option>
|
|
||||||
{APLICACIONES.map((a) => (
|
|
||||||
<option key={a} value={a}>
|
|
||||||
{a}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-6 flex flex-wrap items-center justify-between gap-4 border-t border-rule pt-5">
|
|
||||||
<p
|
|
||||||
aria-live="polite"
|
|
||||||
className="font-mono text-xs uppercase tracking-[0.12em] text-bright"
|
|
||||||
>
|
|
||||||
{conteo}
|
|
||||||
<span className="text-faint"> · de {PRODUCTOS.length} en el catálogo</span>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{hayFiltro ? (
|
|
||||||
<button type="button" onClick={limpiar} className="btn btn-ghost btn-sm">
|
|
||||||
Limpiar filtros
|
|
||||||
</button>
|
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
{resultados.length > 0 ? (
|
||||||
|
<div className="mt-6 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{resultados.map((producto) => (
|
||||||
|
<TarjetaProducto key={producto.slug} producto={producto} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
/* Estado vacío que vende: no dice «sin resultados», invita a
|
||||||
|
pedir el diagnóstico, que es lo que resuelve el caso raro. */
|
||||||
|
<div className="panel mt-6 border-l-2 border-l-signal p-6 sm:p-8">
|
||||||
|
<p className="font-mono text-[0.6875rem] uppercase tracking-[0.16em] text-ember">
|
||||||
|
Tu caso no está en el catálogo estándar
|
||||||
|
</p>
|
||||||
|
<h3 className="mt-3 font-display text-xl font-extrabold">
|
||||||
|
Eso casi siempre significa que necesitas una configuración, no un equipo
|
||||||
|
</h3>
|
||||||
|
<p className="mt-3 max-w-2xl text-sm leading-relaxed text-muted">
|
||||||
|
Ningún equipo cubre por sí solo esa combinación. Suele resolverse con dos equipos
|
||||||
|
o con un sistema dimensionado para la instalación.
|
||||||
|
</p>
|
||||||
|
<div className="mt-6 flex flex-wrap gap-3">
|
||||||
|
<Link href="#cotizar" className="btn btn-signal">
|
||||||
|
Pedir mi diagnóstico
|
||||||
|
</Link>
|
||||||
|
<button type="button" onClick={limpiar} className="btn btn-ghost">
|
||||||
|
Limpiar filtros
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* ---------------- comparación, cerrada por omisión ---------------- */}
|
||||||
|
{resultados.length > 1 ? (
|
||||||
|
<details className="mt-8 rounded-[6px] border border-rule bg-ink">
|
||||||
|
<summary
|
||||||
|
className="cursor-pointer list-none px-4 py-3.5 font-mono text-[0.6875rem] uppercase tracking-[0.14em] text-bright transition-colors hover:text-signal-hi [&::-webkit-details-marker]:hidden"
|
||||||
|
style={{ WebkitTapHighlightColor: "transparent" }}
|
||||||
|
>
|
||||||
|
Comparar especificaciones ({resultados.length}) +
|
||||||
|
</summary>
|
||||||
|
<div className="border-t border-rule p-4">
|
||||||
|
<TablaComparativa productos={resultados} claro={false} />
|
||||||
|
<p className="mt-4 text-xs leading-relaxed text-faint">
|
||||||
|
Capacidad, alcance y clases provienen de la información pública de FSPM. Donde
|
||||||
|
la fuente no declara un dato, la celda lo dice. Antes de cotizar entregamos la
|
||||||
|
ficha técnica por número de modelo.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{/* Navegación, no discurso: las ocho fichas de categoría siguen
|
||||||
|
siendo páginas propias y esta es su entrada desde el catálogo. */}
|
||||||
|
<nav className="mt-8 border-t border-rule pt-5" aria-label="Fichas por categoría">
|
||||||
|
<p className="font-mono text-[0.625rem] uppercase tracking-[0.14em] text-faint">
|
||||||
|
Ficha de categoría
|
||||||
|
</p>
|
||||||
|
<ul className="mt-2.5 flex flex-wrap gap-2">
|
||||||
|
{CATEGORIAS.map((categoria) => (
|
||||||
|
<li key={categoria.slug}>
|
||||||
|
<Link
|
||||||
|
href={`/catalogo/${categoria.slug}`}
|
||||||
|
className="badge hover:border-signal hover:text-bright"
|
||||||
|
>
|
||||||
|
{categoria.nombre}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ---------- resultados en tarjetas ---------- */}
|
|
||||||
{resultados.length > 0 ? (
|
|
||||||
<div className="mt-10 grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
|
|
||||||
{resultados.map((producto) => (
|
|
||||||
<TarjetaProducto key={producto.slug} producto={producto} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="panel mt-10 border-l-2 border-l-ember p-6 sm:p-8">
|
|
||||||
<p className="font-mono text-[0.6875rem] uppercase tracking-[0.16em] text-ember">
|
|
||||||
Sin coincidencias
|
|
||||||
</p>
|
|
||||||
<h3 className="mt-4 font-display text-xl font-extrabold sm:text-2xl">
|
|
||||||
Ningún equipo del catálogo cubre esa combinación
|
|
||||||
</h3>
|
|
||||||
<p className="mt-3 max-w-xl text-sm leading-relaxed text-muted">
|
|
||||||
Eso no significa que tu escenario no tenga solución: significa que probablemente
|
|
||||||
necesitas más de un equipo, o un sistema diseñado para la instalación. Pide un
|
|
||||||
diagnóstico y lo resolvemos con el escenario a la vista.
|
|
||||||
</p>
|
|
||||||
<div className="mt-6 flex flex-wrap gap-3">
|
|
||||||
<Link href="#cotizar" className="btn btn-signal btn-sm">
|
|
||||||
Pedir un diagnóstico
|
|
||||||
</Link>
|
|
||||||
<button type="button" onClick={limpiar} className="btn btn-ghost btn-sm">
|
|
||||||
Quitar los filtros
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* ---------- hoja de comparación: el mundo claro, el documento ---------- */}
|
{/* ---------------- panel de filtros en móvil ---------------- */}
|
||||||
<section
|
<PanelFiltrosMovil
|
||||||
className="grid-bg-lite bg-paper py-14 text-deep sm:py-20"
|
abierto={panelAbierto}
|
||||||
aria-labelledby="tabla-titulo"
|
onCerrar={() => setPanelAbierto(false)}
|
||||||
|
cuantosResultados={resultados.length}
|
||||||
>
|
>
|
||||||
<div className="shell-wide">
|
<FiltrosCatalogo
|
||||||
<Revelar>
|
estado={estado}
|
||||||
<p className="eyebrow mb-4">Hoja de comparación</p>
|
onCambio={cambiar}
|
||||||
<h2
|
conteoPorCategoria={conteoPorCategoria}
|
||||||
id="tabla-titulo"
|
conteoPorEscenario={conteoPorEscenario}
|
||||||
className="font-display text-2xl font-extrabold text-deep sm:text-3xl"
|
conteoPorAgente={conteoPorAgente}
|
||||||
>
|
familias={FAMILIAS}
|
||||||
Los {PRODUCTOS.length} equipos, lado a lado
|
modoBusqueda={buscador.modo}
|
||||||
</h2>
|
avisoBusqueda={buscador.aviso}
|
||||||
<p className="mt-3 max-w-2xl text-sm leading-relaxed text-deep-muted sm:text-base">
|
hayFiltro={hayFiltro}
|
||||||
La misma selección de arriba, en formato de tabla. Donde la fuente no declara un dato,
|
onLimpiar={limpiar}
|
||||||
la celda lo dice: no lo rellenamos con una estimación.
|
ambito="movil"
|
||||||
</p>
|
/>
|
||||||
</Revelar>
|
</PanelFiltrosMovil>
|
||||||
|
|
||||||
<p className="mt-6 font-mono text-[0.6875rem] uppercase tracking-[0.14em] text-deep-muted sm:hidden">
|
{/* ---------------- cierre: el formulario, con el escenario activo ---------------- */}
|
||||||
Desliza la tabla para ver todas las columnas
|
<section
|
||||||
</p>
|
id="cotizar"
|
||||||
|
className="grid-bg-lite border-t border-rule bg-paper py-14 scroll-mt-20 sm:py-20"
|
||||||
<div
|
aria-labelledby="cotizar-titulo"
|
||||||
className="mt-4 overflow-x-auto rounded-[6px] border border-rule-lite bg-white [-webkit-overflow-scrolling:touch]"
|
>
|
||||||
tabIndex={0}
|
<div className="shell">
|
||||||
role="region"
|
<div className="grid gap-10 lg:grid-cols-[1fr_1.05fr] lg:items-start lg:gap-16">
|
||||||
aria-label="Tabla comparativa de productos, desplazable en horizontal"
|
<div>
|
||||||
>
|
<p className="eyebrow mb-4">Siguiente paso</p>
|
||||||
<table className="w-full min-w-[54rem] border-collapse text-left">
|
<h2
|
||||||
<caption className="sr-only">
|
id="cotizar-titulo"
|
||||||
Comparación de productos por categoría, clases de fuego, capacidad, agente y alcance
|
className="font-display text-2xl font-extrabold text-deep sm:text-3xl"
|
||||||
declarado.
|
>
|
||||||
</caption>
|
{escenario
|
||||||
<thead>
|
? `Dinos cómo es tu ${escenario.nombre.toLowerCase()} y te devolvemos la selección`
|
||||||
<tr className="border-b border-rule-lite bg-paper-2">
|
: "Dinos qué hay que proteger y te devolvemos la selección, no un folleto"}
|
||||||
{COLUMNAS.map((encabezado) => (
|
</h2>
|
||||||
<th
|
<p className="mt-4 text-sm leading-relaxed text-deep-muted sm:text-base">
|
||||||
key={encabezado}
|
Con la clase de fuego, la superficie y quién opera el equipo alcanza para proponer una
|
||||||
scope="col"
|
configuración y decirte qué documentación te corresponde exigir. Cuatro campos, sin
|
||||||
className="whitespace-nowrap px-4 py-3 font-mono text-[0.625rem] font-medium uppercase tracking-[0.14em] text-deep-muted"
|
costo y sin compromiso de compra.
|
||||||
>
|
|
||||||
{encabezado}
|
|
||||||
</th>
|
|
||||||
))}
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{resultados.map((p) => (
|
|
||||||
<tr key={p.slug} className="border-b border-rule-lite last:border-b-0">
|
|
||||||
<th scope="row" className="px-4 py-4 text-left align-top">
|
|
||||||
<Link
|
|
||||||
href={`/producto/${p.slug}`}
|
|
||||||
className="link font-sans text-sm font-semibold text-deep [-webkit-tap-highlight-color:transparent]"
|
|
||||||
>
|
|
||||||
{p.nombre}
|
|
||||||
</Link>
|
|
||||||
{p.modelo ? (
|
|
||||||
<span className="mt-1 block font-mono text-[0.625rem] uppercase tracking-[0.1em] text-deep-muted">
|
|
||||||
{p.modelo}
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
</th>
|
|
||||||
<td className="px-4 py-4 align-top font-mono text-xs text-deep-muted">
|
|
||||||
{CATEGORIA_NOMBRE.get(p.categoria) ?? p.categoria}
|
|
||||||
</td>
|
|
||||||
<td className="px-4 py-4 align-top">
|
|
||||||
{p.clases.length > 0 ? (
|
|
||||||
<RielClases activas={p.clases} tamano="sm" claro />
|
|
||||||
) : (
|
|
||||||
<span className="font-mono text-[0.625rem] uppercase tracking-[0.1em] text-deep-muted">
|
|
||||||
No declarada
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
<td className="px-4 py-4 align-top font-mono text-xs text-deep">
|
|
||||||
{p.capacidad ?? <span className="text-deep-muted">Sin dato en la fuente</span>}
|
|
||||||
</td>
|
|
||||||
<td className="px-4 py-4 align-top font-mono text-xs text-deep">
|
|
||||||
{p.agente ?? <span className="text-deep-muted">No aplica</span>}
|
|
||||||
</td>
|
|
||||||
<td className="max-w-[18rem] px-4 py-4 align-top font-mono text-xs leading-relaxed text-deep">
|
|
||||||
{p.alcance ?? <span className="text-deep-muted">Sin dato en la fuente</span>}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
{resultados.length === 0 ? (
|
|
||||||
<p className="px-4 py-8 text-center text-sm text-deep-muted">
|
|
||||||
Ningún equipo coincide con el filtro actual.
|
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<p className="mt-5 max-w-3xl text-xs leading-relaxed text-deep-muted">
|
<FormularioContacto
|
||||||
Alcance, capacidad y clases provienen de la información pública de FSPM. Antes de
|
canal="catalogo"
|
||||||
cotizar entregamos la ficha técnica y el certificado por número de modelo, que es lo que
|
variante="claro"
|
||||||
realmente sostiene cada celda de esta tabla.
|
escenario={escenario?.slug}
|
||||||
</p>
|
industria={escenario?.industria ?? undefined}
|
||||||
|
clases={escenario ? escenario.clases : estado.clases}
|
||||||
|
mensajeInicial={escenario ? `Escenario: ${escenario.nombre}. ` : undefined}
|
||||||
|
titulo="Recibir cotización"
|
||||||
|
descripcion="Respondemos con el escenario interpretado, el equipo que aplica y la lista de documentos que deberíamos entregarte."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -0,0 +1,463 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useRef, type ReactNode } from "react";
|
||||||
|
import { IconoEscenario } from "@/components/marca/IconoEscenario";
|
||||||
|
import { RielClasesSelector } from "@/components/marca/RielClases";
|
||||||
|
import {
|
||||||
|
EJEMPLOS_REGEX,
|
||||||
|
type FamiliaAgente,
|
||||||
|
type ModoBusqueda,
|
||||||
|
} from "@/components/site/catalogo/busqueda";
|
||||||
|
import type { EstadoCatalogo } from "@/components/site/catalogo/estado";
|
||||||
|
import { CATEGORIAS } from "@/data/catalogo";
|
||||||
|
import { ESCENARIOS } from "@/data/escenarios";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FILTROS DEL CATÁLOGO — la barra lateral de una tienda.
|
||||||
|
*
|
||||||
|
* El mismo bloque de controles se pinta dos veces: dentro de la barra lateral
|
||||||
|
* fija en escritorio y dentro del panel de «Filtros» en móvil. Los dos existen
|
||||||
|
* en el DOM a la vez, así que cada instancia recibe un `ambito` que sufija los
|
||||||
|
* `id` de los campos: dos `<label for>` apuntando al mismo `id` rompen el foco
|
||||||
|
* y el lector de pantalla.
|
||||||
|
*
|
||||||
|
* Todo lo que era discurso —«Puerta 01», «Puerta 02», los trece escenarios como
|
||||||
|
* rejilla de tarjetas con su riesgo desarrollado— salió de aquí. El escenario
|
||||||
|
* es un filtro más, en lista compacta con su icono. La lección de escenarios
|
||||||
|
* vive en el inicio, en /soluciones y en /cumplimiento-nom-002.
|
||||||
|
*/
|
||||||
|
|
||||||
|
type Cambio = (parcial: Partial<EstadoCatalogo>) => void;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
estado: EstadoCatalogo;
|
||||||
|
onCambio: Cambio;
|
||||||
|
/** Resultados por categoría con los demás filtros aplicados. */
|
||||||
|
conteoPorCategoria: Map<string, number>;
|
||||||
|
/** Resultados por escenario con los demás filtros aplicados. */
|
||||||
|
conteoPorEscenario: Map<string, number>;
|
||||||
|
/** Resultados por familia de agente con los demás filtros aplicados. */
|
||||||
|
conteoPorAgente: Map<string, number>;
|
||||||
|
familias: readonly FamiliaAgente[];
|
||||||
|
modoBusqueda: ModoBusqueda;
|
||||||
|
avisoBusqueda: string | null;
|
||||||
|
hayFiltro: boolean;
|
||||||
|
onLimpiar: () => void;
|
||||||
|
/** Sufijo de los `id`, para que las dos instancias no colisionen. */
|
||||||
|
ambito: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function Bloque({
|
||||||
|
titulo,
|
||||||
|
children,
|
||||||
|
primero = false,
|
||||||
|
}: {
|
||||||
|
titulo: string;
|
||||||
|
children: ReactNode;
|
||||||
|
primero?: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className={primero ? "" : "mt-6 border-t border-rule pt-6"}>
|
||||||
|
<h3 className="field-label">{titulo}</h3>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Píldora de filtro. Un solo estilo para categoría, escenario y agente. */
|
||||||
|
function Opcion({
|
||||||
|
activo,
|
||||||
|
onClick,
|
||||||
|
deshabilitado = false,
|
||||||
|
children,
|
||||||
|
conteo,
|
||||||
|
amplio = false,
|
||||||
|
}: {
|
||||||
|
activo: boolean;
|
||||||
|
onClick: () => void;
|
||||||
|
deshabilitado?: boolean;
|
||||||
|
children: ReactNode;
|
||||||
|
conteo?: number;
|
||||||
|
/** En el panel móvil el objetivo táctil sube a ~44 px. */
|
||||||
|
amplio?: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClick}
|
||||||
|
disabled={deshabilitado}
|
||||||
|
aria-pressed={activo}
|
||||||
|
className={[
|
||||||
|
"flex w-full min-w-0 items-center justify-between gap-2 rounded-[3px] border px-2.5 text-left transition-colors duration-150",
|
||||||
|
amplio ? "py-3" : "py-2",
|
||||||
|
activo
|
||||||
|
? "border-signal bg-signal/10 text-bright"
|
||||||
|
: deshabilitado
|
||||||
|
? "cursor-not-allowed border-rule bg-transparent text-faint opacity-60"
|
||||||
|
: "border-rule bg-transparent text-muted hover:border-rule-2 hover:text-bright",
|
||||||
|
].join(" ")}
|
||||||
|
>
|
||||||
|
<span className="flex min-w-0 items-center gap-2 text-[0.8125rem] leading-snug">
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
{typeof conteo === "number" ? (
|
||||||
|
<span className="shrink-0 font-mono text-[0.625rem] tabular-nums text-faint">
|
||||||
|
{conteo}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FiltrosCatalogo({
|
||||||
|
estado,
|
||||||
|
onCambio,
|
||||||
|
conteoPorCategoria,
|
||||||
|
conteoPorEscenario,
|
||||||
|
conteoPorAgente,
|
||||||
|
familias,
|
||||||
|
modoBusqueda,
|
||||||
|
avisoBusqueda,
|
||||||
|
hayFiltro,
|
||||||
|
onLimpiar,
|
||||||
|
ambito,
|
||||||
|
}: Props) {
|
||||||
|
const idBusqueda = `catalogo-q-${ambito}`;
|
||||||
|
const idAyuda = `catalogo-q-ayuda-${ambito}`;
|
||||||
|
/* En el panel móvil todo respira más y nada anida su propio scroll: un
|
||||||
|
contenedor desplazable dentro de otro es una trampa con el dedo. */
|
||||||
|
const movil = ambito === "movil";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{/* ---------------- búsqueda ---------------- */}
|
||||||
|
<Bloque titulo="Buscar" primero>
|
||||||
|
<div className="relative">
|
||||||
|
<input
|
||||||
|
id={idBusqueda}
|
||||||
|
type="search"
|
||||||
|
value={estado.q}
|
||||||
|
onChange={(evento) => onCambio({ q: evento.target.value })}
|
||||||
|
placeholder="enforcer, 12 L, espuma"
|
||||||
|
autoComplete="off"
|
||||||
|
spellCheck={false}
|
||||||
|
aria-describedby={idAyuda}
|
||||||
|
className="field appearance-none pr-16 [-webkit-appearance:none] [&::-webkit-search-cancel-button]:hidden [&::-webkit-search-decoration]:hidden"
|
||||||
|
/>
|
||||||
|
{modoBusqueda !== "vacio" ? (
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
className={[
|
||||||
|
"pointer-events-none absolute right-2 top-1/2 -translate-y-1/2 rounded-[2px] border px-1.5 py-1 font-mono text-[0.5625rem] uppercase tracking-[0.12em]",
|
||||||
|
modoBusqueda === "regex"
|
||||||
|
? "border-signal/60 text-signal-hi"
|
||||||
|
: "border-ember-dim text-ember",
|
||||||
|
].join(" ")}
|
||||||
|
>
|
||||||
|
{modoBusqueda === "regex" ? "Regex" : "Texto"}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p id={idAyuda} className="mt-2 text-[0.6875rem] leading-relaxed text-faint">
|
||||||
|
Busca en nombre, modelo, agente, capacidad y aplicaciones. Acepta expresiones regulares.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{avisoBusqueda ? (
|
||||||
|
<p
|
||||||
|
role="status"
|
||||||
|
className="mt-2 border-l-2 border-l-ember pl-2.5 text-[0.6875rem] leading-relaxed text-ember"
|
||||||
|
>
|
||||||
|
{avisoBusqueda}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{/* Los ejemplos enseñan la capacidad, pero detrás de un desplegable:
|
||||||
|
cuatro botones grandes a la vista eran espacio robado a la rejilla. */}
|
||||||
|
<details className="mt-2.5">
|
||||||
|
<summary
|
||||||
|
className="cursor-pointer list-none font-mono text-[0.625rem] uppercase tracking-[0.14em] text-faint transition-colors hover:text-muted [&::-webkit-details-marker]:hidden"
|
||||||
|
style={{ WebkitTapHighlightColor: "transparent" }}
|
||||||
|
>
|
||||||
|
Ejemplos de expresión +
|
||||||
|
</summary>
|
||||||
|
<ul className="mt-2 space-y-1.5">
|
||||||
|
{EJEMPLOS_REGEX.map((ejemplo) => (
|
||||||
|
<li key={ejemplo.patron}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => onCambio({ q: ejemplo.patron })}
|
||||||
|
aria-pressed={estado.q === ejemplo.patron}
|
||||||
|
className={[
|
||||||
|
"w-full rounded-[3px] border px-2 py-1.5 text-left transition-colors duration-150",
|
||||||
|
estado.q === ejemplo.patron
|
||||||
|
? "border-signal bg-signal/10"
|
||||||
|
: "border-rule hover:border-rule-2",
|
||||||
|
].join(" ")}
|
||||||
|
>
|
||||||
|
<code className="block font-mono text-[0.6875rem] leading-none text-bright">
|
||||||
|
{ejemplo.patron}
|
||||||
|
</code>
|
||||||
|
<span className="mt-1 block font-mono text-[0.5625rem] uppercase tracking-[0.1em] text-faint">
|
||||||
|
{ejemplo.explica}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
</Bloque>
|
||||||
|
|
||||||
|
{/* ---------------- categoría ---------------- */}
|
||||||
|
<Bloque titulo="Categoría">
|
||||||
|
<ul className="space-y-1.5">
|
||||||
|
<li>
|
||||||
|
<Opcion
|
||||||
|
amplio={movil}
|
||||||
|
activo={estado.categoria === ""}
|
||||||
|
onClick={() => onCambio({ categoria: "" })}
|
||||||
|
>
|
||||||
|
Todas las categorías
|
||||||
|
</Opcion>
|
||||||
|
</li>
|
||||||
|
{CATEGORIAS.map((categoria) => {
|
||||||
|
const cuantos = conteoPorCategoria.get(categoria.slug) ?? 0;
|
||||||
|
const activo = estado.categoria === categoria.slug;
|
||||||
|
return (
|
||||||
|
<li key={categoria.slug}>
|
||||||
|
<Opcion
|
||||||
|
amplio={movil}
|
||||||
|
activo={activo}
|
||||||
|
conteo={cuantos}
|
||||||
|
deshabilitado={cuantos === 0 && !activo}
|
||||||
|
onClick={() => onCambio({ categoria: activo ? "" : categoria.slug })}
|
||||||
|
>
|
||||||
|
<span className="min-w-0 break-words">{categoria.nombre}</span>
|
||||||
|
</Opcion>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</Bloque>
|
||||||
|
|
||||||
|
{/* ---------------- clases de fuego ---------------- */}
|
||||||
|
<Bloque titulo="Clase de fuego">
|
||||||
|
<div className="rounded-[3px] border border-rule bg-ink/60 px-2.5 py-2.5">
|
||||||
|
<RielClasesSelector
|
||||||
|
valor={estado.clases}
|
||||||
|
onChange={(clases) => onCambio({ clases })}
|
||||||
|
tamano={movil ? "lg" : "md"}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="mt-2 text-[0.6875rem] leading-relaxed text-faint">
|
||||||
|
Deja solo los equipos que cubren todas las clases encendidas.
|
||||||
|
</p>
|
||||||
|
</Bloque>
|
||||||
|
|
||||||
|
{/* ---------------- escenario ---------------- */}
|
||||||
|
<Bloque titulo="Lugar que proteges">
|
||||||
|
<ul
|
||||||
|
className={
|
||||||
|
movil ? "space-y-1.5" : "max-h-[15rem] space-y-1.5 overflow-y-auto pr-1"
|
||||||
|
}
|
||||||
|
style={movil ? undefined : { WebkitOverflowScrolling: "touch" }}
|
||||||
|
>
|
||||||
|
<li>
|
||||||
|
<Opcion
|
||||||
|
amplio={movil}
|
||||||
|
activo={estado.escenario === null}
|
||||||
|
onClick={() => onCambio({ escenario: null })}
|
||||||
|
>
|
||||||
|
Cualquier lugar
|
||||||
|
</Opcion>
|
||||||
|
</li>
|
||||||
|
{ESCENARIOS.map((escenario) => {
|
||||||
|
const cuantos = conteoPorEscenario.get(escenario.slug) ?? 0;
|
||||||
|
const activo = estado.escenario === escenario.slug;
|
||||||
|
return (
|
||||||
|
<li key={escenario.slug}>
|
||||||
|
<Opcion
|
||||||
|
amplio={movil}
|
||||||
|
activo={activo}
|
||||||
|
conteo={cuantos}
|
||||||
|
deshabilitado={cuantos === 0 && !activo}
|
||||||
|
onClick={() => onCambio({ escenario: activo ? null : escenario.slug })}
|
||||||
|
>
|
||||||
|
<IconoEscenario escenario={escenario} tamano="sm" />
|
||||||
|
<span className="min-w-0 break-words">{escenario.nombre}</span>
|
||||||
|
</Opcion>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</Bloque>
|
||||||
|
|
||||||
|
{/* ---------------- familia de agente ---------------- */}
|
||||||
|
{familias.length > 0 ? (
|
||||||
|
<Bloque titulo="Familia de agente">
|
||||||
|
<ul className="space-y-1.5">
|
||||||
|
<li>
|
||||||
|
<Opcion
|
||||||
|
amplio={movil}
|
||||||
|
activo={estado.agente === ""}
|
||||||
|
onClick={() => onCambio({ agente: "" })}
|
||||||
|
>
|
||||||
|
Cualquier agente
|
||||||
|
</Opcion>
|
||||||
|
</li>
|
||||||
|
{familias.map((familia) => {
|
||||||
|
const cuantos = conteoPorAgente.get(familia) ?? 0;
|
||||||
|
const activo = estado.agente === familia;
|
||||||
|
return (
|
||||||
|
<li key={familia}>
|
||||||
|
<Opcion
|
||||||
|
amplio={movil}
|
||||||
|
activo={activo}
|
||||||
|
conteo={cuantos}
|
||||||
|
deshabilitado={cuantos === 0 && !activo}
|
||||||
|
onClick={() => onCambio({ agente: activo ? "" : familia })}
|
||||||
|
>
|
||||||
|
<span className="font-mono text-[0.75rem]">{familia}</span>
|
||||||
|
</Opcion>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
<p className="mt-2 text-[0.6875rem] leading-relaxed text-faint">
|
||||||
|
Son los nombres que declara la fuente en el campo de agente, no una clasificación
|
||||||
|
nuestra.
|
||||||
|
</p>
|
||||||
|
</Bloque>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{/* ---------------- limpiar ---------------- */}
|
||||||
|
{hayFiltro ? (
|
||||||
|
<div className="mt-6 border-t border-rule pt-6">
|
||||||
|
<button type="button" onClick={onLimpiar} className="btn btn-ghost btn-sm w-full">
|
||||||
|
Limpiar filtros
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PANEL DE FILTROS EN MÓVIL.
|
||||||
|
*
|
||||||
|
* Abajo de `lg` la barra lateral no se apila: eso empujaría la rejilla mil
|
||||||
|
* píxeles hacia abajo. Se colapsa en un botón «Filtros (N)» y esto es lo que
|
||||||
|
* abre: una hoja a pantalla completa con cierre por `Escape`, foco atrapado
|
||||||
|
* dentro y el foco devuelto al botón al cerrar.
|
||||||
|
*
|
||||||
|
* Altura en `100dvh` con respaldo `100vh`, porque en Safari de iOS la barra de
|
||||||
|
* direcciones se come el 100vh y el botón de cierre queda fuera de la pantalla.
|
||||||
|
*/
|
||||||
|
export function PanelFiltrosMovil({
|
||||||
|
abierto,
|
||||||
|
onCerrar,
|
||||||
|
cuantosResultados,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
abierto: boolean;
|
||||||
|
onCerrar: () => void;
|
||||||
|
cuantosResultados: number;
|
||||||
|
children: ReactNode;
|
||||||
|
}) {
|
||||||
|
const panel = useRef<HTMLDivElement>(null);
|
||||||
|
const cerrar = useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!abierto) return;
|
||||||
|
|
||||||
|
const anterior = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
||||||
|
const desbordeAnterior = document.body.style.overflow;
|
||||||
|
document.body.style.overflow = "hidden";
|
||||||
|
cerrar.current?.focus();
|
||||||
|
|
||||||
|
function alTeclear(evento: KeyboardEvent) {
|
||||||
|
if (evento.key === "Escape") {
|
||||||
|
evento.preventDefault();
|
||||||
|
onCerrar();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (evento.key !== "Tab") return;
|
||||||
|
|
||||||
|
const nodo = panel.current;
|
||||||
|
if (!nodo) return;
|
||||||
|
|
||||||
|
const enfocables = Array.from(
|
||||||
|
nodo.querySelectorAll<HTMLElement>(
|
||||||
|
'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), summary, [tabindex]:not([tabindex="-1"])',
|
||||||
|
),
|
||||||
|
).filter((el) => el.offsetParent !== null || el === document.activeElement);
|
||||||
|
|
||||||
|
if (enfocables.length === 0) return;
|
||||||
|
|
||||||
|
const primero = enfocables[0];
|
||||||
|
const ultimo = enfocables[enfocables.length - 1];
|
||||||
|
|
||||||
|
if (evento.shiftKey && document.activeElement === primero) {
|
||||||
|
evento.preventDefault();
|
||||||
|
ultimo.focus();
|
||||||
|
} else if (!evento.shiftKey && document.activeElement === ultimo) {
|
||||||
|
evento.preventDefault();
|
||||||
|
primero.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("keydown", alTeclear);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("keydown", alTeclear);
|
||||||
|
document.body.style.overflow = desbordeAnterior;
|
||||||
|
anterior?.focus();
|
||||||
|
};
|
||||||
|
}, [abierto, onCerrar]);
|
||||||
|
|
||||||
|
if (!abierto) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="lg:hidden">
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 z-[59] bg-ink/80 backdrop-blur-[2px] [-webkit-backdrop-filter:blur(2px)]"
|
||||||
|
aria-hidden="true"
|
||||||
|
onClick={onCerrar}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
ref={panel}
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-label="Filtros del catálogo"
|
||||||
|
className="fixed inset-x-0 top-0 z-[60] flex h-[100vh] flex-col border-r border-rule bg-ink shadow-2xl supports-[height:100dvh]:h-[100dvh] sm:max-w-[26rem]"
|
||||||
|
>
|
||||||
|
<div className="flex shrink-0 items-center justify-between gap-4 border-b border-rule px-5 py-4">
|
||||||
|
<h2 className="font-display text-base font-extrabold uppercase tracking-wide text-bright">
|
||||||
|
Filtros
|
||||||
|
</h2>
|
||||||
|
<button
|
||||||
|
ref={cerrar}
|
||||||
|
type="button"
|
||||||
|
onClick={onCerrar}
|
||||||
|
className="btn btn-ghost btn-sm"
|
||||||
|
aria-label="Cerrar los filtros"
|
||||||
|
>
|
||||||
|
Cerrar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="min-h-0 flex-1 overflow-y-auto px-5 py-5"
|
||||||
|
style={{ WebkitOverflowScrolling: "touch" }}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="shrink-0 border-t border-rule px-5 py-4">
|
||||||
|
<button type="button" onClick={onCerrar} className="btn btn-signal btn-sm w-full">
|
||||||
|
Ver {cuantosResultados} {cuantosResultados === 1 ? "equipo" : "equipos"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,295 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
import { RielClases } from "@/components/marca/RielClases";
|
||||||
|
import { CATEGORIAS, type Producto } from "@/data/catalogo";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HOJA DE COMPARACIÓN — recomendación 10.2 de la auditoría de marca.
|
||||||
|
*
|
||||||
|
* Sirve al comprador técnico, que compara antes de pedir precio. Se conserva,
|
||||||
|
* pero con tres correcciones:
|
||||||
|
*
|
||||||
|
* · respeta los filtros activos: recibe la selección ya hecha, no el catálogo
|
||||||
|
* completo;
|
||||||
|
* · cada fila cierra con su propio botón de cotizar, para que comparar y
|
||||||
|
* pedir cotización sean el mismo movimiento;
|
||||||
|
* · en móvil no hay tabla. Una tabla de siete columnas en 320 px no se lee,
|
||||||
|
* así que abajo de `md` se apilan fichas.
|
||||||
|
*
|
||||||
|
* El desplazamiento horizontal vive dentro del contenedor. El cuerpo de la
|
||||||
|
* página nunca se desplaza en horizontal.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const NOMBRE_CATEGORIA = new Map(CATEGORIAS.map((c) => [c.slug, c.nombre]));
|
||||||
|
|
||||||
|
const COLUMNAS = [
|
||||||
|
"Producto",
|
||||||
|
"Categoría",
|
||||||
|
"Clases",
|
||||||
|
"Capacidad",
|
||||||
|
"Agente",
|
||||||
|
"Alcance declarado",
|
||||||
|
"Cotizar",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
productos: Producto[];
|
||||||
|
/** Sobre superficie clara. `true` por omisión: la evidencia va en papel. */
|
||||||
|
claro?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
function SinDato({
|
||||||
|
texto = "Sin dato en la fuente",
|
||||||
|
claro = true,
|
||||||
|
}: {
|
||||||
|
texto?: string;
|
||||||
|
claro?: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={[
|
||||||
|
"font-mono text-[0.625rem] uppercase tracking-[0.1em]",
|
||||||
|
claro ? "text-deep-muted" : "text-faint",
|
||||||
|
].join(" ")}
|
||||||
|
>
|
||||||
|
{texto}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TablaComparativa({ productos, claro = true }: Props) {
|
||||||
|
if (productos.length === 0) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={[
|
||||||
|
"rounded-[6px] border border-dashed p-6 text-sm leading-relaxed",
|
||||||
|
claro ? "border-rule-lite bg-white text-deep-muted" : "border-rule bg-ink-2 text-muted",
|
||||||
|
].join(" ")}
|
||||||
|
>
|
||||||
|
Ningún equipo coincide con el filtro actual, así que la hoja de comparación está vacía.
|
||||||
|
Quita un filtro o pide el diagnóstico y comparamos con el escenario a la vista.
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* ---------- móvil: fichas apiladas, no tabla ---------- */}
|
||||||
|
<ul className="grid gap-3 md:hidden">
|
||||||
|
{productos.map((producto) => {
|
||||||
|
const datos: Array<[string, string | null]> = [
|
||||||
|
["Categoría", NOMBRE_CATEGORIA.get(producto.categoria) ?? producto.categoria],
|
||||||
|
["Capacidad", producto.capacidad],
|
||||||
|
["Agente", producto.agente],
|
||||||
|
["Alcance declarado", producto.alcance],
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
key={producto.slug}
|
||||||
|
className={[
|
||||||
|
"min-w-0 rounded-[6px] border p-4",
|
||||||
|
claro ? "border-rule-lite bg-white" : "border-rule bg-ink-2",
|
||||||
|
].join(" ")}
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
className={[
|
||||||
|
"font-display text-base font-extrabold leading-tight",
|
||||||
|
claro ? "text-deep" : "text-bright",
|
||||||
|
].join(" ")}
|
||||||
|
>
|
||||||
|
{producto.nombre}
|
||||||
|
</p>
|
||||||
|
{producto.modelo ? (
|
||||||
|
<p
|
||||||
|
className={[
|
||||||
|
"mt-1 font-mono text-[0.625rem] uppercase tracking-[0.1em]",
|
||||||
|
claro ? "text-deep-muted" : "text-faint",
|
||||||
|
].join(" ")}
|
||||||
|
>
|
||||||
|
{producto.modelo}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<div className="mt-3">
|
||||||
|
{producto.clases.length > 0 ? (
|
||||||
|
<RielClases activas={producto.clases} tamano="sm" claro={claro} etiqueta="Clases" />
|
||||||
|
) : (
|
||||||
|
<SinDato texto="Clase no declarada" claro={claro} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<dl className="mt-3 grid gap-2">
|
||||||
|
{datos.map(([clave, valor]) => (
|
||||||
|
<div key={clave} className="flex min-w-0 gap-3">
|
||||||
|
<dt
|
||||||
|
className={[
|
||||||
|
"w-28 shrink-0 font-mono text-[0.625rem] uppercase tracking-[0.1em]",
|
||||||
|
claro ? "text-deep-muted" : "text-faint",
|
||||||
|
].join(" ")}
|
||||||
|
>
|
||||||
|
{clave}
|
||||||
|
</dt>
|
||||||
|
<dd
|
||||||
|
className={[
|
||||||
|
"min-w-0 break-words font-mono text-xs",
|
||||||
|
claro ? "text-deep" : "text-bright",
|
||||||
|
].join(" ")}
|
||||||
|
>
|
||||||
|
{valor ?? <SinDato claro={claro} />}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
href={`/producto/${producto.slug}#cotizar`}
|
||||||
|
className="btn btn-signal btn-sm mt-4 w-full"
|
||||||
|
style={{ WebkitTapHighlightColor: "transparent" }}
|
||||||
|
>
|
||||||
|
Cotizar este equipo
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
{/* ---------- md en adelante: la tabla, con scroll propio ---------- */}
|
||||||
|
<div
|
||||||
|
className={[
|
||||||
|
"hidden overflow-x-auto rounded-[6px] border md:block",
|
||||||
|
claro ? "border-rule-lite bg-white" : "border-rule bg-ink-2",
|
||||||
|
].join(" ")}
|
||||||
|
style={{ WebkitOverflowScrolling: "touch" }}
|
||||||
|
tabIndex={0}
|
||||||
|
role="region"
|
||||||
|
aria-label="Hoja de comparación de equipos, desplazable en horizontal"
|
||||||
|
>
|
||||||
|
<table className="w-full min-w-[60rem] border-collapse text-left">
|
||||||
|
<caption className="sr-only">
|
||||||
|
Comparación de la selección activa por categoría, clases de fuego, capacidad, agente y
|
||||||
|
alcance declarado. Cada fila cierra con el enlace para cotizar ese equipo.
|
||||||
|
</caption>
|
||||||
|
<thead>
|
||||||
|
<tr
|
||||||
|
className={[
|
||||||
|
"border-b",
|
||||||
|
claro ? "border-rule-lite bg-paper-2" : "border-rule bg-ink-3",
|
||||||
|
].join(" ")}
|
||||||
|
>
|
||||||
|
{COLUMNAS.map((encabezado) => (
|
||||||
|
<th
|
||||||
|
key={encabezado}
|
||||||
|
scope="col"
|
||||||
|
className={[
|
||||||
|
"whitespace-nowrap px-4 py-3 font-mono text-[0.625rem] font-medium uppercase tracking-[0.14em]",
|
||||||
|
claro ? "text-deep-muted" : "text-muted",
|
||||||
|
].join(" ")}
|
||||||
|
>
|
||||||
|
{encabezado}
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{productos.map((producto) => (
|
||||||
|
<tr
|
||||||
|
key={producto.slug}
|
||||||
|
className={[
|
||||||
|
"border-b last:border-b-0",
|
||||||
|
claro ? "border-rule-lite" : "border-rule",
|
||||||
|
].join(" ")}
|
||||||
|
>
|
||||||
|
<th scope="row" className="px-4 py-4 text-left align-top">
|
||||||
|
<span
|
||||||
|
className={[
|
||||||
|
"block font-sans text-sm font-semibold",
|
||||||
|
claro ? "text-deep" : "text-bright",
|
||||||
|
].join(" ")}
|
||||||
|
>
|
||||||
|
{producto.nombre}
|
||||||
|
</span>
|
||||||
|
{producto.modelo ? (
|
||||||
|
<span
|
||||||
|
className={[
|
||||||
|
"mt-1 block font-mono text-[0.625rem] uppercase tracking-[0.1em]",
|
||||||
|
claro ? "text-deep-muted" : "text-faint",
|
||||||
|
].join(" ")}
|
||||||
|
>
|
||||||
|
{producto.modelo}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
<Link
|
||||||
|
href={`/producto/${producto.slug}`}
|
||||||
|
className={[
|
||||||
|
"mt-1.5 inline-block font-mono text-[0.625rem] uppercase tracking-[0.12em] underline decoration-transparent underline-offset-4",
|
||||||
|
claro
|
||||||
|
? "text-deep-muted hover:decoration-rule-lite"
|
||||||
|
: "text-faint hover:text-muted hover:decoration-rule-2",
|
||||||
|
].join(" ")}
|
||||||
|
style={{ WebkitTapHighlightColor: "transparent" }}
|
||||||
|
>
|
||||||
|
Ficha completa
|
||||||
|
</Link>
|
||||||
|
</th>
|
||||||
|
|
||||||
|
<td
|
||||||
|
className={[
|
||||||
|
"px-4 py-4 align-top font-mono text-xs",
|
||||||
|
claro ? "text-deep-muted" : "text-muted",
|
||||||
|
].join(" ")}
|
||||||
|
>
|
||||||
|
{NOMBRE_CATEGORIA.get(producto.categoria) ?? producto.categoria}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td className="px-4 py-4 align-top">
|
||||||
|
{producto.clases.length > 0 ? (
|
||||||
|
<RielClases activas={producto.clases} tamano="sm" claro={claro} />
|
||||||
|
) : (
|
||||||
|
<SinDato texto="No declarada" claro={claro} />
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td
|
||||||
|
className={[
|
||||||
|
"px-4 py-4 align-top font-mono text-xs",
|
||||||
|
claro ? "text-deep" : "text-bright",
|
||||||
|
].join(" ")}
|
||||||
|
>
|
||||||
|
{producto.capacidad ?? <SinDato claro={claro} />}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td
|
||||||
|
className={[
|
||||||
|
"max-w-[14rem] px-4 py-4 align-top font-mono text-xs leading-relaxed",
|
||||||
|
claro ? "text-deep" : "text-bright",
|
||||||
|
].join(" ")}
|
||||||
|
>
|
||||||
|
{producto.agente ?? <SinDato texto="No aplica" claro={claro} />}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td
|
||||||
|
className={[
|
||||||
|
"max-w-[16rem] px-4 py-4 align-top font-mono text-xs leading-relaxed",
|
||||||
|
claro ? "text-deep" : "text-bright",
|
||||||
|
].join(" ")}
|
||||||
|
>
|
||||||
|
{producto.alcance ?? <SinDato claro={claro} />}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td className="px-4 py-4 align-top">
|
||||||
|
<Link
|
||||||
|
href={`/producto/${producto.slug}#cotizar`}
|
||||||
|
className="btn btn-signal btn-sm whitespace-nowrap"
|
||||||
|
style={{ WebkitTapHighlightColor: "transparent" }}
|
||||||
|
>
|
||||||
|
Cotizar
|
||||||
|
</Link>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,14 +1,37 @@
|
|||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { PastillaEscenario } from "@/components/marca/IconoEscenario";
|
||||||
import { RielClases } from "@/components/marca/RielClases";
|
import { RielClases } from "@/components/marca/RielClases";
|
||||||
import { getCategoria, type Producto } from "@/data/catalogo";
|
import { getCategoria, type Producto } from "@/data/catalogo";
|
||||||
|
import { escenariosDeCategoria, type Escenario } from "@/data/escenarios";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tarjeta de producto. Se usa en el catálogo, en la página de categoría y en
|
* TARJETA DE PRODUCTO — versión de tienda.
|
||||||
* los productos relacionados de una ficha.
|
|
||||||
*
|
*
|
||||||
* La tarjeta no vende: identifica. Nombre, riel de clases y los dos datos que
|
* El cliente rechazó la versión anterior por exceso de capas. Aquí se quitaron
|
||||||
* permiten descartar un equipo de un vistazo. Los campos nulos no se pintan.
|
* dos rótulos («Dónde aplica»), la línea de modelo suelta y el enlace
|
||||||
|
* secundario a la ficha, porque el título ya lo es.
|
||||||
|
*
|
||||||
|
* Orden de lectura, seis capas y ni una más:
|
||||||
|
*
|
||||||
|
* 1 imagen reconocer el equipo
|
||||||
|
* 2 meta en mono categoría · modelo, en una sola línea
|
||||||
|
* 3 título enlazado requisito explícito: el clic en el título
|
||||||
|
* abre `/producto/<slug>`
|
||||||
|
* 4 riel de clases la especificación
|
||||||
|
* 5 dos datos declarados lo que permite descartar, sin celdas vacías
|
||||||
|
* 6 «Cotizar este equipo» el botón primario, al ancla del formulario
|
||||||
|
*
|
||||||
|
* Más dos pastillas de escenario como máximo, con «+N». Cinco pastillas en una
|
||||||
|
* rejilla de tres columnas era ruido.
|
||||||
|
*
|
||||||
|
* ---- Sobre los enlaces ----
|
||||||
|
*
|
||||||
|
* Hay dos destinos distintos en la tarjeta: el título va a la ficha y el botón
|
||||||
|
* al formulario de esa ficha. Por eso la tarjeta NO lleva una capa absoluta que
|
||||||
|
* cubra toda su superficie: taparía uno de los dos. La imagen sí enlaza a la
|
||||||
|
* ficha, pero como comodidad de ratón: `tabIndex={-1}` y `aria-hidden` para no
|
||||||
|
* duplicar el destino en el recorrido de teclado ni en el lector de pantalla.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -17,70 +40,115 @@ type Props = {
|
|||||||
claro?: boolean;
|
claro?: boolean;
|
||||||
/** Oculta el nombre de la categoría, cuando ya se está dentro de ella. */
|
/** Oculta el nombre de la categoría, cuando ya se está dentro de ella. */
|
||||||
sinCategoria?: boolean;
|
sinCategoria?: boolean;
|
||||||
|
/** Cuántas pastillas de escenario se pintan antes del «+N». */
|
||||||
|
maxEscenarios?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function TarjetaProducto({ producto, claro = false, sinCategoria = false }: Props) {
|
/**
|
||||||
|
* Escenarios donde este producto es pertinente, ordenados por qué tan central
|
||||||
|
* es su categoría dentro de cada escenario.
|
||||||
|
*/
|
||||||
|
export function escenariosDeProducto(producto: Producto): Escenario[] {
|
||||||
|
return escenariosDeCategoria(producto.categoria)
|
||||||
|
.slice()
|
||||||
|
.sort((a, b) => {
|
||||||
|
const pa = a.categorias.indexOf(producto.categoria);
|
||||||
|
const pb = b.categorias.indexOf(producto.categoria);
|
||||||
|
return pa === pb ? a.orden - b.orden : pa - pb;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
type Dato = { clave: string; valor: string };
|
||||||
|
|
||||||
|
/** Dos datos, omitiendo los `null`. Nunca se rellena con estimaciones. */
|
||||||
|
function datosDe(producto: Producto): Dato[] {
|
||||||
|
const posibles: Array<Dato | null> = [
|
||||||
|
producto.capacidad ? { clave: "Capacidad", valor: producto.capacidad } : null,
|
||||||
|
producto.agente ? { clave: "Agente", valor: producto.agente } : null,
|
||||||
|
producto.alcance ? { clave: "Alcance declarado", valor: producto.alcance } : null,
|
||||||
|
producto.descarga ? { clave: "Descarga", valor: producto.descarga } : null,
|
||||||
|
];
|
||||||
|
return posibles.filter((d): d is Dato => d !== null).slice(0, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TarjetaProducto({
|
||||||
|
producto,
|
||||||
|
claro = false,
|
||||||
|
sinCategoria = false,
|
||||||
|
maxEscenarios = 2,
|
||||||
|
}: Props) {
|
||||||
const categoria = getCategoria(producto.categoria);
|
const categoria = getCategoria(producto.categoria);
|
||||||
|
const escenarios = escenariosDeProducto(producto);
|
||||||
|
const visibles = escenarios.slice(0, maxEscenarios);
|
||||||
|
const restantes = escenarios.length - visibles.length;
|
||||||
|
const datos = datosDe(producto);
|
||||||
|
|
||||||
|
/* Categoría y modelo en una sola línea: dos párrafos en mono, uno debajo del
|
||||||
|
otro, era una capa de más en una tarjeta que debía adelgazar. */
|
||||||
|
const meta = [sinCategoria ? null : categoria?.nombre, producto.modelo]
|
||||||
|
.filter((parte): parte is string => Boolean(parte))
|
||||||
|
.join(" · ");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<article
|
<article
|
||||||
className={[
|
className={[
|
||||||
"group relative flex h-full flex-col overflow-hidden rounded-[6px] border transition-colors duration-200",
|
"group flex h-full min-w-0 flex-col overflow-hidden rounded-[6px] border transition-colors duration-200",
|
||||||
claro
|
claro
|
||||||
? "border-rule-lite bg-white hover:border-signal/60"
|
? "border-rule-lite bg-white hover:border-signal/60"
|
||||||
: "border-rule bg-ink-2 hover:border-rule-2",
|
: "border-rule bg-ink-2 hover:border-rule-2",
|
||||||
"[-webkit-tap-highlight-color:transparent]",
|
|
||||||
].join(" ")}
|
].join(" ")}
|
||||||
>
|
>
|
||||||
<div
|
{/* 1 — imagen */}
|
||||||
|
<Link
|
||||||
|
href={`/producto/${producto.slug}`}
|
||||||
|
tabIndex={-1}
|
||||||
|
aria-hidden="true"
|
||||||
className={[
|
className={[
|
||||||
"relative aspect-[4/3] w-full overflow-hidden border-b",
|
"relative block aspect-[4/3] w-full overflow-hidden border-b",
|
||||||
claro ? "border-rule-lite bg-paper-2" : "border-rule bg-ink-3",
|
claro ? "border-rule-lite bg-paper-2" : "border-rule bg-ink-3",
|
||||||
].join(" ")}
|
].join(" ")}
|
||||||
|
style={{ WebkitTapHighlightColor: "transparent" }}
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
src={producto.imagen}
|
src={producto.imagen}
|
||||||
alt={producto.nombre}
|
alt=""
|
||||||
fill
|
fill
|
||||||
sizes="(max-width: 640px) 92vw, (max-width: 1024px) 44vw, 22rem"
|
sizes="(max-width: 640px) 92vw, (max-width: 1024px) 44vw, 20rem"
|
||||||
className="object-contain p-5 transition-transform duration-300 group-hover:scale-[1.03]"
|
className="object-contain p-5 transition-transform duration-300 group-hover:scale-[1.03]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</Link>
|
||||||
|
|
||||||
<div className="flex flex-1 flex-col p-5">
|
<div className="flex min-w-0 flex-1 flex-col p-4 sm:p-5">
|
||||||
{!sinCategoria && categoria ? (
|
{/* 2 — meta */}
|
||||||
|
{meta !== "" ? (
|
||||||
<p
|
<p
|
||||||
className={[
|
className={[
|
||||||
"font-mono text-[0.625rem] uppercase tracking-[0.14em]",
|
"truncate font-mono text-[0.625rem] uppercase tracking-[0.14em]",
|
||||||
claro ? "text-deep-muted" : "text-faint",
|
claro ? "text-deep-muted" : "text-faint",
|
||||||
].join(" ")}
|
].join(" ")}
|
||||||
>
|
>
|
||||||
{categoria.nombre}
|
{meta}
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<h3
|
{/* 3 — el título es el enlace a la ficha */}
|
||||||
className={[
|
<h3 className="mt-1.5 font-display text-base font-extrabold leading-tight sm:text-lg">
|
||||||
"mt-1.5 font-display text-lg font-extrabold leading-tight",
|
<Link
|
||||||
claro ? "text-deep" : "text-bright",
|
href={`/producto/${producto.slug}`}
|
||||||
].join(" ")}
|
className={[
|
||||||
>
|
"underline decoration-1 underline-offset-4 transition-colors duration-150",
|
||||||
<Link href={`/producto/${producto.slug}`} className="[-webkit-tap-highlight-color:transparent]">
|
claro
|
||||||
<span className="absolute inset-0" aria-hidden="true" />
|
? "text-deep decoration-rule-lite hover:text-signal hover:decoration-signal"
|
||||||
|
: "text-bright decoration-rule-2 hover:text-signal-hi hover:decoration-signal",
|
||||||
|
].join(" ")}
|
||||||
|
style={{ WebkitTapHighlightColor: "transparent" }}
|
||||||
|
>
|
||||||
{producto.nombre}
|
{producto.nombre}
|
||||||
</Link>
|
</Link>
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<p
|
{/* 4 — el riel de clases */}
|
||||||
className={[
|
<div className="mt-3.5">
|
||||||
"mt-2.5 text-sm leading-relaxed",
|
|
||||||
claro ? "text-deep-muted" : "text-muted",
|
|
||||||
].join(" ")}
|
|
||||||
>
|
|
||||||
{producto.resumen}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="mt-auto pt-5">
|
|
||||||
{producto.clases.length > 0 ? (
|
{producto.clases.length > 0 ? (
|
||||||
<RielClases activas={producto.clases} tamano="sm" claro={claro} etiqueta="Clases" />
|
<RielClases activas={producto.clases} tamano="sm" claro={claro} etiqueta="Clases" />
|
||||||
) : (
|
) : (
|
||||||
@@ -93,36 +161,67 @@ export function TarjetaProducto({ producto, claro = false, sinCategoria = false
|
|||||||
Sin clase de fuego declarada
|
Sin clase de fuego declarada
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{producto.capacidad || producto.alcance ? (
|
{/* 5 — dos datos declarados, sin celdas inventadas */}
|
||||||
<dl
|
{datos.length > 0 ? (
|
||||||
className={[
|
<dl
|
||||||
"mt-4 flex flex-wrap gap-x-6 gap-y-2 border-t pt-4",
|
className={[
|
||||||
claro ? "border-rule-lite" : "border-rule",
|
"spec mt-3.5 grid-cols-1",
|
||||||
].join(" ")}
|
claro ? "border-rule-lite bg-rule-lite" : "",
|
||||||
>
|
].join(" ")}
|
||||||
{producto.capacidad ? (
|
>
|
||||||
<div>
|
{datos.map((dato) => (
|
||||||
<dt className={["spec-key", claro ? "!text-deep-muted" : ""].join(" ")}>
|
<div
|
||||||
Capacidad
|
key={dato.clave}
|
||||||
</dt>
|
className={["spec-cell min-w-0", claro ? "bg-white" : ""].join(" ")}
|
||||||
<dd className={["spec-val", claro ? "!text-deep" : ""].join(" ")}>
|
>
|
||||||
{producto.capacidad}
|
<dt className={["spec-key", claro ? "text-deep-muted" : ""].join(" ")}>
|
||||||
</dd>
|
{dato.clave}
|
||||||
</div>
|
</dt>
|
||||||
) : null}
|
<dd className={["spec-val break-words", claro ? "text-deep" : ""].join(" ")}>
|
||||||
{producto.alcance ? (
|
{dato.valor}
|
||||||
<div>
|
</dd>
|
||||||
<dt className={["spec-key", claro ? "!text-deep-muted" : ""].join(" ")}>
|
</div>
|
||||||
Alcance declarado
|
))}
|
||||||
</dt>
|
</dl>
|
||||||
<dd className={["spec-val", claro ? "!text-deep" : ""].join(" ")}>
|
) : null}
|
||||||
{producto.alcance}
|
|
||||||
</dd>
|
{/* pastillas de escenario: dos y «+N» */}
|
||||||
</div>
|
{visibles.length > 0 ? (
|
||||||
) : null}
|
<ul className="mt-3.5 flex flex-wrap gap-1.5">
|
||||||
</dl>
|
{visibles.map((escenario) => (
|
||||||
) : null}
|
<li key={escenario.slug} className="min-w-0">
|
||||||
|
<PastillaEscenario escenario={escenario} claro={claro} />
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
{restantes > 0 ? (
|
||||||
|
<li
|
||||||
|
className={[
|
||||||
|
"inline-flex items-center rounded-[3px] border px-2 py-1.5 font-mono text-[0.6875rem] leading-none",
|
||||||
|
claro
|
||||||
|
? "border-rule-lite bg-paper-2 text-deep-muted"
|
||||||
|
: "border-rule bg-ink-3 text-muted",
|
||||||
|
].join(" ")}
|
||||||
|
title={escenarios
|
||||||
|
.slice(maxEscenarios)
|
||||||
|
.map((e) => e.nombre)
|
||||||
|
.join(", ")}
|
||||||
|
>
|
||||||
|
+{restantes}
|
||||||
|
</li>
|
||||||
|
) : null}
|
||||||
|
</ul>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{/* 6 — el botón primario */}
|
||||||
|
<div className="mt-auto pt-4">
|
||||||
|
<Link
|
||||||
|
href={`/producto/${producto.slug}#cotizar`}
|
||||||
|
className="btn btn-signal btn-sm w-full"
|
||||||
|
>
|
||||||
|
Cotizar este equipo
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
|||||||
@@ -0,0 +1,139 @@
|
|||||||
|
/**
|
||||||
|
* BÚSQUEDA DEL CATÁLOGO — expresiones regulares con red de seguridad.
|
||||||
|
*
|
||||||
|
* El cliente pidió búsqueda con regex. El problema de una regex escrita en
|
||||||
|
* vivo es que la mitad del tiempo está incompleta: quien escribe `(espuma`
|
||||||
|
* pasa por un estado inválido antes de llegar a `(espuma|agente)`. Si el
|
||||||
|
* `new RegExp` no está protegido, ese carácter intermedio tira la página.
|
||||||
|
*
|
||||||
|
* Por eso hay dos modos y la degradación es silenciosa:
|
||||||
|
*
|
||||||
|
* regex el término compiló como expresión regular con banderas `iu`
|
||||||
|
* texto no compiló, así que se busca la cadena tal cual, sin acentos
|
||||||
|
* ni mayúsculas, y se avisa que la expresión está incompleta
|
||||||
|
*
|
||||||
|
* La prueba se hace campo por campo, no sobre un texto concatenado. Eso es lo
|
||||||
|
* que vuelve útiles los anclajes: `^enforcer` encuentra el modelo
|
||||||
|
* «Enforcer 12» aunque el nombre completo sea «Extintor Enforcer 12 litros».
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { CATEGORIAS, type Producto } from "@/data/catalogo";
|
||||||
|
|
||||||
|
const NOMBRE_CATEGORIA = new Map(CATEGORIAS.map((c) => [c.slug, c.nombre]));
|
||||||
|
|
||||||
|
/** Longitud máxima aceptada. Corta patrones absurdos antes de compilarlos. */
|
||||||
|
const LIMITE_TERMINO = 160;
|
||||||
|
|
||||||
|
/** "portátil" → "portatil". Conserva posiciones, así que `^` sigue sirviendo. */
|
||||||
|
export function sinAcentos(texto: string): string {
|
||||||
|
return texto.normalize("NFD").replace(/\p{Diacritic}/gu, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Campos sobre los que se busca, en el orden en que importan.
|
||||||
|
* Nombre, modelo, agente, categoría, características y aplicaciones —
|
||||||
|
* más los datos declarados, que es lo que un comprador técnico teclea.
|
||||||
|
*/
|
||||||
|
export function camposDe(producto: Producto): string[] {
|
||||||
|
return [
|
||||||
|
producto.nombre,
|
||||||
|
producto.modelo ?? "",
|
||||||
|
producto.agente ?? "",
|
||||||
|
NOMBRE_CATEGORIA.get(producto.categoria) ?? producto.categoria,
|
||||||
|
producto.capacidad ?? "",
|
||||||
|
producto.alcance ?? "",
|
||||||
|
producto.descarga ?? "",
|
||||||
|
producto.resumen,
|
||||||
|
...producto.caracteristicas,
|
||||||
|
...producto.aplicaciones,
|
||||||
|
producto.clases.join(" "),
|
||||||
|
].filter((campo) => campo !== "");
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ModoBusqueda = "vacio" | "regex" | "texto";
|
||||||
|
|
||||||
|
export type Buscador = {
|
||||||
|
modo: ModoBusqueda;
|
||||||
|
/** Aviso discreto cuando la expresión no compiló. Nunca es un error rojo. */
|
||||||
|
aviso: string | null;
|
||||||
|
coincide: (producto: Producto) => boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const AVISO_INCOMPLETA =
|
||||||
|
"La expresión regular todavía está incompleta. Mientras la terminas buscamos el texto tal cual, sin distinguir acentos ni mayúsculas.";
|
||||||
|
|
||||||
|
export function construirBuscador(termino: string): Buscador {
|
||||||
|
const limpio = termino.trim().slice(0, LIMITE_TERMINO);
|
||||||
|
|
||||||
|
if (limpio === "") {
|
||||||
|
return { modo: "vacio", aviso: null, coincide: () => true };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Banderas `iu`: insensible a mayúsculas y con clases Unicode, para que
|
||||||
|
// `\d`, `\s` y `\p{...}` se comporten igual que en la documentación.
|
||||||
|
const patron = new RegExp(limpio, "iu");
|
||||||
|
return {
|
||||||
|
modo: "regex",
|
||||||
|
aviso: null,
|
||||||
|
coincide: (producto) =>
|
||||||
|
camposDe(producto).some(
|
||||||
|
(campo) => patron.test(campo) || patron.test(sinAcentos(campo)),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
// Aquí llega `(`, `[`, `*` a media palabra o cualquier patrón a medias.
|
||||||
|
const aguja = sinAcentos(limpio).toLowerCase();
|
||||||
|
return {
|
||||||
|
modo: "texto",
|
||||||
|
aviso: AVISO_INCOMPLETA,
|
||||||
|
coincide: (producto) =>
|
||||||
|
camposDe(producto).some((campo) =>
|
||||||
|
sinAcentos(campo).toLowerCase().includes(aguja),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ejemplos pulsables. Enseñar la capacidad es parte de la función: nadie
|
||||||
|
* escribe una regex en un buscador si nunca vio que ahí se podía.
|
||||||
|
* Los cuatro devuelven resultados reales sobre este catálogo.
|
||||||
|
*/
|
||||||
|
export const EJEMPLOS_REGEX: Array<{ patron: string; explica: string }> = [
|
||||||
|
{ patron: "^enforcer", explica: "Modelos que arrancan con Enforcer" },
|
||||||
|
{ patron: "\\d+\\s?L\\b", explica: "Cualquier capacidad en litros" },
|
||||||
|
{ patron: "(espuma|agente)", explica: "Espumas y agentes extinguidores" },
|
||||||
|
{ patron: "carretilla|m[oó]vil", explica: "Equipo que se lleva al punto" },
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Familias de agente presentes en el catálogo. No es una clasificación
|
||||||
|
* nuestra: son los nombres comerciales que la propia fuente declara en el
|
||||||
|
* campo `agente`. Un producto puede pertenecer a dos ("FireAde 2000 o
|
||||||
|
* HFC-236"), así que la comparación es por pertenencia, no por igualdad.
|
||||||
|
*/
|
||||||
|
export const FAMILIAS_AGENTE = [
|
||||||
|
"FireAde",
|
||||||
|
"FlameAde",
|
||||||
|
"TrakAde",
|
||||||
|
"VaporAde",
|
||||||
|
"HFC-236",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export type FamiliaAgente = (typeof FAMILIAS_AGENTE)[number];
|
||||||
|
|
||||||
|
export function familiasDeAgente(agente: string | null): FamiliaAgente[] {
|
||||||
|
if (!agente) return [];
|
||||||
|
const texto = agente.toLowerCase();
|
||||||
|
return FAMILIAS_AGENTE.filter((familia) => texto.includes(familia.toLowerCase()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Familias que efectivamente aparecen en el catálogo, en orden de aparición. */
|
||||||
|
export function familiasDisponibles(productos: Producto[]): FamiliaAgente[] {
|
||||||
|
const vistas = new Set<FamiliaAgente>();
|
||||||
|
for (const producto of productos) {
|
||||||
|
for (const familia of familiasDeAgente(producto.agente)) vistas.add(familia);
|
||||||
|
}
|
||||||
|
return FAMILIAS_AGENTE.filter((familia) => vistas.has(familia));
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import { CATEGORIAS } from "@/data/catalogo";
|
||||||
|
import { parseClases, serializeClases, type ClaseFuego } from "@/data/clases-fuego";
|
||||||
|
import { getEscenario } from "@/data/escenarios";
|
||||||
|
import { FAMILIAS_AGENTE } from "@/components/site/catalogo/busqueda";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ESTADO DEL CATÁLOGO — la verdad vive en la cadena de consulta.
|
||||||
|
*
|
||||||
|
* Se extrajo del explorador a su propio módulo porque lo comparten el servidor
|
||||||
|
* —que lee y valida la URL— y el cliente —que la reescribe sin navegar—.
|
||||||
|
*
|
||||||
|
* `/catalogo?escenario=gasolineras&clases=B,K&categoria=rociadores&q=^enforcer`
|
||||||
|
*
|
||||||
|
* Otras páginas del sitio ya enlazan a `/catalogo?escenario=<slug>`. Ese
|
||||||
|
* contrato no se rompe: cualquier parámetro desconocido o inválido se descarta
|
||||||
|
* en silencio, nunca tira la página.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type EstadoCatalogo = {
|
||||||
|
escenario: string | null;
|
||||||
|
clases: ClaseFuego[];
|
||||||
|
categoria: string;
|
||||||
|
agente: string;
|
||||||
|
q: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ESTADO_VACIO: EstadoCatalogo = {
|
||||||
|
escenario: null,
|
||||||
|
clases: [],
|
||||||
|
categoria: "",
|
||||||
|
agente: "",
|
||||||
|
q: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Longitud máxima del término de búsqueda que se acepta desde la URL. */
|
||||||
|
const LIMITE_Q = 160;
|
||||||
|
|
||||||
|
const SLUGS_CATEGORIA = new Set(CATEGORIAS.map((c) => c.slug));
|
||||||
|
const SLUGS_AGENTE = new Set<string>(FAMILIAS_AGENTE);
|
||||||
|
|
||||||
|
function unico(valor: string | string[] | undefined): string {
|
||||||
|
if (Array.isArray(valor)) return valor[0] ?? "";
|
||||||
|
return valor ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/** La cadena de consulta es entrada de usuario: se valida, no se confía. */
|
||||||
|
export function leerEstado(
|
||||||
|
params: Record<string, string | string[] | undefined>,
|
||||||
|
): EstadoCatalogo {
|
||||||
|
const slugEscenario = unico(params.escenario);
|
||||||
|
const categoria = unico(params.categoria);
|
||||||
|
const agente = unico(params.agente);
|
||||||
|
|
||||||
|
return {
|
||||||
|
escenario: getEscenario(slugEscenario) ? slugEscenario : null,
|
||||||
|
clases: parseClases(unico(params.clases)),
|
||||||
|
categoria: SLUGS_CATEGORIA.has(categoria) ? categoria : "",
|
||||||
|
agente: SLUGS_AGENTE.has(agente) ? agente : "",
|
||||||
|
q: unico(params.q).slice(0, LIMITE_Q),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** El inverso: estado a `?a=b&c=d`. Cadena vacía cuando no hay filtro. */
|
||||||
|
export function construirConsulta(estado: EstadoCatalogo): string {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (estado.escenario) params.set("escenario", estado.escenario);
|
||||||
|
if (estado.clases.length > 0) params.set("clases", serializeClases(estado.clases));
|
||||||
|
if (estado.categoria !== "") params.set("categoria", estado.categoria);
|
||||||
|
if (estado.agente !== "") params.set("agente", estado.agente);
|
||||||
|
if (estado.q.trim() !== "") params.set("q", estado.q.trim());
|
||||||
|
const cadena = params.toString();
|
||||||
|
return cadena === "" ? "" : `?${cadena}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hayFiltroActivo(estado: EstadoCatalogo): boolean {
|
||||||
|
return (
|
||||||
|
estado.escenario !== null ||
|
||||||
|
estado.clases.length > 0 ||
|
||||||
|
estado.categoria !== "" ||
|
||||||
|
estado.agente !== "" ||
|
||||||
|
estado.q.trim() !== ""
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Cuántos filtros están puestos. Es el número que va en el botón «Filtros». */
|
||||||
|
export function cuantosFiltros(estado: EstadoCatalogo): number {
|
||||||
|
let n = 0;
|
||||||
|
if (estado.escenario !== null) n += 1;
|
||||||
|
if (estado.clases.length > 0) n += 1;
|
||||||
|
if (estado.categoria !== "") n += 1;
|
||||||
|
if (estado.agente !== "") n += 1;
|
||||||
|
if (estado.q.trim() !== "") n += 1;
|
||||||
|
return n;
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migas de pan de las páginas de contenido.
|
||||||
|
*
|
||||||
|
* Viaja en la prop `antes` de `HeroeLanding`, así que se dibuja sobre la
|
||||||
|
* fotografía: de ahí el color claro y el filete de separación implícito.
|
||||||
|
*/
|
||||||
|
export function Migas({
|
||||||
|
actual,
|
||||||
|
intermedio,
|
||||||
|
}: {
|
||||||
|
/** Nombre de la página actual. No es enlace. */
|
||||||
|
actual: string;
|
||||||
|
/** Nivel intermedio opcional, cuando la página cuelga de otra sección. */
|
||||||
|
intermedio?: { href: string; label: string };
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<nav aria-label="Ruta de navegación">
|
||||||
|
<ol className="flex flex-wrap items-center gap-2 font-mono text-[0.625rem] uppercase tracking-[0.14em] text-faint">
|
||||||
|
<li>
|
||||||
|
<Link
|
||||||
|
href="/"
|
||||||
|
className="transition-colors hover:text-bright [-webkit-tap-highlight-color:transparent]"
|
||||||
|
>
|
||||||
|
Inicio
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
<li aria-hidden="true">/</li>
|
||||||
|
{intermedio ? (
|
||||||
|
<>
|
||||||
|
<li>
|
||||||
|
<Link
|
||||||
|
href={intermedio.href}
|
||||||
|
className="transition-colors hover:text-bright [-webkit-tap-highlight-color:transparent]"
|
||||||
|
>
|
||||||
|
{intermedio.label}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
<li aria-hidden="true">/</li>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
<li className="text-bright">{actual}</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -3,37 +3,47 @@ import { Revelar } from "@/components/site/Revelar";
|
|||||||
import { EMPRESA } from "@/data/empresa";
|
import { EMPRESA } from "@/data/empresa";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DIAGNÓSTICO — el formulario embebido en el inicio, requisito del cliente.
|
* DIAGNÓSTICO — el destino de todos los llamados primarios del inicio.
|
||||||
*
|
*
|
||||||
* Dos columnas: a la izquierda el argumento de por qué conviene pedirlo, a la
|
* La sección lleva `id="diagnostico"` y el formulario se titula exactamente
|
||||||
* derecha la captura. El estudio señaló que el formulario original pedía datos
|
* igual que los botones que lo apuntan: "Recibir cotización". Ese es el
|
||||||
* sin ofrecer nada a cambio; aquí lo que se ofrece está escrito.
|
* punto — el visitante debe reconocer que llegó a donde el botón prometía, sin
|
||||||
|
* un cambio de nombre a medio camino.
|
||||||
|
*
|
||||||
|
* A la izquierda, qué recibe a cambio de sus datos: el estudio señaló que el
|
||||||
|
* formulario original pedía información sin ofrecer nada. Cada paso está
|
||||||
|
* escrito primero en lo que la persona reconoce y después en el término que el
|
||||||
|
* técnico busca.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const QUE_RECIBES = [
|
const QUE_RECIBES = [
|
||||||
{
|
{
|
||||||
paso: "01",
|
paso: "01",
|
||||||
titulo: "Clasificación del escenario",
|
titulo: "Qué se puede quemar y por dónde empieza",
|
||||||
texto:
|
texto:
|
||||||
"Qué clase de fuego tienes, dónde está el punto de origen probable y qué tan rápido puede crecer.",
|
"Qué hay en tu operación que arde y cómo se clasifica: sólidos, líquidos inflamables, equipo energizado, metales o aceite de cocina —clases A, B, C, D y K—, con el punto de origen más probable.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
paso: "02",
|
paso: "02",
|
||||||
titulo: "Agente y equipo que corresponden",
|
titulo: "Qué equipo te toca y cuánto",
|
||||||
texto:
|
texto:
|
||||||
"Qué agente aplica a ese combustible, en qué capacidad y en qué formato: portátil, móvil, fijo o automático.",
|
"Qué agente aplica a ese combustible, en qué capacidad y en qué formato: portátil, móvil, fijo o automático. Sin comprar de más por miedo ni de menos por precio.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
paso: "03",
|
paso: "03",
|
||||||
titulo: "Qué te toca documentar",
|
titulo: "Qué papeles te van a pedir",
|
||||||
texto:
|
texto:
|
||||||
"La lista de lo que debe existir en tu expediente para operación, verificación y renovación de póliza.",
|
"La lista de lo que debe existir en tu expediente para operar, para la verificación de Protección Civil y para renovar la póliza con tu aseguradora.",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export function BloqueDiagnostico() {
|
export function BloqueDiagnostico() {
|
||||||
return (
|
return (
|
||||||
<section className="bg-ink" aria-labelledby="diagnostico-titulo">
|
<section
|
||||||
|
id="diagnostico"
|
||||||
|
className="scroll-mt-24 bg-ink"
|
||||||
|
aria-labelledby="diagnostico-titulo"
|
||||||
|
>
|
||||||
<div className="shell py-16 sm:py-20">
|
<div className="shell py-16 sm:py-20">
|
||||||
<div className="grid gap-10 lg:grid-cols-12 lg:gap-14">
|
<div className="grid gap-10 lg:grid-cols-12 lg:gap-14">
|
||||||
<Revelar className="lg:col-span-5">
|
<Revelar className="lg:col-span-5">
|
||||||
@@ -43,12 +53,12 @@ export function BloqueDiagnostico() {
|
|||||||
className="mt-4 font-display uppercase tracking-[-0.02em] text-bright"
|
className="mt-4 font-display uppercase tracking-[-0.02em] text-bright"
|
||||||
style={{ fontSize: "clamp(1.6rem, 3.6vw, 2.5rem)" }}
|
style={{ fontSize: "clamp(1.6rem, 3.6vw, 2.5rem)" }}
|
||||||
>
|
>
|
||||||
Pide el diagnóstico, no la lista de precios
|
Te cotizamos el escenario, no una lista de precios
|
||||||
</h2>
|
</h2>
|
||||||
<p className="mt-4 text-muted">
|
<p className="mt-4 text-muted">
|
||||||
Una cotización sin escenario es una adivinanza cara de corregir. Cuéntanos qué opera
|
Una cotización hecha sin ver el escenario sale caro de corregir: o sobra equipo o
|
||||||
tu instalación y qué tienes montado hoy; con eso podemos responder algo útil desde el
|
falta cobertura donde importa. Cuéntanos qué opera tu instalación y qué tienes montado
|
||||||
primer correo.
|
hoy, y la primera respuesta ya viene con criterio, no con una lista de productos.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<ol className="mt-8 space-y-5">
|
<ol className="mt-8 space-y-5">
|
||||||
@@ -82,8 +92,8 @@ export function BloqueDiagnostico() {
|
|||||||
<Revelar className="lg:col-span-7" delay={90}>
|
<Revelar className="lg:col-span-7" delay={90}>
|
||||||
<FormularioContacto
|
<FormularioContacto
|
||||||
canal="home"
|
canal="home"
|
||||||
titulo="Solicitar diagnóstico"
|
titulo="Recibe tu cotización"
|
||||||
descripcion="Entre más preciso el escenario, más útil la primera respuesta. Los campos de riesgo son opcionales, pero cambian por completo lo que podemos decirte."
|
descripcion="Cuatro campos. Si en el mensaje nos dices qué operas y qué tienes instalado hoy, la primera respuesta ya trae el equipo que aplica y la lista de documentos que te van a pedir."
|
||||||
/>
|
/>
|
||||||
</Revelar>
|
</Revelar>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,7 +3,13 @@ import { Revelar } from "@/components/site/Revelar";
|
|||||||
import { NORMAS } from "@/data/empresa";
|
import { NORMAS } from "@/data/empresa";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CUMPLIMIENTO — ruta C del inicio.
|
* CUMPLIMIENTO — el argumento normativo, resuelto aquí mismo.
|
||||||
|
*
|
||||||
|
* Antes esta sección terminaba en dos botones que sacaban al visitante del
|
||||||
|
* embudo: `/cumplimiento-nom-002` y `/evidencia`. Las dos son páginas de
|
||||||
|
* lectura y siguen en la navegación y en el pie para quien las busque a
|
||||||
|
* propósito; desde una sección de venta, lo que corresponde es cerrar el
|
||||||
|
* argumento en dos frases y dejar el único llamado apuntando al formulario.
|
||||||
*
|
*
|
||||||
* Aquí sí caben `text-ember` y `.hazard`, con medida: la NOM-002 aplica a todo
|
* Aquí sí caben `text-ember` y `.hazard`, con medida: la NOM-002 aplica a todo
|
||||||
* centro de trabajo en México y el incumplimiento es riesgo real, no un recurso
|
* centro de trabajo en México y el incumplimiento es riesgo real, no un recurso
|
||||||
@@ -29,43 +35,46 @@ export function BloqueNorma() {
|
|||||||
<div className="shell py-16 sm:py-20">
|
<div className="shell py-16 sm:py-20">
|
||||||
<div className="grid gap-10 lg:grid-cols-12 lg:gap-12">
|
<div className="grid gap-10 lg:grid-cols-12 lg:gap-12">
|
||||||
<Revelar className="lg:col-span-5">
|
<Revelar className="lg:col-span-5">
|
||||||
<p className="eyebrow">Ruta C · Por cumplimiento</p>
|
<p className="eyebrow">NOM-002-STPS-2010</p>
|
||||||
<h2
|
<h2
|
||||||
id="norma-titulo"
|
id="norma-titulo"
|
||||||
className="mt-4 font-display uppercase tracking-[-0.02em] text-bright"
|
className="mt-4 font-display uppercase tracking-[-0.02em] text-bright"
|
||||||
style={{ fontSize: "clamp(1.6rem, 3.6vw, 2.5rem)" }}
|
style={{ fontSize: "clamp(1.6rem, 3.6vw, 2.5rem)" }}
|
||||||
>
|
>
|
||||||
Comprar extintores no es cumplir
|
Cumplir la NOM-002 no es comprar extintores
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div className="hazard mt-6 h-1.5 w-24 rounded-[1px]" aria-hidden="true" />
|
<div className="hazard mt-6 h-1.5 w-24 rounded-[1px]" aria-hidden="true" />
|
||||||
|
|
||||||
<p className="mt-6 text-muted">
|
<p className="mt-6 text-muted">
|
||||||
La NOM-002-STPS-2010 aplica a{" "}
|
Comprar el equipo es la parte fácil; lo que se revisa es el papel que prueba que
|
||||||
|
existe, que corresponde a tu nivel de riesgo y que alguien lo inspeccionó. La
|
||||||
|
NOM-002-STPS-2010 aplica a{" "}
|
||||||
<strong className="font-semibold text-ember">todo centro de trabajo en México</strong>
|
<strong className="font-semibold text-ember">todo centro de trabajo en México</strong>
|
||||||
, sin importar el giro ni el tamaño. Y no se satisface con una compra: exige
|
, sin importar giro ni tamaño, y exige clasificar el riesgo de incendio, sostener un
|
||||||
clasificar el riesgo, sostener un plan, capacitar personal y conservar el registro de
|
plan por escrito, capacitar brigadas y conservar el registro de la revisión mensual
|
||||||
todo eso.
|
conforme a la NOM-154-SCFI-2005.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p className="mt-4 text-sm leading-relaxed text-faint">
|
<p className="mt-4 text-sm leading-relaxed text-faint">
|
||||||
La mayoría de los hallazgos que vemos no son por falta de equipo, sino por falta de
|
Por eso la mayoría de los hallazgos que vemos no son por falta de equipo, sino por
|
||||||
expediente. Es la parte que un proveedor de catálogo no te entrega.
|
falta de expediente. Esa es la parte que un proveedor de catálogo no te entrega, y la
|
||||||
|
que resolvemos junto con la compra.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="mt-7 flex flex-wrap gap-3">
|
<div className="mt-7">
|
||||||
<Link href="/cumplimiento-nom-002" className="btn btn-signal">
|
<Link href="#diagnostico" className="btn btn-signal" style={{ WebkitTapHighlightColor: "transparent" }}>
|
||||||
Validar cumplimiento NOM-002
|
Recibir cotización
|
||||||
</Link>
|
|
||||||
<Link href="/evidencia" className="btn btn-ghost">
|
|
||||||
Centro de evidencia
|
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</Revelar>
|
</Revelar>
|
||||||
|
|
||||||
<div className="lg:col-span-7">
|
<div className="lg:col-span-7">
|
||||||
<Revelar>
|
<Revelar>
|
||||||
<ul className="panel divide-y divide-rule">
|
<p className="font-mono text-[0.625rem] uppercase tracking-[0.16em] text-faint">
|
||||||
|
Lo que la norma pide, en orden
|
||||||
|
</p>
|
||||||
|
<ul className="panel mt-3 divide-y divide-rule">
|
||||||
{LO_QUE_EXIGE.map((item) => (
|
{LO_QUE_EXIGE.map((item) => (
|
||||||
<li key={item} className="flex items-start gap-3 px-5 py-3.5">
|
<li key={item} className="flex items-start gap-3 px-5 py-3.5">
|
||||||
<span
|
<span
|
||||||
|
|||||||
@@ -1,16 +1,30 @@
|
|||||||
import Image from "next/image";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { RielClases } from "@/components/marca/RielClases";
|
|
||||||
import { Revelar } from "@/components/site/Revelar";
|
import { Revelar } from "@/components/site/Revelar";
|
||||||
import { getProducto, type Producto } from "@/data/catalogo";
|
import { TarjetaProducto } from "@/components/site/catalogo/TarjetaProducto";
|
||||||
|
import { CATEGORIAS, getProducto, PRODUCTOS, type Producto } from "@/data/catalogo";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CATÁLOGO DESTACADO — seis equipos que cubren el ancho real de la oferta:
|
* EQUIPOS DESTACADOS — seis equipos que cubren el ancho real de la oferta:
|
||||||
* portátil, móvil, fijo, automático, agente y robótico. No es un escaparate de
|
* portátil, móvil, fijo, automático, agente y robótico. No es un escaparate de
|
||||||
* los más vendidos (no tenemos ese dato); es una muestra de amplitud.
|
* los más vendidos (no tenemos ese dato); es una muestra de amplitud.
|
||||||
*
|
*
|
||||||
* La tira `.spec` solo imprime los campos que existen: los `null` del catálogo
|
* ---- Por qué usa la tarjeta del catálogo ----
|
||||||
* se omiten en lugar de rellenarse con guiones o con datos inventados.
|
*
|
||||||
|
* Antes esta sección tenía su propia tarjeta: enlazaba entera a la ficha y
|
||||||
|
* cerraba con un "Ver ficha" en gris. La tarjeta de `/catalogo` hace dos cosas
|
||||||
|
* que esta no hacía y que el embudo necesita: el clic en el título abre la
|
||||||
|
* ficha y un botón rojo lleva directo a `/producto/<slug>#cotizar`. Mantener
|
||||||
|
* dos tarjetas distintas para el mismo producto obligaba al visitante a
|
||||||
|
* reaprender la interfaz al pasar del inicio al catálogo, así que se reúsa
|
||||||
|
* `TarjetaProducto` tal cual.
|
||||||
|
*
|
||||||
|
* ---- Jerarquía de la sección ----
|
||||||
|
*
|
||||||
|
* El llamado primario son los seis "Cotizar este equipo": son el paso más
|
||||||
|
* profundo del embudo que ofrece esta página. Devolver a alguien al catálogo con
|
||||||
|
* un botón más fuerte que ése sería subirlo de nuevo, así que el cierre al
|
||||||
|
* catálogo completo va en peso secundario. El enlace de cabecera que antes
|
||||||
|
* competía con los dos desapareció.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const DESTACADOS = [
|
const DESTACADOS = [
|
||||||
@@ -26,32 +40,30 @@ const PRODUCTOS_DESTACADOS: Producto[] = DESTACADOS.map(getProducto).filter(
|
|||||||
(p): p is Producto => Boolean(p),
|
(p): p is Producto => Boolean(p),
|
||||||
);
|
);
|
||||||
|
|
||||||
function especificaciones(p: Producto): { clave: string; valor: string }[] {
|
|
||||||
return [
|
|
||||||
{ clave: "Capacidad", valor: p.capacidad },
|
|
||||||
{ clave: "Agente", valor: p.agente },
|
|
||||||
{ clave: "Alcance", valor: p.alcance },
|
|
||||||
].filter((c): c is { clave: string; valor: string } => Boolean(c.valor));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function CatalogoDestacado() {
|
export function CatalogoDestacado() {
|
||||||
return (
|
return (
|
||||||
<section className="bg-ink" aria-labelledby="catalogo-titulo">
|
<section id="equipos" className="scroll-mt-24 bg-ink" aria-labelledby="catalogo-titulo">
|
||||||
<div className="shell py-16 sm:py-20">
|
<div className="shell py-16 sm:py-20">
|
||||||
<Revelar className="flex flex-col gap-5 sm:flex-row sm:items-end sm:justify-between">
|
<Revelar className="max-w-[42rem]">
|
||||||
<div className="max-w-[34rem]">
|
<p className="eyebrow">Equipo contra incendio</p>
|
||||||
<p className="eyebrow">Equipo</p>
|
<h2
|
||||||
<h2
|
id="catalogo-titulo"
|
||||||
id="catalogo-titulo"
|
className="mt-4 font-display uppercase tracking-[-0.02em] text-bright"
|
||||||
className="mt-4 font-display uppercase tracking-[-0.02em] text-bright"
|
style={{ fontSize: "clamp(1.6rem, 3.6vw, 2.5rem)" }}
|
||||||
style={{ fontSize: "clamp(1.6rem, 3.6vw, 2.5rem)" }}
|
>
|
||||||
>
|
Del extintor de un litro al cañón automático
|
||||||
Del extintor de un litro al cañón automático
|
</h2>
|
||||||
</h2>
|
<p className="mt-4 text-muted">
|
||||||
</div>
|
Un extintor de mano y un monitor automático resuelven problemas distintos. Estos{" "}
|
||||||
<Link href="/catalogo" className="btn btn-ghost btn-sm shrink-0">
|
{PRODUCTOS_DESTACADOS.length} de los {PRODUCTOS.length} equipos del catálogo cubren el
|
||||||
Catálogo completo
|
ancho de la oferta —portátil, móvil, fijo, automático, agente y robótico— y lo que
|
||||||
</Link>
|
cambia entre ellos es el agente, la capacidad y el alcance: esos tres datos deciden si
|
||||||
|
el equipo sirve para tu escenario o solo llena el requisito de la pared.
|
||||||
|
</p>
|
||||||
|
<p className="mt-3.5 text-sm leading-relaxed text-faint">
|
||||||
|
El nombre abre la ficha técnica con la especificación declarada y lo que documentamos
|
||||||
|
antes de cotizar. El botón rojo lleva a la cotización de ese equipo.
|
||||||
|
</p>
|
||||||
</Revelar>
|
</Revelar>
|
||||||
|
|
||||||
{/* En móvil es un riel con arrastre; desde md se acomoda en retícula. */}
|
{/* En móvil es un riel con arrastre; desde md se acomoda en retícula. */}
|
||||||
@@ -59,82 +71,35 @@ export function CatalogoDestacado() {
|
|||||||
className="mt-10 flex snap-x snap-mandatory gap-4 overflow-x-auto overscroll-x-contain pb-3 md:grid md:grid-cols-2 md:overflow-visible md:pb-0 lg:grid-cols-3"
|
className="mt-10 flex snap-x snap-mandatory gap-4 overflow-x-auto overscroll-x-contain pb-3 md:grid md:grid-cols-2 md:overflow-visible md:pb-0 lg:grid-cols-3"
|
||||||
style={{ WebkitOverflowScrolling: "touch" }}
|
style={{ WebkitOverflowScrolling: "touch" }}
|
||||||
>
|
>
|
||||||
{PRODUCTOS_DESTACADOS.map((p, i) => {
|
{PRODUCTOS_DESTACADOS.map((p, i) => (
|
||||||
const specs = especificaciones(p);
|
<Revelar
|
||||||
return (
|
as="li"
|
||||||
<Revelar
|
key={p.slug}
|
||||||
as="li"
|
delay={(i % 3) * 80}
|
||||||
key={p.slug}
|
className="w-[78%] shrink-0 snap-start sm:w-[52%] md:w-auto"
|
||||||
delay={(i % 3) * 80}
|
>
|
||||||
className="w-[78%] shrink-0 snap-start sm:w-[52%] md:w-auto"
|
<TarjetaProducto producto={p} />
|
||||||
>
|
</Revelar>
|
||||||
<Link
|
))}
|
||||||
href={`/producto/${p.slug}`}
|
|
||||||
className="panel group flex h-full flex-col overflow-hidden transition-colors duration-200 hover:border-rule-2"
|
|
||||||
style={{ WebkitTapHighlightColor: "transparent" }}
|
|
||||||
>
|
|
||||||
<div className="relative aspect-[4/3] w-full bg-ink-3">
|
|
||||||
<Image
|
|
||||||
src={p.imagen}
|
|
||||||
alt={p.nombre}
|
|
||||||
fill
|
|
||||||
sizes="(min-width: 1024px) 21rem, (min-width: 768px) 45vw, 78vw"
|
|
||||||
className="object-contain p-6 transition-transform duration-500 group-hover:scale-[1.04]"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-1 flex-col p-5">
|
|
||||||
{p.modelo ? (
|
|
||||||
<p className="font-mono text-[0.625rem] uppercase tracking-[0.16em] text-faint">
|
|
||||||
{p.modelo}
|
|
||||||
</p>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<h3 className="mt-2 font-display text-base uppercase tracking-[-0.01em] text-bright">
|
|
||||||
{p.nombre}
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<RielClases activas={p.clases} tamano="sm" className="mt-4" />
|
|
||||||
|
|
||||||
{/* auto-fit: una columna en tarjeta angosta, hasta tres cuando
|
|
||||||
hay ancho. Sin media queries y sin desbordes. */}
|
|
||||||
{specs.length > 0 ? (
|
|
||||||
<dl
|
|
||||||
className="spec mt-4"
|
|
||||||
style={{ gridTemplateColumns: "repeat(auto-fit, minmax(6.5rem, 1fr))" }}
|
|
||||||
>
|
|
||||||
{specs.map((s) => (
|
|
||||||
<div key={s.clave} className="spec-cell">
|
|
||||||
<dt className="spec-key">{s.clave}</dt>
|
|
||||||
<dd className="spec-val break-words">{s.valor}</dd>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</dl>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<span className="mt-5 flex items-center gap-2 font-mono text-[0.6875rem] uppercase tracking-[0.1em] text-muted transition-colors group-hover:text-bright">
|
|
||||||
Ver ficha
|
|
||||||
<svg
|
|
||||||
width="13"
|
|
||||||
height="9"
|
|
||||||
viewBox="0 0 13 9"
|
|
||||||
aria-hidden="true"
|
|
||||||
className="transition-transform duration-200 group-hover:translate-x-1"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M0 4.5h10.5M7.5 1.5l3 3-3 3"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="1.4"
|
|
||||||
fill="none"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
</Revelar>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
<Revelar className="mt-9 flex flex-col gap-4 border-t border-rule pt-7 sm:flex-row sm:items-center sm:justify-between">
|
||||||
|
<Link href="/catalogo" className="btn btn-ghost w-full shrink-0 sm:w-auto">
|
||||||
|
Ver los {PRODUCTOS.length} equipos del catálogo
|
||||||
|
<svg width="14" height="10" viewBox="0 0 14 10" aria-hidden="true">
|
||||||
|
<path
|
||||||
|
d="M1 5h11M8.5 1.5L12 5l-3.5 3.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
fill="none"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</Link>
|
||||||
|
<p className="max-w-[34rem] text-sm leading-relaxed text-muted">
|
||||||
|
Filtrable por clase de fuego, por {CATEGORIAS.length} categorías, por familia de agente
|
||||||
|
y por el lugar que proteges.
|
||||||
|
</p>
|
||||||
|
</Revelar>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
import { Revelar } from "@/components/site/Revelar";
|
||||||
|
import { hrefCategoria } from "@/components/site/home/enlaces";
|
||||||
|
import { CATEGORIAS, PRODUCTOS, productosDeCategoria } from "@/data/catalogo";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CATEGORÍAS DEL CATÁLOGO — el acceso directo al equipo.
|
||||||
|
*
|
||||||
|
* Ocupa la posición que antes tenían las rutas por industria: es lo primero
|
||||||
|
* después del héroe porque el encargo del cliente es que el inicio lleve al
|
||||||
|
* catálogo, y ocho enlaces filtrados son el camino más corto para llegar.
|
||||||
|
*
|
||||||
|
* No es el escaparate de ocho categorías que el estudio de marca criticó. Ese
|
||||||
|
* escaparate era el sitio entero: ocho fichas de lectura sin criterio de
|
||||||
|
* selección. Aquí las ocho son **filtros del catálogo**
|
||||||
|
* —`/catalogo?categoria=<slug>`—, con el conteo real de equipos y el `tagline`
|
||||||
|
* que dice para qué sirve la categoría. La ficha de categoría sigue existiendo
|
||||||
|
* en `/catalogo/<slug>` y se llega desde el propio catálogo.
|
||||||
|
*
|
||||||
|
* Va sobre `bg-paper`: el corte de claro justo después del héroe oscuro es la
|
||||||
|
* señal de que aquí empieza el catálogo. Todos los conteos salen de
|
||||||
|
* `productosDeCategoria`; ninguno está escrito a mano.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const TARJETAS = CATEGORIAS.slice()
|
||||||
|
.sort((a, b) => a.orden - b.orden)
|
||||||
|
.map((categoria) => ({
|
||||||
|
categoria,
|
||||||
|
cuantos: productosDeCategoria(categoria.slug).length,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export function CategoriasCatalogo() {
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
id="categorias"
|
||||||
|
className="scroll-mt-24 border-y border-rule-lite bg-paper text-deep"
|
||||||
|
aria-labelledby="categorias-titulo"
|
||||||
|
>
|
||||||
|
<div className="grid-bg-lite">
|
||||||
|
<div className="shell py-14 sm:py-20">
|
||||||
|
<Revelar className="max-w-[42rem]">
|
||||||
|
<p className="eyebrow">Catálogo</p>
|
||||||
|
<h2
|
||||||
|
id="categorias-titulo"
|
||||||
|
className="mt-4 font-display uppercase tracking-[-0.02em] text-deep"
|
||||||
|
style={{ fontSize: "clamp(1.6rem, 3.6vw, 2.5rem)" }}
|
||||||
|
>
|
||||||
|
Entra por el tipo de equipo
|
||||||
|
</h2>
|
||||||
|
<p className="mt-4 text-deep-muted">
|
||||||
|
{CATEGORIAS.length} categorías y {PRODUCTOS.length} equipos: extintores portátiles y
|
||||||
|
fijos, equipos móviles de mayor capacidad, rociadores automáticos, agentes
|
||||||
|
extinguidores, dosificación de espuma, alertamiento de protección civil y cañones
|
||||||
|
robotizados. Cada enlace abre el catálogo ya filtrado.
|
||||||
|
</p>
|
||||||
|
</Revelar>
|
||||||
|
|
||||||
|
<ul className="mt-9 grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
||||||
|
{TARJETAS.map(({ categoria, cuantos }, i) => (
|
||||||
|
<Revelar as="li" key={categoria.slug} delay={(i % 4) * 55} className="h-full">
|
||||||
|
<Link
|
||||||
|
href={hrefCategoria(categoria.slug)}
|
||||||
|
className="group flex h-full min-w-0 flex-col rounded-[4px] border border-rule-lite bg-white p-4 transition-colors duration-150 hover:border-signal"
|
||||||
|
style={{ WebkitTapHighlightColor: "transparent" }}
|
||||||
|
>
|
||||||
|
<span className="flex items-baseline justify-between gap-3">
|
||||||
|
<span className="font-display text-base font-bold uppercase leading-tight tracking-[-0.01em] text-deep">
|
||||||
|
{categoria.nombre}
|
||||||
|
</span>
|
||||||
|
<span className="shrink-0 font-mono text-[0.6875rem] tabular-nums text-signal">
|
||||||
|
{String(cuantos).padStart(2, "0")}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span className="mt-2.5 block text-xs leading-relaxed text-deep-muted">
|
||||||
|
{categoria.tagline}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span className="mt-auto flex items-center gap-1.5 pt-4 font-mono text-[0.625rem] uppercase tracking-[0.14em] text-deep-muted transition-colors group-hover:text-signal">
|
||||||
|
{cuantos === 1 ? "Ver el equipo" : `Ver los ${cuantos} equipos`}
|
||||||
|
<svg
|
||||||
|
width="11"
|
||||||
|
height="8"
|
||||||
|
viewBox="0 0 11 8"
|
||||||
|
aria-hidden="true"
|
||||||
|
className="transition-transform duration-200 group-hover:translate-x-1"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M0 4h9M6 1l3 3-3 3"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.3"
|
||||||
|
fill="none"
|
||||||
|
strokeLinecap="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
</Revelar>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<Revelar className="mt-8 border-t border-rule-lite pt-6">
|
||||||
|
<Link href="/catalogo" className="btn btn-deep w-full sm:w-auto">
|
||||||
|
Ver el catálogo completo
|
||||||
|
<svg width="14" height="10" viewBox="0 0 14 10" aria-hidden="true">
|
||||||
|
<path
|
||||||
|
d="M1 5h11M8.5 1.5L12 5l-3.5 3.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
fill="none"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</Link>
|
||||||
|
</Revelar>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,17 +1,39 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { RielClases } from "@/components/marca/RielClases";
|
import { RielClases } from "@/components/marca/RielClases";
|
||||||
import { Revelar } from "@/components/site/Revelar";
|
import { Revelar } from "@/components/site/Revelar";
|
||||||
import { CLASES_ORDENADAS } from "@/data/clases-fuego";
|
import { hrefClases } from "@/components/site/home/enlaces";
|
||||||
|
import { PRODUCTOS } from "@/data/catalogo";
|
||||||
|
import { CLASES, CLASES_ORDENADAS } from "@/data/clases-fuego";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CLASIFICACIÓN POR CLASE DE FUEGO — ruta B del inicio.
|
* CLASIFICACIÓN POR CLASE DE FUEGO — el criterio técnico del sitio.
|
||||||
*
|
*
|
||||||
* Va sobre `bg-paper`: es evidencia técnica, pertenece al mundo del documento.
|
* Va sobre `bg-paper`: es evidencia técnica, pertenece al mundo del documento.
|
||||||
* El campo que carga el argumento es `riesgoAgenteIncorrecto`: decir qué pasa
|
* El campo que carga el argumento es `riesgoAgenteIncorrecto`: decir qué pasa
|
||||||
* cuando el agente está mal elegido es exactamente lo que ningún competidor
|
* cuando el agente está mal elegido es exactamente lo que ningún competidor del
|
||||||
* del mapa está diciendo, y es lo que convierte el catálogo en criterio.
|
* mapa está diciendo, y es lo que convierte el catálogo en criterio. Ese
|
||||||
|
* argumento se conserva completo; es el más fuerte del sitio.
|
||||||
|
*
|
||||||
|
* ---- Qué cambió ----
|
||||||
|
*
|
||||||
|
* La sección explicaba cinco clases y ofrecía un solo botón genérico al final.
|
||||||
|
* Ahora cada clase es una salida al catálogo filtrado por ella
|
||||||
|
* —`/catalogo?clases=A`— con el conteo real de equipos que la cubren, así que el
|
||||||
|
* argumento técnico y la acción de compra están en la misma línea. Los cinco
|
||||||
|
* enlaces por clase van en peso de enlace, no de botón: el único botón de la
|
||||||
|
* sección es el del cierre.
|
||||||
|
*
|
||||||
|
* El encabezado lleva las palabras que se buscan —"clases de fuego"— y la
|
||||||
|
* consecuencia en la misma línea. `dondeAparece` es la mitad práctica: nombra el
|
||||||
|
* lugar reconocible para que la tabla se pueda leer sin vocabulario previo.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/** Cuántos equipos del catálogo cubren cada clase. Derivado, nunca a mano. */
|
||||||
|
const FILAS = CLASES_ORDENADAS.map((c) => ({
|
||||||
|
info: c,
|
||||||
|
cuantos: PRODUCTOS.filter((p) => p.clases.includes(c.clase)).length,
|
||||||
|
}));
|
||||||
|
|
||||||
export function ClasesFuego() {
|
export function ClasesFuego() {
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
@@ -22,22 +44,24 @@ export function ClasesFuego() {
|
|||||||
<div className="grid-bg-lite">
|
<div className="grid-bg-lite">
|
||||||
<div className="shell py-16 sm:py-20">
|
<div className="shell py-16 sm:py-20">
|
||||||
<Revelar className="max-w-[44rem]">
|
<Revelar className="max-w-[44rem]">
|
||||||
<p className="eyebrow">Ruta B · Por tipo de riesgo</p>
|
<p className="eyebrow">Clases de fuego {CLASES.join(" · ")}</p>
|
||||||
<h2
|
<h2
|
||||||
id="clases-titulo"
|
id="clases-titulo"
|
||||||
className="mt-4 font-display uppercase tracking-[-0.02em] text-deep"
|
className="mt-4 font-display uppercase tracking-[-0.02em] text-deep"
|
||||||
style={{ fontSize: "clamp(1.6rem, 3.6vw, 2.5rem)" }}
|
style={{ fontSize: "clamp(1.6rem, 3.6vw, 2.5rem)" }}
|
||||||
>
|
>
|
||||||
El agente equivocado empeora el incendio
|
Clases de fuego: qué pasa si el agente está mal elegido
|
||||||
</h2>
|
</h2>
|
||||||
<p className="mt-4 text-deep-muted">
|
<p className="mt-4 text-deep-muted">
|
||||||
La clase de fuego no es una etiqueta administrativa: define qué agente puede tocar ese
|
El extintor equivocado puede empeorar el incendio en lugar de apagarlo. La clase de
|
||||||
combustible. Esto es lo que cambia según lo que se esté quemando en tu operación.
|
fuego no es una etiqueta administrativa: define qué agente puede tocar ese combustible
|
||||||
|
y qué reacción provoca si no le corresponde. Cada clase abre el catálogo con los
|
||||||
|
equipos que la cubren.
|
||||||
</p>
|
</p>
|
||||||
</Revelar>
|
</Revelar>
|
||||||
|
|
||||||
<ul className="mt-10 divide-y divide-rule-lite border-y border-rule-lite">
|
<ul className="mt-10 divide-y divide-rule-lite border-y border-rule-lite">
|
||||||
{CLASES_ORDENADAS.map((c, i) => (
|
{FILAS.map(({ info: c, cuantos }, i) => (
|
||||||
<Revelar
|
<Revelar
|
||||||
as="li"
|
as="li"
|
||||||
key={c.clase}
|
key={c.clase}
|
||||||
@@ -62,6 +86,34 @@ export function ClasesFuego() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<RielClases activas={[c.clase]} tamano="sm" claro className="mt-4" />
|
<RielClases activas={[c.clase]} tamano="sm" claro className="mt-4" />
|
||||||
|
|
||||||
|
{/* La salida al catálogo, en la misma fila que el argumento. */}
|
||||||
|
<p className="mt-4">
|
||||||
|
<Link
|
||||||
|
href={hrefClases([c.clase])}
|
||||||
|
className="group inline-flex items-center gap-1.5 font-mono text-[0.6875rem] uppercase tracking-[0.1em] text-signal underline decoration-1 underline-offset-4 transition-colors hover:text-deep"
|
||||||
|
style={{ WebkitTapHighlightColor: "transparent" }}
|
||||||
|
>
|
||||||
|
{cuantos === 1
|
||||||
|
? `Ver 1 equipo clase ${c.clase}`
|
||||||
|
: `Ver los ${cuantos} equipos clase ${c.clase}`}
|
||||||
|
<svg
|
||||||
|
width="11"
|
||||||
|
height="8"
|
||||||
|
viewBox="0 0 11 8"
|
||||||
|
aria-hidden="true"
|
||||||
|
className="transition-transform duration-200 group-hover:translate-x-1"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M0 4h9M6 1l3 3-3 3"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.3"
|
||||||
|
fill="none"
|
||||||
|
strokeLinecap="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="lg:col-span-4">
|
<div className="lg:col-span-4">
|
||||||
@@ -88,10 +140,7 @@ export function ClasesFuego() {
|
|||||||
<div className="lg:col-span-5">
|
<div className="lg:col-span-5">
|
||||||
<div className="panel-lite h-full p-4">
|
<div className="panel-lite h-full p-4">
|
||||||
<p className="flex items-center gap-2 font-mono text-[0.625rem] uppercase tracking-[0.14em] text-deep-muted">
|
<p className="flex items-center gap-2 font-mono text-[0.625rem] uppercase tracking-[0.14em] text-deep-muted">
|
||||||
<span
|
<span aria-hidden="true" className="hazard block h-2.5 w-6 rounded-[1px]" />
|
||||||
aria-hidden="true"
|
|
||||||
className="hazard block h-2.5 w-6 rounded-[1px]"
|
|
||||||
/>
|
|
||||||
Si se usa el agente incorrecto
|
Si se usa el agente incorrecto
|
||||||
</p>
|
</p>
|
||||||
<p className="mt-2.5 text-sm leading-relaxed text-deep">
|
<p className="mt-2.5 text-sm leading-relaxed text-deep">
|
||||||
@@ -103,13 +152,26 @@ export function ClasesFuego() {
|
|||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<Revelar className="mt-9 flex flex-wrap items-center gap-x-6 gap-y-3">
|
<Revelar className="mt-9 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<Link href="/catalogo" className="btn btn-deep">
|
<Link
|
||||||
Ver equipo por clase de fuego
|
href="/catalogo"
|
||||||
|
className="btn btn-deep w-full shrink-0 sm:w-auto"
|
||||||
|
style={{ WebkitTapHighlightColor: "transparent" }}
|
||||||
|
>
|
||||||
|
Ver los {PRODUCTOS.length} equipos del catálogo
|
||||||
|
<svg width="14" height="10" viewBox="0 0 14 10" aria-hidden="true">
|
||||||
|
<path
|
||||||
|
d="M1 5h11M8.5 1.5L12 5l-3.5 3.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
fill="none"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
</Link>
|
</Link>
|
||||||
<p className="max-w-[30rem] text-xs leading-relaxed text-deep-muted">
|
<p className="max-w-[32rem] text-xs leading-relaxed text-deep-muted">
|
||||||
Referencia técnica general. La selección definitiva del agente se hace sobre el
|
Referencia técnica general, no dictamen. La selección definitiva del agente se hace
|
||||||
escenario real, con ficha técnica y hoja de seguridad del producto.
|
sobre el escenario real, con ficha técnica y hoja de seguridad del producto a la
|
||||||
|
vista.
|
||||||
</p>
|
</p>
|
||||||
</Revelar>
|
</Revelar>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,42 +3,62 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useId, useMemo, useState } from "react";
|
import { useId, useMemo, useState } from "react";
|
||||||
import { RielClasesSelector } from "@/components/marca/RielClases";
|
import { RielClasesSelector } from "@/components/marca/RielClases";
|
||||||
|
import { RUTAS_INDUSTRIA } from "@/components/site/home/enlaces";
|
||||||
import { serializeClases, type ClaseFuego } from "@/data/clases-fuego";
|
import { serializeClases, type ClaseFuego } from "@/data/clases-fuego";
|
||||||
import { INDUSTRIAS } from "@/data/industrias";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* EL CLASIFICADOR DE RIESGO — el elemento firma en acción.
|
* EL CLASIFICADOR DE RIESGO — el elemento firma, ahora dentro del embudo.
|
||||||
*
|
*
|
||||||
* El hallazgo central del estudio de marca es que el visitante tenía que
|
* El hallazgo central del estudio de marca es que el visitante tenía que
|
||||||
* adivinar qué solución le correspondía. Esto lo resuelve en dos pasos:
|
* adivinar qué solución le correspondía. Esto lo resuelve en dos pasos: elige
|
||||||
* elige su tipo de operación, enciende las clases de fuego que tiene, y el
|
* su tipo de operación, enciende las clases de fuego que tiene, y el botón lo
|
||||||
* botón lo deja en la ruta correcta con su riesgo ya declarado
|
* deja en el equipo que le toca.
|
||||||
* (`/soluciones/<slug>?clases=B,C`), que la página de destino y el formulario
|
*
|
||||||
* leen como preselección.
|
* ---- Qué cambió ----
|
||||||
|
*
|
||||||
|
* El destino era `/soluciones/<slug>?clases=B,C`: una página de lectura. Ahora
|
||||||
|
* es `/catalogo?escenario=<slug>&clases=B,C`, el catálogo ya filtrado. El
|
||||||
|
* clasificador dejó de ser un desvío hacia contenido consultivo y pasó a ser lo
|
||||||
|
* que su forma siempre prometió: un filtro.
|
||||||
|
*
|
||||||
|
* Las clases solo viajan cuando la persona las enciende. El filtro de clases
|
||||||
|
* del catálogo es conjuntivo —el equipo debe cubrir todas las marcadas— y
|
||||||
|
* mandar por omisión las tres o cuatro clases dominantes del segmento dejaría
|
||||||
|
* el catálogo casi vacío sin que nadie haya pedido ese recorte. Sin selección
|
||||||
|
* viaja solo el escenario, que es el filtro amplio y útil.
|
||||||
*
|
*
|
||||||
* Degradación sin JavaScript: el `<noscript>` oculta la interfaz interactiva y
|
* Degradación sin JavaScript: el `<noscript>` oculta la interfaz interactiva y
|
||||||
* publica las cinco rutas como enlaces directos, cada una con las clases
|
* publica las cinco rutas como enlaces directos al catálogo filtrado.
|
||||||
* dominantes de su segmento en la query string.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export function Clasificador() {
|
export function Clasificador() {
|
||||||
const [slug, setSlug] = useState<string>(INDUSTRIAS[0].slug);
|
const [slug, setSlug] = useState<string>(RUTAS_INDUSTRIA[0].industria.slug);
|
||||||
const [clases, setClases] = useState<ClaseFuego[]>([]);
|
const [clases, setClases] = useState<ClaseFuego[]>([]);
|
||||||
const idPaso1 = useId();
|
const idPaso1 = useId();
|
||||||
const idPaso2 = useId();
|
const idPaso2 = useId();
|
||||||
|
|
||||||
const industria = useMemo(
|
const ruta = useMemo(
|
||||||
() => INDUSTRIAS.find((i) => i.slug === slug) ?? INDUSTRIAS[0],
|
() => RUTAS_INDUSTRIA.find((r) => r.industria.slug === slug) ?? RUTAS_INDUSTRIA[0],
|
||||||
[slug],
|
[slug],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Sin clases encendidas viajan las dominantes del segmento: el destino
|
const operacion = ruta.industria.nombre.toLowerCase();
|
||||||
// nunca queda sin contexto de riesgo.
|
|
||||||
const clasesEfectivas = clases.length > 0 ? clases : industria.clases;
|
// El escenario acota por categorías de equipo; las clases, solo si la persona
|
||||||
const destino = `/soluciones/${industria.slug}?clases=${serializeClases(clasesEfectivas)}`;
|
// las declaró. Nunca se inventa un recorte que no pidió.
|
||||||
|
const destino = useMemo(() => {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (ruta.escenario) params.set("escenario", ruta.escenario.slug);
|
||||||
|
if (clases.length > 0) params.set("clases", serializeClases(clases));
|
||||||
|
const cadena = params.toString();
|
||||||
|
return cadena === "" ? "/catalogo" : `/catalogo?${cadena}`;
|
||||||
|
}, [ruta, clases]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="panel bg-ink-2/85 p-4 sm:p-5" style={{ WebkitBackdropFilter: "blur(6px)", backdropFilter: "blur(6px)" }}>
|
<div
|
||||||
|
className="panel bg-ink-2/85 p-4 sm:p-5"
|
||||||
|
style={{ WebkitBackdropFilter: "blur(6px)", backdropFilter: "blur(6px)" }}
|
||||||
|
>
|
||||||
<noscript>
|
<noscript>
|
||||||
<style>{".clasif-js{display:none!important}.clasif-nojs{display:block!important}"}</style>
|
<style>{".clasif-js{display:none!important}.clasif-nojs{display:block!important}"}</style>
|
||||||
</noscript>
|
</noscript>
|
||||||
@@ -49,7 +69,7 @@ export function Clasificador() {
|
|||||||
id={idPaso1}
|
id={idPaso1}
|
||||||
className="font-mono text-[0.625rem] uppercase tracking-[0.16em] text-faint"
|
className="font-mono text-[0.625rem] uppercase tracking-[0.16em] text-faint"
|
||||||
>
|
>
|
||||||
<span className="text-signal">01</span> Tu tipo de operación
|
<span className="text-signal">01</span> Qué tipo de operación tienes
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -57,15 +77,15 @@ export function Clasificador() {
|
|||||||
aria-labelledby={idPaso1}
|
aria-labelledby={idPaso1}
|
||||||
className="mt-2.5 flex flex-wrap gap-1.5"
|
className="mt-2.5 flex flex-wrap gap-1.5"
|
||||||
>
|
>
|
||||||
{INDUSTRIAS.map((ind) => {
|
{RUTAS_INDUSTRIA.map((r) => {
|
||||||
const activa = ind.slug === industria.slug;
|
const activa = r.industria.slug === ruta.industria.slug;
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={ind.slug}
|
key={r.industria.slug}
|
||||||
type="button"
|
type="button"
|
||||||
role="radio"
|
role="radio"
|
||||||
aria-checked={activa}
|
aria-checked={activa}
|
||||||
onClick={() => setSlug(ind.slug)}
|
onClick={() => setSlug(r.industria.slug)}
|
||||||
className={[
|
className={[
|
||||||
"rounded-[3px] border px-2.5 py-2 text-left font-mono text-[0.6875rem] uppercase tracking-[0.06em] transition-colors duration-150",
|
"rounded-[3px] border px-2.5 py-2 text-left font-mono text-[0.6875rem] uppercase tracking-[0.06em] transition-colors duration-150",
|
||||||
activa
|
activa
|
||||||
@@ -78,7 +98,7 @@ export function Clasificador() {
|
|||||||
WebkitTapHighlightColor: "transparent",
|
WebkitTapHighlightColor: "transparent",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{ind.nombre}
|
{r.industria.nombre}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -93,7 +113,8 @@ export function Clasificador() {
|
|||||||
id={idPaso2}
|
id={idPaso2}
|
||||||
className="font-mono text-[0.625rem] uppercase tracking-[0.16em] text-faint"
|
className="font-mono text-[0.625rem] uppercase tracking-[0.16em] text-faint"
|
||||||
>
|
>
|
||||||
<span className="text-signal">02</span> Clases de fuego que tienes
|
<span className="text-signal">02</span> Clases de fuego presentes
|
||||||
|
<span className="ml-1.5 normal-case tracking-normal text-faint">(opcional)</span>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* El riel se enciende una sola vez al montar: es el elemento firma
|
{/* El riel se enciende una sola vez al montar: es el elemento firma
|
||||||
@@ -103,36 +124,43 @@ export function Clasificador() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="max-w-[16rem] text-xs leading-relaxed text-faint">
|
<p className="max-w-[16rem] text-xs leading-relaxed text-faint" aria-live="polite">
|
||||||
{clases.length === 0
|
{clases.length === 0
|
||||||
? `Si no lo sabes, seguimos con las clases habituales del segmento: ${industria.clases.join(", ")}.`
|
? ruta.escenario
|
||||||
: `Riesgo declarado: ${clases.join(", ")}. Viaja contigo a la siguiente pantalla.`}
|
? `Sin marcar nada abre el catálogo con el equipo que se usa en ${ruta.escenario.nombre.toLowerCase()}.`
|
||||||
|
: "Sin marcar nada abre el catálogo completo."
|
||||||
|
: `Riesgo declarado: ${clases.join(", ")}. El catálogo abre mostrando solo el equipo que cubre esas clases.`}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Link href={destino} className="btn btn-signal mt-4 w-full sm:w-auto">
|
<Link href={destino} className="btn btn-ghost mt-4 w-full sm:w-auto">
|
||||||
Ver mi ruta
|
Ver equipo para {operacion}
|
||||||
<svg width="14" height="10" viewBox="0 0 14 10" aria-hidden="true">
|
<svg width="14" height="10" viewBox="0 0 14 10" aria-hidden="true">
|
||||||
<path d="M1 5h11M8.5 1.5L12 5l-3.5 3.5" stroke="currentColor" strokeWidth="1.5" fill="none" />
|
<path
|
||||||
|
d="M1 5h11M8.5 1.5L12 5l-3.5 3.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
fill="none"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Respaldo sin JavaScript: las cinco rutas como enlaces. */}
|
{/* Respaldo sin JavaScript: las cinco rutas como enlaces al catálogo. */}
|
||||||
<div className="clasif-nojs hidden">
|
<div className="clasif-nojs hidden">
|
||||||
<p className="font-mono text-[0.625rem] uppercase tracking-[0.16em] text-faint">
|
<p className="font-mono text-[0.625rem] uppercase tracking-[0.16em] text-faint">
|
||||||
Elige tu tipo de operación
|
Elige tu tipo de operación
|
||||||
</p>
|
</p>
|
||||||
<ul className="mt-2.5 divide-y divide-rule">
|
<ul className="mt-2.5 divide-y divide-rule">
|
||||||
{INDUSTRIAS.map((ind) => (
|
{RUTAS_INDUSTRIA.map((r) => (
|
||||||
<li key={ind.slug}>
|
<li key={r.industria.slug}>
|
||||||
<Link
|
<Link
|
||||||
href={`/soluciones/${ind.slug}?clases=${serializeClases(ind.clases)}`}
|
href={r.href}
|
||||||
className="flex items-baseline justify-between gap-3 py-2.5"
|
className="flex items-baseline justify-between gap-3 py-2.5"
|
||||||
>
|
>
|
||||||
<span className="text-sm font-semibold text-bright">{ind.nombre}</span>
|
<span className="text-sm font-semibold text-bright">{r.industria.nombre}</span>
|
||||||
<span className="font-mono text-[0.6875rem] text-signal">
|
<span className="font-mono text-[0.6875rem] text-signal">
|
||||||
{ind.clases.join("·")}
|
{r.industria.clases.join("·")}
|
||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -0,0 +1,128 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
import { IconoEscenario } from "@/components/marca/IconoEscenario";
|
||||||
|
import { Revelar } from "@/components/site/Revelar";
|
||||||
|
import { hrefEscenario, primeraOracion } from "@/components/site/home/enlaces";
|
||||||
|
import { CLASES } from "@/data/clases-fuego";
|
||||||
|
import { ESCENARIOS } from "@/data/escenarios";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ENTRADA POR ESCENARIO — el catálogo indexado por el lugar.
|
||||||
|
*
|
||||||
|
* El clasificador del héroe pide declarar clase de fuego. Eso lo sabe un jefe
|
||||||
|
* de seguridad industrial; no lo sabe el dueño de la gasolinera ni la
|
||||||
|
* administradora del hotel, y los dos compran. Estas trece tarjetas indexan el
|
||||||
|
* mismo catálogo por el lugar reconocible, con la iconografía que el sitio
|
||||||
|
* original de FSPM ya usaba para resolver esto.
|
||||||
|
*
|
||||||
|
* Comercialmente es la capa de cola larga: "extintor para gasolinera" y
|
||||||
|
* "protección contra incendios en cocina industrial" traen intención de compra
|
||||||
|
* más alta que "extintor clase B". Por eso el nombre y el riesgo de cada lugar
|
||||||
|
* siguen en texto: acortar la tarjeta no es vaciarla.
|
||||||
|
*
|
||||||
|
* ---- Qué cambió ----
|
||||||
|
*
|
||||||
|
* La sección dejó de presentarse como "una de las dos entradas al catálogo" con
|
||||||
|
* peso comparable al del héroe. Ya no compite: es un filtro más, más abajo y
|
||||||
|
* más compacto. Cada tarjeta va a `/catalogo?escenario=<slug>` —que ya era su
|
||||||
|
* destino— y el riesgo se recorta a su primera oración para que las trece
|
||||||
|
* quepan sin ocupar dos pantallas. La frase completa vive en el catálogo y en
|
||||||
|
* `src/data/escenarios.ts`; aquí se corta en el punto, nunca a media palabra.
|
||||||
|
*
|
||||||
|
* El botón de cotización del pie bajó a enlace de texto: en una sección de
|
||||||
|
* venta el llamado primario son las tarjetas.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const ORDENADOS = [...ESCENARIOS].sort((a, b) => a.orden - b.orden);
|
||||||
|
|
||||||
|
export function EntradaEscenario() {
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
id="por-escenario"
|
||||||
|
className="scroll-mt-24 border-y border-rule bg-ink-2"
|
||||||
|
aria-labelledby="escenario-titulo"
|
||||||
|
>
|
||||||
|
<div className="shell py-16 sm:py-20">
|
||||||
|
<Revelar className="max-w-[46rem]">
|
||||||
|
<p className="eyebrow">Entrada por escenario</p>
|
||||||
|
<h2
|
||||||
|
id="escenario-titulo"
|
||||||
|
className="mt-4 font-display uppercase tracking-[-0.02em] text-bright"
|
||||||
|
style={{ fontSize: "clamp(1.7rem, 3.8vw, 2.6rem)" }}
|
||||||
|
>
|
||||||
|
¿Qué necesitas proteger?
|
||||||
|
</h2>
|
||||||
|
<p className="mt-4 text-base leading-relaxed text-muted">
|
||||||
|
Elige el lugar que se parece al tuyo y el catálogo abre con el equipo que ahí se usa.
|
||||||
|
Cada escenario ya viene mapeado a sus clases de fuego y a las categorías de agente y
|
||||||
|
equipo que le corresponden, así que el filtro es el mismo con el que llegarías desde la
|
||||||
|
especificación técnica.
|
||||||
|
</p>
|
||||||
|
</Revelar>
|
||||||
|
|
||||||
|
<ul className="mt-9 grid gap-3 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||||
|
{ORDENADOS.map((escenario, i) => (
|
||||||
|
<Revelar as="li" key={escenario.slug} delay={(i % 4) * 55} className="h-full">
|
||||||
|
<Link
|
||||||
|
href={hrefEscenario(escenario.slug)}
|
||||||
|
className="group flex h-full min-w-0 flex-col gap-2.5 rounded-[4px] border border-rule bg-ink p-4 transition-colors duration-150 hover:border-signal"
|
||||||
|
style={{ WebkitTapHighlightColor: "transparent" }}
|
||||||
|
>
|
||||||
|
<span className="flex items-start gap-3">
|
||||||
|
<IconoEscenario escenario={escenario} tamano="md" />
|
||||||
|
<span className="min-w-0">
|
||||||
|
<span className="block font-display text-[0.9375rem] font-bold leading-tight text-bright">
|
||||||
|
{escenario.nombre}
|
||||||
|
</span>
|
||||||
|
<span className="mt-1 block font-mono text-[0.625rem] uppercase tracking-[0.12em] text-signal">
|
||||||
|
Clases {escenario.clases.join("·")}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span className="block text-xs leading-relaxed text-muted">
|
||||||
|
{escenario.enLlano}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span className="block border-l-2 border-ember/50 pl-3 text-xs leading-relaxed text-faint">
|
||||||
|
{primeraOracion(escenario.riesgo)}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span className="mt-auto flex items-center gap-1.5 pt-2 font-mono text-[0.625rem] uppercase tracking-[0.14em] text-muted transition-colors group-hover:text-signal-hi">
|
||||||
|
Ver el equipo
|
||||||
|
<svg
|
||||||
|
width="11"
|
||||||
|
height="8"
|
||||||
|
viewBox="0 0 11 8"
|
||||||
|
aria-hidden="true"
|
||||||
|
className="transition-transform duration-200 group-hover:translate-x-1"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M0 4h9M6 1l3 3-3 3"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.3"
|
||||||
|
fill="none"
|
||||||
|
strokeLinecap="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
</Revelar>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<Revelar className="mt-8 border-t border-rule pt-6">
|
||||||
|
<p className="max-w-[46rem] text-sm leading-relaxed text-muted">
|
||||||
|
<strong className="font-semibold text-bright">¿Ninguno se parece al tuyo?</strong> Un
|
||||||
|
mismo predio puede tener clase {CLASES.join(", ")} a la vez —bodega, taller, cuarto
|
||||||
|
eléctrico y comedor en la misma nave— y ahí la selección se hace sobre el sitio, no
|
||||||
|
sobre una lista:{" "}
|
||||||
|
<Link href="#diagnostico" className="link text-bright">
|
||||||
|
recibir cotización
|
||||||
|
</Link>
|
||||||
|
.
|
||||||
|
</p>
|
||||||
|
</Revelar>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ import { Clasificador } from "@/components/site/home/Clasificador";
|
|||||||
import { CATEGORIAS, PRODUCTOS } from "@/data/catalogo";
|
import { CATEGORIAS, PRODUCTOS } from "@/data/catalogo";
|
||||||
import { CLASES } from "@/data/clases-fuego";
|
import { CLASES } from "@/data/clases-fuego";
|
||||||
import { EMPRESA } from "@/data/empresa";
|
import { EMPRESA } from "@/data/empresa";
|
||||||
import { INDUSTRIAS } from "@/data/industrias";
|
import { ESCENARIOS } from "@/data/escenarios";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HÉROE — no es un banner con una cifra grande.
|
* HÉROE — no es un banner con una cifra grande.
|
||||||
@@ -13,16 +13,30 @@ import { INDUSTRIAS } from "@/data/industrias";
|
|||||||
* clasifica. La fotografía va tratada con un degradado direccional desde
|
* clasifica. La fotografía va tratada con un degradado direccional desde
|
||||||
* `--color-ink` para que el texto tenga contraste real, no un lavado gris.
|
* `--color-ink` para que el texto tenga contraste real, no un lavado gris.
|
||||||
*
|
*
|
||||||
* La tira de datos de abajo se deriva de `src/data/`: número de categorías,
|
* ---- Reordenamiento de embudo ----
|
||||||
* equipos en catálogo, clases cubiertas por la taxonomía y año de origen de la
|
*
|
||||||
* tecnología. Ninguna cifra está escrita a mano.
|
* Antes el héroe declaraba "dos entradas al catálogo" y ofrecía tres salidas
|
||||||
|
* del mismo peso: el clasificador, un botón hacia la sección de escenarios y un
|
||||||
|
* enlace al catálogo. Tres caminos con la misma jerarquía son ningún camino.
|
||||||
|
*
|
||||||
|
* Ahora hay **un solo botón en rojo de señal** y va al catálogo, con la cifra
|
||||||
|
* de equipos derivada de `PRODUCTOS.length`. El clasificador se queda —es el
|
||||||
|
* elemento firma— pero baja a la función que de verdad cumple: acotar el
|
||||||
|
* catálogo antes de entrar, con su botón en peso secundario porque lleva al
|
||||||
|
* mismo sitio que el primario, solo filtrado. "Recibir cotización" queda como
|
||||||
|
* enlace de texto: sigue existiendo para quien no quiere navegar catálogo, sin
|
||||||
|
* competir con él.
|
||||||
|
*
|
||||||
|
* La tira de datos se deriva de `src/data/`: categorías, equipos, clases
|
||||||
|
* cubiertas por la taxonomía y escenarios mapeados. Ninguna cifra está escrita
|
||||||
|
* a mano.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const DATOS = [
|
const DATOS = [
|
||||||
{ clave: "Categorías de solución", valor: String(CATEGORIAS.length).padStart(2, "0") },
|
{ clave: "Categorías de equipo", valor: String(CATEGORIAS.length).padStart(2, "0") },
|
||||||
{ clave: "Equipos en catálogo", valor: String(PRODUCTOS.length) },
|
{ clave: "Equipos en catálogo", valor: String(PRODUCTOS.length) },
|
||||||
{ clave: "Clases de fuego", valor: CLASES.join("·") },
|
{ clave: "Clases de fuego", valor: CLASES.join("·") },
|
||||||
{ clave: "Tecnología de origen", valor: String(EMPRESA.fundacionOrigen) },
|
{ clave: "Escenarios mapeados", valor: String(ESCENARIOS.length) },
|
||||||
];
|
];
|
||||||
|
|
||||||
const HEROE_ALTO = `
|
const HEROE_ALTO = `
|
||||||
@@ -79,7 +93,9 @@ export function Heroe() {
|
|||||||
|
|
||||||
<div className="heroe-alto shell relative flex flex-col justify-center py-14 sm:py-20 lg:py-24">
|
<div className="heroe-alto shell relative flex flex-col justify-center py-14 sm:py-20 lg:py-24">
|
||||||
<div className="max-w-[46rem]">
|
<div className="max-w-[46rem]">
|
||||||
<p className="eyebrow anim-rise">Clasificación de riesgo · Agente · Evidencia</p>
|
<p className="eyebrow anim-rise">
|
||||||
|
Extintores · Espumas · Sistemas fijos, móviles y automáticos
|
||||||
|
</p>
|
||||||
|
|
||||||
<h1
|
<h1
|
||||||
id="heroe-titulo"
|
id="heroe-titulo"
|
||||||
@@ -93,29 +109,49 @@ export function Heroe() {
|
|||||||
className="anim-rise mt-6 max-w-[38rem] text-base leading-relaxed text-muted sm:text-lg"
|
className="anim-rise mt-6 max-w-[38rem] text-base leading-relaxed text-muted sm:text-lg"
|
||||||
style={{ animationDelay: "160ms" }}
|
style={{ animationDelay: "160ms" }}
|
||||||
>
|
>
|
||||||
No vendemos catálogo. Clasificamos el riesgo de tu operación, seleccionamos el agente
|
Extintores portátiles y fijos, equipos móviles, rociadores automáticos, agentes
|
||||||
y el equipo que corresponden a ese escenario, y entregamos la documentación que te van
|
espumógenos y dosificadores, para gasolinera, bodega, cocina industrial, planta, mina u
|
||||||
a pedir en auditoría, ante tu aseguradora y en Protección Civil.
|
oficina. Clasificamos el riesgo de incendio del centro de trabajo, elegimos el agente
|
||||||
|
por clase de fuego y entregamos el expediente que se revisa en auditoría, ante tu
|
||||||
|
aseguradora y en Protección Civil.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* ---- el único llamado primario del héroe: el catálogo ---- */}
|
||||||
<div
|
<div
|
||||||
className="anim-rise mt-9 max-w-[46rem]"
|
className="anim-rise mt-8 flex flex-col items-start gap-x-6 gap-y-4 sm:flex-row sm:items-center"
|
||||||
style={{ animationDelay: "240ms" }}
|
style={{ animationDelay: "220ms" }}
|
||||||
>
|
>
|
||||||
<Clasificador />
|
<Link href="/catalogo" className="btn btn-signal w-full sm:w-auto">
|
||||||
|
Ver los {PRODUCTOS.length} equipos
|
||||||
|
<svg width="14" height="10" viewBox="0 0 14 10" aria-hidden="true">
|
||||||
|
<path
|
||||||
|
d="M1 5h11M8.5 1.5L12 5l-3.5 3.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
fill="none"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</Link>
|
||||||
|
<p className="text-sm leading-relaxed text-muted">
|
||||||
|
O si prefieres que lo resolvamos nosotros,{" "}
|
||||||
|
<Link href="#diagnostico" className="link text-bright">
|
||||||
|
recibir cotización
|
||||||
|
</Link>
|
||||||
|
.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* ---- atajo: acotar el catálogo antes de entrar ---- */}
|
||||||
<div
|
<div
|
||||||
className="anim-rise mt-8 flex flex-wrap items-center gap-x-6 gap-y-3"
|
id="clasificador"
|
||||||
style={{ animationDelay: "320ms" }}
|
className="anim-rise mt-9 max-w-[46rem] scroll-mt-28"
|
||||||
|
style={{ animationDelay: "280ms" }}
|
||||||
>
|
>
|
||||||
<Link href="/cumplimiento-nom-002" className="btn btn-ghost">
|
<p className="mb-3 font-mono text-[0.625rem] uppercase tracking-[0.16em] text-faint">
|
||||||
Validar cumplimiento NOM-002
|
Atajo · Entra al catálogo ya filtrado
|
||||||
</Link>
|
</p>
|
||||||
<Link href="/soluciones" className="link text-sm text-muted">
|
<Clasificador />
|
||||||
Ver las {INDUSTRIAS.length} rutas por industria
|
|
||||||
</Link>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Tira de datos: todo derivado de src/data, cero cifras inventadas. */}
|
{/* Tira de datos: todo derivado de src/data, cero cifras inventadas. */}
|
||||||
|
|||||||
@@ -1,131 +1,118 @@
|
|||||||
import Image from "next/image";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { RielClases } from "@/components/marca/RielClases";
|
import { RielClases } from "@/components/marca/RielClases";
|
||||||
import { Revelar } from "@/components/site/Revelar";
|
import { Revelar } from "@/components/site/Revelar";
|
||||||
import { serializeClases } from "@/data/clases-fuego";
|
import { primeraOracion, RUTAS_INDUSTRIA } from "@/components/site/home/enlaces";
|
||||||
import { INDUSTRIAS } from "@/data/industrias";
|
import { INDUSTRIAS } from "@/data/industrias";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* RUTAS POR INDUSTRIA — los cinco segmentos prioritarios del estudio.
|
* RUTAS POR INDUSTRIA — los cinco segmentos prioritarios del estudio, ahora
|
||||||
|
* como puerta al catálogo y no como destino propio.
|
||||||
*
|
*
|
||||||
* El número que aparece en cada tarjeta NO es decorativo ni un contador de
|
* ---- La tensión que resuelve esta versión ----
|
||||||
* lista: es `industria.prioridad`, el orden de prioridad comercial que
|
|
||||||
* estableció la auditoría (sección 7). Codifica información real, por eso se
|
|
||||||
* muestra.
|
|
||||||
*
|
*
|
||||||
* El texto que se lee grande es `dolor`, escrito en la voz del comprador. Es
|
* La auditoría de marca pidió rutas por industria para que el sitio no fuera
|
||||||
* deliberado: el visitante debe reconocer su problema antes de leer una
|
* "un catálogo grande y ya". El cliente quiere el catálogo al centro. Las dos
|
||||||
* categoría de producto.
|
* cosas son compatibles si las rutas **desembocan** en el catálogo en lugar de
|
||||||
|
* desviar de él: aquí siguen cualificando —dolor en la voz del comprador,
|
||||||
|
* contexto, clases dominantes— pero cada tarjeta abre `/catalogo` filtrado por
|
||||||
|
* el escenario que representa al segmento, no `/soluciones/<slug>`.
|
||||||
|
*
|
||||||
|
* Y baja de jerarquía, que es lo que el encargo pide: fuera las fotografías,
|
||||||
|
* fuera la tarjeta doble del segmento de prioridad 1, fuera la retícula que
|
||||||
|
* ocupaba pantalla completa. Quedó una lista compacta de cinco filas. El orden
|
||||||
|
* sigue `industria.prioridad` —la prioridad comercial que fijó la auditoría—
|
||||||
|
* pero no se rotula: decirle a un hotel que es "Prioridad 04" es información
|
||||||
|
* interna que en la página se lee como desprecio.
|
||||||
|
*
|
||||||
|
* Un solo enlace consultivo en toda la sección, al pie y en peso de texto:
|
||||||
|
* `/soluciones`, para quien quiera leer antes de comprar. Ni uno por tarjeta.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export function RutasIndustria() {
|
export function RutasIndustria() {
|
||||||
return (
|
return (
|
||||||
<section id="rutas-industria" className="scroll-mt-24 bg-ink" aria-labelledby="industrias-titulo">
|
<section
|
||||||
<div className="shell py-16 sm:py-20">
|
id="rutas-industria"
|
||||||
<Revelar className="flex flex-col gap-5 sm:flex-row sm:items-end sm:justify-between">
|
className="scroll-mt-24 bg-ink"
|
||||||
<div>
|
aria-labelledby="industrias-titulo"
|
||||||
<p className="eyebrow">Ruta A · Por industria</p>
|
>
|
||||||
<h2
|
<div className="shell py-14 sm:py-16">
|
||||||
id="industrias-titulo"
|
<Revelar className="max-w-[44rem]">
|
||||||
className="mt-4 max-w-[30rem] font-display uppercase tracking-[-0.02em] text-bright"
|
<p className="eyebrow">Por industria</p>
|
||||||
style={{ fontSize: "clamp(1.6rem, 3.6vw, 2.5rem)" }}
|
<h2
|
||||||
>
|
id="industrias-titulo"
|
||||||
Cinco operaciones, cinco escenarios distintos
|
className="mt-4 font-display uppercase tracking-[-0.02em] text-bright"
|
||||||
</h2>
|
style={{ fontSize: "clamp(1.4rem, 3vw, 2rem)" }}
|
||||||
</div>
|
>
|
||||||
<Link href="/soluciones" className="btn btn-ghost btn-sm shrink-0">
|
Protección contra incendios por industria
|
||||||
Comparar las cinco
|
</h2>
|
||||||
</Link>
|
<p className="mt-3.5 text-sm leading-relaxed text-muted">
|
||||||
|
Cada industria quema cosas distintas, responde con personas distintas y le rinde cuentas
|
||||||
|
a autoridades distintas. Estos son los {INDUSTRIAS.length} segmentos por los que entra
|
||||||
|
la mayor parte de nuestros proyectos, con sus clases dominantes; cada uno abre el
|
||||||
|
catálogo con el equipo que ahí aplica.
|
||||||
|
</p>
|
||||||
</Revelar>
|
</Revelar>
|
||||||
|
|
||||||
<ul className="mt-10 grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
<ul className="mt-8 divide-y divide-rule border-y border-rule">
|
||||||
{INDUSTRIAS.map((ind, i) => (
|
{RUTAS_INDUSTRIA.map(({ industria, href }, i) => (
|
||||||
<Revelar
|
<Revelar as="li" key={industria.slug} delay={Math.min(i, 3) * 60}>
|
||||||
as="li"
|
|
||||||
key={ind.slug}
|
|
||||||
delay={(i % 3) * 80}
|
|
||||||
className={ind.prioridad === 1 ? "lg:col-span-2" : undefined}
|
|
||||||
>
|
|
||||||
<Link
|
<Link
|
||||||
href={`/soluciones/${ind.slug}?clases=${serializeClases(ind.clases)}`}
|
href={href}
|
||||||
className="panel group relative flex h-full flex-col overflow-hidden transition-colors duration-200 hover:border-rule-2"
|
className="group grid gap-3 py-5 transition-colors duration-150 lg:grid-cols-12 lg:items-center lg:gap-6"
|
||||||
style={{ WebkitTapHighlightColor: "transparent" }}
|
style={{ WebkitTapHighlightColor: "transparent" }}
|
||||||
>
|
>
|
||||||
<div className="relative aspect-[16/9] w-full overflow-hidden bg-ink-3">
|
<div className="lg:col-span-3">
|
||||||
<Image
|
<span className="block font-mono text-[0.625rem] uppercase tracking-[0.16em] text-faint">
|
||||||
src={ind.imagen}
|
{industria.nombre}
|
||||||
alt=""
|
</span>
|
||||||
aria-hidden="true"
|
<span className="mt-1.5 block font-display text-[0.9375rem] leading-tight tracking-[-0.01em] text-bright transition-colors group-hover:text-signal-hi">
|
||||||
fill
|
{industria.dolor}
|
||||||
sizes={
|
|
||||||
ind.prioridad === 1
|
|
||||||
? "(min-width: 1024px) 42rem, (min-width: 640px) 50vw, 100vw"
|
|
||||||
: "(min-width: 1024px) 21rem, (min-width: 640px) 50vw, 100vw"
|
|
||||||
}
|
|
||||||
className="object-cover opacity-45 transition-all duration-500 group-hover:opacity-65 group-hover:scale-[1.03]"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="absolute inset-0"
|
|
||||||
aria-hidden="true"
|
|
||||||
style={{
|
|
||||||
background:
|
|
||||||
"linear-gradient(to top, var(--color-ink-2) 8%, rgba(20,27,34,0.55) 55%, rgba(20,27,34,0.15) 100%)",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<span className="absolute left-4 top-4 flex items-center gap-2.5">
|
|
||||||
<Image
|
|
||||||
src={ind.icono}
|
|
||||||
alt=""
|
|
||||||
aria-hidden="true"
|
|
||||||
width={26}
|
|
||||||
height={26}
|
|
||||||
className="h-6 w-6 object-contain"
|
|
||||||
/>
|
|
||||||
{/* prioridad del estudio de mercado, no índice de lista */}
|
|
||||||
<span
|
|
||||||
className="font-mono text-[0.625rem] tracking-[0.16em] text-signal"
|
|
||||||
title="Prioridad comercial según el estudio de mercado"
|
|
||||||
>
|
|
||||||
P{String(ind.prioridad).padStart(2, "0")}
|
|
||||||
</span>
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-1 flex-col p-5">
|
<p className="text-sm leading-relaxed text-muted lg:col-span-6">
|
||||||
<p className="font-mono text-[0.625rem] uppercase tracking-[0.16em] text-faint">
|
{primeraOracion(industria.contexto)}
|
||||||
{ind.nombre}
|
</p>
|
||||||
</p>
|
|
||||||
|
|
||||||
<h3 className="mt-2.5 font-display text-lg leading-tight tracking-[-0.01em] text-bright">
|
{/* flex-wrap, no nowrap: a 320 px el riel más "Ver equipo"
|
||||||
{ind.dolor}
|
quedan al límite del ancho útil y tienen que poder bajar de
|
||||||
</h3>
|
línea en lugar de desbordar el cuerpo. */}
|
||||||
|
<div className="flex flex-wrap items-center justify-between gap-x-3 gap-y-2 lg:col-span-3 lg:justify-end">
|
||||||
<p className="mt-3 flex-1 text-sm leading-relaxed text-muted">
|
<RielClases activas={industria.clases} tamano="sm" etiqueta="Clases" />
|
||||||
{ind.contexto.split(". ")[0]}.
|
<span className="flex shrink-0 items-center gap-1.5 font-mono text-[0.625rem] uppercase tracking-[0.12em] text-signal transition-colors group-hover:text-signal-hi">
|
||||||
</p>
|
Ver equipo
|
||||||
|
|
||||||
<div className="mt-5 flex items-center justify-between gap-3">
|
|
||||||
<RielClases activas={ind.clases} tamano="sm" etiqueta="Clases" />
|
|
||||||
<svg
|
<svg
|
||||||
width="13"
|
width="11"
|
||||||
height="9"
|
height="8"
|
||||||
viewBox="0 0 13 9"
|
viewBox="0 0 11 8"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
className="shrink-0 text-muted transition-transform duration-200 group-hover:translate-x-1"
|
className="transition-transform duration-200 group-hover:translate-x-1"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M0 4.5h10.5M7.5 1.5l3 3-3 3"
|
d="M0 4h9M6 1l3 3-3 3"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
strokeWidth="1.4"
|
strokeWidth="1.3"
|
||||||
fill="none"
|
fill="none"
|
||||||
|
strokeLinecap="round"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
</Revelar>
|
</Revelar>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
<Revelar className="mt-5">
|
||||||
|
<p className="text-sm leading-relaxed text-faint">
|
||||||
|
Si prefieres leer el criterio antes de ver equipo —paquete recomendado, preguntas del
|
||||||
|
diagnóstico y obligaciones aplicables por segmento—,{" "}
|
||||||
|
<Link href="/soluciones" className="link text-muted">
|
||||||
|
compara las {INDUSTRIAS.length} rutas
|
||||||
|
</Link>
|
||||||
|
.
|
||||||
|
</p>
|
||||||
|
</Revelar>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,108 +0,0 @@
|
|||||||
import Link from "next/link";
|
|
||||||
import { Revelar } from "@/components/site/Revelar";
|
|
||||||
import { CATEGORIAS } from "@/data/catalogo";
|
|
||||||
import { CLASES } from "@/data/clases-fuego";
|
|
||||||
import { INDUSTRIAS } from "@/data/industrias";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* LAS TRES RUTAS — implementación literal de la recomendación 10.1 del estudio
|
|
||||||
* de marca: rehacer el inicio con tres entradas, por industria, por tipo de
|
|
||||||
* riesgo y por cumplimiento. Son tres formas distintas de llegar al mismo
|
|
||||||
* catálogo, según cómo el comprador tenga formulado su problema.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const RUTAS = [
|
|
||||||
{
|
|
||||||
numero: "A",
|
|
||||||
titulo: "Por industria",
|
|
||||||
resumen:
|
|
||||||
"Sabes qué operas y qué te puede pasar. Entra por tu segmento y te mostramos el escenario, el paquete y la evidencia que aplica.",
|
|
||||||
dato: `${INDUSTRIAS.length} rutas`,
|
|
||||||
href: "/soluciones",
|
|
||||||
cta: "Elegir mi industria",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
numero: "B",
|
|
||||||
titulo: "Por tipo de riesgo",
|
|
||||||
resumen:
|
|
||||||
"Sabes qué se quema pero no qué agente corresponde. Entra por clase de fuego y verás qué pasa si se usa el agente equivocado.",
|
|
||||||
dato: `Clases ${CLASES.join("·")}`,
|
|
||||||
href: "/catalogo",
|
|
||||||
cta: "Ver el catálogo por clase",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
numero: "C",
|
|
||||||
titulo: "Por cumplimiento",
|
|
||||||
resumen:
|
|
||||||
"Tienes un hallazgo abierto, una verificación encima o una renovación de póliza. Entra por la norma y qué te toca documentar.",
|
|
||||||
dato: "NOM-002-STPS-2010",
|
|
||||||
href: "/cumplimiento-nom-002",
|
|
||||||
cta: "Revisar mi cumplimiento",
|
|
||||||
},
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
export function TresRutas() {
|
|
||||||
return (
|
|
||||||
<section className="border-y border-rule bg-ink-2" aria-labelledby="tres-rutas-titulo">
|
|
||||||
<div className="shell py-16 sm:py-20">
|
|
||||||
<Revelar>
|
|
||||||
<p className="eyebrow">Tres formas de entrar</p>
|
|
||||||
<h2
|
|
||||||
id="tres-rutas-titulo"
|
|
||||||
className="mt-4 max-w-[34rem] font-display uppercase tracking-[-0.02em] text-bright"
|
|
||||||
style={{ fontSize: "clamp(1.6rem, 3.6vw, 2.5rem)" }}
|
|
||||||
>
|
|
||||||
Dinos cómo tienes planteado el problema
|
|
||||||
</h2>
|
|
||||||
<p className="mt-4 max-w-[42rem] text-muted">
|
|
||||||
Nuestro catálogo tiene {CATEGORIAS.length} categorías. Ninguna de ellas es la respuesta
|
|
||||||
hasta que sabemos qué operación proteges. Empieza por donde tengas más claridad.
|
|
||||||
</p>
|
|
||||||
</Revelar>
|
|
||||||
|
|
||||||
<ul className="mt-10 grid gap-4 md:grid-cols-3">
|
|
||||||
{RUTAS.map((ruta, i) => (
|
|
||||||
<Revelar as="li" key={ruta.numero} delay={i * 90}>
|
|
||||||
<Link
|
|
||||||
href={ruta.href}
|
|
||||||
className="panel group flex h-full flex-col p-5 transition-colors duration-200 hover:border-rule-2 hover:bg-ink-3 sm:p-6"
|
|
||||||
style={{ WebkitTapHighlightColor: "transparent" }}
|
|
||||||
>
|
|
||||||
<span className="flex items-center justify-between">
|
|
||||||
<span className="font-mono text-xs tracking-[0.16em] text-signal">
|
|
||||||
{ruta.numero}
|
|
||||||
</span>
|
|
||||||
<span className="badge">{ruta.dato}</span>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<h3 className="mt-5 font-display text-xl uppercase tracking-[-0.01em] text-bright">
|
|
||||||
{ruta.titulo}
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<p className="mt-2.5 flex-1 text-sm leading-relaxed text-muted">{ruta.resumen}</p>
|
|
||||||
|
|
||||||
<span className="mt-5 flex items-center gap-2 font-mono text-[0.6875rem] uppercase tracking-[0.1em] text-bright">
|
|
||||||
{ruta.cta}
|
|
||||||
<svg
|
|
||||||
width="13"
|
|
||||||
height="9"
|
|
||||||
viewBox="0 0 13 9"
|
|
||||||
aria-hidden="true"
|
|
||||||
className="transition-transform duration-200 group-hover:translate-x-1"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M0 4.5h10.5M7.5 1.5l3 3-3 3"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="1.4"
|
|
||||||
fill="none"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
</Link>
|
|
||||||
</Revelar>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import { CLASES, serializeClases, type ClaseFuego } from "@/data/clases-fuego";
|
||||||
|
import { ESCENARIOS, type Escenario } from "@/data/escenarios";
|
||||||
|
import { INDUSTRIAS, type Industria } from "@/data/industrias";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DESTINOS DEL INICIO — un solo lugar donde se decide a dónde va cada tarjeta.
|
||||||
|
*
|
||||||
|
* El inicio dejó de tener destinos propios: escenarios, industrias y clases de
|
||||||
|
* fuego ahora **entran al catálogo ya filtrado**. Como el explorador de
|
||||||
|
* `/catalogo` solo entiende `?escenario=`, `?clases=`, `?categoria=` y `?q=`,
|
||||||
|
* la traducción de cada eje de navegación a esa cadena de consulta vive aquí y
|
||||||
|
* no repartida por los componentes.
|
||||||
|
*
|
||||||
|
* Por qué una industria viaja como `?escenario=` y no como `?clases=`: el
|
||||||
|
* filtro de clases del catálogo es conjuntivo —el equipo tiene que cubrir
|
||||||
|
* TODAS las clases marcadas— y cuatro de los cinco segmentos declaran tres o
|
||||||
|
* cuatro clases dominantes. `?clases=A,B,C,D` deja el catálogo casi vacío, que
|
||||||
|
* es justo lo contrario de lo que esta página tiene que lograr. El escenario
|
||||||
|
* filtra por las categorías de equipo que ahí se usan, que es el conjunto útil.
|
||||||
|
*
|
||||||
|
* Ninguna cifra ni ningún par industria–escenario está escrito a mano: todo
|
||||||
|
* sale de `escenario.industria` en `src/data/escenarios.ts`.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** El escenario más representativo de una industria: el de menor `orden`. */
|
||||||
|
export function escenarioDeIndustria(slug: string): Escenario | undefined {
|
||||||
|
return ESCENARIOS.filter((e) => e.industria === slug).sort((a, b) => a.orden - b.orden)[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** `/catalogo` filtrado por el lugar que se opera. */
|
||||||
|
export function hrefEscenario(slug: string): string {
|
||||||
|
return `/catalogo?escenario=${slug}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `/catalogo` filtrado por clase de fuego. Acepta una o varias, y la taxonomía
|
||||||
|
* válida sale de `CLASES`: nada de listas de clases escritas a mano.
|
||||||
|
*/
|
||||||
|
export function hrefClases(clases: readonly string[]): string {
|
||||||
|
const validas = clases.filter((c): c is ClaseFuego =>
|
||||||
|
(CLASES as readonly string[]).includes(c),
|
||||||
|
);
|
||||||
|
const serie = serializeClases(validas);
|
||||||
|
return serie === "" ? "/catalogo" : `/catalogo?clases=${serie}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** `/catalogo` filtrado por categoría de equipo. */
|
||||||
|
export function hrefCategoria(slug: string): string {
|
||||||
|
return `/catalogo?categoria=${slug}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RutaIndustria = {
|
||||||
|
industria: Industria;
|
||||||
|
/** El escenario que representa al segmento dentro del catálogo. */
|
||||||
|
escenario: Escenario | undefined;
|
||||||
|
/** Destino final: catálogo filtrado, nunca una página de lectura. */
|
||||||
|
href: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Las cinco rutas por industria, en el orden de prioridad comercial que fijó
|
||||||
|
* la auditoría, ya resueltas a su destino en el catálogo.
|
||||||
|
*/
|
||||||
|
export const RUTAS_INDUSTRIA: RutaIndustria[] = INDUSTRIAS.slice()
|
||||||
|
.sort((a, b) => a.prioridad - b.prioridad)
|
||||||
|
.map((industria) => {
|
||||||
|
const escenario = escenarioDeIndustria(industria.slug);
|
||||||
|
return {
|
||||||
|
industria,
|
||||||
|
escenario,
|
||||||
|
href: escenario ? hrefEscenario(escenario.slug) : hrefClases(industria.clases),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Primera oración de un texto largo, sin cortar a media palabra. */
|
||||||
|
export function primeraOracion(texto: string): string {
|
||||||
|
const corte = texto.indexOf(". ");
|
||||||
|
return corte === -1 ? texto : `${texto.slice(0, corte)}.`;
|
||||||
|
}
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
import Image from "next/image";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { Revelar } from "@/components/site/Revelar";
|
||||||
|
import { getCategoria, getProducto, type Producto } from "@/data/catalogo";
|
||||||
|
import { guiaDe } from "./guia";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* "¿ES ESTO LO QUE NECESITO?" — el bloque para quien no es del oficio.
|
||||||
|
*
|
||||||
|
* Va inmediatamente debajo del héroe porque es la pregunta con la que llega la
|
||||||
|
* mitad de los visitantes. No repite la especificación: la traduce. Y cierra
|
||||||
|
* con la parte que ningún catálogo pone —cuándo este equipo NO es la respuesta
|
||||||
|
* y a qué producto conviene irse—, con enlace directo a esa otra ficha.
|
||||||
|
*
|
||||||
|
* Eso no es generosidad, es venta: el visitante que se lleva el equipo
|
||||||
|
* equivocado devuelve, reclama o no vuelve. El que se lleva el correcto porque
|
||||||
|
* se lo dijiste, regresa por el resto de la instalación.
|
||||||
|
*
|
||||||
|
* Este texto también es SEO: captura la búsqueda de cola larga de quien no
|
||||||
|
* conoce la terminología —"qué extintor necesito para una gasolinera"— que la
|
||||||
|
* ficha técnica sola nunca alcanza.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function BloqueAdecuacion({ producto }: { producto: Producto }) {
|
||||||
|
const guia = guiaDe(producto);
|
||||||
|
const categoria = getCategoria(producto.categoria);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
className="grid-bg-lite border-t border-rule-lite bg-paper py-14 sm:py-20"
|
||||||
|
aria-labelledby="adecuacion-titulo"
|
||||||
|
>
|
||||||
|
<div className="shell">
|
||||||
|
<div className="grid gap-10 lg:grid-cols-[1fr_1.25fr] lg:gap-14">
|
||||||
|
{/* el equipo, para que se vea mientras se lee si le sirve */}
|
||||||
|
<div className="lg:sticky lg:top-28 lg:self-start">
|
||||||
|
<div className="relative aspect-square w-full overflow-hidden rounded-[6px] border border-rule-lite bg-white">
|
||||||
|
<Image
|
||||||
|
src={producto.imagen}
|
||||||
|
alt={producto.nombre}
|
||||||
|
fill
|
||||||
|
sizes="(max-width: 640px) 88vw, (max-width: 1024px) 80vw, 26rem"
|
||||||
|
className="object-contain p-6 sm:p-10"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{producto.modelo ? (
|
||||||
|
<dl className="panel-lite mt-4 px-4 py-3">
|
||||||
|
<dt className="spec-key !text-deep-muted">Modelo declarado</dt>
|
||||||
|
<dd className="spec-val !text-deep whitespace-normal">{producto.modelo}</dd>
|
||||||
|
</dl>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Revelar>
|
||||||
|
<p className="eyebrow mb-4">En lenguaje llano</p>
|
||||||
|
<h2
|
||||||
|
id="adecuacion-titulo"
|
||||||
|
className="font-display text-2xl font-extrabold leading-tight text-deep sm:text-[2rem]"
|
||||||
|
>
|
||||||
|
¿Es esto lo que necesito?
|
||||||
|
</h2>
|
||||||
|
<p className="mt-5 max-w-2xl text-base leading-relaxed text-deep-muted">
|
||||||
|
{guia.protege}
|
||||||
|
</p>
|
||||||
|
</Revelar>
|
||||||
|
|
||||||
|
{guia.lugares.length > 0 ? (
|
||||||
|
<div className="mt-9">
|
||||||
|
<h3 className="font-mono text-[0.6875rem] uppercase tracking-[0.16em] text-deep-muted">
|
||||||
|
Dónde tiene sentido ponerlo
|
||||||
|
</h3>
|
||||||
|
<ul className="mt-4 grid gap-x-8 gap-y-2.5 sm:grid-cols-2">
|
||||||
|
{guia.lugares.map((lugar) => (
|
||||||
|
<li key={lugar} className="flex gap-3">
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
className="mt-[0.5rem] h-1.5 w-1.5 shrink-0 rounded-full bg-signal"
|
||||||
|
/>
|
||||||
|
<span className="text-sm leading-relaxed text-deep-muted">{lugar}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{guia.cuandoNo.length > 0 ? (
|
||||||
|
<div className="mt-10">
|
||||||
|
<h3 className="font-mono text-[0.6875rem] uppercase tracking-[0.16em] text-ember-dim">
|
||||||
|
Cuándo no es la opción correcta
|
||||||
|
</h3>
|
||||||
|
<p className="mt-3 max-w-2xl text-sm leading-relaxed text-deep-muted">
|
||||||
|
Preferimos decírtelo aquí que en la entrega. Si tu caso es uno de estos, el equipo
|
||||||
|
que te conviene es otro y te lo dejamos a un clic.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ul className="mt-6 grid gap-px bg-rule-lite">
|
||||||
|
{guia.cuandoNo.map((alternativa) => {
|
||||||
|
const destino = getProducto(alternativa.hacia);
|
||||||
|
if (!destino) return null;
|
||||||
|
const destinoCategoria = getCategoria(destino.categoria);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li key={alternativa.caso} className="bg-white p-5 sm:p-6">
|
||||||
|
<p className="border-l-2 border-ember pl-3.5 font-display text-base font-bold leading-snug text-deep">
|
||||||
|
{alternativa.caso}
|
||||||
|
</p>
|
||||||
|
<p className="mt-3 text-sm leading-relaxed text-deep-muted">
|
||||||
|
{alternativa.porque}
|
||||||
|
</p>
|
||||||
|
<Link
|
||||||
|
href={`/producto/${destino.slug}`}
|
||||||
|
className="group mt-4 inline-flex flex-wrap items-center gap-x-2.5 gap-y-1 [-webkit-tap-highlight-color:transparent]"
|
||||||
|
>
|
||||||
|
<span className="font-mono text-[0.625rem] uppercase tracking-[0.14em] text-deep-muted">
|
||||||
|
Conviene
|
||||||
|
</span>
|
||||||
|
<span className="font-mono text-sm font-medium text-deep underline decoration-rule-lite decoration-1 underline-offset-4 transition-colors group-hover:decoration-signal">
|
||||||
|
{destino.nombre}
|
||||||
|
</span>
|
||||||
|
{destinoCategoria ? (
|
||||||
|
<span className="font-mono text-[0.625rem] uppercase tracking-[0.12em] text-deep-muted">
|
||||||
|
· {destinoCategoria.nombre}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
<svg
|
||||||
|
width="11"
|
||||||
|
height="8"
|
||||||
|
viewBox="0 0 11 8"
|
||||||
|
aria-hidden="true"
|
||||||
|
className="shrink-0 text-signal"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M0 4h9M6 1l3 3-3 3"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.3"
|
||||||
|
fill="none"
|
||||||
|
strokeLinecap="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{categoria ? (
|
||||||
|
<p className="mt-8 max-w-2xl text-xs leading-relaxed text-deep-muted">
|
||||||
|
Si ninguno de los casos anteriores es el tuyo y sigues con la duda, descríbenos la
|
||||||
|
instalación en el formulario: es más rápido que comparar{" "}
|
||||||
|
{categoria.nombre.toLowerCase()} uno por uno.
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* "COTIZAR ESTE EQUIPO" — el único llamado a la acción de la ficha.
|
||||||
|
*
|
||||||
|
* Es un ancla, no un botón con JavaScript. Tres razones:
|
||||||
|
* · funciona con el JS deshabilitado y antes de que hidrate la página;
|
||||||
|
* · el navegador le da el comportamiento de enlace gratis —foco, Enter,
|
||||||
|
* abrir en otra pestaña, anunciarlo como enlace en un lector de pantalla—;
|
||||||
|
* · no necesita estado compartido con el formulario.
|
||||||
|
*
|
||||||
|
* El formulario destino ya trae `scroll-mt-28`, así que el salto no queda
|
||||||
|
* debajo del encabezado pegajoso.
|
||||||
|
*
|
||||||
|
* Aparece cuatro veces en la ficha a propósito. Una página larga sin llamado a
|
||||||
|
* la acción a la vista pierde al visitante que ya decidió a los dos scrolls.
|
||||||
|
*/
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
children?: ReactNode;
|
||||||
|
/** signal = acción primaria. ghost sobre oscuro, deep sobre papel. */
|
||||||
|
variante?: "signal" | "ghost" | "deep";
|
||||||
|
tamano?: "md" | "sm";
|
||||||
|
/** Ancho completo en móvil, automático desde 420px. */
|
||||||
|
bloque?: boolean;
|
||||||
|
/** Destino. El formulario de la ficha, salvo que se indique otro. */
|
||||||
|
href?: string;
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const VARIANTE = {
|
||||||
|
signal: "btn-signal",
|
||||||
|
ghost: "btn-ghost",
|
||||||
|
deep: "btn-deep",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export function BotonCotizar({
|
||||||
|
children = "Cotizar este equipo",
|
||||||
|
variante = "signal",
|
||||||
|
tamano = "md",
|
||||||
|
bloque = false,
|
||||||
|
href = "#cotizar",
|
||||||
|
className = "",
|
||||||
|
}: Props) {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
href={href}
|
||||||
|
className={[
|
||||||
|
"btn",
|
||||||
|
VARIANTE[variante],
|
||||||
|
tamano === "sm" ? "btn-sm" : "",
|
||||||
|
bloque ? "w-full min-[420px]:w-auto" : "",
|
||||||
|
"[-webkit-tap-highlight-color:transparent]",
|
||||||
|
className,
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(" ")}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<svg width="12" height="12" viewBox="0 0 12 12" aria-hidden="true" className="shrink-0">
|
||||||
|
<path
|
||||||
|
d="M6 1v8M2.5 6L6 9.5 9.5 6"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.4"
|
||||||
|
fill="none"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { Revelar } from "@/components/site/Revelar";
|
||||||
|
import type { Producto } from "@/data/catalogo";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CARACTERÍSTICAS — lo que la fuente declara del equipo, tal cual.
|
||||||
|
*
|
||||||
|
* Va sobre papel porque pertenece al mundo del documento, no al del riesgo.
|
||||||
|
* Cada punto es una línea de `producto.caracteristicas`: no se reordena, no se
|
||||||
|
* reescribe y no se le agrega nada. Las afirmaciones que el estudio de marca
|
||||||
|
* marcó como no verificables no viven aquí, viven en el compromiso de
|
||||||
|
* documentación.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function Caracteristicas({ producto }: { producto: Producto }) {
|
||||||
|
if (producto.caracteristicas.length === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
className="border-t border-rule-lite bg-paper py-14 sm:py-16"
|
||||||
|
aria-labelledby="caracteristicas-titulo"
|
||||||
|
>
|
||||||
|
<div className="shell">
|
||||||
|
<Revelar>
|
||||||
|
<p className="eyebrow mb-4">Características</p>
|
||||||
|
<h2
|
||||||
|
id="caracteristicas-titulo"
|
||||||
|
className="font-display text-2xl font-extrabold text-deep sm:text-3xl"
|
||||||
|
>
|
||||||
|
Lo que la fuente declara del equipo
|
||||||
|
</h2>
|
||||||
|
</Revelar>
|
||||||
|
|
||||||
|
<ul className="mt-8 grid gap-px bg-rule-lite sm:grid-cols-2">
|
||||||
|
{producto.caracteristicas.map((caracteristica, i) => (
|
||||||
|
<li key={caracteristica} className="flex gap-3.5 bg-white p-5">
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
className="mt-0.5 shrink-0 font-mono text-[0.625rem] font-semibold tracking-[0.08em] text-deep-muted"
|
||||||
|
>
|
||||||
|
{String(i + 1).padStart(2, "0")}
|
||||||
|
</span>
|
||||||
|
<p className="text-sm leading-relaxed text-deep-muted">{caracteristica}</p>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
import Image from "next/image";
|
||||||
|
import { PastillaEscenario } from "@/components/marca/IconoEscenario";
|
||||||
|
import { Revelar } from "@/components/site/Revelar";
|
||||||
|
import { iconoDeAplicacion } from "@/components/site/catalogo/iconos";
|
||||||
|
import type { Producto } from "@/data/catalogo";
|
||||||
|
import { escenariosDeCategoria } from "@/data/escenarios";
|
||||||
|
import { BotonCotizar } from "./BotonCotizar";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ESCENARIOS DONDE APLICA — la iconografía original de FSPM haciendo su trabajo.
|
||||||
|
*
|
||||||
|
* El sitio anterior tenía estos iconos y ya estaba resolviendo el problema
|
||||||
|
* correcto: el visitante no llega pensando "clase B", llega pensando "tengo
|
||||||
|
* una gasolinera". Aquí cada escenario viene con su línea en lenguaje llano,
|
||||||
|
* así que sirve igual al comprador que reconoce su lugar y al buscador que
|
||||||
|
* indexa "extintor para bodega".
|
||||||
|
*
|
||||||
|
* Si la categoría no tiene escenarios declarados —FireMiks es el caso—, no se
|
||||||
|
* deja el hueco: se pintan las aplicaciones que sí declara el catálogo.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function EscenariosProducto({ producto }: { producto: Producto }) {
|
||||||
|
const escenarios = escenariosDeCategoria(producto.categoria);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
className="border-t border-rule bg-ink-2 py-14 sm:py-16"
|
||||||
|
aria-labelledby="escenarios-titulo"
|
||||||
|
>
|
||||||
|
<div className="shell">
|
||||||
|
<Revelar>
|
||||||
|
<p className="eyebrow mb-4">Dónde aplica</p>
|
||||||
|
<h2 id="escenarios-titulo" className="font-display text-2xl font-extrabold sm:text-3xl">
|
||||||
|
{escenarios.length > 0 ? "Escenarios donde este equipo aplica" : "Aplicaciones declaradas"}
|
||||||
|
</h2>
|
||||||
|
<p className="mt-3 max-w-2xl text-sm leading-relaxed text-muted">
|
||||||
|
{escenarios.length > 0
|
||||||
|
? "Reconoce tu operación en la lista y dinos cuál es. El equipo cambia de configuración y de cantidad según el lugar, no según el catálogo."
|
||||||
|
: "La fuente ubica este producto en los siguientes usos. Si el tuyo no está en la lista, descríbelo en el formulario y lo revisamos."}
|
||||||
|
</p>
|
||||||
|
</Revelar>
|
||||||
|
|
||||||
|
{escenarios.length > 0 ? (
|
||||||
|
<ul className="mt-9 grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{escenarios.map((escenario) => (
|
||||||
|
<li
|
||||||
|
key={escenario.slug}
|
||||||
|
className="flex flex-col gap-3 rounded-[4px] border border-rule-2 bg-ink p-4"
|
||||||
|
>
|
||||||
|
<PastillaEscenario escenario={escenario} />
|
||||||
|
<p className="text-sm leading-relaxed text-muted">{escenario.enLlano}</p>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
) : (
|
||||||
|
<ul className="mt-9 grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-4">
|
||||||
|
{producto.aplicaciones.map((aplicacion) => {
|
||||||
|
const icono = iconoDeAplicacion(aplicacion);
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
key={aplicacion}
|
||||||
|
className="flex flex-col items-center gap-3 rounded-[4px] border border-rule-2 bg-ink px-3 py-5 text-center"
|
||||||
|
>
|
||||||
|
{icono ? (
|
||||||
|
<Image
|
||||||
|
src={icono}
|
||||||
|
alt=""
|
||||||
|
aria-hidden="true"
|
||||||
|
width={34}
|
||||||
|
height={34}
|
||||||
|
sizes="34px"
|
||||||
|
className="h-[34px] w-[34px] object-contain"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
className="flex h-[34px] w-[34px] items-center justify-center rounded-[2px] border border-rule-2 font-mono text-xs text-faint"
|
||||||
|
>
|
||||||
|
{aplicacion.slice(0, 2).toUpperCase()}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span className="font-mono text-[0.6875rem] leading-tight tracking-[0.04em] text-bright">
|
||||||
|
{aplicacion}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="mt-9">
|
||||||
|
<BotonCotizar variante="ghost" tamano="sm">
|
||||||
|
Mi caso es este, quiero cotizar
|
||||||
|
</BotonCotizar>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
import { Revelar } from "@/components/site/Revelar";
|
||||||
|
import { getCategoria, type Producto } from "@/data/catalogo";
|
||||||
|
import { CLASES_INFO } from "@/data/clases-fuego";
|
||||||
|
import { BotonCotizar } from "./BotonCotizar";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ESPECIFICACIÓN TÉCNICA — la mitad que sostiene la credibilidad y el SEO.
|
||||||
|
*
|
||||||
|
* La tira `.spec` omite los campos nulos. Nada de "—" ni de "No disponible":
|
||||||
|
* en una ficha técnica un guion es una afirmación de que el dato existe y no
|
||||||
|
* se encontró, y eso es peor que no pintar la fila.
|
||||||
|
*
|
||||||
|
* El párrafo de arriba es deliberadamente denso en vocabulario del oficio
|
||||||
|
* —clase de fuego, agente, capacidad, alcance, aplicación, NOM— porque es el
|
||||||
|
* texto que posiciona la ficha en búsqueda técnica. Se construye con los
|
||||||
|
* campos del catálogo, así que no puede afirmar nada que la fuente no declare.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function EspecificacionProducto({ producto }: { producto: Producto }) {
|
||||||
|
const categoria = getCategoria(producto.categoria);
|
||||||
|
|
||||||
|
const especificacion = [
|
||||||
|
{ clave: "Capacidad", valor: producto.capacidad },
|
||||||
|
{ clave: "Agente", valor: producto.agente },
|
||||||
|
{ clave: "Alcance declarado", valor: producto.alcance },
|
||||||
|
{ clave: "Descarga", valor: producto.descarga },
|
||||||
|
{ clave: "Modelo", valor: producto.modelo },
|
||||||
|
].filter((campo): campo is { clave: string; valor: string } => Boolean(campo.valor));
|
||||||
|
|
||||||
|
const clasesConNombre = producto.clases
|
||||||
|
.map((clase) => `${clase} (${CLASES_INFO[clase].nombre.toLowerCase()})`)
|
||||||
|
.join(", ");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
id="especificacion"
|
||||||
|
className="scroll-mt-24 border-t border-rule bg-ink py-14 sm:py-16"
|
||||||
|
aria-labelledby="especificacion-titulo"
|
||||||
|
>
|
||||||
|
<div className="shell">
|
||||||
|
<div className="grid gap-10 lg:grid-cols-[1fr_1.1fr] lg:gap-14">
|
||||||
|
<div>
|
||||||
|
<Revelar>
|
||||||
|
<p className="eyebrow mb-4">Especificación</p>
|
||||||
|
<h2
|
||||||
|
id="especificacion-titulo"
|
||||||
|
className="font-display text-2xl font-extrabold sm:text-3xl"
|
||||||
|
>
|
||||||
|
Lo que la fuente declara, sin rellenar huecos
|
||||||
|
</h2>
|
||||||
|
</Revelar>
|
||||||
|
|
||||||
|
<p className="mt-5 max-w-2xl text-sm leading-relaxed text-muted">
|
||||||
|
{producto.nombre} es un producto de la categoría{" "}
|
||||||
|
{categoria ? categoria.nombre.toLowerCase() : "catálogo"} del catálogo de FSPM
|
||||||
|
{producto.agente ? `, con agente ${producto.agente}` : ""}
|
||||||
|
{producto.capacidad ? ` y capacidad declarada de ${producto.capacidad}` : ""}.
|
||||||
|
{clasesConNombre
|
||||||
|
? ` La fuente lo declara para fuego de clase ${clasesConNombre}.`
|
||||||
|
: " La fuente no lo presenta como agente extintor, así que no le atribuimos clases de fuego."}
|
||||||
|
{producto.aplicaciones.length > 0
|
||||||
|
? ` Su aplicación declarada abarca ${producto.aplicaciones
|
||||||
|
.map((a) => a.toLowerCase())
|
||||||
|
.join(", ")}.`
|
||||||
|
: ""}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{producto.clases.length > 0 ? (
|
||||||
|
<p className="mt-4 max-w-2xl text-sm leading-relaxed text-muted">
|
||||||
|
Como equipo contra incendio en un centro de trabajo mexicano, la cantidad y la
|
||||||
|
ubicación se definen a partir de la clasificación de riesgo de la
|
||||||
|
NOM-002-STPS-2010, y su mantenimiento se rige por la NOM-154-SCFI-2005. La
|
||||||
|
condición de aplicación por clase se confirma en la ficha técnica del modelo; para
|
||||||
|
clase C la fuente condiciona el desempeño al uso de agua desmineralizada.
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<div className="mt-8">
|
||||||
|
<BotonCotizar>Cotizar este equipo</BotonCotizar>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{especificacion.length > 0 ? (
|
||||||
|
<div
|
||||||
|
className="overflow-x-auto"
|
||||||
|
style={{ WebkitOverflowScrolling: "touch" }}
|
||||||
|
>
|
||||||
|
<dl className="spec min-w-[15rem] sm:grid-cols-2">
|
||||||
|
{especificacion.map((campo) => (
|
||||||
|
<div key={campo.clave} className="spec-cell">
|
||||||
|
<dt className="spec-key">{campo.clave}</dt>
|
||||||
|
<dd className="spec-val whitespace-normal leading-relaxed">{campo.valor}</dd>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
{/* Cierre de venta justo donde termina la especificación: quien
|
||||||
|
acabó de leer capacidad, agente y modelo declarado ya tiene
|
||||||
|
lo que necesitaba para decidir, y no debería tener que
|
||||||
|
buscar el botón. */}
|
||||||
|
<BotonCotizar bloque className="mt-4">
|
||||||
|
Cotizar este equipo
|
||||||
|
</BotonCotizar>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="panel p-5">
|
||||||
|
<p className="font-mono text-[0.6875rem] uppercase tracking-[0.16em] text-ember">
|
||||||
|
Sin especificación numérica publicada
|
||||||
|
</p>
|
||||||
|
<p className="mt-3 text-sm leading-relaxed text-muted">
|
||||||
|
La fuente no publica capacidad, agente ni alcance para este producto. Los datos
|
||||||
|
se piden al fabricante por número de modelo y se entregan con la cotización.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{producto.clases.length > 0 ? (
|
||||||
|
<dl className="mt-5 grid gap-px bg-rule">
|
||||||
|
{producto.clases.map((clase) => (
|
||||||
|
<div key={clase} className="flex gap-3 bg-ink-2 p-4">
|
||||||
|
<dt className="flex h-6 w-6 shrink-0 items-center justify-center rounded-[2px] bg-signal font-mono text-[0.6875rem] font-semibold text-white">
|
||||||
|
{clase}
|
||||||
|
</dt>
|
||||||
|
<dd className="text-xs leading-relaxed text-muted">
|
||||||
|
<span className="font-semibold text-bright">
|
||||||
|
{CLASES_INFO[clase].nombre}.
|
||||||
|
</span>{" "}
|
||||||
|
{CLASES_INFO[clase].combustible}.
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</dl>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { FormularioContacto } from "@/components/formulario/FormularioContacto";
|
||||||
|
import type { ClaseFuego } from "@/data/clases-fuego";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* El formulario de la ficha, con el escenario de procedencia adjunto.
|
||||||
|
*
|
||||||
|
* Si el visitante llegó desde un escenario —`/producto/x?escenario=gasolineras`—
|
||||||
|
* ese dato tiene que viajar al CRM: es la diferencia entre un lead que dice
|
||||||
|
* "extintor de 9 litros" y uno que dice "extintor de 9 litros para una
|
||||||
|
* gasolinera".
|
||||||
|
*
|
||||||
|
* Se lee del lado del cliente y no con `searchParams` de la página a propósito:
|
||||||
|
* leer `searchParams` en el servidor convertiría las veintidós fichas en
|
||||||
|
* páginas dinámicas y perderían la generación estática. Aquí el parámetro no se
|
||||||
|
* pinta en el DOM —solo viaja en el envío—, así que leerlo después de montar no
|
||||||
|
* produce ninguna diferencia de hidratación.
|
||||||
|
*
|
||||||
|
* El resto del contexto —producto, categoría, industria, clases— ya viene
|
||||||
|
* resuelto del servidor, y la lista de escenarios válidos también: así el
|
||||||
|
* catálogo de escenarios no viaja al paquete del cliente para validar un slug.
|
||||||
|
*/
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
producto: { slug: string; nombre: string; categoriaSlug: string };
|
||||||
|
industria?: string;
|
||||||
|
clases?: ClaseFuego[];
|
||||||
|
mensajeInicial: string;
|
||||||
|
/** Slugs de escenario aceptables. Lo que no está en la lista se descarta. */
|
||||||
|
escenariosValidos: string[];
|
||||||
|
titulo?: string;
|
||||||
|
descripcion?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function FormularioCotizar({
|
||||||
|
producto,
|
||||||
|
industria,
|
||||||
|
clases,
|
||||||
|
mensajeInicial,
|
||||||
|
escenariosValidos,
|
||||||
|
titulo = "Cotizar este equipo",
|
||||||
|
descripcion = "Cuatro campos. El equipo que estás viendo ya va registrado en la solicitud, así que la primera respuesta puede ser técnica en lugar de una pregunta de vuelta.",
|
||||||
|
}: Props) {
|
||||||
|
const [escenario, setEscenario] = useState<string | undefined>(undefined);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const valor = new URLSearchParams(window.location.search).get("escenario");
|
||||||
|
if (valor && escenariosValidos.includes(valor)) setEscenario(valor);
|
||||||
|
}, [escenariosValidos]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormularioContacto
|
||||||
|
id="cotizar"
|
||||||
|
canal="producto"
|
||||||
|
producto={producto}
|
||||||
|
industria={industria}
|
||||||
|
escenario={escenario}
|
||||||
|
clases={clases}
|
||||||
|
mensajeInicial={mensajeInicial}
|
||||||
|
titulo={titulo}
|
||||||
|
descripcion={descripcion}
|
||||||
|
textoBoton="Recibir cotización"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import { BloqueDocumentacion } from "@/components/site/catalogo/BloqueDocumentacion";
|
||||||
|
import { Revelar } from "@/components/site/Revelar";
|
||||||
|
import type { Producto } from "@/data/catalogo";
|
||||||
|
import { NORMAS } from "@/data/empresa";
|
||||||
|
import { BotonCotizar } from "./BotonCotizar";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CUMPLIMIENTO Y DOCUMENTACIÓN — el argumento normativo resuelto aquí mismo.
|
||||||
|
*
|
||||||
|
* Corrección directa al encargo del cliente: la ficha ya no manda al visitante
|
||||||
|
* a leer la norma en otra página. El cumplimiento sí es argumento de venta, así
|
||||||
|
* que se resuelve en dos párrafos y termina en el formulario. Quien quiera la
|
||||||
|
* norma completa la encuentra en la navegación y en el pie; quien está a punto
|
||||||
|
* de cotizar no debería tener que salir de la ficha para hacerlo.
|
||||||
|
*
|
||||||
|
* Debajo va el compromiso de documentación: las afirmaciones que el estudio de
|
||||||
|
* marca marcó como no verificables, presentadas como lo que sí podemos
|
||||||
|
* sostener —entregarlas por escrito, por número de modelo—, nunca como
|
||||||
|
* características del producto.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const NOM_002 = NORMAS[0];
|
||||||
|
const NOM_154 = NORMAS[1];
|
||||||
|
|
||||||
|
export function NormaYDocumentacion({ producto }: { producto: Producto }) {
|
||||||
|
const esEquipoDeExtincion = producto.clases.length > 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
className="grid-bg-lite border-t border-rule-lite bg-paper py-14 sm:py-20"
|
||||||
|
aria-labelledby="cumplimiento-titulo"
|
||||||
|
>
|
||||||
|
<div className="shell">
|
||||||
|
<div className="grid gap-10 lg:grid-cols-[1fr_1.15fr] lg:gap-14">
|
||||||
|
<div>
|
||||||
|
<Revelar>
|
||||||
|
<p className="eyebrow mb-4">Cumplimiento</p>
|
||||||
|
<h2
|
||||||
|
id="cumplimiento-titulo"
|
||||||
|
className="font-display text-2xl font-extrabold leading-tight text-deep sm:text-3xl"
|
||||||
|
>
|
||||||
|
Lo que la norma pide para este equipo
|
||||||
|
</h2>
|
||||||
|
</Revelar>
|
||||||
|
|
||||||
|
<p className="mt-5 max-w-2xl text-sm leading-relaxed text-deep-muted sm:text-base">
|
||||||
|
La {NOM_002.clave} aplica a todo centro de trabajo en México: clasifica el riesgo de
|
||||||
|
incendio del inmueble y, según ese nivel, determina el equipo, los sistemas fijos,
|
||||||
|
las alarmas, el plan de emergencia, las brigadas y la capacitación que te van a
|
||||||
|
exigir. La revisión de extintores es mensual y su mantenimiento se rige por la{" "}
|
||||||
|
{NOM_154.clave}, referida por la propia {NOM_002.clave}.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p className="mt-4 max-w-2xl text-sm leading-relaxed text-deep-muted">
|
||||||
|
{esEquipoDeExtincion
|
||||||
|
? "Para este equipo eso significa dos cosas concretas: la cantidad y la ubicación no se deciden por catálogo, se deciden por la clasificación de tu inmueble; y la bitácora de revisión es parte del cumplimiento, no un extra. No necesitas leer la norma para avanzar."
|
||||||
|
: "Este producto no es equipo de extinción, así que por sí mismo no cubre el requisito de equipo contra incendio de la norma. Te decimos con qué sí se cumple y qué te falta, en lugar de venderte de más."}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="mt-7 flex flex-col gap-4 min-[420px]:flex-row min-[420px]:flex-wrap min-[420px]:items-center">
|
||||||
|
<BotonCotizar variante="deep" bloque>
|
||||||
|
Cotizar este equipo
|
||||||
|
</BotonCotizar>
|
||||||
|
<p className="text-xs leading-relaxed text-deep-muted">
|
||||||
|
Descríbenos el inmueble y en la propuesta te decimos qué parte del cumplimiento
|
||||||
|
cubre este equipo y qué queda pendiente.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="mt-7 border-t border-rule-lite pt-4 font-mono text-[0.625rem] uppercase leading-relaxed tracking-[0.12em] text-deep-muted">
|
||||||
|
Referencias: {NOM_002.clave} · {NOM_154.clave}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="lg:pt-1">
|
||||||
|
{producto.claimsPorVerificar.length > 0 ? (
|
||||||
|
<BloqueDocumentacion claims={producto.claimsPorVerificar} claro />
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import { Revelar } from "@/components/site/Revelar";
|
||||||
|
import type { Producto } from "@/data/catalogo";
|
||||||
|
import { CLASES_INFO } from "@/data/clases-fuego";
|
||||||
|
import { BotonCotizar } from "./BotonCotizar";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RIESGO DE USAR EL AGENTE EQUIVOCADO — el argumento técnico más fuerte del
|
||||||
|
* sitio, y ningún competidor lo está dando.
|
||||||
|
*
|
||||||
|
* Sirve a las dos audiencias con el mismo texto: al jefe de seguridad le
|
||||||
|
* confirma que quien escribió la ficha entiende el oficio, y al comprador sin
|
||||||
|
* formación técnica le explica por qué no da lo mismo comprar "un extintor"
|
||||||
|
* en la ferretería.
|
||||||
|
*
|
||||||
|
* Es también el mejor argumento de venta consultiva: no habla del producto,
|
||||||
|
* habla de la consecuencia de elegir mal. Va en `text-ember` porque es un
|
||||||
|
* estado de advertencia, no un adorno.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function RiesgoAgente({ producto }: { producto: Producto }) {
|
||||||
|
if (producto.clases.length === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
className="grid-bg border-t border-rule bg-ink py-14 sm:py-20"
|
||||||
|
aria-labelledby="riesgo-titulo"
|
||||||
|
>
|
||||||
|
<div className="shell">
|
||||||
|
<Revelar>
|
||||||
|
<p className="eyebrow mb-4">Selección del agente</p>
|
||||||
|
<h2 id="riesgo-titulo" className="font-display text-2xl font-extrabold sm:text-3xl">
|
||||||
|
Qué pasa si se usa el agente equivocado
|
||||||
|
</h2>
|
||||||
|
<p className="mt-4 max-w-2xl text-sm leading-relaxed text-muted sm:text-base">
|
||||||
|
Este equipo cubre las clases de abajo. La razón por la que la clase importa no es
|
||||||
|
administrativa: el agente incorrecto sobre el combustible incorrecto empeora el
|
||||||
|
incidente, y quien paga eso es la persona que tiene la manguera en la mano.
|
||||||
|
</p>
|
||||||
|
</Revelar>
|
||||||
|
|
||||||
|
<ul className="mt-9 grid gap-4 sm:grid-cols-2">
|
||||||
|
{producto.clases.map((clase) => {
|
||||||
|
const info = CLASES_INFO[clase];
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
key={clase}
|
||||||
|
className="flex flex-col gap-3 rounded-[4px] border border-rule border-l-2 border-l-ember bg-ink-2 p-5"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
className="flex h-8 w-8 shrink-0 items-center justify-center rounded-[2px] bg-signal font-mono text-sm font-semibold text-white"
|
||||||
|
>
|
||||||
|
{clase}
|
||||||
|
</span>
|
||||||
|
<span className="min-w-0">
|
||||||
|
<span className="block font-display text-base font-bold leading-tight text-bright">
|
||||||
|
Clase {clase} · {info.nombre}
|
||||||
|
</span>
|
||||||
|
<span className="mt-1 block font-mono text-[0.6875rem] leading-snug text-faint">
|
||||||
|
{info.combustible}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-sm leading-relaxed text-muted">
|
||||||
|
<span className="font-mono text-[0.625rem] uppercase tracking-[0.14em] text-ember">
|
||||||
|
Si el agente es el equivocado
|
||||||
|
</span>
|
||||||
|
<span className="mt-1.5 block">{info.riesgoAgenteIncorrecto}</span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{info.dondeAparece.length > 0 ? (
|
||||||
|
<p className="mt-auto border-t border-rule pt-3 text-xs leading-relaxed text-faint">
|
||||||
|
Aparece en {info.dondeAparece.map((d) => d.toLowerCase()).join(", ")}.
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div className="mt-9 flex flex-col gap-4 sm:flex-row sm:items-center">
|
||||||
|
<BotonCotizar bloque>Que revisen mi caso</BotonCotizar>
|
||||||
|
<p className="max-w-md text-xs leading-relaxed text-faint">
|
||||||
|
Dinos qué material se maneja en el área y te decimos qué clase tienes antes de
|
||||||
|
cotizarte nada. Si el equipo correcto es otro, también te lo decimos.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
import Image from "next/image";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { RielClases } from "@/components/marca/RielClases";
|
||||||
|
import type { Producto } from "@/data/catalogo";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Producto relacionado con su propio botón de cotizar.
|
||||||
|
*
|
||||||
|
* La tarjeta del catálogo hace toda su superficie enlazable con una capa
|
||||||
|
* absoluta, y eso impide poner una segunda acción dentro. Aquí hacen falta dos:
|
||||||
|
* ver la ficha y cotizar directo. Un visitante que ya sabe que ese es el equipo
|
||||||
|
* no tiene que pasar por la ficha para pedir precio.
|
||||||
|
*
|
||||||
|
* El enlace de cotizar apunta a `#cotizar` de la otra ficha, así que llega con
|
||||||
|
* el formulario a la vista y con ese producto ya registrado.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function TarjetaRelacionada({ producto }: { producto: Producto }) {
|
||||||
|
return (
|
||||||
|
<article className="flex h-full flex-col overflow-hidden rounded-[6px] border border-rule-lite bg-white">
|
||||||
|
<div className="relative aspect-[4/3] w-full overflow-hidden border-b border-rule-lite bg-paper-2">
|
||||||
|
<Image
|
||||||
|
src={producto.imagen}
|
||||||
|
alt={producto.nombre}
|
||||||
|
fill
|
||||||
|
sizes="(max-width: 640px) 88vw, (max-width: 1024px) 44vw, 22rem"
|
||||||
|
className="object-contain p-5"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-1 flex-col p-5">
|
||||||
|
<h3 className="font-display text-lg font-extrabold leading-tight text-deep">
|
||||||
|
<Link
|
||||||
|
href={`/producto/${producto.slug}`}
|
||||||
|
className="underline decoration-rule-lite decoration-1 underline-offset-4 transition-colors hover:decoration-signal [-webkit-tap-highlight-color:transparent]"
|
||||||
|
>
|
||||||
|
{producto.nombre}
|
||||||
|
</Link>
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className="mt-3">
|
||||||
|
{producto.clases.length > 0 ? (
|
||||||
|
<RielClases activas={producto.clases} tamano="sm" claro etiqueta="Clases" />
|
||||||
|
) : (
|
||||||
|
<p className="font-mono text-[0.625rem] uppercase tracking-[0.12em] text-deep-muted">
|
||||||
|
Sin clase de fuego declarada
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{producto.capacidad || producto.alcance ? (
|
||||||
|
<dl className="mt-4 flex flex-wrap gap-x-6 gap-y-2">
|
||||||
|
{producto.capacidad ? (
|
||||||
|
<div>
|
||||||
|
<dt className="spec-key !text-deep-muted">Capacidad</dt>
|
||||||
|
<dd className="spec-val !text-deep">{producto.capacidad}</dd>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{producto.alcance ? (
|
||||||
|
<div>
|
||||||
|
<dt className="spec-key !text-deep-muted">Alcance declarado</dt>
|
||||||
|
<dd className="spec-val !text-deep">{producto.alcance}</dd>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</dl>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<div className="mt-auto flex flex-wrap gap-2.5 pt-5">
|
||||||
|
<Link
|
||||||
|
href={`/producto/${producto.slug}#cotizar`}
|
||||||
|
className="btn btn-signal btn-sm [-webkit-tap-highlight-color:transparent]"
|
||||||
|
>
|
||||||
|
Cotizar
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href={`/producto/${producto.slug}`}
|
||||||
|
className="btn btn-sm border-rule-lite text-deep [-webkit-tap-highlight-color:transparent] hover:border-deep"
|
||||||
|
>
|
||||||
|
Ver ficha
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,531 @@
|
|||||||
|
import type { Producto } from "@/data/catalogo";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* LA CAPA PRÁCTICA DE LA FICHA — "¿es esto lo que necesito?".
|
||||||
|
*
|
||||||
|
* El catálogo está descrito en el vocabulario del oficio: agente, clase de
|
||||||
|
* fuego, capacidad, alcance. Eso sirve al jefe de seguridad industrial y sirve
|
||||||
|
* para posicionar en búsqueda, pero deja fuera al comprador que llega
|
||||||
|
* pensando "tengo una gasolinera" o "necesito pasar la verificación".
|
||||||
|
*
|
||||||
|
* Este archivo es la traducción. Tres cosas por producto, en lenguaje llano:
|
||||||
|
*
|
||||||
|
* protege qué está cuidando este equipo, sin tecnicismos
|
||||||
|
* lugares dónde tiene sentido ponerlo
|
||||||
|
* cuandoNo cuándo NO es la opción correcta y qué conviene en su lugar
|
||||||
|
*
|
||||||
|
* `cuandoNo` es el bloque que más vende. Decirle a alguien que se lleve otro
|
||||||
|
* equipo cierra más operaciones que venderle el equivocado, y además evita la
|
||||||
|
* devolución, el reclamo y el equipo mal dimensionado en una verificación.
|
||||||
|
*
|
||||||
|
* REGLA DE HONESTIDAD (docs/CONTRATO.md): aquí no se declara ninguna
|
||||||
|
* especificación. Toda cifra que aparece —capacidades, temperaturas de
|
||||||
|
* disparo, galones de espuma terminada, cobertura— está copiada de
|
||||||
|
* `src/data/catalogo.ts` y se presenta atribuida a la fuente. Lo demás es
|
||||||
|
* criterio de aplicación, redactado como recomendación y no como dato del
|
||||||
|
* producto.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type Alternativa = {
|
||||||
|
/** El caso del visitante, como él lo diría. */
|
||||||
|
caso: string;
|
||||||
|
/** Por qué este equipo no es la respuesta para ese caso. */
|
||||||
|
porque: string;
|
||||||
|
/** Slug de `PRODUCTOS` al que conviene mandarlo. */
|
||||||
|
hacia: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Guia = {
|
||||||
|
protege: string;
|
||||||
|
lugares: string[];
|
||||||
|
cuandoNo: Alternativa[];
|
||||||
|
/**
|
||||||
|
* Texto con el que arranca el campo de mensaje del formulario. Se completa
|
||||||
|
* en cinco segundos: el visitante solo sustituye lo que está entre
|
||||||
|
* paréntesis. `{n}` se reemplaza por el nombre del producto.
|
||||||
|
*/
|
||||||
|
mensaje: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Base por categoría
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const POR_CATEGORIA: Record<string, Guia> = {
|
||||||
|
"extintores-portatiles": {
|
||||||
|
protege:
|
||||||
|
"Un fuego que apenas empieza y todavía está en un solo punto: un bote de basura encendido, una sartén, un tablero eléctrico, un derrame chico de combustible. Se descuelga de la pared y lo opera una persona sola, sin entrenamiento previo.",
|
||||||
|
lugares: [
|
||||||
|
"Casa, departamento y cochera",
|
||||||
|
"Oficina, recepción y cuarto de archivo",
|
||||||
|
"Comercio y local con atención al público",
|
||||||
|
"Taller, cuarto de máquinas y tablero eléctrico",
|
||||||
|
"Cabina de vehículo y maquinaria",
|
||||||
|
"Cocina chica y área de comedor",
|
||||||
|
],
|
||||||
|
cuandoNo: [
|
||||||
|
{
|
||||||
|
caso: "Tienes que cubrir una nave, un patio o una gasolinera completa",
|
||||||
|
porque:
|
||||||
|
"Un portátil se descarga en segundos y quien responde tiene que llegar caminando hasta el fuego. En superficie grande la respuesta se agota antes de llegar.",
|
||||||
|
hacia: "carretilla-100-litros",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
caso: "El fuego puede arrancar cuando no hay nadie: bodega de noche, cuarto de máquinas, campana de cocina",
|
||||||
|
porque:
|
||||||
|
"Un extintor necesita una mano que lo accione. Si no hay quien esté presente, el equipo tiene que activarse solo.",
|
||||||
|
hacia: "esfera-rociadora-automatica",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
caso: "Lo que te falta es el agente para recargar equipos que ya tienes instalados",
|
||||||
|
porque:
|
||||||
|
"Aquí estás pagando el recipiente además del agente. Si el equipo ya está montado, lo que se cotiza es el concentrado.",
|
||||||
|
hacia: "fireade-2000",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
mensaje:
|
||||||
|
"Me interesa cotizar este equipo: {n}. La instalación es en (dónde) y necesito (cuántas) piezas.",
|
||||||
|
},
|
||||||
|
|
||||||
|
"extintores-fijos": {
|
||||||
|
protege:
|
||||||
|
"Un punto de riesgo que ya tienes identificado y que no siempre tiene a alguien enfrente. El equipo queda montado ahí y responde cuando se dispara, con o sin personal en el área.",
|
||||||
|
lugares: [
|
||||||
|
"Taller y cuarto de máquinas",
|
||||||
|
"Almacén y zona de racks",
|
||||||
|
"Gasolinera: isla, patio y trasvase",
|
||||||
|
"Cuarto eléctrico y centro de carga",
|
||||||
|
"Bodega de materia prima",
|
||||||
|
],
|
||||||
|
cuandoNo: [
|
||||||
|
{
|
||||||
|
caso: "El riesgo se mueve y no sabes dónde va a arrancar",
|
||||||
|
porque:
|
||||||
|
"Un equipo instalado protege el punto donde está montado. Si el riesgo se desplaza —maniobras, unidades, patio— la respuesta tiene que viajar con él.",
|
||||||
|
hacia: "carretilla-70-litros",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
caso: "Solo necesitas el equipo mínimo de un local o una oficina",
|
||||||
|
porque:
|
||||||
|
"Un sistema instalado es más de lo que ese caso pide. Conviene resolverlo con equipo portátil dimensionado por área.",
|
||||||
|
hacia: "enforcer-6-litros",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
caso: "El área es amplia y abierta, y quieres que algo la vigile completa",
|
||||||
|
porque:
|
||||||
|
"Una unidad fija cubre su propio punto. Para nave completa el planteamiento es de detección y cañón, no de un equipo por zona.",
|
||||||
|
hacia: "enforcer-robot",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
mensaje:
|
||||||
|
"Me interesa cotizar este equipo: {n}. La instalación es en (dónde) y el área a proteger es de aproximadamente (cuántos) m².",
|
||||||
|
},
|
||||||
|
|
||||||
|
"productos-moviles": {
|
||||||
|
protege:
|
||||||
|
"Volumen de agente que se empuja hasta donde está el fuego. Es la respuesta de patio: más litros que un portátil y ruedas para llegar a la zona donde arrancó el incidente.",
|
||||||
|
lugares: [
|
||||||
|
"Patio de maniobras y zona de trasvase",
|
||||||
|
"Gasolinera y estación de carburación",
|
||||||
|
"Taller y almacén de gran superficie",
|
||||||
|
"Mina, aeropuerto y plataforma",
|
||||||
|
"Nave industrial con pasillos amplios",
|
||||||
|
],
|
||||||
|
cuandoNo: [
|
||||||
|
{
|
||||||
|
caso: "El acceso tiene escalones, pasillos angostos o hay que subir de nivel",
|
||||||
|
porque: "Un equipo con ruedas no sube escalones. Donde no rueda, no llega.",
|
||||||
|
hacia: "enforcer-12-litros",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
caso: "El turno lo cubre una sola persona, o de noche no hay nadie",
|
||||||
|
porque:
|
||||||
|
"Alguien tiene que empujarlo y accionarlo. Si no hay quien lo haga, conviene un equipo que se active por sí solo.",
|
||||||
|
hacia: "esfera-rociadora-automatica",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
caso: "Ya tienes red hidráulica con bomba y lo que falta es la espuma",
|
||||||
|
porque:
|
||||||
|
"Con agua a presión disponible, dosificar el concentrado en la propia red rinde más que mover recipientes.",
|
||||||
|
hacia: "firemiks",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
mensaje:
|
||||||
|
"Me interesa cotizar este equipo: {n}. Lo necesito para (qué área o instalación) y serían (cuántas) unidades.",
|
||||||
|
},
|
||||||
|
|
||||||
|
rociadores: {
|
||||||
|
protege:
|
||||||
|
"Un área cerrada donde el fuego puede empezar sin testigos. La esfera se activa cuando la temperatura del ambiente pasa su umbral, así que responde de noche, en fin de semana y con el área vacía.",
|
||||||
|
lugares: [
|
||||||
|
"Centro de cómputo y cuarto de servidores",
|
||||||
|
"Museo, biblioteca y acervo",
|
||||||
|
"Campana de cocina y área de freidoras",
|
||||||
|
"Almacén y taller",
|
||||||
|
"Oficina y cuarto de archivo",
|
||||||
|
"Cuarto de residuos y basurero",
|
||||||
|
],
|
||||||
|
cuandoNo: [
|
||||||
|
{
|
||||||
|
caso: "Quieres atacar tú el fuego en el momento en que lo ves",
|
||||||
|
porque:
|
||||||
|
"Una esfera espera un umbral de temperatura; no se acciona a voluntad. Para respuesta inmediata en mano el equipo es otro.",
|
||||||
|
hacia: "enforcer-9-litros",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
caso: "El área es mucho mayor que la cobertura de una unidad",
|
||||||
|
porque:
|
||||||
|
"La cobertura que declara la fuente es por esfera. Cubrir más superficie es sumar unidades o cambiar de planteamiento.",
|
||||||
|
hacia: "enforcer-robot",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
caso: "Tienes que proteger patio, unidades o exteriores",
|
||||||
|
porque:
|
||||||
|
"A cielo abierto el calor no se acumula bajo un techo y el viento mueve el frente. Ahí pesa más el alcance del chorro.",
|
||||||
|
hacia: "enforcer-60",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
mensaje:
|
||||||
|
"Me interesa cotizar este equipo: {n}. El área a proteger es (qué tipo de espacio) de aproximadamente (cuántos) m².",
|
||||||
|
},
|
||||||
|
|
||||||
|
"agentes-extinguidores": {
|
||||||
|
protege:
|
||||||
|
"Es el agente, no el equipo que lo aplica. Se cotiza por volumen para recargar equipos, alimentar un sistema fijo o abastecer a una brigada.",
|
||||||
|
lugares: [
|
||||||
|
"Recarga de equipos ya instalados",
|
||||||
|
"Sistemas fijos a base de agua",
|
||||||
|
"Brigada industrial y central de bomberos",
|
||||||
|
"Combate en exteriores y forestal",
|
||||||
|
],
|
||||||
|
cuandoNo: [
|
||||||
|
{
|
||||||
|
caso: "Lo que necesitas es el equipo que lo aplica",
|
||||||
|
porque:
|
||||||
|
"El agente sin recipiente ni línea de descarga no protege nada. Primero se define el equipo y luego el volumen.",
|
||||||
|
hacia: "enforcer-12-litros",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
caso: "Es una operación de patio y no hay red hidráulica",
|
||||||
|
porque:
|
||||||
|
"Sin agua a presión el agente necesita un equipo que lo cargue y lo lance hasta el punto.",
|
||||||
|
hacia: "enforcer-60",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
mensaje:
|
||||||
|
"Me interesa cotizar este producto: {n}. Lo necesito para (qué riesgo o material) y el volumen aproximado es (cuánto).",
|
||||||
|
},
|
||||||
|
|
||||||
|
firemiks: {
|
||||||
|
protege:
|
||||||
|
"Dosifica el concentrado de espuma dentro de una red de agua que ya existe. No almacena agente ni apaga por sí mismo: hace que la línea de manguera salga con espuma en la proporción correcta.",
|
||||||
|
lugares: [
|
||||||
|
"Sistema fijo contra incendio a base de agua",
|
||||||
|
"Brigada industrial con red hidráulica",
|
||||||
|
"Central de bomberos",
|
||||||
|
"Planta con bomba y toma de agua",
|
||||||
|
],
|
||||||
|
cuandoNo: [
|
||||||
|
{
|
||||||
|
caso: "No tienes red hidráulica, bomba ni toma de agua a presión",
|
||||||
|
porque:
|
||||||
|
"La fuente indica que funciona únicamente con el flujo de agua. Sin ese flujo no hay dosificación posible.",
|
||||||
|
hacia: "enforcer-60",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
caso: "Buscas un equipo que una persona pueda descolgar y accionar",
|
||||||
|
porque: "Esto es parte de una instalación, no un equipo de mano.",
|
||||||
|
hacia: "enforcer-12-litros",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
mensaje:
|
||||||
|
"Me interesa cotizar este equipo: {n}. La red hidráulica es de (qué caudal o diámetro, si lo sabes) y la aplicación es en (dónde).",
|
||||||
|
},
|
||||||
|
|
||||||
|
"proteccion-civil": {
|
||||||
|
protege:
|
||||||
|
"No apaga: avisa. Recibe el alertamiento sísmico y lo distribuye al audio del inmueble para que el personal empiece a moverse antes de que se sienta el movimiento.",
|
||||||
|
lugares: [
|
||||||
|
"Oficina y edificio corporativo",
|
||||||
|
"Escuela, hospital y auditorio",
|
||||||
|
"Planta y centro de distribución",
|
||||||
|
"Comercio con afluencia de público",
|
||||||
|
],
|
||||||
|
cuandoNo: [
|
||||||
|
{
|
||||||
|
caso: "Lo que te falta es equipo de extinción para la visita de verificación",
|
||||||
|
porque:
|
||||||
|
"Un receptor de alertamiento no sustituye extintores ni plan de emergencia: son requisitos distintos y se revisan por separado.",
|
||||||
|
hacia: "enforcer-9-litros",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
mensaje:
|
||||||
|
"Me interesa cotizar este equipo: {n}. El inmueble es (qué tipo) en (qué ciudad) y somos (cuántas) personas.",
|
||||||
|
},
|
||||||
|
|
||||||
|
robots: {
|
||||||
|
protege:
|
||||||
|
"Vigila un área amplia y abierta: detecta el inicio del fuego, orienta el cañón y descarga sobre el punto. Es infraestructura —pide agua, bomba, obra e instalación eléctrica—, no un equipo que se compra y se cuelga.",
|
||||||
|
lugares: [
|
||||||
|
"Nave industrial y bodega de gran altura",
|
||||||
|
"Museo y auditorio",
|
||||||
|
"Comercio de gran superficie",
|
||||||
|
"Almacén con racks altos",
|
||||||
|
],
|
||||||
|
cuandoNo: [
|
||||||
|
{
|
||||||
|
caso: "El espacio es reducido o no hay red hidráulica con bomba",
|
||||||
|
porque:
|
||||||
|
"Sin agua a presión el cañón no tiene con qué descargar, y en un área chica la instalación no se justifica.",
|
||||||
|
hacia: "esfera-rociadora-automatica",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
caso: "Necesitas cumplir este mes con el equipo mínimo",
|
||||||
|
porque:
|
||||||
|
"Un sistema con obra no se resuelve en una semana; el equipo portátil sí, y no estorba si después instalas el sistema.",
|
||||||
|
hacia: "enforcer-12-litros",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
mensaje:
|
||||||
|
"Me interesa cotizar este equipo: {n}. La instalación es (qué tipo de nave o inmueble) de aproximadamente (cuántos) m², y (sí / no) hay red hidráulica con bomba.",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Ajustes por producto
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/** Alternativas que se agregan a las de la categoría, al principio de la lista. */
|
||||||
|
type Ajuste = Partial<Omit<Guia, "cuandoNo">> & {
|
||||||
|
cuandoNo?: Alternativa[];
|
||||||
|
/** Reemplaza las alternativas de la categoría en lugar de sumarse. */
|
||||||
|
reemplazaCuandoNo?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const POR_PRODUCTO: Record<string, Ajuste> = {
|
||||||
|
// --- portátiles: la capacidad cambia el consejo ---
|
||||||
|
"enforcer-1-litro": {
|
||||||
|
protege:
|
||||||
|
"Un fuego que apenas empieza en un espacio reducido: la cocina de casa, la cabina de un vehículo, un cuarto eléctrico chico. Cabe donde no cabe un extintor grande y lo levanta cualquier persona.",
|
||||||
|
cuandoNo: [
|
||||||
|
{
|
||||||
|
caso: "Es para un centro de trabajo y tiene que pasar verificación",
|
||||||
|
porque:
|
||||||
|
"La NOM-002-STPS-2010 dimensiona el equipo por la clasificación de riesgo y la superficie del inmueble, no por el formato más chico. Conviene revisar el área antes de elegir capacidad.",
|
||||||
|
hacia: "enforcer-9-litros",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"enforcer-2-litros": {
|
||||||
|
protege:
|
||||||
|
"Un fuego que apenas empieza en un espacio reducido: cocina, cabina de vehículo, cuarto de servicio. Formato de mano, para tenerlo a un paso del punto de riesgo.",
|
||||||
|
cuandoNo: [
|
||||||
|
{
|
||||||
|
caso: "Es para un centro de trabajo y tiene que pasar verificación",
|
||||||
|
porque:
|
||||||
|
"La NOM-002-STPS-2010 dimensiona el equipo por la clasificación de riesgo y la superficie del inmueble, no por el formato más chico.",
|
||||||
|
hacia: "enforcer-9-litros",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"enforcer-12-litros": {
|
||||||
|
cuandoNo: [
|
||||||
|
{
|
||||||
|
caso: "Quien lo va a operar no puede manejar 12 litros con una mano",
|
||||||
|
porque:
|
||||||
|
"El peso lleno pesa tanto como la capacidad en la decisión: un equipo que no se levanta no se usa cuando hace falta.",
|
||||||
|
hacia: "enforcer-6-litros",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
// --- fijos ---
|
||||||
|
"enforcer-10": {
|
||||||
|
protege:
|
||||||
|
"Un punto de riesgo con líquidos inflamables o material apilado, en taller, almacén o gasolinera. Es un sistema de espuma de aire comprimido: la fuente declara que sus 10 galones de solución producen 200 galones de espuma terminada, y lo presenta en configuración fija o móvil.",
|
||||||
|
},
|
||||||
|
"esfera-sin-detector-de-humo": {
|
||||||
|
protege:
|
||||||
|
"Un punto cerrado que no siempre tiene a alguien enfrente. Se monta arriba del riesgo y la fuente indica que dispara con su detector de temperatura a 68 °C, así que responde con el área vacía.",
|
||||||
|
cuandoNo: [
|
||||||
|
{
|
||||||
|
caso: "Quieres accionarlo tú en el momento en que ves el fuego",
|
||||||
|
porque: "Dispara por temperatura, no a voluntad. Para eso el equipo es de mano.",
|
||||||
|
hacia: "enforcer-9-litros",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"esfera-con-detector-de-humo": {
|
||||||
|
protege:
|
||||||
|
"El mismo punto cerrado sin vigilancia, con detección adelantada: la fuente le atribuye detector de temperatura a 68 °C y además sensor de humo, que empieza a leer antes de que el calor suba.",
|
||||||
|
cuandoNo: [
|
||||||
|
{
|
||||||
|
caso: "Quieres accionarlo tú en el momento en que ves el fuego",
|
||||||
|
porque: "Dispara por sensor, no a voluntad. Para eso el equipo es de mano.",
|
||||||
|
hacia: "enforcer-9-litros",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
// --- móviles ---
|
||||||
|
"enforcer-60": {
|
||||||
|
protege:
|
||||||
|
"Respuesta de patio con volumen: 60 galones de solución que la fuente declara como 1,200 galones de espuma terminada, montados sobre ruedas para llegar a la isla, la plataforma o el frente de trabajo.",
|
||||||
|
},
|
||||||
|
"carretilla-30-litros": {
|
||||||
|
cuandoNo: [
|
||||||
|
{
|
||||||
|
caso: "El patio es grande y el recorrido hasta el punto es largo",
|
||||||
|
porque:
|
||||||
|
"A más superficie, más agente y menos viajes: la capacidad se elige por el recorrido real, no por el precio de lista.",
|
||||||
|
hacia: "carretilla-100-litros",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
// --- rociadores ---
|
||||||
|
"esfera-rociadora-automatica": {
|
||||||
|
protege:
|
||||||
|
"Un área cerrada donde el fuego puede empezar sin testigos. La fuente indica que se activa al detectar temperatura ambiental mayor a 57 o 69 °C —con detección de humo como opción— y declara 9 m² con FireAde 2000 y 12 m² con HFC-236, así que la cobertura se calcula por unidad y no por inmueble.",
|
||||||
|
},
|
||||||
|
|
||||||
|
// --- agentes: cada uno hace algo distinto, y eso hay que decirlo ---
|
||||||
|
"fireade-2000": {
|
||||||
|
protege:
|
||||||
|
"Es el agente, no el equipo. La fuente lo describe como doble agente —espuma AR-AFFF y agente humectante— para integrarse en extintores, carretillas, sistemas fijos y equipo de brigada. Se cotiza por volumen.",
|
||||||
|
cuandoNo: [
|
||||||
|
{
|
||||||
|
caso: "Tu problema es polvo en suspensión, no fuego",
|
||||||
|
porque:
|
||||||
|
"Para eso la línea tiene una formulación distinta, presentada como supresor de polvo y sin clases de fuego atribuidas.",
|
||||||
|
hacia: "trakade",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
caso: "Necesitas limpiar o descontaminar un tanque",
|
||||||
|
porque:
|
||||||
|
"Ese trabajo es de mantenimiento, no de extinción, y lo cubre otra formulación de la línea.",
|
||||||
|
hacia: "vaporade-plus",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"flameade-retardante": {
|
||||||
|
protege:
|
||||||
|
"No apaga: retarda. Se aplica antes del riesgo, con un rocío ligero sobre el tejido o el material, para que tarde más en encender. Es prevención, no respuesta.",
|
||||||
|
lugares: [
|
||||||
|
"Cortinaje, telón y butaquería",
|
||||||
|
"Escenografía, stand y montaje temporal",
|
||||||
|
"Textil y tapicería",
|
||||||
|
"Material de decoración en interiores",
|
||||||
|
],
|
||||||
|
reemplazaCuandoNo: true,
|
||||||
|
cuandoNo: [
|
||||||
|
{
|
||||||
|
caso: "Lo que necesitas es apagar un fuego, no evitar que prenda",
|
||||||
|
porque:
|
||||||
|
"Un retardante no sustituye al equipo de extinción ni cuenta como tal frente a una verificación.",
|
||||||
|
hacia: "enforcer-9-litros",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
caso: "Necesitas el agente para cargar extintores o un sistema",
|
||||||
|
porque:
|
||||||
|
"FlameAde y FireAde 2000 no son intercambiables: uno retarda la ignición, el otro está presentado para extinguir.",
|
||||||
|
hacia: "fireade-2000",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
mensaje:
|
||||||
|
"Me interesa cotizar este producto: {n}. Lo necesito para tratar (qué material) en (dónde), y son aproximadamente (cuántos) m² o piezas.",
|
||||||
|
},
|
||||||
|
trakade: {
|
||||||
|
protege:
|
||||||
|
"No es equipo contra incendio: es supresión de polvo. La fuente lo describe humectando y recubriendo la partícula para que no se levante, en camino de terracería, fuentes de polvo de carbón y polvo suspendido.",
|
||||||
|
lugares: [
|
||||||
|
"Camino de terracería y patio de maniobras",
|
||||||
|
"Mina y beneficio de minerales",
|
||||||
|
"Punto de transferencia y banda transportadora",
|
||||||
|
"Patio de almacenamiento a granel",
|
||||||
|
],
|
||||||
|
reemplazaCuandoNo: true,
|
||||||
|
cuandoNo: [
|
||||||
|
{
|
||||||
|
caso: "Tu problema es fuego, no polvo",
|
||||||
|
porque:
|
||||||
|
"La fuente presenta TrakAde como supresor de polvo, así que no le atribuimos clases de fuego ni capacidad de extinción.",
|
||||||
|
hacia: "fireade-2000",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
caso: "Necesitas limpiar o descontaminar un tanque",
|
||||||
|
porque: "Ese uso lo cubre otra formulación de la línea.",
|
||||||
|
hacia: "vaporade-plus",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
mensaje:
|
||||||
|
"Me interesa cotizar este producto: {n}. El polvo se genera en (dónde) y la superficie o el volumen a tratar es (cuánto).",
|
||||||
|
},
|
||||||
|
"vaporade-plus": {
|
||||||
|
protege:
|
||||||
|
"Limpieza y descontaminación de tanques: control de vapor, reducción de lodo y tratamiento de residuo de hidrocarburo. Es un producto de mantenimiento, no de extinción.",
|
||||||
|
lugares: [
|
||||||
|
"Tanque de almacenamiento y fondo de tanque",
|
||||||
|
"Terminal y patio de hidrocarburos",
|
||||||
|
"Equipo con residuo aceitoso",
|
||||||
|
"Lavado de superficie externa a alta presión",
|
||||||
|
],
|
||||||
|
reemplazaCuandoNo: true,
|
||||||
|
cuandoNo: [
|
||||||
|
{
|
||||||
|
caso: "Lo que necesitas es apagar fuego",
|
||||||
|
porque:
|
||||||
|
"La fuente no presenta VaporAde Plus como agente extintor; por eso no le atribuimos clases de fuego.",
|
||||||
|
hacia: "fireade-2000",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
caso: "El problema es polvo en suspensión",
|
||||||
|
porque: "Ese uso lo cubre otra formulación de la línea.",
|
||||||
|
hacia: "trakade",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
mensaje:
|
||||||
|
"Me interesa cotizar este producto: {n}. El trabajo es en (qué tanque o equipo), con residuo de (qué producto), y el volumen es (cuánto).",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/** Guía de respaldo, para que una categoría nueva nunca deje la ficha vacía. */
|
||||||
|
const RESPALDO: Guia = {
|
||||||
|
protege:
|
||||||
|
"Un riesgo de incendio que conviene revisar en sitio antes de decidir el equipo. Con la descripción de tu instalación te decimos si este es el producto correcto o si conviene otro.",
|
||||||
|
lugares: [],
|
||||||
|
cuandoNo: [],
|
||||||
|
mensaje: "Me interesa cotizar este equipo: {n}. Lo necesito para (qué instalación) en (dónde).",
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Guía de un producto: la base de su categoría con los ajustes del modelo.
|
||||||
|
* Las alternativas del producto van primero porque son las más específicas.
|
||||||
|
*/
|
||||||
|
export function guiaDe(producto: Producto): Guia {
|
||||||
|
const base = POR_CATEGORIA[producto.categoria] ?? RESPALDO;
|
||||||
|
const ajuste = POR_PRODUCTO[producto.slug];
|
||||||
|
|
||||||
|
if (!ajuste) return base;
|
||||||
|
|
||||||
|
const cuandoNo = ajuste.reemplazaCuandoNo
|
||||||
|
? (ajuste.cuandoNo ?? [])
|
||||||
|
: [...(ajuste.cuandoNo ?? []), ...base.cuandoNo];
|
||||||
|
|
||||||
|
return {
|
||||||
|
protege: ajuste.protege ?? base.protege,
|
||||||
|
lugares: ajuste.lugares ?? base.lugares,
|
||||||
|
cuandoNo,
|
||||||
|
mensaje: ajuste.mensaje ?? base.mensaje,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mensaje ya redactado para el formulario. Es lo que hace que "Cotizar este
|
||||||
|
* equipo" deje el campo listo: el visitante solo sustituye los paréntesis.
|
||||||
|
*/
|
||||||
|
export function mensajeInicialDe(producto: Producto): string {
|
||||||
|
return guiaDe(producto).mensaje.replace("{n}", producto.nombre);
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import type { Producto } from "@/data/catalogo";
|
||||||
|
import { INDUSTRIAS } from "@/data/industrias";
|
||||||
|
import { FOTOS } from "@/components/site/HeroeLanding";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Presentación del héroe y contexto deducible de la ficha.
|
||||||
|
*
|
||||||
|
* Hay veintidós fichas y once fotografías en el archivo de FSPM. Para que dos
|
||||||
|
* fichas no se vean idénticas se combinan dos cosas: una fotografía por
|
||||||
|
* categoría y un recorte que se desplaza con el `orden` del producto. El
|
||||||
|
* desplazamiento es horizontal y acotado, así que el sujeto de la fotografía
|
||||||
|
* nunca se pierde.
|
||||||
|
*/
|
||||||
|
|
||||||
|
type Encabezado = { foto: string; enfoqueBase: number; enfoqueY: string };
|
||||||
|
|
||||||
|
const POR_CATEGORIA: Record<string, Encabezado> = {
|
||||||
|
"extintores-portatiles": { foto: FOTOS.practica, enfoqueBase: 40, enfoqueY: "48%" },
|
||||||
|
"extintores-fijos": { foto: FOTOS.planta, enfoqueBase: 44, enfoqueY: "38%" },
|
||||||
|
"productos-moviles": { foto: FOTOS.galeria, enfoqueBase: 42, enfoqueY: "42%" },
|
||||||
|
rociadores: { foto: FOTOS.portadaGaleria, enfoqueBase: 50, enfoqueY: "32%" },
|
||||||
|
"agentes-extinguidores": { foto: FOTOS.bombero, enfoqueBase: 54, enfoqueY: "36%" },
|
||||||
|
firemiks: { foto: FOTOS.publicaciones, enfoqueBase: 50, enfoqueY: "45%" },
|
||||||
|
"proteccion-civil": { foto: FOTOS.proteccionCivil, enfoqueBase: 50, enfoqueY: "40%" },
|
||||||
|
robots: { foto: FOTOS.normas, enfoqueBase: 46, enfoqueY: "40%" },
|
||||||
|
};
|
||||||
|
|
||||||
|
const RESPALDO: Encabezado = { foto: FOTOS.practica, enfoqueBase: 50, enfoqueY: "45%" };
|
||||||
|
|
||||||
|
export function encabezadoDe(producto: Producto): { foto: string; enfoque: string } {
|
||||||
|
const base = POR_CATEGORIA[producto.categoria] ?? RESPALDO;
|
||||||
|
// el orden arranca en 1: el primer producto queda en el enfoque base
|
||||||
|
const x = Math.min(78, Math.max(22, base.enfoqueBase + (producto.orden - 1) * 5));
|
||||||
|
return { foto: base.foto, enfoque: `${x}% ${base.enfoqueY}` };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Industria del lead deducida de la categoría del producto.
|
||||||
|
*
|
||||||
|
* Se deduce **solo** cuando la correspondencia es inequívoca en los datos: si
|
||||||
|
* una sola industria de `INDUSTRIAS` declara esa categoría, el lead se
|
||||||
|
* clasifica con ella. Si la categoría aparece en varias —los portátiles
|
||||||
|
* aparecen en tres—, no se adivina: el vendedor lo confirma en la llamada.
|
||||||
|
*/
|
||||||
|
export function industriaDeCategoria(categoriaSlug: string): string | undefined {
|
||||||
|
const candidatas = INDUSTRIAS.filter((i) => i.categorias.includes(categoriaSlug));
|
||||||
|
return candidatas.length === 1 ? candidatas[0].slug : undefined;
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
import { TarjetaEscenario } from "@/components/marca/IconoEscenario";
|
||||||
|
import { Revelar } from "@/components/site/Revelar";
|
||||||
|
import type { Escenario } from "@/data/escenarios";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BANDA DE ESCENARIOS — la capa práctica, reutilizable en las páginas de
|
||||||
|
* soluciones.
|
||||||
|
*
|
||||||
|
* En `/soluciones` publica los trece escenarios: cuatro de ellos no encajan en
|
||||||
|
* ninguna de las cinco rutas por industria y sin esta banda quedarían sin
|
||||||
|
* entrada propia. En `/soluciones/[industria]` publica solo los del segmento,
|
||||||
|
* con el riesgo explicado, porque ahí sirve para confirmar que el visitante
|
||||||
|
* está en la ruta correcta antes de leer el paquete.
|
||||||
|
*
|
||||||
|
* Cada tarjeta enlaza al catálogo filtrado por escenario. No es un llamado
|
||||||
|
* secundario ni un adorno: en esta banda las tarjetas son el llamado primario
|
||||||
|
* de la sección.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const RETICULA = {
|
||||||
|
1: "max-w-[42rem] sm:grid-cols-1",
|
||||||
|
2: "sm:grid-cols-2",
|
||||||
|
3: "sm:grid-cols-2 lg:grid-cols-3",
|
||||||
|
4: "sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
escenarios: Escenario[];
|
||||||
|
/** Etiqueta corta en mono. */
|
||||||
|
eyebrow: string;
|
||||||
|
/** El `<h2>` de la sección. Descriptivo, con las palabras que se buscan. */
|
||||||
|
titulo: string;
|
||||||
|
/** Primera frase para cualquiera, segunda con el dato técnico. */
|
||||||
|
descripcion: string;
|
||||||
|
/** Mostrar el párrafo de riesgo de cada escenario. */
|
||||||
|
conRiesgo?: boolean;
|
||||||
|
/** Sobre `bg-paper`. */
|
||||||
|
claro?: boolean;
|
||||||
|
/** Id para anclas. */
|
||||||
|
id?: string;
|
||||||
|
/** Columnas máximas de la retícula. */
|
||||||
|
columnas?: 3 | 4;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function BandaEscenarios({
|
||||||
|
escenarios,
|
||||||
|
eyebrow,
|
||||||
|
titulo,
|
||||||
|
descripcion,
|
||||||
|
conRiesgo = false,
|
||||||
|
claro = false,
|
||||||
|
id,
|
||||||
|
columnas = 4,
|
||||||
|
}: Props) {
|
||||||
|
if (escenarios.length === 0) return null;
|
||||||
|
|
||||||
|
const idTitulo = `${id ?? "escenarios"}-titulo`;
|
||||||
|
const ordenados = [...escenarios].sort((a, b) => a.orden - b.orden);
|
||||||
|
// Con una o dos tarjetas, una retícula de cuatro columnas las deja
|
||||||
|
// flotando a un tercio de ancho. El tope de columnas nunca excede el
|
||||||
|
// número de tarjetas.
|
||||||
|
const reticula = Math.min(columnas, Math.max(ordenados.length, 1)) as 1 | 2 | 3 | 4;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
id={id}
|
||||||
|
className={[
|
||||||
|
"scroll-mt-24 border-y",
|
||||||
|
claro ? "border-rule-lite bg-paper text-deep" : "border-rule bg-ink-2",
|
||||||
|
].join(" ")}
|
||||||
|
aria-labelledby={idTitulo}
|
||||||
|
>
|
||||||
|
<div className="shell py-14 sm:py-18">
|
||||||
|
<Revelar className="max-w-[46rem]">
|
||||||
|
<p className="eyebrow">{eyebrow}</p>
|
||||||
|
<h2
|
||||||
|
id={idTitulo}
|
||||||
|
className={[
|
||||||
|
"mt-4 font-display uppercase tracking-[-0.02em]",
|
||||||
|
claro ? "text-deep" : "text-bright",
|
||||||
|
].join(" ")}
|
||||||
|
style={{ fontSize: "clamp(1.5rem, 3.4vw, 2.2rem)" }}
|
||||||
|
>
|
||||||
|
{titulo}
|
||||||
|
</h2>
|
||||||
|
<p
|
||||||
|
className={[
|
||||||
|
"mt-4 text-base leading-relaxed",
|
||||||
|
claro ? "text-deep-muted" : "text-muted",
|
||||||
|
].join(" ")}
|
||||||
|
>
|
||||||
|
{descripcion}
|
||||||
|
</p>
|
||||||
|
</Revelar>
|
||||||
|
|
||||||
|
<ul className={["mt-9 grid gap-4", RETICULA[reticula]].join(" ")}>
|
||||||
|
{ordenados.map((escenario, i) => (
|
||||||
|
<Revelar as="li" key={escenario.slug} delay={(i % 4) * 60} className="h-full">
|
||||||
|
<TarjetaEscenario
|
||||||
|
escenario={escenario}
|
||||||
|
href={`/catalogo?escenario=${escenario.slug}`}
|
||||||
|
claro={claro}
|
||||||
|
conRiesgo={conRiesgo}
|
||||||
|
/>
|
||||||
|
</Revelar>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,18 +1,28 @@
|
|||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { PastillaEscenario } from "@/components/marca/IconoEscenario";
|
||||||
import { RielClases } from "@/components/marca/RielClases";
|
import { RielClases } from "@/components/marca/RielClases";
|
||||||
import { Revelar } from "@/components/site/Revelar";
|
import { Revelar } from "@/components/site/Revelar";
|
||||||
import { getCategoria } from "@/data/catalogo";
|
import { getCategoria } from "@/data/catalogo";
|
||||||
import { serializeClases } from "@/data/clases-fuego";
|
import { serializeClases } from "@/data/clases-fuego";
|
||||||
|
import { ESCENARIOS } from "@/data/escenarios";
|
||||||
import type { Industria } from "@/data/industrias";
|
import type { Industria } from "@/data/industrias";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fila del índice de soluciones. A diferencia de la tarjeta del inicio, aquí
|
* Fila del índice de soluciones. A diferencia de la tarjeta del inicio, aquí
|
||||||
* cabe el contexto completo y las categorías de producto asociadas, para que el
|
* cabe el contexto completo, los lugares concretos que cubre la ruta y las
|
||||||
* visitante pueda comparar los cinco segmentos sin entrar a cada uno.
|
* categorías de producto asociadas, para que el visitante pueda comparar los
|
||||||
|
* cinco segmentos sin entrar a cada uno.
|
||||||
*
|
*
|
||||||
* El número de prioridad viene de `industria.prioridad`: es el orden que fijó
|
* Las dos capas conviven en la misma fila y cada una hace un solo trabajo:
|
||||||
* el estudio de mercado, no la posición en la lista.
|
* `PastillaEscenario` dice el lugar reconocible —"gasolinera", "flotas y equipo
|
||||||
|
* pesado"— y `RielClases` dice la especificación. Quien reconoce el lugar no
|
||||||
|
* necesita la clase para decidir entrar; quien busca la clase no tiene que
|
||||||
|
* leer el párrafo.
|
||||||
|
*
|
||||||
|
* El orden de las filas sigue `industria.prioridad`, pero el número ya no se
|
||||||
|
* rotula: era información comercial interna que en público se lee como un
|
||||||
|
* ranking del cliente.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export function FilaRuta({ industria }: { industria: Industria }) {
|
export function FilaRuta({ industria }: { industria: Industria }) {
|
||||||
@@ -20,6 +30,10 @@ export function FilaRuta({ industria }: { industria: Industria }) {
|
|||||||
.map(getCategoria)
|
.map(getCategoria)
|
||||||
.filter((c): c is NonNullable<ReturnType<typeof getCategoria>> => Boolean(c));
|
.filter((c): c is NonNullable<ReturnType<typeof getCategoria>> => Boolean(c));
|
||||||
|
|
||||||
|
const escenarios = ESCENARIOS.filter((e) => e.industria === industria.slug).sort(
|
||||||
|
(a, b) => a.orden - b.orden,
|
||||||
|
);
|
||||||
|
|
||||||
const destino = `/soluciones/${industria.slug}?clases=${serializeClases(industria.clases)}`;
|
const destino = `/soluciones/${industria.slug}?clases=${serializeClases(industria.clases)}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -57,26 +71,35 @@ export function FilaRuta({ industria }: { industria: Industria }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="lg:col-span-8">
|
<div className="lg:col-span-8">
|
||||||
<p className="flex flex-wrap items-center gap-x-3 gap-y-1 font-mono text-[0.625rem] uppercase tracking-[0.16em] text-faint">
|
<p className="font-mono text-[0.625rem] uppercase tracking-[0.16em] text-faint">
|
||||||
<span className="text-signal" title="Prioridad comercial según el estudio de mercado">
|
{industria.nombreLargo}
|
||||||
Prioridad {String(industria.prioridad).padStart(2, "0")}
|
|
||||||
</span>
|
|
||||||
<span aria-hidden="true" className="text-rule-2">
|
|
||||||
/
|
|
||||||
</span>
|
|
||||||
<span>{industria.nombreLargo}</span>
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2 className="mt-3 font-display text-xl uppercase leading-tight tracking-[-0.015em] text-bright sm:text-2xl">
|
<h3 className="mt-3 font-display text-xl uppercase leading-tight tracking-[-0.015em] text-bright sm:text-2xl">
|
||||||
<Link href={destino} className="transition-colors hover:text-signal-hi">
|
<Link href={destino} className="transition-colors hover:text-signal-hi">
|
||||||
{industria.dolor}
|
{industria.dolor}
|
||||||
</Link>
|
</Link>
|
||||||
</h2>
|
</h3>
|
||||||
|
|
||||||
<p className="mt-4 max-w-[46rem] text-sm leading-relaxed text-muted sm:text-base">
|
<p className="mt-4 max-w-[46rem] text-sm leading-relaxed text-muted sm:text-base">
|
||||||
{industria.contexto}
|
{industria.contexto}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
{escenarios.length > 0 ? (
|
||||||
|
<div className="mt-6">
|
||||||
|
<p className="font-mono text-[0.625rem] uppercase tracking-[0.14em] text-faint">
|
||||||
|
Lugares que cubre esta ruta
|
||||||
|
</p>
|
||||||
|
<ul className="mt-2.5 flex flex-wrap gap-2">
|
||||||
|
{escenarios.map((escenario) => (
|
||||||
|
<li key={escenario.slug}>
|
||||||
|
<PastillaEscenario escenario={escenario} />
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<div className="mt-6 grid gap-6 sm:grid-cols-2">
|
<div className="mt-6 grid gap-6 sm:grid-cols-2">
|
||||||
<div>
|
<div>
|
||||||
<p className="font-mono text-[0.625rem] uppercase tracking-[0.14em] text-faint">
|
<p className="font-mono text-[0.625rem] uppercase tracking-[0.14em] text-faint">
|
||||||
@@ -87,7 +110,7 @@ export function FilaRuta({ industria }: { industria: Industria }) {
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p className="font-mono text-[0.625rem] uppercase tracking-[0.14em] text-faint">
|
<p className="font-mono text-[0.625rem] uppercase tracking-[0.14em] text-faint">
|
||||||
Categorías asociadas
|
Equipo que se usa
|
||||||
</p>
|
</p>
|
||||||
<ul className="mt-2.5 flex flex-wrap gap-1.5">
|
<ul className="mt-2.5 flex flex-wrap gap-1.5">
|
||||||
{categorias.map((c) => (
|
{categorias.map((c) => (
|
||||||
@@ -104,8 +127,8 @@ export function FilaRuta({ industria }: { industria: Industria }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Link href={destino} className="btn btn-ghost btn-sm mt-7">
|
<Link href={destino} className="btn btn-ghost btn-sm mt-7" style={{ WebkitTapHighlightColor: "transparent" }}>
|
||||||
Ver la ruta de {industria.nombre}
|
Ver la ruta de {industria.nombre.toLowerCase()}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,225 @@
|
|||||||
|
/**
|
||||||
|
* ESCENARIOS — la capa práctica del sitio.
|
||||||
|
*
|
||||||
|
* El catálogo técnico está organizado por clase de fuego, capacidad y agente.
|
||||||
|
* Eso sirve a un jefe de seguridad industrial y sirve para posicionar en
|
||||||
|
* búsqueda, pero deja fuera a la mitad de los compradores: el dueño de una
|
||||||
|
* gasolinera o la administradora de un hotel no llegan pensando "necesito
|
||||||
|
* clase B", llegan pensando "tengo una gasolinera".
|
||||||
|
*
|
||||||
|
* Estos escenarios son ese puente. Cada uno usa la iconografía del sitio
|
||||||
|
* original de FSPM —que ya estaba resolviendo este problema— y traduce el
|
||||||
|
* lugar reconocible a las clases de fuego y los productos que le corresponden.
|
||||||
|
*
|
||||||
|
* Los iconos son archivos reales de `public/media/iconos/`. No inventes rutas.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { ClaseFuego } from "./clases-fuego";
|
||||||
|
|
||||||
|
export type Escenario = {
|
||||||
|
slug: string;
|
||||||
|
/** Como lo diría el visitante, no como lo diría un ingeniero. */
|
||||||
|
nombre: string;
|
||||||
|
/**
|
||||||
|
* Icono del set original de FSPM. `null` cuando el set no tiene uno que
|
||||||
|
* corresponda: es preferible un marcador tipográfico a un icono equivocado.
|
||||||
|
* Si el cliente entrega el archivo faltante, basta ponerlo aquí.
|
||||||
|
*/
|
||||||
|
icono: string | null;
|
||||||
|
/** Una línea en lenguaje llano: qué se está protegiendo aquí. */
|
||||||
|
enLlano: string;
|
||||||
|
/** El riesgo concreto de este lugar, sin tecnicismos. */
|
||||||
|
riesgo: string;
|
||||||
|
clases: ClaseFuego[];
|
||||||
|
/** Slugs de `CATEGORIAS` relevantes, en orden de pertinencia. */
|
||||||
|
categorias: string[];
|
||||||
|
/** Slug de `INDUSTRIAS` al que pertenece, si aplica. */
|
||||||
|
industria: string | null;
|
||||||
|
orden: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ESCENARIOS: Escenario[] = [
|
||||||
|
{
|
||||||
|
slug: "industria",
|
||||||
|
nombre: "Planta industrial",
|
||||||
|
icono: "/media/iconos/icono-industria.png",
|
||||||
|
enLlano: "Naves de producción, maquinaria, tableros eléctricos y almacén de materia prima.",
|
||||||
|
riesgo:
|
||||||
|
"Conviven material combustible, equipo energizado y a veces líquidos inflamables en el mismo turno. Un paro de línea cuesta más que el equipo perdido.",
|
||||||
|
clases: ["A", "B", "C"],
|
||||||
|
categorias: ["extintores-portatiles", "productos-moviles", "extintores-fijos", "rociadores", "firemiks"],
|
||||||
|
industria: "industria-energia",
|
||||||
|
orden: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: "gasolineras",
|
||||||
|
nombre: "Gasolinera",
|
||||||
|
icono: "/media/iconos/icono-gasolineras.png",
|
||||||
|
enLlano: "Islas de despacho, tanques, patio de maniobras y personal de turno.",
|
||||||
|
riesgo:
|
||||||
|
"Combustible a presión con público a pocos metros. Quien responde es el despachador, así que el equipo tiene que funcionar en el primer intento.",
|
||||||
|
clases: ["A", "B", "C"],
|
||||||
|
categorias: ["productos-moviles", "extintores-portatiles", "agentes-extinguidores"],
|
||||||
|
industria: "gasolineras-flotas",
|
||||||
|
orden: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: "bodegas",
|
||||||
|
nombre: "Bodega o centro de distribución",
|
||||||
|
icono: "/media/iconos/iconos-aeropuertos.png",
|
||||||
|
enLlano: "Racks altos, tarima de madera, cartón, plástico y montacargas eléctricos.",
|
||||||
|
riesgo:
|
||||||
|
"Mucha superficie y carga combustible apilada en altura. El riesgo cambia de pasillo a pasillo y el equipo casi nunca está donde arranca el fuego.",
|
||||||
|
clases: ["A", "B", "C"],
|
||||||
|
categorias: ["extintores-portatiles", "productos-moviles", "extintores-fijos", "rociadores"],
|
||||||
|
industria: "logistica-bodegas",
|
||||||
|
orden: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: "cocinas-hoteles",
|
||||||
|
nombre: "Cocina industrial u hotel",
|
||||||
|
icono: null,
|
||||||
|
enLlano: "Freidoras, planchas, campanas, ductos de extracción y comedor.",
|
||||||
|
riesgo:
|
||||||
|
"El aceite a temperatura de fritura es fuego clase K, y casi nada del equipo genérico colgado en la pared está calificado para eso. El agua proyecta la grasa encendida sobre el operador.",
|
||||||
|
clases: ["A", "B", "C", "K"],
|
||||||
|
categorias: ["rociadores", "extintores-portatiles", "extintores-fijos"],
|
||||||
|
industria: "cocinas-hoteles",
|
||||||
|
orden: 4,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: "oficinas",
|
||||||
|
nombre: "Oficina o comercio",
|
||||||
|
icono: "/media/iconos/icono-oficina.png",
|
||||||
|
enLlano: "Equipo de cómputo, archivo, mobiliario y centro de carga.",
|
||||||
|
riesgo:
|
||||||
|
"Riesgo moderado, pero la obligación normativa es la misma que en una planta. El error caro es comprar de más por miedo o de menos por precio.",
|
||||||
|
clases: ["A", "C"],
|
||||||
|
categorias: ["extintores-portatiles", "rociadores", "proteccion-civil"],
|
||||||
|
industria: "pymes-oficinas",
|
||||||
|
orden: 5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: "hogar",
|
||||||
|
nombre: "Casa habitación",
|
||||||
|
icono: "/media/iconos/icono-hogar.png",
|
||||||
|
enLlano: "Cocina, instalación eléctrica, garaje y bodega doméstica.",
|
||||||
|
riesgo:
|
||||||
|
"Nadie está entrenado y nadie está de guardia. El equipo tiene que ser operable por cualquier persona de la familia, sin instrucciones.",
|
||||||
|
clases: ["A", "B", "C", "K"],
|
||||||
|
categorias: ["extintores-portatiles", "rociadores"],
|
||||||
|
industria: "pymes-oficinas",
|
||||||
|
orden: 6,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: "minas",
|
||||||
|
nombre: "Mina y beneficio de minerales",
|
||||||
|
icono: "/media/iconos/iconos-minas.png",
|
||||||
|
enLlano: "Maquinaria pesada, bandas transportadoras, diésel y polvo metálico.",
|
||||||
|
riesgo:
|
||||||
|
"Accesos largos, ventilación limitada y presencia de metales combustibles. El agua sobre varios de esos metales reacciona y agrava el incidente.",
|
||||||
|
clases: ["A", "B", "C", "D"],
|
||||||
|
categorias: ["productos-moviles", "agentes-extinguidores", "extintores-fijos", "firemiks"],
|
||||||
|
industria: "industria-energia",
|
||||||
|
orden: 7,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: "flotas-maquinaria",
|
||||||
|
nombre: "Flotas y equipo pesado",
|
||||||
|
icono: "/media/iconos/icono-central-bomberos.png",
|
||||||
|
enLlano: "Autotanques, tractocamiones, montacargas y maquinaria de obra.",
|
||||||
|
riesgo:
|
||||||
|
"El incendio empieza lejos de cualquier instalación fija: en el motor, en la línea hidráulica o en el tanque. La respuesta tiene que viajar con la unidad.",
|
||||||
|
clases: ["A", "B", "C"],
|
||||||
|
categorias: ["productos-moviles", "extintores-portatiles", "agentes-extinguidores"],
|
||||||
|
industria: "gasolineras-flotas",
|
||||||
|
orden: 8,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: "museos-patrimonio",
|
||||||
|
nombre: "Museo y acervo",
|
||||||
|
icono: "/media/iconos/icono-museos.png",
|
||||||
|
enLlano: "Obra, documento, mobiliario histórico y salas con público.",
|
||||||
|
riesgo:
|
||||||
|
"Lo que se protege es irreemplazable, así que el daño del agente importa tanto como su capacidad de extinguir. Un residuo corrosivo arruina lo que el fuego no alcanzó.",
|
||||||
|
clases: ["A", "C"],
|
||||||
|
categorias: ["rociadores", "extintores-portatiles", "agentes-extinguidores"],
|
||||||
|
industria: null,
|
||||||
|
orden: 9,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: "auditorios-afluencia",
|
||||||
|
nombre: "Auditorio y lugar de afluencia",
|
||||||
|
icono: "/media/iconos/icono-auditorios.png",
|
||||||
|
enLlano: "Escenario, butaquería, instalación eléctrica temporal y salidas de emergencia.",
|
||||||
|
riesgo:
|
||||||
|
"El problema no es solo el fuego, es la evacuación simultánea de mucha gente. Aquí la verificación de Protección Civil es estricta y periódica.",
|
||||||
|
clases: ["A", "C"],
|
||||||
|
categorias: ["extintores-portatiles", "rociadores", "proteccion-civil"],
|
||||||
|
industria: null,
|
||||||
|
orden: 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: "exteriores",
|
||||||
|
nombre: "Patios y exteriores",
|
||||||
|
icono: "/media/iconos/icono-exterior.png",
|
||||||
|
enLlano: "Almacenamiento a cielo abierto, contenedores, patios de residuos y estacionamientos.",
|
||||||
|
riesgo:
|
||||||
|
"Sin muros que contengan y con viento a favor, el fuego cambia de dirección. El alcance del chorro pesa más que la capacidad del recipiente.",
|
||||||
|
clases: ["A", "B"],
|
||||||
|
categorias: ["productos-moviles", "agentes-extinguidores", "robots"],
|
||||||
|
industria: "logistica-bodegas",
|
||||||
|
orden: 11,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: "agricultura-forestal",
|
||||||
|
nombre: "Agricultura y forestal",
|
||||||
|
icono: "/media/iconos/icono-agricultura.png",
|
||||||
|
enLlano: "Cultivo, rastrojo, silos, maquinaria agrícola y almacenamiento de combustible.",
|
||||||
|
riesgo:
|
||||||
|
"Superficies enormes sin red hidráulica cerca y material seco que propaga rápido. Sin agua a presión disponible, el agente y su rendimiento por litro deciden el resultado.",
|
||||||
|
clases: ["A", "B"],
|
||||||
|
categorias: ["agentes-extinguidores", "productos-moviles"],
|
||||||
|
industria: null,
|
||||||
|
orden: 12,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: "bosques",
|
||||||
|
nombre: "Bosque y área natural",
|
||||||
|
icono: "/media/iconos/icono-bosques.png",
|
||||||
|
enLlano: "Masa forestal, brecha cortafuego y brigadas de combate.",
|
||||||
|
riesgo:
|
||||||
|
"Acceso difícil, sin infraestructura y con el frente moviéndose. Todo lo que se use tiene que cargarse a mano y no puede contaminar suelo ni escurrimiento.",
|
||||||
|
clases: ["A", "B"],
|
||||||
|
categorias: ["agentes-extinguidores", "productos-moviles"],
|
||||||
|
industria: null,
|
||||||
|
orden: 13,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function getEscenario(slug: string): Escenario | undefined {
|
||||||
|
return ESCENARIOS.find((e) => e.slug === slug);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Escenarios donde una categoría de producto es pertinente. */
|
||||||
|
export function escenariosDeCategoria(categoriaSlug: string): Escenario[] {
|
||||||
|
return ESCENARIOS.filter((e) => e.categorias.includes(categoriaSlug));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Escenarios cuyo perfil de riesgo cubre alguna de estas clases. */
|
||||||
|
export function escenariosPorClases(clases: ClaseFuego[]): Escenario[] {
|
||||||
|
if (clases.length === 0) return ESCENARIOS;
|
||||||
|
return ESCENARIOS.filter((e) => clases.some((c) => e.clases.includes(c)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Icono de característica, no de escenario: acompaña a los agentes de baja
|
||||||
|
* toxicidad. Se usa como insignia, nunca como opción de "elige tu lugar".
|
||||||
|
*/
|
||||||
|
export const ICONO_NO_TOXICO = "/media/iconos/icono-no-toxico.png";
|
||||||
|
|
||||||
|
/** Opciones para selectores. */
|
||||||
|
export const OPCIONES_ESCENARIO = ESCENARIOS.map((e) => ({
|
||||||
|
value: e.slug,
|
||||||
|
label: e.nombre,
|
||||||
|
}));
|
||||||
@@ -18,8 +18,16 @@ export const esquemaLead = z.object({
|
|||||||
puesto: texto(90).optional(),
|
puesto: texto(90).optional(),
|
||||||
ciudad: texto(90).optional(),
|
ciudad: texto(90).optional(),
|
||||||
|
|
||||||
// --- clasificación del riesgo, lo que el propio lead declara ---
|
// --- clasificación del riesgo ---
|
||||||
|
//
|
||||||
|
// El formulario visible ya no pregunta nada de esto: pedir industria, clases
|
||||||
|
// de fuego, superficie y urgencia costaba más conversión de lo que aportaba.
|
||||||
|
// Ahora se deduce del contexto de la página donde ocurrió el envío —la ficha
|
||||||
|
// del producto, la ruta por industria, el escenario elegido— y viaja en
|
||||||
|
// campos ocultos. Menos fricción para el visitante, la misma riqueza para el
|
||||||
|
// análisis comercial.
|
||||||
industria: texto(60).optional(),
|
industria: texto(60).optional(),
|
||||||
|
escenario: texto(60).optional(),
|
||||||
clases: z.array(z.enum(CLASES)).max(5).optional(),
|
clases: z.array(z.enum(CLASES)).max(5).optional(),
|
||||||
superficie: texto(40).optional(),
|
superficie: texto(40).optional(),
|
||||||
urgencia: texto(40).optional(),
|
urgencia: texto(40).optional(),
|
||||||
|
|||||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user