1(function(){"use strict";var __webpack_modules__={
1"./src/snippets/braze/inbox-history.js":function(__unused_webpack___webpack_module__,__webpack_exports__,__webpack_require__){eval("{__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ HISTORY_ITEMS_PER_PAGE: () => (/* binding */ HISTORY_ITEMS_PER_PAGE),\n/* harmony export */ HISTORY_SELECTOR: () => (/* binding */ HISTORY_SELECTOR),\n/* harmony export */ initHistoryFilterListeners: () => (/* binding */ initHistoryFilterListeners),\n/* harmony export */ paginate: () => (/* binding */ paginate),\n/* harmony export */ renderHistory: () => (/* binding */ renderHistory)\n/* harmony export */ });\n/* harmony import */ var _loyalty_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./loyalty.js */ \"./src/snippets/braze/loyalty.js\");\n\n\nconst HISTORY_SELECTOR = '[data-history]';\n\n/**\n * Rows per page, shared by the rewards, purchase and activity grids.\n *\n * Kept low so the pager actually appears: it only renders above one page, and\n * a typical account has well under a dozen rows, so a larger page size hid it\n * from most shoppers. The purchase grid sets the ceiling \u2014 each of its rows\n * carries an expandable line-item panel, so a page there is twice the height\n * the count suggests.\n */\nconst HISTORY_ITEMS_PER_PAGE = 5;\n\n/** @type {{ rewards: Array, purchases: Array, activities: Array, locale: string }} */\nlet historyDataCache = { rewards: [], purchases: [], activities: [], locale: 'en' };\n\n/**\n * @param {HTMLElement|null|undefined} el\n * @param {boolean} isHidden\n */\nfunction setHidden(el, isHidden) {\n if (!el) return;\n el.hidden = isHidden;\n el.classList.toggle('hidden', isHidden);\n}\n\n/**\n * @param {HTMLElement} container\n * @returns {{ orderNumber: string, totalPoints: string }}\n */\nfunction historyLabels(container) {\n return {\n orderNumber: container?.dataset?.i18nOrderNumber || 'Order Number',\n totalPoints: container?.dataset?.i18nTotalPoints || 'Total Points',\n statusFulfilled: container?.dataset?.i18nStatusFulfilled || '',\n statusPending: container?.dataset?.i18nStatusPending || '',\n };\n}\n\n/**\n * @param {string} label\n * @param {boolean} disabled\n * @param {() => void} onClick\n * @returns {HTMLButtonElement}\n */\nfunction createPageNavButton(label, disabled, onClick) {\n const button = document.createElement('button');\n button.type = 'button';\n button.className = 'braze-inbox__history-page-button';\n button.textContent = label;\n button.disabled = disabled;\n if (disabled) button.classList.add('braze-inbox__history-page-button--disabled');\n button.addEventListener('click', onClick);\n return button;\n}\n\n/**\n * @param {number} currentPage\n * @param {number} totalPages\n * @returns {[number, number]}\n */\nfunction visiblePageRange(currentPage, totalPages) {\n const maxVisiblePages = 5;\n let startPage = Math.max(1, currentPage - Math.floor(maxVisiblePages / 2));\n let endPage = Math.min(totalPages, startPage + maxVisiblePages - 1);\n if (endPage - startPage < maxVisiblePages - 1) {\n startPage = Math.max(1, endPage - maxVisiblePages + 1);\n }\n return [startPage, endPage];\n}\n\n/**\n * @param {number} totalItems\n * @param {number} currentPage\n * @param {number} itemsPerPage\n * @param {Function} onPageChange\n * @returns {DocumentFragment}\n */\nfunction createPagination(totalItems, currentPage, itemsPerPage, onPageChange) {\n const fragment = document.createDocumentFragment();\n const totalPages = Math.ceil(totalItems / itemsPerPage);\n\n const prev = createPageNavButton('<', currentPage === 1, () => {\n if (currentPage > 1) onPageChange(currentPage - 1);\n });\n prev.setAttribute('aria-label', 'Previous page');\n fragment.appendChild(prev);\n\n const [startPage, endPage] = visiblePageRange(currentPage, totalPages);\n for (let i = startPage; i <= endPage; i++) {\n const pageButton = createPageNavButton(String(i), false, () => onPageChange(i));\n pageButton.setAttribute('aria-label', `Page ${i}`);\n pageButton.setAttribute('aria-current', i === currentPage ? 'page' : 'false');\n if (i === currentPage) pageButton.classList.add('braze-inbox__history-page-button--active');\n fragment.appendChild(pageButton);\n }\n\n const next = createPageNavButton('>', currentPage === totalPages, () => {\n if (currentPage < totalPages) onPageChange(currentPage + 1);\n });\n next.setAttribute('aria-label', 'Next page');\n fragment.appendChild(next);\n\n return fragment;\n}\n\n/**\n * @param {HTMLElement} section\n * @param {boolean} isEmpty\n */\nfunction setSectionEmpty(section, isEmpty) {\n const loadingEl = section.querySelector('[data-history-state=\"loading\"]');\n const loadedEl = section.querySelector('[data-history-state=\"loaded\"]');\n const emptyEl = section.querySelector('[data-history-state=\"empty\"]');\n section.setAttribute('data-state', isEmpty ? 'empty' : 'loaded');\n setHidden(loadingEl, true);\n setHidden(loadedEl, isEmpty);\n setHidden(emptyEl, !isEmpty);\n}\n\n/**\n * @param {HTMLElement|null} paginationEl\n * @param {number} totalItems\n * @param {number} safePage\n * @param {(page: number) => void} onPageChange\n */\nfunction renderPagination(paginationEl, totalItems, safePage, onPageChange) {\n if (!paginationEl) return;\n paginationEl.replaceChildren();\n const totalPages = Math.max(1, Math.ceil(totalItems / HISTORY_ITEMS_PER_PAGE));\n if (totalPages > 1) {\n paginationEl.appendChild(\n createPagination(totalItems, safePage, HISTORY_ITEMS_PER_PAGE, onPageChange),\n );\n }\n}\n\n/**\n * Resolves a requested page against a row set.\n *\n * Shared by all three history grids, so a page number out of range clamps\n * rather than rendering an empty grid.\n *\n * @param {Array} items\n * @param {number} page\n * @returns {{ safePage: number, pageItems: Array }}\n */\nfunction paginate(items, page) {\n const totalPages = Math.max(1, Math.ceil(items.length / HISTORY_ITEMS_PER_PAGE));\n const safePage = Math.min(Math.max(page, 1), totalPages);\n const pageItems = items.slice(\n (safePage - 1) * HISTORY_ITEMS_PER_PAGE,\n safePage * HISTORY_ITEMS_PER_PAGE,\n );\n return { safePage, pageItems };\n}\n\n/**\n * @param {object} reward\n * @returns {object}\n */\nfunction normalizeReward(reward) {\n return {\n ...reward,\n date: (0,_loyalty_js__WEBPACK_IMPORTED_MODULE_0__.pickHistoryDate)(reward, [\n 'date',\n 'dateAwarded',\n 'awardedDate',\n 'createdAt',\n 'timestamp',\n 'redemptionDate',\n 'issuedDate',\n ]),\n title: reward.title || reward.name || reward.rewardName || reward.rewardTitle || '',\n status: reward.status || reward.rewardStatus || reward.state || '',\n points: reward.points ?? reward.pointsSpent ?? reward.pointAmount ?? reward.numPoints,\n };\n}\n\n/**\n * @param {Object} purchase\n * @returns {Array}\n */\nfunction getPurchaseLineItems(purchase) {\n if (Array.isArray(purchase.items) && purchase.items.length) return purchase.items;\n if (Array.isArray(purchase.skus) && purchase.skus.length) return purchase.skus;\n if (Array.isArray(purchase.lineItems) && purchase.lineItems.length) return purchase.lineItems;\n if (Array.isArray(purchase.line_items) && purchase.line_items.length) return purchase.line_items;\n return [];\n}\n\n/**\n * @param {object} purchase\n * @returns {object}\n */\nfunction normalizePurchase(purchase) {\n return {\n ...purchase,\n date: (0,_loyalty_js__WEBPACK_IMPORTED_MODULE_0__.pickHistoryDate)(purchase, [\n 'date',\n 'purchaseDate',\n 'orderDate',\n 'createdAt',\n 'timestamp',\n 'transactionDate',\n ]),\n orderNumber: (0,_loyalty_js__WEBPACK_IMPORTED_MODULE_0__.getPurchaseOrderNumber)(purchase),\n total: purchase.total ?? purchase.amount ?? purchase.orderTotal ?? purchase.totalAmount,\n pointsAwarded:\n purchase.pointsAwarded ?? purchase.totalPoints ?? purchase.points ?? purchase.numPoints,\n pointsNote: purchase.pointsNote || purchase.pointsStatus || purchase.refundNote || '',\n items: getPurchaseLineItems(purchase),\n };\n}\n\n/**\n * @param {object} activity\n * @returns {object}\n */\nfunction normalizeActivity(activity) {\n return {\n ...activity,\n date: (0,_loyalty_js__WEBPACK_IMPORTED_MODULE_0__.pickHistoryDate)(activity, [\n 'date',\n 'completedDate',\n 'activityDate',\n 'createdAt',\n 'timestamp',\n ]),\n title: activity.activityName || activity.title || activity.name || activity.description || '',\n points: activity.points ?? activity.numPoints ?? activity.pointsAwarded ?? activity.pointValue,\n };\n}\n\n/**\n * @param {object} reward\n * @param {{ statusFulfilled: string, statusPending: string }} labels\n * @returns {HTMLElement}
1\n */\nfunction createRewardRow(reward, labels) {\n const row = document.createElement('div');\n row.className = 'braze-inbox__history-row braze-inbox__history-row--rewards';\n\n const dateCell = document.createElement('div');\n dateCell.className = 'braze-inbox__history-cell';\n dateCell.textContent = (0,_loyalty_js__WEBPACK_IMPORTED_MODULE_0__.formatHistoryDateUtc)(reward.date);\n\n const rewardCell = document.createElement('div');\n rewardCell.className = 'braze-inbox__history-cell';\n rewardCell.textContent = reward.title || '';\n\n const statusCell = document.createElement('div');\n statusCell.className = 'braze-inbox__history-cell';\n statusCell.textContent = (0,_loyalty_js__WEBPACK_IMPORTED_MODULE_0__.formatRewardStatus)(reward.status, labels);\n\n const pointsCell = document.createElement('div');\n pointsCell.className = 'braze-inbox__history-cell braze-inbox__history-cell--align-end';\n if (reward.points == null || reward.points === '') {\n pointsCell.textContent = '';\n } else {\n const points = Number(reward.points);\n pointsCell.textContent = Number.isFinite(points) ? String(points) : String(reward.points);\n }\n\n row.append(dateCell, rewardCell, statusCell, pointsCell);\n return row;\n}\n\n/**\n * @param {string} className\n * @param {string} text\n * @returns {HTMLElement}\n */\nfunction historyCell(className, text) {\n const cell = document.createElement('div');\n cell.className = className;\n cell.textContent = text;\n return cell;\n}\n\n/**\n * @param {object} purchase\n * @param {{ orderNumber: string, totalPoints: string }} labels\n * @param {string} locale\n * @returns {{ row: HTMLElement, expandWrap: HTMLElement|null }}\n */\nfunction createPurchaseRow(purchase, labels, locale) {\n const lineItems = purchase.items || [];\n const expandable = lineItems.length > 0;\n const orderLabel = purchase.orderNumber\n ? `${labels.orderNumber} ${purchase.orderNumber}`\n : labels.orderNumber;\n\n const row = document.createElement(expandable ? 'button' : 'div');\n if (expandable) {\n row.type = 'button';\n row.setAttribute('data-trigger', 'purchase-expand');\n row.setAttribute('aria-expanded', 'false');\n }\n row.className = 'braze-inbox__history-row braze-inbox__history-row--purchases';\n\n const total = (0,_loyalty_js__WEBPACK_IMPORTED_MODULE_0__.formatLoyaltyCurrency)(purchase.total, locale);\n const detailsCell = historyCell(\n 'braze-inbox__history-cell',\n expandable ? `\u25BE ${orderLabel}` : orderLabel,\n );\n row.append(\n historyCell('braze-inbox__history-cell', (0,_loyalty_js__WEBPACK_IMPORTED_MODULE_0__.formatHistoryDateUtc)(purchase.date)),\n detailsCell,\n historyCell('braze-inbox__history-cell', total || '-'),\n historyCell(\n 'braze-inbox__history-cell braze-inbox__history-cell--align-end',\n formatPurchasePoints(purchase, labels.totalPoints),\n ),\n );\n\n if (!expandable) return { row, expandWrap: null };
1\n\n const expandWrap = buildPurchaseExpandPanel(lineItems, locale);\n row.addEventListener('click', () => {\n const open = row.getAttribute('aria-expanded') === 'true';\n row.setAttribute('aria-expanded', open ? 'false' : 'true');\n expandWrap.hidden = open;\n detailsCell.textContent = `${open ? '\u25BE' : '\u25B4'} ${orderLabel}`;\n });\n return { row, expandWrap };\n}\n\n/**\n * @param {object} purchase\n * @param {string} totalPointsLabel\n * @returns {string}\n */\nfunction formatPurchasePoints(purchase, totalPointsLabel) {\n const pts = purchase.pointsAwarded;\n if (pts == null || pts === '') return '';\n const numeric = Number(pts);\n const signed = Number.isFinite(numeric)\n ? numeric >= 0\n ? `+${numeric}`\n : String(numeric)\n : String(pts);\n const note = purchase.pointsNote ? ` (${purchase.pointsNote})` : '';\n const alreadyNoted = /partial refund/i.test(String(pts));\n return alreadyNoted ? `${totalPointsLabel} ${pts}` : `${totalPointsLabel} ${signed}${note}`;\n}\n\n/**\n * @param {Array} lineItems\n * @param {string} locale\n * @returns {HTMLElement}\n */\nfunction buildPurchaseExpandPanel(lineItems, locale) {\n const expandWra
1p = document.createElement('div');\n expandWrap.className = 'braze-inbox__history-expand';\n expandWrap.hidden = true;\n expandWrap.setAttribute('data-purchase-expand-panel', 'true');\n\n lineItems.forEach((item) => {\n const skuRow = document.createElement('div');\n skuRow.className = 'braze-inbox__history-row braze-inbox__history-row--sku';\n\n // A line item has no date of its own, so the Date column stays empty and the\n // SKU sits under \"Purchase Details\" beside the order number it breaks down.\n const dateCell = document.createElement('div');\n dateCell.className = 'braze-inbox__history-cell';\n\n const detailsCell = document.createElement('div');\n detailsCell.className = 'braze-inbox__history-cell';\n detailsCell.textContent = (0,_loyalty_js__WEBPACK_IMPORTED_MODULE_0__.formatLineItemDetails)(item);\n\n const amountCell = document.createElement('div');\n amountCell.className = 'braze-inbox__history-cell';\n const amount = item.amount ?? item.price ?? item.total;\n amountCell.textContent = (0,_loyalty_js__WEBPACK_IMPORTED_MODULE_0__.formatLoyaltyCurrency)(amount, locale);\n\n // Fourth column is \"Pts awarded\" \u2014 blank until QA asked for per-item points\n // now that these rows expand. Aligned like the purchase row above it.\n const pointsCell = document.createElement('div');\n pointsCell.className = 'braze-inbox__history-cell braze-inbox__history-cell--align-end';\n pointsCell.textContent = (0,_loyalty_js__WEBPACK_IMPORTED_MODULE_0__.formatLineItemPoints)(item);\n\n skuRow.append(dateCell, detailsCell, amountCell, pointsCell);
1\n expandWrap.appendChild(skuRow);\n });\n\n return expandWrap;\n}\n\n/**\n * @param {object} activity\n * @returns {HTMLElement}\n */\nfunction createActivityRow(activity) {\n const row = document.createElement('div');\n row.className = 'braze-inbox__history-row braze-inbox__history-row--activities';\n\n const dateCell = document.createElement('div');\n dateCell.className = 'braze-inbox__history-cell';\n dateCell.textContent = (0,_loyalty_js__WEBPACK_IMPORTED_MODULE_0__.formatHistoryDateUtc)(activity.date);\n\n const activityCell = document.createElement('div');\n activityCell.className = 'braze-inbox__history-cell';\n activityCell.textContent = activity.title || '';\n\n const pointsCell = document.createElement('div');\n pointsCell.className = 'braze-inbox__history-cell braze-inbox__history-cell--align-end';\n const points = Number(activity.points) || 0;\n pointsCell.textContent = points >= 0 ? `+${points}` : String(points);\n\n row.append(dateCell, activityCell, pointsCell);\n return row;\n}\n\n/**\n * @param {HTMLElement} section\n * @param {Array} rewards\n * @param {string} locale\n * @param {{ statusFulfilled: string, statusPending: string }} labels\n * @param {number} page\n */\nfunction renderRewardsHistory(section, rewards, locale, labels, page = 1) {\n const bodyEl = section.querySelector('[data-history-body=\"rewards\"]');\n const paginationEl = section.querySelector('[data-history-pagination=\"rewards\"]');\n\n if (!rewards || rewards.length === 0) {\n setSectionEmpty(section, true);\n if (paginationEl) paginationEl.replaceChildren();\n return;\n }\n\n const { safePage, pageItems } = paginate(rewards, page);\n bodyEl.replaceChildren();\n pageItems.forEach((reward) => bodyEl.appendChild(createRewardRow(reward, labels)));\n renderPagination(paginationEl, rewards.length, safePage, (newPage) => {\n renderRewardsHistory(section, rewards, locale, labels, newPage);\n });\n setSectionEmpty(section, false);\n}\n\n/**\n * @param {HTMLElement} section\n * @param {Array} purchases\n * @param {string} locale\n * @param {HTMLElement} container\n * @param {number} page\n */\nfunction renderPurchaseHistory(section, purchases, locale, container, page = 1) {\n const bodyEl = section.querySelector('[data-history-body=\"purchases\"]');\n const paginationEl = section.querySelector('[data-history-pagination=\"purchases\"]');\n const labels = historyLabels(container);\n\n if (!purchases || purchases.length === 0) {\n setSectionEmpty(section, true);\n if (paginationEl) paginationEl.replaceChildren();\n return;\n }\n\n const { safePage, pageItems } = paginate(purchases, page);\n bodyEl.replaceChildren();\n pageItems.forEach((purchase) => {\n const { row, expandWrap } = createPurchaseRow(purchase, labels, locale);\n bodyEl.appendChild(row);\n if (expandWrap) bodyEl.appendChild(expandWrap);\n });\n renderPagination(paginationEl, purchases.length, safePage, (newPage) => {\n renderPurchaseHistory(section, purchases, locale, container, newPage);\n });\n setSectionEmpty(section, false);\n}\n\n/**\n * @param {HTMLElement} section\n * @param {Array} activities\n * @param {string} locale\n * @param {number} page\n */\nfunction renderActivityHistory(section, activities, locale, page = 1) {\n const bodyEl = section.querySelector('[data-history-body=\"activities\"]');\n const paginationEl = section.querySelector('[data-history-pagination=\"activities\"]');\n\n if (!activities || activities.length === 0) {\n setSectionEmpty(section, true);\n if (paginationEl) paginationEl.replaceChildren();\n return;\n }\n\n const { safePage, pageItems } = paginate(activities, page);\n bodyEl.replaceChildren();\n pageItems.forEach((activity) => bodyEl.appendChild(createActivityRow(activity)));\n renderPagination(paginationEl, activities.length, safePage, (newPage) => {\n renderActivityHistory(section, activities, locale, newPage);\n });\n setSectionEmpty(section, false);\n}\n\n/**\n * @param {HTMLElement} container\n */\nfunction refreshHistorySections(container) {\n const locale = historyDataCache.locale;\n const rewardsSection = container.querySelector('[data-history-section=\"rewards\"]');\n const purchasesSection = container.querySelector('[data-history-section=\"purchases\"]');\n const activitiesSection = container.querySelector('[data-history-section=\"activities\"]');\n\n if (rewardsSection) {\n const filter = rewardsSection.querySelector('[data-history-filter=\"rewards\"]')?.value || 'all';\n renderRewardsHistory(\n rewardsSection,\n historyDataCache.rewards.filter((row) => (0,_loyalty_js__WEBPACK_IMPORTED_MODULE_0__.isWithinDays)(row.date, filter)),\n locale,\n historyLabels(container),\n );\n }\n\n if (purchasesSection) {\n const filter =\n purchasesSection.querySelector('[data-history-filter=\"purchases\"]')?.value || 'all';\n renderPurchaseHistory(\n purchasesSection,\n historyDataCache.purchases.filter((row) => (0,_loyalty_js__WEBPACK_IMPORTED_MODULE_0__.isWithinDays)(row.date, filter)),\n locale,\n container,\n );\n }\n\n if (activitiesSection) {\n const filter =\n activitiesSection.querySelector('[data-history-filter=\"activities\"]')?.value || 'all';\n renderActivityHistory(\n activitiesSection,\n historyDataCache.activities.filter((row) => (0,_loyalty_js__WEBPACK_IMPORTED_MODULE_0__.isWithinDays)(row.date, filter)),\n locale,\n );\n }\n}\n\n/**\n * Main render function for History tab.\n * @param {HTMLElement} container\n * @param {Object} data\n * @param {string} locale\n */\nfunction renderHistory(container, data, locale) {\n if (!data || !data.loggedIn) return;\n\n historyDataCache = {\n rewards: (Array.isArray(data.history?.rewards) ? data.history.rewards : []).map(\n normalizeReward,\n ),\n purchases: (Array.isArray(data.history?.purchases) ? data.history.purchases : []).map(\n normalizePurchase,\n ),\n activities: (Array.isArray(data.history?.activities) ? data.history.activities : []).map(\n normalizeActivity,\n ),\n locale: locale || 'en',\n };\n\n refreshHistorySections(container);\n container.setAttribute('data-state', 'loaded');\n}\n\n/**\n * Wires history filter change listener once.\n */\nfunction initHistoryFilterListeners() {\n document.addEventListener('change', (e) => {\n const filter = e.target.closest('[data-history-filter]');\n if (!filter) return;\n const container = filter.closest(HISTORY_SELECTOR);\n if (container) refreshHistorySections(container);\n });\n}\n\n\n//# sourceURL=webpack://ardene-theme/./src/snippets/braze/inbox-history.js?\n}")},
1"./src/snippets/braze/inbox-offers.js":function(__unused_webpack___webpack_module__,__webpack_exports__,__webpack_require__){eval("{__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ OFFER_DETAILS_CLOSE_SELECTOR: () => (/* binding */ OFFER_DETAILS_CLOSE_SELECTOR),\n/* harmony export */ closeOfferDetails: () => (/* binding */ closeOfferDetails),\n/* harmony export */ createOfferTitle: () => (/* binding */ createOfferTitle),\n/* harmony export */ initOfferDetailsListeners: () => (/* binding */ initOfferDetailsListeners),\n/* harmony export */ openOfferDetails: () => (/* binding */ openOfferDetails),\n/* harmony export */ renderOffers: () => (/* binding */ renderOffers)\n/* harmony export */ });\n/* harmony import */ var _loyalty_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./loyalty.js */ \"./src/snippets/braze/loyalty.js\");\n/* harmony import */ var _safe_url_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./safe-url.js */ \"./src/snippets/braze/safe-url.js\");\n\n\n\nconst OFFER_DETAILS_SELECTOR = '[data-offer-details]';\nconst REWARDS_OFFERS_SELECTOR = '[data-rewards-offers]';\n\n/**\n * The details overlay's close control.\n *\n * Deliberately not the theme's usual `data-close`: drawer-component closes\n * itself on any `[data-close]` it contains, so sharing that attribute dismissed\n * the whole rewards drawer along with the overlay. Both click listeners are on\n * `document`, so stopping propagation would not have helped \u2014 only a selector\n * the drawer does not claim does.\n */\nconst OFFER_DETAILS_CLOSE_SELECTOR = '[data-offer-details-close]';\n\nlet offerDetailsPreviousFocus = null;\n\n/**\n * @param {HTMLElement|null|undefined} el\n * @param {boolean} isHidden\n */\nfunction setHidden(el, isHidden) {\n if (!el) return;\n el.hidden = isHidden;\n el.classList.toggle('hidden', isHidden);\n}\n\n/**\n * Static padlock SVG for offer cards (trusted markup, not API content).\n * @returns {string}\n */\nfunction getOfferIcon() {\n return `<svg width=\"40\" height=\"40\" viewBox=\"0 0 40 40\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\">\n <rect x=\"12\" y=\"18\" width=\"16\" height=\"14\" rx=\"2\" stroke=\"#000000\" stroke-width=\"1.5\" fill=\"none\"/>\n <path d=\"M15 18V14C15 11.2386 17.2386 9 20 9C22.7614 9 25 11.2386 25 14V18\" stroke=\"#000000\" stroke-width=\"1.5\" stroke-linecap=\"round\" fill=\"none\"/>\n <circle cx=\"20\" cy=\"25\" r=\"1.5\" fill=\"#000000\"/>\n </svg>`;\n}\n\n/**\n * Reads a label off the offers container, falling back to English.\n *\n * @param {string} datasetKey\n * @param {string} fallback\n * @returns {string}\n */\nfunction offersLabel(datasetKey, fallback) {\n return document.querySelector(REWARDS_OFFERS_SELECTOR)?.dataset[datasetKey] || fallback;\n}\n\n/**\n * Builds the \"Expires: March 1, 2027\" line shared by the card and the overlay.\n *\n * @param {number|string|null|undefined} expiresTimestamp\n * @param {string} locale\n * @returns {string} empty when the offer has no expiry\n */\nfunction formatExpiryLine(expiresTimestamp, locale) {\n if (!expiresTimestamp) return '';\n const expiryDate = (0,_loyalty_js__WEBPACK_IMPORTED_MODULE_0__.formatLoyaltyDate)(expiresTimestamp, locale, {\n year: 'numeric',\n month: 'long',\n day: 'numeric',\n });\n return `${offersLabel('i18nExpires', 'Expires')}: ${expiryDate}`;\n}\n\n/**\n * Opens the in-drawer offer details overlay.\n * @param {Object} offer\n * @param {string} locale\n */\nfunction openOfferDetails(offer, locale) {\n const panel = document.querySelector(OFFER_DETAILS_SELECTOR);\n if (!panel) return;\n\n const titleEl = panel.querySelector('[data-offer-details-title]');\n const expiryEl = panel.querySelector('[data-offer-details-expiry]');\n const descEl = panel.querySelector('[data-offer-details-description]');\n\n if (titleEl) titleEl.textContent = offer.title || offer.name || '';\n\n if (expiryEl) {\n expiryEl.textContent = formatExpiryLine(offer.expiresTimestamp, locale);\n expiryEl.hidden = !expiryEl.textContent;\n }\n\n if (descEl) {\n descEl.textContent = offer.description || offer.terms || '';\n descEl.hidden = !descEl.textContent;\n }\n\n offerDetailsPreviousFocus = document.activeElement;\n panel.setAttribute('data-active', 'true');\n panel.setAttribute('aria-hidden', 'false');\n panel.hidden = false;
1\n\n panel.querySelector(OFFER_DETAILS_CLOSE_SELECTOR)?.focus();\n}\n\n/**\n * Closes the in-drawer offer details overlay.\n */\nfunction closeOfferDetails() {\n const panel = document.querySelector(OFFER_DETAILS_SELECTOR);\n if (!panel) return;\n\n panel.setAttribute('data-active', 'false');\n panel.setAttribute('aria-hidden', 'true');\n panel.hidden = true;\n\n if (offerDetailsPreviousFocus && typeof offerDetailsPreviousFocus.focus === 'function') {\n offerDetailsPreviousFocus.focus();\n }\n offerDetailsPreviousFocus = null;\n}\n\n/**\n * Creates a single offer card.\n *\n * The card used to be one big button that opened the details overlay, which\n * meant a reward's configured CrowdTwist URL was unreachable \u2014 clicking the\n * title opened the overlay instead of the destination, unlike production. The\n * card is now a plain container with two distinct controls: the title links to\n * the reward URL, and Details opens the overlay. Keeping them separate is also\n * what makes the title a real link rather than a button that happens to\n * navigate.\n *\n * A reward with no usable URL keeps a non-interactive title; the overlay is\n * still reachable through Details, so no offer becomes a dead end.\n *\n * @param {Object} offer\n * @param {string} locale\n * @returns {HTMLElement}\n */\nfunction createOfferCard(offer, locale) {\n const card = document.createElement('div');\n card.className = 'braze-inbox__offer-card';\n\n const icon = document.createElement('div');\n icon.className = 'braze-inbox__offer-icon';\n icon.innerHTML = getOfferIcon();\n icon.setAttribute('aria-hidden', 'true');\n\n const content = document.createElement('div');\n content.className = 'braze-inbox__offer-content';\n content.appendChild(createOfferTitle(offer));\n\n const expiryLine = formatExpiryLine(offer.expiresTimestamp, locale);\n if (expiryLine) {\n const expiry = document.createElement('p');\n expiry.className = 'braze-inbox__offer-expiry';\n expiry.textContent = expiryLine;\n content.appendChild(expiry);\n }\n\n content.appendChild(createOfferDetailsButton(offer, locale));\n\n card.appendChild(icon);\n card.appendChild(content);\n\n return card;\n}\n\n/**\n * The reward title: a link to its configured URL, or inert copy without one.\n *\n * @param {Object} offer\n * @returns {HTMLElement}\n */\nfunction createOfferTitle(offer) {\n const offerUrl = offer.url || '';\n const hasUrl = (0,_safe_url_js__WEBPACK_IMPORTED_MODULE_1__.isSafeUrl)(offerUrl);\n\n const title = document.createElement(hasUrl ? 'a' : 'p');\n title.className = 'braze-inbox__offer-title';\n title.textContent = offer.title || offer.name || '';\n if (hasUrl) {\n title.href = offerUrl;\n title.setAttribute('data-actionable', 'true');\n }\n return title;\n}\n\n/**\n * The Details control, which opens the overlay rather than navigating.\n *\n * @param {Object} offer\n * @param {string} locale\n * @returns {HTMLButtonElement}\n */\nfunction createOfferDetailsButton(offer, locale) {\n const label = offersLabel('i18nDetails', 'Details');\n const titleText = offer.title || offer.name || '';\n\n const details = document.createElement('button');\n details.type = 'button';\n details.className = 'braze-inbox__offer-details';\n details.setAttribute('data-trigger', 'offer-details');\n details.textContent = label;\n details.setAttribute('aria-label', titleText ? `${label}: ${titleText}` : label);\n details.addEventListener('click', (e) => {\n e.preventDefault();\n openOfferDetails(offer, locale);\n });\n return details;\n}\n\n/**\n * Renders offers into the offers container.\n * @param {HTMLElement} container\n * @param {Object} data\n * @param {string} locale\n */\nfunction renderOffers(container, data, locale) {\n const offersLoaded = container.querySelector('[data-offers-state=\"loaded\"]');\n const offersEmpty = container.querySelector('[data-offers-state=\"empty\"]');\n const offersLoading = container.querySelector('[data-offers-state=\"loading\"]');\n\n if (!data?.loggedIn || !data.offers || data.offers.length === 0) {\n container.setAttribute('data-state', 'empty');\n setHidden(offersEmpty, false);\n setHidden(offersLoading, true);\n return;\n }\n\n const offersList = document.createElement('div');\n offersList.className = 'braze-inbox__offers-list';
1\n\n data.offers.forEach((offer) => {\n offersList.appendChild(createOfferCard(offer, locale));\n });\n\n offersLoaded.replaceChildren(offersList);\n\n container.setAttribute('data-state', 'loaded');\n setHidden(offersLoaded, false);\n setHidden(offersLoading, true);\n}\n\n/**\n * Wires offer-details overlay close / focus trap listeners once.\n *\n * The close control is marked with `data-offer-details-close`, not the theme's\n * usual `data-close`: drawer-component closes itself on any `[data-close]` it\n * contains, so sharing that attribute dismissed the whole rewards drawer along\n * with the overlay. Both listeners are on `document`, so stopping propagation\n * would not have helped \u2014 only a selector the drawer does not claim does.\n */\nfunction initOfferDetailsListeners() {\n document.addEventListener('click', (e) => {\n const closeBtn = e.target.closest(OFFER_DETAILS_CLOSE_SELECTOR);\n if (closeBtn) {\n e.preventDefault();\n closeOfferDetails();\n return;\n }\n const panel = document.querySelector(OFFER_DETAILS_SELECTOR);\n if (panel?.getAttribute('data-active') === 'true' && e.target === panel) {\n closeOfferDetails();\n }\n });\n\n document.addEventListener('keydown', (e) => {\n const panel = document.querySelector(OFFER_DETAILS_SELECTOR);\n if (!panel || panel.getAttribute('data-active') !== 'true') return;\n\n if (e.key === 'Escape') {\n e.preventDefault();\n closeOfferDetails();\n return;\n }\n\n if (e.key !== 'Tab') return;\n const focusable = panel.querySelectorAll(\n 'button, [href], input, select, textarea, [tabindex]:not([tabindex=\"-1\"])',\n );\n if (!focusable.length) return;\n const first = focusable[0];\n const last = focusable[focusable.length - 1];\n if (e.shiftKey && document.activeElement === first) {\n e.preventDefault();\n last.focus();\n } else if (!e.shiftKey && document.activeElement === last) {\n e.preventDefault();\n first.focus();\n }\n });\n}\n\n\n//# sourceURL=webpack://ardene-theme/./src/snippets/braze/inbox-offers.js?\n}")},
1"./src/snippets/braze/inbox-profile.js":function(__unused_webpack___webpack_module__,__webpack_exports__,__webpack_require__){eval("{__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ renderProfile: () => (/* binding */ renderProfile)\n/* harmony export */ });\n/* harmony import */ var _loyalty_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./loyalty.js */ \"./src/snippets/braze/loyalty.js\");\n\n\n/**\n * @param {HTMLElement|null|undefined} el\n * @param {boolean} isHidden\n */\nfunction setHidden(el, isHidden) {\n if (!el) return;\n el.hidden = isHidden;\n el.classList.toggle('hidden', isHidden);\n}\n\n/**\n * @param {HTMLElement} parent\n * @param {string} className\n * @param {string} text\n */\nfunction appendText(parent, className, text) {\n if (!text) return;\n const el = document.createElement('p');\n el.className = className;\n el.textContent = text;\n parent.appendChild(el);\n}\n\n/**\n * @param {string} levelTitle\n * @returns {string}\n */\nfunction levelFromTitle(levelTitle) {\n const match = String(levelTitle || '').match(/(\\d+)/);\n const n = match ? Number(match[1]) : 1;\n if (n === 2 || n === 3) return String(n);\n return '1';\n}\n\n/**\n * Resolves the profile header's spend line for the member's current state.\n *\n * @param {DOMStringMap} i18n\n * @param {number|null} spendNeeded\n * @param {string} expirationDate already-formatted, empty when absent\n * @param {boolean} isMaintenance\n * @param {string} locale\n * @returns {string}\n */\nfunction resolveSpendText(i18n, spendNeeded, expirationDate, isMaintenance, locale) {\n const variant = (0,_loyalty_js__WEBPACK_IMPORTED_MODULE_0__.spendMessageVariant)(spendNeeded, {\n isMaintenance,\n hasExpiry: Boolean(expirationDate),\n });\n const templates = {\n qualified:\n i18n.i18nSpendQualified ||\n \"You're all set! {{amount}} left to spend \u2014 your level status is secured until {{date}}.\",\n maintain:\n i18n.i18nSpendMaintain || 'Spend {{amount}} by {{date}} to maintain your level status!',\n levelUp: i18n.i18nSpendLevelUp || 'Spend {{amount}} by {{date}} to level up!',\n fallback: i18n.i18nSpendFallback || 'Keep shopping to maintain or level up your status!',\n };\n\n return (0,_loyalty_js__WEBPACK_IMPORTED_MODULE_0__.fillTemplate)(templates[variant], {\n amount: (0,_loyalty_js__WEBPACK_IMPORTED_MODULE_0__.formatSpendAmount)(spendNeeded, locale),\n date: expirationDate,\n });\n}\n\n/**\n * @param {DOMStringMap} i18n\n * @param {object} data\n * @param {string} locale\n */\nfunction buildProfileViewModel(i18n, data, locale) {\n const memberName = [data.member?.firstName, data.member?.lastName].filter(Boolean).join(' ');\n const memberSince = data.member?.memberSinceTimestamp\n ? (0,_loyalty_js__WEBPACK_IMPORTED_MODULE_0__.formatLoyaltyDate)(data.member.memberSinceTimestamp, locale, {\n year: 'numeric',\n month: 'short',\n day: 'numeric',\n })\n : '';\n const levelTitle = data.tier?.currentLevel?.title || 'Level 1';\n const spendNeeded = (0,_loyalty_js__WEBPACK_IMPORTED_MODULE_0__.resolveSpendNeeded)(data.tier?.spendNeeded);\n const expirationDate = data.tier?.expirationDate\n ? (0,_loyalty_js__WEBPACK_IMPORTED_MODULE_0__.formatLoyaltyDate)(data.tier.expirationDate, locale, {\n year: 'numeric',\n month: 'short',\n day: 'numeric',\n })\n : '';\n const points = data.points?.redeemable || 0;\n const neededForReward = (0,_loyalty_js__WEBPACK_IMPORTED_MODULE_0__.neededPointsForReward)(points, data.points?.neededForReward);\n\n return {\n memberName,\n memberSinceLine: memberSince ? `${i18n.i18nMemberSince || 'Member since'} ${memberSince}` : '',\n levelTitle,\n levelKey: levelFromTitle(levelTitle),\n spendText: resolveSpendText(\n i18n,\n spendNeeded,\n expirationDate,\n Boolean(data.tier?.isMaintenance),\n locale,\n ),\n points,\n pointsAwayText: (0,_loyalty_js__WEBPACK_IMPORTED_MODULE_0__.fillTemplate)(\n i18n.i18nPointsAway || '{{count}} points away from a {{value}} reward',\n {\n count: neededForReward,\n value: (0,_loyalty_js__WEBPACK_IMPORTED_MODULE_0__.formatLoyaltyCurrency)(data.points?.rewardValue || 5, locale, { decimals: 0 }),\n },\n ),\n showPointsAway: !(neededForReward === 0 && points >= 100),\n pointsLabel: i18n.i18nPoints || 'Points',\n };\n}\n\n/**\n * @param {ReturnType<typeof buildProfileViewModel>} view\n * @returns {{ info: HTMLElement, pointsWrap: HTMLElement }}\n */\nfunction buildProfileDom(view) {\n const info = document.createElement('div');\n info.className = 'braze-inbox__profile-info';\n appendText(info, 'braze-inbox__profile-name', view.memberName);\n appendText(info, 'braze-inbox__profile-member-since', view.memberSinceLine);\n appendText(info, 'braze-inbox__profile-level', view.levelTitle);\n appendText(info, 'braze-inbox__profile-progress', view.spendText);\n\n const pointsWrap = document.createElement('div');\n pointsWrap.className = 'braze-inbox__profile-points';\n const circle = document.createElement('div');\n circle.className = 'braze-inbox__points-circle';\n circle.setAttribute('data-level', view.levelKey);
1\n appendText(circle, 'braze-inbox__points-count', String(view.points));\n appendText(circle, 'braze-inbox__points-label', view.pointsLabel);\n pointsWrap.appendChild(circle);\n if (view.showPointsAway) {\n appendText(pointsWrap, 'braze-inbox__points-away', view.pointsAwayText);\n }\n\n return { info, pointsWrap };\n}\n\n/**\n * Renders profile data into the profile container using textContent only.\n * @param {HTMLElement} container\n * @param {Object} data\n * @param {string} locale\n */\nfunction renderProfile(container, data, locale) {\n const loadedDiv = container.querySelector('[data-profile-state=\"loaded\"]');\n const loadingDiv = container.querySelector('[data-profile-state=\"loading\"]');\n\n if (!data?.loggedIn || data.error) {\n container.setAttribute('data-state', 'error');\n loadedDiv.replaceChildren();\n const err = document.createElement('p');\n err.style.textAlign = 'center';\n err.style.padding = '20px';\n err.textContent = 'Unable to load profile';\n loadedDiv.appendChild(err);\n setHidden(loadedDiv, false);\n setHidden(loadingDiv, true);\n return;\n }\n\n const view = buildProfileViewModel(container.dataset, data, locale);\n const { info, pointsWrap } = buildProfileDom(view);\n loadedDiv.replaceChildren(info, pointsWrap);\n container.setAttribute('data-state', 'loaded');\n setHidden(loadedDiv, false);\n setHidden(loadingDiv, true);\n}\n\n\n//# sourceURL=webpack://ardene-theme/./src/snippets/braze/inbox-profile.js?\n}")},
1"./src/snippets/braze/inbox-ways-to-earn.js":function(__unused_webpack___webpack_module__,__webpack_exports__,__webpack_require__){eval("{__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ WAYS_TO_EARN_SELECTOR: () => (/* binding */ WAYS_TO_EARN_SELECTOR),\n/* harmony export */ fetchRewardsData: () => (/* binding */ fetchRewardsData),\n/* harmony export */ renderWaysToEarn: () => (/* binding */ renderWaysToEarn)\n/* harmony export */ });\n/* harmony import */ var _loyalty_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./loyalty.js */ \"./src/snippets/braze/loyalty.js\");\n/* harmony import */ var _safe_url_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./safe-url.js */ \"./src/snippets/braze/safe-url.js\");\n/* harmony import */ var _inbox_profile_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./inbox-profile.js */ \"./src/snippets/braze/inbox-profile.js\");\n\n\n\n\nconst WAYS_TO_EARN_SELECTOR = '[data-ways-to-earn]';\nconst REWARDS_PROFILE_SELECTOR = '[data-rewards-profile]';\n\n/**\n * @param {HTMLElement|null|undefined} el\n * @param {boolean} isHidden\n */\nfunction setHidden(el, isHidden) {\n if (!el) return;\n el.hidden = isHidden;\n el.classList.toggle('hidden', isHidden);\n}\n\n/**\n * @param {string} customerId\n * @param {string} locale\n * @returns {Promise<Object|null>}\n */\nasync function fetchRewardsData(customerId, locale) {\n try {\n const res = await fetch(\n `/apps/tools/api/rewards-summary?logged_in_customer_id=${customerId}&locale=${locale}`,\n );\n if (!res.ok) throw new Error(`HTTP ${res.status}`);\n return await res.json();\n } catch (error) {\n console.error('[Braze Inbox] Failed to fetch rewards data:', error);\n return null;\n }\n}\n\n/**\n * @param {HTMLElement} container\n * @returns {string}\n */\nfunction weburlSuffixFrom(container) {\n return container?.dataset?.weburlSuffix || 'en';\n}\n\n/**\n * @param {HTMLElement} card\n */\nfunction restoreActivityCard(card) {\n card.disabled = false;\n card.removeAttribute('aria-disabled');\n delete card.dataset.activityState;\n}\n\n/**\n * @param {HTMLElement} card\n * @param {HTMLElement} container\n * @param {string} locale\n */\nfunction markActivityCompleted(card, container, locale) {\n delete card.dataset.activityState;\n card.removeAttribute('data-trigger');\n card.removeAttribute('role');\n card.removeAttribute('tabindex');\n card.disabled = true;\n\n const titleEl = card.querySelector('.braze-inbox__activity-name');\n if (titleEl) {\n titleEl.removeAttribute('data-actionable');\n titleEl.style.textDecoration = 'none';\n }\n\n const content = card.querySelector('.braze-inbox__activity-content');\n if (content && !content.querySelector('.braze-inbox__activity-date')) {\n const dateEl = document.createElement('p');\n dateEl.className = 'braze-inbox__activity-date';\n const completedLabel = container.dataset.i18nCompleted || 'Completed';\n const date = (0,_loyalty_js__WEBPACK_IMPORTED_MODULE_0__.formatLoyaltyDate)(Math.floor(Date.now() / 1000), locale, {\n month: 'short',\n day: 'numeric',\n year: 'numeric',\n });\n dateEl.textContent = `${completedLabel} ${date}`;\n content.appendChild(dateEl);\n }\n}\n\n/**\n * @param {Object} activity\n * @param {string} activityUrl\n * @param {HTMLElement} card\n * @param {HTMLElement} container\n */\nasync function handleActivityClick(activity, activityUrl, card, container) {\n if (card.disabled) return;\n card.disabled = true;\n card.setAttribute('aria-disabled', 'true');\n card.dataset.activityState = 'loading';\n if ((0,_safe_url_js__WEBPACK_IMPORTED_MODULE_1__.isSafeUrl)(activityUrl)) window.open(activityUrl, '_blank', 'noopener,noreferrer');\n\n const profileContainer = document.querySelector(REWARDS_PROFILE_SELECTOR);\n const customerId = profileContainer?.dataset.customerId;\n const locale = profileContainer?.dataset.locale || 'en';\n if (!customerId) {\n console.error('[Ways to Earn] Customer ID n
1ot found');\n restoreActivityCard(card);\n return;\n }\n\n try {\n const response = await fetch(\n `/apps/tools/api/rewards-summary?logged_in_customer_id=${customerId}&locale=${locale}`,\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ activityId: activity.activityId }),\n },\n );\n const result = await response.json();\n if (!result.success) {\n restoreActivityCard(card);\n console.error('[Ways to Earn]', result.error || 'Failed to earn points');\n return;\n }\n markActivityCompleted(card, container, locale);\n if (profileContainer) {\n const profileData = await fetchRewardsData(customerId, locale);\n if (profileData) (0,_inbox_profile_js__WEBPACK_IMPORTED_MODULE_2__.renderProfile)(profileContainer, profileData, locale);\n }\n } catch (error) {\n console.error('[Ways to Earn] Error crediting activity:', error);\n restoreActivityCard(card);\n }\n}\n\n/**\n * @param {number|string|null} points\n * @param {string} ptsLabel\n * @returns {HTMLElement}\n */\nfunction createActivityBadge(points, ptsLabel) {\n const badge = document.createElement('div');\n badge.className = 'braze-inbox__activity-badge';\n\n if (typeof points === 'number') {\n const pointsEl = document.createElement('span');\n pointsEl.className = 'braze-inbox__activity-points';\n pointsEl.textContent = String(points);\n const ptsEl = document.createElement('span');\n ptsEl.className = 'braze-inbox__activity-pts';\n ptsEl.textContent = ptsLabel;\n badge.append(pointsEl, ptsEl);\n } else if (points != null && points !== '') {\n const pointsEl = document.createElement('span');\n pointsEl.className = 'braze-inbox__activity-points';\n pointsEl.textContent = String(points);\n badge.appendChild(pointsEl);\n } else {\n const ptsEl = document.createElement('span');\n ptsEl.className = 'braze-inbox__activity-pts';\n ptsEl.textContent = ptsLabel;\n badge.appendChild(ptsEl);\n }\n\n return badge;\n}\n\n/**\n * @param {Object} activity\n * @param {string} locale\n * @param {HTMLElement} container\n * @returns {HTMLElement}\n */\nfunction createWaysToEarnCard(activity, locale, container) {\n const ptsLabel = container.dataset.i18nPts || 'Pts';\n const activityName = activity.activityName || activity.title || activity.name || '';\n const activityUrl = (0,_loyalty_js__WEBPACK_IMPORTED_MODULE_0__.getActivityWebUrl)(activity, weburlSuffixFrom(container));\n const isActionable = (0,_loyalty_js__WEBPACK_IMPORTED_MODULE_0__.isActivityActionable)(activity, activityUrl, _safe_url_js__WEBPACK_IMPORTED_MODULE_1__.isSafeUrl);\n\n const card = document.createElement(isActionable ? 'button' : 'div');\n if (isActionable) {\n card.type = 'button';\n card.setAttribute('data-trigger', 'activity-earn');\n }\n card.className = 'braze-inbox__activity-card';\n card.dataset.activityId = activity.activityId;\n card.appendChild(createActivityBadge((0,_loyalty_js__WEBPACK_IMPORTED_MODULE_0__.getActivityPoints)(activity), ptsLabel));\n\n const content = document.createElement('div');\n content.className = 'braze-inbox__activity-content';\n const title = document.createElement(isActionable ? 'span' : 'p');\n title.className = 'braze-inbox__activity-name';\n if (isActionable) title.setAttribute('data-actionable', 'true');\n title.textContent = activityName;\n content.appendChild(title);\n\n if (!isActionable && activity.date) {\n const dateEl = document.createElement('p');\n dateEl.className = 'braze-inbox__activity-date';\n dateEl.textContent = `${container.dataset.i18nCompleted || 'Completed'} ${(0,_loyalty_js__WEBPACK_IMPORTED_MODULE_0__.formatLoyaltyDate)(\n activity.date,\n locale,\n { month: 'short', day: 'numeric', year: 'numeric' },\n )}`;\n content.appendChild(dateEl);\n }\n card.appendChild(content);\n\n if (isActionable) {\n card.addEventListener('click', (e) => {\n e.preventDefault();\n handleActivityClick(activity, activityUrl, card, container);\n });\n }\n return card;\n}\n\n/**\n * @param {HTMLElement} container\n * @param {Object} data\n * @param {string} locale\n */\nfunction renderWaysToEarn(container, data, locale) {\n const loadedEl = container.querySelector('[data-ways-to-earn-state=\"loaded\"]');\n const emptyEl = container.querySelector('[data-ways-to-earn-state=\"empty\"]');\n const loadingEl = container.querySelector('[data-ways-to-earn-state=\"loading\"]');\n const activities = (0,_loyalty_js__WEBPACK_IMPORTED_MODULE_0__.getWaysToEarnActivities)(data);\n\n if (!data?.loggedIn || activities.length === 0) {\n container.setAttribute('data-state', 'empty');\n setHidden(emptyEl, false);\n setHidden(loadingEl, true);\n setHidden(loadedEl, true);\n return;\n }\n\n const activitiesGrid = document.createElement('div');\n activitiesGrid.className = 'braze-inbox__activities-grid';
1\n activities.forEach((activity) => {\n activitiesGrid.appendChild(createWaysToEarnCard(activity, locale, container));\n });\n\n loadedEl.replaceChildren(activitiesGrid);\n container.setAttribute('data-state', 'loaded');\n setHidden(loadedEl, false);\n setHidden(loadingEl, true);\n setHidden(emptyEl, true);\n}\n\n\n\n\n//# sourceURL=webpack://ardene-theme/./src/snippets/braze/inbox-ways-to-earn.js?\n}")},
1"./src/snippets/braze/loyalty.js":function(__unused_webpack___webpack_module__,__webpack_exports__,__webpack_require__){eval("{__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ REWARD_POINT_THRESHOLD: () => (/* binding */ REWARD_POINT_THRESHOLD),\n/* harmony export */ fillTemplate: () => (/* binding */ fillTemplate),\n/* harmony export */ formatHistoryDateUtc: () => (/* binding */ formatHistoryDateUtc),\n/* harmony export */ formatLineItemDetails: () => (/* binding */ formatLineItemDetails),\n/* harmony export */ formatLineItemPoints: () => (/* binding */ formatLineItemPoints),\n/* harmony export */ formatLoyaltyCurrency: () => (/* binding */ formatLoyaltyCurrency),\n/* harmony export */ formatLoyaltyDate: () => (/* binding */ formatLoyaltyDate),\n/* harmony export */ formatRewardStatus: () => (/* binding */ formatRewardStatus),\n/* harmony export */ formatSpendAmount: () => (/* binding */ formatSpendAmount),\n/* harmony export */ getActivityPoints: () => (/* binding */ getActivityPoints),\n/* harmony export */ getActivityWebUrl: () => (/* binding */ getActivityWebUrl),\n/* harmony export */ getPurchaseOrderNumber: () => (/* binding */ getPurchaseOrderNumber),\n/* harmony export */ getWaysToEarnActivities: () => (/* binding */ getWaysToEarnActivities),\n/* harmony export */ isActivityActionable: () => (/* binding */ isActivityActionable),\n/* harmony export */ isWithinDays: () => (/* binding */ isWithinDays),\n/* harmony export */ neededPointsForReward: () => (/* binding */ neededPointsForReward),\n/* harmony export */ pickHistoryDate: () => (/* binding */ pickHistoryDate),\n/* harmony export */ resolveSpendNeeded: () => (/* binding */ resolveSpendNeeded),\n/* harmony export */ spendMessageVariant: () => (/* binding */ spendMessageVariant),\n/* harmony export */ toUtcCalendarDate: () => (/* binding */ toUtcCalendarDate)\n/* harmony export */ });\n/**\n * Loyalty helpers shared by the Braze inbox rewards UI.\n *\n * Dates from CrowdTwist / rewards-summary are often UTC midnight. Formatting\n * them in the shopper's local timezone shifts the calendar day back by one\n * in Americas timezones \u2014 always format the UTC calendar date instead.\n */\n\nconst REWARD_POINT_THRESHOLD = 100;\n\n/**\n * Points still needed for the next $5 reward.\n *\n * Prefers a positive API `neededForReward`. Otherwise derives from redeemable\n * points using the 100-point reward rule so we never show a false \"0 points away\".\n *\n * @param {number} redeemable\n * @param {number} [apiNeeded]\n * @param {number} [threshold=100]\n * @returns {number}\n */\nfunction neededPointsForReward(redeemable, apiNeeded, threshold = REWARD_POINT_THRESHOLD) {\n const points = Number(redeemable) || 0;\n const api = Number(apiNeeded);\n if (Number.isFinite(api) && api > 0) return api;\n\n if (points <= 0) return threshold;\n const remainder = points % threshold;\n return remainder === 0 ? 0 : threshold - remainder;\n}\n\n/**\n * Turns a reward-history status token into drawer copy.\n *\n * The app proxy sends a machine value (`fulfilled` / `pending`) rather than a\n * label, because this drawer is bilingual and owns its own strings. Anything\n * unrecognised passes through unchanged: a status the proxy adds later should\n * show up as something visible rather than silently blanking the cell.\n *\n * @param {string|null|undefined} status\n * @param {{ statusFulfilled?: string, statusPending?: string }} labels\n * @returns {string}\n */\nfunction formatRewardStatus(status, labels = {}) {\n if (!status) return '';\n if (status === 'fulfilled') return labels.statusFulfilled || status;\n if (status === 'pending') return labels.statusPending || status;\n return String(status);\n}\n\n/**\n * Resolves a tier spend requirement, preserving a genuine zero.\n *\n * A member who has already met their threshold has a `spendNeeded` of 0, and\n * that is a real amount the profile header should state. Coercing with\n * `Number(x) || 0` made zero indistinguishable from a missing field, so a\n * `> 0` guard hid both behind generic copy and the amount was never shown \u2014\n * the same class of false-zero this module already guards against in\n * neededPointsForReward.\n *\n * @param {unknown} spendNeeded\n * @returns {number|null} the amount, or null when there is nothing to show\n */\nfunction resolveSpendNeeded(spendNeeded) {\n if (spendNeeded === null || spendNeeded === undefined || spendNeeded === '') return null;\n const amount = Number(spendNeeded);\n return Number.isFinite(amount) ? amount : null;\n}\n\n/**\n * Formats a loyalty dollar amount for the locale the drawer renders in.\n *\n * French Canadian puts the sign after the amount and separates decimals with a\n * comma (`30,90 $`); English keeps `$30.90`. The drawer used to assemble\n * `'$' + n.toFixed(2)` by hand while the French copy carried its own `$`\n * prefix, so every amount read as English on the French storefront. Intl owns\n * the symbol, its placement and both separators now, and the copy interpolates\n * an already-formatted amount.\n *\n *
1Loyalty amounts are CrowdTwist programme dollars rather than cart money, so\n * the currency is a single default instead of being read off the cart.\n *\n * @param {number|string|null|undefined} value\n * @param {string} [locale='en']\n * @param {{ decimals?: number, currency?: string }} [options]\n * @returns {string} the formatted amount, or '' when there is none to state\n */\nfunction formatLoyaltyCurrency(value, locale = 'en', options = {}) {\n if (value === null || value === undefined || value === '') return '';\n const amount = Number(value);\n if (!Number.isFinite(amount)) return '';\n\n const { decimals = 2, currency = 'CAD' } = options;\n const format = (tag) =>\n new Intl.NumberFormat(tag, {\n style: 'currency',\n currency,\n currencyDisplay: 'narrowSymbol',\n minimumFractionDigits: decimals,\n maximumFractionDigits: decimals,\n }).format(amount);\n\n try {\n return format(locale || 'en');\n } catch {\n // Intl throws on a malformed language tag. A bad `request.locale.iso_code`\n // should cost the member their formatting, not their balance.\n return format('en');\n }\n}\n\n/**\n * Formats a resolved spend amount for the profile copy.\n *\n * Every spend message names the amount now, including the qualified one, so the\n * formatted value has to keep a genuine zero (that is the member's real \"left to\n * spend\") while refusing a negative one: a member who overshot their threshold\n * still has $0 left to spend, not -$40.\n *\n * @param {number|null|undefined} spendNeeded resolved amount, or null when absent\n * @param {string} [locale='en']\n * @returns {string} a localized amount, or '' when there is nothing to state\n */\nfunction formatSpendAmount(spendNeeded, locale = 'en') {\n if (spendNeeded === null || spendNeeded === undefined) return '';\n return formatLoyaltyCurrency(Math.max(spendNeeded, 0), locale);\n}\n\n/**\n * Picks which spend message the profile header should show.\n *\n * Showing the amount is the point \u2014 a member asked why theirs was missing \u2014 but\n * a member who has already met their threshold has 0 left to spend, and\n * \"Spend $0.00 by Dec 14 to maintain your level status!\" reads worse than the\n * generic fallback it replaced. Zero (or an overshoot) is its own state: they\n * have qualified, and the only useful fact left is the date it holds until.\n *\n * @param {number|null|undefined} spendNeeded resolved amount, or null when absent\n * @param {{ isMaintenance?: boolean, hasExpiry?: boolean }} context\n * @returns {'qualified'|'maintain'|'levelUp'|'fallback'}\n */\nfunction spendMessageVariant(\n spendNeeded,\n { isMaintenance = false, hasExpiry = false } = {},\n) {\n // Every non-fallback message names a date, so no date means no message.\n if (spendNeeded === null || spendNeeded === undefined || !hasExpiry) return 'fallback';\n if (spendNeeded <= 0) return 'qualified';\n return isMaintenance ? 'maintain' : 'levelUp';\n}\n\n/**\n * Parses a loyalty timestamp into a Date representing the UTC calendar day.\n *\n * @param {number|string|null|undefined} timestamp Unix seconds or ISO / YYYY-MM-DD\n * @returns {Date|null}\n */\nfunction toUtcCalendarDate(timestamp) {\n if (timestamp == null || timestamp === '') return null;\n\n if (typeof timestamp === 'string') {\n const ymd = timestamp.match(/^(\\d{4})-(\\d{2})-(\\d{2})/);\n if (ymd) {\n return new Date(Date.UTC(Number(ymd[1]), Number(ymd[2]) - 1, Number(ymd[3])));\n }\n const parsed = new Date(timestamp);\n if (Number.isNaN(parsed.getTime())) return null;\n return new Date(Date.UTC(parsed.getUTCFullYear(), parsed.getUTCMonth(), parsed.getUTCDate()));\n }\n\n const numeric = Number(timestamp);\n if (!Number.isFinite(numeric)) return null;\n // Heuristic: values under 1e12 are Unix seconds.\n const ms = numeric < 1e12 ? numeric * 1000 : numeric;\n const parsed = new Date(ms);\n if (Number.isNaN(parsed.getTime())) return null;\n return new Date(Date.UTC(parsed.getUTCFullYear(), parsed.getUTCMonth(), parsed.getUTCDate()));\n}\n\n/**\n * Formats a loyalty date using UTC calendar parts so expiry is not off by one day.\n *\n * @param {number|string|null|undefined} timestamp\n * @param {string} [locale='en']\n * @param {Intl.DateTimeFormatOptions} [options]\n * @returns {string}\n */\nfunction formatLoyaltyDate(\n timestamp,\n locale = 'en',\n options = { year: 'numeric', month: 'long', day: 'numeric' },\n) {\n const date = toUtcCalendarDate(timestamp);\n if (!date) return '';\n return date.toLocaleDateString(locale, { ...options, timeZone: 'UTC' });\n}\n\n/**\n * History table date as M/D/YYYY in UTC (matches live loyalty dashboard).\n *\n * @param {number|string|null|undefined} timestamp\n * @returns {string}
1\n */\nfunction formatHistoryDateUtc(timestamp) {\n const date = toUtcCalendarDate(timestamp);\n if (!date) return '';\n const month = date.getUTCMonth() + 1;\n const day = date.getUTCDate();\n const year = date.getUTCFullYear();\n return `${month}/${day}/${year}`;\n}\n\n/**\n * Whether a history row falls within the last N days (client-side filter).\n *\n * @param {number|string|null|undefined} timestamp\n * @param {string|number} daysFilter 'all' | 30 | 90 | 180 | 270 | 365\n * @returns {boolean}\n */\nfunction isWithinDays(timestamp, daysFilter) {\n if (daysFilter === 'all' || daysFilter == null || daysFilter === '') return true;\n const days = Number(daysFilter);\n if (!Number.isFinite(days) || days <= 0) return true;\n\n const date = toUtcCalendarDate(timestamp);\n if (!date) return false;\n\n const cutoff = Date.now() - days * 24 * 60 * 60 * 1000;\n return date.getTime() >= cutoff;\n}\n\n/**\n * Resolves a CrowdTwist activity web URL from additional data points.\n * Merchants configure `weburlen` / `weburlfr` / `weburlus` (and casing variants).\n *\n * @param {object|null|undefined} activity\n * @param {string}
1 [weburlSuffix='en'] `en` | `fr` | `us`\n * @returns {string}\n */\nfunction getActivityWebUrl(activity, weburlSuffix = 'en') {\n if (!activity || typeof activity !== 'object') return '';\n\n const suffix = String(weburlSuffix || 'en').toLowerCase();\n const bags = [\n activity.additionalData,\n activity.extraData,\n activity.userData,\n activity.data,\n activity.customData,\n activity,\n ].filter((bag) => bag && typeof bag === 'object' && !Array.isArray(bag));\n\n const keyVariants = [\n `weburl${suffix}`,\n `webUrl${suffix}`,\n `web_url_${suffix}`,\n `WEBURL${suffix.toUpperCase()}`,\n ];\n\n for (const bag of bags) {\n for (const key of keyVariants) {\n const value = bag[key];\n if (typeof value === 'string' && value.trim()) return value.trim();\n }\n }\n\n for (const bag of bags) {\n for (const key of ['url', 'weburl', 'webUrl', 'web_url', 'link']) {\n const value = bag[key];\n if (typeof value === 'string' && value.trim()) return value.trim();\n }\n }\n\n return '';\n}\n\n/**\n * \"Purchase Details\" text for one purchase line item.\n *\n * The SKU belongs in the details column beside the order number it breaks\n * down, not in the Date column. Quantity comes along because it has nowhere\n * else to sit once the SKU takes that column, and it is named only when it is\n * more than one \u2014 \"x 1\" on every row is noise, not information.\n *\n * @param {object|null|undefined} item\n * @returns {string} empty when the item names nothing\n */\nfunction formatLineItemDetails(item) {\n if (!item || typeof item !== 'object') return '';\n\n const label = item.sku || item.name || item.title || '';\n if (!label) return '';\n\n const quantity = Number(item.quantity);\n return Number.isFinite(quantity) && quantity > 1 ? `${label} \\u00d7 ${quantity}` : label;\n}\n\n/**\n * Signed points for one purchase line item, for the expanded SKU rows.\n *\n * An item that genuinely earned nothing reads \"+0\" \u2014 an excluded item is a real\n * case and the shopper should see it \u2014 while an item whose points field is\n * absent stays blank rather than claiming zero. Same false-zero distinction as\n * neededPointsForReward and resolveSpendNeeded.\n *\n * @param {object|null|undefined} item\n * @returns {string} signed points, or '' when the item carries none\n */\nfunction formatLineItemPoints(item) {\n if (!item || typeof item !== 'object') return '';\n\n const raw = item.points ?? item.pointsAwarded ?? item.points_awarded ?? item.pointsEarned;\n if (raw == null || raw === '') return '';\n\n const numeric = Number(raw);\n if (!Number.isFinite(numeric)) return String(raw);\n return numeric >= 0 ? `+${numeric}` : String(numeric);\n}\n\n/**\n * Points value for a ways-to-earn activity (defensive field names).\n *\n * @param {object|null|undefined} activity\n * @returns {number|string|null}\n */\nfunction getActivityPoints(activity) {\n if (!activity || typeof activity !== 'object') return null;\n const raw =\n activity.points ??\n activity.numPoints ??\n activity.pointValue ??\n activity.pointsAwarded ??\n activity.bonusPoints ??\n activity.pointAmount;\n if (raw == null || raw === '') return null;\n if (typeof raw === 'number') return raw;\n const asNumber = Number(raw);\n return Number.isFinite(asNumber) ? asNumber : String(raw);\n}\n\n/**\n * First usable timestamp from common CrowdTwist / proxy field names.\n *\n * @param {object|null|undefined} row\n * @param {string[]} keys\n * @returns {number|string|null|undefined}\n */\nfunction pickHistoryDate(row, keys = []) {\n if (!row || typeof row !== 'object') return null;\n for (const key of keys) {\n if (row[key] != null && row[key] !== '') return row[key];\n }\n return null;\n}\n\n/**\n * Order number for purchase history \u2014 never fall back to a generic `id`.\n *\n * @param {object|null|undefined} purchase\n * @returns {string}\n */\nfunction getPurchaseOrderNumber(purchase) {\n if (!purchase || typeof purchase !== 'object') return '';\n const raw =\n purchase.orderNumber ||\n purchase.order_number ||\n purchase.orderId ||\n purchase.order_id ||\n purchase.details ||\n purchase.title ||\n '';\n return raw != null && raw !== '' ? String(raw) : '';\n}\n\n/**\n * Whether a ways-to-earn catalog activity should be clickable.\n * Catalog lists never include history rows; still respect explicit completed/blocked flags.\n *\n * @param {object|null|undefined} activity\n * @param {string} activityUrl\n * @param {(url: unknown) => boolean} isSafeUrlFn\n * @returns {boolean}\n */\nfunction isActivityActionable(activity, activityUrl, isSafeUrlFn) {\n if (!activity || typeof isSafeUrlFn !== 'function') return false;\n if (!isSafeUrlFn(activityUrl)) return false;\n if (activity.canEarnAgain === false) return false;\n if (activity.completed === true || activity.isCompleted === true) return false;\n return true;\n}\n\n/**\n * Available ways-to-earn activities from the summary payload.\n * Never falls back to history (completed) activities.\n *\n * @param {object|null|undefined} data\n * @returns {Array}
1\n */\nfunction getWaysToEarnActivities(data) {\n if (!data || typeof data !== 'object') return [];\n if (Array.isArray(data.waysToEarn) && data.waysToEarn.length) return data.waysToEarn;\n if (Array.isArray(data.activities) && data.activities.length) return data.activities;\n if (Array.isArray(data.availableActivities) && data.availableActivities.length) {\n return data.availableActivities;\n }\n return [];\n}\n\n/**\n * Interpolates `{{key}}` placeholders in a Liquid `t` template string.\n *\n * @param {string} template\n * @param {Record<string, string|number>} vars\n * @returns {string}\n */\nfunction fillTemplate(template, vars = {}) {\n if (!template) return '';\n return String(template).replace(/\\{\\{\\s*(\\w+)\\s*\\}\\}/g, (_, key) =>\n vars[key] != null ? String(vars[key]) : '',\n );\n}\n\n\n//# sourceURL=webpack://ardene-theme/./src/snippets/braze/loyalty.js?\n}")},
1"./src/snippets/braze/safe-url.js":function(__unused_webpack___webpack_module__,__webpack_exports__,__webpack_require__){eval("{__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ isSafeUrl: () => (/* binding */ isSafeUrl)\n/* harmony export */ });\n/**\n * URL scheme validation for Braze-authored content.\n *\n * Card and banner URLs are authored in the Braze dashboard, not by the theme.\n * Assigning one straight to `element.href` lets a `javascript:` URL execute in\n * the storefront origin the moment a shopper clicks the card. `data:` and\n * `vbscript:` are the same class of problem.\n *\n * Braze's own `allowUserSuppliedJavascript` initialization flag exists to stop\n * exactly this, which is the clearest signal that dashboard-authored content is\n * not to be trusted with a raw href.\n */\n\n/** Absolute http(s) \u2014 the only remote schemes a card may link to. */\nconst ABSOLUTE_HTTP = /^https?:\\/\\//i;\n\n/**\n * Returns true when a URL is safe to assign to an href or src.\n *\n * Accepts absolute http(s) and root-relative paths. Protocol-relative URLs\n * (`//host`) are rejected: they are absolute links to another origin wearing a\n * relative disguise.\n *\n * @param {unknown} url\n * @returns {boolean}\n */\nfunction isSafeUrl(url) {\n if (typeof url !== 'string') return false;\n\n const trimmed = url.trim();\n if (!trimmed) return false;\n\n if (trimmed.startsWith('//')) return false;\n if (trimmed.startsWith('/')) return true;\n\n return ABSOLUTE_HTTP.test(trimmed);\n}\n\n\n//# sourceURL=webpack://ardene-theme/./src/snippets/braze/safe-url.js?\n}")},
1"./src/snippets/snippet.braze-inbox.css":function(__unused_webpack_module,__webpack_exports__,__webpack_require__){eval("{__webpack_require__.r(__webpack_exports__);\n// extracted by mini-css-extract-plugin\n\n\n//# sourceURL=webpack://ardene-theme/./src/snippets/snippet.braze-inbox.css?\n}")},
1"./src/snippets/snippet.braze-inbox.js":function(__unused_webpack___webpack_module__,__webpack_exports__,__webpack_require__){eval("{__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _braze_inbox_profile_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./braze/inbox-profile.js */ \"./src/snippets/braze/inbox-profile.js\");\n/* harmony import */ var _braze_inbox_offers_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./braze/inbox-offers.js */ \"./src/snippets/braze/inbox-offers.js\");\n/* harmony import */ var _braze_inbox_ways_to_earn_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./braze/inbox-ways-to-earn.js */ \"./src/snippets/braze/inbox-ways-to-earn.js\");\n/* harmony import */ var _braze_inbox_history_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./braze/inbox-history.js */ \"./src/snippets/braze/inbox-history.js\");\n\n\n\n\n\nconst FEED_SELECTOR = '[data-braze-inbox-feed]';\nconst EMPTY_SELECTOR = '[data-braze-inbox-empty]';\nconst TAB_SELECTOR = '[data-braze-inbox-tab]';\nconst BADGE_SELECTOR = '[data-braze-inbox-count]';\nconst ACTIVE_TAB_CLASS = 'braze-inbox__tab--active';\n\nlet currentCards = [];\nlet activeCategory = 'all';\nlet activeObserver = null;\n\n/** @returns {{ logContentCardImpressions: Function, logContentCardClick: Function, logCardDismissal: Function }} */\nfunction sdk() {\n return window.BRAZE_SDK ?? {};\n}\n\n/**\n * Returns inbox cards filtered from the full card pool.\n * @param {Array} cards\n * @returns {Array}\n */\nfunction getInboxCards(cards) {\n return cards.filter(c => {\n const extras = c.extras ?? {};\n return extras.placement === 'inbox' && !c.dismissed && !c.isControl;\n });\n}\n\n/**\n * Returns cards matching the active category tab, or all inbox cards if \"all\".\n * @param {Array} inboxCards\n * @returns {Array}\n */\nfunction filterByCategory(inboxCards) {\n if (activeCategory === 'all') return inboxCards;\n return inboxCards.filter(c => {\n const extras = c.extras ?? {};\n return extras.category === activeCategory;\n });\n}\n\n/**\n * Updates all badge elements with the unviewed count.\n * @param {Array} inboxCards\n */\nfunction updateBadge(inboxCards) {\n const unviewed = inboxCards.filter(c => !c.viewed).length;\n document.querySelectorAll(BADGE_SELECTOR).forEach(el => {\n el.textContent = unviewed > 0 ? String(unviewed) : '';\n el.style.display = unviewed > 0 ? '' : 'none';\n });\n}\n\n/**\n * Builds a single inbox card DOM element.\n * @param {Object} card\n * @returns {HTMLElement}\n */\nfunction buildInboxCard(card) {\n const extras = card.extras ?? {};\n const article = document.createElement('article');\n article.className = 'braze-inbox__card';\n if (!card.viewed) {\n article.classList.add('braze-inbox__card--unread');\n }\n article.dataset.brazeCardId = card.id;\n\n if (card.imageUrl) {\n const img = document.createElement('img');\n img.src = card.imageUrl;\n img.alt = card.title || '';\n img.className = 'braze-inbox__card-image';\n img.loading = 'lazy';\n article.appendChild(img);\n }\n\n const body = document.createElement('div');\n body.className = 'braze-inbox__card-body';\n\n if (card.title) {\n const h3 = document.createElement('h3');\n h3.className = 'braze-inbox__card-title';\n h3.textContent = card.title;\n body.appendChild(h3);\n }\n\n if (card.description) {\n const p = document.createElement('p');\n p.className = 'braze-inbox__card-description';\n p.textContent = card.description;\n body.appendChild(p);\n }\n\n if (extras.category) {\n const tag = document.createElement('span');\n tag.className = 'braze-inbox__card-tag';\n tag.textContent = extras.category;\n body.appendChild(tag);\n }\n\n article.appendChild(body);\n\n if (card.dismissible) {\n const btn = document.createElement('button');\n btn.type = 'button';\n btn.className = 'braze-inbox__card-dismiss';\n btn.setAttribute('aria-label', 'Dismiss');\n btn.textContent = '\\u00d7';\n btn.addEventListener('click', (e) => {\n e.preventDefault();\n e.stopPropagation();
1\n sdk().logCardDismissal?.(card);\n article.remove();\n currentCards = currentCards.filter(c => c.id !== card.id);\n const inboxCards = getInboxCards(currentCards);\n updateBadge(inboxCards);\n if (filterByCategory(inboxCards).length === 0) {\n showEmpty(true);\n }\n });\n article.appendChild(btn);\n }\n\n // Card click \u2014 log and navigate (only allow http/https URLs)\n if (card.url && /^https?:\\/\\//.test(card.url)) {\n article.style.cursor = 'pointer';\n article.addEventListener('click', () => {\n sdk().logContentCardClick?.(card);\n window.location.href = card.url;\n });\n }\n\n return article;\n}\n\n/**\n * Shows or hides the empty state.\n * @param {boolean} show\n */\nfunction showEmpty(show) {\n const emptyEl = document.querySelector(EMPTY_SELECTOR);\n if (emptyEl) {\n emptyEl.classList.toggle('hidden', !show);\n }\n}\n\n/**\n * Renders the inbox feed with the current filter state.\n */\nfunction renderInbox() {\n const feed = document.querySelector(FEED_SELECTOR);\n if (!feed) return;\n\n // Disconnect previous observer before re-rendering\n if (activeObserver) {\n activeObserver.disconnect();\n activeObserver = null;\n }\n\n // Keep the empty-state element, clear rendered cards\n feed.querySelectorAll('.braze-inbox__card').forEach(el => el.remove());\n\n const inboxCards = getInboxCards(currentCards);\n const visible = filterByCategory(inboxCards);\n\n updateBadge(inboxCards);\n\n if (visible.length === 0) {\n showEmpty(true);\n return;\n }\n\n showEmpty(false);\n\n activeObserver = new IntersectionObserver((entries) => {\n entries.forEach(entry => {\n if (!entry.isIntersecting) return;\n const cardId = entry.target.dataset.brazeCardId;\n const card = inboxCards.find(c => c.id === cardId);\n if (card) {\n sdk().logContentCardImpressions?.([card]);\n activeObserver.unobserve(entry.target);\n }\n });\n }, { threshold: 0.5 });\n\n visible.forEach(card => {\n const el = buildInboxCard(card);\n activeObserver.observe(el);\n feed.appendChild(el);\n });\n}\n\n// --- Tab handling ---\n\nconst PANEL_ID = 'braze-inbox-panel';\nconst REWARDS_PANEL_ID = 'braze-inbox-rewards-panel';\nconst REWARDS_PANEL_ATTR = 'data-active-panel';\n\n/**\n * Shows the Braze feed panel and hides the rewards panel.\n */\nfunction showBrazePanel() {\n const brazePanel = document.getElementById(PANEL_ID);\n const rewardsPanel = document.getElementById(REWARDS_PANEL_ID);\n const wrapper = brazePanel?.closest('.braze-inbox__panels');\n\n if (brazePanel) brazePanel.classList.remove('hidden');\n if (rewardsPanel) rewardsPanel.classList.add('hidden');\n if (wrapper) wrapper.setAttribute(REWARDS_PANEL_ATTR, 'braze');\n}\n\n/**\n * Shows the rewards widget panel and hides the Braze feed panel.\n */\nfunction showRewardsPanel() {\n const brazePanel = document.getElementById(PANEL_ID);\n const rewardsPanel = document.getElementById(REWARDS_PANEL_ID);\n const wrapper = brazePanel?.closest('.braze-inbox__panels');\n\n if (brazePanel) brazePanel.classList.add('hidden');\n if (rewardsPanel) rewardsPanel.classList.remove('hidden');\n if (wrapper) wrapper.setAttribute(REWARDS_PANEL_ATTR, 'rewards');\n\n document.dispatchEvent(new CustomEvent('rewards-widget:activate'));\n}\n\nfunction activateTab(tab) {\n const isRewardsTab = tab.dataset.rewardsTab === 'rewards';\n\n // Update tab button states\n document.querySelectorAll(TAB_SELECTOR).forEach(t => {\n t.classList.remove(ACTIVE_TAB_CLASS);\n t.setAttribute('aria-selected', 'false');\n t.setAttribute('tabindex', '-1');\n });\n tab.classList.add(ACTIVE_TAB_CLASS);\n tab.setAttribute('aria-selected', 'true');\n tab.setAttribute('tabindex', '0');\n\n if (isRewardsTab) {\n showRewardsPanel();\n // Do not update activeCategory or call renderInbox \u2014 rewards widget handles its own panel\n return;\n }\n\n // Non-rewards tab: show Braze feed and filter by category\n showBrazePanel();\n activeCategory = tab.dataset.brazeInboxTab;\n\n const panel = document.getElementById(PANEL_ID);\n if (panel) {\n panel.setAttribute('aria-labelledby', tab.id);\n }\n\n renderInbox();\n}\n\ndocument.addEventListener('click', (e) => {\n const tab = e.target.closest(TAB_SELECTOR);\n if (!tab) return;\n activateTab(tab);\n});\n\ndocument.addEventListener('keydown', (e) => {\n const tab = e.target.closest(TAB_SELECTOR);\n if (!tab) return;\n\n const tabs = Array.from(document.querySelectorAll(TAB_SELECTOR));\n const idx = tabs.indexOf(tab);\n if (idx === -1) return;\n\n let next;\n if (e.key === 'ArrowRight') {\n next = tabs[(idx + 1) % tabs.length];\n } else if (e.key === 'ArrowLeft') {\n next = tabs[(idx - 1 + tabs.length) % tabs.length];\n } else if (e.key === 'Home') {\n next = tabs[0];\n } else if (e.key === 'End') {\n next = tabs[tabs.length - 1];\n }\n\n if (next) {\n e.preventDefault();\n next.focus();\n activateTab(next);\n }\n});\n\n// --- Listen for card updates from snippet.braze.js ---\ndocument.addEventListener('braze:content-cards-updated',
1(e) => {\n currentCards = e.detail?.cards ?? [];\n renderInbox();\n});\n\n// --- Pick up cached cards if they arrived before this script loaded ---\nconst cached = sdk().getCachedContentCards?.();\nif (cached?.cards?.length) {\n currentCards = cached.cards;\n renderInbox();\n}\n\n// ========================================\n// Promotional Tabs (Not Logged In)\n// ========================================\n\nconst PROMO_TAB_SELECTOR = '[data-promo-tab]';\nconst PROMO_PANEL_SELECTOR = '[data-promo-panel]';\nconst PROMO_ACTIVE_TAB_CLASS = 'braze-inbox__tab--active';\n\n/**\n * Activates a promotional tab and shows its panel.\n * @param {HTMLElement} tab\n */\nfunction activatePromoTab(tab) {\n const targetPanel = tab.dataset.promoTab;\n \n // Update all tab states\n document.querySelectorAll(PROMO_TAB_SELECTOR).forEach(t => {\n t.classList.remove(PROMO_ACTIVE_TAB_CLASS);\n t.setAttribute('aria-selected', 'false');\n t.setAttribute('tabindex', '-1');\n });\n \n // Activate clicked tab\n tab.classList.add(PROMO_ACTIVE_TAB_CLASS);\n tab.setAttribute('aria-selected', 'true');\n tab.setAttribute('tabindex', '0');\n \n // Hide all panels\n document.querySelectorAll(PROMO_PANEL_SELECTOR).forEach(panel => {\n panel.classList.add('hidden');\n });\n \n // Show target panel\n const panel = document.querySelector(`[data-
1promo-panel=\"${targetPanel}\"]`);\n if (panel) {\n panel.classList.remove('hidden');\n // Scroll tab into view (with smooth snap behavior)\n tab.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' });\n }\n}\n\n// Click handler for promotional tabs\ndocument.addEventListener('click', (e) => {\n const promoTab = e.target.closest(PROMO_TAB_SELECTOR);\n if (!promoTab) return;\n activatePromoTab(promoTab);\n});\n\n// Keyboard navigation for promotional tabs\ndocument.addEventListener('keydown', (e) => {\n const promoTab = e.target.closest(PROMO_TAB_SELECTOR);\n if (!promoTab) return;\n \n const promoTabs = Array.from(document.querySelectorAll(PROMO_TAB_SELECTOR));\n const idx = promoTabs.indexOf(promoTab);\n if (idx === -1) return;\n \n let next;\n if (e.key === 'ArrowRight') {\n next = promoTabs[(idx + 1) % promoTabs.length];\n } else if (e.key === 'ArrowLeft') {\n next = promoTabs[(idx - 1 + promoTabs.length) % promoTabs.length];\n } else if (e.key === 'Home') {\n next = promoTabs[0];\n } else if (e.key === 'End') {\n next = promoTabs[promoTabs.length - 1];\n }\n \n if (next) {\n e.preventDefault();\n next.focus();\n activatePromoTab(next);\n }\n});\n\n// ========================================\n// Perks Tab - Level Switching\n// ========================================\n\nconst LEVEL_TAB_SELECTOR = '[data-level-tab]';\nconst LEVEL_VALUE_SELECTOR = '[data-level-value]';\nconst LEVEL_DISPLAY_SELECTOR = '[data-level-display]';\n\n/**\n * Activates a level tab and shows corresponding values in the table.\n * @param {HTMLElement} tab\n */\nfunction activateLevelTab(tab) {\n const targetLevel = tab.dataset.levelTab;\n const perksRoot = tab.closest('.braze-perks') || document;\n\n // Update all tab states within this perks table\n perksRoot.querySelectorAll(LEVEL_TAB_SELECTOR).forEach(t => {\n t.setAttribute('aria-selected', 'false');\n t.setAttribute('tabindex', '-1');\n t.setAttribute('data-active', 'false');\n });\n\n tab.setAttribute('aria-selected', 'true');\n tab.setAttribute('tabindex', '0');\n tab.setAttribute('data-active', 'true');\n perksRoot.setAttribute('data-active-level', targetLevel);\n\n // Drive visibility with the HTML `hidden` attribute (values use `hidden`, not `.hidden`)\n perksRoot.querySelectorAll(LEVEL_VALUE_SELECTOR).forEach(val => {\n const isActive = val.dataset.levelValue === targetLevel;\n val.hidden = !isActive;\n val.setAttribute('data-active', isActive ? 'true' : 'false');\n });\n\n const levelDisplay = perksRoot.querySelector(LEVEL_DISPLAY_SELECTOR);\n if (levelDisplay) {\n levelDisplay.textContent = tab.textContent.trim();\n }\n}\n\n// Click handler for level tabs\ndocument.addEventListener('click', (e) => {\n const levelTab = e.target.closest(LEVEL_TAB_SELECTOR);\n if (!levelTab) return;\n activateLevelTab(levelTab);\n});\n\n// Keyboard navigation for level tabs\ndocument.addEventListener('keydown', (e) => {\n const levelTab = e.target.closest(LEVEL_TAB_SELECTOR);\n if (!levelTab) return;\n \n const levelTabs = Array.from(document.querySelectorAll(LEVEL_TAB_SELECTOR));\n const idx = levelTabs.indexOf(levelTab);\n if (idx === -1) return;\n \n let next;\n if (e.key === 'ArrowRight') {\n next = levelTabs[(idx + 1) % levelTabs.length];\n } else if (e.key === 'ArrowLeft') {\n next = levelTabs[(idx - 1 + levelTabs.length) % levelTabs.length];\n } else if (e.key === 'Home') {\n next = levelTabs[0];\n } else if (e.key === 'End') {\n next = levelTabs[levelTabs.length - 1];\n }\n \n if (next) {\n e.preventDefault();\n next.focus();\n activateLevelTab(next);\n }\n});\n\n// ========================================\n// FAQ Tab - Accordion Toggle\n// ========================================\n\nconst FAQ_TOGGLE_SELECTOR = '[data-faq-toggle]';\nconst FAQ_CONTENT_SELECTOR = '[data-faq-content]';\n\n/**\n * Toggles FAQ accordion item open/closed.\n * @param {HTMLElement} button\n */\nfunction toggleFaqItem(button) {\n const isExpanded = button.getAttribute('aria-expanded') === 'true';\n const content = button.nextElementSibling;\n \n if (!content || !content.matches(FAQ_CONTENT_SELECTOR)) return;\n \n button.setAttribute('aria-expanded', String(!isExpanded));\n content.hidden = isExpanded;\n}\n\n// Click handler for FAQ toggles\ndocument.addEventListener('click', (e) => {\n const faqToggle = e.target.closest(FAQ_TOGGLE_SELECTOR);\n if (!faqToggle) return;\n toggleFaqItem(faqToggle);\n});\n\n// Keyboard handler for FAQ toggles (Enter/Space)\ndocument.addEventListener('keydown', (e) => {\n if (e.key !== 'Enter' && e.key !== ' ') return;\n \n const faqToggle = e.target.closest(FAQ_TOGGLE_SELECTOR);\n if (!faqToggle) return;\n \n e.preventDefault();\n toggleFaqItem(faqToggle);\n});\n\n\n// ========================================\n// Logged-In User Experience\n// ========================================\n\nconst LOGGED_IN_TAB_SELECTOR = '[data-logged-in-tab]';\nconst LOGGED_IN_PANEL_SELECTOR = '[data-logged-in-panel]';\nconst REWARDS_PROFILE_SELECTOR = '[data-rewards-profile]';\nconst REWARDS_OFFERS_SELECTOR = '[data-rewards-offers]';\n\n/**\n * Activates a logged-in tab and shows its panel.\n * @param {HTMLElement} tabToActivate\n */\nfunction activateLoggedInTab(tabToActivate) {\n const tabs = document.querySelectorAll(LOGGED_IN_TAB_SELECTOR);\n const panels = document.querySelectorAll(LOGGED_IN_PANEL_SELECTOR);\n const targetId = tabToActivate.dataset.loggedInTab;
1\n\n tabs.forEach((tab) => {\n const isActive = tab === tabToActivate;\n tab.classList.toggle('braze-inbox__tab--active', isActive);\n tab.setAttribute('aria-selected', String(isActive));\n tab.setAttribute('tabindex', isActive ? '0' : '-1');\n });\n\n panels.forEach((panel) => {\n const shouldShow = panel.dataset.loggedInPanel === targetId;\n panel.hidden = !shouldShow;\n panel.classList.toggle('hidden', !shouldShow);\n });\n\n tabToActivate.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' });\n}\n\ndocument.addEventListener('click', (e) => {\n const tab = e.target.closest(LOGGED_IN_TAB_SELECTOR);\n if (tab) {\n e.preventDefault();\n activateLoggedInTab(tab);\n }\n});\n\ndocument.addEventListener('keydown', (e) => {\n const tab = e.target.closest(LOGGED_IN_TAB_SELECTOR);\n if (!tab) return;\n\n const tabs = Array.from(document.querySelectorAll(LOGGED_IN_TAB_SELECTOR));\n const currentIndex = tabs.indexOf(tab);\n let nextIndex;\n\n switch (e.key) {\n case 'ArrowLeft':\n nextIndex = currentIndex > 0 ? currentIndex - 1 : tabs.length - 1;\n tabs[nextIndex].focus();\n activateLoggedInTab(tabs[nextIndex]);\n e.preventDefault();\n break;\n case 'ArrowRight':\n nextIndex = currentIndex < tabs.length - 1 ? currentIndex + 1 : 0;\n tabs[nextIndex].focus();\n activateLoggedInTab(tabs[nextIndex]);\n e.preventDefault();\n break;\n case 'Home':\n tabs[0].focus();\n activateLoggedInTab(tabs[0]);\n e.preventDefault();\n break;\n case 'End':\n tabs[tabs.length - 1].focus();\n activateLoggedInTab(tabs[tabs.length - 1]);\n e.preventDefault();\n break;\n }\n});\n\n(0,_braze_inbox_offers_js__WEBPACK_IMPORTED_MODULE_1__.initOfferDetailsListeners)();\n(0,_braze_inbox_history_js__WEBPACK_IMPORTED_MODULE_3__.initHistoryFilterListeners)();\n\ndocument.addEventListener('DOMContentLoaded', () => {\n const profileContainer = document.querySelector(REWARDS_PROFILE_SELECTOR);\n const offersContainer = document.querySelector(REWARDS_OFFERS_SELECTOR);\n const waysToEarnContainer = document.querySelector(_braze_inbox_ways_to_earn_js__WEBPACK_IMPORTED_MODULE_2__.WAYS_TO_EARN_SELECTOR);\n const historyContainer = document.querySelector(_braze_inbox_history_js__WEBPACK_IMPORTED_MODULE_3__.HISTORY_SELECTOR);\n\n if (!profileContainer || !offersContainer) return;\n\n const customerId = profileContainer.dataset.customerId;\n const locale = profileContainer.dataset.locale || 'en';\n if (!customerId) return;\n\n profileContainer.setAttribute('data-state', 'loading');\n const profileLoading = profileContainer.querySelector('[data-profile-state=\"loading\"]');\n if (profileLoading) profileLoading.hidden = false;\n\n offersContainer.setAttribute('data-state', 'loading');\n const offersLoading = offersContainer.querySelector('[data-offers-state=\"loading\"]');\n if (offersLoading) offersLoading.hidden = false;\n\n if (waysToEarnContainer) {\n waysToEarnContainer.setAttribute('data-state', 'loading');\n const wLoading = waysToEarnContainer.querySelector('[data-ways-to-earn-state=\"loading\"]');\n if (wLoading) wLoading.hidden = false;\n }\n\n if (historyContainer) {\n historyContainer.setAttribute('data-state', 'loading');\n historyContainer.querySelectorAll('[data-history-state=\"loading\"]').forEach((el) => {\n el.hidden = false;\n });\n }\n\n (0,_braze_inbox_ways_to_earn_js__WEBPACK_IMPORTED_MODULE_2__.fetchRewardsData)(customerId, locale).then((data) => {\n if (data) {\n (0,_braze_inbox_profile_js__WEBPACK_IMPORTED_MODULE_0__.renderProfile)(profileContainer, data, locale);\n (0,_braze_inbox_offers_js__WEBPACK_IMPORTED_MODULE_1__.renderOffers)(offersContainer, data, locale);\n if (waysToEarnContainer) (0,_braze_inbox_ways_to_earn_js__WEBPACK_IMPORTED_MODULE_2__.renderWaysToEarn)(waysToEarnContainer, data, locale);\n if (historyContainer) (0,_braze_inbox_history_js__WEBPACK_IMPORTED_MODULE_3__.renderHistory)(historyContainer, data, locale);\n return;\n }\n\n profileContainer.setAttribute('data-state', 'error');\n offersContainer.setAttribute('data-state', 'error');\n if (waysToEarnContainer) waysToEarnContainer.setAttribute('data-state', 'error');\n if (historyContainer) historyContainer.setAttribute('data-state', 'error');\n });\n});\n\n\n//# sourceURL=webpack://ardene-theme/./src/snippets/snippet.braze-inbox.js?\n}")}
1},__webpack_module_cache__={};function __webpack_require__(moduleId){var cachedModule=__webpack_module_cache__[moduleId];if(cachedModule!==void 0)return cachedModule.exports;var module=__webpack_module_cache__[moduleId]={exports:{}};return __webpack_modules__[moduleId](module,module.exports,__webpack_require__),module.exports}(function(){__webpack_require__.d=function(exports,definition){for(var key in definition)__webpack_require__.o(definition,key)&&!__webpack_require__.o(exports,key)&&Object.defineProperty(exports,key,{enumerable:!0,get:definition[key]})}})(),(function(){__webpack_require__.o=function(obj,prop){return Object.prototype.hasOwnProperty.call(obj,prop)}})(),(function(){__webpack_require__.r=function(exports){typeof Symbol!="undefined"&&Symbol.toStringTag&&Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(exports,"__esModule",{value:!0})}})(),__webpack_require__("./src/snippets/snippet.braze-inbox.js");var __webpack_exports__=__webpack_require__("./src/snippets/snippet.braze-inbox.css")})(); 2//# sourceMappingURL=/cdn/shop/t/239/assets/snippet.braze-inbox.js.map?v=70479139155257703581790728904
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.