{"product_id":"3d-designer-motocross-graphics-kit","title":"3D Designer Motocross Graphics kit","description":"\u003cscript\u003e(async () =\u003e {\n  const urlParams = new URLSearchParams(window.location.search);\n  let token = urlParams.get(\"token\");\n  const variantId = urlParams.get(\"variant\");\n  if (!token \u0026\u0026 variantId) {\n    try {\n      const cart = await (await fetch(\"\/cart.js\")).json();\n      const match = (cart.items || []).find((item) =\u003e {\n        const t = item.properties?.Token || item.properties?.[\"Token\"];\n        return String(item.variant_id) === String(variantId) \u0026\u0026 t;\n      });\n      if (match) {\n        token = match.properties.Token || match.properties[\"Token\"];\n        const next = new URL(window.location.href);\n        next.searchParams.set(\"token\", token);\n        window.location.replace(next.pathname + next.search + next.hash);\n        return;\n      }\n    } catch (e) {\n      console.warn(\"[Garage360] cart token lookup failed\", e);\n    }\n  }\n  if (!token) return;\n\n  const apiUrl = \"https:\/\/getproduct-nttowmih7a-uc.a.run.app\/Te9hN91oprUoY4nhY4IL\/\" + token;\n  const res = await fetch(apiUrl);\n  const data = await res.json();\n  if (!data || data.error) return;\n\n  const form = document.querySelector('form[action*=\"\/cart\/add\"]');\n  if (!form) return;\n\n  const ensureProp = (name, value) =\u003e {\n    let input = form.querySelector('input[name=\"properties[' + name + ']\"]');\n    if (!input) {\n      input = document.createElement(\"input\");\n      input.type = \"hidden\";\n      input.name = \"properties[\" + name + \"]\";\n      form.appendChild(input);\n    }\n    input.value = value || \"\";\n  };\n\n  ensureProp(\"Rider Name\", data.rider_name || \"\");\n  ensureProp(\"Product Summary\", data.product_summary || \"\");\n  ensureProp(\"Token\", token);\n  if (data.preview_url) ensureProp(\"_preview_url\", data.preview_url);\n\n  const applyPreview = (url) =\u003e {\n    if (!url) {\n      console.warn(\"[Garage360] applyPreview: no url\");\n      return;\n    }\n\n    const form = document.querySelector('form[action*=\"\/cart\/add\"]');\n    const roots = [\n      form?.closest(\n        \"product-info, .product, .product-single, .productView, main, body\",\n      ),\n      document.querySelector(\n        \"media-gallery, .product__media, .product-gallery, .productView-img-container, .product-single__media\",\n      ),\n    ].filter(Boolean);\n\n    const imgs = new Set();\n    roots.forEach((root) =\u003e {\n      root.querySelectorAll(\"img\").forEach((img) =\u003e {\n        if ((img.naturalWidth || img.width || 0) \u0026\u0026 img.width \u003e 0 \u0026\u0026 img.width \u003c 40)\n          return;\n        if (img.closest(\"header, footer, nav, .cart, .thumbnail, .thumbs\"))\n          return;\n        imgs.add(img);\n      });\n    });\n\n    document\n      .querySelectorAll(\n        \"img[data-main-image], img[id^='product-featured-image'], .product-single__media img, .productView-img-container img, media-gallery img, .product__media img, .product-media img, .product__photo img, .product-gallery img\",\n      )\n      .forEach((img) =\u003e imgs.add(img));\n\n    if (!imgs.size) {\n      console.warn(\"[Garage360] no product images matched\");\n      return;\n    }\n\n    imgs.forEach((img) =\u003e {\n      img.src = url;\n      img.removeAttribute(\"srcset\");\n      img.removeAttribute(\"data-src\");\n      img.removeAttribute(\"data-srcset\");\n      if (img.dataset) {\n        delete img.dataset.src;\n        delete img.dataset.srcset;\n      }\n    });\n  };\n\n  if (data.preview_url) {\n    console.log(\"[Garage360] preview_url\", data.preview_url);\n    applyPreview(data.preview_url);\n    setInterval(() =\u003e applyPreview(data.preview_url), 500);\n  }\n\n  const shopAttr = data.shop_attr || {};\n  console.log(\"[Garage360] token\", token);\n  console.log(\"[Garage360] shop_attr\", shopAttr);\n\n  const optionSlugs = [\n    [\"print-media\", shopAttr[\"print-media\"]],\n    [\"laminate\", shopAttr[\"laminate\"]],\n    [\"swing-arms\", shopAttr[\"swing-arms\"]],\n  ];\n  const attrBySlug = {};\n  optionSlugs.forEach(([slug, value]) =\u003e {\n    if (value) attrBySlug[slug] = String(value).trim();\n  });\n\n  const slugify = (s) =\u003e\n    String(s || \"\")\n      .trim()\n      .toLowerCase()\n      .replace(\/:\/g, \"\")\n      .replace(\/\\s+\/g, \"-\");\n\n  const getPicker = () =\u003e\n    document.querySelector(\"variant-picker\") ||\n    document.querySelector(\"variant-radios\") ||\n    document.querySelector('form[action*=\"\/cart\/add\"]');\n\n  if (!document.querySelector(\"variant-picker\") \u0026\u0026 !document.querySelector(\"variant-radios\")) {\n    for (let i = 0; i \u003c 20 \u0026\u0026 !document.querySelector(\"variant-radios, variant-picker\"); i++) {\n      await new Promise((r) =\u003e setTimeout(r, 100));\n    }\n  }\n\n  const optionValue = (el) =\u003e\n    String(el.getAttribute(\"data-option-value\") || el.value || \"\")\n      .trim()\n      .toUpperCase();\n\n  const isPicked = (el) =\u003e\n    !!(el.checked || el.getAttribute(\"aria-checked\") === \"true\" ||\n      el.closest('[aria-checked=\"true\"]'));\n\n  const findFieldset = (picker, slug) =\u003e {\n    if (!picker || !slug) return null;\n    let fieldset = picker.querySelector('[data-option-slug=\"' + slug + '\"]');\n    if (fieldset) return fieldset;\n\n    const needle = slug.replace(\/-\/g, \" \").toUpperCase();\n    const fieldsets = [...picker.querySelectorAll(\"fieldset, .product-form__input\")];\n    return (\n      fieldsets.find((fs) =\u003e {\n        const name = (\n          fs.querySelector('input[type=\"radio\"]')?.getAttribute(\"name\") ||\n          fs.querySelector(\"legend\")?.textContent ||\n          \"\"\n        )\n          .replace(\/:\/g, \"\")\n          .trim()\n          .toUpperCase();\n        return name === needle || name.includes(needle);\n      }) || null\n    );\n  };\n\n  const findMatch = (fieldset, value) =\u003e {\n    const target = String(value || \"\").trim().toUpperCase();\n    if (!fieldset || !target) return null;\n    const radios = [...fieldset.querySelectorAll('input[type=\"radio\"]')];\n    return radios.find((r) =\u003e optionValue(r) === target) || null;\n  };\n\n  const waitForVariantUiSettle = (timeoutMs = 500) =\u003e\n    new Promise((resolve) =\u003e {\n      const root =\n        document.querySelector(\"product-info\") ||\n        document.querySelector(\"variant-picker\")?.closest(\"product-info, .product, main\") ||\n        document.querySelector(\"variant-picker\") ||\n        document.querySelector(\"variant-radios\") ||\n        document.body;\n\n      let quietTimer = null;\n      let done = false;\n      const finish = () =\u003e {\n        if (done) return;\n        done = true;\n        observer.disconnect();\n        clearTimeout(quietTimer);\n        clearTimeout(hardStop);\n        resolve();\n      };\n\n      const bump = () =\u003e {\n        clearTimeout(quietTimer);\n        quietTimer = setTimeout(finish, 400);\n      };\n\n      const observer = new MutationObserver(bump);\n      observer.observe(root, {\n        childList: true,\n        subtree: true,\n        attributes: true,\n      });\n      const hardStop = setTimeout(finish, timeoutMs);\n      bump();\n    });\n\n  const selectIfNeeded = (slug, value) =\u003e {\n    const picker = getPicker();\n    if (!picker || !value) return false;\n    const fieldset = findFieldset(picker, slug);\n    if (!fieldset) {\n      console.warn(\"[Garage360] no fieldset\", slug);\n      return false;\n    }\n    const match = findMatch(fieldset, value);\n    if (!match) {\n      console.warn(\n        \"[Garage360] no option match\",\n        slug,\n        value,\n        [...fieldset.querySelectorAll('input[type=\"radio\"]')].map((r) =\u003e r.value),\n      );\n      return false;\n    }\n    if (isPicked(match)) {\n      console.log(\"[Garage360] already selected\", slug, String(value).trim().toUpperCase());\n      return false;\n    }\n    const label = fieldset.querySelector('label[for=\"' + match.id + '\"]');\n    (label || match).click();\n    console.log(\"[Garage360] clicked\", slug, String(value).trim().toUpperCase());\n    return true;\n  };\n\n  const lockOption = (slug, value) =\u003e {\n    const picker = getPicker();\n    if (!picker || !value) return false;\n    const fieldset = findFieldset(picker, slug);\n    if (!fieldset) return false;\n    const target = String(value).trim().toUpperCase();\n    const radios = [...fieldset.querySelectorAll('input[type=\"radio\"]')];\n    const match = radios.find((r) =\u003e optionValue(r) === target);\n    if (!match) return false;\n\n    radios.forEach((r) =\u003e {\n      const wrap = r.closest(\"li\") || r.closest(\"label\") || r;\n      const selected = r === match;\n      r.disabled = !selected;\n      if (wrap \u0026\u0026 wrap !== r) {\n        wrap.style.pointerEvents = selected ? \"\" : \"none\";\n        wrap.style.opacity = selected ? \"1\" : \"0.5\";\n      }\n    });\n    console.log(\"[Garage360] locked\", slug, target);\n    return true;\n  };\n\n  const lockAll = () =\u003e {\n    optionSlugs.forEach(([slug, value]) =\u003e {\n      if (value) lockOption(slug, value);\n    });\n  };\n\n  const setFormVariantId = (variantId) =\u003e {\n    const id = String(variantId);\n    document\n      .querySelectorAll('form[action*=\"\/cart\/add\"] [name=\"id\"], input[name=\"id\"]')\n      .forEach((input) =\u003e {\n        input.value = id;\n      });\n  };\n\n  const fetchProductJson = async () =\u003e {\n    try {\n      const handle = window.location.pathname\n        .split(\"\/products\/\")[1]\n        ?.split(\"\/\")[0]\n        ?.split(\"?\")[0];\n      if (!handle) return null;\n      const r = await fetch(\"\/products\/\" + encodeURIComponent(handle) + \".js\");\n      if (!r.ok) return null;\n      return await r.json();\n    } catch (e) {\n      console.warn(\"[Garage360] product.js failed\", e);\n      return null;\n    }\n  };\n\n  const findVariant = (product) =\u003e {\n    if (!product || !product.variants || !product.variants.length) return null;\n    const optionNames = (product.options || []).map((o) =\u003e\n      typeof o === \"string\" ? o : o.name,\n    );\n    return (\n      product.variants.find((v) =\u003e\n        optionNames.every((name, i) =\u003e {\n          const wanted = attrBySlug[slugify(name)];\n          if (!wanted) return true;\n          const actual = String(\n            (v.options \u0026\u0026 v.options[i]) || v[\"option\" + (i + 1)] || \"\",\n          ).trim();\n          return actual.toUpperCase() === wanted.toUpperCase();\n        }),\n      ) || null\n    );\n  };\n\n  const product = await fetchProductJson();\n  const matched = findVariant(product);\n  if (matched) {\n    console.log(\"[Garage360] resolved variant\", matched.id, matched.title);\n    setFormVariantId(matched.id);\n    const currentVariant = urlParams.get(\"variant\");\n    if (String(currentVariant || \"\") !== String(matched.id)) {\n      const next = new URL(window.location.href);\n      next.searchParams.set(\"token\", token);\n      next.searchParams.set(\"variant\", String(matched.id));\n      window.location.replace(next.pathname + next.search + next.hash);\n      return;\n    }\n  } else {\n    console.warn(\"[Garage360] no matching variant\", attrBySlug);\n    let didClick = false;\n    for (const [slug, value] of optionSlugs) {\n      if (!value) continue;\n      if (selectIfNeeded(slug, value)) {\n        didClick = true;\n        await waitForVariantUiSettle();\n      }\n    }\n    if (didClick) await waitForVariantUiSettle();\n  }\n\n  lockAll();\n  setTimeout(lockAll, 400);\n})();\u003c\/script\u003e","brand":"Factory Backing","offers":[{"title":"WHITE \/ GLOSS","offer_id":43039552766048,"sku":null,"price":232.0,"currency_code":"CAD","in_stock":true},{"title":"WHITE \/ MATTE","offer_id":43039552798816,"sku":null,"price":261.0,"currency_code":"CAD","in_stock":true},{"title":"WHITE \/ GALACTIC","offer_id":43039552831584,"sku":null,"price":319.0,"currency_code":"CAD","in_stock":true},{"title":"SILVER CHROME \/ GLOSS","offer_id":43039552864352,"sku":null,"price":362.0,"currency_code":"CAD","in_stock":true},{"title":"SILVER CHROME \/ MATTE","offer_id":43039552897120,"sku":null,"price":376.0,"currency_code":"CAD","in_stock":true},{"title":"SILVER CHROME \/ GALACTIC","offer_id":43039552929888,"sku":null,"price":434.0,"currency_code":"CAD","in_stock":true},{"title":"HOLOGRAPHIC CHROME \/ GLOSS","offer_id":43039552962656,"sku":null,"price":362.0,"currency_code":"CAD","in_stock":true},{"title":"HOLOGRAPHIC CHROME \/ MATTE","offer_id":43039552995424,"sku":null,"price":376.0,"currency_code":"CAD","in_stock":true},{"title":"HOLOGRAPHIC CHROME \/ GALACTIC","offer_id":43039553028192,"sku":null,"price":434.0,"currency_code":"CAD","in_stock":true}],"thumbnail_url":"\/\/cdn.shopify.com\/s\/files\/1\/0573\/8165\/0528\/files\/1_1c99aa19-e1fb-4965-a18c-6fc91c1adc7e.webp?v=1787788648","url":"https:\/\/www.factorybacking.com\/products\/3d-designer-motocross-graphics-kit","provider":"Factory Backing","version":"1.0","type":"link"}