{ "version": 3, "sources": ["../../../../node_modules/lodash/_baseDifference.js", "../../../../node_modules/lodash/difference.js", "../../../javascript/entrypoints/warp/product-form.tsx", "../../../javascript/warp/context/MarketContext.ts", "../../../javascript/warp/context/BrowsingOutOfRegionContext.ts", "../../../javascript/warp/components/pdp/BasicProduct/index.tsx", "../../../javascript/warp/components/BrowsingOutOfRegion.tsx", "../../../javascript/warp/components/OutOfStock.tsx", "../../../javascript/warp/components/ActionButton.tsx", "../../../javascript/warp/utilities/createNotificationRequest.ts", "../../../javascript/warp/components/pdp/BasicProduct/ProductSubmit.tsx", "../../../javascript/warp/components/pdp/BasicProduct/QuantityPicker.tsx", "../../../javascript/warp/components/pdp/BasicProduct/AddToBasket.tsx", "../../../javascript/warp/utilities/localeParams.ts", "../../../javascript/warp/utilities/api.ts", "../../../javascript/warp/components/pdp/VariantSelector/MultipleVariantSelector/index.tsx", "../../../javascript/warp/context/MultipleVariantSelectorContext.tsx", "../../../javascript/warp/components/pdp/VariantSelector/MultipleVariantSelector/MultipleVariantSelectorDialog.tsx", "../../../javascript/warp/components/pdp/VariantSelector/MultipleVariantSelector/OptionSelector.tsx", "../../../javascript/warp/components/pdp/VariantSelector/MultipleVariantSelector/Option.tsx", "../../../javascript/warp/components/pdp/VariantSelector/MultipleVariantSelector/Checkbox.tsx", "../../../javascript/warp/utilities/product.ts", "../../../javascript/warp/components/pdp/VariantSelector/QuantityPicker.tsx", "../../../javascript/warp/components/pdp/Price.tsx", "../../../javascript/warp/components/pdp/VariantSelector/MultipleVariantSelector/Footer.tsx", "../../../javascript/warp/components/pdp/VariantSelector/Slideshow.tsx", "../../../javascript/warp/utilities/translation.ts", "../../../javascript/warp/components/pdp/StitchYourPhoto/index.tsx", "../../../javascript/warp/components/Stepper/index.tsx", "../../../javascript/warp/context/StepperContext.tsx", "../../../javascript/warp/components/Stepper/Step.tsx", "../../../javascript/warp/components/pdp/StitchYourPhoto/Colour.tsx", "../../../javascript/warp/context/StitchYourPhotoContext.tsx", "../../../javascript/warp/components/pdp/StitchYourPhoto/Defaults.ts", "../../../javascript/warp/components/pdp/StitchYourPhoto/Services.ts", "../../../javascript/warp/components/Stepper/SubStep.tsx", "../../../javascript/warp/components/pdp/StitchYourPhoto/Format.tsx", "../../../javascript/warp/icons/QuestionMark.tsx", "../../../javascript/warp/components/pdp/StitchYourPhoto/Thread.tsx", "../../../javascript/warp/components/pdp/StitchYourPhoto/Tools.tsx", "../../../javascript/warp/components/pdp/StitchYourPhoto/Tool.tsx", "../../../javascript/warp/components/pdp/StitchYourPhoto/Upload.tsx", "../../../javascript/warp/icons/Quality.tsx", "../../../javascript/warp/components/pdp/StitchYourPhoto/StitchYourPhotoDialog.tsx", "../../../javascript/warp/components/WizardDialog/index.tsx", "../../../javascript/warp/components/WizardDialog/StepContent.tsx", "../../../javascript/warp/components/pdp/StitchYourPhoto/Footer.tsx", "../../../javascript/warp/icons/ArrowIcon.tsx", "../../../javascript/warp/components/pdp/VariantSelector/VariantSelectorWizard/index.tsx", "../../../javascript/warp/context/VariantSelectorWizardContext.tsx", "../../../javascript/warp/components/pdp/VariantSelector/VariantSelectorWizard/VariantSelectorWizardDialog.tsx", "../../../javascript/warp/components/pdp/VariantSelector/VariantSelectorWizard/WizardDialog.tsx", "../../../javascript/warp/components/pdp/VariantSelector/VariantSelectorWizard/Footer.tsx", "../../../javascript/warp/components/pdp/VariantSelector/VariantSelectorWizard/SelectedVariantsHeader.tsx", "../../../javascript/warp/components/Carousel/PrevArrow.tsx", "../../../javascript/warp/components/Carousel/NextArrow.tsx", "../../../javascript/warp/components/pdp/VariantSelector/VariantSelectorWizard/ColourStep/index.tsx", "../../../javascript/warp/components/pdp/VariantSelector/VariantSelectorWizard/ColourStep/FiltersContext.tsx", "../../../javascript/warp/components/pdp/VariantSelector/VariantSelectorWizard/ColourStep/Step.tsx", "../../../javascript/warp/components/pdp/VariantSelector/VariantSelectorWizard/ColourStep/Colour.tsx", "../../../javascript/warp/components/pdp/VariantSelector/ColourItem.tsx", "../../../javascript/warp/icons/MailIcon.tsx", "../../../javascript/warp/components/pdp/VariantSelector/VariantSelectorWizard/ColourStep/ColourBadges.tsx", "../../../javascript/warp/components/pdp/VariantSelector/VariantSelectorWizard/ColourStep/Filters.tsx", "../../../javascript/warp/icons/Check.tsx", "../../../javascript/warp/components/pdp/VariantSelector/VariantSelectorWizard/ColourStep/SearchFilter.tsx", "../../../javascript/warp/components/pdp/VariantSelector/VariantSelectorWizard/ListViewIcon.tsx", "../../../javascript/warp/components/pdp/VariantSelector/VariantSelectorWizard/GridViewIcon.tsx", "../../../javascript/warp/components/pdp/VariantSelector/VariantSelectorWizard/ArrowIcon.tsx", "../../../javascript/warp/icons/FilterIcon.tsx", "../../../javascript/warp/icons/SearchIcon.tsx", "../../../javascript/warp/components/pdp/VariantSelector/VariantSelectorWizard/ColourStep/hooks.ts", "../../../javascript/warp/components/pdp/VariantSelector/VariantSelectorWizard/SingleStepFooter.tsx", "../../../javascript/warp/components/pdp/VariantSelector/VariantSelectorWizard/OptionTypeStep.tsx", "../../../javascript/warp/components/pdp/VariantSelector/VariantSelectorWizard/OptionValue.tsx", "../../../javascript/warp/components/pdp/VariantSelector/VariantSelectorWizard/SelectedValuesHeader.tsx", "../../../javascript/warp/components/pdp/VariantSelector/KitSelector/index.tsx", "../../../javascript/warp/components/pdp/VariantSelector/KitSelector/Dialog.tsx", "../../../javascript/warp/components/pdp/VariantSelector/KitSelector/Steps/Pattern.tsx", "../../../javascript/warp/components/pdp/VariantSelector/KitSelector/context/KitSelectorContext.tsx", "../../../javascript/warp/components/pdp/VariantSelector/KitSelector/Footer/index.tsx", "../../../javascript/warp/components/pdp/VariantSelector/KitSelector/Steps/FixedColours.tsx", "../../../javascript/warp/components/pdp/VariantSelector/KitSelector/Colour.tsx", "../../../javascript/warp/components/pdp/VariantSelector/KitSelector/Steps/Download/index.tsx", "../../../javascript/warp/components/pdp/VariantSelector/KitSelector/Steps/Download/LoggedInDownload.tsx", "../../../javascript/warp/components/pdp/VariantSelector/KitSelector/context/KitSelectorDownloadFormContext.tsx", "../../../javascript/warp/components/pdp/VariantSelector/KitSelector/hooks/useDownloadPattern.ts", "../../../javascript/warp/components/pdp/VariantSelector/KitSelector/Steps/Download/LoggedOutDownload.tsx", "../../../javascript/warp/components/pdp/VariantSelector/KitSelector/LoginForm.tsx", "../../../javascript/warp/components/InputField.tsx", "../../../javascript/warp/components/pdp/VariantSelector/KitSelector/Steps/Download/NewsletterDownload.tsx", "../../../javascript/warp/components/pdp/VariantSelector/KitSelector/Footer/Download.tsx"], "sourcesContent": ["var SetCache = require('./_SetCache'),\n arrayIncludes = require('./_arrayIncludes'),\n arrayIncludesWith = require('./_arrayIncludesWith'),\n arrayMap = require('./_arrayMap'),\n baseUnary = require('./_baseUnary'),\n cacheHas = require('./_cacheHas');\n\n/** Used as the size to enable large array optimizations. */\nvar LARGE_ARRAY_SIZE = 200;\n\n/**\n * The base implementation of methods like `_.difference` without support\n * for excluding multiple arrays or iteratee shorthands.\n *\n * @private\n * @param {Array} array The array to inspect.\n * @param {Array} values The values to exclude.\n * @param {Function} [iteratee] The iteratee invoked per element.\n * @param {Function} [comparator] The comparator invoked per element.\n * @returns {Array} Returns the new array of filtered values.\n */\nfunction baseDifference(array, values, iteratee, comparator) {\n var index = -1,\n includes = arrayIncludes,\n isCommon = true,\n length = array.length,\n result = [],\n valuesLength = values.length;\n\n if (!length) {\n return result;\n }\n if (iteratee) {\n values = arrayMap(values, baseUnary(iteratee));\n }\n if (comparator) {\n includes = arrayIncludesWith;\n isCommon = false;\n }\n else if (values.length >= LARGE_ARRAY_SIZE) {\n includes = cacheHas;\n isCommon = false;\n values = new SetCache(values);\n }\n outer:\n while (++index < length) {\n var value = array[index],\n computed = iteratee == null ? value : iteratee(value);\n\n value = (comparator || value !== 0) ? value : 0;\n if (isCommon && computed === computed) {\n var valuesIndex = valuesLength;\n while (valuesIndex--) {\n if (values[valuesIndex] === computed) {\n continue outer;\n }\n }\n result.push(value);\n }\n else if (!includes(values, computed, comparator)) {\n result.push(value);\n }\n }\n return result;\n}\n\nmodule.exports = baseDifference;\n", "var baseDifference = require('./_baseDifference'),\n baseFlatten = require('./_baseFlatten'),\n baseRest = require('./_baseRest'),\n isArrayLikeObject = require('./isArrayLikeObject');\n\n/**\n * Creates an array of `array` values not included in the other given arrays\n * using [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero)\n * for equality comparisons. The order and references of result values are\n * determined by the first array.\n *\n * **Note:** Unlike `_.pullAll`, this method returns a new array.\n *\n * @static\n * @memberOf _\n * @since 0.1.0\n * @category Array\n * @param {Array} array The array to inspect.\n * @param {...Array} [values] The values to exclude.\n * @returns {Array} Returns the new array of filtered values.\n * @see _.without, _.xor\n * @example\n *\n * _.difference([2, 1], [2, 3]);\n * // => [1]\n */\nvar difference = baseRest(function(array, values) {\n return isArrayLikeObject(array)\n ? baseDifference(array, baseFlatten(values, 1, isArrayLikeObject, true))\n : [];\n});\n\nmodule.exports = difference;\n", "import ReactDOM from \"react-dom\"\nimport React from \"react\"\n\nimport parseJSONData from \"@warp/utilities/parseJSONData\"\n\nimport MarketContext from \"@warp/context/MarketContext\"\nimport TranslationContext from \"@warp/context/TranslationContext\"\nimport BrowsingOutOfRegionContext from \"@warp/context/BrowsingOutOfRegionContext\"\n\nimport { Badge, Image, ProductData } from \"@warp/types/product\"\nimport BasicProduct from \"@warp/components/pdp/BasicProduct\"\nimport MultipleVariantSelector from \"@warp/components/pdp/VariantSelector/MultipleVariantSelector\"\nimport StitchYourPhoto from \"@warp/components/pdp/StitchYourPhoto\"\nimport VariantSelectorWizard from \"@warp/components/pdp/VariantSelector/VariantSelectorWizard\"\nimport KitSelector from \"@warp/components/pdp/VariantSelector/KitSelector\"\n\ndocument.addEventListener(\"DOMContentLoaded\", () => {\n const node = document.getElementById(\"react-product-form-data\")\n\n if (node) {\n const pdpType = node.dataset.pdpType || \"unspecified\"\n const customPattern = node.dataset.customPattern === \"true\"\n const currentUserEmail = node.dataset.currentUserEmail\n\n const carouselNode = document.getElementById(\"product-images-carousel-data\")\n let slides: Image[] = []\n let badges: Badge[] = []\n if (carouselNode) {\n slides = parseJSONData(carouselNode.dataset, \"images\")\n badges = parseJSONData(carouselNode.dataset, \"badges\")\n }\n\n const productData = parseJSONData(\n node.dataset,\n \"productData\"\n ) as ProductData\n const browsingOutOfRegion = node.dataset.browsingOutOfRegion === \"true\"\n const translations = parseJSONData(node.dataset, \"translations\")\n const locale = node.dataset.locale || \"\"\n const stockLocationId: number = Number(node.dataset.stockLocationId)\n const countryIso = node.dataset.countryIso || \"\"\n\n const hasVariants =\n (productData.variants.filter(\n (variant) => variant.id !== productData.masterVariant.id\n )?.length ?? 0) > 0\n\n let component = null\n\n switch (pdpType) {\n case \"simple_product\":\n case \"simple_bundle\":\n component = hasVariants ? (\n
\n \n
\n ) : (\n \n )\n break\n case \"yarn\":\n case \"yarn_family\":\n case \"thread\":\n component = (\n
\n \n
\n )\n break\n case \"fixed_kit\":\n case \"custom_kit\":\n component = (\n
\n \n
\n )\n }\n\n if (customPattern) {\n const accessories = parseJSONData(node.dataset, \"accessories\")\n\n component = (\n
\n \n
\n )\n } else if (!component) {\n component = hasVariants ? (\n
\n \n
\n ) : (\n \n )\n }\n\n if (!component) {\n return\n }\n\n ReactDOM.render(\n \n \n \n {component}\n \n \n ,\n document.getElementById(\"react-product-form\")\n )\n\n // Get all the buttons that open the variant selector\n // and add a click event listener to them\n const variantButtons = document.querySelectorAll(\n \".react-modal-button\"\n )\n\n variantButtons.forEach((button) => {\n const modalName = button.dataset.modal\n if (!modalName) {\n return\n }\n\n const modalInitialOption = button.dataset.option\n\n button.addEventListener(\"click\", () => {\n // The modal component will listen for this event and open itself\n // This is the only way i found to open the modal with buttons\n // spread across multiple html templates\n const openEvent = new CustomEvent(`${modalName}ModalOpen`, {\n detail: {\n initialOption: modalInitialOption\n }\n })\n document.dispatchEvent(openEvent)\n })\n })\n } else {\n console.error(\"Failed to initialize Basic Product Form React App.\")\n }\n})\n", "import React, { useContext } from \"react\"\n\ntype Market = {\n countryIso: string\n locale: string\n stockLocationId: number\n}\n\nconst MarketContext = React.createContext({\n countryIso: \"\",\n locale: \"\",\n stockLocationId: 0\n})\n\nfunction useMarket() {\n return useContext(MarketContext)\n}\n\nexport { MarketContext as default, Market as Market, useMarket }\n", "import React from \"react\"\n\ntype BrowsingOutOfRegion = boolean\n\nconst BrowsingOutOfRegionContext =\n React.createContext(false)\n\nexport { BrowsingOutOfRegion, BrowsingOutOfRegionContext as default }\n", "import React, { useContext, useState } from \"react\"\n\nimport BrowsingOutOfRegion from \"@warp/components/BrowsingOutOfRegion\"\nimport OutOfStock from \"@warp/components/OutOfStock\"\nimport TranslationContext, {\n Translations\n} from \"@warp/context/TranslationContext\"\nimport ProductSubmit from \"./ProductSubmit\"\n\nimport { addToCart } from \"@warp/utilities/api\"\nimport { errorToast, handleErrors } from \"@warp/utilities/errors\"\n\ninterface ProductData {\n variants: Variant[]\n masterVariant: MasterVariant\n}\n\ninterface Variant {\n id: number\n sku: string\n price: string\n discountedPrice: string | null\n savings: string | null\n inStock: boolean\n}\n\ninterface MasterVariant {\n id: number\n sku: string\n}\n\ninterface BasicProductFormAppProps {\n productData: ProductData\n}\n\nconst BasicProductFormApp: React.FC = ({\n productData\n}) => {\n const [quantity, setQuantity] = useState(1)\n\n const translations = useContext(TranslationContext)\n const selectedVariant = productData?.variants[0]\n\n const handleAddToCart = async () => {\n try {\n await addToCart({\n quantity: quantity,\n variantId: selectedVariant?.id\n })\n } catch (error) {\n const handled =\n error instanceof Response ? await handleErrors(error) : false\n if (!handled) {\n errorToast(new Error(translations[\"errors.something_went_wrong\"]))\n }\n }\n }\n\n return (\n \n !!selectedVariant && !selectedVariant?.inStock ? (\n \n ) : (\n \n )\n }\n renderOutOfRegion={() => null}\n />\n )\n}\n\nexport { BasicProductFormApp as default }\n", "import React, { ReactNode, useContext } from \"react\"\nimport BrowsingOutOfRegionContext from \"../context/BrowsingOutOfRegionContext\"\n\ninterface BrowsingOutOfRegion {\n renderOutOfRegion: () => ReactNode\n renderInRegion: () => ReactNode\n}\n\nconst BrowsingOutOfRegion: React.FC = ({\n renderOutOfRegion,\n renderInRegion\n}) => {\n const browsingOutOfRegion = useContext(BrowsingOutOfRegionContext)\n\n return browsingOutOfRegion ? renderOutOfRegion() : renderInRegion()\n}\n\nexport default BrowsingOutOfRegion\n", "import React, { useMemo, useState } from \"react\"\nimport ActionButton from \"./ActionButton\"\n\nimport { useMarket } from \"../context/MarketContext\"\nimport { useTranslation } from \"../context/TranslationContext\"\n\nimport createNotificationRequest from \"../utilities/createNotificationRequest\"\n\ninterface OutOfStockProps {\n sku: string\n variantId: number\n}\n\nconst OutOfStock: React.FC = ({ sku, variantId }) => {\n const [email, setEmail] = useState(\"\")\n const [success, setSuccess] = useState(false)\n\n const market = useMarket()\n const translations = useTranslation()\n\n const handleNotifyClick = async (\n countryIso: string,\n locale: string,\n stockLocationId: number\n ) => {\n if (!email) {\n /* TODO: Prompt user to input their email */\n return\n }\n\n const response = await createNotificationRequest({\n variantId,\n email,\n stockLocationId,\n countryIso,\n locale\n })\n if (response.status == 200) {\n setSuccess(true)\n }\n }\n\n const outOfStockNotify = useMemo(\n () => translations.out_of_stock_notify.replace(\"$sku\", sku),\n [translations, sku]\n )\n const outOfStockSuccess = useMemo(\n () => translations.out_of_stock_success.replace(\"$sku\", sku),\n [translations, sku]\n )\n\n return (\n
\n

\n \n {\n setEmail(e.target.value)\n }}\n />\n\n {\n handleNotifyClick(\n market.countryIso,\n market.locale,\n market.stockLocationId\n )\n }}\n />\n
\n \n \n )\n}\n\nexport default OutOfStock\n", "import React from \"react\"\n\ninterface ActionButtonProps {\n additionalClassNames?: string[]\n disabled?: boolean\n hoverable?: boolean\n fullWidth?: boolean\n alternativeColour?: boolean\n darkGoldColour?: boolean\n alternativeHover?: boolean\n onClick?: () => void\n text: string\n type?: \"submit\" | \"reset\" | \"button\" | undefined\n name?: string\n}\n\nconst ActionButton: React.FC = ({\n additionalClassNames = [],\n alternativeColour = false,\n alternativeHover = false,\n darkGoldColour = false,\n disabled = false,\n fullWidth = false,\n hoverable = false,\n name,\n onClick,\n text,\n type = \"button\"\n}) => {\n const getClasses = () => {\n let classes = [\"action-button\"]\n\n if (disabled) {\n classes.push(\"action-button__disabled\")\n }\n if (hoverable) {\n classes.push(\"action-button--hoverable\")\n }\n if (fullWidth) {\n classes.push(\"action-button--full-width\")\n }\n if (alternativeColour) {\n classes.push(\"action-button--almost-white\")\n }\n if (darkGoldColour) {\n classes.push(\"action-button--dark-gold\")\n }\n if (alternativeHover) {\n classes.push(\"action-button--light-blue-hover\")\n }\n\n if (additionalClassNames.length > 0) {\n return classes.concat(additionalClassNames).join(\" \")\n } else {\n return classes.join(\" \")\n }\n }\n\n return (\n \n {text}\n \n )\n}\n\nexport default ActionButton\n", "const createNotificationRequest = ({\n variantId,\n stockLocationId,\n email,\n countryIso,\n locale\n}: {\n variantId: number\n email: string\n stockLocationId: number\n countryIso: string\n locale: string\n}) => {\n const spreeApiUrl = `/${countryIso}/${locale}/stock_notification_requests?`\n let queryParameters = [\n `quilt_stock_notification_request[variant_id]=${variantId.toString(10)}`,\n `quilt_stock_notification_request[stock_location_id]=${stockLocationId}`,\n `quilt_stock_notification_request[email]=${email}`,\n `quilt_stock_notification_request[locale]=${locale}`,\n `quilt_stock_notification_request[country_iso]=${countryIso}`\n ].join(\"&\")\n\n const csrfToken = document\n .querySelector(\"meta[name=csrf-token]\")\n ?.getAttribute(\"content\")\n\n return fetch(spreeApiUrl + queryParameters, {\n method: \"POST\",\n headers: {\n \"X-CSRF-Token\": csrfToken ?? \"\"\n }\n })\n}\n\nexport default createNotificationRequest\n", "import React from \"react\"\n\nimport QuantityPicker from \"./QuantityPicker\"\nimport AddToBasket from \"./AddToBasket\"\n\ninterface ProductSubmitProps {\n quantity: number\n setQuantity: (quantity: number) => void\n isDisabled: boolean\n isInStock: boolean\n handleAddToCart: () => Promise | null\n disabledText: string | null\n}\n\nconst ProductSubmit: React.FC = ({\n quantity,\n setQuantity,\n isDisabled,\n isInStock,\n handleAddToCart,\n disabledText\n}) => {\n return (\n
\n
\n \n
\n
\n \n
\n
\n )\n}\n\nexport default ProductSubmit\n", "import React, { useState, useEffect, useRef } from \"react\"\n\ninterface QuantityPickerProps {\n quantity: number\n setQuantity: (quantity: number) => void\n variantId?: number | null\n setCartQuantity?: (quantityValue: number) => void\n disabled?: boolean\n}\n\nconst QuantityPicker: React.FC = ({\n quantity,\n setQuantity,\n variantId,\n disabled = false,\n setCartQuantity = () => {}\n}) => {\n const updateQuantity = (amount: number) => {\n if (disabled) return\n const newAmount = quantity + amount\n setQuantity(newAmount >= 0 ? newAmount : 0)\n }\n\n const ref = useRef(null)\n\n const [isFreeForm, setIsFreeForm] = useState(false)\n\n const variantQuantityFormId = `variant-${variantId}`\n\n const inputClasses = disabled\n ? \"quantity-picker__amount--input quantity-picker__amount--input--disabled\"\n : \"quantity-picker__amount--input\"\n\n const buttonClasses = disabled\n ? \"quantity-picker__button quantity-picker__button--disabled\"\n : \"quantity-picker__button\"\n\n const freeFormQuantity = (event: React.ChangeEvent) => {\n const newQuantity = parseInt(event.target.value)\n if (newQuantity > 0) {\n setQuantity(newQuantity)\n setIsFreeForm(true)\n }\n }\n\n const handleKeyPress = (event: React.KeyboardEvent) => {\n // Only applies to non-selected variants on CPP\n if (event.key === \"Enter\" && quantity > 0) {\n setCartQuantity(quantity)\n setIsFreeForm(false)\n }\n }\n\n useEffect(() => {\n const handleClickOutside = (event: MouseEvent) => {\n // Only applies to non-selected variants on CPP\n if (ref?.current?.contains(event.target as Node)) return\n if (isFreeForm && quantity > 0) {\n setCartQuantity(quantity)\n setIsFreeForm(false)\n }\n }\n document.addEventListener(\"click\", handleClickOutside)\n return () => {\n document.removeEventListener(\"click\", handleClickOutside)\n }\n }, [quantity, setCartQuantity, isFreeForm])\n\n return (\n
\n {\n if (quantity - 1 > 0) {\n updateQuantity(-1)\n\n if (isFreeForm) {\n setCartQuantity(quantity - 1)\n setIsFreeForm(false)\n } else {\n setCartQuantity(quantity - 1)\n }\n }\n }}\n disabled={quantity === 1}\n >\n \u2212\n \n
\n \n
\n {\n updateQuantity(1)\n if (isFreeForm) {\n setCartQuantity(quantity + 1)\n setIsFreeForm(false)\n } else {\n setCartQuantity(quantity + 1)\n }\n }}\n >\n +\n \n
\n )\n}\n\nexport default QuantityPicker\n", "import React, { useContext, useState } from \"react\"\n\nimport TranslationContext, {\n Translations\n} from \"@warp/context/TranslationContext\"\nimport ActionButton from \"@warp/components/ActionButton\"\n\ninterface AddToBasketProps {\n onClick?: () => void\n disabled?: boolean\n fullWidth?: boolean\n inStock?: boolean\n hoverable?: boolean\n disabledText?: string | null\n}\n\nconst AddToBasket: React.FC = ({\n onClick = () => {},\n disabled = false,\n fullWidth = false,\n inStock = true,\n hoverable = false,\n disabledText = null\n}) => {\n const [buttonText, setButtonText] = useState()\n\n const translations = useContext(TranslationContext)\n\n const handleClick = async (translations: Translations) => {\n onClick()\n setButtonText(translations.added)\n await new Promise((resolve) => setTimeout(resolve, 1500))\n setButtonText(translations.add_to_basket)\n }\n\n const getButtonText = (translations: Translations) => {\n if (!inStock) return translations.out_of_stock\n return buttonText ? buttonText : translations.add_to_basket\n }\n\n return (\n
\n handleClick(translations)}\n disabled={disabled}\n fullWidth={fullWidth}\n hoverable={hoverable}\n />\n
\n )\n}\n\nexport default AddToBasket\n", "// This method gets the first two items in the pathname\n// which are the locale and country iso of the user's session.\nconst getLocaleParams = () => {\n return [\n window.location.pathname.split(\"/\")[1],\n window.location.pathname.split(\"/\")[2]\n ]\n}\n\nexport default getLocaleParams\n", "import getLocaleParams from \"./localeParams\"\nimport aa from \"search-insights\"\n\ntype VariantParam = {\n quantity: number\n variantId: number\n}\n\ntype Params = VariantParam | Array<{ id: number; quantity: number }>\n\nfunction isSingleVariant(params: Params): params is VariantParam {\n return \"variantId\" in params\n}\n\nexport const addToCart = (params: Params) => {\n const [country_iso, locale] = getLocaleParams()\n\n const body = JSON.stringify(\n isSingleVariant(params)\n ? {\n quantity: params.quantity,\n variant_id: params.variantId\n }\n : { variants: params }\n )\n\n return fetch(`/${country_iso}/${locale}/orders/populate`, {\n body: body,\n headers: {\n \"Content-Type\": \"application/json\",\n Accept: \"application/json\"\n },\n method: \"POST\"\n })\n .then((response) =>\n response.ok ? response.json() : Promise.reject(response)\n )\n .then((json) => {\n if (!window.miniCartAdded) {\n return\n }\n\n window.miniCartAdded(json.miniCartAdded, json.miniCart, json.itemCount)\n\n // GA4 watches the window's dataLayer for event data.\n window.dataLayer = window.dataLayer || []\n window.dataLayer.push(json.gtmOrder)\n\n // We send Algolia data through the Insights API.\n if (!!json.algoliaData?.insightsData?.userToken) {\n aa(\"init\", {\n appId: json.algoliaData.appId,\n apiKey: json.algoliaData.apiKey,\n region: \"de\",\n // The user token will only be present in the DOM if the user\n // has consented through Cookiebot.\n useCookie: true\n })\n\n aa(json.algoliaData.methodName, json.algoliaData.insightsData)\n }\n })\n}\n\nexport const downloadPattern = ({\n email,\n variantId,\n countryIso,\n locale\n}: {\n email: string\n variantId: number\n countryIso: string\n locale: string\n}) => {\n const body = JSON.stringify({\n email: email,\n variant_id: variantId,\n locale: locale,\n country_iso: countryIso\n })\n\n return fetch(`/api/quilt/free_patterns/download`, {\n body: body,\n headers: {\n \"Content-Type\": \"application/json\",\n Accept: \"application/json\"\n },\n method: \"POST\"\n })\n}\n", "import * as React from \"react\"\nimport { Badge, Image, ProductData } from \"@warp/types/product\"\nimport { MultipleVariantSelectorProvider } from \"@warp/context/MultipleVariantSelectorContext\"\nimport MultipleVariantSelectorDialog from \"./MultipleVariantSelectorDialog\"\n\nexport interface VariantSelectorProps {\n productName: string\n productData: ProductData\n slides: Image[]\n badges: Badge[]\n}\n\nexport default ({\n productName,\n productData,\n slides,\n badges\n}: VariantSelectorProps) => {\n return (\n \n \n \n )\n}\n", "import React, { useMemo } from \"react\"\n\nimport { ProductData } from \"../types/product\"\nimport MarketContext from \"./MarketContext\"\n\ntype SelectedVariantsState = { [id: number]: number }\n\ntype SelectedVariantsAction =\n | {\n type: \"add\" | \"remove\" | \"increment\" | \"decrement\"\n payload: { id: number }\n }\n | {\n type: \"setQuantity\"\n payload: { id: number; quantity: number }\n }\n | {\n type: \"reset\"\n }\n\ninterface MultipleVariantSelectorContextValue {\n totalPrice?: string\n totalAmount?: number\n productData: ProductData\n masterVariantPrice?: string\n selectedVariants: SelectedVariantsState\n}\n\nconst MultipleVariantSelectorContext = React.createContext<\n MultipleVariantSelectorContextValue & {\n addVariant: (id: number) => void\n removeVariant: (id: number) => void\n incrementVariant: (id: number) => void\n decrementVariant: (id: number) => void\n setVariantQuantity: (id: number, quantity: number) => void\n reset: () => void\n }\n>({\n productData: {} as ProductData,\n selectedVariants: {} as SelectedVariantsState,\n addVariant: () => {},\n removeVariant: () => {},\n incrementVariant: () => {},\n decrementVariant: () => {},\n setVariantQuantity: () => {},\n reset: () => {}\n})\n\ntype MultipleVariantSelectorContextProviderProps = {\n value: Omit<\n MultipleVariantSelectorContextValue,\n \"totalPrice\" | \"totalAmount\" | \"masterVariantPrice\" | \"selectedVariants\"\n >\n children: React.ReactNode\n}\n\nfunction selectedVariantsReducer(\n state: SelectedVariantsState,\n action: SelectedVariantsAction\n): SelectedVariantsState {\n // This is a utility function that removes a variant from the state.\n // We can't do `delete state[action.payload.id]` because\n // React won't detect the state change and rerender the right components.\n const remove = () => {\n if (!(\"payload\" in action)) {\n return state\n }\n\n return Object.keys(state)\n .filter((id) => parseInt(id) !== action.payload.id)\n .reduce((result, id) => {\n result[parseInt(id)] = state[parseInt(id)]\n return result\n }, {} as SelectedVariantsState)\n }\n switch (action.type) {\n case \"add\":\n case \"increment\":\n return {\n ...state,\n // If the variant is not in the state (`state[action.payload.id] === undefined`),\n // add it with a quantity of 1\n [action.payload.id]: (state[action.payload.id] || 0) + 1\n }\n case \"remove\":\n return remove()\n case \"decrement\":\n if (state[action.payload.id]) {\n if (state[action.payload.id] === 1) {\n return remove()\n }\n return {\n ...state,\n [action.payload.id]: state[action.payload.id] - 1\n }\n }\n return state\n case \"setQuantity\":\n // We don't care about negative quantities\n if (action.payload.quantity < 0) {\n return state\n }\n\n // If there's already a variant but the quantity is 0, remove it\n if (state[action.payload.id] && action.payload.quantity === 0) {\n return remove()\n }\n\n // Otherwise, se the new quantity.\n // This works even if the variant is not in the state yet\n return {\n ...state,\n [action.payload.id]: action.payload.quantity\n }\n case \"reset\":\n return {}\n default:\n return state\n }\n}\n\nexport const MultipleVariantSelectorProvider = ({\n value,\n children\n}: MultipleVariantSelectorContextProviderProps) => {\n const [selectedVariants, dispatchSelectedVariantsAction] = React.useReducer(\n selectedVariantsReducer,\n {}\n )\n const [productData] = React.useState(value.productData)\n const { locale } = React.useContext(MarketContext)\n\n const masterVariantPrice = value.productData.variants.find(\n (variant) => variant.id === value.productData.masterVariant.id\n )?.price\n\n function addVariant(id: number) {\n dispatchSelectedVariantsAction({ type: \"add\", payload: { id } })\n }\n\n function removeVariant(id: number) {\n dispatchSelectedVariantsAction({ type: \"remove\", payload: { id } })\n }\n\n function incrementVariant(id: number) {\n dispatchSelectedVariantsAction({ type: \"increment\", payload: { id } })\n }\n\n function decrementVariant(id: number) {\n dispatchSelectedVariantsAction({ type: \"decrement\", payload: { id } })\n }\n\n function setVariantQuantity(id: number, quantity: number) {\n dispatchSelectedVariantsAction({\n type: \"setQuantity\",\n payload: { id, quantity }\n })\n }\n\n function reset() {\n dispatchSelectedVariantsAction({ type: \"reset\" })\n }\n\n const totalAmount = useMemo(\n () =>\n Object.keys(selectedVariants).reduce((total, variantId) => {\n const variant = productData.variants.find(\n (variant) => variant.id === parseInt(variantId)\n )\n return (\n total +\n (variant?.rawDiscountedAmount || variant?.rawAmount || 0) *\n selectedVariants[parseInt(variantId)]\n )\n }, 0),\n [selectedVariants, productData]\n )\n\n const totalPrice = useMemo(\n () =>\n Intl.NumberFormat(locale, {\n style: \"currency\",\n currency: productData.variants[0].currency\n }).format(totalAmount),\n [totalAmount, locale]\n )\n\n return (\n \n {children}\n \n )\n}\n\nexport function useMultipleVariantSelector() {\n const context = React.useContext(MultipleVariantSelectorContext)\n\n if (context === undefined) {\n throw new Error(\n \"useMultipleVariantSelector must be used within a MultipleVariantSelectorProvider\"\n )\n }\n\n return context\n}\n", "import * as React from \"react\"\nimport OptionSelector from \"./OptionSelector\"\nimport Dialog from \"@warp/components/Dialog\"\nimport { Badge, Image } from \"@warp/types/product\"\nimport { useMultipleVariantSelector } from \"@warp/context/MultipleVariantSelectorContext\"\nimport Footer from \"./Footer\"\nimport Slideshow from \"../Slideshow\"\nimport { useTranslation } from \"@warp/context/TranslationContext\"\nimport { productTranslation } from \"@warp/utilities/translation\"\n\ninterface VariantSelectorDialogProps {\n title: string\n slides: Image[]\n badges: Badge[]\n}\n\nexport default function VariantSelectorDialog({\n title,\n slides,\n badges\n}: VariantSelectorDialogProps) {\n const translations = useTranslation()\n\n const { productData, reset } = useMultipleVariantSelector()\n\n const orderedVariants = React.useMemo(() => {\n const outOfStock = productData.variants.filter(\n (variant) => !variant.inStock\n )\n const rest = productData.variants.filter((variant) => variant.inStock)\n\n return [...rest, ...outOfStock]\n }, [productData])\n\n return (\n }\n sidebar={}\n onCloseCallback={reset}\n contentClassName=\"multiple-variants-dialog\"\n >\n 1\n ? translations[\"components.products.please_select_options\"]\n : productTranslation(\n translations[\"components.products.please_select\"],\n productData\n )\n }\n variants={orderedVariants}\n />\n \n )\n}\n", "import React from \"react\"\nimport { Variant } from \"@warp/types/product\"\nimport Option from \"./Option\"\n\nexport interface OptionSelectorProps {\n title?: string | null\n variants: Variant[]\n}\n\nexport default function OptionSelector({\n title,\n variants\n}: OptionSelectorProps) {\n return (\n <>\n {title && (\n
\n

