Compare commits

...
Author SHA1 Message Date
devops 313bb2eab4 Merge fix/navbar-departamentos-hover-gap into main
Build and Push Medusa / Escaneo de Secretos (Gitleaks) (push) Successful in 27s
Build and Push Frontend / Escaneo de Secretos (Gitleaks) (push) Successful in 23s
Build and Push Medusa / Construir y publicar Medusa (push) Successful in 26m6s
Build and Push Frontend / Construir y Subir Imagen (push) Successful in 14m54s
2026-09-06 21:04:24 +00:00
devops 710da06c0a Merge fix/catalog-sidebar-scroll into main 2026-09-06 21:04:23 +00:00
devops 9d08169092 Merge fix/product-import-dedupe-por-titulo into main 2026-09-06 21:04:23 +00:00
devopsandClaude Sonnet 5 1aa7802a61 fix(navbar): el menú Departamentos no se cierra al mover el cursor hacia él
El panel del mega-menu tenía mt-1 respecto al botón -- como es
position:absolute no suma a la altura del contenedor que escucha
mouseenter/mouseleave, así que ese margen quedaba como una franja
muerta: mover el cursor del botón hacia el panel disparaba
mouseleave a mitad de camino y cerraba el menú antes de poder hacer
clic en una categoría (reportado en vivo).

Se elimina el gap (mt-1) y se agrega un delay de 200ms antes de cerrar,
cancelable si el cursor vuelve a entrar, como margen extra para
movimientos rápidos o en diagonal cerca del borde.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01THZFwPzrwv6VAbuwmaf5vR
2026-09-06 21:04:16 +00:00
devopsandClaude Sonnet 5 b83722e9c5 fix(catalog): scroll propio en el sidebar de filtros
El <aside> de Filtros es sticky pero sin altura máxima ni overflow
propio -- con 30+ categorías más marca/edad/precio, el panel se pasa
del viewport y queda inalcanzable (reportado en vivo: había que bajar
el zoom del navegador para ver "Marca"/"Edad"/"Precio").

overflow-y-auto + max-h acotado al viewport (solo en lg, que es donde
aplica sticky) le da scroll propio al panel sin depender del scroll de
la página.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01THZFwPzrwv6VAbuwmaf5vR
2026-09-06 21:04:04 +00:00
devopsandClaude Sonnet 5 8d543f5eb1 fix(product-import): no duplicar producto al re-importar el mismo título
Re-subir la misma fila de CSV (ej. para agregar más fotos o actualizar
la descripción de un producto ya cargado) creaba un producto nuevo con
handle distinto -- confirmado en vivo re-importando "Hello Kitty &
Friends" para sumar fotos: quedaron dos productos con el mismo título
en la tienda.

Ahora, si el título coincide (case-insensitive/trim) con un producto ya
importado, se actualiza ese producto en vez de crear uno: las fotos
nuevas se agregan a las que ya tenía (no se reemplazan) y la
descripción se actualiza si la fila trae una. No toca
precio/cantidad/categoría/colección del producto existente.

El duplicado ya creado se fusionó en vivo (fotos movidas al original,
duplicado borrado) sin esperar este deploy.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01THZFwPzrwv6VAbuwmaf5vR
2026-09-06 21:03:56 +00:00
devops df791e0c28 chore: release v1.0.226 [skip ci] 2026-09-06 19:55:30 +00:00
gitea-actions feda1283d4 chore(gitops): deploy Medusa v1.0.225 [skip ci] 2026-09-06 19:46:05 +00:00
devops 8fd0514e76 Merge feat/product-gallery-hover-zoom into main
Build and Push Medusa / Escaneo de Secretos (Gitleaks) (push) Successful in 21s
Build and Push Frontend / Escaneo de Secretos (Gitleaks) (push) Successful in 19s
Build and Push Medusa / Construir y publicar Medusa (push) Successful in 23m54s
Build and Push Frontend / Construir y Subir Imagen (push) Successful in 9m11s
2026-09-06 19:19:19 +00:00
devops 1cde29b344 Merge fix/product-import-crear-categoria-coleccion into main 2026-09-06 19:19:18 +00:00
devopsandClaude Sonnet 5 8517102b63 feat(product-gallery): zoom automático de la imagen al pasar el cursor
Antes el único zoom era el modal interactivo (click -> Dialog con scale/
drag). Ahora la imagen principal también hace zoom estilo lupa (2.2x)
que sigue la posición del cursor con mousemove/transform-origin, sin
tocar el modal existente (sigue abriéndose igual con el click).

Solo mouse -- mousemove no dispara de forma continua en touch, así que
en mobile simplemente no aparece el efecto, sin romper nada ahí.

