UNPKG

medusa-variant-images-updated

Version:
323 lines (322 loc) 12.5 kB
import { jsxs, Fragment, jsx } from "react/jsx-runtime"; import { useFieldArray, Controller } from "react-hook-form"; import FileUploadField from "./components/FileUploadField.js"; import { CheckCircleSolid, CircleDottedLine } from "@medusajs/icons"; import { clx } from "@medusajs/ui"; import { useState, useEffect, useRef } from "react"; const VariantsImagesMediaForm = ({ form, type, onDeleteImage }) => { const { control, path, setValue, getValues, watch } = form; const { fields, append, remove } = useFieldArray({ control, name: path("images") }); const [hasNewUploads, setHasNewUploads] = useState(false); const [keyActionPerformed, setKeyActionPerformed] = useState(false); const [selectionCounter, setSelectionCounter] = useState(1); const imagesInfo = watch(path("images")) || []; const hasSelectedImages = imagesInfo.some((img) => img.info.selected); const isThumbnailSet = imagesInfo.some((img) => img.type === "thumbnail"); const getThumbnailImage = (images) => images.find((img) => img.type === "thumbnail"); useEffect(() => { const handleGlobalKeyDown = (e) => { if (e.target && e.target.tagName === "INPUT") return; if (e.key.toLowerCase() === "d") { e.preventDefault(); const currentImages = getValues(path("images")); const toDeleteIndices = []; const toDeleteImages = []; currentImages.forEach((image, i) => { var _a; if ((_a = image == null ? void 0 : image.info) == null ? void 0 : _a.selected) { toDeleteIndices.push(i); toDeleteImages.push(image); } }); if (toDeleteIndices.length > 0) { toDeleteIndices.sort((a, b) => b - a).forEach((i) => remove(i)); toDeleteImages.forEach((image) => { if (typeof image.id === "number" && Number.isInteger(image.id)) { onDeleteImage(image); } }); const remainingImages = getValues(path("images")); const prevThumbnail = getThumbnailImage(currentImages); let clearedSelection = remainingImages.map((img) => ({ ...img, info: { ...img.info, selected: false, selectedNumber: -1 } })); if (prevThumbnail && clearedSelection.some((img) => img.url === prevThumbnail.url)) { clearedSelection = clearedSelection.map( (img) => img.url === prevThumbnail.url ? { ...img, type: "thumbnail" } : { ...img, type: void 0 } ); } else { clearedSelection = clearedSelection.map((img) => ({ ...img, type: void 0 })); } setValue(path("images"), clearedSelection, { shouldDirty: true }); setKeyActionPerformed(true); } } if (type === "media" && e.key.toLowerCase() === "t") { e.preventDefault(); const currentImages = getValues(path("images")); const selectedImages = currentImages.map((img, idx) => ({ ...img, idx })).filter((img) => img.info.selected && img.info.selectedNumber > 0); if (selectedImages.length > 0) { selectedImages.sort((a, b) => a.info.selectedNumber - b.info.selectedNumber); const firstSelectedIdx = selectedImages[0].idx; const updatedImages = currentImages.map((img, i) => ({ ...img, type: i === firstSelectedIdx ? "thumbnail" : "media", info: { ...img.info, selected: i === firstSelectedIdx, selectedNumber: i === firstSelectedIdx ? img.info.selectedNumber : -1 } })); setValue(path("images"), updatedImages, { shouldDirty: true }); setKeyActionPerformed(true); } } }; document.addEventListener("keydown", handleGlobalKeyDown); return () => document.removeEventListener("keydown", handleGlobalKeyDown); }, [getValues, path, remove, setValue, onDeleteImage, type]); const handleFilesChosen = (files) => { if (files.length) { const toAppend = files.map((file) => ({ url: URL.createObjectURL(file), name: file.name, size: file.size, nativeFile: file, info: { selected: false, selectedNumber: -1 } })); const currentImages = getValues(path("images")); const prevThumbnail = getThumbnailImage(currentImages); append(toAppend); setHasNewUploads(true); setTimeout(() => { const updatedImages = getValues(path("images")); if (prevThumbnail && updatedImages.some((img) => img.url === prevThumbnail.url)) { setValue( path("images"), updatedImages.map( (img) => img.url === prevThumbnail.url ? { ...img, type: "thumbnail" } : { ...img, type: void 0 } ), { shouldDirty: true } ); } }, 0); } }; const toggleSelected = (index) => { const currentImages = getValues(path("images")); const updatedImages = currentImages.map((img, i) => { if (i === index) { const isSelecting = !img.info.selected; return { ...img, info: { ...img.info, selected: isSelecting, selectedNumber: isSelecting ? selectionCounter : -1 } }; } if (type === "thumbnail" && i !== index) { return { ...img, info: { ...img.info, selected: false, selectedNumber: -1 } }; } return img; }); setValue(path("images"), updatedImages, { shouldDirty: true }); if (updatedImages[index].info.selected) { setSelectionCounter(selectionCounter + 1); if (type === "thumbnail") { setKeyActionPerformed(true); } } else if (type === "thumbnail" && !updatedImages.some((img) => img.info.selected)) { setKeyActionPerformed(false); } }; const handleRemoveImage = (index, image) => { const currentImages = getValues(path("images")); const prevThumbnail = getThumbnailImage(currentImages); remove(index); if (typeof image.id === "number" && Number.isInteger(image.id)) { onDeleteImage(image); } setTimeout(() => { const updatedImages = getValues(path("images")); if (prevThumbnail && updatedImages.some((img) => img.url === prevThumbnail.url)) { setValue( path("images"), updatedImages.map( (img) => img.url === prevThumbnail.url ? { ...img, type: "thumbnail" } : { ...img, type: void 0 } ), { shouldDirty: true } ); } else { setValue( path("images"), updatedImages.map((img) => ({ ...img, type: void 0 })), { shouldDirty: true } ); } }, 0); }; const canSaveAndClose = type === "thumbnail" ? hasSelectedImages || hasNewUploads || keyActionPerformed : isThumbnailSet || hasSelectedImages || hasNewUploads || keyActionPerformed; return /* @__PURE__ */ jsxs(Fragment, { children: [ /* @__PURE__ */ jsxs("div", { className: "flex size-full flex-col-reverse lg:grid lg:grid-cols-[1fr_560px]", children: [ fields.length > 0 ? /* @__PURE__ */ jsxs("div", { className: "bg-ui-bg-subtle size-full overflow-auto divide-y", children: [ /* @__PURE__ */ jsxs("div", { className: "p-6 flex flex-col space-y-1", children: [ /* @__PURE__ */ jsx("h2", { children: "Uploads" }), /* @__PURE__ */ jsx("p", { className: "txt-small text-ui-fg-subtle", children: type === "thumbnail" ? /* @__PURE__ */ jsx("span", { children: "Select an image to use as the variant thumbnail." }) : /* @__PURE__ */ jsx("span", { children: "Select images to use as the variant images." }) }) ] }), /* @__PURE__ */ jsx("div", { className: "grid h-fit auto-rows-auto grid-cols-[repeat(auto-fill,minmax(150px,1fr))] gap-6 p-6", children: fields.map((field, index) => /* @__PURE__ */ jsx( Image, { image: field, index, form, onSelected: () => { }, onRemove: handleRemoveImage, type, toggleSelected }, field.id || field.url )) }) ] }) : /* @__PURE__ */ jsx("div", { className: "w-[70%]" }), /* @__PURE__ */ jsx("div", { className: "bg-ui-bg-base overflow-auto border-b px-6 py-4 lg:border-b-0 lg:border-l", children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-y-2", children: [ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-y-1", children: [ /* @__PURE__ */ jsx("h2", { children: "Media" }), /* @__PURE__ */ jsx("p", { className: "txt-small text-ui-fg-subtle", children: "Add images to your product media." }) ] }), /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx( FileUploadField, { onFileChosen: handleFilesChosen, placeholder: "Drag and drop images, or click to upload.", multiple: true, filetypes: ["image/gif", "image/jpeg", "image/png", "image/webp"], className: "py-large" } ) }) ] }) }) ] }), /* @__PURE__ */ jsx("div", { className: "mt-4 flex justify-end", children: /* @__PURE__ */ jsx( "button", { type: "button", disabled: !canSaveAndClose, className: clx( "px-6 py-2 rounded font-semibold", canSaveAndClose ? "bg-blue-600 text-white hover:bg-blue-700" : "bg-gray-300 text-gray-500 cursor-not-allowed" ), onClick: () => { setHasNewUploads(false); setKeyActionPerformed(false); }, children: "Save and Close" } ) }) ] }); }; const Image = ({ image, index, form, onSelected, onRemove, type, toggleSelected }) => { const { control, path } = form; const buttonRef = useRef(null); const handleKeyDown = (e) => { if (e.key.toLowerCase() === "d") { e.preventDefault(); onRemove(index, image); } if (e.key.toLowerCase() === "t") { e.preventDefault(); toggleSelected(index); } }; return /* @__PURE__ */ jsx( Controller, { name: path(`images.${index}.info`), control, render: ({ field: { value } }) => /* @__PURE__ */ jsxs( "button", { ref: buttonRef, className: clx( "relative shadow-elevation-card-rest hover:shadow-elevation-card-hover focus-visible:shadow-borders-focus bg-ui-bg-subtle-hover group aspect-square h-auto max-w-full overflow-hidden rounded-lg outline-none", { "bg-grey-500": value.selected } ), type: "button", tabIndex: 0, onClick: () => { toggleSelected(index); onSelected(index); }, onKeyDown: handleKeyDown, children: [ (image == null ? void 0 : image.type) === "thumbnail" && /* @__PURE__ */ jsx("span", { className: "absolute top-2 left-2 px-2 py-1 rounded bg-yellow-400 text-xs font-bold z-10", children: "Thumbnail" }), /* @__PURE__ */ jsx( "img", { src: image.url, alt: image.name || "Uploaded image", className: clx( "rounded-lg object-cover bg-clip-border w-full h-full duration-300 brightness-100", { "brightness-50": value.selected } ) } ), /* @__PURE__ */ jsx( "span", { className: clx( "hidden group-hover:block absolute top-2 right-2 mix-blend-difference", { "block mix-blend-normal": value.selected, "bg-neutral-950 flex rounded-full group-hover:flex flex-row items-center w-10 h-[19px] px-0.5": value.selected && type === "media" } ), children: value.selected ? /* @__PURE__ */ jsx(CheckCircleSolid, { className: "text-green-300" }) : /* @__PURE__ */ jsx(CircleDottedLine, {}) } ) ] } ) } ); }; export { VariantsImagesMediaForm as default };