Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8fd0514e76 | ||
|
|
1cde29b344 | ||
|
|
8517102b63 | ||
|
|
7773bcdecf |
No files matched your search
@@ -7,7 +7,9 @@ import type {
|
|||||||
IStockLocationService,
|
IStockLocationService,
|
||||||
} from "@medusajs/framework/types";
|
} from "@medusajs/framework/types";
|
||||||
import {
|
import {
|
||||||
|
createCollectionsWorkflow,
|
||||||
createInventoryItemsWorkflow,
|
createInventoryItemsWorkflow,
|
||||||
|
createProductCategoriesWorkflow,
|
||||||
createProductsWorkflow,
|
createProductsWorkflow,
|
||||||
uploadFilesWorkflow,
|
uploadFilesWorkflow,
|
||||||
} from "@medusajs/medusa/core-flows";
|
} from "@medusajs/medusa/core-flows";
|
||||||
@@ -20,6 +22,15 @@ import { csvRowsToObjects, parseCsv } from "../../../lib/csv";
|
|||||||
// fila se procesa independiente (una foto rota o categoria mal escrita
|
// fila se procesa independiente (una foto rota o categoria mal escrita
|
||||||
// no tumba el resto del archivo).
|
// no tumba el resto del archivo).
|
||||||
//
|
//
|
||||||
|
// categoria/coleccion: si el nombre no matchea una categoria/coleccion
|
||||||
|
// ya existente (case-insensitive), se crea nueva -- antes se descartaba
|
||||||
|
// en silencio y el producto quedaba sin categoria/coleccion sin avisar
|
||||||
|
// (confirmado en vivo: "Hello Kitty & Friends" nunca se creo como
|
||||||
|
// coleccion). El handle de la nueva categoria/coleccion se genera con
|
||||||
|
// el mismo slugify() ASCII-safe de mas abajo, por la misma razon que el
|
||||||
|
// handle del producto: el default de Medusa no quita tildes/enies y
|
||||||
|
// rompe la ruta publica.
|
||||||
|
//
|
||||||
// Las fotos se descargan del origen y se re-alojan en el mismo
|
// Las fotos se descargan del origen y se re-alojan en el mismo
|
||||||
// MinIO/S3 que ya usa el resto del catalogo (uploadFilesWorkflow ->
|
// MinIO/S3 que ya usa el resto del catalogo (uploadFilesWorkflow ->
|
||||||
// mismo File Module Provider de medusa-config.ts, ya corre en el NAS).
|
// mismo File Module Provider de medusa-config.ts, ya corre en el NAS).
|
||||||
@@ -215,8 +226,33 @@ export async function POST(req: MedusaRequest<{ csv: string }>, res: MedusaRespo
|
|||||||
const inventoryItemId = inventoryItems[0]?.id;
|
const inventoryItemId = inventoryItems[0]?.id;
|
||||||
if (!inventoryItemId) throw new Error("No se pudo crear el registro de inventario");
|
if (!inventoryItemId) throw new Error("No se pudo crear el registro de inventario");
|
||||||
|
|
||||||
const categoryId = row.categoria ? categoryByName.get(row.categoria.toLowerCase().trim()) : undefined;
|
const categoryName = row.categoria?.trim();
|
||||||
const collectionId = row.coleccion ? collectionByName.get(row.coleccion.toLowerCase().trim()) : undefined;
|
let categoryId = categoryName ? categoryByName.get(categoryName.toLowerCase()) : undefined;
|
||||||
|
if (categoryName && !categoryId) {
|
||||||
|
const { result: createdCategories } = await createProductCategoriesWorkflow(req.scope).run({
|
||||||
|
input: {
|
||||||
|
product_categories: [
|
||||||
|
{ name: categoryName, handle: slugify(categoryName, `${Date.now().toString(36)}${index}`) },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
categoryId = createdCategories[0].id;
|
||||||
|
categoryByName.set(categoryName.toLowerCase(), categoryId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const collectionName = row.coleccion?.trim();
|
||||||
|
let collectionId = collectionName ? collectionByName.get(collectionName.toLowerCase()) : undefined;
|
||||||
|
if (collectionName && !collectionId) {
|
||||||
|
const { result: createdCollections } = await createCollectionsWorkflow(req.scope).run({
|
||||||
|
input: {
|
||||||
|
collections: [
|
||||||
|
{ title: collectionName, handle: slugify(collectionName, `${Date.now().toString(36)}${index}`) },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
collectionId = createdCollections[0].id;
|
||||||
|
collectionByName.set(collectionName.toLowerCase(), collectionId);
|
||||||
|
}
|
||||||
|
|
||||||
await createProductsWorkflow(req.scope).run({
|
await createProductsWorkflow(req.scope).run({
|
||||||
input: {
|
input: {
|
||||||
|
|||||||
@@ -2,19 +2,36 @@
|
|||||||
|
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { Maximize2, Minus, Plus, RotateCcw, ZoomIn } from "lucide-react";
|
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";
|
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
|
||||||
|
|
||||||
interface Offset { x: number; y: number }
|
interface Offset { x: number; y: number }
|
||||||
interface DragState { pointerId: number; startX: number; startY: number; originX: number; originY: 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 }) {
|
export function ProductGallery({ images, name }: { images: string[]; name: string }) {
|
||||||
const [selected, setSelected] = useState(images[0] ?? "");
|
const [selected, setSelected] = useState(images[0] ?? "");
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [scale, setScale] = useState(1);
|
const [scale, setScale] = useState(1);
|
||||||
const [offset, setOffset] = useState<Offset>({ x: 0, y: 0 });
|
const [offset, setOffset] = useState<Offset>({ x: 0, y: 0 });
|
||||||
|
const [hoverZoom, setHoverZoom] = useState<HoverZoomOrigin | null>(null);
|
||||||
const drag = useRef<DragState | 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 applyScale = (next: number) => {
|
||||||
const value = Math.min(3, Math.max(1, Number(next.toFixed(2))));
|
const value = Math.min(3, Math.max(1, Number(next.toFixed(2))));
|
||||||
setScale(value);
|
setScale(value);
|
||||||
@@ -44,8 +61,24 @@ export function ProductGallery({ images, name }: { images: string[]; name: strin
|
|||||||
</div>
|
</div>
|
||||||
<Dialog open={open} onOpenChange={changeOpen}>
|
<Dialog open={open} onOpenChange={changeOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<button type="button" className="group relative order-1 aspect-square overflow-hidden rounded-[2rem] bg-slate-50 lg:order-2">
|
<button
|
||||||
<Image src={selected} alt={name} fill priority sizes="(max-width: 1024px) 100vw, 50vw" className="object-cover transition duration-500 group-hover:scale-[1.03]" />
|
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>
|
<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>
|
</button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
|
|||||||
Reference in new issue
Block a user