# App React — Canasta Básica (ReactPress)

App React independiente embebida en WordPress mediante ReactPress. Muestra precios de productos de la canasta básica de México consumiendo la API pública del backend Laravel de Pesos y Centavos.

---

## Ubicación

```
reactpress/canasta-basica/       ← Raíz de la app (fuente + config)
├── src/                         ← Código fuente React/TypeScript
├── dist/                        ← Build compilado (generado por npm run build)
├── package.json
├── vite.config.ts
├── tsconfig.json
├── postcss.config.js
├── index.html
├── .env                         ← Variables locales (no commitear)
└── .env.example                 ← Plantilla de variables
```

> **Importante:** La carpeta `dist/` es la que se sube o configura en WordPress/ReactPress. El directorio `node_modules/` y `dist/` están en `.gitignore`.

---

## Requisitos — Windows + FNM + Node 24

Esta app requiere Node.js 24. En Windows con FNM, activar antes de cualquier comando:

```powershell
fnm env --use-on-cd --shell powershell | Out-String | Invoke-Expression
fnm use 24
```

---

## Comandos

Desde `reactpress/canasta-basica/`:

```powershell
# 1. Activar Node 24
fnm env --use-on-cd --shell powershell | Out-String | Invoke-Expression
fnm use 24

# 2. Instalar dependencias (solo primera vez o tras cambios en package.json)
npm install

# 3. Servidor de desarrollo con hot reload
npm run dev
# → http://localhost:5173

# 4. Build de producción
npm run build
# → genera dist/

# 5. Preview del build (sin servidor Laravel activo)
npm run preview
```

---

## Variables de entorno

Crear `reactpress/canasta-basica/.env` basándose en `.env.example`:

| Variable | Local | Producción |
|---|---|---|
| `VITE_API_BASE_URL` | `http://pesos-y-centavos.test` | `https://api.pesosycentavos.com` |

> Las variables deben empezar con `VITE_` para ser accesibles desde el cliente.

**Ejemplo `.env` local:**
```
VITE_API_BASE_URL=http://pesos-y-centavos.test
```

**Ejemplo `.env` producción:**
```
VITE_API_BASE_URL=https://api.pesosycentavos.com
```

---

## Endpoints consumidos

Todos son `GET`, públicos, sin autenticación, sin CSRF, con `Accept: application/json`.

| Endpoint | Descripción |
|---|---|
| `GET /api/basic-basket/filters` | Opciones para los selects (productos + tiendas activos) |
| `GET /api/basic-basket/products` | Listado paginado con filtros |
| `GET /api/basic-basket/products/{slug}` | Detalle de un producto con precios por tienda |

### Parámetros de filtrado del listado

| Parámetro | Descripción |
|---|---|
| `search` | Búsqueda por nombre/slug/categoría |
| `product` | Slug exacto del producto (preferido sobre `category`) |
| `store` | Slug de la tienda |
| `per_page` | Resultados por página (default 12, máx 48) |
| `page` | Número de página |

---

## Estructura de componentes

```
src/
├── main.tsx                    ← Punto de entrada (monta <App /> en #root)
├── App.tsx                     ← Routing por hash, carga filtros y listado
├── vite-env.d.ts               ← Tipos de import.meta.env
├── api/
│   ├── client.ts               ← fetch wrapper (sin credentials, sin CSRF)
│   └── basicBasket.ts          ← fetchFilters(), fetchProducts(), fetchProductDetail()
├── types/
│   └── basicBasket.ts          ← Tipos TypeScript: ProductListItem, StorePrice, etc.
├── utils/
│   └── formatters.ts           ← formatDate(), formatPrice(), productHashUrl()
├── styles/
│   └── index.css               ← Tailwind v4 @import + @theme con colores de marca
└── components/
    ├── ProductFilters.tsx       ← Barra de filtros (búsqueda + selects + botón Filtrar)
    ├── ProductGrid.tsx          ← Grid responsive de cards
    ├── ProductCard.tsx          ← Card individual con imagen, precio, botón Comparar
    ├── Pagination.tsx           ← Paginación anterior/siguiente + páginas numeradas
    ├── ProductDetail.tsx        ← Vista detalle: imagen grande + lista de tiendas
    ├── StorePriceCard.tsx       ← Card horizontal de tienda con precio y botón
    ├── LoadingState.tsx         ← Spinner accesible
    ├── ErrorState.tsx           ← Mensaje de error con botón Reintentar
    └── EmptyState.tsx           ← Estado vacío con botón Limpiar filtros
```

---

## Colores de marca

Definidos en `src/styles/index.css` con `@theme` de Tailwind v4:

```css
@theme {
  --color-brand-mint: #87afa4;   /* Verde/menta principal */
  --color-brand-dark: #3d3c3a;   /* Gris oscuro */
  --color-surface:    #f4f5f6;   /* Fondo claro */
}
```

Para cambiar colores de marca, editar únicamente esas variables en `index.css`.

---

## Navegación (hash routing)

La app usa hash routing para ser compatible con ReactPress sin configuración adicional en WordPress:

| Hash | Vista |
|---|---|
| `#/` o vacío | Listado con filtros y paginación |
| `#/producto/{slug}` | Detalle/comparativa del producto |

- Al hacer click en "Comparar" en una card, actualiza `window.location.hash` y muestra el detalle.
- El botón "Volver" llama a `history.pushState` para limpiar el hash y regresar al listado.
- El hash se sincroniza con el evento `hashchange`, por lo que funciona con el botón Atrás del navegador.

