UNPKG

@alphabite/medusa-category-images

Version:

Alphabite's Medusa Category Images Plugin

1,692 lines 180 kB
import { jsxs, jsx, Fragment } from "react/jsx-runtime"; import { defineWidgetConfig } from "@medusajs/admin-sdk"; import { DropdownMenu, IconButton, clx, Checkbox, Tooltip, Text, Button, usePrompt, Container, Heading, CommandBar, Label as Label$1, Hint as Hint$1, Prompt, Drawer, FocusModal, toast } from "@medusajs/ui"; import { EllipsisHorizontal, ThumbnailBadge, Pencil, Spinner as Spinner$1, InformationCircleSolid, Trash, ArrowDownTray, TriangleLeftMini, TriangleRightMini } from "@medusajs/icons"; import { Link, useLocation, useBlocker, useNavigate, useSearchParams, useParams as useParams$1 } from "react-router-dom"; import React, { useState, useMemo, createContext, forwardRef, useId, useContext, useCallback, useEffect, useRef, useLayoutEffect, memo, useReducer, cloneElement, Fragment as Fragment$1 } from "react"; import { useTranslation } from "react-i18next"; import { useQueryClient, useMutation, useQuery } from "@tanstack/react-query"; import Medusa from "@medusajs/js-sdk"; import { useParams } from "react-router"; import { Slot } from "radix-ui"; import { FormProvider, useFormContext, useFormState, Controller, get, appendErrors, useForm, useFieldArray } from "react-hook-form"; import { unstable_batchedUpdates, createPortal } from "react-dom"; import { z } from "@medusajs/framework/zod"; const ActionMenu = ({ groups }) => { return /* @__PURE__ */ jsxs(DropdownMenu, { children: [ /* @__PURE__ */ jsx(DropdownMenu.Trigger, { asChild: true, children: /* @__PURE__ */ jsx(IconButton, { size: "small", variant: "transparent", children: /* @__PURE__ */ jsx(EllipsisHorizontal, {}) }) }), /* @__PURE__ */ jsx(DropdownMenu.Content, { children: groups.map((group, index) => { if (!group.actions.length) { return null; } const isLast = index === groups.length - 1; return /* @__PURE__ */ jsxs(DropdownMenu.Group, { children: [ group.actions.map((action, index2) => { if (action.onClick) { return /* @__PURE__ */ jsxs( DropdownMenu.Item, { disabled: action.disabled, onClick: (e2) => { e2.stopPropagation(); action.onClick(); }, className: clx( "[&_svg]:text-ui-fg-subtle flex items-center gap-x-2", { "[&_svg]:text-ui-fg-disabled": action.disabled } ), children: [ action.icon, /* @__PURE__ */ jsx("span", { children: action.label }) ] }, index2 ); } return /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx( DropdownMenu.Item, { className: clx( "[&_svg]:text-ui-fg-subtle flex items-center gap-x-2", { "[&_svg]:text-ui-fg-disabled": action.disabled } ), asChild: true, disabled: action.disabled, children: /* @__PURE__ */ jsxs(Link, { to: action.to, onClick: (e2) => e2.stopPropagation(), children: [ action.icon, /* @__PURE__ */ jsx("span", { children: action.label }) ] }) } ) }, index2); }), !isLast && /* @__PURE__ */ jsx(DropdownMenu.Separator, {}) ] }, index); }) }) ] }); }; const MediaImageItem = ({ media_item, index, handleCheckedChange, selection }) => { const isSelected = selection[media_item.id]; return /* @__PURE__ */ jsxs( "div", { className: "shadow-elevation-card-rest hover:shadow-elevation-card-hover transition-fg group relative aspect-square size-full cursor-pointer overflow-hidden rounded-[8px]", children: [ /* @__PURE__ */ jsx( "div", { className: clx( "transition-fg invisible absolute right-2 top-2 opacity-0 group-hover:visible group-hover:opacity-100", { "visible opacity-100": isSelected } ), children: /* @__PURE__ */ jsx( Checkbox, { checked: selection[media_item.id] || false, onCheckedChange: () => handleCheckedChange(media_item.id) } ) } ), media_item.isThumbnail && /* @__PURE__ */ jsx("div", { className: "absolute left-2 top-2", children: /* @__PURE__ */ jsx(Tooltip, { content: "thumbnail", children: /* @__PURE__ */ jsx(ThumbnailBadge, {}) }) }), /* @__PURE__ */ jsx(Link, { to: `media`, state: { curr: index }, children: /* @__PURE__ */ jsx( "img", { src: media_item.url, alt: `category image`, className: "size-full object-cover" } ) }) ] }, media_item.id ); }; const NoMediaImagesFounded = () => { return /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center gap-y-4 pb-8 pt-6", children: [ /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center", children: [ /* @__PURE__ */ jsx( Text, { size: "small", leading: "compact", weight: "plus", className: "text-ui-fg-subtle", children: "No media yet" } ), /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-muted", children: "Add media to the category to showcase it in your storefront." }) ] }), /* @__PURE__ */ jsx(Button, { size: "small", variant: "secondary", asChild: true, children: /* @__PURE__ */ jsx(Link, { to: "media?view=edit", children: "Add media" }) }) ] }); }; const sdk = new Medusa({ baseUrl: __BACKEND_URL__ || "http://localhost:9000", debug: process.env.NODE_ENV === "development", auth: { type: "session" } }); const QUERY_KEYS = { CATEGORY_IMAGES: (category_id) => ["categories-images", category_id] }; const useDeleteProductCategoryImages = () => { const queryClient = useQueryClient(); return useMutation({ mutationFn: async ({ images_id, id }) => { return await sdk.client.fetch( `/admin/product-category/${id}/images`, { method: "DELETE", query: { images_id } } ); }, onSuccess: (updated_images, variables) => { queryClient.setQueryData( QUERY_KEYS.CATEGORY_IMAGES(variables.id), (oldData) => { const cloneOldData = structuredClone(oldData); const updated_images_map = cloneOldData.images.filter( (image) => !variables.images_id.includes(image.id) ).map((image) => { const updatedImage = updated_images.find( (img) => img.id === image.id ); return updatedImage ? { ...image, rank: updatedImage.rank } : image; }).sort((a2, b) => a2.rank - b.rank); return { ...cloneOldData, images: updated_images_map }; } ); }, onError: (error) => { console.error("Error deleting category images:", error); } }); }; const MediaSection = ({ images, param_id }) => { const isImages = images.length > 0; const [selection, setSelection] = useState({}); const { mutateAsync: deleteImages } = useDeleteProductCategoryImages(); const { t: t2 } = useTranslation(); const prompt = usePrompt(); const handleCheckedChange = (id) => { setSelection((prev) => { if (prev[id]) { const { [id]: _, ...rest } = prev; return rest; } else { return { ...prev, [id]: true }; } }); }; const handleDelete = async () => { const ids2 = Object.keys(selection); const includingThumbnail = ids2.some( (id) => { var _a; return (_a = images.find(({ media_item: m }) => m.id === id)) == null ? void 0 : _a.media_item.isThumbnail; } ); const res = await prompt({ title: t2("general.areYouSure"), description: includingThumbnail ? t2("products.media.deleteWarningWithThumbnail", { count: ids2.length }) : t2("products.media.deleteWarning", { count: ids2.length }), confirmText: t2("actions.delete"), cancelText: t2("actions.cancel") }); if (!res) { return; } await deleteImages( { id: param_id, images_id: ids2 }, { onSuccess: () => setSelection({}) } ); }; return /* @__PURE__ */ jsxs(Container, { className: "p-0 divide-y", children: [ /* @__PURE__ */ jsxs("div", { className: "py-4 px-6 flex justify-between items-center", children: [ /* @__PURE__ */ jsx(Heading, { level: "h1", children: "Media" }), /* @__PURE__ */ jsx( ActionMenu, { groups: [ { actions: [ { icon: /* @__PURE__ */ jsx(Pencil, {}), label: "Edit", to: "media?view=edit" } ] } ] } ) ] }), isImages ? /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-[repeat(auto-fill,minmax(96px,1fr))] gap-4 px-6 py-4", children: [ images.map((image, i2) => /* @__PURE__ */ jsx( MediaImageItem, { handleCheckedChange, selection, media_item: image.media_item, index: i2 }, `category-image-item-${i2}` )), /* @__PURE__ */ jsx(CommandBar, { open: !!Object.keys(selection).length, children: /* @__PURE__ */ jsxs(CommandBar.Bar, { children: [ /* @__PURE__ */ jsx(CommandBar.Value, { children: t2("general.countSelected", { count: Object.keys(selection).length }) }), /* @__PURE__ */ jsx(CommandBar.Seperator, {}), /* @__PURE__ */ jsx( CommandBar.Command, { action: handleDelete, label: t2("actions.delete"), shortcut: "d" } ) ] }) }) ] }) : /* @__PURE__ */ jsx(NoMediaImagesFounded, {}) ] }); }; const useListProductCategoryImages = ({ id }) => { return useQuery({ queryKey: QUERY_KEYS.CATEGORY_IMAGES(id), queryFn: async () => { const category_images_res = await sdk.client.fetch(`/admin/product-category/${id}`, { method: "GET" }); if (!category_images_res.length) { return { images: [], thumbnail: "", id: "" }; } return mapCategoryImages(category_images_res, id); } }); }; function mapCategoryImages(categoryImages, category_id) { var _a, _b; const thumbnail = ((_b = (_a = categoryImages[0].product_category) == null ? void 0 : _a.metadata) == null ? void 0 : _b.thumbnail) || null; const images = categoryImages.map(({ id, url, rank }) => ({ id, url, isThumbnail: url === thumbnail, rank })); return { images, thumbnail, id: category_id }; } const Spinner = () => { return /* @__PURE__ */ jsx(Spinner$1, { className: "text-ui-fg-muted animate-spin" }); }; const CategoryImages = () => { const { id } = useParams(); const { data: category, isLoading } = useListProductCategoryImages({ id }); const ready = !isLoading && !!category; return /* @__PURE__ */ jsx(Fragment, { children: ready ? /* @__PURE__ */ jsx( MediaSection, { param_id: id, images: category.images.map(({ isThumbnail, url, id: id2 }, i2) => ({ media_item: { id: id2, url, isThumbnail }, index: i2 })) } ) : /* @__PURE__ */ jsx(Container, { children: /* @__PURE__ */ jsx(Spinner, {}) }) }); }; defineWidgetConfig({ zone: "product_category.details.after" }); const useStateAwareTo = (prev) => { const location = useLocation(); const to = useMemo(() => { var _a; const params = (_a = location.state) == null ? void 0 : _a.restore_params; if (!params) { return prev; } return `${prev}?${params.toString()}`; }, [location.state, prev]); return to; }; const Provider = FormProvider; const FormFieldContext = createContext( {} ); const Field = ({ ...props }) => { return /* @__PURE__ */ jsx(FormFieldContext.Provider, { value: { name: props.name }, children: /* @__PURE__ */ jsx(Controller, { ...props }) }); }; const FormItemContext = createContext( {} ); const useFormField = () => { const fieldContext = useContext(FormFieldContext); const itemContext = useContext(FormItemContext); const { getFieldState } = useFormContext(); const formState = useFormState({ name: fieldContext.name }); const fieldState = getFieldState(fieldContext.name, formState); if (!fieldContext) { throw new Error("useFormField should be used within a FormField"); } const { id } = itemContext; return { id, name: fieldContext.name, formItemId: `${id}-form-item`, formLabelId: `${id}-form-item-label`, formDescriptionId: `${id}-form-item-description`, formErrorMessageId: `${id}-form-item-message`, ...fieldState }; }; const Item = forwardRef( ({ className, ...props }, ref) => { const id = useId(); return /* @__PURE__ */ jsx(FormItemContext.Provider, { value: { id }, children: /* @__PURE__ */ jsx( "div", { ref, className: clx("flex flex-col space-y-2", className), ...props } ) }); } ); Item.displayName = "Form.Item"; const Label = forwardRef(({ className, optional = false, tooltip, icon, ...props }, ref) => { const { formLabelId, formItemId } = useFormField(); const { t: t2 } = useTranslation(); return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-x-1", children: [ /* @__PURE__ */ jsx( Label$1, { id: formLabelId, ref, className: clx(className), htmlFor: formItemId, size: "small", weight: "plus", ...props } ), tooltip && /* @__PURE__ */ jsx(Tooltip, { content: tooltip, children: /* @__PURE__ */ jsx(InformationCircleSolid, { className: "text-ui-fg-muted" }) }), icon, optional && /* @__PURE__ */ jsxs(Text, { size: "small", leading: "compact", className: "text-ui-fg-muted", children: [ "(", t2("fields.optional"), ")" ] }) ] }); }); Label.displayName = "Form.Label"; const Control = forwardRef(({ ...props }, ref) => { const { error, formItemId, formDescriptionId, formErrorMessageId, formLabelId } = useFormField(); return /* @__PURE__ */ jsx( Slot.Root, { ref, id: formItemId, "aria-describedby": !error ? `${formDescriptionId}` : `${formDescriptionId} ${formErrorMessageId}`, "aria-invalid": !!error, "aria-labelledby": formLabelId, ...props } ); }); Control.displayName = "Form.Control"; const Hint = forwardRef(({ className, ...props }, ref) => { const { formDescriptionId } = useFormField(); return /* @__PURE__ */ jsx( Hint$1, { ref, id: formDescriptionId, className, ...props } ); }); Hint.displayName = "Form.Hint"; const ErrorMessage = forwardRef(({ className, children, ...props }, ref) => { const { error, formErrorMessageId } = useFormField(); const msg = error ? String(error == null ? void 0 : error.message) : children; if (!msg || msg === "undefined") { return null; } return /* @__PURE__ */ jsx( Hint$1, { ref, id: formErrorMessageId, className, variant: error ? "error" : "info", ...props, children: msg } ); }); ErrorMessage.displayName = "Form.ErrorMessage"; const Form$2 = Object.assign(Provider, { Item, Label, Control, Hint, ErrorMessage, Field }); const RouteModalForm = ({ form, blockSearchParams: blockSearch = false, children, onClose }) => { const { t: t2 } = useTranslation(); const { formState: { isDirty } } = form; const blocker = useBlocker(({ currentLocation, nextLocation }) => { const { isSubmitSuccessful } = nextLocation.state || {}; if (isSubmitSuccessful) { onClose == null ? void 0 : onClose(true); return false; } const isPathChanged = currentLocation.pathname !== nextLocation.pathname; const isSearchChanged = currentLocation.search !== nextLocation.search; if (blockSearch) { const shouldBlock2 = isDirty && (isPathChanged || isSearchChanged); if (isPathChanged) { onClose == null ? void 0 : onClose(isSubmitSuccessful); } return shouldBlock2; } const shouldBlock = isDirty && isPathChanged; if (isPathChanged) { onClose == null ? void 0 : onClose(isSubmitSuccessful); } return shouldBlock; }); const handleCancel = () => { var _a; (_a = blocker == null ? void 0 : blocker.reset) == null ? void 0 : _a.call(blocker); }; const handleContinue = () => { var _a; (_a = blocker == null ? void 0 : blocker.proceed) == null ? void 0 : _a.call(blocker); onClose == null ? void 0 : onClose(false); }; return /* @__PURE__ */ jsxs(Form$2, { ...form, children: [ children, /* @__PURE__ */ jsx(Prompt, { open: blocker.state === "blocked", variant: "confirmation", children: /* @__PURE__ */ jsxs(Prompt.Content, { children: [ /* @__PURE__ */ jsxs(Prompt.Header, { children: [ /* @__PURE__ */ jsx(Prompt.Title, { children: t2("general.unsavedChangesTitle") }), /* @__PURE__ */ jsx(Prompt.Description, { children: t2("general.unsavedChangesDescription") }) ] }), /* @__PURE__ */ jsxs(Prompt.Footer, { children: [ /* @__PURE__ */ jsx(Prompt.Cancel, { onClick: handleCancel, type: "button", children: t2("actions.cancel") }), /* @__PURE__ */ jsx(Prompt.Action, { onClick: handleContinue, type: "button", children: t2("actions.continue") }) ] }) ] }) }) ] }); }; const RouteModalProviderContext = createContext(null); const RouteModalProvider = ({ prev, children }) => { const navigate = useNavigate(); const [closeOnEscape, setCloseOnEscape] = useState(true); const handleSuccess = useCallback( (path) => { const to = path || prev; navigate(to, { replace: true, state: { isSubmitSuccessful: true } }); }, [navigate, prev] ); const value = useMemo( () => ({ handleSuccess, setCloseOnEscape, __internal: { closeOnEscape } }), [handleSuccess, setCloseOnEscape, closeOnEscape] ); return /* @__PURE__ */ jsx(RouteModalProviderContext.Provider, { value, children }); }; const StackedModalContext = createContext(null); const StackedModalProvider = ({ children, onOpenChange }) => { const [state, setState] = useState({}); const getIsOpen = (id) => { return state[id] || false; }; const setIsOpen = (id, open) => { setState((prevState) => ({ ...prevState, [id]: open })); onOpenChange(open); }; const register = (id) => { setState((prevState) => ({ ...prevState, [id]: false })); }; const unregister = (id) => { setState((prevState) => { const newState = { ...prevState }; delete newState[id]; return newState; }); }; return /* @__PURE__ */ jsx( StackedModalContext.Provider, { value: { getIsOpen, setIsOpen, register, unregister }, children } ); }; const useStackedModal = () => { const context = useContext(StackedModalContext); if (!context) { throw new Error( "useStackedModal must be used within a StackedModalProvider" ); } return context; }; const Root$3 = ({ prev = "..", children }) => { const navigate = useNavigate(); const [open, setOpen] = useState(false); const [stackedModalOpen, onStackedModalOpen] = useState(false); const to = useStateAwareTo(prev); useEffect(() => { setOpen(true); return () => { setOpen(false); onStackedModalOpen(false); }; }, []); const handleOpenChange = (open2) => { if (!open2) { document.body.style.pointerEvents = "auto"; navigate(to, { replace: true }); return; } setOpen(open2); }; return /* @__PURE__ */ jsx(Drawer, { open, onOpenChange: handleOpenChange, children: /* @__PURE__ */ jsx(RouteModalProvider, { prev: to, children: /* @__PURE__ */ jsx(StackedModalProvider, { onOpenChange: onStackedModalOpen, children: /* @__PURE__ */ jsx( Drawer.Content, { "aria-describedby": void 0, className: clx({ "!bg-ui-bg-disabled !inset-y-5 !right-5": stackedModalOpen }), children } ) }) }) }); }; const Header$3 = Drawer.Header; const Title$3 = Drawer.Title; const Description$3 = Drawer.Description; const Body$3 = Drawer.Body; const Footer$3 = Drawer.Footer; const Close$3 = Drawer.Close; const Form$1 = RouteModalForm; Object.assign(Root$3, { Header: Header$3, Title: Title$3, Body: Body$3, Description: Description$3, Footer: Footer$3, Close: Close$3, Form: Form$1 }); const useRouteModal = () => { const context = useContext(RouteModalProviderContext); if (!context) { throw new Error("useRouteModal must be used within a RouteModalProvider"); } return context; }; const Root$2 = ({ prev = "..", children }) => { const navigate = useNavigate(); const [open, setOpen] = useState(false); const [stackedModalOpen, onStackedModalOpen] = useState(false); const to = useStateAwareTo(prev); useEffect(() => { setOpen(true); return () => { setOpen(false); onStackedModalOpen(false); }; }, []); const handleOpenChange = (open2) => { if (!open2) { document.body.style.pointerEvents = "auto"; navigate(to, { replace: true }); return; } setOpen(open2); }; return /* @__PURE__ */ jsx(FocusModal, { open, onOpenChange: handleOpenChange, children: /* @__PURE__ */ jsx(RouteModalProvider, { prev: to, children: /* @__PURE__ */ jsx(StackedModalProvider, { onOpenChange: onStackedModalOpen, children: /* @__PURE__ */ jsx(Content$2, { stackedModalOpen, children }) }) }) }); }; const Content$2 = ({ stackedModalOpen, children }) => { const { __internal } = useRouteModal(); const shouldPreventClose = !__internal.closeOnEscape; return /* @__PURE__ */ jsx( FocusModal.Content, { onEscapeKeyDown: shouldPreventClose ? (e2) => { e2.preventDefault(); } : void 0, className: clx({ "!bg-ui-bg-disabled !inset-x-5 !inset-y-3": stackedModalOpen }), children } ); }; const Header$2 = FocusModal.Header; const Title$2 = FocusModal.Title; const Description$2 = FocusModal.Description; const Footer$2 = FocusModal.Footer; const Body$2 = FocusModal.Body; const Close$2 = FocusModal.Close; const Form = RouteModalForm; const RouteFocusModal = Object.assign(Root$2, { Header: Header$2, Title: Title$2, Body: Body$2, Description: Description$2, Footer: Footer$2, Close: Close$2, Form }); const Root$1 = ({ id, children }) => { const { register, unregister, getIsOpen, setIsOpen } = useStackedModal(); useEffect(() => { register(id); return () => unregister(id); }, []); return /* @__PURE__ */ jsx(Drawer, { open: getIsOpen(id), onOpenChange: (open) => setIsOpen(id, open), children }); }; const Close$1 = Drawer.Close; Close$1.displayName = "StackedDrawer.Close"; const Header$1 = Drawer.Header; Header$1.displayName = "StackedDrawer.Header"; const Body$1 = Drawer.Body; Body$1.displayName = "StackedDrawer.Body"; const Trigger$1 = Drawer.Trigger; Trigger$1.displayName = "StackedDrawer.Trigger"; const Footer$1 = Drawer.Footer; Footer$1.displayName = "StackedDrawer.Footer"; const Title$1 = Drawer.Title; Title$1.displayName = "StackedDrawer.Title"; const Description$1 = Drawer.Description; Description$1.displayName = "StackedDrawer.Description"; const Content$1 = forwardRef(({ className, ...props }, ref) => { return /* @__PURE__ */ jsx( Drawer.Content, { ref, className: clx(className), overlayProps: { className: "bg-transparent" }, ...props } ); }); Content$1.displayName = "StackedDrawer.Content"; Object.assign(Root$1, { Close: Close$1, Header: Header$1, Body: Body$1, Content: Content$1, Trigger: Trigger$1, Footer: Footer$1, Description: Description$1, Title: Title$1 }); const Root = ({ id, onOpenChangeCallback, children }) => { const { register, unregister, getIsOpen, setIsOpen } = useStackedModal(); useEffect(() => { register(id); return () => unregister(id); }, []); const handleOpenChange = (open) => { setIsOpen(id, open); onOpenChangeCallback == null ? void 0 : onOpenChangeCallback(open); }; return /* @__PURE__ */ jsx(FocusModal, { open: getIsOpen(id), onOpenChange: handleOpenChange, children }); }; const Close = FocusModal.Close; Close.displayName = "StackedFocusModal.Close"; const Header = FocusModal.Header; Header.displayName = "StackedFocusModal.Header"; const Body = FocusModal.Body; Body.displayName = "StackedFocusModal.Body"; const Trigger = FocusModal.Trigger; Trigger.displayName = "StackedFocusModal.Trigger"; const Footer = FocusModal.Footer; Footer.displayName = "StackedFocusModal.Footer"; const Title = FocusModal.Title; Title.displayName = "StackedFocusModal.Title"; const Description = FocusModal.Description; Description.displayName = "StackedFocusModal.Description"; const Content = forwardRef(({ className, ...props }, ref) => { return /* @__PURE__ */ jsx( FocusModal.Content, { ref, className: clx("!top-6", className), overlayProps: { className: "bg-transparent" }, ...props } ); }); Content.displayName = "StackedFocusModal.Content"; Object.assign(Root, { Close, Header, Body, Content, Trigger, Footer, Description, Title }); const MediaViewContext = createContext( null ); const MediaGallery = ({ media: media_, category_id }) => { const { state } = useLocation(); const [curr, setCurr] = useState((state == null ? void 0 : state.curr) || 0); const { mutate: deleteImages, isPending } = useDeleteProductCategoryImages(); const { t: t2 } = useTranslation(); const prompt = usePrompt(); const media = media_.images; const next = useCallback(() => { if (isPending) { return; } setCurr((prev2) => (prev2 + 1) % media.length); }, [media, isPending]); const prev = useCallback(() => { if (isPending) { return; } setCurr((prev2) => (prev2 - 1 + media.length) % media.length); }, [media, isPending]); const goTo = useCallback( (index) => { if (isPending) { return; } setCurr(index); }, [isPending] ); const handleDownloadCurrent = () => { if (isPending) { return; } const a2 = document.createElement("a"); a2.href = media[curr].url; a2.download = "image"; a2.target = "_blank"; a2.click(); }; const handleDeleteCurrent = async () => { const current = media[curr]; const res = await prompt({ title: t2("general.areYouSure"), description: current.isThumbnail ? t2("products.media.deleteWarningWithThumbnail", { count: 1 }) : t2("products.media.deleteWarning", { count: 1 }), confirmText: t2("actions.delete"), cancelText: t2("actions.cancel") }); if (!res) { return; } if (curr === media.length - 1) { setCurr((prev2) => prev2 - 1); } deleteImages({ id: category_id, images_id: [current.id] }); }; useEffect(() => { const handleKeyDown = (e2) => { if (e2.key === "ArrowRight") { next(); } else if (e2.key === "ArrowLeft") { prev(); } }; document.addEventListener("keydown", handleKeyDown); return () => { document.removeEventListener("keydown", handleKeyDown); }; }, [next, prev]); const noMedia = !media.length; return /* @__PURE__ */ jsxs("div", { className: "flex size-full flex-col overflow-hidden", children: [ /* @__PURE__ */ jsx(RouteFocusModal.Header, { children: /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-end gap-x-2", children: [ /* @__PURE__ */ jsxs( IconButton, { size: "small", type: "button", onClick: handleDeleteCurrent, disabled: noMedia, children: [ /* @__PURE__ */ jsx(Trash, {}), /* @__PURE__ */ jsx("span", { className: "sr-only", children: "Delete" }) ] } ), /* @__PURE__ */ jsxs( IconButton, { size: "small", type: "button", onClick: handleDownloadCurrent, disabled: noMedia, children: [ /* @__PURE__ */ jsx(ArrowDownTray, {}), /* @__PURE__ */ jsx("span", { className: "sr-only", children: "Download" }) ] } ), /* @__PURE__ */ jsx(Button, { variant: "secondary", size: "small", asChild: true, children: /* @__PURE__ */ jsx(Link, { to: { pathname: ".", search: "view=edit" }, children: "Edit" }) }) ] }) }), /* @__PURE__ */ jsxs(RouteFocusModal.Body, { className: "flex flex-col overflow-hidden", children: [ /* @__PURE__ */ jsx(Canvas, { curr, media }), /* @__PURE__ */ jsx( Preview, { curr, media, prev, next, goTo } ) ] }) ] }); }; const Canvas = ({ media, curr }) => { const { t: t2 } = useTranslation(); if (media.length === 0) { return /* @__PURE__ */ jsxs("div", { className: "bg-ui-bg-subtle flex size-full flex-col items-center justify-center gap-y-4 pb-8 pt-6", children: [ /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center", children: [ /* @__PURE__ */ jsx( Text, { size: "small", leading: "compact", weight: "plus", className: "text-ui-fg-subtle", children: t2("products.media.emptyState.header") } ), /* @__PURE__ */ jsx(Text, { size: "small", className: "text-ui-fg-muted", children: t2("products.media.emptyState.description") }) ] }), /* @__PURE__ */ jsx(Button, { size: "small", variant: "secondary", asChild: true, children: /* @__PURE__ */ jsx(Link, { to: "?view=edit", children: t2("products.media.emptyState.action") }) }) ] }); } return /* @__PURE__ */ jsx("div", { className: "bg-ui-bg-subtle relative size-full overflow-hidden", children: /* @__PURE__ */ jsx("div", { className: "flex size-full items-center justify-center p-6", children: /* @__PURE__ */ jsxs("div", { className: "relative inline-block max-h-full max-w-full", children: [ media[curr].isThumbnail && /* @__PURE__ */ jsx("div", { className: "absolute left-2 top-2", children: /* @__PURE__ */ jsx(Tooltip, { content: t2("products.media.thumbnailTooltip"), children: /* @__PURE__ */ jsx(ThumbnailBadge, {}) }) }), /* @__PURE__ */ jsx( "img", { src: media[curr].url, alt: "", className: "object-fit shadow-elevation-card-rest max-h-[calc(100vh-200px)] w-auto rounded-xl object-contain" } ) ] }) }) }); }; const MAX_VISIBLE_ITEMS = 8; const Preview = ({ media, curr, prev, next, goTo }) => { if (!media.length) { return null; } const getVisibleItems = (media2, index) => { if (media2.length <= MAX_VISIBLE_ITEMS) { return media2; } const half = Math.floor(MAX_VISIBLE_ITEMS / 2); const start = (index - half + media2.length) % media2.length; const end = (start + MAX_VISIBLE_ITEMS) % media2.length; if (end < start) { return [...media2.slice(start), ...media2.slice(0, end)]; } else { return media2.slice(start, end); } }; const visibleItems = getVisibleItems(media, curr); return /* @__PURE__ */ jsxs("div", { className: "flex shrink-0 items-center justify-center gap-x-2 border-t p-3", children: [ /* @__PURE__ */ jsx( IconButton, { size: "small", variant: "transparent", className: "text-ui-fg-muted", type: "button", onClick: prev, children: /* @__PURE__ */ jsx(TriangleLeftMini, {}) } ), /* @__PURE__ */ jsx("div", { className: "flex items-center gap-x-2", children: visibleItems.map((item) => { const isCurrentImage = item.id === media[curr].id; const originalIndex = media.findIndex((i2) => i2.id === item.id); return /* @__PURE__ */ jsx( "button", { type: "button", onClick: () => goTo(originalIndex), className: clx( "transition-fg size-7 overflow-hidden rounded-[4px] outline-none", { "shadow-borders-focus": isCurrentImage } ), children: /* @__PURE__ */ jsx("img", { src: item.url, alt: "", className: "size-full object-cover" }) }, item.id ); }) }), /* @__PURE__ */ jsx( IconButton, { size: "small", variant: "transparent", className: "text-ui-fg-muted", type: "button", onClick: next, children: /* @__PURE__ */ jsx(TriangleRightMini, {}) } ) ] }); }; function useCombinedRefs() { for (var _len = arguments.length, refs = new Array(_len), _key = 0; _key < _len; _key++) { refs[_key] = arguments[_key]; } return useMemo( () => (node) => { refs.forEach((ref) => ref(node)); }, // eslint-disable-next-line react-hooks/exhaustive-deps refs ); } const canUseDOM = typeof window !== "undefined" && typeof window.document !== "undefined" && typeof window.document.createElement !== "undefined"; function isWindow(element) { const elementString = Object.prototype.toString.call(element); return elementString === "[object Window]" || // In Electron context the Window object serializes to [object global] elementString === "[object global]"; } function isNode(node) { return "nodeType" in node; } function getWindow(target) { var _target$ownerDocument, _target$ownerDocument2; if (!target) { return window; } if (isWindow(target)) { return target; } if (!isNode(target)) { return window; } return (_target$ownerDocument = (_target$ownerDocument2 = target.ownerDocument) == null ? void 0 : _target$ownerDocument2.defaultView) != null ? _target$ownerDocument : window; } function isDocument(node) { const { Document } = getWindow(node); return node instanceof Document; } function isHTMLElement(node) { if (isWindow(node)) { return false; } return node instanceof getWindow(node).HTMLElement; } function isSVGElement(node) { return node instanceof getWindow(node).SVGElement; } function getOwnerDocument(target) { if (!target) { return document; } if (isWindow(target)) { return target.document; } if (!isNode(target)) { return document; } if (isDocument(target)) { return target; } if (isHTMLElement(target) || isSVGElement(target)) { return target.ownerDocument; } return document; } const useIsomorphicLayoutEffect = canUseDOM ? useLayoutEffect : useEffect; function useEvent(handler) { const handlerRef = useRef(handler); useIsomorphicLayoutEffect(() => { handlerRef.current = handler; }); return useCallback(function() { for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } return handlerRef.current == null ? void 0 : handlerRef.current(...args); }, []); } function useInterval() { const intervalRef = useRef(null); const set = useCallback((listener, duration) => { intervalRef.current = setInterval(listener, duration); }, []); const clear = useCallback(() => { if (intervalRef.current !== null) { clearInterval(intervalRef.current); intervalRef.current = null; } }, []); return [set, clear]; } function useLatestValue(value, dependencies) { if (dependencies === void 0) { dependencies = [value]; } const valueRef = useRef(value); useIsomorphicLayoutEffect(() => { if (valueRef.current !== value) { valueRef.current = value; } }, dependencies); return valueRef; } function useLazyMemo(callback, dependencies) { const valueRef = useRef(); return useMemo( () => { const newValue = callback(valueRef.current); valueRef.current = newValue; return newValue; }, // eslint-disable-next-line react-hooks/exhaustive-deps [...dependencies] ); } function useNodeRef(onChange) { const onChangeHandler = useEvent(onChange); const node = useRef(null); const setNodeRef = useCallback( (element) => { if (element !== node.current) { onChangeHandler == null ? void 0 : onChangeHandler(element, node.current); } node.current = element; }, //eslint-disable-next-line [] ); return [node, setNodeRef]; } function usePrevious(value) { const ref = useRef(); useEffect(() => { ref.current = value; }, [value]); return ref.current; } let ids = {}; function useUniqueId(prefix, value) { return useMemo(() => { if (value) { return value; } const id = ids[prefix] == null ? 0 : ids[prefix] + 1; ids[prefix] = id; return prefix + "-" + id; }, [prefix, value]); } function createAdjustmentFn(modifier) { return function(object) { for (var _len = arguments.length, adjustments = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { adjustments[_key - 1] = arguments[_key]; } return adjustments.reduce((accumulator, adjustment) => { const entries = Object.entries(adjustment); for (const [key2, valueAdjustment] of entries) { const value = accumulator[key2]; if (value != null) { accumulator[key2] = value + modifier * valueAdjustment; } } return accumulator; }, { ...object }); }; } const add = /* @__PURE__ */ createAdjustmentFn(1); const subtract = /* @__PURE__ */ createAdjustmentFn(-1); function hasViewportRelativeCoordinates(event) { return "clientX" in event && "clientY" in event; } function isKeyboardEvent(event) { if (!event) { return false; } const { KeyboardEvent } = getWindow(event.target); return KeyboardEvent && event instanceof KeyboardEvent; } function isTouchEvent(event) { if (!event) { return false; } const { TouchEvent } = getWindow(event.target); return TouchEvent && event instanceof TouchEvent; } function getEventCoordinates(event) { if (isTouchEvent(event)) { if (event.touches && event.touches.length) { const { clientX: x, clientY: y } = event.touches[0]; return { x, y }; } else if (event.changedTouches && event.changedTouches.length) { const { clientX: x, clientY: y } = event.changedTouches[0]; return { x, y }; } } if (hasViewportRelativeCoordinates(event)) { return { x: event.clientX, y: event.clientY }; } return null; } const CSS = /* @__PURE__ */ Object.freeze({ Translate: { toString(transform) { if (!transform) { return; } const { x, y } = transform; return "translate3d(" + (x ? Math.round(x) : 0) + "px, " + (y ? Math.round(y) : 0) + "px, 0)"; } }, Scale: { toString(transform) { if (!transform) { return; } const { scaleX, scaleY } = transform; return "scaleX(" + scaleX + ") scaleY(" + scaleY + ")"; } }, Transform: { toString(transform) { if (!transform) { return; } return [CSS.Translate.toString(transform), CSS.Scale.toString(transform)].join(" "); } }, Transition: { toString(_ref) { let { property, duration, easing } = _ref; return property + " " + duration + "ms " + easing; } } }); const SELECTOR = "a,frame,iframe,input:not([type=hidden]):not(:disabled),select:not(:disabled),textarea:not(:disabled),button:not(:disabled),*[tabindex]"; function findFirstFocusableNode(element) { if (element.matches(SELECTOR)) { return element; } return element.querySelector(SELECTOR); } const hiddenStyles = { display: "none" }; function HiddenText(_ref) { let { id, value } = _ref; return React.createElement("div", { id, style: hiddenStyles }, value); } function LiveRegion(_ref) { let { id, announcement, ariaLiveType = "assertive" } = _ref; const visuallyHidden = { position: "fixed", top: 0, left: 0, width: 1, height: 1, margin: -1, border: 0, padding: 0, overflow: "hidden", clip: "rect(0 0 0 0)", clipPath: "inset(100%)", whiteSpace: "nowrap" }; return React.createElement("div", { id, style: visuallyHidden, role: "status", "aria-live": ariaLiveType, "aria-atomic": true }, announcement); } function useAnnouncement() { const [announcement, setAnnouncement] = useState(""); const announce = useCallback((value) => { if (value != null) { setAnnouncement(value); } }, []); return { announce, announcement }; } const DndMonitorContext = /* @__PURE__ */ createContext(null); function useDndMonitor(listener) { const registerListener = useContext(DndMonitorContext); useEffect(() => { if (!registerListener) { throw new Error("useDndMonitor must be used within a children of <DndContext>"); } const unsubscribe = registerListener(listener); return unsubscribe; }, [listener, registerListener]); } function useDndMonitorProvider() { const [listeners] = useState(() => /* @__PURE__ */ new Set()); const registerListener = useCallback((listener) => { listeners.add(listener); return () => listeners.delete(listener); }, [listeners]); const dispatch = useCallback((_ref) => { let { type, event } = _ref; listeners.forEach((listener) => { var _listener$type; return (_listener$type = listener[type]) == null ? void 0 : _listener$type.call(listener, event); }); }, [listeners]); return [dispatch, registerListener]; } const defaultScreenReaderInstructions = { draggable: "\n To pick up a draggable item, press the space bar.\n While dragging, use the arrow keys to move the item.\n Press space again to drop the item in its new position, or press escape to cancel.\n " }; const defaultAnnouncements = { onDragStart(_ref) { let { active } = _ref; return "Picked up draggable item " + active.id + "."; }, onDragOver(_ref2) { let { active, over } = _ref2; if (over) { return "Draggable item " + active.id + " was moved over droppable area " + over.id + "."; } return "Draggable item " + active.id + " is no longer over a droppable area."; }, onDragEnd(_ref3) { let { active, over } = _ref3; if (over) { return "Draggable item " + active.id + " was dropped over droppable area " + over.id; } return "Draggable item " + active.id + " was dropped."; }, onDragCancel(_ref4) { let { active } = _ref4; return "Dragging was cancelled. Draggable item " + active.id + " was dropped."; } }; function Accessibility(_ref) { let { announcements = defaultAnnouncements, container, hiddenTextDescribedById, screenReaderInstructions = defaultScreenReaderInstructions } = _ref; const { announce, announcement } = useAnnouncement(); const liveRegionId = useUniqueId("DndLiveRegion"); const [mounted, setMounted] = useState(false); useEffect(() => { setMounted(true); }, []); useDndMonitor(useMemo(() => ({ onDragStart(_ref2) { let { active } = _ref2; announce(announcements.onDragStart({ active })); }, onDragMove(_ref3) { let { active, over } = _ref3; if (announcements.onDragMove) { announce(announcements.onDragMove({ active, over })); } }, onDragOver(_ref4) { let { active, over } = _ref4; announce(announcements.onDragOver({ active, over })); }, onDragEnd(_ref5) { let { active, over } = _ref5; announce(announcements.onDragEnd({ active, over })); }, onDragCancel(_ref6) { let { active, over } = _ref6; announce(announcements.onDragCancel({ active, over })); } }), [announce, announcements])); if (!mounted) { return null; } const markup = React.createElement(React.Fragment, null, React.createElement(HiddenText, { id: hiddenTextDescribedById, value: screenReaderInstructions.draggable }), React.createElement(LiveRegion, { id: liveRegionId, announcement })); return container ? createPortal(markup, container) : markup; } var Action; (function(Action2) { Action2["DragStart"] = "dragStart"; Action2["DragMove"] = "dragMove"; Action2["DragEnd"] = "dragEnd"; Action2["DragCancel"] = "dragCancel"; Action2["DragOver"] = "dragOver"; Action2["RegisterDroppable"] = "registerDroppable"; Action2["SetDroppableDisabled"] = "setDroppableDisabled"; Action2["UnregisterDroppable"] = "unregisterDroppable"; })(Action || (Action = {})); function noop() { } function useSensor(sensor, options) { return useMemo( () => ({ sensor, options: options != null ? options : {} }), // eslint-disable-next-line react-hooks/exhaustive-deps [sensor, options] ); } function useSensors() { for (var _len = arguments.length, sensors = new Array(_len), _key = 0; _key < _len; _key++) { sensors[_key] = arguments[_key]; } return useMemo( () => [...sensors].filter((sensor) => sensor != null), // eslint-disable-next-line react-hooks/exhaustive-deps [...sensors] ); } const defaultCoordinates = /* @__PURE__ */ Object.freeze({ x: 0, y: 0 }); function distanceBetween(p1, p2) { return Math.sqrt(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2)); } function getRelativeTransformOrigin(event, rect) { const eventCoordinates = getEventCoordinates(event); if (!eventCoordinates) { return "0 0"; } const transformOrigin = { x: (eventCoordinates.x - rect.left) / rect.width * 100, y: (eventCoordinates.y - rect.top) / rect.height * 100 }; return transformOrigin.x + "% " + transformOrigin.y + "%"; } function sortCollisionsAsc(_ref, _ref2) { let { data: { value: a2 } } = _ref; let { data: { value: b } } = _ref2; return a2 - b; } function sortCollisionsDesc(_ref3, _ref4) { let { data: { value: a2 } } = _ref3; let { data: { value: b } } = _ref4; return b - a2; } function cornersOfRectangle(_ref5) { let { left, top, height, width } = _ref5; return [{ x: left, y: top }, { x: left + width, y: top }, { x: left, y: top + height }, { x: left + width, y: top + height }]; } function getFirstCollision(collisions, property) { if (!collisions || collisions.length === 0) { return null; } const [firstCollision] = collisions; return firstCollision[property]; } const closestCorners = (_ref) => { let { collisionRect, droppableRects, droppableContainers } = _ref; const corners = cornersOfRectangle(collisionRect); const collisions = []; for (const droppableContainer of droppableContainers) { const { id } = droppableContainer; const rect = droppableRects.get(id); if (rect) { const rectCorners = cornersOfRectangle(rect); const distances = corners.reduce((accumulator, corner, index) => { return accumulator + distanceBetween(rectCorners[index], corner); }, 0); const effectiveDistance = Number((distances / 4).toFixed(4)); collisions.push({ id, data: { droppableContainer, value: effectiveDistance } }); } } return collisions.sort(sortCollisionsAsc); }; function getIntersectionRatio(entry, target) { const top = Math.max(target.top, entry.top); const left = Math.max(target.left, entry.left); const right = Math.min(target.left + target.width, entry.left + entry.width); const bottom = Math.min(target.top + target.height, entry.top + entry.height); const width = right - left; const height = bottom - top; if (left < right && top < bottom) { const targetArea = target.width * target.height; const entryArea = entry.width * entry.height; const intersectionArea = width * height; const intersectionRatio = intersectionArea / (targetArea + entryArea - intersectionArea); return Number(intersectionRatio.toFixed(4)); } return 0; } const rectIntersection = (_ref) => { let { collisionRect, droppableRects, droppableContainers } = _ref; const collisions = []; for (const droppableContainer of droppableContainers) { const { id } = droppableContainer; const rect = droppableRects.get(id); if (rect) { const intersectionRatio = getIntersectionRatio(rect, collisionRect); if (intersectionRatio > 0) { collisions.push({ id, data: { droppableContainer, value: intersectionRatio } }); } } } return collisions.sort(sortCollisionsDesc); }; function adjustScale(transform, rect1, rect2) { return { ...transform, scaleX: rect1 && rect2 ? rect1.width / rect2.width : 1, scaleY: rect1 && rect2 ? rect1.height / rect2.height : 1 }; } function getRect