Verificado: tsc --noEmit limpio, página de producto renderiza 200 con
datos mock y el nuevo markup presente. No se pudo verificar visualmente
el hover en un navegador real (sin chromium-cli ni claude-in-chrome
disponibles en este shell headless).

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01THZFwPzrwv6VAbuwmaf5vR
2026-09-06 19:18:11 +00:00
7 changed files with 115 additions and 15 deletions

No files matched your search

@@ -14,6 +14,7 @@ type RowResult = {
row: number;
title: string;
ok: boolean;
action?: "created" | "updated";
error?: string;
};
@@ -91,9 +92,11 @@ const ProductImportPanel = () => {
<div className="px-6 py-4">
<Text size="small" className="text-ui-fg-subtle">
Columnas: <code>titulo, descripcion, precio, categoria, coleccion, cantidad, fotos</code>. Varias fotos se
separan con <code>;</code> dentro de la misma celda. <code>categoria</code>/<code>coleccion</code> deben
escribirse igual que en la tienda o se dejan vacías. Cada foto se descarga y se guarda en el almacenamiento
propio de la tienda (no queda enlazada al sitio de origen).
separan con <code>;</code> dentro de la misma celda. Si <code>categoria</code>/<code>coleccion</code> no
coinciden con una ya existente, se crean nuevas. Si <code>titulo</code> coincide con un producto ya
importado, no se duplica: se le agregan las fotos nuevas y se actualiza la descripción si la fila trae una.
Cada foto se descarga y se guarda en el almacenamiento propio de la tienda (no queda enlazada al sitio de
origen).
</Text>
</div>
@@ -122,7 +125,7 @@ const ProductImportPanel = () => {
<td className="py-2">{result.title}</td>
<td className="py-2">
{result.ok ? (
<Badge color="green">Importado</Badge>
<Badge color="green">{result.action === "updated" ? "Actualizado (ya existía)" : "Importado"}</Badge>
) : (
<span className="text-rose-600">{result.error}</span>
)}
@@ -11,6 +11,7 @@ import {
createInventoryItemsWorkflow,
createProductCategoriesWorkflow,
createProductsWorkflow,
updateProductsWorkflow,
uploadFilesWorkflow,
} from "@medusajs/medusa/core-flows";
import { csvRowsToObjects, parseCsv } from "../../../lib/csv";
@@ -31,6 +32,18 @@ import { csvRowsToObjects, parseCsv } from "../../../lib/csv";
// handle del producto: el default de Medusa no quita tildes/enies y
// rompe la ruta publica.
//
// Re-importar el mismo titulo (ej. para agregar mas fotos o actualizar
// la descripcion de un producto ya cargado) actualiza el producto
// existente en vez de crear uno nuevo -- antes cada re-importacion
// generaba un producto duplicado con handle distinto (confirmado en
// vivo con "Hello Kitty & Friends": misma fila re-subida para sumar
// fotos, dos productos con el mismo titulo en la tienda). El match es
// por titulo exacto (case-insensitive/trim), igual que categoria y
// coleccion; en un match, las fotos nuevas se agregan a las que ya
// tenia (no se reemplazan) y la descripcion se actualiza si la fila
// trae una. No toca precio/cantidad/categoria/coleccion del producto
// existente -- eso se edita a mano en el admin si hace falta.
//
// Las fotos se descargan del origen y se re-alojan en el mismo
// MinIO/S3 que ya usa el resto del catalogo (uploadFilesWorkflow ->
// mismo File Module Provider de medusa-config.ts, ya corre en el NAS).
@@ -61,6 +74,7 @@ type RowResult = {
row: number;
title: string;
ok: boolean;
action?: "created" | "updated";
error?: string;
};
@@ -150,7 +164,7 @@ export async function POST(req: MedusaRequest<{ csv: string }>, res: MedusaRespo
const salesChannelService = req.scope.resolve<ISalesChannelModuleService>(Modules.SALES_CHANNEL);
const productService = req.scope.resolve<IProductModuleService>(Modules.PRODUCT);
const [locations, shippingProfiles, salesChannels, categories, collections] = await Promise.all([
const [locations, shippingProfiles, salesChannels, categories, collections, existingProducts] = await Promise.all([
stockLocationService.listStockLocations({}),
fulfillmentService.listShippingProfiles({}),
salesChannelService.listSalesChannels({}),
@@ -161,6 +175,7 @@ export async function POST(req: MedusaRequest<{ csv: string }>, res: MedusaRespo
// los trae completos).
productService.listProductCategories({}, { select: ["id", "name"] }),
productService.listProductCollections({}, { select: ["id", "title"] }),
productService.listProducts({}, { select: ["id", "title"], relations: ["images"] }),
]);
const locationId = locations[0]?.id;
@@ -180,6 +195,9 @@ export async function POST(req: MedusaRequest<{ csv: string }>, res: MedusaRespo
const collectionByName = new Map(
collections.filter((c) => c.title).map((c) => [c.title.toLowerCase().trim(), c.id]),
);
const productByTitle = new Map(
existingProducts.filter((p) => p.title).map((p) => [p.title.toLowerCase().trim(), p]),
);
const results: RowResult[] = [];
@@ -210,6 +228,23 @@ export async function POST(req: MedusaRequest<{ csv: string }>, res: MedusaRespo
},
});
const existingProduct = productByTitle.get(title.toLowerCase());
if (existingProduct) {
await updateProductsWorkflow(req.scope).run({
input: {
products: [
{
id: existingProduct.id,
description: row.descripcion?.trim() || undefined,
images: [...(existingProduct.images ?? []).map((image) => ({ url: image.url })), ...uploadedFiles.map((f) => ({ url: f.url }))],
},
],
},
});
results.push({ row: rowNumber, title, ok: true, action: "updated" });
continue;
}
const quantity = Number(row.cantidad);
const { result: inventoryItems } = await createInventoryItemsWorkflow(req.scope).run({
input: {
@@ -283,7 +318,7 @@ export async function POST(req: MedusaRequest<{ csv: string }>, res: MedusaRespo
},
});
results.push({ row: rowNumber, title, ok: true });
results.push({ row: rowNumber, title, ok: true, action: "created" });
} catch (error) {
results.push({ row: rowNumber, title: title || `Fila ${rowNumber}`, ok: false, error: (error as Error).message });
}
@@ -65,7 +65,7 @@ spec:
memory: 64Mi
- name: migrations
image: gitea.cruzcloud.net/devops/ecommerce-medusa:v1.0.223
image: gitea.cruzcloud.net/devops/ecommerce-medusa:v1.0.225
imagePullPolicy: IfNotPresent
command:
- node
@@ -86,7 +86,7 @@ spec:
containers:
- name: app
image: gitea.cruzcloud.net/devops/ecommerce-medusa:v1.0.223
image: gitea.cruzcloud.net/devops/ecommerce-medusa:v1.0.225
imagePullPolicy: IfNotPresent
envFrom:
- configMapRef:
@@ -352,7 +352,11 @@ export function CatalogClient({
</div>
<div className="grid gap-8 lg:grid-cols-[260px_1fr]">
<aside className="h-fit space-y-6 rounded-lg border border-slate-100 bg-white p-6 shadow-lg shadow-ari-marino/5 lg:sticky lg:top-44">
{/* overflow-y-auto + max-h propio en lg: sticky no le da scroll
propio al contenido, y con 30+ categorias mas marca/edad/precio
el panel se pasa del viewport sin forma de llegar al resto salvo
bajando el zoom del navegador (reportado en vivo). */}
<aside className="h-fit space-y-6 overflow-y-auto rounded-lg border border-slate-100 bg-white p-6 shadow-lg shadow-ari-marino/5 lg:sticky lg:top-44 lg:max-h-[calc(100vh-13rem)]">
<div className="flex items-center justify-between">
<strong className="flex items-center gap-2 text-lg font-black">
<SlidersHorizontal className="size-5 text-ari-coral-claro" />
@@ -199,6 +199,24 @@ function CategoryNavBar({
function DepartmentsMegaMenu({ tree, isActive }: { tree: CategoryNode[]; isActive: (name: string) => boolean }) {
const [open, setOpen] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
const closeTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
const cancelClose = () => {
if (closeTimer.current) {
clearTimeout(closeTimer.current);
closeTimer.current = null;
}
};
// Delay corto en vez de cerrar al toque: aunque el gap entre el boton y
// el panel ya se elimino (antes era la causa real -- el panel es
// absolute y no suma a la altura del contenedor, asi que un mt-1 ahi
// dejaba una franja muerta que cerraba el menu a mitad de camino,
// reportado en vivo), este margen extra tolera movimientos rapidos o en
// diagonal cerca del borde sin reabrir nada raro.
const scheduleClose = () => {
cancelClose();
closeTimer.current = setTimeout(() => setOpen(false), 200);
};
useEffect(() => {
if (!open) return;
@@ -216,8 +234,15 @@ function DepartmentsMegaMenu({ tree, isActive }: { tree: CategoryNode[]; isActiv
};
}, [open]);
useEffect(() => () => cancelClose(), []);
const openNow = () => {
cancelClose();
setOpen(true);
};
return (
<div ref={containerRef} className="shrink-0" onMouseEnter={() => setOpen(true)} onMouseLeave={() => setOpen(false)}>
<div ref={containerRef} className="shrink-0" onMouseEnter={openNow} onMouseLeave={scheduleClose}>
<button
type="button"
onClick={() => setOpen((value) => !value)}
@@ -231,7 +256,7 @@ function DepartmentsMegaMenu({ tree, isActive }: { tree: CategoryNode[]; isActiv
</button>
{open && (
<div className="absolute left-0 top-full z-50 mt-1 max-h-[80vh] w-[min(92vw,64rem)] overflow-y-auto rounded-2xl border border-white/10 bg-ari-marino p-6 shadow-2xl">
<div className="absolute left-0 top-full z-50 max-h-[80vh] w-[min(92vw,64rem)] overflow-y-auto rounded-2xl border border-white/10 bg-ari-marino p-6 shadow-2xl">
<div className="grid grid-cols-2 gap-x-6 gap-y-5 sm:grid-cols-3 lg:grid-cols-6">
{tree.map((root) => (
<div key={root.id}>
@@ -2,19 +2,36 @@
import Image from "next/image";
import { Maximize2, Minus, Plus, RotateCcw, ZoomIn } from "lucide-react";
import { PointerEvent, WheelEvent, useRef, useState } from "react";
import { MouseEvent, PointerEvent, WheelEvent, useRef, useState } from "react";
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
interface Offset { x: number; y: number }
interface DragState { pointerId: number; startX: number; startY: number; originX: number; originY: number }
interface HoverZoomOrigin { x: number; y: number }
// Zoom automatico al pasar el cursor (estilo lupa clasico de e-commerce)
// sobre la imagen principal -- solo mouse (mousemove no dispara de forma
// continua en touch, asi que en mobile simplemente no aparece el efecto,
// sin romper nada). Independiente del zoom interactivo del modal
// (click -> Dialog con scale/drag), que sigue igual.
const HOVER_ZOOM_SCALE = 2.2;
export function ProductGallery({ images, name }: { images: string[]; name: string }) {
const [selected, setSelected] = useState(images[0] ?? "");
const [open, setOpen] = useState(false);
const [scale, setScale] = useState(1);
const [offset, setOffset] = useState<Offset>({ x: 0, y: 0 });
const [hoverZoom, setHoverZoom] = useState<HoverZoomOrigin | null>(null);
const drag = useRef<DragState | null>(null);
const trackHoverZoom = (event: MouseEvent<HTMLButtonElement>) => {
const rect = event.currentTarget.getBoundingClientRect();
setHoverZoom({
x: ((event.clientX - rect.left) / rect.width) * 100,
y: ((event.clientY - rect.top) / rect.height) * 100,
});
};
const applyScale = (next: number) => {
const value = Math.min(3, Math.max(1, Number(next.toFixed(2))));
setScale(value);
@@ -44,8 +61,24 @@ export function ProductGallery({ images, name }: { images: string[]; name: strin
</div>
<Dialog open={open} onOpenChange={changeOpen}>
<DialogTrigger asChild>
<button type="button" className="group relative order-1 aspect-square overflow-hidden rounded-[2rem] bg-slate-50 lg:order-2">
<Image src={selected} alt={name} fill priority sizes="(max-width: 1024px) 100vw, 50vw" className="object-cover transition duration-500 group-hover:scale-[1.03]" />
<button
type="button"
className="group relative order-1 aspect-square cursor-zoom-in overflow-hidden rounded-[2rem] bg-slate-50 lg:order-2"
onMouseMove={trackHoverZoom}
onMouseLeave={() => setHoverZoom(null)}
>
<Image
src={selected}
alt={name}
fill
priority
sizes="(max-width: 1024px) 100vw, 50vw"
className="object-cover transition-transform duration-150 ease-out"
style={{
transform: `scale(${hoverZoom ? HOVER_ZOOM_SCALE : 1})`,
transformOrigin: hoverZoom ? `${hoverZoom.x}% ${hoverZoom.y}%` : "center",
}}
/>
<span className="absolute bottom-5 right-5 inline-flex items-center gap-2 rounded-full bg-white px-4 py-2 text-xs font-black shadow-xl"><ZoomIn className="size-4" />Zoom interactivo</span>
</button>
</DialogTrigger>
@@ -25,7 +25,7 @@ spec:
runAsUser: 1001
containers:
- name: app
image: gitea.cruzcloud.net/devops/ecommerce-frontend:v1.0.219
image: gitea.cruzcloud.net/devops/ecommerce-frontend:v1.0.226
env:
- name: HEADLESS_PROVIDER
value: medusa