---

## Botón "Ir al producto"

El botón aparece en `StorePriceCard` dentro de la vista de detalle.

| Condición | Comportamiento |
|---|---|
| `external_url !== null` AND `external_url_type === "store_product"` | Enlace `<a>` activo con `target="_blank" rel="noopener noreferrer"` |
| `external_url === null` o `external_url_type !== "store_product"` | Botón `<button disabled>` con título explicativo |

> El campo `external_url` viene de `store_product_targets.store_product_url` en el backend. No usa `shopping_url` (URL proxy de Google Shopping).

Para configurar la URL de un producto en una tienda específica, editar `targetOverrides()` en `database/seeders/StoreProductTargetSeeder.php` y correr el seeder.

---

## Qué NO construye esta app

Las siguientes secciones pertenecen a WordPress y no deben replicarse aquí:

- ✗ Header / navbar / menú
- ✗ Topbar (redes sociales, login)
- ✗ Logo
- ✗ Hero/banner
- ✗ CTA "Ayúdanos a crear una comunidad informada"
- ✗ Sección de noticias relacionadas
- ✗ Footer

La app inicia directamente con la barra de filtros.

---

## Integración con ReactPress

1. Hacer `npm run build` → genera `reactpress/canasta-basica/dist/`
2. En WordPress con ReactPress instalado, crear o editar una app apuntando a la carpeta `dist/`
3. ReactPress servirá el `index.html` e inyectará el JS/CSS compilado en la página de WordPress

> El `base: './'` en `vite.config.ts` asegura que los assets usen rutas relativas, lo que es necesario para que funcionen correctamente cuando ReactPress los sirve desde WordPress.

### Archivo de build a subir

Subir o sincronizar únicamente el contenido de `dist/`:

```
dist/
├── index.html
└── assets/
    ├── index-[hash].js
    └── index-[hash].css
```

---

## Limitaciones SEO

Esta es una SPA (Single Page Application) renderizada en el cliente. Las implicaciones son:

- Los motores de búsqueda ven el HTML estático de WordPress, no el contenido cargado dinámicamente por React.
- Los precios y nombres de productos **no son indexados** por Google salvo que uses SSR o pre-rendering.
- ReactPress no ofrece SSR nativo.
- Para SEO de precios, complementar con datos estructurados (`JSON-LD`) en el template de WordPress o mediante un plugin de WordPress.

---

## Pruebas manuales (checklist)

Antes de deploy, verificar:

- [ ] **Carga listado:** abre la app en `localhost:5173`, se muestran las cards de productos.
- [ ] **Filtro por texto:** escribe en "Buscar…" y click en "Filtrar", el grid se actualiza.
- [ ] **Filtro por producto:** selecciona un producto del select, click "Filtrar", muestra solo ese producto.
- [ ] **Filtro por tienda:** selecciona una tienda, click "Filtrar", muestra solo productos de esa tienda.
- [ ] **Limpiar filtros:** con filtros activos, aparece botón "✕", al hacer click limpia y recarga.
- [ ] **Paginación:** si hay más de 12 productos, aparecen botones de página.
- [ ] **Abrir detalle:** click en "Comparar" en cualquier card, hash cambia a `#/producto/{slug}`.
- [ ] **Volver al listado:** click en "Volver", hash se limpia, regresa al listado.
- [ ] **Botón Atrás del navegador:** funciona con el hash routing.
- [ ] **Tienda con `external_url`:** en detalle, botón "Ir al producto" es un enlace activo que abre en nueva pestaña.
- [ ] **Tienda sin `external_url`:** botón "Ir al producto" aparece deshabilitado (gris, no clickeable).
- [ ] **Precio histórico:** si `is_price_stale = true`, aparece etiqueta "Último precio" en la card y "Precio histórico (fecha)" en el detalle.
- [ ] **Producto sin precio:** muestra "Precio no disponible" sin romper el layout.
- [ ] **Imagen faltante:** muestra placeholder SVG sin errores.
- [ ] **Botón Comparte:** abre Web Share API en mobile; en desktop copia URL al portapapeles y muestra "¡URL copiada!".
- [ ] **Responsive mobile:** filtros apilados, cards en 1 columna, detalle en columna única.
- [ ] **Responsive tablet:** 2 columnas de cards.
- [ ] **Responsive desktop:** 3 columnas de cards, imagen y tiendas lado a lado en detalle.

---

## Checklist de deploy

1. [ ] Configurar `VITE_API_BASE_URL=https://api.pesosycentavos.com` en `.env`
2. [ ] Ejecutar `npm run build`
3. [ ] Verificar que `dist/` se generó correctamente
4. [ ] Subir/sincronizar `dist/` en WordPress con ReactPress
5. [ ] Verificar que la API CORS admite el dominio de WordPress (ver `config/cors.php`)
6. [ ] Probar la página de WordPress donde está embebida la app
7. [ ] Verificar que las imágenes cargan (URLs absolutas del backend)
8. [ ] Verificar botón "Ir al producto" con una tienda que tenga URL configurada

---

## Archivos backend relacionados

| Archivo | Rol |
|---|---|
| `app/Http/Controllers/BasicBasketProductController.php` | Endpoints `index`, `show`, `filters` |
| `app/Services/BasicBasketPriceService.php` | Queries de productos con filtros |
| `config/cors.php` | CORS: incluye `http://localhost:5173` |
| `routes/api.php` | Rutas de la API pública |
| `docs/basic-basket-public-api.md` | Documentación completa del backend |