{title}

\n
\n )}\n
\n {variants.map((variant) => (\n
\n \n )\n}\n", "import React from \"react\"\nimport cn from \"classnames\"\nimport { Variant } from \"@warp/types/product\"\nimport Checkbox from \"./Checkbox\"\nimport QuantityPicker from \"../QuantityPicker\"\nimport { useMultipleVariantSelector } from \"@warp/context/MultipleVariantSelectorContext\"\nimport OutOfStock from \"@warp/components/OutOfStock\"\nimport Price from \"@warp/components/pdp/Price\"\n\nexport interface OptionProps {\n variant: Variant\n}\n\nconst Option = ({ variant }: OptionProps) => {\n const { selectedVariants, setVariantQuantity } = useMultipleVariantSelector()\n const [notifyHidden, setNotifyHidden] = React.useState(true)\n\n const setQuantity = (quantity: number) => {\n setVariantQuantity(variant.id, quantity)\n }\n\n const handleNotifyClick = () => {\n setNotifyHidden((previous) => !previous)\n }\n\n return (\n \n
\n
\n \n {selectedVariants[variant.id] > 0 && (\n \n )}\n \n
\n
\n\n {!notifyHidden && }\n \n )\n}\n\nexport default Option\n", "import React, { useMemo } from \"react\"\nimport { Variant } from \"@warp/types/product\"\nimport { useMultipleVariantSelector } from \"@warp/context/MultipleVariantSelectorContext\"\nimport { getOptionValuesText } from \"@warp/utilities/product\"\n\ninterface CheckboxProps {\n variant: Variant\n}\n\nconst Checkbox = ({ variant }: CheckboxProps) => {\n const { addVariant, removeVariant, selectedVariants, productData } =\n useMultipleVariantSelector()\n\n const checked = useMemo(\n () => selectedVariants[variant.id] > 0,\n [selectedVariants, variant.id]\n )\n\n function handleChange() {\n if (checked) {\n removeVariant(variant.id)\n } else {\n addVariant(variant.id)\n }\n }\n\n let label: React.ReactNode = getOptionValuesText(\n productData,\n variant.optionValueIds\n )\n\n if (variant.colour) {\n label = (\n <>\n \n {label}\n \n )\n }\n\n return (\n
\n \n
\n )\n}\n\nexport default Checkbox\n", "import { ProductData } from \"@warp/types/product\"\n\nexport function getColourType(productData: ProductData) {\n return productData.optionTypes.find((optionType) =>\n productData.colourTypeIds.includes(optionType.id)\n )\n}\n\n/**\n * Returns the title for the option type presentations,\n * excluding any colour option types, joined with a pipe (`|`).\n *\n * The colour option type has a separate step in the variant selection.\n */\nexport function getSelectableOptionsText(productData: ProductData) {\n return productData.optionTypes\n .filter((optionType) => !productData.colourTypeIds.includes(optionType.id))\n .map((optionType) => optionType.presentation)\n .join(\" | \")\n}\n\n/**\n * Returns the title for the option value presentations,\n * joined with a pipe (`|`).\n */\nexport function getOptionValuesText(\n productData: ProductData,\n optionValueIds: number[]\n) {\n return productData.optionValues\n .filter((value) => optionValueIds.includes(value.id))\n .map((value) => value.presentation)\n .join(\" | \")\n}\n", "import React, { useRef } from \"react\"\n\ninterface QuantityPickerProps {\n quantity: number\n setQuantity: (quantity: number) => void\n variantId?: number | null\n disabled?: boolean\n}\n\nconst QuantityPicker: React.FC = ({\n quantity,\n setQuantity,\n variantId,\n disabled = false\n}) => {\n const updateQuantity = (amount: number) => {\n if (disabled) return\n const newAmount = quantity + amount\n setQuantity(newAmount >= 0 ? newAmount : 0)\n }\n\n const ref = useRef(null)\n\n const variantQuantityFormId = `variant-${variantId}-quantity`\n\n const inputClasses = disabled\n ? \"quantity-picker__amount--input quantity-picker__amount--input--disabled\"\n : \"quantity-picker__amount--input\"\n\n const buttonClasses = disabled\n ? \"quantity-picker__button quantity-picker__button--disabled\"\n : \"quantity-picker__button\"\n\n return (\n
\n {\n updateQuantity(-1)\n }}\n >\n \u2212\n \n
\n \n
\n {\n updateQuantity(1)\n }}\n >\n +\n \n
\n )\n}\n\nexport default QuantityPicker\n", "import React from \"react\"\nimport cn from \"classnames\"\nimport { Variant } from \"@warp/types/product\"\nimport TranslationContext from \"@warp/context/TranslationContext\"\n\ninterface PriceProps {\n variant: Variant\n onNotifyClick?: () => void\n}\n\nexport default function Price({ variant, onNotifyClick }: PriceProps) {\n const translations = React.useContext(TranslationContext)\n const finalPrice = variant.discountedPrice || variant.price\n\n return (\n
\n {variant.discountedPrice && (\n
{variant.price}
\n )}\n \n {variant.inStock ? (\n finalPrice\n ) : (\n <>\n {translations.out_of_stock}\n {onNotifyClick && (\n \n {translations.get_notified}\n \n )}\n \n )}\n
\n \n )\n}\n", "import React from \"react\"\nimport { useMultipleVariantSelector } from \"@warp/context/MultipleVariantSelectorContext\"\nimport { addToCart } from \"@warp/utilities/api\"\nimport { errorToast, handleErrors } from \"@warp/utilities/errors\"\n\ninterface FooterProps {\n translations: Record\n}\n\nconst Footer = ({ translations }: FooterProps) => {\n const { totalPrice, totalAmount, selectedVariants } =\n useMultipleVariantSelector()\n const [isLoading, setIsLoading] = React.useState(false)\n\n // The button is disabled if the sum of all the selected variant\n // quantities is 0\n const disabled =\n Object.values(selectedVariants).reduce(\n (total, quantity) => total + quantity,\n 0\n ) === 0\n\n const handleAddToBasket = async () => {\n setIsLoading(true)\n try {\n await addToCart(\n // This just maps an object of the form { : }\n // into an array of the form [{ id: , quantity: }]\n Object.entries(selectedVariants).map(([id, quantity]) => ({\n id: parseInt(id),\n quantity\n }))\n )\n const event = new Event(\"variantModalClose\")\n document.dispatchEvent(event)\n setIsLoading(false)\n } catch (error) {\n const handled =\n error instanceof Response ? await handleErrors(error) : false\n if (!handled) {\n errorToast(new Error(translations[\"errors.something_went_wrong\"]))\n }\n setIsLoading(false)\n }\n }\n\n return (\n <>\n
\n \n {translations.add_to_basket}\n {totalAmount ? (\n {totalPrice}\n ) : null}\n \n \n )\n}\n\nexport default Footer\n", "import React from \"react\"\nimport { Badge, Image } from \"@warp/types/product\"\nimport { useMultipleVariantSelector } from \"@warp/context/MultipleVariantSelectorContext\"\nimport ZoomableImage from \"@warp/components/pdp/Zoom/ZoomableImage\"\nimport Badges from \"@warp/components/pdp/Badges/Badges\"\n\ninterface SlideshowProps {\n slides: Image[]\n badges: Badge[]\n overrideZoomImages?: boolean\n}\n\nexport default function Slideshow({\n slides,\n badges,\n overrideZoomImages\n}: SlideshowProps) {\n const { selectedVariants } = useMultipleVariantSelector()\n\n const variantIds = Object.keys(selectedVariants).map((id) =>\n Number.parseInt(id)\n )\n\n const filteredSlides = React.useMemo(() => {\n if (variantIds.length > 0) {\n return slides.filter((slide) => variantIds.includes(slide.variantId))\n }\n\n return slides\n }, [slides, selectedVariants])\n\n const handleClick = (id: number) => () => {\n document.dispatchEvent(\n new CustomEvent(\"zoomModalOpen\", {\n detail: {\n id: id,\n variantIds: variantIds,\n images: overrideZoomImages ? slides : null\n }\n })\n )\n }\n\n return (\n
\n {filteredSlides.map((image) => (\n
\n \n \n
\n ))}\n
\n )\n}\n", "import { ProductData } from \"@warp/types/product\"\nimport { getColourType, getSelectableOptionsText } from \"./product\"\n\nconst PLACEHOLDER = \"$js_drawer\"\n\nexport function productTranslation(\n translation: string,\n productData: ProductData\n): string | null {\n let unselectedDrawerText = translation\n\n /* If the translation does not include the placeholder, fallback to default\n * disabled messaging. */\n if (translation.includes(PLACEHOLDER)) {\n const colourType = getColourType(productData)\n const selectableOptionsText = getSelectableOptionsText(productData)\n if (colourType) {\n unselectedDrawerText = translation.replace(\n PLACEHOLDER,\n colourType.presentation\n )\n } else if (selectableOptionsText) {\n unselectedDrawerText = translation.replace(\n PLACEHOLDER,\n selectableOptionsText\n )\n }\n }\n\n return unselectedDrawerText\n}\n\nexport function optionTypeTranslation(\n translation: string,\n optionTypePresentation: string\n) {\n return translation.replace(PLACEHOLDER, optionTypePresentation)\n}\n", "import React from \"react\"\n\nimport { ProductData, Variant } from \"@warp/types/product\"\nimport Stepper from \"@warp/components/Stepper\"\nimport { Step } from \"@warp/types/stepper\"\nimport Colour from \"./Colour\"\nimport Format from \"./Format\"\nimport Thread from \"./Thread\"\nimport Tools from \"./Tools\"\nimport Upload from \"./Upload\"\nimport { StitchYourPhotoProvider } from \"@warp/context/StitchYourPhotoContext\"\nimport { useTranslation } from \"@warp/context/TranslationContext\"\nimport StitchYourPhotoDialog from \"./StitchYourPhotoDialog\"\n\nexport interface StitchYourPhotoProps {\n productName: string\n productData: ProductData\n accessories: Variant[]\n helpAndAdvicePath: string\n}\n\nexport default ({\n productData,\n accessories,\n helpAndAdvicePath\n}: StitchYourPhotoProps) => {\n const translations = useTranslation()\n const orderedAccessories = React.useMemo(() => {\n const outOfStock = accessories.filter((accessory) => !accessory.inStock)\n const rest = accessories.filter((accessory) => accessory.inStock)\n\n return [...rest, ...outOfStock]\n }, [accessories])\n\n const steps: Step[] = [\n {\n id: \"upload\",\n label: translations.step_upload,\n component: (\n \n )\n },\n {\n id: \"colour\",\n label: translations.step_colour_title,\n component: \n },\n {\n id: \"format\",\n label: translations.step_format,\n component: \n },\n {\n id: \"thread\",\n label: translations.step_thread,\n component: \n },\n {\n id: \"tools\",\n label: translations.step_tools,\n component: (\n \n )\n }\n ]\n\n return (\n true)}\n clickableSteps={steps.map(() => true)}\n >\n \n \n \n \n )\n}\n", "import cn from \"classnames\"\nimport React from \"react\"\nimport { StepperContainerProps, StepperProps } from \"@warp/types/stepper\"\nimport { StepperProvider, useStepper } from \"@warp/context/StepperContext\"\nimport Step from \"./Step\"\n\nconst Stepper = (props: StepperProps) => {\n const {\n children,\n initialStep = 0,\n completedSteps,\n steps,\n clickableSteps\n } = props\n\n return (\n \n {children}\n \n )\n}\n\nexport const StepperContainer = React.forwardRef<\n HTMLDivElement,\n StepperContainerProps\n>((props, ref) => {\n const { className, children, ...rest } = props\n const { currentStep } = useStepper()\n\n const childArr = React.Children.toArray(children)\n\n const items = childArr.filter(\n (child) => React.isValidElement(child) && child.type === Step\n )\n\n const stepCount = items.length\n\n return (\n \n {items.map((child, i) => {\n const stepProps = { index: i, isCurrentStep: i === currentStep.index }\n\n if (React.isValidElement(child)) {\n return React.cloneElement(child, stepProps)\n }\n return null\n })}\n \n )\n})\n\nStepper.defaultProps = {\n responsive: true\n}\n\nexport default Stepper\n", "import React from \"react\"\nimport { Step, StepperProps } from \"../types/stepper\"\n\n// Define the context value type\ninterface StepperContextValue extends StepperProps {\n isError?: boolean\n isLoading?: boolean\n activeStepIndex: number\n initialStep: number\n completedSteps: boolean[]\n clickableSteps: boolean[]\n steps: Step[]\n}\n\nconst StepperContext = React.createContext<\n StepperContextValue & {\n nextStep: (\n isPrevStepCompleted?: boolean,\n isPrevStepClickable?: boolean\n ) => void\n prevStep: (\n isNextStepCompleted?: boolean,\n isNextStepClickable?: boolean\n ) => void\n resetSteps: () => void\n setStep: (\n stepIndex: number,\n isCompleted?: boolean,\n isClickable?: boolean\n ) => void\n setStepCompleted: (stepIndex: number, isCompleted: boolean) => void\n setStepClickable: (stepIndex: number, isClickable: boolean) => void\n setIsLoading: (loading: boolean) => void\n }\n>({\n steps: [],\n completedSteps: [],\n clickableSteps: [],\n activeStepIndex: 0,\n initialStep: 0,\n isLoading: false,\n nextStep: () => {},\n prevStep: () => {},\n resetSteps: () => {},\n setStep: () => {},\n setStepCompleted: () => {},\n setStepClickable: () => {},\n setIsLoading: () => {}\n})\n\ntype StepperContextProviderProps = {\n value: Omit<\n StepperContextValue,\n | \"activeStepIndex\"\n | \"steps\"\n | \"isLoading\"\n | \"completedSteps\"\n | \"clickableSteps\"\n > & {\n steps: StepperContextValue[\"steps\"]\n completedSteps?: boolean[]\n clickableSteps?: boolean[]\n }\n children: React.ReactNode\n}\n\n// StepperProvider\nexport const StepperProvider = ({\n value,\n children\n}: StepperContextProviderProps) => {\n const [activeStepIndex, setActiveStepIndex] = React.useState(\n value.initialStep\n )\n const [isLoading, setIsLoading] = React.useState(false)\n const [completedSteps, setCompletedSteps] = React.useState(\n value.completedSteps ?? []\n )\n const [clickableSteps, setClickableSteps] = React.useState(\n value.clickableSteps ?? []\n )\n const steps = React.useMemo(\n () =>\n value.steps.map((step, index) => ({\n ...step,\n index,\n isLastStep: index === value.steps.length - 1\n })),\n [value.steps]\n )\n\n const nextStep = (\n isPrevStepCompleted?: boolean,\n isPrevStepClickable?: boolean\n ) => {\n const currentStepIndex = activeStepIndex\n const nextActiveStepIndex = Math.min(activeStepIndex + 1, steps.length - 1)\n setActiveStepIndex(nextActiveStepIndex)\n\n if (isPrevStepCompleted !== undefined) {\n setCompletedSteps((prevCompletedSteps) => {\n const newCompletedSteps = [...prevCompletedSteps]\n newCompletedSteps[currentStepIndex] = isPrevStepCompleted\n return newCompletedSteps\n })\n }\n\n if (isPrevStepClickable !== undefined) {\n setClickableSteps((prevClickableSteps) => {\n const newClickableSteps = [...prevClickableSteps]\n newClickableSteps[currentStepIndex] = isPrevStepClickable\n return newClickableSteps\n })\n }\n }\n\n const prevStep = (\n isNextStepCompleted?: boolean,\n isNextStepClickable?: boolean\n ) => {\n const currentStepIndex = activeStepIndex\n const nextActiveStepIndex = Math.max(currentStepIndex - 1, 0)\n setActiveStepIndex(nextActiveStepIndex)\n\n if (isNextStepCompleted !== undefined) {\n setCompletedSteps((prevCompletedSteps) => {\n const newCompletedSteps = [...prevCompletedSteps]\n newCompletedSteps[currentStepIndex] = isNextStepCompleted\n return newCompletedSteps\n })\n }\n\n if (isNextStepClickable !== undefined) {\n setClickableSteps((prevClickableSteps) => {\n const newClickableSteps = [...prevClickableSteps]\n newClickableSteps[currentStepIndex] = isNextStepClickable\n return newClickableSteps\n })\n }\n }\n\n const resetSteps = () => {\n setActiveStepIndex(value.initialStep)\n setCompletedSteps(value.completedSteps ?? [])\n setClickableSteps(value.clickableSteps ?? [])\n setIsLoading(false)\n }\n\n const setStep = (\n stepIndex: number,\n isCompleted?: boolean,\n isClickable?: boolean\n ) => {\n setActiveStepIndex(stepIndex)\n\n if (isCompleted !== undefined) {\n setCompletedSteps((prevCompletedSteps) => {\n const newCompletedSteps = [...prevCompletedSteps]\n newCompletedSteps[stepIndex] = isCompleted\n return newCompletedSteps\n })\n }\n\n if (isClickable !== undefined) {\n setClickableSteps((prevClickableSteps) => {\n const newClickableSteps = [...prevClickableSteps]\n newClickableSteps[stepIndex] = isClickable\n return newClickableSteps\n })\n }\n }\n\n const setStepCompleted = (stepIndex: number, isCompleted: boolean) => {\n setCompletedSteps((prevCompletedSteps) => {\n const newCompletedSteps = [...prevCompletedSteps]\n newCompletedSteps[stepIndex] = isCompleted\n return newCompletedSteps\n })\n }\n\n const setStepClickable = (stepIndex: number, isClickable: boolean) => {\n setClickableSteps((prevClickableSteps) => {\n const newClickableSteps = [...prevClickableSteps]\n newClickableSteps[stepIndex] = isClickable\n return newClickableSteps\n })\n }\n\n return (\n \n {children}\n \n )\n}\n\n// useStepper hook to access the context\nexport function useStepper() {\n const context = React.useContext(StepperContext)\n\n if (context === undefined) {\n throw new Error(\"useStepper must be used within a StepperProvider\")\n }\n\n const { children, className, ...rest } = context\n\n const isFirstStep = context.activeStepIndex === 0\n const isLastStep = context.activeStepIndex === context.steps.length - 1\n const hasCompletedAllSteps =\n context.completedSteps.filter(Boolean).length === context.steps.length\n\n const currentStep = context.steps[context.activeStepIndex]\n\n return {\n ...rest,\n isFirstStep,\n isLastStep,\n hasCompletedAllSteps,\n currentStep,\n steps: context.steps\n }\n}\n", "import React from \"react\"\nimport cn from \"classnames\"\nimport { useStepper } from \"@warp/context/StepperContext\"\nimport { StepProps } from \"@warp/types/stepper\"\n\nconst Step = React.forwardRef(\n (props, ref: React.Ref) => {\n const { index, onClick } = props\n\n const { setStep, currentStep, steps, completedSteps, clickableSteps } =\n useStepper()\n\n const handleStepClick = () => {\n setStep(index || 0)\n onClick?.()\n }\n\n const step = steps[index]\n const isCurrentStep = step.id === currentStep.id\n const isCompletedStep = completedSteps[index]\n const isClickableStep = clickableSteps[index]\n\n // By setting `hideHeader` we can hide the step\n // in the dialog header while still showing the content\n if (currentStep.hideHeader) {\n return null\n }\n\n return (\n \n
\n \n \n {index + 1}\n
\n {step.label}\n \n \n \n )\n }\n)\n\nexport default Step\n", "import React from \"react\"\n\nimport { useStitchYourPhoto } from \"@warp/context/StitchYourPhotoContext\"\nimport SubStep from \"@warp/components/Stepper/SubStep\"\nimport { STITCH_YOUR_PHOTO_VALUES } from \"./Defaults\"\n\nexport interface ColourProps {\n translations: Record\n}\n\nfunction Colour({ translations }: ColourProps) {\n const { selectedFilter, setSelectedFilter, selectedColor, setSelectedColor } =\n useStitchYourPhoto()\n\n const handleFilterChange = (event: React.ChangeEvent) => {\n setSelectedFilter(event.target.value)\n }\n\n const handleColorChange = (event: React.ChangeEvent) => {\n setSelectedColor(event.target.value)\n }\n\n return (\n
\n \n
\n {STITCH_YOUR_PHOTO_VALUES.filters.map((filter, i) => (\n \n \n {translations[`step_colour_filter_${filter}`]}\n \n ))}\n
\n
\n \n {STITCH_YOUR_PHOTO_VALUES.colors.map((value, i) => {\n return (\n
\n \n
\n )\n })}\n
\n\n
\n\n

