Compare commits
21
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
71c8752eeb | ||
|
|
cda25dea2e | ||
|
|
392a832109 | ||
|
|
a069807960 | ||
|
|
96b08eda01 | ||
|
|
e6c3e0ed8d | ||
|
|
35fdea5df5 | ||
|
|
8d44f2e771 | ||
|
|
ef6dc1f6a8 | ||
|
|
352762b9d2 | ||
|
|
58cc93bb2f | ||
|
|
5eaa94818f | ||
|
|
66a81ad9a3 | ||
|
|
39e1bf61d3 | ||
|
|
b5a1f70d1d | ||
|
|
9d231322b8 | ||
|
|
098ff990a6 | ||
|
|
ff59618a2a | ||
|
|
44e343eca7 | ||
|
|
c77e958f04 | ||
|
|
08fc4174cb |
No files matched your search
@@ -12,6 +12,12 @@ import { createEpaycoClient, type EpaycoEnv } from "../../../../lib/epayco";
|
||||
// contra el sandbox real en esta sesion -- se prueban los nombres de
|
||||
// campo mas probables (bankCode/bankName y variantes) y se degrada a
|
||||
// lista vacia si no matchea nada, en vez de romper el checkout entero.
|
||||
//
|
||||
// Verificado en vivo despues (navegador real, Playwright): el sandbox
|
||||
// real de ePayco incluye un primer elemento placeholder
|
||||
// (bankCode "0", "A continuación seleccione su banco...") mezclado con
|
||||
// los bancos reales -- ningun banco PSE real tiene codigo "0", asi que
|
||||
// se filtra explicitamente en vez de mostrarlo como opcion seleccionable.
|
||||
const CACHE_TTL_MS = 6 * 60 * 60 * 1000;
|
||||
let cache: { banks: Array<{ pseCode: string; description: string }>; fetchedAt: number } | null = null;
|
||||
|
||||
@@ -42,7 +48,7 @@ export async function GET(req: MedusaRequest, res: MedusaResponse) {
|
||||
? { pseCode: String(pseCode), description: String(description ?? pseCode) }
|
||||
: null;
|
||||
})
|
||||
.filter((bank): bank is { pseCode: string; description: string } => bank !== null);
|
||||
.filter((bank): bank is { pseCode: string; description: string } => bank !== null && bank.pseCode !== "0");
|
||||
|
||||
cache = { banks, fetchedAt: Date.now() };
|
||||
res.json({ banks });
|
||||
|
||||
@@ -244,9 +244,12 @@ reubicó su contenedor según el breakpoint.
|
||||
## Consecuencias
|
||||
|
||||
- Checkout real de punta a punta (pago + envío calculado) queda
|
||||
desplegado y verificado a nivel API/backend, pero **ambos proveedores
|
||||
siguen en modo sandbox** — no se ha completado ni un pago real ni un
|
||||
envío real todavía. Ver checklist de pruebas abajo.
|
||||
desplegado y verificado con navegador real hasta la redirección al
|
||||
banco de PSE (ver nota de implementación 2026-09-10 abajo), pero
|
||||
**ambos proveedores siguen en modo sandbox** — no se ha completado ni
|
||||
un pago real ni un envío real todavía, y el login bancario de PSE
|
||||
tampoco se completó (falta credencial de prueba). Ver checklist
|
||||
ampliado abajo.
|
||||
- La precisión de la tarifa de envío depende de cargar peso/dimensiones
|
||||
reales por producto — deuda de datos explícita, no de código; candidato
|
||||
natural a extender el importador CSV existente con columnas nuevas.
|
||||
@@ -262,37 +265,163 @@ reubicó su contenedor según el breakpoint.
|
||||
|
||||
## Checklist de pruebas reales desde el storefront (pendiente, no ejecutado en este ADR)
|
||||
|
||||
Todo lo de arriba se verificó a nivel API/backend con llamadas directas
|
||||
o carritos de prueba armados por API — **no** todavía manejando el
|
||||
storefront real como lo haría un comprador. Pendiente, con la próxima
|
||||
sesión que tenga acceso a un navegador real:
|
||||
Todo lo de arriba se verificó primero a nivel API/backend con llamadas
|
||||
directas o carritos de prueba armados por API, sin manejar el
|
||||
storefront real como lo haría un comprador. Esa parte del checklist se
|
||||
cerró el 2026-09-10 (ver nota de implementación abajo) usando un
|
||||
navegador real de verdad, no curl.
|
||||
|
||||
1. **Pago PSE completo:** agregar un producto real al carrito → checkout
|
||||
→ elegir PSE → seleccionar un banco del listado real (sandbox) →
|
||||
confirmar que la redirección al banco ocurre (esto es justo lo que
|
||||
nunca se probó — el nombre del campo de URL de `bank.create()` está
|
||||
manejado a la defensiva y podría no matchear ningún candidato) →
|
||||
volver a `/checkout/pse/return` → confirmar que la orden queda en
|
||||
estado correcto.
|
||||
2. **Envío calculado real:** en el paso de dirección del checkout,
|
||||
confirmar que el precio de envío mostrado ya no es el fijo de
|
||||
$15.000 sino el que devuelve EnvíoClick en vivo para esa dirección
|
||||
(visible también sin completar el pago, solo llegando al paso de
|
||||
shipping).
|
||||
3. **Resiliencia del fallback de envío:** si es posible, forzar una
|
||||
dirección o producto que haga fallar la cotización real (por ejemplo,
|
||||
un DANE code inválido) y confirmar que el checkout sigue avanzando
|
||||
con el precio de respaldo en vez de romperse — el comportamiento
|
||||
documentado en `calculatePrice()`, nunca ejercitado con un fallo real.
|
||||
4. **Banner dinámico:** confirmar en `/` que el carrusel muestra
|
||||
productos reales del catálogo (no las tres imágenes viejas
|
||||
hardcodeadas) y que cada slide lleva al producto correcto al hacer
|
||||
clic.
|
||||
5. **Buscador:** en un viewport mobile real (no solo devtools), confirmar
|
||||
que el buscador ya no se ve recortado y que el autocompletado
|
||||
despliega bien debajo de la fila nueva a todo el ancho.
|
||||
6. **Webhook de ePayco con un pago real de sandbox:** confirmar que,
|
||||
tras completar un pago PSE de prueba desde el banco sandbox, el
|
||||
webhook mueve la orden a completada sin intervención manual (hoy solo
|
||||
se confirmó que el endpoint responde `200` a un `POST` sintético, no
|
||||
con un webhook real disparado por ePayco).
|
||||
### RESUELTO (2026-09-10) — navegador real vía Docker en la NAS
|
||||
|
||||
El shell donde corren los comandos de esta sesión tiene su propia red
|
||||
restringida para `npm` (un proxy interno que rompe algunas descargas de
|
||||
paquetes — no relacionado con la red real del lab). **Docker en la NAS
|
||||
sí sale a internet real, sin ese filtro** — confirmado en vivo
|
||||
(`docker pull` normal, un contenedor de Node golpeando
|
||||
`registry.npmjs.org` sin problema). Se armó un harness reutilizable con
|
||||
la imagen oficial `mcr.microsoft.com/playwright:v1.51.1-jammy`
|
||||
(Chromium real) corrida puntualmente contra `shop.cruzcloud.net`,
|
||||
guardado en `.browser-scratch/` en la raíz del lab (`checks.mjs` +
|
||||
`README.md` con el comando exacto para reactivarlo — no hace falta
|
||||
ningún contenedor corriendo de forma persistente, la imagen y las
|
||||
dependencias ya quedan cacheadas localmente en la NAS).
|
||||
|
||||
**Hallazgo no obvio:** el daemon de Docker de la NAS ve un filesystem
|
||||
distinto al que ve el shell de esta sesión (que corre en su propio
|
||||
sandbox, aunque monte los mismos repos git) — un volumen `-v` apuntando
|
||||
a un path de `/tmp` del shell no tira error pero monta un directorio
|
||||
vacío del lado del contenedor. Hay que usar un path bajo
|
||||
`/media/HDD-Storage/AppData/k3d-lab-v3/lab-cruzcloud/` (montaje real
|
||||
compartido) para que el contenedor vea los archivos de verdad.
|
||||
|
||||
**Resultados, con capturas reales:**
|
||||
|
||||
1. **Pago PSE completo — FUNCIONA.** Carrito → dirección Bogotá →
|
||||
envío calculado → banco PSE (sandbox) → **la redirección real
|
||||
funcionó**: el nombre de campo manejado a la defensiva en
|
||||
`bank.create()`/`initiatePayment` matcheó al primer intento, y el
|
||||
navegador llegó a la página real de autorización de PSE
|
||||
(`registro.desarrollo.pse.com.co`, ambiente de desarrollo/sandbox
|
||||
de PSE Colombia). No se completó el login bancario (requiere
|
||||
credenciales de prueba específicas de PSE que no se tenían a mano) —
|
||||
sigue pendiente confirmar el webhook con un pago sandbox completado
|
||||
de principio a fin (ver checklist nuevo abajo).
|
||||
2. **Envío calculado real — FUNCIONA.** El paso de pago mostró
|
||||
`Envío $8.941` (mismo valor ya visto en la verificación por API de
|
||||
la sección anterior) en vez del fijo de $15.000, para una dirección
|
||||
real de Bogotá.
|
||||
3. **Banner dinámico — funciona, pero destapó un bug real:** un
|
||||
producto marcado `featured: true` pero **sin stock**
|
||||
(`available: false`, "Toy Story 5 - Rex") salía primero en el
|
||||
carrusel con botón "Comprar ahora" hacia una página donde en
|
||||
realidad no se puede comprar. Corregido en el mismo commit que este
|
||||
hallazgo (filtro adicional `&& product.available` en
|
||||
`app/page.tsx`).
|
||||
4. **Buscador mobile — funciona.** El buscador ya no se ve recortado,
|
||||
y el autocompletado despliega bien debajo de la fila nueva a todo el
|
||||
ancho (probado con el viewport de un iPhone 13 real, no devtools).
|
||||
5. **Bug adicional encontrado, no buscado:** el sandbox real de ePayco
|
||||
(`bank.getBanks()`) devuelve un primer elemento placeholder
|
||||
(`bankCode: "0"`, `"A continuación seleccione su banco..."` con el
|
||||
texto mal decodificado — unicode escapado literal) mezclado con los
|
||||
bancos reales, mostrado como una opción más, seleccionable, en el
|
||||
`<select>` del checkout. Ningún banco PSE real tiene código "0" —
|
||||
se filtra explícitamente en `pse-banks/route.ts`.
|
||||
|
||||
Ampliado 2026-09-10 con el resto de la superficie real del storefront
|
||||
(4 páginas + `components/{account,cart,catalog,checkout,home,layout,
|
||||
product,search,wishlist}`), agrupado por riesgo, y ejecutado el mismo
|
||||
día contra `shop.cruzcloud.net` con el harness de `.browser-scratch/`.
|
||||
|
||||
### RESUELTO (2026-09-11) — segunda tanda con navegador real
|
||||
|
||||
Resultados reales, uno por uno (código de cada prueba en
|
||||
`checks.mjs`):
|
||||
|
||||
- **`/checkout` con carrito vacío (ítem 9) — OK.** Muestra "Tu carrito
|
||||
está vacío" con link a `/catalog`, no rompe.
|
||||
- **Fallback "Confirmar por WhatsApp" (ítem 10) — OK.** Abre
|
||||
`api.whatsapp.com/send?phone=...` en pestaña nueva con el mensaje del
|
||||
pedido armado correctamente.
|
||||
- **Filtros de `/catalog` (ítem 11) — OK.** Un filtro de categoría real
|
||||
("Figuras de acción") redujo los productos mostrados de 18 a 4. Nota:
|
||||
estos filtros (categoría/marca/edad dentro de la barra lateral) son
|
||||
**estado de cliente, no reflejado en la URL** — distinto de los
|
||||
filtros por query param (`?category=...`) que sí usa la navegación
|
||||
del header. Ninguno de los dos es un bug, son dos mecanismos
|
||||
distintos coexistiendo; queda como nota para no confundirlos.
|
||||
- **Búsqueda sin resultados (ítem 12) — BUG real encontrado y
|
||||
corregido.** El dropdown de autocompletado no mostraba absolutamente
|
||||
nada (ni "Buscando…" ni ningún mensaje) cuando una búsqueda de 2+
|
||||
caracteres no tenía coincidencias — el usuario se quedaba sin
|
||||
feedback de que su búsqueda había corrido. Corregido en
|
||||
`predictive-search.tsx`: ahora muestra "No encontramos resultados
|
||||
para…" con el botón de buscar en todo el catálogo.
|
||||
- **Slug de producto inválido (ítem 14) — hallazgo, no corregido
|
||||
todavía.** La página sí muestra el contenido correcto de "Este tesoro
|
||||
no existe" (el código ya llama a `notFound()` de
|
||||
`next/navigation`, como corresponde), **pero la respuesta HTTP real
|
||||
es `200`, no `404`** — confirmado independientemente con `curl`, no
|
||||
es un artefacto de Playwright. No se investigó a fondo en qué capa se
|
||||
pierde el status real (Next.js standalone vs. alguna capa de
|
||||
Cloudflare/NPM/Traefik en el medio, ver
|
||||
`incidente-cloudflared-504-quic-2026-08.md` para precedente de
|
||||
problemas de red en esa cadena) — bajo impacto en la experiencia del
|
||||
usuario (el contenido es correcto), pero sí afecta SEO/crawlers.
|
||||
Queda como pendiente de investigar, no de corregir a ciegas.
|
||||
- **Zoom interactivo del producto (ítem 15) — OK.** Hover-zoom y el
|
||||
modal de click-to-zoom siguen funcionando igual tras los cambios
|
||||
recientes al storefront.
|
||||
- **Wishlist/favoritos (ítem 17) — OK.** Agregar a favoritos +
|
||||
recargar la página: persiste correctamente vía `localStorage`.
|
||||
- **Otros viewports, tablet y desktop grande (ítem 19) — OK.**
|
||||
Capturas guardadas, sin problemas de layout visibles en el banner ni
|
||||
el header en esos anchos.
|
||||
- **Página 404 genérica (ítem 20) — OK.** Una ruta que no existe cae en
|
||||
`not-found.tsx` con status HTTP `404` real (a diferencia del caso de
|
||||
producto inválido arriba — la diferencia de comportamiento entre
|
||||
ambos casos es justamente lo que quedó como pendiente de investigar).
|
||||
- **Accesibilidad básica (ítem 21) — OK, parcial.** El buscador es
|
||||
alcanzable navegando solo con `Tab` desde el body, sin mouse. Falta
|
||||
probar el resto del flujo de checkout por teclado.
|
||||
- **Registro/login con email y contraseña (parte del ítem 16) — OK.**
|
||||
Crear cuenta con nombre/apellido/email/contraseña funciona y deja la
|
||||
sesión iniciada. La parte de Google OAuth sigue sin probarse (necesita
|
||||
una cuenta de Google real, no algo que se pueda automatizar sin
|
||||
credenciales).
|
||||
|
||||
### Pendiente — lo que queda sin probar
|
||||
|
||||
3. **Resiliencia del fallback de envío:** forzar una dirección o
|
||||
producto que haga fallar la cotización real de EnvíoClick (ej. un
|
||||
DANE code inválido) y confirmar que el checkout sigue avanzando con
|
||||
el precio de respaldo ($15.000) en vez de romperse — comportamiento
|
||||
documentado en `calculatePrice()`, nunca ejercitado con un fallo
|
||||
real.
|
||||
6. **Webhook de ePayco con un pago real de sandbox completado:**
|
||||
conseguir credenciales de prueba reales de PSE (persona/clave de
|
||||
algún banco del ambiente `desarrollo`), completar el login bancario
|
||||
de punta a punta, y confirmar que el webhook mueve la orden a
|
||||
completada sin intervención manual.
|
||||
7. **Pago cancelado o rechazado en el banco PSE:** confirmar que la
|
||||
orden no queda en un estado ambiguo ("colgada" en
|
||||
`pending_authorization` para siempre).
|
||||
8. **Persona jurídica + NIT** en el formulario de PSE — solo se probó
|
||||
persona natural con cédula.
|
||||
13. **Selección de variantes** (cuando un producto tiene más de una) y
|
||||
que el precio/disponibilidad mostrado cambie según la variante
|
||||
elegida — ningún producto probado hasta ahora tenía más de una
|
||||
variante real.
|
||||
14b. **Investigar por qué `/product/<slug-inválido>` responde HTTP 200**
|
||||
en vez de 404 pese a llamar `notFound()` correctamente (ver
|
||||
hallazgo arriba) — identificar la capa responsable antes de tocar
|
||||
código.
|
||||
16b. **Login con Google (OAuth completo)** incluyendo
|
||||
`/auth/google/callback` — necesita una cuenta de Google real de
|
||||
prueba.
|
||||
18. **Bug conocido de Firefox Android** (ver memoria de proyecto,
|
||||
`known-bug-firefox-android-layout`) — Playwright puede emular
|
||||
Android/Chrome pero no Firefox; hace falta `playwright-firefox` o
|
||||
un dispositivo real.
|
||||
21b. **Accesibilidad del checkout completo** por teclado (no solo el
|
||||
buscador) — tab order y foco visible en los 3 pasos.
|
||||
@@ -25,7 +25,7 @@ spec:
|
||||
runAsNonRoot: false
|
||||
containers:
|
||||
- name: app
|
||||
image: gitea.cruzcloud.net/devops/docs-portal:v1.0.224
|
||||
image: gitea.cruzcloud.net/devops/docs-portal:v1.0.244
|
||||
# El base saca TODAS las capabilities. Probado en vivo y
|
||||
# revertido: con drop: ALL (aunque se agregue NET_BIND_SERVICE
|
||||
# de vuelta) el propio entrypoint de nginx:1.27-alpine falla al
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Mail, MapPin, MessageCircle } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
export const metadata: Metadata = { title: "Contacto | ARI Shopping" };
|
||||
|
||||
// Numero real confirmado en vivo (probando el boton "Confirmar por
|
||||
// WhatsApp" del checkout con navegador real): mismo canal que ya usan
|
||||
// los clientes para confirmar pedidos, consistencia a proposito.
|
||||
const WHATSAPP_URL = "https://wa.me/573016138155";
|
||||
// Correo corporativo nuevo ([email protected] -> reenviado por
|
||||
// Cloudflare Email Routing a la casilla real) -- ver nota de
|
||||
// implementacion en ADR 0010 para el paso manual de DNS pendiente.
|
||||
const CONTACT_EMAIL = "[email protected]";
|
||||
|
||||
export default function ContactoPage() {
|
||||
return (
|
||||
<section className="mx-auto max-w-2xl px-4 py-14 text-center lg:py-20">
|
||||
<span className="text-xs font-black uppercase tracking-[.18em] text-ari-coral">Ayuda</span>
|
||||
<h1 className="mt-3 text-4xl font-black tracking-tight sm:text-5xl">Contacto</h1>
|
||||
<p className="mt-5 text-lg leading-8 text-slate-600">
|
||||
¿Tenés una pregunta sobre tu pedido, un producto o un cambio? Escribinos, te respondemos lo antes posible.
|
||||
</p>
|
||||
|
||||
<div className="mt-10 grid gap-4 sm:grid-cols-2">
|
||||
<a
|
||||
href={WHATSAPP_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex flex-col items-center gap-3 rounded-2xl border border-slate-200 p-6 transition hover:border-ari-marino hover:bg-ari-marino/5"
|
||||
>
|
||||
<MessageCircle className="size-7 text-ari-marino" />
|
||||
<div>
|
||||
<p className="font-black">WhatsApp</p>
|
||||
<p className="mt-1 text-sm text-slate-500">+57 301 613 8155</p>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
href={`mailto:${CONTACT_EMAIL}`}
|
||||
className="flex flex-col items-center gap-3 rounded-2xl border border-slate-200 p-6 transition hover:border-ari-marino hover:bg-ari-marino/5"
|
||||
>
|
||||
<Mail className="size-7 text-ari-marino" />
|
||||
<div>
|
||||
<p className="font-black">Correo</p>
|
||||
<p className="mt-1 text-sm text-slate-500">{CONTACT_EMAIL}</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className="mt-10 flex items-center justify-center gap-2 text-sm text-slate-500">
|
||||
<MapPin className="size-4" />
|
||||
Despachamos desde Bogotá D.C., Colombia
|
||||
</div>
|
||||
|
||||
<Button asChild variant="primary" size="lg" className="mt-10">
|
||||
<a href={WHATSAPP_URL} target="_blank" rel="noopener noreferrer">
|
||||
<MessageCircle className="size-5" />
|
||||
Escribirnos por WhatsApp
|
||||
</a>
|
||||
</Button>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Calculator, Clock, MapPinned, Truck } from "lucide-react";
|
||||
|
||||
export const metadata: Metadata = { title: "Envíos | ARI Shopping" };
|
||||
|
||||
// ADR 0010 §2: el costo de envio se calcula en vivo con EnvioClick (peso +
|
||||
// direccion), no es un valor fijo -- el copy de abajo describe ese
|
||||
// comportamiento real, no un placeholder. Transportadoras reales
|
||||
// confirmadas en una cotizacion en vivo contra la API de EnvioClick
|
||||
// (PIBOX, COORDINADORA, ENVIA, RAPPI, TCC) -- EnvioClick cotiza entre
|
||||
// ellas y el sistema toma automaticamente la mas economica por pedido.
|
||||
const CARRIERS = ["Coordinadora", "Envía", "TCC", "Rappi", "Pibox"];
|
||||
|
||||
export default function EnviosPage() {
|
||||
return (
|
||||
<section className="mx-auto max-w-3xl px-4 py-14 lg:py-20">
|
||||
<span className="text-xs font-black uppercase tracking-[.18em] text-ari-coral">Ayuda</span>
|
||||
<h1 className="mt-3 text-4xl font-black tracking-tight sm:text-5xl">Envíos</h1>
|
||||
<p className="mt-5 text-lg leading-8 text-slate-600">
|
||||
Hacemos envíos a nivel nacional desde nuestra bodega en Bogotá D.C.
|
||||
</p>
|
||||
|
||||
<div className="mt-10 grid gap-6 sm:grid-cols-2">
|
||||
<div className="rounded-2xl border border-slate-200 p-6">
|
||||
<Calculator className="size-6 text-ari-marino" />
|
||||
<h2 className="mt-3 font-black">Costo calculado en tiempo real</h2>
|
||||
<p className="mt-2 text-sm leading-6 text-slate-600">
|
||||
El valor del envío se calcula automáticamente según el peso de tu pedido y la dirección de entrega —
|
||||
lo ves exacto en el checkout antes de pagar, nunca es una tarifa a ciegas.
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-2xl border border-slate-200 p-6">
|
||||
<Truck className="size-6 text-ari-marino" />
|
||||
<h2 className="mt-3 font-black">Transportadoras aliadas</h2>
|
||||
<p className="mt-2 text-sm leading-6 text-slate-600">
|
||||
Cotizamos automáticamente entre {CARRIERS.join(", ")} y asignamos la tarifa más conveniente para cada
|
||||
pedido.
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-2xl border border-slate-200 p-6">
|
||||
<Clock className="size-6 text-ari-marino" />
|
||||
<h2 className="mt-3 font-black">Tiempos de entrega</h2>
|
||||
<p className="mt-2 text-sm leading-6 text-slate-600">
|
||||
Varían según la transportadora asignada y tu ciudad — una vez despachado tu pedido te compartimos el
|
||||
número de guía para hacerle seguimiento.
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-2xl border border-slate-200 p-6">
|
||||
<MapPinned className="size-6 text-ari-marino" />
|
||||
<h2 className="mt-3 font-black">Cobertura</h2>
|
||||
<p className="mt-2 text-sm leading-6 text-slate-600">
|
||||
Envíos a todo el territorio nacional colombiano desde nuestra bodega en Bogotá D.C.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="mt-10 text-sm text-slate-500">
|
||||
¿Tenés dudas puntuales sobre un pedido? Escribinos por{" "}
|
||||
<a href="/contacto" className="font-bold text-ari-marino hover:underline">
|
||||
nuestros canales de contacto
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -21,9 +21,13 @@ export default async function HomePage() {
|
||||
// ADR 0010: el banner toma primero productos destacados (curaduria
|
||||
// explicita del admin via el flag "featured"); si no hay ninguno,
|
||||
// cae a los mismos "tesoros" (nuevos/destacados) de la seccion de
|
||||
// abajo para no repetir la logica de seleccion.
|
||||
const featuredProducts = products.filter((product) => product.featured);
|
||||
const bannerProducts = featuredProducts.length > 0 ? featuredProducts : treasures;
|
||||
// abajo para no repetir la logica de seleccion. Se exige ademas
|
||||
// "available" -- encontrado probando en vivo con un navegador real
|
||||
// (Playwright): un producto featured pero sin stock disponible salio
|
||||
// primero en el carrusel con boton "Comprar ahora" hacia una pagina
|
||||
// donde en realidad no se puede comprar.
|
||||
const featuredProducts = products.filter((product) => product.featured && product.available);
|
||||
const bannerProducts = featuredProducts.length > 0 ? featuredProducts : treasures.filter((product) => product.available);
|
||||
return <><HeroCarousel products={bannerProducts} /><TrustBar /><div className="content-auto"><CategoryTiles categories={categories} /></div><section className="content-auto mx-auto max-w-7xl px-4 py-14"><div className="mb-8 flex items-end justify-between gap-5"><div><span className="text-xs font-black uppercase tracking-[.18em] text-ari-coral">Novedades</span><h2 className="mt-2 text-4xl font-black tracking-tight sm:text-5xl">Tesoros recién llegados</h2><p className="mt-3 text-slate-500">Desliza y encuentra nuevos favoritos.</p></div><Button asChild variant="outline" className="hidden sm:inline-flex"><Link href="/catalog">Ver catálogo <ArrowRight className="size-4" /></Link></Button></div><ProductCarousel products={treasures} /></section><section className="content-auto mx-auto max-w-7xl px-4 pb-20"><div className="grid overflow-hidden rounded-[2rem] bg-ari-marino text-white shadow-2xl lg:grid-cols-[1fr_.7fr]"><div className="p-8 sm:p-12"><span className="text-xs font-black uppercase tracking-[.18em] text-ari-coral-claro">Club ARI</span><h2 className="mt-3 text-4xl font-black tracking-tight sm:text-6xl">Promociones, lanzamientos y mucha diversión.</h2><p className="mt-5 max-w-xl text-white/80">Únete a nuestra comunidad y recibe primero las novedades del catálogo.</p><Button variant="yellow" size="lg" className="mt-7">Quiero unirme</Button></div>{/* No hay herramienta de generacion de imagenes disponible para crear un
|
||||
asset nuevo -- se reconstruye el mismo efecto (fondo punteado +
|
||||
fotos estilo polaroid) con CSS y fotos reales de producto ya
|
||||
|
||||
@@ -65,7 +65,7 @@ spec:
|
||||
memory: 64Mi
|
||||
|
||||
- name: migrations
|
||||
image: gitea.cruzcloud.net/devops/ecommerce-medusa:v1.0.234
|
||||
image: gitea.cruzcloud.net/devops/ecommerce-medusa:v1.0.239
|
||||
imagePullPolicy: IfNotPresent
|
||||
command:
|
||||
- node
|
||||
@@ -86,7 +86,7 @@ spec:
|
||||
|
||||
containers:
|
||||
- name: app
|
||||
image: gitea.cruzcloud.net/devops/ecommerce-medusa:v1.0.234
|
||||
image: gitea.cruzcloud.net/devops/ecommerce-medusa:v1.0.239
|
||||
imagePullPolicy: IfNotPresent
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
|
||||
@@ -1,10 +1,30 @@
|
||||
import Link from "next/link";
|
||||
import { Facebook, Instagram, MessageCircle } from "lucide-react";
|
||||
|
||||
// "Envíos" y "Contacto" ya tienen paginas reales (app/envios,
|
||||
// app/contacto -- contenido real: transportadoras de EnvioClick,
|
||||
// WhatsApp/correo reales). "Preguntas frecuentes" y "Cambios" todavia
|
||||
// no tienen contenido real decidido -- quedan apuntando a /catalog
|
||||
// (no rotas, pero tampoco resueltas) hasta que se defina que van a
|
||||
// decir.
|
||||
const HELP_LINKS = [
|
||||
{ label: "Preguntas frecuentes", href: "/catalog" },
|
||||
{ label: "Envíos", href: "/envios" },
|
||||
{ label: "Cambios", href: "/catalog" },
|
||||
{ label: "Contacto", href: "/contacto" },
|
||||
];
|
||||
|
||||
const SHOP_LINKS = [
|
||||
{ label: "Novedades", href: "/catalog" },
|
||||
{ label: "Juguetes", href: "/catalog" },
|
||||
{ label: "Mochilas", href: "/catalog" },
|
||||
{ label: "Regalos", href: "/catalog" },
|
||||
];
|
||||
|
||||
export function Footer() {
|
||||
return <footer className="bg-ari-marino text-white"><div className="mx-auto grid max-w-7xl gap-10 px-4 py-14 sm:grid-cols-2 lg:grid-cols-4"><div><div className="flex flex-col items-start gap-3"><img src="/images/brand/logo-ari.svg" alt="ARI shopping" className="h-11 w-auto" width={200} height={64} />{/* eslint-disable-line @next/next/no-img-element -- vector propio, next/image no optimiza SVG sin habilitar dangerouslyAllowSVG */}<p className="text-xs text-white/60">Tesoros para cada aventura.</p></div><div className="mt-5 flex gap-3"><Instagram /><Facebook /><MessageCircle /></div></div><FooterColumn title="Compra" links={["Novedades", "Juguetes", "Mochilas", "Regalos"]} /><FooterColumn title="Ayuda" links={["Preguntas frecuentes", "Envíos", "Cambios", "Contacto"]} /><div><h3 className="font-black">Newsletter</h3><p className="mt-3 text-sm leading-6 text-white/60">Recibe lanzamientos y promociones especiales.</p><div className="mt-4 flex rounded-full bg-white p-1"><input className="min-w-0 flex-1 bg-transparent px-4 text-sm text-slate-950 outline-none" placeholder="Tu correo" /><button className="rounded-full bg-ari-coral px-4 text-sm font-black text-white">Unirme</button></div></div></div><div className="border-t border-white/10 py-5 text-center text-xs text-white/50">© 2026 ARI Shopping · Frontend headless v4.1 · GitOps</div></footer>;
|
||||
return <footer className="bg-ari-marino text-white"><div className="mx-auto grid max-w-7xl gap-10 px-4 py-14 sm:grid-cols-2 lg:grid-cols-4"><div><div className="flex flex-col items-start gap-3"><img src="/images/brand/logo-ari.svg" alt="ARI shopping" className="h-11 w-auto" width={200} height={64} />{/* eslint-disable-line @next/next/no-img-element -- vector propio, next/image no optimiza SVG sin habilitar dangerouslyAllowSVG */}<p className="text-xs text-white/60">Tesoros para cada aventura.</p></div><div className="mt-5 flex gap-3"><Instagram /><Facebook /><MessageCircle /></div></div><FooterColumn title="Compra" links={SHOP_LINKS} /><FooterColumn title="Ayuda" links={HELP_LINKS} /><div><h3 className="font-black">Newsletter</h3><p className="mt-3 text-sm leading-6 text-white/60">Recibe lanzamientos y promociones especiales.</p><div className="mt-4 flex rounded-full bg-white p-1"><input className="min-w-0 flex-1 bg-transparent px-4 text-sm text-slate-950 outline-none" placeholder="Tu correo" /><button className="rounded-full bg-ari-coral px-4 text-sm font-black text-white">Unirme</button></div></div></div><div className="border-t border-white/10 py-5 text-center text-xs text-white/50">© 2026 ARI Shopping · Frontend headless v4.1 · GitOps</div></footer>;
|
||||
}
|
||||
|
||||
function FooterColumn({ title, links }: { title: string; links: string[] }) {
|
||||
return <div><h3 className="font-black">{title}</h3><div className="mt-4 grid gap-3 text-sm text-white/60">{links.map((link) => <Link key={link} href="/catalog" className="hover:text-ari-coral-claro">{link}</Link>)}</div></div>;
|
||||
function FooterColumn({ title, links }: { title: string; links: { label: string; href: string }[] }) {
|
||||
return <div><h3 className="font-black">{title}</h3><div className="mt-4 grid gap-3 text-sm text-white/60">{links.map((link) => <Link key={link.label} href={link.href} className="hover:text-ari-coral-claro">{link.label}</Link>)}</div></div>;
|
||||
}
|
||||
@@ -87,6 +87,18 @@ export function PredictiveSearch() {
|
||||
Buscando…
|
||||
</div>
|
||||
)}
|
||||
{/* Encontrado probando en vivo con navegador real: sin este bloque,
|
||||
una busqueda de 2+ caracteres sin coincidencias no mostraba nada
|
||||
(ni "buscando" ni un mensaje) -- el usuario se quedaba sin ningun
|
||||
feedback de que su busqueda de verdad corrio. Se ofrece igual el
|
||||
boton de buscar en todo el catalogo porque esa busqueda completa
|
||||
puede usar criterios mas amplios que el autocompletado. */}
|
||||
{!loading && normalized.length >= 2 && matches.length === 0 && (
|
||||
<div className="absolute left-0 right-0 top-[calc(100%+10px)] z-50 rounded-2xl border border-slate-200 bg-white p-2 text-center shadow-2xl">
|
||||
<p className="p-3 text-sm text-slate-500">No encontramos resultados para “{query.trim()}”.</p>
|
||||
<button type="submit" className="w-full rounded-xl bg-ari-marino px-4 py-3 text-sm font-black text-white hover:brightness-110">Buscar “{query.trim()}” en todo el catálogo</button>
|
||||
</div>
|
||||
)}
|
||||
{matches.length > 0 && (
|
||||
<div className="absolute left-0 right-0 top-[calc(100%+10px)] z-50 overflow-hidden rounded-2xl border border-slate-200 bg-white p-2 shadow-2xl">
|
||||
{matches.map((product) => (
|
||||
|
||||
@@ -25,7 +25,7 @@ spec:
|
||||
runAsUser: 1001
|
||||
containers:
|
||||
- name: app
|
||||
image: gitea.cruzcloud.net/devops/ecommerce-frontend:v1.0.235
|
||||
image: gitea.cruzcloud.net/devops/ecommerce-frontend:v1.0.243
|
||||
env:
|
||||
- name: HEADLESS_PROVIDER
|
||||
value: medusa
|
||||
|
||||
Reference in new issue
Block a user