{translations.step_colour_disclaimer_paragraph_1}

\n

{translations.step_colour_disclaimer_paragraph_2}

\n

{translations.step_colour_disclaimer_paragraph_3}

\n
\n )\n}\n\nexport default Colour\n", "import React, { useEffect } from \"react\"\n\nimport { STITCH_YOUR_PHOTO_VALUES } from \"../components/pdp/StitchYourPhoto/Defaults\"\nimport { sendWizardData } from \"../components/pdp/StitchYourPhoto/Services\"\nimport { ProductData } from \"../types/product\"\nimport { useStepper } from \"./StepperContext\"\nimport { Variant } from \"../types/product\"\nimport { useMarket } from \"./MarketContext\"\n\ninterface StitchYourPhotoContextValue {\n originalImage?: File\n originalImagePreviewUrl?: string\n crossStitchImageUrl?: string\n totalPrice?: string\n productData: ProductData\n patternPrice?: string\n threadPrice?: string\n patternAmount?: number\n threadAmount?: number\n pictureCode?: string\n selectedFilter: string\n selectedColor: string\n selectedCanvasType: string\n selectedCanvasSize: string\n selectedThread: string\n selectedAccessories: Variant[]\n toggleAccessory: (accessory: Variant) => void\n customPatternId?: number\n}\n\nconst StitchYourPhotoContext = React.createContext<\n StitchYourPhotoContextValue & {\n setOriginalImage: (image: File) => void\n setOriginalImagePreviewUrl: (previewURL: string) => void\n setCrossStitchImageUrl: (imageUrl: string) => void\n setThreadAmount: (amount: number) => void\n setPictureCode: (code: string) => void\n setSelectedFilter: (filter: string) => void\n setSelectedColor: (color: string) => void\n setSelectedCanvasType: (canvasType: string) => void\n setSelectedCanvasSize: (canvasSize: string) => void\n setSelectedThread: (thread: string) => void\n reset: () => void\n setSelectedAccessories: (accessories: Variant[]) => void\n setCustomPatternId: (id: number) => void\n }\n>({\n productData: {} as ProductData,\n selectedFilter: \"\",\n selectedColor: \"\",\n selectedCanvasType: \"\",\n selectedCanvasSize: \"\",\n selectedThread: \"\",\n setOriginalImage: () => {},\n setOriginalImagePreviewUrl: () => {},\n setCrossStitchImageUrl: () => {},\n setThreadAmount: () => {},\n setPictureCode: () => {},\n setSelectedFilter: () => {},\n setSelectedColor: () => {},\n setSelectedCanvasType: () => {},\n setSelectedCanvasSize: () => {},\n setSelectedThread: () => {},\n reset: () => {},\n selectedAccessories: [],\n setSelectedAccessories: () => {},\n toggleAccessory: () => {},\n setCustomPatternId: () => {}\n})\n\ntype StitchYourPhotoContextProviderProps = {\n value: Omit<\n StitchYourPhotoContextValue,\n | \"pictureCode\"\n | \"selectedFilter\"\n | \"selectedColor\"\n | \"selectedCanvasType\"\n | \"selectedCanvasSize\"\n | \"selectedThread\"\n | \"selectedAccessories\"\n | \"toggleAccessory\"\n >\n children: React.ReactNode\n}\n\nexport const StitchYourPhotoProvider = ({\n value,\n children\n}: StitchYourPhotoContextProviderProps) => {\n const [originalImage, setOriginalImage] = React.useState()\n const [originalImagePreviewUrl, setOriginalImagePreviewUrl] =\n React.useState()\n const [crossStitchImageUrl, setCrossStitchImageUrl] =\n React.useState(\"\")\n const [threadAmount, setThreadAmount] = React.useState(0)\n const { countryIso, locale } = useMarket()\n const [productData] = React.useState(value.productData)\n const [pictureCode, setPictureCode] = React.useState(\"\")\n const [selectedThread, setSelectedThread] = React.useState(\n STITCH_YOUR_PHOTO_VALUES.defaultThread\n )\n const [selectedFilter, setSelectedFilter] = React.useState(\n STITCH_YOUR_PHOTO_VALUES.defaultFilter\n )\n const [selectedColor, setSelectedColor] = React.useState(\n STITCH_YOUR_PHOTO_VALUES.defaultColor.toString()\n )\n const [selectedCanvasType, setSelectedCanvasType] = React.useState(\n STITCH_YOUR_PHOTO_VALUES.defaultCanvasType\n )\n const [selectedCanvasSize, setSelectedCanvasSize] = React.useState(\n STITCH_YOUR_PHOTO_VALUES.defaultCanvasSize\n )\n\n const patternAmount = value.productData.variants.find(\n (variant) => variant.id === value.productData.masterVariant.id\n )?.rawAmount\n\n const currency = productData.variants[0].currency\n\n const patternPrice = React.useMemo(() => {\n return Intl.NumberFormat(locale, {\n style: \"currency\",\n currency: currency\n }).format(patternAmount || 0)\n }, [locale, patternAmount])\n\n const threadPrice = React.useMemo(() => {\n return Intl.NumberFormat(locale, {\n style: \"currency\",\n currency: currency\n }).format(threadAmount)\n }, [locale, threadAmount])\n\n const [selectedAccessories, setSelectedAccessories] = React.useState<\n Variant[]\n >([])\n\n const [customPatternId, setCustomPatternId] = React.useState()\n\n const { setIsLoading } = useStepper()\n\n const reset = () => {\n setOriginalImage(undefined)\n setOriginalImagePreviewUrl(undefined)\n setCrossStitchImageUrl(\"\")\n setPictureCode(\"\")\n setThreadAmount(0)\n setSelectedFilter(STITCH_YOUR_PHOTO_VALUES.defaultFilter)\n setSelectedColor(STITCH_YOUR_PHOTO_VALUES.defaultColor.toString())\n setSelectedCanvasType(STITCH_YOUR_PHOTO_VALUES.defaultCanvasType)\n setSelectedCanvasSize(STITCH_YOUR_PHOTO_VALUES.defaultCanvasSize)\n setSelectedThread(STITCH_YOUR_PHOTO_VALUES.defaultThread)\n setSelectedAccessories([])\n setCustomPatternId(undefined)\n\n // This ensures that the object URL is revoked when\n // the dialog resets, preventing memory leaks\n if (originalImagePreviewUrl) URL.revokeObjectURL(originalImagePreviewUrl)\n }\n\n const toggleAccessory = (accessory: Variant) => {\n setSelectedAccessories((prev) =>\n prev.includes(accessory)\n ? prev.filter((other) => other.id !== accessory.id)\n : [...prev, accessory]\n )\n }\n\n const updateData = async (data: Record) => {\n setIsLoading(true)\n try {\n const result = await sendWizardData(data, countryIso, locale)\n setCrossStitchImageUrl(result.preview_image_url)\n setCustomPatternId(result.custom_pattern.id)\n setThreadAmount(result.custom_pattern_with_thread_amount)\n } catch (error) {\n throw new Error(\n error instanceof Error ? error.message : \"An unknown error occurred\"\n )\n } finally {\n setIsLoading(false)\n }\n }\n\n // Automatically update the image when selectedFilter, selectedColor or other props change\n // Update only if pictureCode and either image_type or number_of_colours is present\n useEffect(() => {\n if (\n pictureCode &&\n (selectedFilter ||\n selectedColor ||\n selectedCanvasType ||\n selectedCanvasSize)\n ) {\n // create the payload and remap the keys to match the rails backend api\n const update = async () => {\n try {\n const data = {\n picture_code: pictureCode,\n image_type: selectedFilter,\n number_of_colours: selectedColor,\n canvas_type: selectedCanvasType,\n finished_size: selectedCanvasSize,\n dots_number: \"100\"\n }\n\n await updateData(data)\n } catch (error) {\n throw new Error(\n error instanceof Error ? error.message : \"An unknown error occurred\"\n )\n }\n }\n\n update()\n }\n }, [selectedFilter, selectedColor, selectedCanvasType, selectedCanvasSize])\n\n const accessoriesAmount = React.useMemo(\n () =>\n selectedAccessories.reduce((total, accessory) => {\n return total + (accessory.rawDiscountedAmount || accessory.rawAmount)\n }, 0),\n [selectedAccessories]\n )\n\n const totalPrice = React.useMemo(() => {\n const productAmount =\n selectedThread === \"pdfWithThread\" ? threadAmount : patternAmount ?? 0\n\n if (productAmount === 0) return \"\"\n\n return Intl.NumberFormat(locale, {\n style: \"currency\",\n currency: currency\n }).format(productAmount + accessoriesAmount)\n }, [locale, accessoriesAmount, selectedThread, threadAmount])\n\n return (\n \n {children}\n \n )\n}\n\nexport function useStitchYourPhoto() {\n const context = React.useContext(StitchYourPhotoContext)\n\n if (context === undefined) {\n throw new Error(\"useStepper must be used within a StepperProvider\")\n }\n\n return context\n}\n", "export const STITCH_YOUR_PHOTO_VALUES = {\n filters: [\"MEDIANCUT\", \"NEUQUANT\", \"OCTTREE\"],\n colors: [\"50\", \"30\", \"20\", \"10\"],\n defaultFilter: \"MEDIANCUT\",\n defaultColor: \"10\",\n defaultCanvasType: \"5.5\",\n defaultCanvasSize: \"120\",\n defaultThread: \"pdfWithThread\",\n // the name property should be translated from \"spree.stitch_your_photos.*\"\n canvasTypes: [\n {\n name: \"aida_14\",\n value: \"5.5\"\n },\n {\n name: \"aida_16\",\n value: \"6\"\n },\n {\n name: \"aida_18\",\n value: \"7\"\n }\n ],\n canvasSizes: [\n {\n name: \"size_medium_large\",\n value: \"138\"\n },\n {\n name: \"size_medium\",\n value: \"120\"\n },\n {\n name: \"size_small\",\n value: \"91\"\n }\n ]\n}\n", "import { Variant } from \"@warp/types/product\"\nimport { errorToast, handleErrors } from \"@warp/utilities/errors\"\n\nexport const sendWizardData = async (\n data: any,\n countryIso: string,\n locale: string\n) => {\n const url = `/${countryIso}/${locale}/custom_patterns`\n const response = await fetch(url, {\n method: \"POST\",\n headers: {\n Accept: \"application/json\",\n \"Content-Type\": \"application/json\",\n \"X-CSRF-Token\": (\n document.querySelector('meta[name=\"csrf-token\"]') as HTMLMetaElement\n )?.content // Include the CSRF token\n },\n body: JSON.stringify(data)\n })\n\n if (!response.ok) {\n const errorResponse = await response.json()\n throw new Error(errorResponse.error)\n }\n\n return await response.json()\n}\n\nexport const uploadImage = async (\n file: File,\n countryIso: string,\n locale: string\n) => {\n const formData = new FormData()\n formData.append(\"quilt_custom_pattern[original_attachment]\", file)\n\n const url = `/${countryIso}/${locale}/custom_patterns/upload_image`\n const response = await fetch(url, {\n method: \"POST\",\n headers: {\n Accept: \"application/json\",\n \"X-CSRF-Token\": (\n document.querySelector('meta[name=\"csrf-token\"]') as HTMLMetaElement\n )?.content\n },\n body: formData\n })\n\n if (!response.ok) {\n const errorResponse = await response.json()\n throw new Error(errorResponse.error)\n }\n\n const result = await response.json()\n\n return result\n}\n\nexport const addToCart = async (\n customPatternId: number,\n addThreadsToCart: boolean,\n accessories: Variant[] = [],\n countryIso: string,\n locale: string,\n defaultErrorMessage: string\n) => {\n const url = `/${countryIso}/${locale}/custom_patterns/add_to_cart`\n const response = await fetch(url, {\n method: \"POST\",\n headers: {\n Accept:\n \"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript\",\n \"Content-Type\": \"application/json\",\n \"X-CSRF-Token\": (\n document.querySelector('meta[name=\"csrf-token\"]') as HTMLMetaElement\n )?.content\n },\n body: JSON.stringify({\n custom_pattern_id: customPatternId,\n add_threads_to_cart: addThreadsToCart,\n accessory_ids: accessories.map((accessory) => accessory.id)\n })\n })\n\n // when there's an error and it's not handled by `handleErrors`, show the\n // default error message. Also, always throw an error to stop the execution\n // of the function.\n if (!response.ok) {\n const handled = await handleErrors(response)\n if (!handled) {\n errorToast(new Error(defaultErrorMessage))\n }\n throw new Error()\n }\n\n const blob = await response.blob()\n\n const objectUrl = URL.createObjectURL(blob)\n const script = document.createElement(\"script\")\n script.src = objectUrl\n script.type = \"text/javascript\"\n document.head.appendChild(script)\n}\n", "import React from \"react\"\n\ninterface SubStepProps {\n title: string\n children: React.ReactNode\n}\n\nconst SubStep = ({ title, children }: SubStepProps) => {\n return (\n
\n

{title}

\n {children}\n
\n )\n}\n\nexport default SubStep\n", "import React from \"react\"\n\nimport { useStitchYourPhoto } from \"@warp/context/StitchYourPhotoContext\"\nimport { QuestionMark } from \"@warp/icons/QuestionMark\"\nimport SubStep from \"@warp/components/Stepper/SubStep\"\nimport { STITCH_YOUR_PHOTO_VALUES } from \"./Defaults\"\n\nexport interface FormatProps {\n translations: Record\n}\n\nfunction Format({ translations }: FormatProps) {\n const {\n selectedCanvasType,\n selectedCanvasSize,\n setSelectedCanvasType,\n setSelectedCanvasSize\n } = useStitchYourPhoto()\n\n const handleCanvasTypeChange = (\n event: React.ChangeEvent\n ) => {\n setSelectedCanvasType(event.target.value)\n }\n\n const handleCanvasSizeChange = (\n event: React.ChangeEvent\n ) => {\n setSelectedCanvasSize(event.target.value)\n }\n\n return (\n
\n \n {STITCH_YOUR_PHOTO_VALUES.canvasTypes.map((canvasType, i) => (\n
\n \n \n {translations[canvasType.name + \"_description\"]}\n \n \n
\n ))}\n
\n \n {STITCH_YOUR_PHOTO_VALUES.canvasSizes.map((canvasSize, i) => (\n
\n \n
\n ))}\n
\n\n
\n\n

{translations.step_format_disclaimer}

\n
\n )\n}\n\nexport default Format\n", "import React from \"react\"\n\nexport function QuestionMark({ fill, ...rest }: React.SVGProps) {\n return (\n \n \n \n )\n}\n", "import React from \"react\"\n\nimport { useStitchYourPhoto } from \"@warp/context/StitchYourPhotoContext\"\nimport SubStep from \"@warp/components/Stepper/SubStep\"\n\nexport interface ThreadProps {\n translations: Record\n}\n\nfunction Thread({ translations }: ThreadProps) {\n const { selectedThread, setSelectedThread, threadPrice, patternPrice } =\n useStitchYourPhoto()\n\n return (\n
\n \n
\n \n {threadPrice}\n
\n
\n \n {patternPrice}\n
\n
\n\n
\n\n
\n \n )\n}\n\nexport default Thread\n", "import React from \"react\"\nimport SubStep from \"@warp/components/Stepper/SubStep\"\nimport { useStitchYourPhoto } from \"@warp/context/StitchYourPhotoContext\"\nimport { Variant } from \"@warp/types/product\"\nimport Tool from \"./Tool\"\nexport interface ToolsProps {\n translations: Record\n accessories: Variant[]\n}\n\nfunction Tools({ translations, accessories }: ToolsProps) {\n const { toggleAccessory, selectedAccessories } = useStitchYourPhoto()\n\n return (\n
\n \n {accessories.map((accessory, index) => {\n return (\n \n )\n })}\n \n
\n )\n}\n\nexport default Tools\n", "import React from \"react\"\nimport cn from \"classnames\"\nimport Price from \"@warp/components/pdp/Price\"\nimport OutOfStock from \"@warp/components/OutOfStock\"\nimport { Variant } from \"@warp/types/product\"\n\ninterface ToolProps {\n accessory: Variant\n selectedAccessories: Variant[]\n toggleAccessory: (accessory: Variant) => void\n}\n\nconst Tool: React.FC = ({\n accessory,\n selectedAccessories,\n toggleAccessory\n}) => {\n const [notifyHidden, setNotifyHidden] = React.useState(true)\n\n const handleNotifyClick = () => {\n setNotifyHidden((previous) => !previous)\n }\n\n return (\n \n
\n \n \n
\n\n {!notifyHidden && (\n \n )}\n \n )\n}\n\nexport default Tool\n", "import React from \"react\"\n\nimport { useStepper } from \"@warp/context/StepperContext\"\nimport { useStitchYourPhoto } from \"@warp/context/StitchYourPhotoContext\"\nimport { QualityIcon } from \"@warp/icons/Quality\"\nimport { useMarket } from \"@warp/context/MarketContext\"\nimport { uploadImage, sendWizardData } from \"./Services\"\n\ntype UploadProps = {\n helpAndAdvicePath: string\n translations: Record\n}\n\nconst MAX_IMAGE_SIZE = 5 * 1024 * 1024\nconst VALID_IMAGE_FORMATS = \"image/jpeg,image/png,image/tif\"\n\nfunction Upload({ helpAndAdvicePath, translations }: UploadProps) {\n const fileInputRef = React.useRef(null)\n\n const { nextStep, isLoading, setIsLoading, activeStepIndex } = useStepper()\n const {\n originalImagePreviewUrl,\n setOriginalImage,\n setOriginalImagePreviewUrl,\n setCrossStitchImageUrl,\n selectedFilter,\n setPictureCode,\n selectedColor,\n selectedCanvasType,\n selectedCanvasSize,\n setCustomPatternId,\n setThreadAmount,\n reset\n } = useStitchYourPhoto()\n const { countryIso, locale } = useMarket()\n\n const [errorMessage, setErrorMessage] = React.useState(\"\")\n\n const handleFileChange = async (\n event: React.ChangeEvent\n ) => {\n // This ensures that the object URL is revoked when\n // a new file is selected, preventing memory leaks\n if (originalImagePreviewUrl) URL.revokeObjectURL(originalImagePreviewUrl)\n\n const file = event?.target?.files?.item(0)\n if (file && file.size > MAX_IMAGE_SIZE) {\n setErrorMessage(translations[\"step_upload_size_error\"])\n return\n }\n\n if (file && !VALID_IMAGE_FORMATS.includes(file.type)) {\n setErrorMessage(translations[\"step_upload_format_error\"])\n return\n }\n\n setErrorMessage(\"\")\n\n if (file) {\n setOriginalImage(file)\n setOriginalImagePreviewUrl(URL.createObjectURL(file))\n await handleImageUpload(file)\n }\n }\n\n const handleImageUpload = async (file: File) => {\n setIsLoading(true)\n try {\n let result = await uploadImage(file, countryIso, locale)\n const data = {\n picture_code: result.picture_code,\n image_type: selectedFilter,\n number_of_colours: selectedColor,\n canvas_type: selectedCanvasType,\n finished_size: selectedCanvasSize,\n dots_number: \"100\"\n }\n result = await sendWizardData(data, countryIso, locale)\n setPictureCode(result.custom_pattern.picture_code)\n setCrossStitchImageUrl(result.preview_image_url)\n setCustomPatternId(result.custom_pattern.id)\n setThreadAmount(result.custom_pattern_with_thread_amount)\n nextStep(true)\n } catch (error) {\n // The error that can happen at this point is likely to be \"canvas too big\".\n // It means the image has an unsupported canvas ratio.\n // The correct translation for this error is provided directly by the rails controller.\n // File format error and file size error are already handled in the handleFileChange function.\n if (error instanceof Error) {\n setErrorMessage(error.message)\n } else {\n setErrorMessage(\"An unknown error occurred\")\n }\n } finally {\n setIsLoading(false)\n }\n }\n\n React.useEffect(() => {\n // The user can arrive to this step by closing and reopening the modal\n // or by clicking the back button in the next step. In these cases\n // we need to clean up the state because there can be a moment where\n // the previous image is shown before being replace by the new one.\n reset()\n }, [])\n\n return (\n
\n \n

{translations.please_note}

\n

\n {translations.upload_file_types}\n

\n

\n {translations.upload_copyright_notice}\n

\n {errorMessage &&

{errorMessage}

}\n\n
\n {isLoading &&
}\n {!isLoading && (\n fileInputRef.current?.click()}\n >\n {translations.upload_file}\n \n )}\n \n \n {translations.upload_help}\n \n
\n
\n )\n}\n\nexport default Upload\n", "import React from \"react\"\n\nexport function QualityIcon({ fill, ...rest }: React.SVGProps) {\n return (\n \n \n \n )\n}\n", "import React from \"react\"\nimport cn from \"classnames\"\n\nimport { useTranslation } from \"@warp/context/TranslationContext\"\nimport WizardDialog from \"@warp/components/WizardDialog\"\nimport Footer from \"./Footer\"\nimport { useStitchYourPhoto } from \"@warp/context/StitchYourPhotoContext\"\nimport { useStepper } from \"@warp/context/StepperContext\"\nimport { CloseIcon } from \"@warp/icons/CloseIcon\"\nimport ZoomAndPan from \"@warp/components/pdp/Zoom/ZoomAndPan\"\nimport ZoomableImage from \"@warp/components/pdp/Zoom/ZoomableImage\"\n\nexport default function StitchYourPhotoDialog() {\n const translations = useTranslation()\n const [isZoomActive, setIsZoomActive] = React.useState(false)\n const { originalImagePreviewUrl, crossStitchImageUrl } = useStitchYourPhoto()\n const { isFirstStep, isLoading, activeStepIndex } = useStepper()\n\n const toggleZoom = () => {\n setIsZoomActive((prev) => !prev)\n }\n\n const stitchImage = crossStitchImageUrl\n ? {\n srcset: { \"1x\": crossStitchImageUrl || \"\" },\n alt: \"Stitched Image\"\n }\n : null\n\n const dialogProps = React.useMemo(() => {\n if (isFirstStep) {\n return {\n title: null,\n sidebar: null,\n footer: null,\n contentClassName: \"wizard__upload-content\",\n headerWrapperClassName: \"wizard__upload-header-wrapper\"\n }\n }\n\n return {\n sidebar: (\n \n {originalImagePreviewUrl && !isZoomActive && (\n
\n \"Original\n
\n )}\n {isLoading &&
}\n {stitchImage && !isLoading && !isZoomActive && (\n \n )}\n {isZoomActive && crossStitchImageUrl && (\n
\n \n \n
\n )}\n \n )\n }\n }, [\n isFirstStep,\n isZoomActive,\n originalImagePreviewUrl,\n isLoading,\n stitchImage,\n crossStitchImageUrl\n ])\n\n return (\n }\n dialogProps={dialogProps}\n isStepCompleted={(index) => activeStepIndex > index}\n />\n )\n}\n", "import React from \"react\"\n\nimport { useStepper } from \"@warp/context/StepperContext\"\nimport Dialog, { DialogProps } from \"../Dialog\"\nimport { StepperContainer } from \"../Stepper\"\nimport Step from \"../Stepper/Step\"\nimport StepContent from \"./StepContent\"\n\nexport interface WizardDialogProps {\n name: string\n description?: string\n subHeader?: React.ReactNode\n footer?: React.ReactNode\n dialogProps?: Omit, \"footer\">\n isStepCompleted?: (stepIndex: number) => boolean\n onStepClick?: (stepIndex: number) => void\n}\n\nexport default function WizardDialog({\n footer,\n subHeader,\n dialogProps,\n name,\n description,\n isStepCompleted,\n onStepClick\n}: WizardDialogProps) {\n const { steps } = useStepper()\n const { activeStepIndex, setStepClickable, clickableSteps } = useStepper()\n\n React.useEffect(() => {\n if (!clickableSteps[activeStepIndex]) {\n setStepClickable(activeStepIndex, true)\n }\n }, [activeStepIndex])\n\n const handleStepClick = React.useCallback(\n (index: number) => () => onStepClick?.(index),\n [onStepClick]\n )\n\n let title: string | React.ReactNode = (\n \n {steps.map((_, index) => (\n isStepCompleted(index))}\n onClick={handleStepClick(index)}\n />\n ))}\n \n )\n\n // This enables the ability to hide the title if\n // `dialogProps.title` is null\n if (dialogProps?.title === null || dialogProps?.title) {\n title = dialogProps.title\n }\n\n return (\n \n {subHeader}\n \n \n )\n}\n", "import { useStepper } from \"@warp/context/StepperContext\"\n\ninterface StepContentProps {}\n\nfunction StepContent({}: StepContentProps) {\n const { currentStep } = useStepper()\n\n return currentStep.component\n}\n\nexport default StepContent\n", "import React from \"react\"\n\nimport { ArrowIcon } from \"@warp/icons/ArrowIcon\"\nimport { useStepper } from \"@warp/context/StepperContext\"\nimport { useStitchYourPhoto } from \"@warp/context/StitchYourPhotoContext\"\nimport { addToCart } from \"./Services\"\nimport { errorToast, handleErrors } from \"@warp/utilities/errors\"\nimport { useMarket } from \"@warp/context/MarketContext\"\ninterface FooterProps {\n translations: Record\n}\n\nfunction Footer({ translations }: FooterProps) {\n const { nextStep, prevStep, hasCompletedAllSteps, isLastStep, isFirstStep } =\n useStepper()\n const [isLoading, setIsLoading] = React.useState(false)\n\n const { customPatternId, selectedAccessories, selectedThread, totalPrice } =\n useStitchYourPhoto()\n\n const { countryIso, locale } = useMarket()\n\n const handleAddToBasket = async () => {\n if (customPatternId) {\n setIsLoading(true)\n try {\n await addToCart(\n customPatternId,\n selectedThread === \"pdfWithThread\",\n selectedAccessories,\n countryIso,\n locale,\n translations[\"errors.something_went_wrong\"]\n )\n // when no error is thrown, dispatch an event to close the modal\n const openEvent = new Event(\"stitchYourPhotoModalClose\")\n document.dispatchEvent(openEvent)\n } catch (error) {\n // we're already handling error messages in the `addToCart` function\n } finally {\n setIsLoading(false)\n }\n }\n }\n\n const handleNextStep = () => {\n nextStep()\n }\n\n const handlePrevStep = () => {\n prevStep()\n }\n\n return hasCompletedAllSteps && isLastStep ? (\n <>\n \n \n \n \n {translations.add_to_basket}\n {totalPrice}\n \n \n ) : (\n <>\n \n \n \n\n
\n {translations.total}\n {totalPrice}\n
\n\n \n {translations.next_step}\n \n \n \n )\n}\n\nexport default Footer\n", "import React from \"react\"\n\nexport function ArrowIcon({ stroke, ...rest }: React.SVGProps) {\n return (\n \n \n \n )\n}\n", "import * as React from \"react\"\nimport { Image, ProductData, ProductType } from \"@warp/types/product\"\nimport { VariantSelectorWizardProvider } from \"@warp/context/VariantSelectorWizardContext\"\nimport VariantSelectorWizardDialog from \"./VariantSelectorWizardDialog\"\n\nexport interface VariantSelectorProps {\n productType: ProductType\n productName: string\n productData: ProductData\n slides: Image[]\n}\n\nexport default ({\n productName,\n productData,\n productType,\n slides\n}: VariantSelectorProps) => {\n return (\n \n \n \n )\n}\n", "import React, { useMemo } from \"react\"\nimport { Image, ProductData, ProductType, Variant } from \"../types/product\"\nimport MarketContext from \"./MarketContext\"\n\n// The state for selected variant IDs (array of variant IDs)\ntype SelectedVariantsState = Array<{ id: number; quantity: number }>\n\ntype SelectedOptionValueState = Record // Maps option type ID to option value ID\n\ntype SelectedVariantsAction =\n | { type: \"add\" | \"increment\"; payload: { id: number; quantity?: number } }\n | { type: \"remove\"; payload: { id: number } }\n | { type: \"updateVariantQuantity\"; payload: { id: number; quantity: number } }\n | { type: \"reset\" }\n\ntype SelectedOptionValueAction =\n | {\n type: \"selectOptionValue\"\n payload: { optionTypeId: number; optionValueId: number }\n }\n | { type: \"removeOptionValue\"; payload: { optionTypeId: number } }\n | { type: \"reset\" }\n\ninterface VariantSelectorWizardContextValue {\n totalPrice?: string\n totalAmount?: number\n productType: ProductType\n productData: ProductData\n // Option Type List without the colour_group type,\n // because it is only used in the colour filters\n optionTypes: ProductData[\"optionTypes\"]\n slides: Image[]\n masterVariantPrice?: string\n selectedVariants: SelectedVariantsState\n selectedOptionValueIds: SelectedOptionValueState\n // Variants filtered based on what the user has\n // previously selected\n filteredVariants: Variant[]\n}\n\nconst VariantSelectorWizardContext = React.createContext<\n VariantSelectorWizardContextValue & {\n addVariant: (id: number) => void\n removeVariant: (id: number) => void\n updateVariantQuantity: (id: number, quantity: number) => void\n reset: () => void\n selectOptionValue: (optionTypeId: number, optionValueId: number) => void\n removeOptionValue: (optionTypeId: number) => void\n }\n>({\n productData: {} as ProductData,\n productType: \"unspecified\",\n selectedVariants: [],\n optionTypes: [],\n slides: [],\n selectedOptionValueIds: {},\n filteredVariants: [],\n addVariant: () => {},\n removeVariant: () => {},\n updateVariantQuantity: () => {},\n reset: () => {},\n selectOptionValue: () => {},\n removeOptionValue: () => {}\n})\n\ntype VariantSelectorWizardContextProviderProps = {\n value: Omit<\n VariantSelectorWizardContextValue,\n | \"totalPrice\"\n | \"totalAmount\"\n | \"selectedVariants\"\n | \"selectedOptionValueIds\"\n | \"optionTypes\"\n | \"filteredVariants\"\n >\n children: React.ReactNode\n}\n\nfunction selectedVariantsReducer(\n state: SelectedVariantsState,\n action: SelectedVariantsAction\n): SelectedVariantsState {\n switch (action.type) {\n case \"add\":\n case \"increment\":\n const existingVariant = state.find(\n (variant) => variant.id === action.payload.id\n )\n if (existingVariant) {\n // Increment quantity if variant already exists\n return state.map((variant) =>\n variant.id === action.payload.id\n ? {\n ...variant,\n quantity: variant.quantity + (action.payload.quantity || 1)\n }\n : variant\n )\n }\n // Add new variant with default or provided quantity\n return [\n ...state,\n { id: action.payload.id, quantity: action.payload.quantity || 1 }\n ]\n case \"remove\":\n return state.filter((variant) => variant.id !== action.payload.id)\n case \"updateVariantQuantity\":\n if (action.payload.quantity <= 0) {\n return state.filter((variant) => variant.id !== action.payload.id)\n }\n return state.map((variant) =>\n variant.id === action.payload.id\n ? { ...variant, quantity: action.payload.quantity }\n : variant\n )\n case \"reset\":\n return []\n default:\n return state\n }\n}\n\nfunction selectedOptionValueReducer(\n state: SelectedOptionValueState,\n action: SelectedOptionValueAction\n): SelectedOptionValueState {\n switch (action.type) {\n case \"selectOptionValue\":\n return {\n ...state,\n [action.payload.optionTypeId]: action.payload.optionValueId\n }\n case \"removeOptionValue\":\n const newState = { ...state }\n delete newState[action.payload.optionTypeId]\n return newState\n case \"reset\":\n return {}\n default:\n return state\n }\n}\n\nexport const VariantSelectorWizardProvider = ({\n value,\n children\n}: VariantSelectorWizardContextProviderProps) => {\n const [selectedVariants, dispatchSelectedVariantsAction] = React.useReducer(\n selectedVariantsReducer,\n []\n )\n const [productData] = React.useState(value.productData)\n const { locale } = React.useContext(MarketContext)\n\n const masterVariantPrice = value.productData.variants.find(\n (variant) => variant.id === value.productData.masterVariant.id\n )?.price\n\n const optionTypeValueCount = productData.optionValues.reduce<\n Record\n >((count, optionValue) => {\n if (optionValue.optionTypeID === productData.colourGroupTypeID) {\n return count\n }\n\n if (count[optionValue.optionTypeID]) {\n count[optionValue.optionTypeID] += 1\n } else {\n count[optionValue.optionTypeID] = 1\n }\n\n return count\n }, {})\n\n // We don't need to show the colour group option type\n // and all the option types that have only one option value\n const optionTypes = productData.optionTypes.filter(\n (optionType) =>\n optionType.id !== productData.colourGroupTypeID &&\n optionTypeValueCount[optionType.id] > 1\n )\n\n // Set the option types that has only one option value as selected\n // so that we can show it to the user\n const initialSelectedOptionValueIds = Object.entries(\n optionTypeValueCount\n ).reduce>((result, [optionTypeId, count]) => {\n if (count === 1) {\n const id = parseInt(optionTypeId)!\n result[id] = productData.optionValues.find(\n (optionValue) => optionValue.optionTypeID === id\n )!.id\n }\n\n return result\n }, {})\n\n const [selectedOptionValueIds, dispatchSelectedOptionValueAction] =\n React.useReducer(selectedOptionValueReducer, initialSelectedOptionValueIds)\n\n const addVariant = (id: number, quantity = 1) =>\n dispatchSelectedVariantsAction({ type: \"add\", payload: { id, quantity } })\n\n const updateVariantQuantity = (id: number, quantity: number) =>\n dispatchSelectedVariantsAction({\n type: \"updateVariantQuantity\",\n payload: { id, quantity }\n })\n\n const removeVariant = (id: number) =>\n dispatchSelectedVariantsAction({ type: \"remove\", payload: { id } })\n\n const reset = () => {\n dispatchSelectedVariantsAction({ type: \"reset\" })\n dispatchSelectedOptionValueAction({ type: \"reset\" })\n }\n\n const selectOptionValue = (optionTypeId: number, optionValueId: number) =>\n dispatchSelectedOptionValueAction({\n type: \"selectOptionValue\",\n payload: { optionTypeId, optionValueId }\n })\n\n const removeOptionValue = (optionTypeId: number) =>\n dispatchSelectedOptionValueAction({\n type: \"removeOptionValue\",\n payload: { optionTypeId }\n })\n\n const totalAmount = useMemo(() => {\n return selectedVariants.reduce((total, { id, quantity }) => {\n const variant = productData.variants.find((v) => v.id === id)\n return (\n total +\n quantity * (variant?.rawDiscountedAmount || variant?.rawAmount || 0)\n )\n }, 0)\n }, [selectedVariants, productData])\n\n const totalPrice = useMemo(() => {\n if (!productData.variants[0]) return \"\"\n return Intl.NumberFormat(locale, {\n style: \"currency\",\n currency: productData.variants[0].currency\n }).format(totalAmount)\n }, [totalAmount, locale])\n\n const filteredVariants = useMemo(() => {\n const selectedEntries = Object.entries(selectedOptionValueIds)\n\n return productData.variants.filter((variant) =>\n selectedEntries.every(([, valueId]) =>\n variant.optionValueIds.includes(Number(valueId))\n )\n )\n }, [selectedOptionValueIds, productData.variants])\n\n return (\n \n {children}\n \n )\n}\n\nexport function useVariantSelectorWizard() {\n const context = React.useContext(VariantSelectorWizardContext)\n if (context === undefined) {\n throw new Error(\n \"useVariantSelectorWizard must be used within a VariantSelectorWizardProvider\"\n )\n }\n return context\n}\n", "import React from \"react\"\nimport Dialog from \"@warp/components/Dialog\"\nimport { Image } from \"@warp/types/product\"\nimport { useVariantSelectorWizard } from \"@warp/context/VariantSelectorWizardContext\"\nimport { useTranslation } from \"@warp/context/TranslationContext\"\nimport WizardDialog from \"./WizardDialog\"\nimport Footer from \"./Footer\"\nimport SelectedVariantsHeader from \"./SelectedVariantsHeader\"\nimport ColourTypeStep from \"./ColourStep\"\nimport SingleStepFooter from \"./SingleStepFooter\"\nimport { Step as StepItem } from \"@warp/types/stepper\"\nimport OptionTypeStep from \"./OptionTypeStep\"\nimport Stepper from \"@warp/components/Stepper\"\nimport SelectedValuesHeader from \"./SelectedValuesHeader\"\n\ninterface VariantSelectorWizardDialogProps {\n title: string\n slides: Image[]\n}\n\nexport default function VariantSelectorWizardDialog({\n title\n}: VariantSelectorWizardDialogProps) {\n const translations = useTranslation()\n\n const {\n productData,\n productType,\n optionTypes,\n selectedVariants,\n selectedOptionValueIds\n } = useVariantSelectorWizard()\n\n const dialogProps = {\n modalName: \"variant\",\n description:\n translations[\"components.products.variant_selector_dialog_description\"],\n contentClassName: `variant-dialog-wizard ${\n selectedVariants.length > 0 ? \"selected-variants-preview\" : \"\"\n }`\n }\n\n if (optionTypes.length > 1) {\n // Separate option types by non-colour and colour\n const nonColourOptionTypes = optionTypes.filter(\n (optionType) => !productData.colourTypeIds.includes(optionType.id)\n )\n\n // Get the first colour option type, if any\n const firstColourOptionType = optionTypes.find((optionType) =>\n productData.colourTypeIds.includes(optionType.id)\n )\n\n // Map non-colour option types into steps\n const steps: StepItem[] = nonColourOptionTypes.map((optionType, index) => ({\n id: `option-type-${optionType.id}`,\n label: optionType.presentation,\n component: (\n \n ),\n metadata: {\n optionTypeId: optionType.id\n },\n index: index\n }))\n\n // Add the first colour option type as the last step, if it exists\n if (firstColourOptionType) {\n steps.push({\n id: `colour-type-${firstColourOptionType.id}`,\n label: firstColourOptionType.presentation,\n component: ,\n metadata: {\n optionTypeId: firstColourOptionType.id // Always include the optionType.id in metadata\n },\n index: steps.length\n })\n }\n\n const subHeader = (\n <>\n {Object.keys(selectedOptionValueIds).length > 0 && (\n \n )}\n {selectedVariants.length > 0 && }\n \n )\n\n return (\n \n }\n />\n \n )\n }\n\n // Here we are sure that the product has only one option type (excluding the colour_group),\n // so we only need to check if it corresponds to a colour type\n if (productData.colourTypeIds.includes(optionTypes[0].id)) {\n return (\n }>\n {selectedVariants.length > 0 && }\n \n \n )\n }\n}\n", "import React from \"react\"\nimport GeneralWizardDialog, {\n WizardDialogProps as GeneralWizardDialogProps\n} from \"@warp/components/WizardDialog\"\nimport { useStepper } from \"@warp/context/StepperContext\"\nimport { useVariantSelectorWizard } from \"@warp/context/VariantSelectorWizardContext\"\nimport Slideshow from \"../Slideshow\"\n\nexport interface WizardDialogProps extends GeneralWizardDialogProps {}\n\nexport default function WizardDialog({\n dialogProps,\n ...props\n}: WizardDialogProps) {\n const { steps, setStepCompleted, setStepClickable, currentStep } =\n useStepper()\n const { removeOptionValue, productData, productType, optionTypes } =\n useVariantSelectorWizard()\n\n const handleStepClick = React.useCallback(\n (index: number) => {\n for (let i = index + 1; i < steps.length; i++) {\n if (steps[i]?.metadata?.optionTypeId) {\n removeOptionValue(steps[i]!.metadata!.optionTypeId)\n setStepCompleted(i, false)\n setStepClickable(i, false)\n }\n }\n },\n [steps, removeOptionValue, setStepCompleted]\n )\n\n const sidebar = React.useMemo(() => {\n const optionType = optionTypes.find(\n (option) => option.id === currentStep?.metadata?.optionTypeId\n )\n\n if (productType === \"thread\" && optionType?.name === \"size\") {\n const sizeImage = productData.images.filter(\n (image) => image.type === \"size_step\"\n )\n return sizeImage ? (\n \n ) : null\n }\n\n return null\n }, [productData, productType, optionTypes, currentStep])\n\n return (\n \n )\n}\n", "import { useStepper } from \"@warp/context/StepperContext\"\nimport { useTranslation } from \"@warp/context/TranslationContext\"\nimport { useVariantSelectorWizard } from \"@warp/context/VariantSelectorWizardContext\"\nimport { ArrowIcon } from \"@warp/icons/ArrowIcon\"\nimport { addToCart } from \"@warp/utilities/api\"\nimport { errorToast } from \"@warp/utilities/errors\"\nimport React from \"react\"\n\nfunction Footer() {\n const translations = useTranslation()\n const {\n nextStep,\n prevStep,\n hasCompletedAllSteps,\n isLastStep,\n isFirstStep,\n currentStep,\n resetSteps,\n setStepClickable\n } = useStepper()\n\n const [isLoading, setIsLoading] = React.useState(false)\n const {\n selectedOptionValueIds,\n selectedVariants,\n totalPrice,\n removeOptionValue,\n reset\n } = useVariantSelectorWizard()\n\n const nextStepDisabled = React.useMemo(() => {\n const { optionTypeId } = currentStep.metadata || {}\n return !(optionTypeId && selectedOptionValueIds[optionTypeId])\n }, [currentStep.metadata, selectedOptionValueIds])\n\n const handleAddToBasket = async () => {\n setIsLoading(true)\n try {\n // Transform selectedVariants into the required format\n const payload = selectedVariants.map(({ id, quantity }) => ({\n id,\n quantity\n }))\n\n await addToCart(payload)\n } catch (error) {\n console.error(\"Error adding to basket:\", error)\n errorToast(new Error(translations[\"errors.something_went_wrong\"]))\n } finally {\n setIsLoading(false)\n resetSteps()\n reset()\n setStepClickable(0, true)\n // Dispatch event to close the modal\n document.dispatchEvent(new Event(\"variantModalClose\"))\n }\n }\n\n const handlePrevStep = () => {\n const { optionTypeId } = currentStep.metadata || {}\n if (optionTypeId && selectedOptionValueIds[optionTypeId]) {\n removeOptionValue(optionTypeId)\n }\n prevStep()\n }\n\n const handleNextStep = () => {\n nextStep(true)\n }\n\n return isLastStep ? (\n <>\n \n \n \n \n {translations.add_to_basket}\n {totalPrice}\n \n \n ) : (\n <>\n \n \n \n\n
\n {translations.total}\n {totalPrice}\n
\n\n \n {translations.next_step}\n \n \n \n )\n}\n\nexport default Footer\n", "import { useVariantSelectorWizard } from \"@warp/context/VariantSelectorWizardContext\"\nimport { Variant } from \"@warp/types/product\"\nimport { getLastSrcsetUrl } from \"@warp/utilities/images\"\nimport React from \"react\"\nimport Price from \"../../Price\"\nimport { CloseIcon } from \"@warp/icons/CloseIcon\"\nimport { useTranslation } from \"@warp/context/TranslationContext\"\nimport PrevArrow from \"@warp/components/Carousel/PrevArrow\"\nimport NextArrow from \"@warp/components/Carousel/NextArrow\"\nimport horizontalScroll from \"@warp/utilities/horizontalScroll\"\n\ntype SelectedVariant = Variant & {\n imageUrl?: string\n imageAlt?: string\n quantity: number\n}\n\nexport default function SelectedVariantsHeader() {\n const {\n selectedVariants: selectedVariantsData,\n productData,\n slides,\n removeVariant\n } = useVariantSelectorWizard()\n const t = useTranslation()\n const containerRef = React.useRef(null)\n const scrollableRef = React.useRef(null)\n const [arrowsEnabled, setArrowsEnabled] = React.useState(false)\n\n React.useLayoutEffect(() => {\n setArrowsEnabled(\n (containerRef.current?.clientWidth ?? 0) <\n (scrollableRef.current?.scrollWidth ?? 0)\n )\n }, [selectedVariantsData])\n\n React.useLayoutEffect(() => {\n if (arrowsEnabled) {\n horizontalScroll()\n }\n }, [arrowsEnabled])\n\n const selectedVariants = selectedVariantsData.map(\n ({ id: selectedVariantId, quantity }) => {\n const variant = productData.variants.find(\n (variant) => variant.id === selectedVariantId\n )\n const image = slides.find(\n (slide) => slide.variantId === selectedVariantId\n )\n\n return {\n ...variant,\n imageUrl: variant?.colour\n ? variant?.colour?.swatch\n : image\n ? getLastSrcsetUrl(image.srcset)\n : null,\n imageAlt: variant?.colour?.presentation || image?.alt,\n quantity\n }\n }\n ) as SelectedVariant[]\n\n const handleRemove = (variantId: number) => () => {\n removeVariant(variantId)\n }\n\n return (\n \n \n {arrowsEnabled && (\n \n )}\n {selectedVariants.map((variant) => (\n \n
\n
\n {variant.quantity}\n
\n
\n {variant.imageUrl && (\n \n )}\n
\n \n \n \n
\n
\n \n \n {t[\"components.product.dialog.selected_variants.each\"]}\n \n
\n \n ))}\n {arrowsEnabled && (\n \n )}\n \n \n )\n}\n", "import React from \"react\"\n\nexport default function PrevArrow({ title }: { title: string }) {\n return (\n \n )\n}\n", "import React from \"react\"\n\nexport default function NextArrow({ title }: { title: string }) {\n return (\n \n )\n}\n", "import React from \"react\"\nimport { FiltersProvider } from \"./FiltersContext\"\nimport ColourTypeStep from \"./Step\"\n\nexport default function ColourTypeStepWrapper() {\n return (\n \n \n \n )\n}\n", "import React from \"react\"\n\ninterface FiltersContextValue {\n colourGroupIds: number[]\n search: string\n}\n\nconst FiltersContext = React.createContext<\n FiltersContextValue & {\n selectColourGroup: (id: number) => void\n removeColourGroup: (id: number) => void\n setSearch: (search: string) => void\n reset: () => void\n }\n>({\n colourGroupIds: [],\n search: \"\",\n selectColourGroup: () => {},\n removeColourGroup: () => {},\n setSearch: () => {},\n reset: () => {}\n})\n\ntype FiltersProviderProps = {\n children: React.ReactNode\n}\n\nexport const FiltersProvider = ({ children }: FiltersProviderProps) => {\n const [colourGroupIds, setColourGroupIds] = React.useState([])\n const [search, setSearch] = React.useState(\"\")\n\n function selectColourGroup(id: number) {\n setColourGroupIds((prev) => {\n if (prev.includes(id)) {\n return prev\n }\n return [...prev, id]\n })\n }\n\n function removeColourGroup(id: number) {\n setColourGroupIds((prev) => prev.filter((i) => i !== id))\n }\n\n function reset() {\n setColourGroupIds([])\n setSearch(\"\")\n }\n\n return (\n \n {children}\n \n )\n}\n\nexport function useFilters() {\n const context = React.useContext(FiltersContext)\n\n if (context === undefined) {\n throw new Error(\"useFilters must be used within a FiltersProvider\")\n }\n\n return context\n}\n", "import React, {\n useEffect,\n useLayoutEffect,\n useMemo,\n useRef,\n useState\n} from \"react\"\nimport Colour from \"./Colour\"\nimport ColourFilters from \"./Filters\"\nimport SearchFilter from \"./SearchFilter\"\nimport { useTranslation } from \"@warp/context/TranslationContext\"\nimport { ListViewIcon } from \"../ListViewIcon\"\nimport { GridViewIcon } from \"../GridViewIcon\"\nimport { ArrowIcon } from \"../ArrowIcon\"\nimport FilterIcon from \"@warp/icons/FilterIcon\"\nimport SearchIcon from \"@warp/icons/SearchIcon\"\nimport OutOfStock from \"@warp/components/OutOfStock\"\nimport { useGroupedVariants } from \"./hooks\"\nimport { useVariantSelectorWizard } from \"@warp/context/VariantSelectorWizardContext\"\nimport { useStepper } from \"@warp/context/StepperContext\"\nimport { useFilters } from \"./FiltersContext\"\n\nconst ColourTypeStep = () => {\n const groupedVariants = useGroupedVariants()\n const { selectedVariants } = useVariantSelectorWizard()\n const { setStepCompleted, activeStepIndex, completedSteps } = useStepper()\n const { search } = useFilters()\n const t = useTranslation()\n\n const [isGridView, setIsGridView] = React.useState(true) // State to toggle between grid and list views\n const [showFilters, setShowFilters] = React.useState(false)\n const [showSearch, setShowSearch] = React.useState(false)\n const [showScrollToTopButton, setShowScrollToTopButton] = useState(false)\n const [selectedOutOfStockVariantId, setSelectedOutOfStockVariantId] =\n useState(null)\n\n const containerRef = useRef(null)\n const outOfStockContainerRef = useRef(null)\n\n const outOfStockVariants = React.useMemo(\n () =>\n groupedVariants.find(\n ({ colourGroup }) => colourGroup.name === \"out_of_stock\"\n )?.variants || [],\n [groupedVariants]\n )\n\n const selectedOutOfStockVariant = useMemo(() => {\n return outOfStockVariants.find(\n (variant) => variant.id === selectedOutOfStockVariantId\n )\n }, [selectedOutOfStockVariantId, outOfStockVariants])\n\n useLayoutEffect(() => {\n // We use useLayoutEffect becuase we want to wait\n // until the email field html for the out of stock notification\n // is available\n if (selectedOutOfStockVariantId && outOfStockContainerRef.current) {\n outOfStockContainerRef.current.scrollIntoView({ behavior: \"smooth\" })\n }\n }, [selectedOutOfStockVariantId])\n\n useEffect(() => {\n const stepCompleted = selectedVariants.length > 0\n if (completedSteps[activeStepIndex] !== stepCompleted) {\n setStepCompleted(activeStepIndex, stepCompleted)\n }\n }, [selectedVariants, activeStepIndex, setStepCompleted])\n\n useEffect(() => {\n // We need a reference to the radix dialog body to determine its scroll position\n // Maybe this could be accomplished with ref forwarding, or using the context API.\n const dialogBody = document.querySelector(\n '[data-dialog-body=\"variant-dialog-body\"]'\n ) as HTMLDivElement\n\n if (!dialogBody) return\n\n const handleScroll = () => {\n // Get the container's first item\n const firstItem = containerRef.current?.querySelector(\n isGridView\n ? \".colour-option-type__grid > *\"\n : \".colour-option-type__list > *\"\n )\n\n if (!firstItem) return\n // Get the radix dialog body's bounding rect to determine its top edge position\n const dialogRect = dialogBody.getBoundingClientRect()\n // Get the first item's position relative to the viewport\n const itemRect = firstItem.getBoundingClientRect()\n\n // Shows the scroll to top button if the first item's bottom edge\n // is above the dialog's top edge.\n // This logic could be upgraded to be more dynamic but to be effective\n // we need to know the exact height of the dialog body\n // by subtracting to the Dialog entire modal the heights of the header, footer\n // and other elements that might be present in the dialog later in the user interaction.\n // For instance the presence of the selected variants.\n // Sounds like a resize observer could also be involved.\n // For now, we are keeping it simple and here we are just dividing\n // itemRect.bottom by 2 to make the button appear earlier.\n // This logic is just a placeholder.\n setShowScrollToTopButton(itemRect.bottom / 2 < dialogRect.top)\n }\n\n dialogBody.addEventListener(\"scroll\", handleScroll)\n handleScroll()\n\n return () => {\n dialogBody.removeEventListener(\"scroll\", handleScroll)\n }\n }, [isGridView])\n\n const handleToggleFilters = () => {\n setShowFilters(!showFilters)\n }\n\n const handleToggleSearch = () => {\n setShowSearch(!showSearch)\n }\n\n const handleSearchClose = () => {\n setShowSearch(false)\n }\n\n const handleFiltersClose = () => {\n setShowFilters(false)\n }\n\n const handleScrollToTop = () => {\n const dialogBody = document.querySelector(\n '[data-dialog-body=\"variant-dialog-body\"]'\n )\n if (dialogBody) {\n dialogBody.scrollTo({ top: 0, behavior: \"smooth\" })\n }\n }\n\n let outOfStockMessage = \"\"\n if (outOfStockVariants.length > 0) {\n const pluralKey = outOfStockVariants.length > 1 ? \"other\" : \"one\"\n outOfStockMessage = t[`out_of_stock_message.${pluralKey}`].replace(\n \"$count\",\n `${outOfStockVariants.length}`\n )\n }\n\n return (\n <>\n {showFilters && }\n
\n
\n
\n
\n \n {!showSearch && (\n \n )}\n
\n {showSearch && }\n
\n setIsGridView(true)}\n >\n \n \n setIsGridView(false)}\n >\n \n \n
\n
\n
\n {groupedVariants.length === 0 ? (\n
\n {search ? (\n <>\n {t[\"components.site_search.no_results_matching\"]}\n

{`'${search}'`}

\n \n ) : (\n {t[\"components.products.dialog.filters.no_results\"]}\n )}\n
\n ) : (\n \n {groupedVariants.map(({ colourGroup, variants }) => (\n \n
\n \n {\"presentation\" in colourGroup\n ? colourGroup.presentation\n : t[\n `components.products.dialog.colour_groups.${colourGroup.name}`\n ]}\n \n
\n {variants.map((variant) => (\n \n ))}\n
\n ))}\n {outOfStockVariants.length > 0 && (\n \n {outOfStockMessage}\n {!!selectedOutOfStockVariant && (\n \n )}\n
\n )}\n \n )}\n {showScrollToTopButton && (\n \n \n \n )}\n \n \n )\n}\n\nexport default ColourTypeStep\n", "import React, { useMemo } from \"react\"\nimport { Variant } from \"@warp/types/product\"\nimport { useVariantSelectorWizard } from \"@warp/context/VariantSelectorWizardContext\"\nimport ColourItem from \"../../ColourItem\"\n\nexport interface Colour {\n gridView?: boolean\n variant: Variant\n onOutOfStockVariantSelected: (id: number) => void\n}\n\nconst Colour = ({ gridView, variant, onOutOfStockVariantSelected }: Colour) => {\n const {\n productData,\n selectedVariants,\n addVariant,\n updateVariantQuantity,\n removeVariant\n } = useVariantSelectorWizard()\n\n const isVariantSelected = useMemo(\n () => selectedVariants.some((v) => v.id === variant?.id),\n [selectedVariants, variant?.id]\n )\n\n const quantity = useMemo(\n () => selectedVariants.find((v) => v.id === variant?.id)?.quantity || 0,\n [selectedVariants, variant?.id]\n )\n\n const setQuantity = (quantity: number) => {\n variant?.id && updateVariantQuantity(variant.id, quantity)\n }\n\n const checked = isVariantSelected && quantity > 0\n\n const handleChange = () => {\n if (!variant.inStock) {\n onOutOfStockVariantSelected(variant.id)\n return\n }\n\n if (checked) {\n removeVariant(variant.id)\n } else {\n addVariant(variant.id)\n }\n }\n\n return (\n \n )\n}\n\nexport default Colour\n", "import React, { useCallback, useMemo } from \"react\"\nimport cn from \"classnames\"\nimport { Badge, Colour, KitVariant, Variant } from \"@warp/types/product\"\nimport MailIcon from \"@warp/icons/MailIcon\"\nimport { useTranslation } from \"@warp/context/TranslationContext\"\nimport QuantityPicker from \"./QuantityPicker\"\nimport ColourBadges from \"./VariantSelectorWizard/ColourStep/ColourBadges\"\n\nexport interface ColourItemProps {\n variant: Variant | KitVariant\n checked?: boolean\n gridView?: boolean\n quantity?: number\n badges?: Badge[]\n className?: string\n onChange?: (selected: boolean) => void\n onOutOfStockSelected?: (variantId: number) => void\n onQuantityChange?: (quantity: number) => void\n}\n\nexport default function ColourItem({\n variant,\n checked,\n quantity,\n badges,\n className,\n gridView = true,\n onChange,\n onOutOfStockSelected,\n onQuantityChange\n}: ColourItemProps) {\n const t = useTranslation()\n\n const image =\n (variant.colour as Colour)?.swatch ||\n (variant as KitVariant).swatch ||\n variant.image ||\n \"\"\n\n const handleChange = useCallback(() => {\n if (!variant.inStock) {\n onOutOfStockSelected?.(variant.id)\n return\n }\n\n onChange?.(!checked)\n }, [variant, onOutOfStockSelected, onChange, checked])\n\n const label = useMemo(\n () => variant.colour?.presentation || variant.colour?.code,\n [variant.colour]\n )\n\n return (\n \n
\n \n {!gridView && !variant.inStock ? (\n
\n {t[\"out_of_stock\"]}\n
\n ) : checked && quantity && onQuantityChange ? (\n
\n
\n \n
\n
\n ) : null}\n
\n {(badges?.length ?? 0) > 0 && (\n \n )}\n \n )\n}\n", "import React from \"react\"\n\nconst MailIcon = () => (\n \n \n \n \n \n \n \n \n)\n\nexport default MailIcon\n", "import React from \"react\"\nimport Badge from \"@warp/components/pdp/Badges/Badge\"\nimport {\n KitVariant,\n Badge as PositionalBadge,\n Variant\n} from \"@warp/types/product\"\n\ninterface ColourBadges {\n variant: Variant | KitVariant\n badges?: PositionalBadge[]\n}\n\nconst ColourBadges = ({ variant, badges }: ColourBadges) => {\n if (!badges || badges.length === 0) {\n return null\n }\n\n // For now we support only the on sale badge\n const filteredBadges = badges.filter(\n (badge) => badge.key === \"variants_on_sale\"\n )\n\n return (\n
\n {filteredBadges.map((badge, index) => (\n \n ))}\n
\n )\n}\n\nexport default ColourBadges\n", "import { useVariantSelectorWizard } from \"@warp/context/VariantSelectorWizardContext\"\nimport { CloseIcon } from \"@warp/icons/CloseIcon\"\nimport React from \"react\"\nimport { useFilters } from \"./FiltersContext\"\nimport { ColourGroup } from \"@warp/types/product\"\nimport CheckIcon from \"@warp/icons/Check\"\nimport { useTranslation } from \"@warp/context/TranslationContext\"\n\nexport interface ColourFiltersProps {\n onClose: () => void\n}\n\ntype ColourInfo = Record<\n number,\n { colourGroup: ColourGroup; variantCount: number }\n>\n\nexport default function ColourFilters({ onClose }: ColourFiltersProps) {\n const { filteredVariants } = useVariantSelectorWizard()\n const { colourGroupIds, selectColourGroup, removeColourGroup, reset } =\n useFilters()\n const t = useTranslation()\n\n const colourGroupInfo: ColourInfo = React.useMemo(() => {\n const colourGroupInfo: ColourInfo = {}\n\n filteredVariants.forEach((variant) => {\n if (variant.colour?.colourGroup) {\n if (!colourGroupInfo[variant.colour.colourGroup.id]) {\n colourGroupInfo[variant.colour.colourGroup.id] = {\n colourGroup: variant.colour.colourGroup,\n variantCount: 0\n }\n }\n\n colourGroupInfo[variant.colour.colourGroup.id].variantCount += 1\n }\n })\n\n return colourGroupInfo\n }, [filteredVariants])\n\n const handleColourGroupChange = (colourGroupId: number) => () => {\n if (colourGroupIds.includes(colourGroupId)) {\n removeColourGroup(colourGroupId)\n } else {\n selectColourGroup(colourGroupId)\n }\n }\n\n return (\n
\n
\n
\n
\n
\n {t[\"components.products.dialog.filters.title\"]}\n
\n \n \n {t[\"components.products.dialog_close\"]}\n \n
\n
\n\n
\n
\n

\n {t[\"step_colour_title\"]}\n

\n
    \n {Object.values(colourGroupInfo).map((info) => (\n \n \n \n {colourGroupIds.includes(info.colourGroup.id) && (\n \n )}\n \n ))}\n
\n
\n
\n\n
\n
\n \n \n {t[\"components.products.dialog.filters.reset\"]}\n \n \n \n \n {t[\"components.products.dialog.filters.done\"]}\n \n \n
\n
\n
\n
\n )\n}\n", "import React from \"react\"\n\nexport default function CheckIcon() {\n return (\n \n \n \n )\n}\n", "import React from \"react\"\nimport { useFilters } from \"./FiltersContext\"\nimport { useTranslation } from \"@warp/context/TranslationContext\"\nimport { CloseIcon } from \"@warp/icons/CloseIcon\"\n\nexport interface SearchFilterProps {\n onClose: () => void\n}\n\nexport default function SearchFilter({ onClose }: SearchFilterProps) {\n const { search, setSearch } = useFilters()\n const t = useTranslation()\n\n const handleChange = (e: React.ChangeEvent) => {\n setSearch(e.target.value)\n }\n\n const handleClose = () => {\n setSearch(\"\")\n onClose()\n }\n\n return (\n
\n \n \n
\n )\n}\n", "import React from \"react\"\n\nconst ListViewIcon = () => {\n return (\n \n \n \n \n \n \n \n )\n}\n\nexport { ListViewIcon }\n", "import React from \"react\"\n\nconst GridViewIcon = () => {\n return (\n \n \n \n \n \n )\n}\n\nexport { GridViewIcon }\n", "import React from \"react\"\n\nconst ArrowIcon = () => {\n return (\n \n \n \n \n )\n}\n\nexport { ArrowIcon }\n", "import React from \"react\"\n\nconst FilterIcon = () => (\n \n \n \n)\n\nexport default FilterIcon\n", "import React from \"react\"\n\nconst SearchIcon = () => (\n \n \n \n)\n\nexport default SearchIcon\n", "import React from \"react\"\nimport { useFilters } from \"./FiltersContext\"\nimport { useVariantSelectorWizard } from \"@warp/context/VariantSelectorWizardContext\"\nimport { ColourGroup, Variant } from \"@warp/types/product\"\n\ntype GroupedVariants = Array<{\n colourGroup: ColourGroup | { name: \"other\" } | { name: \"out_of_stock\" }\n variants: Variant[]\n}>\n\nexport function useGroupedVariants() {\n const { filteredVariants, selectedOptionValueIds, productData } =\n useVariantSelectorWizard()\n const { colourGroupIds, search } = useFilters()\n\n const availableVariants = React.useMemo(() => {\n let variants = filteredVariants.filter((variant) => variant.colour !== null)\n\n if (search) {\n variants = variants.filter(\n (variant) =>\n variant.sku.includes(search) ||\n variant.title?.includes(search) ||\n variant.colour?.colourGroup?.presentation\n ?.toLowerCase()\n ?.includes(search.toLowerCase()) ||\n variant.colour?.code?.toLowerCase()?.includes(search.toLowerCase()) ||\n variant.colour?.presentation\n ?.toLowerCase()\n ?.includes(search.toLowerCase())\n )\n }\n\n if (colourGroupIds.length > 0) {\n variants = variants.filter(\n (variant) =>\n variant.colour?.colourGroup?.id &&\n colourGroupIds.includes(variant.colour.colourGroup.id)\n )\n }\n\n return variants\n }, [selectedOptionValueIds, productData.variants, colourGroupIds, search])\n\n const groupedVariants = React.useMemo(() => {\n let groupedVariants: GroupedVariants = []\n const outOfStockVariants: Variant[] = []\n const otherVariants: Variant[] = []\n\n for (const variant of availableVariants) {\n if (!variant.inStock) {\n outOfStockVariants.push(variant)\n continue\n }\n\n if (!variant.colour?.colourGroup) {\n otherVariants.push(variant)\n continue\n }\n\n const index = groupedVariants.findIndex(\n ({ colourGroup }) =>\n \"id\" in colourGroup &&\n colourGroup.id === variant.colour!.colourGroup!.id\n )\n\n if (index === -1) {\n groupedVariants.push({\n colourGroup: variant.colour!.colourGroup!,\n variants: [variant]\n })\n } else {\n groupedVariants[index].variants.push(variant)\n }\n }\n\n if (otherVariants.length > 0) {\n groupedVariants = groupedVariants.concat([\n {\n colourGroup: { name: \"other\" },\n variants: otherVariants\n }\n ])\n }\n\n if (outOfStockVariants.length > 0) {\n groupedVariants = groupedVariants.concat([\n {\n colourGroup: { name: \"out_of_stock\" },\n variants: outOfStockVariants\n }\n ])\n }\n\n return groupedVariants\n }, [availableVariants])\n\n return groupedVariants\n}\n", "import React from \"react\"\nimport { addToCart } from \"@warp/utilities/api\"\nimport { errorToast, handleErrors } from \"@warp/utilities/errors\"\nimport { useVariantSelectorWizard } from \"@warp/context/VariantSelectorWizardContext\"\nimport { useTranslation } from \"@warp/context/TranslationContext\"\n\nconst SingleStepFooter = () => {\n const { totalPrice, totalAmount, selectedVariants, reset } =\n useVariantSelectorWizard()\n const [isLoading, setIsLoading] = React.useState(false)\n const translations = useTranslation()\n\n // The button is disabled if the sum of all the selected variant\n // quantities is 0\n const disabled =\n selectedVariants.reduce((total, variant) => total + variant.quantity, 0) ===\n 0\n\n const handleAddToBasket = async () => {\n setIsLoading(true)\n try {\n await addToCart(\n // This just maps an object of the form { : }\n // into an array of the form [{ id: , quantity: }]\n selectedVariants.map((variant) => ({\n id: variant.id,\n quantity: variant.quantity\n }))\n )\n const event = new Event(\"variantModalClose\")\n document.dispatchEvent(event)\n } catch (error) {\n const handled =\n error instanceof Response ? await handleErrors(error) : false\n if (!handled) {\n errorToast(new Error(translations[\"errors.something_went_wrong\"]))\n }\n } finally {\n setIsLoading(false)\n reset()\n }\n }\n\n return (\n <>\n
\n \n {translations.add_to_basket}\n {totalAmount ? (\n {totalPrice}\n ) : null}\n \n \n )\n}\n\nexport default SingleStepFooter\n", "import { useVariantSelectorWizard } from \"@warp/context/VariantSelectorWizardContext\"\nimport React, { useMemo } from \"react\"\nimport type {\n OptionType,\n OptionValue as OptionValueType\n} from \"@warp/types/product\"\nimport { optionTypeTranslation } from \"@warp/utilities/translation\"\nimport { useTranslation } from \"@warp/context/TranslationContext\"\nimport OptionValue from \"./OptionValue\"\nimport difference from \"lodash/difference\"\nimport uniq from \"lodash/uniq\"\n\nexport interface OptionTypeStepProps {\n optionType: OptionType\n showAvailableVariants?: boolean\n showMinPrices?: boolean\n customTitle?: boolean\n}\n\nexport default function OptionTypeStep({\n optionType,\n customTitle = false,\n showAvailableVariants = false,\n showMinPrices = false\n}: OptionTypeStepProps) {\n const { productData, selectedOptionValueIds } = useVariantSelectorWizard()\n const translations = useTranslation()\n\n const optionValues = useMemo(() => {\n // Filter all option values for the current option type\n let values: OptionValueType[] = productData.optionValues.filter(\n (optionValue) => optionValue.optionTypeID === optionType.id\n )\n\n // Exclude the current option type's selected value (if any) from the global selectedOptionValueIds\n const filteredSelectedOptionValueIds = Object.entries(\n selectedOptionValueIds\n )\n .filter(([typeId]) => parseInt(typeId) !== optionType.id)\n .map(([, valueId]) => valueId)\n\n // If other option values are selected, filter variants based on compatibility\n if (filteredSelectedOptionValueIds.length > 0) {\n const selectableValueIds = uniq(\n productData.variants\n .filter(\n (variant) =>\n difference(filteredSelectedOptionValueIds, variant.optionValueIds)\n .length === 0\n )\n .flatMap((variant) => variant.optionValueIds)\n )\n\n // Select only the option values for which there is an available variant\n values = values.filter((value) => selectableValueIds.includes(value.id))\n }\n\n return values\n }, [optionType, selectedOptionValueIds, productData])\n\n const optionValueIds = useMemo(\n () => optionValues.map((value) => value.id),\n [optionValues]\n )\n\n const additionalData = useMemo(() => {\n if (!showAvailableVariants) {\n return {}\n }\n\n // We get all the selected option values EXCEPT\n // the one in the current step (if the user has\n // selected any). This is because\n // we want to show the available variants for all\n // the options in the step\n const valueIds = Object.values(selectedOptionValueIds).filter(\n (value) => !optionValueIds.includes(Number(value))\n )\n\n return productData.variants.reduce<\n Record<\n number,\n {\n available: number\n minPrice: string | null\n minAmount: number | null\n }\n >\n >((result, variant) => {\n // Don't include the variants that don't have\n // the currently selected option values\n if (\n !valueIds.every((value) =>\n variant.optionValueIds.includes(Number(value))\n )\n ) {\n return result\n }\n\n if (variant.inStock) {\n variant.optionValueIds\n .filter((valueId) => optionValueIds.includes(valueId))\n .forEach((valueId) => {\n if (!result[valueId]) {\n result[valueId] = {\n available: 0,\n minPrice: null,\n minAmount: null\n }\n }\n\n result[valueId].available += 1\n\n const amount = variant.rawDiscountedAmount || variant.rawAmount\n if (\n !result[valueId].minAmount ||\n amount < result[valueId].minAmount!\n ) {\n result[valueId].minAmount = amount\n result[valueId].minPrice =\n variant.discountedPrice || variant.price\n }\n })\n }\n\n return result\n }, {})\n }, [productData, optionValueIds, showAvailableVariants])\n\n const optionTypeKey = optionType.name.trim().toLowerCase().replace(/\\s/g, \"_\")\n const optionTypeTranslations = (\n translations[\"components.product.dialog.option_types\"] as unknown as Record<\n string,\n any\n >\n )?.[optionTypeKey] as Record\n\n const defaultTitle = optionTypeTranslation(\n translations[\"components.products.please_select\"],\n optionType.presentation\n )\n const titleText = customTitle\n ? optionTypeTranslations?.title || defaultTitle\n : defaultTitle\n const subtitleText = optionTypeTranslations?.subtitle\n\n return (\n <>\n
\n

{titleText}

\n {subtitleText && customTitle && (\n \n {subtitleText}\n \n )}\n
\n
\n {optionValues.map((optionValue) => (\n \n ))}\n
\n \n )\n}\n", "import { useTranslation } from \"@warp/context/TranslationContext\"\nimport { useVariantSelectorWizard } from \"@warp/context/VariantSelectorWizardContext\"\nimport { OptionType, OptionValue } from \"@warp/types/product\"\nimport { getLastSrcsetUrl } from \"@warp/utilities/images\"\nimport cn from \"classnames\"\nimport React, { useMemo } from \"react\"\n\nexport interface OptionValueProps {\n optionValue: OptionValue\n optionType: OptionType\n additionalData?: OptionValue\n availableVariants?: number | null\n minPrice?: string | null\n}\n\nconst OptionValue = ({\n optionValue,\n optionType,\n availableVariants,\n minPrice\n}: OptionValueProps) => {\n const {\n selectOptionValue,\n selectedOptionValueIds,\n removeOptionValue,\n productData,\n productType\n } = useVariantSelectorWizard()\n const t = useTranslation()\n\n // Check if the option value is currently selected for this option type\n const checked = useMemo(\n () => selectedOptionValueIds[optionType.id] === optionValue.id,\n [selectedOptionValueIds, optionType.id, optionValue.id]\n )\n\n // Determine if the option value should be disabled\n const isDisabled = useMemo(() => {\n const otherSelectedOptionValues = Object.entries(selectedOptionValueIds)\n .filter(([typeId]) => parseInt(typeId) !== optionType.id)\n .map(([, valueId]) => valueId!)\n\n const validVariants = productData.variants.filter((variant) =>\n otherSelectedOptionValues.every((valueId) =>\n variant.optionValueIds.includes(valueId)\n )\n )\n\n const validOptionValueIds = [\n ...new Set(validVariants.flatMap((variant) => variant.optionValueIds))\n ]\n\n return !validOptionValueIds.includes(optionValue.id)\n }, [productData, optionType.id, selectedOptionValueIds, optionValue.id])\n\n // Handle selection or deselection of the option value\n function handleChange() {\n if (checked) {\n removeOptionValue(optionType.id) // Remove selection for the current option type\n } else {\n selectOptionValue(optionType.id, optionValue.id) // Select this option value\n }\n }\n\n let availableVariantsText = null\n if (availableVariants) {\n const key = (\n t[\"components.product.dialog.available_variants\"] as unknown as Record<\n string,\n any\n >\n )?.[productType]\n if (key) {\n availableVariantsText = availableVariants > 1 ? key[\"other\"] : key[\"one\"]\n availableVariantsText = availableVariantsText.replace(\n \"%{count}\",\n availableVariants\n )\n }\n } else if (availableVariants === 0) {\n availableVariantsText = (\n t[\"components.product.dialog.no_variants_available\"] as unknown as Record<\n string,\n any\n >\n )?.[productType]\n }\n\n return (\n \n
\n
\n
\n
\n
\n
\n )\n}\n\nexport default OptionValue\n", "import { useVariantSelectorWizard } from \"@warp/context/VariantSelectorWizardContext\"\nimport React from \"react\"\n\nexport default function SelectedValuesHeader() {\n const { productData, selectedOptionValueIds } = useVariantSelectorWizard()\n\n const optionValues = React.useMemo(\n () =>\n productData.optionValues.reduce>(\n (result, value) => {\n result[value.id] = value.presentation\n return result\n },\n {}\n ),\n [productData.optionValues]\n )\n\n const optionTypes = React.useMemo(\n () =>\n productData.optionTypes.reduce<\n Record\n >((result, type) => {\n result[type.id] = {\n presentation: type.presentation,\n position: type.position\n }\n return result\n }, {}),\n [productData.optionTypes]\n )\n\n const selectedOptions = React.useMemo(\n () =>\n Object.entries(selectedOptionValueIds)\n .filter(([_, value]) => value !== undefined)\n .map(([optionTypeId, optionValueId]) => ({\n optionType: optionTypes[parseInt(optionTypeId)].presentation,\n optionValue: optionValues[optionValueId!],\n position: optionTypes[parseInt(optionTypeId)].position\n }))\n .sort((a, b) => a.position - b.position),\n [optionTypes, optionValues, selectedOptionValueIds]\n )\n\n return (\n
\n {selectedOptions.map(({ optionType, optionValue }) => (\n
\n {optionType}: {optionValue}\n
\n ))}\n
\n )\n}\n", "import * as React from \"react\"\nimport { Image, ProductData, ProductType } from \"@warp/types/product\"\nimport Dialog from \"./Dialog\"\nimport { KitSelectorProvider } from \"./context/KitSelectorContext\"\nimport { KitSelectorDownloadFormProvider } from \"./context/KitSelectorDownloadFormContext\"\n\nexport interface VariantSelectorProps {\n productType: ProductType\n productName: string\n productData: ProductData\n currentUserEmail?: string\n slides: Image[]\n}\n\nexport default ({\n productName,\n productData,\n productType,\n currentUserEmail,\n slides\n}: VariantSelectorProps) => {\n return (\n \n \n \n \n \n )\n}\n", "import React from \"react\"\nimport { Image } from \"@warp/types/product\"\nimport { useTranslation } from \"@warp/context/TranslationContext\"\nimport Stepper from \"@warp/components/Stepper\"\nimport WizardDialog, { WizardDialogProps } from \"@warp/components/WizardDialog\"\nimport { Step } from \"@warp/types/stepper\"\nimport PatternStep from \"./Steps/Pattern\"\nimport Footer from \"./Footer\"\nimport Slideshow from \"../Slideshow\"\nimport { useKitSelector } from \"./context/KitSelectorContext\"\nimport FixedColoursStep from \"./Steps/FixedColours\"\nimport DownloadStep from \"./Steps/Download\"\nimport DownloadFooter from \"./Footer/Download\"\n\ninterface KitDialogProps {\n title: string\n slides: Image[]\n}\n\nexport default function KitDialog({ slides }: KitDialogProps) {\n const t = useTranslation()\n const { setPdfOnly, pdfOnly, productType } = useKitSelector()\n\n const dialogProps: WizardDialogProps[\"dialogProps\"] = {\n modalName: \"kit\",\n description: t[\"components.products.kit_selector_dialog_description\"],\n contentClassName: \"kit-dialog\",\n // TODO: The hardcoded slides are temporary until we\n // have a specific image for the kit products.\n sidebar: (\n \n ),\n onOpenCallback(event) {\n if (event && event instanceof CustomEvent && event.detail.initialOption) {\n setPdfOnly(event.detail.initialOption === \"pdfOnly\")\n }\n }\n }\n\n const steps: Step[] = [\n {\n id: \"pattern-step\",\n label: t[\"components.products.kit_builder.pattern\"],\n component: \n }\n ]\n\n if (pdfOnly) {\n steps.push({\n id: \"download\",\n label: t[\"components.products.kit_builder.download\"],\n component: \n })\n } else if (productType === \"fixed_kit\") {\n steps.push({\n id: \"fixed-colours-step\",\n label: t[\"components.products.kit_builder.threads\"],\n component: \n })\n }\n\n return (\n \n :