Shopping

The headphones I use daily are 50% off for Prime Day — and they're not Apple, Beats, or Bose

The headphones I use daily are 50% off for Prime Day — and they're not Apple, Beats, or Bose
Image: tomsguide.com

Prime Day is officially in full swing, and if you’ve been holding out for a pair of premium headphones that truly justify their reputation, the Sennheiser Momentum 4's deserve a spot in your cart.

These are my absolute go-to daily headphones. Whether I'm blocking out outside chatter, surviving a long-haul flight, or just getting lost in a favorite album, they consistently deliver unmatched audio clarity and noise cancellation.

Right now, Amazon has slashed the price to just $179, knocking over $100 off. Having put these through their paces every single day for years, I can confidently say they easily outperform everything else I own. If you want elite sound without the elite hefty price tag, don't sleep on this deal.

These Sennheiser headphones are down to $179 for Prime Day. Between the huge battery life, great sound, and solid noise cancellation, I end up wearing these pretty much all day, every day. If you’ve been waiting for a price drop, now is the time to grab them.View Deal

There's a few reasons why these cans earned a 4.5-star rating in our Sennheiser Momentum 4 Wireless review.

First and foremost, the sound quality is what made me a huge fan. Music feels genuinely alive on the Momentum 4s, with a depth and clarity that makes you start noticing things you'd missed in songs you've listened to a hundred times. Podcasts, playlists, films — everything just sounds better than it has any right to at this price point.

Comfort is the other thing that keeps coming up when I wear these. The earcups are well padded, the fit is light, and I can go hours without thinking about the fact that I'm wearing headphones at all. For long commutes or full work days, this matters.

The noise cancelling does its job without the uncomfortable pressure you get from some ANC headphones, which is a more common complaint than brands like to admit. Transparency mode is a quick tap away when you need to tune back in to the world around you, and it's seamless enough that switching between the two becomes second nature.

Sennheiser Momentum 4 headphones

(Image credit: Tom's Guide)

You also get an amazing 60 hours on a single charge, meaning I only plug mine in about once a week, even though I use them every single day. The Momentum 4 Wireless are also great for calls. Whether I’m working in a noisy coffee shop or out on a windy walk, the mic keeps my voice clear and blocks out the background chaos.

If you just want a pair of headphones that work perfectly without any fuss, these are it. I didn’t expect to get this attached to them, but I really can't see myself switching to anything else.

At $179 for Prime Day, they're still an investment, but they are an absolute steal for the quality you get. They might not get the same mainstream buzz as Apple or Beats, but they absolutely should.

/** * Tom's Guide Deals Finder - Vanilla JS Encapsulated Engine */ (function() { // --- Freyr Analytics Adapter --- function initAnalytics() { window.dataLayer = window.dataLayer || []; window.googletag = window.googletag || {}; window.googletag.cmd = window.googletag.cmd || []; window.hawk = window.hawk || { analytics: { freyr: [] } }; window.hawk.analytics = window.hawk.analytics || { freyr: [] }; window.hawk.analytics.freyr = window.hawk.analytics.freyr || []; window.freyr = window.freyr || { cmd: [] }; const scriptSrc = 'https://freyr.futurecdn.net/freyr.js'; const hostname = typeof window !== 'undefined' ? window.location.hostname : ''; const isTestEnv = typeof window.navigator !== 'undefined' && (window.navigator.webdriver || window.navigator.userAgent.includes('Headless')); const shouldSendRealAnalytics = !isTestEnv && hostname && hostname !== 'localhost' && hostname !== '127.0.0.1' && !hostname.includes('run.app'); if (shouldSendRealAnalytics && !document.querySelector(`script[src="${scriptSrc}"]`)) { const script = document.createElement('script'); script.src = scriptSrc; script.async = true; document.head.appendChild(script); } } function storeEventForDebug(name, data) { if (!window.hawk || !window.hawk.analytics || !window.hawk.analytics.freyr) return; window.hawk.analytics.freyr.push({ name, data }); try { if (typeof window !== 'undefined' && window.localStorage) { window.localStorage.setItem("hawk", JSON.stringify(window.hawk)); } } catch (e) { // Ignore storage issues } try { window.dispatchEvent(new CustomEvent("hawk-analytics-update")); } catch (e) {} } function sendToFreyr(eventName, data) { if (typeof window === 'undefined') return; window.freyr = window.freyr || { cmd: [] }; window.freyr.cmd.push(() => { if (window.freyr && window.freyr.pushAndUpdate) { window.freyr.pushAndUpdate(eventName, data); } }); } function sendEvent(event, skip = false) { try { storeEventForDebug(event.name, event.data); if (!skip) { sendToFreyr(event.name, event.data); } } catch (e) { // Ensure tracking errors don't surface to the user } } function getCookie(name) { try { const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)')); return match ? match[2] : null; } catch (e) { return null; } } function getTimeAgo(dateString) { if (!dateString) return ''; const date = new Date(dateString); const now = new Date(); const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000); if (diffInSeconds < 60) return 'Just now'; const diffInMinutes = Math.floor(diffInSeconds / 60); if (diffInMinutes 1 ? 's' : ''} ago`; const diffInHours = Math.floor(diffInMinutes / 60); if (diffInHours 1 ? 's' : ''} ago`; const diffInDays = Math.floor(diffInHours / 24); if (diffInDays 1 ? 's' : ''} ago`; const diffInMonths = Math.floor(diffInDays / 30); if (diffInMonths 1 ? 's' : ''} ago`; const diffInYears = Math.floor(diffInDays / 365); return `${diffInYears} yr${diffInYears > 1 ? 's' : ''} ago`; } function normalizeCurrency(symbol) { const map = { '£': 'GBP', '$': 'USD', 'A$': 'AUD', 'CA$': 'CAD', '€': 'EUR' }; return map[symbol] || symbol; } function trackElementInteraction(props) { sendEvent({ name: 'elementInteraction', data: { element: { action: props.action || "click", id: props.id || undefined, class: props.class || undefined, name: props.name || undefined, text: props.text || undefined, label: props.label || undefined, container: props.container || undefined, url: props.url || undefined, articleId: props.articleId || undefined } } }); } function generateRevenueId(url, productName, merchantName, modelId) { const str = `${window.location.href}|${productName}|${merchantName}|${modelId || ''}|${new Date().toDateString()}|tomsguide`; let hash = 0; for (let i = 0; i < str.length; i++) { const char = str.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; } let numericStr = Math.abs(hash).toString(); while (numericStr.length { let voucherPct = null; let rawPrice = parseFloat(deal.rawPrice) || parseFloat(deal.price) || null; let rawMsrp = parseFloat(deal.rawMsrp) || parseFloat(deal.msrp) || null; if (rawMsrp > rawPrice && rawPrice > 0) { voucherPct = Math.round((1 - (rawPrice / rawMsrp)) * 100); } let numId = null; if (deal.externalProductId && !isNaN(parseInt(deal.externalProductId))) { numId = parseInt(deal.externalProductId); } else if (deal.id && !isNaN(parseInt(deal.id))) { numId = parseInt(deal.id); } else { numId = deal.matchId || null; } return { product: { primary: { id: numId, name: deal.productName || deal.title || "", type: "deal", price: rawPrice, previousPrice: rawMsrp, currency: currency || 'USD', preorder: false, labels: deal.modelBrand || deal.brand ? [ { type: "brand", value: deal.modelBrand || deal.brand } ] : [], link: deal.url, originalLink: deal.url, revenueId: revenueId || null, startTime: null, endTime: null, voucherCode: null, voucherAudience: null, voucherPercentageSaving: voucherPct, voucherMoneySaving: null, voucherType: null, offerExclusive: false, offerScope: null, globalId: deal.productKey || null, inStock: deal.inStock !== false, contractProvider: null, contractMinutes: null, contractTexts: null, contractData: null, contractLength: null, contractMonthlyPrice: null, contractCurrency: currency || 'USD' } }, merchant: { id: deal.merchantId ? parseInt(deal.merchantId) : null, name: deal.merchant || "Retailer", url: deal.merchantUrl || null, network: deal.merchantNetwork || null }, model: { id: deal.modelId ? parseInt(deal.modelId) : null, brand: deal.modelBrand || deal.brand || null, name: deal.productName || deal.title || "", parent: deal.modelParent || null } }; }); trackHawkEvent({ clickType: "appeared", widgetId: widgetId, productCategoryName: "deals", zeroBasedProductIndexOrNull: null, totalDealsOrProducts: deals.length, productsArray: productsArray, queryName: queryName, widgetTypeName: widgetTypeName }); } // 1. Setup Shadow DOM Sandbox const currentScript = document.currentScript; let hostContainer = null; let template = null; if (currentScript) { let prev = currentScript.previousElementSibling; while (prev) { if (prev.tagName === 'TEMPLATE' && prev.classList.contains('tg-df-widget-template')) { template = prev; } else if (prev.tagName === 'DIV' && prev.classList.contains('tg-df-widget-host') && !prev.hasAttribute('data-initialized')) { hostContainer = prev; break; } prev = prev.previousElementSibling; } } // Fallbacks in case script is deferred if (!hostContainer) { const hosts = document.querySelectorAll('.tg-df-widget-host:not([data-initialized])'); if (hosts.length > 0) hostContainer = hosts[0]; } // Safely embedded template for CMS environments const rawTemplate = ` \x3Cstyle> /* --- Shadow DOM Base Reset --- */ *, *::before, *::after { box-sizing: border-box; } img, picture, svg, video { max-width: 100%; height: auto; display: block; } /* 1. Scoped CSS for Tom's Guide Deals Widget All classes are prefixed with \`tg-df-\` to prevent CMS style leakage. */ .tg-df-container { container-type: inline-size; container-name: tg-df; --tg-df-blue: #1F69FF; --tg-df-blue-hover: #004d8c; --tg-df-text: #222222; --tg-df-text-muted: #555555; --tg-df-bg: #ffffff; --tg-df-bg-secondary: #f4f4f4; --tg-df-border: #e2e8f0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; color: var(--tg-df-text); background-color: transparent; width: 100%; max-width: 1200px; margin: 0 auto; padding-bottom: 24px; } .tg-df-container *, .tg-df-container *::before, .tg-df-container *::after { margin: 0; padding: 0; box-sizing: border-box; } .tg-df-container img { border: none; margin: 0; padding: 0; } .tg-df-container a { text-decoration: none; color: inherit; } /* 2. Search & Filter Bar */ .tg-df-controls { display: flex; flex-direction: column; align-items: center; gap: 20px; margin-bottom: 32px; width: 100%; position: relative; z-index: 20; } .tg-df-top-bar { display: flex; width: 100%; max-width: 760px; gap: 12px; margin: 0 auto; align-items: center; } .tg-df-search-wrapper { position: relative; flex: 1; width: 100%; box-shadow: 0 8px 24px rgba(0,0,0,0.06); border-radius: 40px; background: var(--tg-df-bg); border: 1px solid var(--tg-df-border); z-index: 100; } .tg-df-autocomplete-dropdown { position: absolute; top: calc(100% + 4px); left: 0; right: 0; background: var(--tg-df-bg); border: 1px solid var(--tg-df-border); border-radius: 12px; box-shadow: 0 8px 32px rgba(0,0,0,0.12); max-height: 300px; overflow-y: auto; z-index: 200; display: none; } .tg-df-autocomplete-dropdown.active { display: block; } .tg-df-autocomplete-item { padding: 12px 24px; cursor: pointer; font-size: 14px; color: var(--tg-df-text); transition: background 0.1s ease; } .tg-df-autocomplete-item:hover { background: var(--tg-df-bg-secondary); } .tg-df-search-input { width: 100%; padding: 16px 64px 16px 24px; font-size: 16px; border: 2px solid transparent; border-radius: 40px; outline: none; transition: border-color 0.2s ease, box-shadow 0.2s ease; color: var(--tg-df-text); background: transparent; } .tg-df-search-input:focus { border-color: transparent; box-shadow: 0 0 0 3px rgba(0, 108, 196, 0.15); } .tg-df-search-input::placeholder { color: #999999; } .tg-df-search-btn { position: absolute; right: 8px; top: 50%; transform: translateY(-50%); width: 40px; height: 40px; border-radius: 50%; background: #222; border: none; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: background 0.2s ease; } .tg-df-search-btn:hover { background: #000; } .tg-df-search-icon { width: 16px; height: 16px; fill: #fff; } .tg-df-settings-wrapper { position: relative; } .tg-df-settings-btn { width: 48px; height: 48px; border-radius: 50%; background: var(--tg-df-bg); border: 1px solid var(--tg-df-border); box-shadow: 0 4px 12px rgba(0,0,0,0.04); display: flex; align-items: center; justify-content: center; cursor: pointer; transition: all 0.2s ease; color: var(--tg-df-text-muted); flex-shrink: 0; } .tg-df-settings-btn:hover { background: var(--tg-df-bg-secondary); border-color: #0000ff; color: var(--tg-df-text); } .tg-df-settings-btn svg { width: 24px; height: 24px; fill: currentColor; } .tg-df-settings-dropdown { position: absolute; top: calc(100% + 8px); right: 0; background: var(--tg-df-bg); border: 1px solid var(--tg-df-border); border-radius: 12px; box-shadow: 0 8px 32px rgba(0,0,0,0.12); width: 280px; padding: 20px; display: none; z-index: 100; flex-direction: column; gap: 20px; } .tg-df-settings-dropdown.active { display: flex; } .tg-df-settings-dropdown-backdrop { display: none; position: fixed; inset: 0; z-index: 99; } .tg-df-settings-dropdown-backdrop.active { display: block; } .tg-df-setting-item { display: flex; flex-direction: column; gap: 10px; } .tg-df-setting-label { font-size: 11px; font-weight: 700; color: var(--tg-df-text-muted); text-transform: uppercase; letter-spacing: 0.5px; } .tg-df-region-select { padding: 10px 12px; border-radius: 8px; border: 1px solid var(--tg-df-border); font-size: 15px; outline: none; background: var(--tg-df-bg-secondary); color: var(--tg-df-text); cursor: pointer; width: 100%; } .tg-df-toggle { position: relative; display: inline-block; width: 44px; height: 24px; flex-shrink: 0; } .tg-df-toggle input { opacity: 0; width: 0; height: 0; } .tg-df-slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccc; transition: .2s; border-radius: 24px; } .tg-df-slider:before { position: absolute; content: ""; height: 18px; width: 18px; left: 3px; bottom: 3px; background-color: white; transition: .2s; border-radius: 50%; } .tg-df-toggle input:checked + .tg-df-slider { background-color: #1F69FF; } .tg-df-toggle input:checked + .tg-df-slider:before { transform: translateX(20px); } .tg-df-dl-row { flex-direction: row; align-items: center; justify-content: space-between; } .tg-df-dl-row-text { font-size: 14px; font-weight: 600; color: var(--tg-df-text); } .tg-df-dl-row-subtext { font-size: 12px; font-weight: 400; line-height: 1.3; color: var(--tg-df-text-muted); margin-top: 4px; display: block; } .tg-df-filters-container { position: relative; width: 100%; max-width: 800px; } .tg-df-scroll-btn { display: none; position: absolute; top: 50%; transform: translateY(-50%); width: 32px; height: 32px; background: white; border: 1px solid var(--tg-df-border); border-radius: 50%; align-items: center; justify-content: center; cursor: pointer; z-index: 10; box-shadow: 0 2px 8px rgba(0,0,0,0.1); color: var(--tg-df-text-primary); padding: 0; } .tg-df-scroll-btn svg { width: 16px; height: 16px; } .tg-df-scroll-btn:hover { background: #f4f4f4; } .tg-df-scroll-btn.left { left: 0px; } .tg-df-scroll-btn.right { right: 0px; } @container tg-df (max-width: 768px) { .tg-df-scroll-btn { display: flex; top: 22px; /* vertically center within the 44px high filter buttons */ } } .tg-df-filters { display: grid; width: 100%; grid-template-columns: repeat(4, 1fr); gap: 12px; margin: 0 auto; max-width: 800px; } .tg-df-sort-wrapper { position: relative; display: flex; align-items: center; width: 100%; } .tg-df-sort-icon { position: absolute; left: 14px; width: 14px; height: 14px; fill: var(--tg-df-text-muted); pointer-events: none; } .tg-df-sort-select, .tg-df-filter-select { width: 100%; padding: 10px 36px 10px 38px; font-size: 14px; border: 1px solid var(--tg-df-border); border-radius: 100px; outline: none; appearance: none; background-color: var(--tg-df-bg-secondary); background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 12 12'%3E%3Cpath fill='%23555555' d='M6 8L1 3h10z'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 14px center; color: var(--tg-df-text); cursor: pointer; font-weight: 500; transition: all 0.2s ease; } .tg-df-price-input::-webkit-outer-spin-button, .tg-df-price-input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; } .tg-df-price-input { -moz-appearance: textfield; } .tg-df-sort-select:hover, .tg-df-filter-select:hover { background-color: #e2e8f0; } .tg-df-multiselect-container { position: relative; } @container tg-df (max-width: 768px) { .tg-df-filters-container { position: relative; width: 100%; max-width: 800px; } .tg-df-scroll-btn { display: none; position: absolute; top: 50%; transform: translateY(-50%); width: 32px; height: 32px; background: white; border: 1px solid var(--tg-df-border); border-radius: 50%; align-items: center; justify-content: center; cursor: pointer; z-index: 10; box-shadow: 0 2px 8px rgba(0,0,0,0.1); color: var(--tg-df-text-primary); padding: 0; } .tg-df-scroll-btn svg { width: 16px; height: 16px; } .tg-df-scroll-btn:hover { background: #f4f4f4; } .tg-df-scroll-btn.left { left: 0px; } .tg-df-scroll-btn.right { right: 0px; } @container tg-df (max-width: 768px) { .tg-df-scroll-btn { display: flex; top: 22px; /* vertically center within the 44px high filter buttons */ } } .tg-df-filters { width: 100%; margin: 0; margin-bottom: -320px; padding: 0 16px 320px 16px; display: flex; flex-wrap: nowrap; gap: 8px; overflow-x: auto; overflow-y: hidden; pointer-events: none; scrollbar-width: none; -webkit-overflow-scrolling: touch; } .tg-df-filters::-webkit-scrollbar { display: none; } .tg-df-sort-wrapper { pointer-events: auto; flex: 0 0 auto; width: 175px; min-width: 175px; } } .tg-df-multiselect-trigger { display: block; background: #fff; user-select: none; width: 100%; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } .tg-df-multiselect-dropdown { display: none; position: absolute; top: calc(100% + 4px); left: 0; width: 100%; min-width: 220px; max-height: 300px; overflow-y: auto; background: var(--tg-df-bg); border: 1px solid var(--tg-df-border); border-radius: 8px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); z-index: 100; padding: 8px 0; } .tg-df-multiselect-dropdown.active { display: block; } .tg-df-ms-option { padding: 8px 16px; display: flex; align-items: center; gap: 8px; cursor: pointer; font-size: 14px; } .tg-df-ms-option:hover { background-color: var(--tg-df-bg-secondary); } .tg-df-ms-option input { cursor: pointer; accent-color: #1f69ff; } .tg-df-sort-select:focus, .tg-df-filter-select:focus { border-color: #0000ff; box-shadow: 0 0 0 3px rgba(0, 0, 255, 0.2); background-color: var(--tg-df-bg); } /* 3. Deal Grid Layout */ .tg-df-grid.tg-df-grid-auto { padding-top: 24px; } .tg-df-grid, .tg-df-grid.layout-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 10px; } .tg-df-grid.layout-row { grid-template-columns: 1fr; gap: 16px; } .tg-df-grid.layout-row .tg-df-card { flex-direction: row; align-items: stretch; height: auto; box-shadow: none; border-bottom: 1px solid var(--tg-df-border); } .tg-df-grid.layout-row .tg-df-card:hover { box-shadow: none; } .tg-df-grid.layout-row .tg-df-card-image-box { width: 140px; min-width: 140px; aspect-ratio: 3/4; border-right: none; padding: 16px 16px 16px 32px; } .tg-df-grid.layout-row .tg-df-card-body { padding: 16px; justify-content: space-between; } .tg-df-grid.layout-row .tg-df-card-title { font-size: 15px; margin-bottom: 16px; } .tg-df-grid.layout-row .tg-df-card-stars { margin-bottom: 8px; } .tg-df-grid.layout-row .tg-df-card-footer { flex-direction: column; align-items: flex-start; gap: 0; } .tg-df-grid.layout-row .tg-df-card-merchant-pill { margin-bottom: 4px; } .tg-df-grid.layout-row .tg-df-card-price-group { margin-bottom: 8px; } .tg-df-grid.layout-row .tg-df-price-group { width: auto; } .tg-df-grid.layout-row .tg-df-card-cta { width: 100%; max-width: 200px; padding: 10px 24px; font-size: 13px; flex-shrink: 0; text-align: center; justify-content: center; } /* 4. Deal Card Design */ .tg-df-card { position: relative; display: flex; flex-direction: column; background-color: #ffffff; border-radius: 0; overflow: hidden; transition: transform 0.2s ease, box-shadow 0.2s ease; text-decoration: none; color: inherit; height: 100%; box-shadow: 0 0 16px rgba(0, 0, 0, 0.08); border: 1px solid var(--tg-df-border); } .tg-df-card:hover { box-shadow: 0 0 24px rgba(0, 0, 0, 0.12); } .tg-df-card-image-box { width: 100%; aspect-ratio: 3/4; background-color: #f8f8f8; display: flex; align-items: center; justify-content: center; position: relative; overflow: hidden; padding: 32px; flex: 0 0 auto; } .tg-df-card-image { max-width: 100%; max-height: 100%; width: auto; height: auto; object-fit: contain; mix-blend-mode: multiply; /* Helps white background images blend into secondary bg */ transition: transform 0.3s ease; } .tg-df-card:hover .tg-df-card-image { transform: scale(1.05); /* Zoom in on hover */ } .tg-df-card-discount-badge { position: absolute; top: 12px; left: 12px; background: #dc2626; /* Red */ color: #ffffff; padding: 6px 8px; font-size: 11px; font-weight: 500; text-transform: uppercase; letter-spacing: 0.5px; border-radius: 0; z-index: 10; } .tg-df-card-merchant-pill { display: block; padding: 0; font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; border-radius: 0; color: var(--tg-df-text-muted); margin-bottom: 8px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .tg-df-card-body { padding: 16px; display: flex; flex-direction: column; flex-grow: 1; min-width: 0; } .tg-df-card-badges { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 8px; } .tg-df-tag { display: inline-flex; align-items: center; padding: 4px 6px; font-size: 11px; font-weight: 700; text-transform: uppercase; border-radius: 4px; gap: 4px; } .tg-df-tag-prime { background-color: #00A8E1; color: #fff; } .tg-df-tag-coupons { background-color: #f1f5f9; color: #334155; border: 1px solid #cbd5e1; cursor: pointer; transition: background-color 0.2s; } .tg-df-tag-coupons:hover { background-color: #e2e8f0; } .tg-df-tag-outline { background-color: #f1f5f9; color: #334155; border: 1px solid #cbd5e1; cursor: pointer; transition: background-color 0.2s; } .tg-df-tag-outline:hover { background-color: #e2e8f0; } @keyframes tg-df-spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } .tg-df-coupon-spinner { border: 2px solid #e2e8f0; border-top: 2px solid #3b82f6; border-radius: 50%; width: 14px; height: 14px; animation: tg-df-spin 1s linear infinite; margin: 4px 8px; display: inline-block; } /* Vouchers Modal */ .tg-df-modal-backdrop { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); z-index: 10000; display: flex; align-items: center; justify-content: center; opacity: 0; pointer-events: none; transition: opacity 0.3s; } .tg-df-modal-backdrop.active { opacity: 1; pointer-events: auto; } .tg-df-modal { background: #fff; border-radius: 12px; width: 90%; max-width: 400px; max-height: 80vh; display: flex; flex-direction: column; box-shadow: 0 10px 40px rgba(0,0,0,0.2); transform: translateY(20px); transition: transform 0.3s; } .tg-df-modal-backdrop.active .tg-df-modal { transform: translateY(0); } .tg-df-modal-header { padding: 16px; border-bottom: 1px solid #e2e8f0; display: flex; align-items: center; justify-content: space-between; } .tg-df-modal-title { font-size: 16px; font-weight: 600; margin: 0; } .tg-df-modal-close { background: none; border: none; cursor: pointer; padding: 4px; color: #64748b; } .tg-df-modal-body { padding: 16px; overflow-y: auto; } .tg-df-voucher-item { padding: 12px; border: 1px dashed #cbd5e1; border-radius: 8px; margin-bottom: 10px; background: #f8fafc; display: flex; align-items: center; gap: 12px; text-decoration: none; color: inherit; transition: background-color 0.2s, border-color 0.2s; } .tg-df-voucher-item:hover { background: #f1f5f9; border-color: #94a3b8; } .tg-df-voucher-item:last-child { margin-bottom: 0; } .tg-df-voucher-logo { width: 48px; height: 48px; object-fit: contain; border-radius: 4px; background: #fff; border: 1px solid #e2e8f0; flex-shrink: 0; } .tg-df-voucher-content { flex: 1; min-width: 0; } .tg-df-voucher-title { font-size: 14px; font-weight: 600; margin: 0 0 4px 0; line-height: 1.3; color: #0f172a; } .tg-df-voucher-expiry { font-size: 12px; color: #64748b; display: flex; align-items: center; gap: 4px; margin-top: 6px; } .tg-df-voucher-code { display: inline-flex; align-items: center; background: #f1f5f9; border: 1px dashed #cbd5e1; padding: 6px 10px; font-family: monospace; font-weight: 700; font-size: 14px; color: #0f172a; border-radius: 4px; margin-top: 8px; cursor: pointer; transition: all 0.2s ease; } .tg-df-voucher-code:hover { background: #e2e8f0; border-color: #94a3b8; } .tg-df-voucher-code.copied { background: #ecfdf5; border-color: #10b981; color: #10b981; } .tg-df-voucher-cta { display: inline-block; margin-top: 8px; font-size: 13px; font-weight: 600; color: #2563eb; text-decoration: none; } .tg-df-card-title { font-size: 15px; font-weight: 400; line-height: 1.4; margin: 0 0 12px 0; color: var(--tg-df-text); } .tg-df-card-footer { margin-top: auto; display: flex; flex-direction: column; width: 100%; } .tg-df-card-price-group { display: flex; flex-direction: row; align-items: center; gap: 8px; margin-bottom: 12px; } .tg-df-card-price { font-size: 16px; font-weight: 700; color: #dc2626; /* Red price */ line-height: 1; } .tg-df-card-msrp { font-size: 13px; color: var(--tg-df-text-muted); text-decoration: line-through; } .tg-df-container .tg-df-card-cta { display: flex; align-items: center; justify-content: center; width: 100%; box-sizing: border-box; background-color: #1f69ff; color: #ffffff; font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; padding: 12px 16px; border-radius: 0; border: none; cursor: pointer; transition: background-color 0.2s ease; } .tg-df-card:hover .tg-df-card-cta, .tg-df-card-cta:hover { background-color: #1555cc; } /* 5. State & Skeleton Styles */ .tg-df-message { grid-column: 1 / -1; text-align: center; padding: 48px 24px; color: var(--tg-df-text-muted); font-size: 16px; background: var(--tg-df-bg); border: 1px solid var(--tg-df-border); border-radius: 8px; } @keyframes tg-df-shimmer { 0% { background-position: -200% 0; } 100% { background-position: 200% 0; } } .tg-df-skeleton { background: linear-gradient(90deg, var(--tg-df-bg-secondary) 25%, #e2e8f0 50%, var(--tg-df-bg-secondary) 75%); background-size: 200% 100%; animation: tg-df-shimmer 1.5s infinite; border-radius: 4px; } .tg-df-skeleton-img { width: 100%; height: 100%; position: absolute; top: 0; left: 0; } .tg-df-skeleton-text { height: 16px; margin-bottom: 8px; width: 100%; } .tg-df-skeleton-text.short { width: 40%; } .tg-df-skeleton-text.title { height: 20px; margin-bottom: 16px; } /* Editor Floating Bar & Elements */ .tg-df-editor-bar { position: sticky; top: 120px; z-index: 1000; background: #111827; color: #fff; padding: 12px 16px; border-radius: 8px; margin-bottom: 16px; display: flex; align-items: center; justify-content: space-between; box-shadow: 0 4px 12px rgba(0,0,0,0.15); } .tg-df-editor-bar-text { font-weight: 600; font-size: 14px; } .tg-df-editor-copy-btn { background: #10b981; color: #fff; padding: 6px 16px; border: none; border-radius: 4px; font-weight: 600; cursor: pointer; display: flex; align-items: center; font-size: 13px; } .tg-df-editor-copy-btn:hover { background: #059669; } .tg-df-deal-checkbox { position: absolute; top: 12px; right: 12px; z-index: 10; width: 20px; height: 20px; cursor: pointer; pointer-events: auto; } /* 6. Mobile List View (Stacks into a cleaner horizontal row/list) */ @container tg-df (max-width: 599px) { .tg-df-controls { padding: 16px 16px 8px; } .tg-df-top-bar { width: 100%; } .tg-df-settings-dropdown { position: fixed; top: auto; bottom: 0; left: 0; right: 0; width: 100%; border-radius: 20px 20px 0 0; padding: 24px; box-shadow: 0 -8px 32px rgba(0,0,0,0.15); z-index: 1000; border: none; border-top: 1px solid var(--tg-df-border); } .tg-df-settings-dropdown-backdrop.active { background: rgba(0,0,0,0.4); } .tg-df-search-wrapper { box-shadow: 0 0 16px rgba(0,0,0,0.08); } .tg-df-sort-wrapper.tg-df-price-range-wrapper { flex: 0 0 auto; min-width: max-content; width: auto; } .tg-df-sort-select, .tg-df-filter-select { width: 100%; text-align: left; padding: 10px 24px 10px 32px; background-position: right 8px center; text-overflow: ellipsis; white-space: nowrap; overflow: hidden; } .tg-df-sort-icon { left: 10px; } .tg-df-grid:not(.layout-grid):not(.layout-row), .tg-df-grid.layout-row { grid-template-columns: 1fr; gap: 16px; } .tg-df-grid.tg-df-grid-auto { padding-top: 24px; } .tg-df-grid.tg-df-grid-auto .tg-df-card, .tg-df-grid.layout-row .tg-df-card { flex-direction: row; align-items: stretch; height: auto; box-shadow: none; /* simple line on mobile if preferred, or keep */ border-bottom: 1px solid var(--tg-df-border); } .tg-df-grid.tg-df-grid-auto .tg-df-card:hover, .tg-df-grid.layout-row .tg-df-card:hover { box-shadow: none; } .tg-df-grid.tg-df-grid-auto .tg-df-card-image-box, .tg-df-grid.layout-row .tg-df-card-image-box { width: 120px; min-width: 120px; aspect-ratio: 3/4; border-right: none; padding: 12px; } .tg-df-grid.tg-df-grid-auto .tg-df-card-body, .tg-df-grid.layout-row .tg-df-card-body { padding: 12px; justify-content: space-between; } .tg-df-grid.tg-df-grid-auto .tg-df-card-title, .tg-df-grid.layout-row .tg-df-card-title { font-size: 14px; margin-bottom: 12px; } /* Single column mobile grid override */ .tg-df-grid.layout-grid { grid-template-columns: 1fr; gap: 16px; } .tg-df-grid.layout-grid .tg-df-card-image-box { padding: 12px; } .tg-df-grid.layout-grid .tg-df-card-body { padding: 10px; } .tg-df-grid.layout-grid .tg-df-card-title { font-size: 13px; margin-bottom: 8px; } .tg-df-grid.layout-grid .tg-df-card-price { font-size: 14px; } .tg-df-card-footer { flex-direction: column; align-items: stretch; gap: 0; width: 100%; min-width: 0; } .tg-df-card-merchant-pill { margin-bottom: 4px; } .tg-df-card-price-group { flex: 1 1 auto; margin-bottom: 8px; } .tg-df-card-price { font-size: 16px; } .tg-df-card-msrp { display: block; } .tg-df-grid.layout-row .tg-df-card-cta, .tg-df-container .tg-df-card-cta { width: 100%; max-width: none; min-width: 0; box-sizing: border-box; padding: 8px 16px; font-size: 12px; flex: 0 0 auto; text-align: center; white-space: normal; line-height: 1.2; } } .tg-df-container.is-carousel { min-height: 760px; background-color: #E7F0FF; padding: 0 0 24px 0; border-radius: 24px; width: 100vw; max-width: 1200px; position: relative; left: 50%; transform: translateX(-50%); } .tg-df-container.is-carousel.hide-header-details { min-height: 480px; } /* 7. Carousel View Mode */ .tg-df-container .tg-df-carousel-host { /* Layout is now handled by container wrapper */ } .tg-df-container .tg-df-carousel-eyebrow { color: #1F69FF; font-weight: 700; font-size: 14px; text-transform: uppercase; letter-spacing: 1px; padding: 24px 16px 0 16px; display: none; } .tg-df-container .tg-df-carousel-query-title { color: #011535; font-size: 28px; font-weight: 600; padding: 0 16px 24px 16px; line-height: 1.2; display: none; } .tg-df-container .tg-df-carousel-blue-box { background-color: transparent; border-radius: 0; padding: 24px 24px 0 24px; margin: 0; color: #1F69FF; position: relative; overflow: hidden; } .tg-df-container .tg-df-carousel-bg-circle-1 { display: none; } .tg-df-container .tg-df-carousel-bg-circle-2 { display: none; } .tg-df-container .tg-df-carousel-bg-circle-3 { display: none; } .tg-df-container .tg-df-carousel-box-content { position: relative; z-index: 10; } .tg-df-container .tg-df-carousel-box-eyebrow { background-color: transparent; color: #1F69FF; font-weight: 700; font-size: 14px; text-transform: uppercase; letter-spacing: 1px; display: inline-block; padding: 0; border-radius: 0; } .tg-df-container .tg-df-carousel-box-title { font-size: 28px; font-weight: 600; line-height: 1.2; margin-top: 8px; color: #1e293b; } .tg-df-container .tg-df-countdown-wrapper { position: absolute; top: 0; right: 0; display: flex; flex-direction: column; align-items: flex-end; gap: 12px; transform: scale(0.67); transform-origin: top right; } .tg-df-container .tg-df-countdown-title { font-size: 14px; font-weight: 600; color: #011535; margin: 0; } .tg-df-container .tg-df-countdown-blocks { display: flex; gap: 16px; } .tg-df-container .tg-df-countdown-item { display: flex; flex-direction: column; align-items: center; gap: 4px; } .tg-df-container .tg-df-countdown-box { width: 59px; height: 59px; background: #03FE9E; border-radius: 15px; display: flex; align-items: center; justify-content: center; } .tg-df-container .tg-df-countdown-num { font-family: 'Inter', sans-serif; font-weight: 700; font-size: 20px; line-height: normal; color: #011535; } .tg-df-container .tg-df-countdown-label { font-family: 'Inter', sans-serif; font-weight: 500; font-size: 16px; line-height: normal; color: #1e293b; text-transform: uppercase; } .tg-df-container .tg-df-carousel-box-subtitle { font-size: 16px; margin-top: 8px; font-weight: 300; color: #1e293b; line-height: 24px; } .tg-df-container .tg-df-carousel-roundels-wrapper { position: relative; margin-top: 24px; margin-left: -24px; margin-right: -24px; } .tg-df-container .tg-df-carousel-roundels { display: flex; gap: 16px; overflow-x: auto; scrollbar-width: none; padding-top: 12px; padding-bottom: 24px; padding-left: 24px; padding-right: 24px; margin-left: 0; margin-right: 0; } .tg-df-container .tg-df-carousel-scroll-left, .tg-df-container .tg-df-carousel-scroll-right { position: absolute; top: 50%; transform: translateY(-50%); height: 36px; width: 36px; display: flex; align-items: center; justify-content: center; border-radius: 50%; background-color: #ffffff; border: 1px solid #e2e8f0; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); color: #1F69FF; cursor: pointer; transition: all 0.2s; margin-top: -4px; z-index: 20; } .tg-df-container .tg-df-carousel-scroll-left { left: 8px; } .tg-df-container .tg-df-carousel-scroll-right { right: 8px; } .tg-df-carousel-filters-outer .tg-df-carousel-scroll-left { left: 0px; } .tg-df-carousel-filters-outer .tg-df-carousel-scroll-right { right: 0px; } .tg-df-carousel-filters-outer { margin-left: -24px; margin-right: -24px; padding-left: 24px; padding-right: 24px; } .tg-df-grid-wrapper { position: relative; } @container tg-df (max-width: 599px) { .tg-df-carousel-filters-outer { margin-left: -16px; margin-right: -16px; padding-left: 16px; padding-right: 16px; } } .tg-df-container .tg-df-carousel-scroll-left:hover, .tg-df-container .tg-df-carousel-scroll-right:hover { background-color: rgba(255, 255, 255, 0.6); } .tg-df-container .tg-df-carousel-roundels::-webkit-scrollbar { display: none; } .tg-df-container .tg-df-roundel { display: flex; flex-direction: column; align-items: center; gap: 8px; cursor: pointer; min-width: 120px; flex-shrink: 0; } .tg-df-container .tg-df-roundel-img-box { width: 120px; height: 120px; border-radius: 50%; background: white; display: flex; align-items: center; justify-content: center; overflow: hidden; box-shadow: 0px 3px 14px 0px rgba(30, 41, 59, 0.08); transition: box-shadow 0.2s; } .tg-df-container .tg-df-roundel:hover .tg-df-roundel-img-box { box-shadow: 0 0 0 2px #E7F0FF, 0 0 0 4px #1F69FF; } .tg-df-container .tg-df-roundel.active .tg-df-roundel-img-box { box-shadow: 0 0 0 2px #E7F0FF, 0 0 0 4px #1F69FF; } .tg-df-container .tg-df-roundel:hover .tg-df-roundel-img-box img { transform: scale(1.08); } .tg-df-container .tg-df-roundel-img-box img { width: 100%; height: 100%; object-fit: contain; padding: 10px; box-sizing: border-box; transition: transform 0.3s ease; } .tg-df-container .tg-df-roundel-label { font-size: 13px; font-weight: 400; color: #1e293b; text-align: center; } .tg-df-container .tg-df-carousel-filters-label { font-size: 16px; font-weight: 400; color: #1e293b; white-space: nowrap; margin-right: 4px; } .tg-df-container .tg-df-carousel-filters-wrap { display: flex; align-items: center; flex-wrap: nowrap; gap: 8px; margin-top: 8px; overflow-x: auto; scrollbar-width: none; -webkit-overflow-scrolling: touch; padding-bottom: 8px; margin-left: -24px; margin-right: -24px; padding-left: 24px; padding-right: 24px; } .tg-df-container .tg-df-carousel-filters-wrap::-webkit-scrollbar { display: none; } .tg-df-container .tg-df-carousel-filter-btn img, .tg-df-container .tg-df-carousel-filter-btn picture { height: 20px; width: 20px; object-fit: contain; object-position: center; display: inline-flex; align-items: center; justify-content: center; margin-right: 6px; } .tg-df-container .tg-df-carousel-filter-btn picture img { margin-right: 0; height: 100%; width: 100%; } .tg-df-container .tg-df-carousel-filter-btn img.active-img, .tg-df-container .tg-df-carousel-filter-btn picture:has(.active-img) { display: none; } .tg-df-container .tg-df-carousel-filter-btn:hover img.inactive-img, .tg-df-container .tg-df-carousel-filter-btn.active img.inactive-img, .tg-df-container .tg-df-carousel-filter-btn:hover picture:has(.inactive-img), .tg-df-container .tg-df-carousel-filter-btn.active picture:has(.inactive-img) { display: none; } .tg-df-container .tg-df-carousel-filter-btn:hover img.active-img, .tg-df-container .tg-df-carousel-filter-btn.active img.active-img, .tg-df-container .tg-df-carousel-filter-btn:hover picture:has(.active-img), .tg-df-container .tg-df-carousel-filter-btn.active picture:has(.active-img) { display: inline-flex; } .tg-df-container .tg-df-carousel-filter-btn { background: #ffffff; border: 2px solid #1e293b; color: #1e293b; border-radius: 24px; padding: 6px 16px; font-size: 14px; font-weight: 600; cursor: pointer; transition: all 0.2s; flex-shrink: 0; white-space: nowrap; } .tg-df-container .tg-df-carousel-filter-btn svg { margin-right: 6px; } .tg-df-container .tg-df-carousel-filter-btn { display: inline-flex; align-items: center; } .tg-df-container .tg-df-carousel-filter-btn:hover { background: #1e293b; color: white; border-color: #1e293b; } .tg-df-container .tg-df-carousel-filter-btn.active { background: #1e293b; color: white; border-color: #1e293b; } .tg-df-grid.carousel-compact { display: flex; flex-wrap: nowrap; overflow-x: auto; gap: 16px; padding: 16px 24px; align-items: stretch; scrollbar-width: none; } .tg-df-grid.carousel-compact::-webkit-scrollbar { display: none; } .tg-df-grid.carousel-compact .tg-df-card { flex: 0 0 auto; width: 200px; min-height: auto; height: auto; display: flex; flex-direction: column; border-radius: 15px; border: none; box-shadow: 0 0 16px rgba(0,0,0,0.08); overflow: visible; } .tg-df-grid.carousel-compact .tg-df-card-image-box { padding: 12px; background-color: transparent; border-radius: 15px 15px 0 0; height: 130px; } .tg-df-grid.carousel-compact .tg-df-card-image { mix-blend-mode: normal; } .tg-df-grid.carousel-compact .tg-df-card-discount-badge { border-radius: 0; top: 0px; left: 0px; padding: 4px 8px; font-size: 11px; } .tg-df-grid.carousel-compact .tg-df-card-body { padding: 8px 12px 12px 12px; } .tg-df-grid.carousel-compact .tg-df-card-title { font-size: 14px; font-weight: 400; margin-bottom: 8px; color: #011535; } .tg-df-grid.carousel-compact .tg-df-card-body:not(:has(.tg-df-card-stars)):not(:has(.tg-df-tag-prime)):not(:has(.tg-df-coupon-wrapper:not([style*="none"]))) > .tg-df-card-title, .tg-df-grid.carousel-compact .tg-df-card-body:not(:has(.tg-df-card-stars)):has(> .tg-df-card-title:first-child) > .tg-df-card-title { } .tg-df-grid.carousel-compact .tg-df-card-cta { border-radius: 5px; padding: 8px 10px; margin-top: 4px; background-color: #1F69FF; } .tg-df-grid.carousel-compact .tg-df-card-price-group { margin-bottom: 2px; } .tg-df-grid.carousel-compact .tg-df-card-merchant-pill { margin-bottom: 2px; } @container tg-df (max-width: 599px) { .tg-df-container .tg-df-carousel-blue-box-title { font-size: 24px; } .tg-df-container .tg-df-countdown-title { display: none; } .tg-df-container .tg-df-countdown-wrapper { position: absolute; top: 0; right: 0; align-items: flex-end; transform: scale(0.40); transform-origin: top right; } .tg-df-container .tg-df-roundel { min-width: 88px; } .tg-df-container .tg-df-roundel-img-box { width: 88px; height: 88px; } } /* REPLICA BLOCK STYLES */ .tg-df-grid.layout-replica-2 { grid-template-columns: repeat(2, 1fr) !important; gap: 20px; } .tg-df-grid.layout-replica-1 { grid-template-columns: 1fr !important; gap: 20px; } .tg-df-container .hawk-deal-widget-container { border-bottom: 1px solid #e5e7eb; display: flex; flex-direction: column; margin: 0; padding: 20px 0; box-sizing: border-box; font-family: inherit; } .tg-df-container .hawk-deal-widget-wrap { display: flex; flex-direction: row; align-items: flex-start; width: 100%; gap: 24px; } .tg-df-container .hawk-deal-widget-image-container { display: flex; flex-shrink: 0; justify-content: center; width: 160px; height: 160px; align-items: center; background: white; margin-bottom: 0px; } .tg-df-container .hawk-deal-widget-title-product-title { color: #111827; font-size: 18px; font-weight: 700; line-height: 1.4; display: inline; } .tg-df-container .hawk-deal-widget-title-price { font-size: 18px; font-weight: 700; line-height: 1.4; white-space: nowrap; color: #2563eb; } .tg-df-container .hawk-deal-widget-title-price-now { font-weight: 700; } .tg-df-container .hawk-deal-widget-title-retailer-price:hover { text-decoration: underline; } .tg-df-container .hawk-deal-widget-title-retailer { font-size: 18px; font-weight: 700; line-height: 1.4; color: #2563eb; } .tg-df-container .hawk-deal-widget-title-was-price { color: #dc2626; font-size: 16px; font-weight: 500; line-height: 1.4; text-decoration: line-through; white-space: nowrap; margin-left: 8px; margin-right: 8px; } .tg-df-container .hawk-deal-widget-text-body-container { position: relative; width: 100%; box-sizing: border-box; } .tg-df-container .hawk-deal-widget-text-body-main { font-size: 16px; width: 100%; margin-bottom: 12px; } .tg-df-container .hawk-deal-widget-text-body-description { display: block; font-size: 15px; margin-top: 12px; color: #4b5563; line-height: 1.6; } .tg-df-container .hawk-deal-widget-text-body-description p { margin: 0; line-height: 1.6; } .tg-df-container .hawk-deal-widget-text-cta-container { display: flex; flex-direction: column; gap: 12px; width: 100%; flex: 1; min-width: 0; box-sizing: border-box; } .tg-df-container .hawk-deal-widget-footer { display: flex; justify-content: flex-end; width: 100%; margin-top: auto; } .tg-df-container .hawk-deal-widget-button-wrapper { display: flex; flex-direction: column; align-items: flex-end; justify-content: flex-end; width: 100%; } .tg-df-container .hawk-deal-widget-preferred-partner-wrapper { display: flex; flex-direction: row; } @container tg-df (min-width: 600px) { .tg-df-mobile-only { display: none !important; } } @container tg-df (max-width: 599px) { .tg-df-desktop-only { display: none !important; } .tg-df-grid.layout-replica-2 { grid-template-columns: 1fr !important; } .tg-df-grid.savings-squad-cards { grid-template-columns: 1fr !important; display: flex; flex-direction: column; } } .tg-df-grid.savings-squad-cards .tg-df-card-title { -webkit-line-clamp: unset !important; display: block !important; overflow: visible !important; } @container tg-df (max-width: 500px) { .tg-df-container .hawk-deal-widget-wrap { display: block; } .tg-df-container .hawk-deal-widget-image-container { display: block; float: left; margin: 0 16px 8px 0; width: 120px; max-width: 120px; height: auto; align-items: normal; justify-content: normal; } .tg-df-container .hawk-deal-widget-text-cta-container { display: block; text-align: left; } .tg-df-container .hawk-deal-widget-footer { display: block; margin-top: 16px; clear: both; width: 100%; } .tg-df-container .hawk-deal-widget-button-wrapper { display: block; width: 100%; } .tg-df-container .hawk-deal-widget-button-wrapper .hawk-deal-widget-preferred-partner-wrapper { display: block; width: 100%; } .tg-df-container .hawk-affiliate-link-deal-button { box-sizing: border-box !important; display: flex !important; max-width: none !important; width: 100% !important; margin: 0 !important; } } .tg-df-container .hawk-affiliate-link-deal-button { align-items: center; background-color: #1f69ff; box-sizing: border-box; color: #ffffff !important; display: flex; font-size: 14px; font-weight: 700; justify-content: center; letter-spacing: 0.5px; line-height: 1; min-width: 160px; padding: 14px 24px; text-align: center; text-decoration: none; text-transform: uppercase; width: 100%; word-break: normal; border-radius: 4px; border: 0; transition: background-color 0.2s; } .tg-df-container .hawk-affiliate-link-deal-button:hover { background-color: #0056e0; text-decoration: none; } .tg-df-container .hawk-lazy-image-deal-widget { display: block; height: auto; margin: auto; max-height: 160px; max-width: 100%; mix-blend-mode: multiply; object-fit: contain; } .tg-df-container .hawk-deal-widget-text-cta-container a { color: #2563eb; text-decoration: none; display: inline; } .tg-df-container .hawk-deal-widget-text-cta-container a:hover { text-decoration: underline; } .tg-df-container .hawk-deal-widget-text-cta-container a:has(.hawk-deal-widget-title-product-title) { color: #111827; } .tg-df-container .hawk-deal-widget-text-cta-container a:hover .hawk-deal-widget-title-product-title, .tg-df-container .hawk-deal-widget-text-cta-container a:hover .hawk-deal-widget-title-retailer-price { text-decoration: underline; } .tg-df-savings-squad-header { margin-bottom: 24px; text-align: center; display: none; } .tg-df-banner-img-desktop { display: block; width: 100%; height: auto; margin-bottom: 32px; } .tg-df-banner-img-mobile { display: none; width: 100%; height: auto; margin-bottom: 32px; } @container tg-df (max-width: 600px) { .tg-df-banner-img-desktop { display: none; } .tg-df-banner-img-mobile { display: block; } } .tg-df-header-title { font-size: 28px; font-weight: 700; color: var(--tg-df-text); margin: 32px 0 12px 0; line-height: 1.3; } .tg-df-header-subtitle { font-size: 16px; color: var(--tg-df-text-muted); margin: 0 0 32px 0; line-height: 1.5; } \x3C/style> \x3C!-- Widget Container --\x3E \x3Cdiv class="tg-df-container" id="signal-deals-finder-root"> \x3Cdiv class="tg-df-savings-squad-header" id="tg-df-savings-squad-header"> \x3Cpicture> \x3Cimg src="https://cdn.mos.cms.futurecdn.net/flexiimages/xkh2og7m3d1778189998.png" alt="Deals Banner" class="tg-df-banner-img-desktop" /> \x3Cimg src="https://cdn.mos.cms.futurecdn.net/flexiimages/gmak6rtdf41778245089.png" alt="Deals Banner Mobile" class="tg-df-banner-img-mobile" /> \x3C/picture> \x3Cdiv class="tg-df-header-text"> \x3Ch2 class="tg-df-header-title" id="tg-df-header-title">Editor's Choice Deals\x3C/h2> \x3Cp class="tg-df-header-subtitle" id="tg-df-header-subtitle">Discover the best discounts currently available, curated daily by the Tom's Guide Savings Squad.\x3C/p> \x3C/div> \x3C/div> \x3C!-- Editor Floating Bar --\x3E \x3Cdiv class="tg-df-editor-bar" id="tg-df-editor-bar" style="display:none;"> \x3Cdiv class="tg-df-editor-bar-text" style="display: flex; align-items: center;"> \x3Cspan id="tg-df-selected-count">0\x3C/span>\x26nbsp;Deals Selected \x3Cbutton class="tg-df-editor-clear-btn" id="tg-df-editor-clear" type="button" style="margin-left: 12px; font-size: 13px; color: #9ca3af; background: none; border: none; cursor: pointer; text-decoration: underline;">Clear All\x3C/button> \x3C/div> \x3Cbutton class="tg-df-editor-copy-btn" id="tg-df-editor-copy" type="button"> \x3Csvg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 6px;">\x3Crect x="9" y="9" width="13" height="13" rx="2" ry="2">\x3C/rect>\x3Cpath d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1">\x3C/path>\x3C/svg> Copy to CMS \x3C/button> \x3C/div> \x3Cdiv class="tg-df-carousel-host" id="tg-df-carousel-host" style="display: none;"> \x3Cdiv class="tg-df-carousel-eyebrow">DEAL FINDER\x3C/div> \x3Cdiv class="tg-df-carousel-query-title" id="tg-df-carousel-title-label">Best Deals\x3C/div> \x3Cdiv class="tg-df-carousel-blue-box"> \x3Cdiv class="tg-df-carousel-bg-circle-1" aria-hidden="true">\x26nbsp;\x3C/div> \x3Cdiv class="tg-df-carousel-bg-circle-2" aria-hidden="true">\x26nbsp;\x3C/div> \x3Cdiv class="tg-df-carousel-bg-circle-3" aria-hidden="true">\x26nbsp;\x3C/div> \x3Cdiv class="tg-df-carousel-box-content"> \x3Cdiv class="tg-df-countdown-wrapper" id="tg-df-countdown-wrapper" style="display:none;"> \x3Cdiv class="tg-df-countdown-title" id="tg-df-countdown-title">Prime Day starts in\x3C/div> \x3Cdiv class="tg-df-countdown-blocks"> \x3Cdiv class="tg-df-countdown-item">\x3Cdiv class="tg-df-countdown-box">\x3Cdiv class="tg-df-countdown-num" id="tg-df-cd-days">0\x3C/div>\x3C/div>\x3Cdiv class="tg-df-countdown-label">DAYS\x3C/div>\x3C/div> \x3Cdiv class="tg-df-countdown-item">\x3Cdiv class="tg-df-countdown-box">\x3Cdiv class="tg-df-countdown-num" id="tg-df-cd-hrs">0\x3C/div>\x3C/div>\x3Cdiv class="tg-df-countdown-label">HRS\x3C/div>\x3C/div> \x3Cdiv class="tg-df-countdown-item">\x3Cdiv class="tg-df-countdown-box">\x3Cdiv class="tg-df-countdown-num" id="tg-df-cd-min">0\x3C/div>\x3C/div>\x3Cdiv class="tg-df-countdown-label">MIN\x3C/div>\x3C/div> \x3Cdiv class="tg-df-countdown-item">\x3Cdiv class="tg-df-countdown-box">\x3Cdiv class="tg-df-countdown-num" id="tg-df-cd-sec">0\x3C/div>\x3C/div>\x3Cdiv class="tg-df-countdown-label">SEC\x3C/div>\x3C/div> \x3C/div> \x3C/div> \x3Cdiv class="tg-df-carousel-box-eyebrow">DEAL FINDER\x3C/div> \x3Cdiv class="tg-df-carousel-box-title">Find Deals Fast\x3C/div> \x3Cdiv class="tg-df-carousel-box-subtitle">The latest deals from the biggest retailers, all in one place\x3C/div> \x3Cdiv class="tg-df-carousel-roundels-wrapper"> \x3Cbutton class="tg-df-carousel-scroll-left" type="button" aria-label="Scroll left" style="display:none;" onclick="this.parentElement.querySelector('.tg-df-carousel-roundels').scrollBy({left: -200, behavior: 'smooth'})">\x3Csvg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">\x3Cpath d="m15 18-6-6 6-6">\x3C/path>\x3C/svg>\x3C/button> \x3Cdiv class="tg-df-carousel-roundels"> \x3Cdiv class="tg-df-roundel tg-df-carousel-cat" data-query="Televisions" data-pr="all"> \x3Cdiv class="tg-df-roundel-img-box"> \x3Cimg src="https://cdn.mos.cms.futurecdn.net/wcMxTsHgqu3roMbAx7RLnT-132-100.png" alt="TVs" /> \x3C/div> \x3Cspan class="tg-df-roundel-label">TVs\x3C/span> \x3C/div> \x3Cdiv class="tg-df-roundel tg-df-carousel-cat" data-query="Phones" data-pr="over50"> \x3Cdiv class="tg-df-roundel-img-box"> \x3Cimg src="https://cdn.mos.cms.futurecdn.net/G3KGaRGzj24F6PUsw4bWpT-132-100.png" alt="Phones" /> \x3C/div> \x3Cspan class="tg-df-roundel-label">Phones\x3C/span> \x3C/div> \x3Cdiv class="tg-df-roundel tg-df-carousel-cat" data-query="Computing" data-pr="all"> \x3Cdiv class="tg-df-roundel-img-box"> \x3Cimg src="https://cdn.mos.cms.futurecdn.net/znNvsLzx8NEgNkD9HSFSnT-132-100.png" alt="Computing" /> \x3C/div> \x3Cspan class="tg-df-roundel-label">Computing\x3C/span> \x3C/div> \x3Cdiv class="tg-df-roundel tg-df-carousel-cat" data-query="Gaming" data-pr="all"> \x3Cdiv class="tg-df-roundel-img-box"> \x3Cimg src="https://cdn.mos.cms.futurecdn.net/Pgew8yaRQeZFHqHjTzvBnT-132-100.png" alt="Gaming" /> \x3C/div> \x3Cspan class="tg-df-roundel-label">Gaming\x3C/span> \x3C/div> \x3Cdiv class="tg-df-roundel tg-df-carousel-cat" data-query="Mattresses" data-pr="over500"> \x3Cdiv class="tg-df-roundel-img-box"> \x3Cimg src="https://cdn.mos.cms.futurecdn.net/cW7xsaLyesxkHFVSiC4kmT-132-100.png" alt="Mattresses" /> \x3C/div> \x3Cspan class="tg-df-roundel-label">Mattresses\x3C/span> \x3C/div> \x3Cdiv class="tg-df-roundel tg-df-carousel-cat" data-query="Audio" data-pr="over30"> \x3Cdiv class="tg-df-roundel-img-box"> \x3Cimg src="https://cdn.mos.cms.futurecdn.net/pCvBVHuhaQVjKt3VgCjbqT-132-100.png" alt="Audio" /> \x3C/div> \x3Cspan class="tg-df-roundel-label">Audio\x3C/span> \x3C/div> \x3C/div> \x3Cbutton class="tg-df-carousel-scroll-right" type="button" aria-label="Scroll right" onclick="this.parentElement.querySelector('.tg-df-carousel-roundels').scrollBy({left: 200, behavior: 'smooth'})">\x3Csvg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">\x3Cpath d="m9 18 6-6-6-6">\x3C/path>\x3C/svg>\x3C/button> \x3C/div> \x3C/div> \x3Cdiv class="tg-df-carousel-filters-outer" style="position: relative;"> \x3Cbutton class="tg-df-carousel-scroll-left" type="button" aria-label="Scroll left" style="display:none;" onclick="this.parentElement.querySelector('.tg-df-carousel-filters-wrap').scrollBy({left: -200, behavior: 'smooth'})">\x3Csvg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">\x3Cpath d="m15 18-6-6 6-6">\x3C/path>\x3C/svg>\x3C/button> \x3Cdiv class="tg-df-carousel-filters-wrap"> \x3Cbutton class="tg-df-carousel-filter-btn" data-d="0">All\x3C/button> \x3Cbutton class="tg-df-carousel-filter-btn" data-ot="amazon_lightning"> \x3Cimg src="https://cdn.mos.cms.futurecdn.net/HqAui7w97ft2NPqBtQ5r38-600-100.png" class="inactive-img" alt="" />\x3Cimg src="https://cdn.mos.cms.futurecdn.net/yWPQ5yyQRhUwVKzGwYbh38-600-100.png" class="active-img" alt="" /> Lightning deals\x3C/button> \x3Cbutton class="tg-df-carousel-filter-btn" data-ot="amazon_prime"> \x3Cimg src="https://cdn.mos.cms.futurecdn.net/fwoVXvL79turN3Ph535m38-600-100.png" class="inactive-img" alt="" />\x3Cimg src="https://cdn.mos.cms.futurecdn.net/u75QjVpt3w2EsMimJiRo38-600-100.png" class="active-img" alt="" /> Prime deals\x3C/button> \x3Cbutton class="tg-df-carousel-filter-btn" data-d="10"> \x3Csvg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-tag">\x3Cpath d="M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z">\x3C/path>\x3Ccircle cx="7.5" cy="7.5" r=".5" fill="currentColor">\x3C/circle>\x3C/svg> Min 10% off\x3C/button> \x3Cbutton class="tg-df-carousel-filter-btn" data-d="15"> \x3Csvg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-tag">\x3Cpath d="M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z">\x3C/path>\x3Ccircle cx="7.5" cy="7.5" r=".5" fill="currentColor">\x3C/circle>\x3C/svg> Min 15% off\x3C/button> \x3Cbutton class="tg-df-carousel-filter-btn" data-d="25"> \x3Csvg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-tag">\x3Cpath d="M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z">\x3C/path>\x3Ccircle cx="7.5" cy="7.5" r=".5" fill="currentColor">\x3C/circle>\x3C/svg> Min 25% off\x3C/button> \x3Cbutton class="tg-df-carousel-filter-btn" data-pr="under50"> \x3Csvg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-badge-dollar-sign">\x3Cpath d="M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z">\x3C/path>\x3Cpath d="M16 8h-6a2 2 0 1 0 0 4h4a2 2 0 1 1 0 4H8">\x3C/path>\x3Cpath d="M12 18V6">\x3C/path>\x3C/svg> Under $50\x3C/button> \x3C/div> \x3Cbutton class="tg-df-carousel-scroll-right" type="button" aria-label="Scroll right" style="display:none;" onclick="this.parentElement.querySelector('.tg-df-carousel-filters-wrap').scrollBy({left: 200, behavior: 'smooth'})">\x3Csvg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">\x3Cpath d="m9 18 6-6-6-6">\x3C/path>\x3C/svg>\x3C/button> \x3C/div> \x3C/div> \x3C/div> \x3C!-- Search & Filter Controls --\x3E \x3Cdiv class="tg-df-top-bar" id="tg-df-top-bar" style="position: relative; z-index: 100; margin: 0 auto 20px;"> \x3Cdiv class="tg-df-search-wrapper"> \x3Cinput type="text" class="tg-df-search-input" placeholder="Search for deals, products, or brands..."> \x3Cbutton type="button" class="tg-df-search-btn" aria-label="Search"> \x3Csvg class="tg-df-search-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> \x3Cpath d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/> \x3C/svg> \x3C/button> \x3Cdiv class="tg-df-autocomplete-dropdown" id="tg-df-autocomplete">\x3C/div> \x3C/div> \x3C/div> \x3Cdiv class="tg-df-controls" id="tg-df-controls" style="display:flex;"> \x3Cdiv class="tg-df-filters-container" style="position: relative; width: 100%; max-width: 800px; margin: 0 auto;"> \x3Cbutton class="tg-df-scroll-btn left" style="display: none;" onclick="this.parentElement.querySelector('.tg-df-filters').scrollBy({left: -200, behavior: 'smooth'})"> \x3Csvg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">\x3Cpath d="M15 18l-6-6 6-6"/>\x3C/svg> \x3C/button> \x3Cbutton class="tg-df-scroll-btn right" style="display: none;" onclick="this.parentElement.querySelector('.tg-df-filters').scrollBy({left: 200, behavior: 'smooth'})"> \x3Csvg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">\x3Cpath d="M9 18l6-6-6-6"/>\x3C/svg> \x3C/button> \x3Cdiv class="tg-df-filters"> \x3Cdiv class="tg-df-sort-wrapper" id="tg-df-category-filter-wrapper" style="display: none;"> \x3Csvg class="tg-df-sort-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> \x3Cpath d="M10 18h4v-2h-4v2zM3 6v2h18V6H3zm3 7h12v-2H6v2z"/> \x3C/svg> \x3Cselect class="tg-df-filter-select" id="tg-df-category-filter" aria-label="Category"> \x3Coption value="all">All Categories\x3C/option> \x3C/select> \x3C/div> \x3Cdiv class="tg-df-sort-wrapper tg-df-multiselect-container" id="tg-df-brand-filter-wrapper" style="display:none;"> \x3Csvg class="tg-df-sort-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> \x3Cpath d="M4.25 5.61C6.27 8.2 10 13 10 13v6c0 .55.45 1 1 1h2c.55 0 1-.45 1-1v-6s3.72-4.8 5.74-7.39A.998.998 0 0 0 18.95 4H5.04c-.83 0-1.3.95-.79 1.61z"/> \x3C/svg> \x3Cdiv class="tg-df-filter-select tg-df-multiselect-trigger" id="tg-df-brand-trigger" tabindex="0"> Any Brand \x3C/div> \x3Cdiv class="tg-df-multiselect-dropdown" id="tg-df-brand-dropdown"> \x3C!-- Populated via script --\x3E \x3C/div> \x3C/div> \x3Cdiv class="tg-df-sort-wrapper tg-df-price-range-wrapper" id="tg-df-custom-price-wrapper" style="display: flex; align-items:center; justify-content:center; padding: 10px 20px; gap: 8px; border: 1px solid var(--tg-df-border); border-radius: 100px; background-color: var(--tg-df-bg);"> \x3Cspan style="font-size:14px; font-weight:500; color:var(--tg-df-text-primary);">Price\x3C/span> \x3Cinput type="number" class="tg-df-price-input" id="tg-df-custom-price-min" placeholder="Min" style="width: 48px; background: transparent; border: none; color: var(--tg-df-text-primary); outline: none; font-size: 14px; text-align: center; padding: 0;"> \x3Cspan style="color:var(--tg-df-text-muted)">-\x3C/span> \x3Cinput type="number" class="tg-df-price-input" id="tg-df-custom-price-max" placeholder="Max" style="width: 48px; background: transparent; border: none; color: var(--tg-df-text-primary); outline: none; font-size: 14px; text-align: center; padding: 0;"> \x3C/div> \x3Cdiv class="tg-df-sort-wrapper" id="tg-df-legacy-price-wrapper"> \x3Csvg class="tg-df-sort-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> \x3Cpath d="M21.41 11.58l-9-9C12.05 2.22 11.55 2 11 2H4c-1.1 0-2 .9-2 2v7c0 .55.22 1.05.59 1.42l9 9c.36.36.86.58 1.41.58.55 0 1.05-.22 1.41-.59l7-7c.37-.36.59-.86.59-1.41 0-.55-.23-1.06-.59-1.42zM5.5 7C4.67 7 4 6.33 4 5.5S4.67 4 5.5 4 7 4.67 7 5.5 6.33 7 5.5 7z"/> \x3C/svg> \x3Cselect class="tg-df-filter-select" id="tg-df-price-filter" aria-label="Filter Prices"> \x3Coption value="all">All Prices\x3C/option> \x3Coption value="under50">Under $50\x3C/option> \x3Coption value="50_100">$50 - $100\x3C/option> \x3Coption value="100_200">$100 - $200\x3C/option> \x3Coption value="200_500">$200 - $500\x3C/option> \x3Coption value="over500">Over $500\x3C/option> \x3C/select> \x3C/div> \x3Cdiv class="tg-df-sort-wrapper" id="tg-df-discount-filter-wrapper"> \x3Csvg class="tg-df-sort-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"> \x3Cpath d="M21.41 11.58l-9-9C12.05 2.22 11.55 2 11 2H4c-1.1 0-2 .9-2 2v7c0 .55.22 1.05.59 1.42l9 9c.36.36.86.58 1.41.58.55 0 1.05-.22 1.41-.59l7-7c.37-.36.59-.86.59-1.41 0-.55-.23-1.06-.59-1.42zM5.5 7C4.67 7 4 6.33 4 5.5S4.67 4 5.5 4 7 4.67 7 5.5 6.33 7 5.5 7z"/> \x3C/svg> \x3Cselect class="tg-df-filter-select" id="tg-df-discount-filter" aria-label="Discount Amount"> \x3Coption value="all">Any discount\x3C/option> \x3Coption value="5">Min 5%\x3C/option> \x3Coption value="10">Min 10%\x3C/option> \x3Coption value="15">Min 15%\x3C/option> \x3Coption value="20">Min 20%\x3C/option> \x3Coption value="25">Min 25%\x3C/option> \x3Coption value="30">Min 30%\x3C/option> \x3Coption value="40">Min 40%\x3C/option> \x3Coption value="50">Min 50%\x3C/option> \x3Coption value="60">Min 60%\x3C/option> \x3Coption value="70">Min 70%\x3C/option> \x3C/select> \x3C/div> \x3C/div> \x3C/div> \x3C/div> \x3C!-- Deals Grid Wrapper --\x3E \x3Cdiv class="tg-df-grid-wrapper tg-df-carousel-cards-wrapper" id="tg-df-grid-wrapper"> \x3Cbutton class="tg-df-carousel-scroll-left" type="button" aria-label="Scroll left" style="display:none;" onclick="this.parentElement.querySelector('#tg-df-grid').scrollBy({left: -200, behavior: 'smooth'})">\x3Csvg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">\x3Cpath d="m15 18-6-6 6-6">\x3C/path>\x3C/svg>\x3C/button> \x3Cdiv class="tg-df-grid" id="tg-df-grid"> \x3C!-- Content populated by JavaScript --\x3E \x3C/div> \x3Cbutton class="tg-df-carousel-scroll-right" type="button" aria-label="Scroll right" style="display:none;" onclick="this.parentElement.querySelector('#tg-df-grid').scrollBy({left: 200, behavior: 'smooth'})">\x3Csvg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">\x3Cpath d="m9 18 6-6-6-6">\x3C/path>\x3C/svg>\x3C/button> \x3C/div> \x3C!-- Vouchers Modal --\x3E \x3Cdiv class="tg-df-modal-backdrop" id="tg-df-vouchers-modal"> \x3Cdiv class="tg-df-modal"> \x3Cdiv class="tg-df-modal-header"> \x3Ch3 class="tg-df-modal-title" id="tg-df-vouchers-title">Available Coupons & Deals\x3C/h3> \x3Cbutton class="tg-df-modal-close" id="tg-df-vouchers-close"> \x3Csvg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> \x3Cline x1="18" y1="6" x2="6" y2="18">\x3C/line> \x3Cline x1="6" y1="6" x2="18" y2="18">\x3C/line> \x3C/svg> \x3C/button> \x3C/div> \x3Cdiv class="tg-df-modal-body" id="tg-df-vouchers-content"> \x3C!-- Vouchers injected here --\x3E \x3C/div> \x3C/div> \x3C/div> \x3C/div>`; if (!template) { template = document.createElement('template'); template.innerHTML = rawTemplate; } let shadowRoot = null; if (hostContainer && template) { hostContainer.setAttribute('data-initialized', 'true'); shadowRoot = hostContainer.attachShadow({ mode: 'open' }); shadowRoot.appendChild(template.content.cloneNode(true)); } class DealsFinderWidget { constructor(config) { this.rootNode = config.rootNode || document; this.hostContainer = config.hostContainer || null; this.rootId = config.rootId || 'signal-deals-finder-root'; this.root = this.rootNode.querySelector('#' + this.rootId); if (!this.root) return; this.widgetId = (window.crypto && window.crypto.randomUUID) ? window.crypto.randomUUID() : 'widget-' + Date.now() + '-' + Math.random().toString(36).slice(2); this.grid = this.root.querySelector('#tg-df-grid'); this.tagsContainer = this.root.querySelector('#tg-df-tags-container'); this.categoryFilter = this.root.querySelector('#tg-df-category-filter'); this.categoryFilterWrapper = this.root.querySelector('#tg-df-category-filter-wrapper'); this.searchInput = this.root.querySelector('.tg-df-search-input'); this.autocompleteDropdown = this.root.querySelector('#tg-df-autocomplete'); this.sortSelect = this.root.querySelector('.tg-df-sort-select'); this.searchBtn = this.root.querySelector('.tg-df-search-btn'); this.settingsToggle = this.root.querySelector('#tg-df-settings-toggle'); this.settingsPanel = this.root.querySelector('#tg-df-settings-panel'); this.settingsBackdrop = this.root.querySelector('#tg-df-settings-backdrop'); this.regionSelect = this.root.querySelector('#tg-df-region-select'); this.retailerSelect = this.root.querySelector('#tg-df-retailer-select'); this.offerTypeSelect = this.root.querySelector('#tg-df-offer-type-select'); this.viewModeSelect = this.root.querySelector('#tg-df-view-mode-select'); this.rowsSelect = this.root.querySelector('#tg-df-rows-select'); this.dealModeToggle = this.root.querySelector('#tg-df-deal-mode'); this.editorModeToggle = this.root.querySelector('#tg-df-editor-mode'); this.priceFilter = this.root.querySelector('#tg-df-price-filter'); this.discountFilter = this.root.querySelector('#tg-df-discount-filter'); this.editorBar = this.root.querySelector('#tg-df-editor-bar'); this.editorSelectedCount = this.root.querySelector('#tg-df-selected-count'); this.editorCopyBtn = this.root.querySelector('#tg-df-editor-copy'); this.editorClearBtn = this.root.querySelector('#tg-df-editor-clear'); this.apiUrl = 'https://search-api.fie.future.net.uk/widget.php'; this.deals = []; this.displayLimit = 12; this.airedaleArticles = null; this.airedaleTags = []; this.airedaleTagCounts = {}; this.activeDealTag = null; this.selectedBrands = []; this.currentQuery = ''; this.editorMode = this.hostContainer ? this.hostContainer.hasAttribute('data-editor-mode') : false; this.viewModeOverride = this.hostContainer ? this.hostContainer.getAttribute('data-view-mode') : null; this.selectedDeals = new Map(); this.brandFilterWrapper = this.root.querySelector('#tg-df-brand-filter-wrapper'); this.brandTrigger = this.root.querySelector('#tg-df-brand-trigger'); this.brandDropdown = this.root.querySelector('#tg-df-brand-dropdown'); this.customPriceWrapper = this.root.querySelector('#tg-df-custom-price-wrapper'); this.customPriceMin = this.root.querySelector('#tg-df-custom-price-min'); this.customPriceMax = this.root.querySelector('#tg-df-custom-price-max'); this.legacyPriceWrapper = this.root.querySelector('#tg-df-legacy-price-wrapper'); this.discountFilterWrapper = this.root.querySelector('#tg-df-discount-filter-wrapper'); this.initResizeObserver(); this.init(); } getViewMode() { console.log("DEBUG getViewMode -> override:", this.viewModeOverride, "editorMode:", this.editorMode); if (this.viewModeOverride && (!this.editorMode || !this.viewModeSelect)) { return this.viewModeOverride; } return (this.viewModeSelect && this.viewModeSelect.value) ? this.viewModeSelect.value : (this.viewModeOverride || 'auto'); } applyLayoutMode() { if (!this.grid) return; const mode = this.getViewMode(); console.log("[DEBUG] applyLayoutMode CALLED! mode=", mode); this.grid.classList.remove('layout-row', 'layout-grid', 'tg-df-grid-auto', 'carousel-compact', 'layout-replica-1', 'layout-replica-2'); const carouselHost = this.root.querySelector('#tg-df-carousel-host'); const controlsDiv = this.root.querySelector('#tg-df-controls'); const topBarDiv = this.root.querySelector('#tg-df-top-bar'); const headerElement = this.root.querySelector('#tg-df-savings-squad-header'); if (headerElement) { headerElement.style.display = mode === 'savings_squad' ? 'block' : 'none'; } if (mode === 'carousel') { this.grid.classList.add('carousel-compact'); if (carouselHost) carouselHost.style.display = 'block'; if (controlsDiv) controlsDiv.style.display = 'none'; if (topBarDiv) topBarDiv.style.display = 'none'; if (this.root.classList.contains('tg-df-container')) { this.root.classList.add('is-carousel'); } } else { if (carouselHost) carouselHost.style.display = 'none'; if (controlsDiv) controlsDiv.style.display = 'flex'; if (topBarDiv) topBarDiv.style.display = 'block'; if (this.root.classList.contains('tg-df-container')) { this.root.classList.remove('is-carousel'); } } if (mode === 'grid') { this.grid.classList.add('layout-grid'); } else if (mode === 'row') { this.grid.classList.add('layout-row'); } else if (mode === 'savings_squad') { this.grid.classList.add('tg-df-grid-auto', 'savings-squad-cards'); } else if (mode !== 'carousel') { this.grid.classList.add('tg-df-grid-auto'); } const settingsWrapper = this.root.querySelector('.tg-df-settings-wrapper'); if (settingsWrapper) { settingsWrapper.style.display = mode === 'auto' ? 'none' : 'block'; } if (this.customPriceWrapper) { this.customPriceWrapper.style.display = mode === 'auto' ? 'flex' : 'none'; } if (this.legacyPriceWrapper) { this.legacyPriceWrapper.style.display = mode === 'auto' ? 'none' : 'flex'; } if (this.discountFilterWrapper) { this.discountFilterWrapper.style.display = mode === 'auto' ? 'none' : 'flex'; } } initResizeObserver() { try { if (window.parent === window) return; } catch (e) { // cross origin frame check threw } const emitHeight = () => { try { const height = document.documentElement.scrollHeight || document.body.scrollHeight; const msg = { type: 'embed-size', height: height }; if (window.parent && window.parent !== window) { window.parent.postMessage(msg, '*'); window.parent.postMessage(JSON.stringify({ ...msg, sentinel: 'amp' }), '*'); } } catch (e) {} }; if (window.ResizeObserver) { try { const ro = new ResizeObserver(() => emitHeight()); ro.observe(document.body); if (this.root) ro.observe(this.root); } catch(e){ console.warn(e); } } window.addEventListener('resize', emitHeight); setTimeout(emitHeight, 300); } initCountdown() { this.cdWrapper = this.root.querySelector('#tg-df-countdown-wrapper'); let searchSource = window.location.search; if (this.hostContainer && this.hostContainer.hasAttribute('data-widget-config')) { searchSource = this.hostContainer.getAttribute('data-widget-config'); } else if (typeof window !== 'undefined' && window.__WIDGET_CONFIG__) { searchSource = window.__WIDGET_CONFIG__; } const params = new URLSearchParams(searchSource); this.showCountdown = params.get('show_countdown') === 'true'; const showHeaderDetails = params.get('show_header_details') !== 'false'; const eyebrow = this.root.querySelector('.tg-df-carousel-box-eyebrow'); const title = this.root.querySelector('.tg-df-carousel-box-title'); const subtitle = this.root.querySelector('.tg-df-carousel-box-subtitle'); if (!showHeaderDetails) { let containerElement = this.root.classList.contains('tg-df-container') ? this.root : this.root.querySelector('.tg-df-container'); if (containerElement) containerElement.classList.add('hide-header-details'); if (eyebrow) eyebrow.style.display = 'none'; if (title) title.style.display = 'none'; if (subtitle) subtitle.style.display = 'none'; } if (!this.cdWrapper) return; this.cdTitle = this.root.querySelector('#tg-df-countdown-title'); this.cdDays = this.root.querySelector('#tg-df-cd-days'); this.cdHrs = this.root.querySelector('#tg-df-cd-hrs'); this.cdMin = this.root.querySelector('#tg-df-cd-min'); this.cdSec = this.root.querySelector('#tg-df-cd-sec'); this.updateCountdown(); this.cdInterval = setInterval(() => this.updateCountdown(), 1000); } updateCountdown() { if (!this.cdWrapper) return; if (!this.showCountdown) { this.cdWrapper.style.display = 'none'; return; } const area = this.getAreaCode(); let offset = '-04:00'; if (['DE', 'FR', 'IT', 'ES', 'NL'].includes(area)) { offset = '+02:00'; } else if (['GB', 'IE', 'UK'].includes(area)) { offset = '+01:00'; } const startTime = new Date('2026-06-23T00:00:00' + offset).getTime(); const endTime = new Date('2026-06-26T00:00:00' + offset).getTime(); const now = Date.now(); let targetTime = 0; if (now < startTime) { targetTime = startTime; if (this.cdTitle) this.cdTitle.textContent = 'Prime Day starts in'; this.cdWrapper.style.display = 'flex'; } else if (now 2 || (this.getViewMode() === 'savings_squad')) { this.fetchDeals(this.currentQuery); } else { this.render(); } } updatePriceDropdownCurrency() { if (!this.priceFilter || !this.regionSelect) return; const currencySymbols = { 'US': '$', 'GB': '£', 'CA': '$CA', 'AU': '$AU', 'DE': '€', 'FR': '€', 'IT': '€', }; const area = this.getAreaCode(); const cur = currencySymbols[area || 'US'] || '$'; const options = this.priceFilter.options; for (let i = 0; i < options.length; i++) { const opt = options[i]; if (opt.value === 'all') { opt.innerText = 'All Prices'; } else if (opt.value === 'under50') { opt.innerText = `Under ${cur}50`; } else if (opt.value === '50_100') { opt.innerText = `${cur}50 - ${cur}100`; } else if (opt.value === '100_200') { opt.innerText = `${cur}100 - ${cur}200`; } else if (opt.value === '200_500') { opt.innerText = `${cur}200 - ${cur}500`; } else if (opt.value === 'over500') { opt.innerText = `Over ${cur}500`; } } } populateBrandDropdown(values) { if (!this.brandDropdown || !this.brandFilterWrapper) return; this.brandFilterWrapper.style.display = 'flex'; // show the wrapper let html = ''; const allChecked = this.selectedBrands.length === 0 ? 'checked' : ''; const _div = ''; const _span = ''; html += `\x3Cdiv class="tg-df-ms-option">\x3Cinput type="checkbox" value="" ${allChecked} class="tg-df-brand-chk"> Any Brand${_div}`; values.forEach(v => { if (!v.formatted_value || v.formatted_value === 'Any Brand') return; const isChecked = this.selectedBrands.includes(v.formatted_value) ? 'checked' : ''; html += `\x3Cdiv class="tg-df-ms-option">\x3Cinput type="checkbox" value="${this.escapeHTML(v.formatted_value)}" ${isChecked} class="tg-df-brand-chk"> ${this.escapeHTML(v.formatted_value)} \x3Cspan style="color:var(--tg-df-text-muted);font-size:12px">(${v.count || 0})${_span}${_div}`; }); this.brandDropdown.innerHTML = html; // Re-bind listeners const chks = this.brandDropdown.querySelectorAll('.tg-df-brand-chk'); chks.forEach(chk => { chk.addEventListener('change', (e) => { const val = e.target.value; if (val === '') { this.selectedBrands = []; } else { if (e.target.checked) { if (!this.selectedBrands.includes(val)) this.selectedBrands.push(val); } else { this.selectedBrands = this.selectedBrands.filter(b => b !== val); } } if (this.selectedBrands.length === 0) { this.brandTrigger.innerText = 'Any Brand'; } else if (this.selectedBrands.length === 1) { this.brandTrigger.innerText = this.selectedBrands[0]; } else { this.brandTrigger.innerText = `${this.selectedBrands.length} Brands selected`; } // Only call API if changed from UI interactions if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) { this.updateURLParams(); this.fetchDeals(this.currentQuery); } }); }); // Update button text on load if (this.selectedBrands.length === 0) { this.brandTrigger.innerText = 'Any Brand'; } else if (this.selectedBrands.length === 1) { this.brandTrigger.innerText = this.selectedBrands[0]; } else { this.brandTrigger.innerText = `${this.selectedBrands.length} Brands selected`; } } updateURLParams() { const url = new URL(window.location); if (this.currentQuery && this.currentQuery !== 'Gaming laptops') { url.searchParams.set('q', this.currentQuery); } else { url.searchParams.delete('q'); url.searchParams.delete('search'); url.searchParams.delete('query'); } if (this.regionSelect && this.regionSelect.value !== 'auto') { url.searchParams.set('region', this.regionSelect.value); } else { url.searchParams.delete('region'); } if (this.retailerSelect && this.retailerSelect.value) { url.searchParams.set('retailer', this.retailerSelect.value); } else { url.searchParams.delete('retailer'); } if (this.selectedBrands && this.selectedBrands.length > 0) { url.searchParams.set('brands', this.selectedBrands.join(',')); } else { url.searchParams.delete('brands'); } if (this.offerTypeSelect && this.offerTypeSelect.value) { url.searchParams.set('offer_type', this.offerTypeSelect.value); } else { url.searchParams.delete('offer_type'); } if (this.viewModeSelect && this.viewModeSelect.value !== 'auto') { url.searchParams.set('view_mode', this.viewModeSelect.value); } else { url.searchParams.delete('view_mode'); } if (this.rowsSelect && this.rowsSelect.value !== '12') { url.searchParams.set('rows', this.rowsSelect.value); } else { url.searchParams.delete('rows'); } const min = this.customPriceMin ? this.customPriceMin.value : ''; const max = this.customPriceMax ? this.customPriceMax.value : ''; if (min || max) { url.searchParams.set('price', `${min}_${max}`); } else if (this.priceFilter && this.priceFilter.value !== 'all') { url.searchParams.set('price', this.priceFilter.value); } else { url.searchParams.delete('price'); } if (this.discountFilter && this.discountFilter.value !== 'all' && this.discountFilter.value !== '0') { const v = parseInt(this.discountFilter.value); if (!isNaN(v) && v > 0) { const ratio = (100 - v) / 100; url.searchParams.set('min_discount_ratio', ratio.toString()); } } else { url.searchParams.delete('min_discount_ratio'); } if (this.sortSelect && this.sortSelect.value !== 'date_desc') { url.searchParams.set('sort', this.sortSelect.value); } else { url.searchParams.delete('sort'); } if (this.dealModeToggle && this.dealModeToggle.checked) { url.searchParams.set('deal_mode', 'true'); } else { url.searchParams.delete('deal_mode'); } window.history.replaceState({}, '', url); } bindEvents() { const handleFiltersScroll = () => { const filters = this.root.querySelector('.tg-df-filters'); const leftBtn = this.root.querySelector('.tg-df-scroll-btn.left'); const rightBtn = this.root.querySelector('.tg-df-scroll-btn.right'); if (filters && leftBtn && rightBtn) { const { scrollLeft, scrollWidth, clientWidth } = filters; leftBtn.style.display = scrollLeft > 0 ? 'flex' : 'none'; rightBtn.style.display = Math.ceil(scrollLeft + clientWidth) { origRenderCategories.apply(this, args); setTimeout(handleFiltersScroll, 50); }; } } const roundels = this.root.querySelectorAll('.tg-df-carousel-cat'); roundels.forEach(r => { r.addEventListener('click', () => { const q = r.getAttribute('data-query'); const pr = r.getAttribute('data-pr'); if (typeof trackHawkEvent !== 'undefined') { trackHawkEvent({ clickType: "CC", widgetId: this.widgetId, productCategoryName: "deals", zeroBasedProductIndexOrNull: null, totalDealsOrProducts: null, areaClicked: "Category Roundel", revenueId: this.revenueId, isoCurrencyCode: typeof this.getAreaCode === 'function' ? (this.getAreaCode() === 'GB' ? 'GBP' : 'USD') : 'USD', queryName: q, widgetTypeName: this.widgetTypeName }); } this.currentQuery = q; const label = this.root.querySelector('#tg-df-carousel-title-label'); if (label) label.textContent = 'Best ' + q; if (this.priceFilter) this.priceFilter.value = pr || 'all'; if (this.discountFilter) this.discountFilter.value = '5'; if (this.searchInput) this.searchInput.value = q; roundels.forEach(ro => ro.classList.remove('active')); r.classList.add('active'); this.fetchDeals(this.currentQuery); }); }); const discBtns = this.root.querySelectorAll('.tg-df-carousel-filter-btn'); discBtns.forEach(b => { b.addEventListener('click', () => { const d = b.getAttribute('data-d'); const pr = b.getAttribute('data-pr'); const ot = b.getAttribute('data-ot'); let label = b.innerText ? b.innerText.trim() : ''; let filterType = 'unknown'; let filterVal = 'unknown'; if (d !== null) { filterType = 'discount'; filterVal = d; } else if (pr !== null) { filterType = 'price'; filterVal = pr; } else if (ot !== null) { filterType = 'offertype'; filterVal = ot; } if (typeof trackElementInteraction === 'function') trackElementInteraction({ id: `filter-${filterType}-${filterVal}`, name: 'Filter Button', label: label }); if (d !== null) { if (this.discountFilter) this.discountFilter.value = this.discountFilter.value === d ? '0' : d; } else if (pr !== null) { if (this.priceFilter) this.priceFilter.value = this.priceFilter.value === pr ? 'all' : pr; } else if (ot !== null) { if (this.offerTypeSelect) this.offerTypeSelect.value = this.offerTypeSelect.value === ot ? 'all' : ot; } else { if (this.discountFilter) this.discountFilter.value = '0'; if (this.priceFilter) this.priceFilter.value = 'all'; if (this.offerTypeSelect) this.offerTypeSelect.value = 'all'; } if (d === null && pr === null && ot === null && b.getAttribute("data-type") !== "custom") { discBtns.forEach(ro => ro.classList.remove('active')); b.classList.add('active'); } else if (b.getAttribute("data-type") !== "custom") { // Only operate on hardcoded buttons (those without data-type) discBtns.forEach(ro => { if (!ro.getAttribute('data-d') && !ro.getAttribute('data-pr') && !ro.getAttribute('data-ot') && ro.getAttribute('data-type') !== 'custom') ro.classList.remove('active'); }); let makeActive = true; if (d !== null) { if (b.classList.contains('active')) makeActive = false; discBtns.forEach(ro => { if (ro.getAttribute('data-d') !== null && ro.getAttribute('data-type') !== 'custom') ro.classList.remove('active') }); } else if (pr !== null) { if (b.classList.contains('active')) makeActive = false; discBtns.forEach(ro => { if (ro.getAttribute('data-pr') !== null && ro.getAttribute('data-type') !== 'custom') ro.classList.remove('active') }); } else if (ot !== null) { if (b.classList.contains('active')) makeActive = false; discBtns.forEach(ro => { if (ro.getAttribute('data-ot') !== null && ro.getAttribute('data-type') !== 'custom') ro.classList.remove('active') }); } if (makeActive) b.classList.add('active'); // Check if anything is active, if not activate "All" let anyActive = false; discBtns.forEach(ro => { if (ro.classList.contains('active') && ro.getAttribute('data-type') !== 'custom') anyActive = true; }); if (!anyActive) { discBtns.forEach(ro => { if (!ro.getAttribute('data-d') && !ro.getAttribute('data-pr') && !ro.getAttribute('data-ot') && ro.getAttribute('data-type') !== 'custom') ro.classList.add('active'); }); } } this.fetchDeals(this.currentQuery); }); }); if (this.brandTrigger && this.brandDropdown) { this.brandTrigger.addEventListener('click', () => { this.brandDropdown.classList.toggle('active'); }); document.addEventListener('click', (e) => { if (this.brandFilterWrapper && !e.composedPath().includes(this.brandFilterWrapper)) { this.brandDropdown.classList.remove('active'); } }); } const showAutocomplete = () => { if (this.getViewMode() !== 'savings_squad' || !this.autocompleteDropdown || !this.airedaleTags) return; let terms = this.airedaleTags; if (this.airedaleBrands) { terms = terms.concat(this.airedaleBrands.map(b => b.formatted_value)); } terms = [...new Set(terms)]; const query = this.searchInput.value.trim(); let matches = []; if (query.length > 0) { matches = terms.filter(t => t.toLowerCase().includes(query.toLowerCase()) && t.toLowerCase() !== query.toLowerCase()); } else { matches = terms; } if (matches.length > 0) { this.autocompleteDropdown.innerHTML = matches.map(m => `\x3Cdiv class="tg-df-autocomplete-item" data-tag="${this.escapeHTML(m)}">${this.escapeHTML(m)}`).join(''); this.autocompleteDropdown.classList.add('active'); } else { this.autocompleteDropdown.classList.remove('active'); } }; let debounceTimer; if(this.searchInput) { this.searchInput.addEventListener('focus', showAutocomplete); this.searchInput.addEventListener('input', (e) => { clearTimeout(debounceTimer); const query = e.target.value.trim(); this.currentQuery = query; showAutocomplete(); debounceTimer = setTimeout(() => { this.updateURLParams(); if (query.length > 2) { this.fetchDeals(query); } else if (query.length === 0) { if (this.getViewMode() === 'savings_squad') { this.activeDealTag = null; this.currentQuery = ''; if (this.categoryFilter) this.categoryFilter.value = 'all'; this.fetchDeals(''); } else { this.deals = []; this.render(); } } }, 400); }); this.searchInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') { if (this.autocompleteDropdown) this.autocompleteDropdown.classList.remove('active'); clearTimeout(debounceTimer); const query = e.target.value.trim(); this.currentQuery = query; let isTag = false; if (this.airedaleTags && this.airedaleTags.includes(query)) isTag = true; if (this.airedaleBrands && this.airedaleBrands.some(b => b.formatted_value === query)) isTag = true; this.activeDealTag = isTag ? query : null; trackElementInteraction({ id: 'search-submit', name: 'Ask', label: 'Ask (main search)', text: query }); this.updateURLParams(); if (query.length > 2 || (this.getViewMode() === 'savings_squad')) { if (query.length === 0 && this.getViewMode() === 'savings_squad') { if (this.categoryFilter) this.categoryFilter.value = 'all'; } this.fetchDeals(query); } } }); } if (this.autocompleteDropdown) { this.autocompleteDropdown.addEventListener('click', (e) => { const item = e.target.closest('.tg-df-autocomplete-item'); if (item) { const tag = item.getAttribute('data-tag'); this.currentQuery = tag; if (this.searchInput) this.searchInput.value = tag; this.activeDealTag = tag; if (this.categoryFilter && this.airedaleTags.includes(tag)) { this.categoryFilter.value = tag; } this.autocompleteDropdown.classList.remove('active'); this.updateURLParams(); this.fetchDeals(tag); } }); document.addEventListener('click', (e) => { if (this.autocompleteDropdown && this.searchInput && !e.composedPath().includes(this.searchInput) && !e.composedPath().includes(this.autocompleteDropdown)) { this.autocompleteDropdown.classList.remove('active'); } }); } if (this.searchBtn) { this.searchBtn.addEventListener('click', () => { if (this.autocompleteDropdown) this.autocompleteDropdown.classList.remove('active'); clearTimeout(debounceTimer); const query = this.searchInput.value.trim(); trackElementInteraction({ id: 'search-submit', name: 'Ask', label: 'Ask (main search)', text: query }); let isTag = false; if (this.airedaleTags && this.airedaleTags.includes(query)) isTag = true; if (this.airedaleBrands && this.airedaleBrands.some(b => b.formatted_value === query)) isTag = true; this.activeDealTag = isTag ? query : null; this.currentQuery = query; this.updateURLParams(); if (query.length > 2 || (this.getViewMode() === 'savings_squad')) { if (query.length === 0 && this.getViewMode() === 'savings_squad') { if (this.categoryFilter) this.categoryFilter.value = 'all'; } this.fetchDeals(query); } }); } if(this.sortSelect && this.sortSelect.querySelector('option[value="date_desc"]') === null) { const option = document.createElement('option'); option.value = "date_desc"; option.text = "Newest First"; this.sortSelect.insertBefore(option, this.sortSelect.firstChild); } if(this.sortSelect) this.sortSelect.addEventListener('change', () => { trackElementInteraction({ id: `sort-option-${this.sortSelect.value}`, name: 'Sort', label: `Sort: ${this.sortSelect.options[this.sortSelect.selectedIndex].text}` }); this.updateURLParams(); if (this.deals.length > 0) { this.sortData(); this.render(); } }); const priceFilter = this.root.querySelector('#tg-df-price-filter'); if (priceFilter) { this.priceFilter = priceFilter; this.priceFilter.addEventListener('change', () => { trackElementInteraction({ id: `filter-price-${this.priceFilter.value}`, name: 'Price', label: this.priceFilter.options[this.priceFilter.selectedIndex].text }); this.updateURLParams(); if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) { this.fetchDeals(this.currentQuery); } else { this.render(); } }); } const updateCustomPrice = () => { this.updateURLParams(); if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) { this.fetchDeals(this.currentQuery); } else { this.render(); } }; if (this.customPriceMin) { this.customPriceMin.addEventListener('change', updateCustomPrice); this.customPriceMin.addEventListener('keypress', (e) => { if (e.key === 'Enter') updateCustomPrice(); }); } if (this.customPriceMax) { this.customPriceMax.addEventListener('change', updateCustomPrice); this.customPriceMax.addEventListener('keypress', (e) => { if (e.key === 'Enter') updateCustomPrice(); }); } const discountFilter = this.root.querySelector('#tg-df-discount-filter'); if (discountFilter) { this.discountFilter = discountFilter; this.discountFilter.addEventListener('change', () => { trackElementInteraction({ id: `filter-discount-${this.discountFilter.value}`, name: 'Discount', label: this.discountFilter.options[this.discountFilter.selectedIndex].text }); this.updateURLParams(); if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) { this.fetchDeals(this.currentQuery); } else { this.render(); } }); } if (this.categoryFilter) { this.categoryFilter.addEventListener('change', (e) => { const val = e.target.value === 'all' ? null : e.target.value; this.activeDealTag = val; if (val) { this.currentQuery = val; } else { if (this.searchInput && this.currentQuery === document.querySelector('#tg-df-brand-trigger')?.getAttribute('data-active-brand')) { // don't clear current query if a brand is selected } else if (this.searchInput) { this.currentQuery = ''; this.searchInput.value = ''; } } this.fetchSavingsSquad(); }); } if (this.settingsToggle) { this.settingsToggle.addEventListener('click', () => { const o = this.settingsPanel.classList.toggle('active'); this.settingsBackdrop.classList.toggle('active'); if (o) trackElementInteraction({ id: 'filter-open', name: 'Filters', label: 'Open filters' }); }); } if (this.settingsBackdrop) { this.settingsBackdrop.addEventListener('click', () => { this.settingsPanel.classList.remove('active'); this.settingsBackdrop.classList.remove('active'); }); } if (this.regionSelect) { this.regionSelect.addEventListener('change', () => { trackElementInteraction({ id: `filter-region-${this.regionSelect.value}`, name: 'Region', label: this.regionSelect.options[this.regionSelect.selectedIndex].text }); this.updateURLParams(); this.updatePriceDropdownCurrency(); if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) { this.fetchDeals(this.currentQuery); } }); } if (this.retailerSelect) { this.retailerSelect.addEventListener('change', () => { trackElementInteraction({ id: `filter-merchant-${this.retailerSelect.value}`, name: 'Retailer', label: this.retailerSelect.options[this.retailerSelect.selectedIndex].text }); this.updateURLParams(); if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) { this.fetchDeals(this.currentQuery); } }); } if (this.offerTypeSelect) { this.offerTypeSelect.addEventListener('change', () => { trackElementInteraction({ id: `filter-offertype-${this.offerTypeSelect.value}`, name: 'Offer Type', label: this.offerTypeSelect.options[this.offerTypeSelect.selectedIndex].text }); this.updateURLParams(); if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) { this.fetchDeals(this.currentQuery); } }); } if (this.viewModeSelect) { this._prevViewMode = this.viewModeSelect.value; this.viewModeSelect.addEventListener('change', () => { trackElementInteraction({ id: `filter-viewmode-${this.viewModeSelect.value}`, name: 'View Mode', label: this.viewModeSelect.options[this.viewModeSelect.selectedIndex].text }); // Reset all active toggles and filters to prevent config carry-over this.selectedBrands = []; if (this.brandTrigger) this.brandTrigger.innerText = 'Select Brands'; if (this.brandDropdown) { const chks = this.brandDropdown.querySelectorAll('.tg-df-brand-chk'); chks.forEach(chk => { chk.checked = false; }); } if (this.priceFilter) this.priceFilter.value = 'all'; if (this.customPriceMin) this.customPriceMin.value = ''; if (this.customPriceMax) this.customPriceMax.value = ''; if (this.sortSelect) this.sortSelect.value = 'date_desc'; if (this.discountFilter) this.discountFilter.value = '0'; if (this.retailerSelect) this.retailerSelect.value = ''; if (this.offerTypeSelect) this.offerTypeSelect.value = ''; if (this.rowsSelect) this.rowsSelect.value = '12'; if (this.categoryFilter) this.categoryFilter.value = 'all'; this.activeDealTag = null; this.updateURLParams(); this.applyLayoutMode(); if (this.getViewMode() === 'savings_squad' || this._prevViewMode === 'savings_squad') { this.fetchDeals(this.currentQuery); } else { this.render(); } this._prevViewMode = this.viewModeSelect.value; }); } if (this.rowsSelect) { this.rowsSelect.addEventListener('change', () => { this.updateURLParams(); if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) { this.fetchDeals(this.currentQuery); } }); } if (this.dealModeToggle) { this.dealModeToggle.addEventListener('change', () => { this.updateURLParams(); this.render(); }); } if (this.editorModeToggle) { this.editorModeToggle.addEventListener('change', (e) => { this.editorMode = e.target.checked; this.render(); this.updateFloatingCopyBar(); }); } if (this.editorCopyBtn) { this.editorCopyBtn.addEventListener('click', () => { this.copySelectedDealsToCMS(); }); } if (this.editorClearBtn) { this.editorClearBtn.addEventListener('click', () => { this.selectedDeals.clear(); this.render(); this.updateFloatingCopyBar(); }); } if (this.grid) { this.grid.addEventListener('change', (e) => { if (e.target.classList.contains('tg-df-deal-checkbox')) { const dealId = e.target.getAttribute('data-id'); if (e.target.checked) { const dealObj = this.deals.find(d => d.id === dealId); if (dealObj) this.selectedDeals.set(dealId, dealObj); } else { this.selectedDeals.delete(dealId); } this.updateFloatingCopyBar(); } }); this.grid.addEventListener('click', (e) => { const dealCard = e.target.closest('[data-action="deal-click"]'); const similarCard = e.target.closest('[data-action="view-similar-click"]'); const cardLink = dealCard || similarCard; if (cardLink) { const productName = cardLink.getAttribute('data-product-name'); const merchantName = cardLink.getAttribute('data-merchant-name'); const productId = cardLink.getAttribute('data-analytics-id'); const price = parseFloat(cardLink.getAttribute('data-price')) || null; const prevPriceStr = cardLink.getAttribute('data-previous-price'); const previousPrice = prevPriceStr ? parseFloat(prevPriceStr) : null; const originalLink = cardLink.getAttribute('data-original-link'); const rewrittenLink = cardLink.getAttribute('href'); const revenueId = cardLink.getAttribute('data-revenue-id'); const index = parseInt(cardLink.getAttribute('data-index'), 10) || 0; const inStock = cardLink.getAttribute('data-in-stock') === 'true'; const totalText = cardLink.getAttribute('data-total'); const totalDeals = parseInt(totalText, 10) || 0; const productCategoryName = 'deals'; const trackingParams = { widgetId: this.widgetId, productCategoryName: productCategoryName, product: { modelId: cardLink.getAttribute('data-model-id') || null, matchId: cardLink.getAttribute('data-match-id') || null, brand: cardLink.getAttribute('data-model-brand') || null, parent: cardLink.getAttribute('data-model-parent') || null, name: productName, price: price, previousPrice: previousPrice, link: rewrittenLink, originalLink: originalLink, inStock: inStock }, zeroBasedProductIndexOrNull: index, totalDealsOrProducts: totalDeals, merchant: { id: cardLink.getAttribute('data-merchant-id') || null, network: cardLink.getAttribute('data-merchant-network') || null, url: cardLink.getAttribute('data-merchant-url') || null, name: merchantName }, revenueId: revenueId, widgetTypeName: this.widgetTypeName, isoCurrencyCode: normalizeCurrency(this.escapeHTML(cardLink.getAttribute('data-currency') || '$')) }; if (dealCard) { trackDealClick(trackingParams); } else { trackViewSimilarClick(trackingParams); } } const couponsBtn = e.target.closest('[data-action="coupons-click"]'); if (couponsBtn) { trackElementInteraction({ id: 'product-card-show-coupons', name: 'Coupons', label: `Product card coupons: ${couponsBtn.getAttribute('data-merchant')}` }); } }); } this.setupScrollListeners(); } setupScrollListeners() { const containers = [ this.root.querySelector('.tg-df-carousel-roundels'), this.root.querySelector('.tg-df-carousel-filters-wrap'), this.root.querySelector('#tg-df-grid') ]; containers.forEach(container => { if (!container) return; const checkScroll = () => { if (!container.parentElement) return; const leftBtn = container.parentElement.querySelector('.tg-df-carousel-scroll-left'); const rightBtn = container.parentElement.querySelector('.tg-df-carousel-scroll-right'); if (leftBtn) { if (container.scrollLeft <= 5) leftBtn.style.display = 'none'; else leftBtn.style.display = 'flex'; } if (rightBtn) { if (container.scrollWidth = container.scrollWidth - container.clientWidth - 5) { rightBtn.style.display = 'none'; } else { rightBtn.style.display = 'flex'; } } }; container.addEventListener('scroll', checkScroll); checkScroll(); window.addEventListener('resize', checkScroll); const observer = new MutationObserver(checkScroll); observer.observe(container, { childList: true, subtree: true, characterData: false }); }); } get widgetTypeName() { const mode = this.viewModeSelect ? this.viewModeSelect.value : (this.viewModeOverride || 'auto'); switch(mode) { case 'carousel': return 'Carousel'; case 'savings_squad': return 'Savings Squad'; case 'grid': return 'Grid'; case 'row': return 'Row'; default: return 'Auto Collection'; } } getAreaCode() { if (this.regionSelect && this.regionSelect.value) { if (this.regionSelect.value === 'auto') return null; return this.regionSelect.value; } let area = null; try { const locale = window.navigator.language || window.navigator.userLanguage; if (locale && locale.includes('-')) { area = locale.split('-')[1].toUpperCase(); } else if (locale && locale.length === 2) { if (locale.toUpperCase() === 'EN') { area = 'US'; } else { area = locale.toUpperCase(); } } } catch (e) { /* Ignore */ } // Map to known valid options or fallback to US const valid = ['US', 'GB', 'CA', 'AU', 'DE', 'FR', 'IT']; if (area === 'UK') area = 'GB'; if (valid.includes(area)) { return area; } return 'US'; } async fetchDeals(query) { this.showLoading(); this.deals = []; this.displayLimit = (this.rowsSelect && this.rowsSelect.value) ? parseInt(this.rowsSelect.value, 10) : 12; try { console.log("getViewMode returns:", this.getViewMode()); if (this.getViewMode() === 'savings_squad') { await this.fetchSavingsSquad(); } else { if (this.isBroadQuery(query)) { await this.fetchAdviserDeals(query); } else { await this.fetchHawkDeals(query); if (this.deals.length === 0) { await this.fetchAdviserDeals(query); } } } } catch (error) { console.warn("[Tom's Guide Widget] Fetch error:", error); this.showError(); } } async fetchSavingsSquad() { let topArticles = this.airedaleArticles; if (!topArticles) { const airedaleUrl = `https://airedale.futurecdn.net/feeds/feed_1781000519267.json?site=tomsguide&articleType=deals&limit=50`; let res; try { res = await fetch(airedaleUrl); } catch(e) { try { res = await fetch(`https://airedale.futurecdn.net/feeds/feed_1776420579726.json?site=tomsguide&articleType=deals&limit=50`); } catch (err) { console.warn("Fallback fetch failed", err); return; } } if (!res.ok) throw new Error('Airedale API Error'); const articles = await res.json(); topArticles = Array.isArray(articles) ? articles.slice(0, 50) : ((articles.data && Array.isArray(articles.data)) ? articles.data.slice(0, 50) : []); this.airedaleArticles = topArticles; let tagCounts = {}; topArticles.forEach((a) => { let articleTags = new Set(); if (a.articlecategory && Array.isArray(a.articlecategory)) { a.articlecategory.forEach((t) => articleTags.add(t)); } articleTags.forEach(t => { tagCounts[t] = (tagCounts[t] || 0) + 1; }); }); this.airedaleTags = Object.keys(tagCounts).sort((a, b) => tagCounts[b] - tagCounts[a]); this.airedaleTagCounts = tagCounts; } let targetArticles = topArticles; if (!this.activeDealTag && this.currentQuery) { const tagMatch = this.airedaleTags.find(t => t.toLowerCase() === this.currentQuery.toLowerCase()); if (tagMatch) { this.activeDealTag = tagMatch; } } if (this.activeDealTag) { const cleanTag = this.activeDealTag.toLowerCase().replace(/&/g, '').replace(/[^a-z0-9]+/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, ''); const encodedTag = encodeURIComponent(cleanTag); const url = `https://airedale.futurecdn.net/feeds/feed_1781000519267.json?site=tomsguide&articleType=deals&limit=50&articleCategoryHandle=${encodedTag}`; try { const res = await fetch(url); if (res.ok) { const articles = await res.json(); targetArticles = Array.isArray(articles) ? articles.slice(0, 50) : ((articles.data && Array.isArray(articles.data)) ? articles.data.slice(0, 50) : []); } } catch(e) { console.warn("Failed to fetch by activeDealTag", e); } } let extractedDeals = []; let seenUrls = new Set(); let overallBrandsCounts = {}; // First pass: extract ALL brands from topArticles so the dropdown has all options topArticles.forEach((article) => { if (!article.articlepage) return; let pageData = []; try { pageData = JSON.parse(article.articlepage[0]); } catch(e){ console.warn(e); } const savingsSquad = pageData.filter((p) => p.type === 'deal' || p.type === 'featured-product'); savingsSquad.forEach((block) => { const data = block.data || {}; if (data.brand) { const cleanBrand = data.brand.replace(/^\d+\.\s*/, '').trim(); overallBrandsCounts[cleanBrand] = (overallBrandsCounts[cleanBrand] || 0) + 1; } }); }); targetArticles.forEach((article) => { if (!article.articlepage) return; let pageData = []; try { pageData = JSON.parse(article.articlepage[0]); } catch(e){ console.warn(e); } const savingsSquad = pageData.filter((p) => p.type === 'deal' || p.type === 'featured-product'); savingsSquad.forEach((block, idx) => { const data = block.data || {}; const isFeatured = block.type === 'featured-product'; const link = data.link || {}; const priceObj = data.price || {}; const image = data.image || {}; if (data.brand) { data.brand = data.brand.replace(/^\d+\.\s*/, '').trim(); } const externalUrl = isFeatured ? data.url : (link.href || null); let summaryTitle = isFeatured ? (data.name || data.brand) : (data.productName || link.label || article.articlename); let description = isFeatured ? (data.strapline || '') : (data.text || ''); if (!isFeatured && !data.productName && data.text) { const brSplit = data.text.split(new RegExp('\x3Cbr\\s*\\/?\\x3E', 'i')); if (brSplit.length > 1) { summaryTitle = brSplit[0].replace(/]+>/g, '').trim(); description = brSplit.slice(1).join(' ').replace(/
/gi, ' ').replace(/]*>/gi, ' ').replace(/]+>/g, '').replace(/\s+/g, ' ').trim(); } else { const match = data.text.match(/\x3Cstrong>(.*?)/); if (match) { summaryTitle = match[1].replace(/]+>/g, '').trim(); if (summaryTitle.endsWith(':')) summaryTitle = summaryTitle.slice(0, -1); } } } let imageUrl = isFeatured ? image.mos : (image.src || null); if (imageUrl && imageUrl.startsWith('//')) imageUrl = 'https:' + imageUrl; description = description.replace(/
/gi, ' ').replace(/]*>/gi, ' ').replace(/]+>/g, '').replace(/\s+/g, ' ').replace(/View Deal$/i, '').trim(); let merchantName = data.retailer || ''; if (!merchantName && externalUrl) { try { merchantName = new URL(externalUrl).hostname.replace('www.', '').split('.')[0]; merchantName = merchantName.charAt(0).toUpperCase() + merchantName.slice(1); }catch(e){ console.warn(e); } } if (!merchantName) merchantName = 'Retailer'; const q = (this.currentQuery || '').toLowerCase(); const activeTagLogic = (this.activeDealTag || '').toLowerCase(); if (q.length > 2 && q !== activeTagLogic) { const searchTarget = `${summaryTitle || ''} ${description || ''}`.toLowerCase(); if (!searchTarget.includes(q)) return; } let rawPrice = 0; let rawMsrp = 0; let currencyStr = '$'; if (isFeatured) { rawPrice = typeof data.salePrice === 'number' && data.salePrice > 0 ? data.salePrice : (typeof data.price === 'number' ? data.price : 0); rawMsrp = typeof data.salePrice === 'number' && typeof data.price === 'number' && data.price > data.salePrice ? data.price : 0; currencyStr = data.currency === 'GBP' ? '£' : '$'; } else { rawPrice = priceObj.amount ? parseFloat(priceObj.amount) : 0; rawMsrp = priceObj.amountWas ? parseFloat(priceObj.amountWas) : 0; currencyStr = priceObj.currency === 'GBP' ? '£' : '$'; } let savingAmt = 0; let savingLabel = ''; if (rawPrice > 0 && rawMsrp > rawPrice) { savingAmt = parseFloat((rawMsrp - rawPrice).toFixed(2)); savingLabel = `Save ${currencyStr}${savingAmt}`; } // Apply Brand filter if (this.selectedBrands && this.selectedBrands.length > 0) { const itemBrand = (data.brand || '').toLowerCase(); const hasMatch = this.selectedBrands.some(sb => sb.toLowerCase() === itemBrand); if (!hasMatch) return; } // Apply Price filter let priceFilterVal = null; const min = this.customPriceMin ? this.customPriceMin.value : ''; const max = this.customPriceMax ? this.customPriceMax.value : ''; if (min || max) { priceFilterVal = `${min}_${max}`; } else if (this.priceFilter && this.priceFilter.value !== 'all') { priceFilterVal = this.priceFilter.value; } if (priceFilterVal && rawPrice > 0) { if (priceFilterVal === 'under50' && rawPrice >= 50) return; if (priceFilterVal === 'over50' && rawPrice <= 50) return; if (priceFilterVal === 'over30' && rawPrice <= 30) return; if (priceFilterVal === 'over500' && rawPrice <= 500) return; if (priceFilterVal.includes('_')) { const parts = priceFilterVal.split('_'); const min = parseFloat(parts[0]); const max = parseFloat(parts[1]); if (!isNaN(min) && rawPrice max) return; } } // Apply Discount filter if (this.discountFilter && this.discountFilter.value !== 'all' && this.discountFilter.value !== '0') { const requiredDiscount = parseInt(this.discountFilter.value); if (!isNaN(requiredDiscount) && requiredDiscount > 0) { if (!rawMsrp || rawMsrp <= rawPrice) return; const ratio = Math.round((1 - (rawPrice / rawMsrp)) * 100); if (ratio 0 ? rawPrice.toString() : '', msrp: rawMsrp > 0 ? rawMsrp.toString() : '', currency: currencyStr, isCheckPrice: !rawPrice, savingLabel: savingLabel, savingType: rawMsrp > rawPrice ? 'amount' : 'none', isPrime: false, starRating: null, description: description, text: data.text || '', authorName: article.articleauthortext ? article.articleauthortext[0] : (article.articleauthor ? article.articleauthor[0] : ''), authorRole: article.articleauthorrole ? article.articleauthorrole[0] : '', authorImage: article.articleauthormedia ? article.articleauthormedia[0] : '', documentUrl: article.documenturl ? article.documenturl[0] : '', modifiedDate: article.contentmodifieddate || article.modifieddate || '' }); }); }); const airedaleBrandsList = Object.keys(overallBrandsCounts).map(b => ({ formatted_value: b, count: overallBrandsCounts[b] })).sort((a,b) => b.count - a.count); if (this.getViewMode() === 'savings_squad') { this.populateBrandDropdown(airedaleBrandsList.slice(0, 15)); if (this.brandFilterWrapper) { if (airedaleBrandsList.length === 0) { this.brandFilterWrapper.style.display = 'none'; } else { this.brandFilterWrapper.style.display = 'flex'; } } } this.deals = extractedDeals; this.sortData(); this.render(); if (typeof trackDealsAppeared !== 'undefined') { trackDealsAppeared(this.widgetId, this.deals, this.revenueId, typeof this.getAreaCode === 'function' ? (this.getAreaCode() === 'GB' ? 'GBP' : 'USD') : 'USD', this.currentQuery, this.widgetTypeName); } } isBroadQuery(query) { const q = query.toLowerCase(); const intentModifiers = ['deals', 'best', 'sale', 'under', 'cheap', 'offers', 'discount']; return intentModifiers.some(term => q.includes(term)); } async fetchHawkDeals(query) { const url = new URL(this.apiUrl); url.searchParams.append('model_name', query); const areaCode = this.getAreaCode(); if (areaCode) { url.searchParams.append('area', areaCode); } if (this.retailerSelect && this.retailerSelect.value) { url.searchParams.append('filter_merchant_name', this.retailerSelect.value); } if (this.selectedBrands && this.selectedBrands.length > 0) { url.searchParams.append('filter_label[text_brand]', this.selectedBrands.join(',')); } let priceVal = null; const min = this.customPriceMin ? this.customPriceMin.value : ''; const max = this.customPriceMax ? this.customPriceMax.value : ''; if (min || max) { priceVal = `${min}_${max}`; } else if (this.priceFilter && this.priceFilter.value !== 'all') { priceVal = this.priceFilter.value; } if (priceVal) { if (priceVal === 'under50') { url.searchParams.append('filter_max_price', '50'); } else if (priceVal === 'over50') { url.searchParams.append('filter_min_price', '50'); } else if (priceVal === 'over30') { url.searchParams.append('filter_min_price', '30'); } else if (priceVal === 'over500') { url.searchParams.append('filter_min_price', '500'); } else if (priceVal.includes('_')) { const parts = priceVal.split('_'); if (parts[0]) url.searchParams.append('filter_min_price', parts[0]); if (parts[1]) url.searchParams.append('filter_max_price', parts[1]); } } if (this.discountFilter && this.discountFilter.value !== 'all' && this.discountFilter.value !== '0') { const v = parseInt(this.discountFilter.value); if (!isNaN(v) && v > 0) { const ratio = (100 - v) / 100; url.searchParams.append('min_discount_ratio', ratio.toString()); } } if (this.offerTypeSelect && this.offerTypeSelect.value) { url.searchParams.append('offer', this.offerTypeSelect.value); } url.searchParams.append('filter_product_types', 'deals'); if (this.rowsSelect && this.rowsSelect.value) { url.searchParams.append('rows', this.rowsSelect.value); } else { url.searchParams.append('rows', '12'); // default } let response; try { response = await fetch(url.toString()); } catch(e) { if (window.location.protocol === 'file:') { console.warn("[Tom's Guide Widget] fetch from file:// blocked by local CORS policy, falling back to Adviser mock."); await this.fetchAdviserDeals(query); return; } console.warn("Hawk fetch failed", e); this.deals = []; this.render(); return; } if (!response.ok) { throw new Error('Hawk API Response Error'); } const rawData = await response.json(); // Safely locate data array from potentially wrapped response let offers = []; let modelInfoArray = []; let brandFilterData = null; if (rawData && rawData.widget && rawData.widget.data && Array.isArray(rawData.widget.data.filters)) { brandFilterData = rawData.widget.data.filters.find(f => f.type === 'label_text_brand'); } else if (rawData && rawData.data && Array.isArray(rawData.data.filters)) { brandFilterData = rawData.data.filters.find(f => f.type === 'label_text_brand'); } if (brandFilterData && Array.isArray(brandFilterData.values) && brandFilterData.values.length > 0) { this.populateBrandDropdown(brandFilterData.values); } else { if (this.brandFilterWrapper && this.selectedBrands.length === 0) { this.brandFilterWrapper.style.display = 'none'; } } if (rawData && rawData.widget && rawData.widget.data) { if (Array.isArray(rawData.widget.data.offers)) offers = rawData.widget.data.offers; if (rawData.widget.data.model_info && typeof rawData.widget.data.model_info === 'object') { modelInfoArray = Array.isArray(rawData.widget.data.model_info) ? rawData.widget.data.model_info : Object.values(rawData.widget.data.model_info); } } else if (rawData && rawData.data) { if (Array.isArray(rawData.data.offers)) offers = rawData.data.offers; if (rawData.data.model_info && typeof rawData.data.model_info === 'object') { modelInfoArray = Array.isArray(rawData.data.model_info) ? rawData.data.model_info : Object.values(rawData.data.model_info); } } else { if (Array.isArray(rawData)) offers = rawData; else if (rawData && Array.isArray(rawData.offers)) offers = rawData.offers; else if (rawData && rawData.offers && Array.isArray(rawData.offers.offer)) offers = rawData.offers.offer; else if (rawData && rawData.offers) offers = [].concat(rawData.offers); if (rawData && rawData.model_info && typeof rawData.model_info === 'object') { modelInfoArray = Array.isArray(rawData.model_info) ? rawData.model_info : Object.values(rawData.model_info); } } let modelDetails = {}; modelInfoArray.forEach(m => { const mId = m.model_id || m.id; if (mId) { modelDetails[mId] = { score: m.score != null ? parseFloat(m.score) : null, brand: m.brand || null, parent: (m.parents && Array.isArray(m.parents) && m.parents.length > 0) ? m.parents[0].name : null }; } }); offers.forEach(item => { let data = { ...item }; const mId = data.model_id; if (mId && modelDetails[mId]) { data.review_score = modelDetails[mId].score; data.model_brand = modelDetails[mId].brand; data.model_parent = modelDetails[mId].parent; } else { data.review_score = null; } let itemOffers = []; if (Array.isArray(item.offers)) itemOffers = item.offers; else if (Array.isArray(item.offer)) itemOffers = item.offer; else if (item.offers && typeof item.offers === 'object') itemOffers = [item.offers]; else if (item.offer && typeof item.offer === 'object') itemOffers = [item.offer]; if (itemOffers.length > 0) { itemOffers.forEach(subItem => { let subData = { ...item, ...subItem }; const subId = subData.model_id; if (subId && modelDetails[subId]) { subData.review_score = modelDetails[subId].score; subData.model_brand = modelDetails[subId].brand; subData.model_parent = modelDetails[subId].parent; } else if (data.review_score != null) { subData.review_score = data.review_score; } if (subData.merchant && typeof subData.merchant === 'object') { subData.merchant_name = subData.merchant.name; } this.deals.push(this.extractDealData(subData)); }); return; } if (item.merchant && typeof item.merchant === 'object') { data.merchant_name = item.merchant.name; } this.deals.push(this.extractDealData(data)); }); this.sortData(); this.render(); if (typeof trackDealsAppeared !== 'undefined') { trackDealsAppeared(this.widgetId, this.deals, this.revenueId, typeof this.getAreaCode === 'function' ? (this.getAreaCode() === 'GB' ? 'GBP' : 'USD') : 'USD', this.currentQuery, this.widgetTypeName); } } async fetchAdviserDeals(query) { // ====================================================================== // TODO: ADVISER API REPLACEMENT // The code below simulates the Adviser API response using mock data. // Once the real endpoint is ready, remove getAdviserMockData() and // perform an actual fetch() request similar to fetchHawkDeals(). // Example: // const area = this.getAreaCode(); // let apiUrl = `https://your-adviser-api.com/search?q=${query}&area=${area}`; // if (this.priceFilter && this.priceFilter.value !== 'all') { // const val = this.priceFilter.value; // if (val === 'under50') apiUrl += '&filter_max_price=50'; // else if (val === '50_100') apiUrl += '&filter_max_price=100'; // else if (val === '100_200') apiUrl += '&filter_max_price=200'; // else if (val === '200_500') apiUrl += '&filter_max_price=500'; // } // const res = await fetch(apiUrl); // const rawData = await res.json(); // ====================================================================== // Simulating network latency await new Promise(resolve => setTimeout(resolve, 400)); const rawData = this.getAdviserMockData(); let offers = []; if (rawData && rawData.data && rawData.data.Get && Array.isArray(rawData.data.Get.Deal)) { offers = rawData.data.Get.Deal; } // Basic client-side filtering for the mock if we want it to react to the query const q = query.toLowerCase(); const selectedRetailer = (this.retailerSelect && this.retailerSelect.value) ? this.retailerSelect.value.toLowerCase() : null; offers.forEach(item => { const dataObj = item; // Apply retailer filter const itemRetailer = (dataObj.dataRetailer || '').toLowerCase(); if (selectedRetailer && itemRetailer !== selectedRetailer && !itemRetailer.includes(selectedRetailer)) { return; } // Apply mock price filter let price = dataObj.dataDiscountedPrice || 0; if (typeof price === 'string') { price = parseFloat(price.replace(/[^0-9.]/g, '')); } let priceVal = null; const min = this.customPriceMin ? this.customPriceMin.value : ''; const max = this.customPriceMax ? this.customPriceMax.value : ''; if (min || max) { priceVal = `${min}_${max}`; } else if (this.priceFilter && this.priceFilter.value !== 'all') { priceVal = this.priceFilter.value; } if (priceVal) { if (priceVal === 'under50' && price >= 50) return; if (priceVal === 'over50' && price <= 50) return; if (priceVal === 'over30' && price <= 30) return; if (priceVal === 'over500' && price <= 500) return; if (priceVal.includes('_')) { const parts = priceVal.split('_'); if (parts[0] && price parseFloat(parts[1])) return; } } // Map Adviser schema to our widget's expected schema const mappedData = { url: dataObj.linkHREF || dataObj.dataLink || '#', image: dataObj.imageURL || (dataObj.image && dataObj.image.src) || '', title: dataObj.dataProduct || (dataObj.product && dataObj.product.name) || 'Product Deal', merchant: dataObj.dataRetailer || 'Retailer', price: dataObj.dataDiscountedPrice || 0, currency: dataObj.dataCurrency === 'USD' ? '$' : (dataObj.dataCurrency || '$'), msrp: dataObj.dataOriginalPrice || null }; const titleLow = mappedData.title.toLowerCase(); const merchLow = mappedData.merchant.toLowerCase(); // Smarter mock filtering let isMatch = false; if (q === '' || this.isBroadQuery(q)) { isMatch = true; } else if (titleLow.includes(q) || merchLow.includes(q)) { isMatch = true; } else if ((q.includes('laptop') || q.includes('mac') || q.includes('pc')) && (titleLow.includes('macbook') || titleLow.includes('laptop'))) { isMatch = true; } else if ((q.includes('tv') || q.includes('television')) && (titleLow.includes('tv') || titleLow.includes('oled') || titleLow.includes('qled'))) { isMatch = true; } else if ((q.includes('phone') || q.includes('smartphone')) && (titleLow.includes('galaxy') || titleLow.includes('phone'))) { isMatch = true; } else if ((q.match(/watch|fitness|run|shoe/)) && (titleLow.includes('forerunner') || titleLow.includes('saucony') || titleLow.includes('watch'))) { isMatch = true; } if (isMatch) { this.deals.push(this.extractDealData(mappedData)); } }); let rowLimit = 12; if (this.rowsSelect && this.rowsSelect.value) { rowLimit = parseInt(this.rowsSelect.value, 10) || 12; } // Intentionally omitting the slice here to allow "Load More" to work if the API returns more this.sortData(); this.render(); if (typeof trackDealsAppeared !== 'undefined') { trackDealsAppeared(this.widgetId, this.deals, this.revenueId, typeof this.getAreaCode === 'function' ? (this.getAreaCode() === 'GB' ? 'GBP' : 'USD') : 'USD', this.currentQuery, this.widgetTypeName); } } getAdviserMockData() { return { "data": { "Get": { "Deal": [ { "dataCurrency": "USD", "dataDiscountedPrice": 300, "dataOriginalPrice": 399, "dataProduct": "Samsung Galaxy A36", "dataRetailer": "Samsung", "imageURL": "https://cdn.mos.cms.futurecdn.net/MqDYsukV3JBG54te6dEs7j.jpg" }, { "dataCurrency": "USD", "dataDiscountedPrice": 14, "dataOriginalPrice": 24, "dataProduct": "Blink Mini", "dataRetailer": "Amazon", "imageURL": "http://cdn.mos.cms.futurecdn.net/3JurmAjHsDa5tPdaHAwEV8.jpg" }, { "dataCurrency": "USD", "dataDiscountedPrice": 59, "dataOriginalPrice": 99, "dataProduct": "Ring Video Doorbell", "dataRetailer": "Amazon", "imageURL": "https://cdn.mos.cms.futurecdn.net/rAh4uR7AsAsALCCLTXnLNJ.jpg" }, { "dataCurrency": "USD", "dataDiscountedPrice": 10, "dataOriginalPrice": 599, "dataProduct": "MacBook Neo", "dataRetailer": "Amazon", "imageURL": "https://cdn.mos.cms.futurecdn.net/Lg4Dvg68j9SbB5CPNrTEpH.jpg" }, { "dataCurrency": "USD", "dataDiscountedPrice": 749, "dataOriginalPrice": 849, "dataProduct": "65\\\" Fire TV Omni 4K QLED TV", "dataRetailer": "Amazon", "imageURL": "https://cdn.mos.cms.futurecdn.net/SG34ZWodUkLTxJvMTbjPYR.jpg" }, { "dataCurrency": "USD", "dataDiscountedPrice": 71, "dataOriginalPrice": 160, "dataProduct": "Saucony Hurricane 24", "dataRetailer": "Amazon", "imageURL": "https://cdn.mos.cms.futurecdn.net/vxf7UD5T2Am7guVzFoFcZ4.jpg" }, { "dataCurrency": "USD", "dataDiscountedPrice": 649, "dataOriginalPrice": 749, "dataProduct": "Garmin Forerunner 970", "dataRetailer": "Amazon", "imageURL": "https://cdn.mos.cms.futurecdn.net/3GKnEu7CdhtxPMfnPCMCiA.png" }, { "dataCurrency": "USD", "dataDiscountedPrice": 1049, "dataOriginalPrice": 1499, "dataProduct": "LG 48\\\" C4 4K OLED TV", "dataRetailer": "Amazon", "imageURL": "https://cdn.mos.cms.futurecdn.net/imvwZV9zoMD6fn9Afuge35.jpg" }, { "dataCurrency": "USD", "dataDiscountedPrice": 1499, "dataOriginalPrice": 2199, "dataProduct": "Samsung 49\\\" Odyssey Neo G9 4K Gaming Monitor", "dataRetailer": "Amazon", "imageURL": "http://cdn.mos.cms.futurecdn.net/XWDEJ5dUAE2nhK8k3Jk7k7.png" }, { "dataCurrency": "USD", "dataDiscountedPrice": 299, "dataOriginalPrice": 699, "dataProduct": "EGOHOME Black Memory Foam Mattress (queen)", "dataRetailer": "Amazon", "imageURL": "https://cdn.mos.cms.futurecdn.net/hMUemtAejNETLVYxNrktzm.jpg" } ] } } }; } decodeHTML(html) { if (!html) return ''; const txt = document.createElement("textarea"); txt.innerHTML = String(html); return txt.value; } extractDealData(item) { const priceRawStr = String(item.price || item.current_price || '0'); const msrpRawStr = String(item.was_price || item.msrp || item.original_price || '0'); const rawPrice = parseFloat(priceRawStr.replace(/[^\d.]/g, '')) || 0; const rawMsrp = parseFloat(msrpRawStr.replace(/[^\d.]/g, '')) || 0; const isCheckPrice = rawPrice === 0 || priceRawStr === '0.00' || priceRawStr === '0'; let originalImageUrl = item.image || item.image_url || item.product_image || ''; let imageUrl = originalImageUrl; if ((!imageUrl || isCheckPrice) && item.model_image_url) { imageUrl = item.model_image_url; originalImageUrl = imageUrl; } else if ((!imageUrl || isCheckPrice) && item.model_image) { imageUrl = item.model_image; originalImageUrl = imageUrl; } if (imageUrl) { imageUrl = imageUrl.replace(/-(\d+)-(\d+)(\.[a-z.]+)$/i, '$3'); } let fallbackImage = ''; if (originalImageUrl && originalImageUrl !== imageUrl) { fallbackImage = originalImageUrl; } else if (item.model_image && item.model_image !== imageUrl) { fallbackImage = item.model_image; } else if (item.model_image_url && item.model_image_url !== imageUrl) { fallbackImage = item.model_image_url; } const rawCurrency = item.currency || item.currency_symbol || '$'; let savingLabel = item.percentage_saving_label || ''; if (!savingLabel && rawMsrp > rawPrice && rawPrice > 0) { const pct = Math.round(((rawMsrp - rawPrice) / rawMsrp) * 100); if (pct > 0) { savingLabel = `${pct}% OFF`; } } const isPrime = item.shipping && item.shipping.prime === true; let scoreRaw = (item.review_score !== undefined && item.review_score !== null && item.review_score > 0) ? parseFloat(item.review_score) : null; let starRating = 0; if (scoreRaw !== null) { starRating = Math.round((scoreRaw > 10 ? scoreRaw / 20 : scoreRaw / 2) * 2) / 2; } return { id: item.offer_id || item.link || item.url || item.offer_link || Math.random().toString(), url: item.link || item.url || item.offer_link || '#', image: imageUrl, fallbackImage: fallbackImage, title: item.name || item.title || item.model_name || item.product_name || 'Unknown Product', brand: item.brand || '', productName: item.model_name || item.product_name || item.name || '', merchant: item.merchant_name || item.merchant || item.retailer || 'Retailer', price: item.price !== undefined ? String(item.price) : '0.00', currency: this.decodeHTML(rawCurrency), msrp: item.was_price || item.msrp || item.original_price || null, rawPrice: rawPrice, rawMsrp: rawMsrp, hasWasPrice: (item.was_price !== undefined && item.was_price !== null), isCheckPrice: isCheckPrice, savingLabel: savingLabel, isPrime: isPrime, starRating: starRating > 0 ? starRating : null, modelId: item.model_id || '', productKey: item.product_key || '', merchantId: (item.merchant && typeof item.merchant === 'object') ? item.merchant.id || '' : '', matchId: item.match_id || '', merchantNetwork: (item.merchant && typeof item.merchant === 'object') ? item.merchant.an || '' : '', merchantUrl: (item.merchant && typeof item.merchant === 'object') ? item.merchant.url || '' : '', modelBrand: item.model_brand || item.brand || '', modelParent: item.model_parent || '' }; } sortData() { const sortVal = this.sortSelect ? this.sortSelect.value : 'date_desc'; if (sortVal === 'price_asc') { this.deals.sort((a, b) => a.rawPrice - b.rawPrice); } else if (sortVal === 'price_desc') { this.deals.sort((a, b) => b.rawPrice - a.rawPrice); } else if (sortVal === 'discount_desc') { this.deals.sort((a, b) => { const aDiscount = a.rawMsrp > a.rawPrice ? (a.rawMsrp - a.rawPrice) : 0; const bDiscount = b.rawMsrp > b.rawPrice ? (b.rawMsrp - b.rawPrice) : 0; return bDiscount - aDiscount; }); } else if (sortVal === 'date_desc') { this.deals.sort((a, b) => { let dateA = 0; let dateB = 0; if (a && a.modifiedDate) { const valA = Array.isArray(a.modifiedDate) ? a.modifiedDate[0] : a.modifiedDate; dateA = new Date(valA).getTime(); if (isNaN(dateA)) dateA = 0; } if (b && b.modifiedDate) { const valB = Array.isArray(b.modifiedDate) ? b.modifiedDate[0] : b.modifiedDate; dateB = new Date(valB).getTime(); if (isNaN(dateB)) dateB = 0; } return dateB - dateA; }); } } getFilteredDeals() { let filteredDeals = [...this.deals]; if (this.dealModeToggle && this.dealModeToggle.checked) { filteredDeals = filteredDeals.filter(d => d.hasWasPrice || (d.msrp && d.rawMsrp > d.rawPrice)); } return filteredDeals; } showLoading() { const _div = ''; const skeletonCardHtml = ` \x3Cdiv class="tg-df-card"> \x3Cdiv class="tg-df-card-image-box"> \x3Cdiv class="tg-df-skeleton tg-df-skeleton-img">${_div} ${_div} \x3Cdiv class="tg-df-card-body"> \x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short">${_div} \x3Cdiv class="tg-df-skeleton tg-df-skeleton-text title">${_div} \x3Cdiv class="tg-df-skeleton tg-df-skeleton-text title">${_div} \x3Cdiv class="tg-df-card-footer mt-auto"> \x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short" style="height:24px;">${_div} ${_div} ${_div} \x3Cdiv class="tg-df-skeleton tg-df-skeleton-text" style="height:44px; margin:0; border-radius:0;">${_div} ${_div}`; this.grid.innerHTML = Array(4).fill(skeletonCardHtml).join(''); } showError() { const _div = ''; this.grid.innerHTML = `\x3Cdiv class="tg-df-message"> An error occurred while finding deals. Please check your connection and try again. ${_div}`; } escapeHTML(str) { if (!str) return ''; return String(str).replace(/[&'"]/g, tag => ({ '&': '&', '<': '': '>', "'": ''', '"': '"' }[tag] || tag)); } bindCouponButtons() { const btns = this.root.querySelectorAll('.tg-df-tag-coupons'); btns.forEach(btn => { btn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); const merchant = btn.getAttribute('data-merchant'); this.openVouchersModal(merchant); }); }); const closeBtn = this.root.querySelector('#tg-df-vouchers-close'); const backdrop = this.root.querySelector('#tg-df-vouchers-modal'); if (closeBtn) { closeBtn.onclick = () => this.closeVouchersModal(); } if (backdrop) { backdrop.onclick = (e) => { if (e.target === backdrop) this.closeVouchersModal(); }; } } closeVouchersModal() { const backdrop = this.root.querySelector('#tg-df-vouchers-modal'); if (backdrop) backdrop.classList.remove('active'); } async checkMerchantsCouponsBulk(merchants) { if (!merchants || merchants.length === 0) return {}; const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 4000); try { const area = this.getAreaCode(); const url = new URL('https://search-api.fie.future.net.uk/widget.php'); url.searchParams.append('model_name', 'Everything'); url.searchParams.append('language', 'en-GB'); if (area) url.searchParams.append('area', area); url.searchParams.append('combine_product_types', '1'); url.searchParams.append('filter_merchant_name', merchants.join(',')); url.searchParams.append('all_filters', 'false'); url.searchParams.append('exclude_unlabelled', 'false'); url.searchParams.append('include_specs', 'false'); url.searchParams.append('sort', 'voucher'); url.searchParams.append('distinct_merchants', 'natural'); url.searchParams.append('filter_product_types', 'vouchers,offer_deals,newsletter'); url.searchParams.append('rows', '120'); url.searchParams.append('origin', 'widgets-clientside'); let res; try { res = await fetch(url.toString(), { signal: controller.signal }); } catch (e) { return {}; } clearTimeout(timeoutId); if (!res.ok) return {}; const data = await res.json(); let offers = []; if (data && data.widget && data.widget.data && Array.isArray(data.widget.data.offers)) { offers = data.widget.data.offers; } else if (data && data.data && Array.isArray(data.data.offers)) { offers = data.data.offers; } else if (Array.isArray(data)) { offers = data; } else if (data && Array.isArray(data.offers)) { offers = data.offers; } else if (data && data.offers && Array.isArray(data.offers.offer)) { offers = data.offers.offer; } else if (data && Array.isArray(data.data)) { offers = data.data; } const foundMerchants = new Set(); offers.forEach(o => { let mName = o.merchant_name || o.merchant || o.retailer; if (mName && typeof mName === 'object') mName = mName.name; if (mName) foundMerchants.add(String(mName).toLowerCase()); }); const resultMap = {}; merchants.forEach(m => { if (m) resultMap[m] = foundMerchants.has(String(m).toLowerCase()); }); return resultMap; } catch (e) { return {}; } } async openVouchersModal(merchantName) { const backdrop = this.root.querySelector('#tg-df-vouchers-modal'); const title = this.root.querySelector('#tg-df-vouchers-title'); const content = this.root.querySelector('#tg-df-vouchers-content'); if (!backdrop || !content) return; // HACK: Hide closing tags const _div = ''; const _span = ''; const _a = ''; const _h4 = ''; const _svg = ''; const _circle = ''; const _polyline = ''; const _rect = ''; const _path = ''; title.innerText = `${merchantName} Coupons & Deals`; content.innerHTML = `\x3Cdiv class="tg-df-skeleton tg-df-skeleton-text">${_div} \x3Cdiv class="tg-df-skeleton tg-df-skeleton-text">${_div}`; backdrop.classList.add('active'); try { const area = this.getAreaCode(); const url = new URL('https://search-api.fie.future.net.uk/widget.php'); url.searchParams.append('model_name', 'Everything'); url.searchParams.append('language', 'en-GB'); if (area) url.searchParams.append('area', area); url.searchParams.append('combine_product_types', '1'); url.searchParams.append('filter_merchant_name', merchantName); url.searchParams.append('all_filters', 'false'); url.searchParams.append('exclude_unlabelled', 'false'); url.searchParams.append('include_specs', 'false'); url.searchParams.append('sort', 'voucher'); url.searchParams.append('distinct_merchants', 'natural'); url.searchParams.append('filter_product_types', 'vouchers,offer_deals,newsletter'); url.searchParams.append('rows', '50'); url.searchParams.append('origin', 'widgets-clientside'); const res = await fetch(url.toString()); if (!res.ok) throw new Error('API Error'); const data = await res.json(); let offers = []; if (data && data.widget && data.widget.data && Array.isArray(data.widget.data.offers)) { offers = data.widget.data.offers; } else if (data && data.data && Array.isArray(data.data.offers)) { offers = data.data.offers; } else if (Array.isArray(data)) { offers = data; } else if (data && Array.isArray(data.offers)) { offers = data.offers; } else if (data && data.offers && Array.isArray(data.offers.offer)) { offers = data.offers.offer; } else if (data && Array.isArray(data.data)) { offers = data.data; } if (offers.length === 0) { content.innerHTML = `\x3Cdiv class="tg-df-message">No vouchers currently available for ${this.escapeHTML(merchantName)}.${_div}`; return; } content.innerHTML = offers.map((v, idx) => { let offerObj = v; if (v.offers && v.offers.offer) { offerObj = Array.isArray(v.offers.offer) ? v.offers.offer[0] : v.offers.offer; } else if (v.offer) { offerObj = Array.isArray(v.offer) ? v.offer[0] : v.offer; } let logoUrl = v.logo_url || offerObj.logo_url || ''; if (!logoUrl && v.merchant) { if (Array.isArray(v.merchant) && v.merchant.length > 0) logoUrl = v.merchant[0].logo_url || ''; else logoUrl = v.merchant.logo_url || ''; } const offerName = offerObj.name || offerObj.title || v.name || v.title || 'Special Offer'; const endTime = offerObj.end_time || v.end_time || ''; const linkUrl = offerObj.link || offerObj.url || v.link || v.url || '#'; let foundVoucherCode = ''; const findVoucherCode = (obj) => { if (!obj || typeof obj !== 'object') return; if (obj.type === 'voucher_code' && obj.display_value) { foundVoucherCode = obj.display_value; return; } if (Array.isArray(obj)) { for (const item of obj) { findVoucherCode(item); if (foundVoucherCode) return; } } else { for (const k in obj) { if (Object.prototype.hasOwnProperty.call(obj, k)) { findVoucherCode(obj[k]); if (foundVoucherCode) return; } } } }; findVoucherCode(offerObj); if (!foundVoucherCode) findVoucherCode(v); const voucherCode = foundVoucherCode || offerObj.voucher_code || v.voucher_code || ''; const codeHtml = voucherCode ? `\x3Cspan class="tg-df-voucher-code" data-action="copy-code" data-code="${this.escapeHTML(voucherCode)}" title="Copy to clipboard"> \x3Cspan class="tg-df-voucher-code-text">${this.escapeHTML(voucherCode)}${_span} \x3Csvg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-left:6px;flex-shrink:0;" class="tg-df-voucher-copy-icon"> \x3Crect x="9" y="9" width="13" height="13" rx="2" ry="2">${_rect} \x3Cpath d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1">${_path} ${_svg} ${_span}` : ''; const logoHtml = logoUrl ? `\x3Cimg src="${this.escapeHTML(logoUrl)}" alt="${this.escapeHTML(offerName)}" class="tg-df-voucher-logo" />` : `\x3Cdiv class="tg-df-voucher-logo" style="background:#e2e8f0;">${_div}`; let expiryHtml = ''; if (endTime) { let dStr = endTime; if (!isNaN(dStr) && String(dStr).length === 10) dStr = Number(dStr) * 1000; const d = new Date(dStr); if (!isNaN(d.getTime())) { const options = { year: 'numeric', month: 'short', day: 'numeric' }; expiryHtml = ` \x3Cdiv class="tg-df-voucher-expiry"> \x3Csvg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> \x3Ccircle cx="12" cy="12" r="10">${_circle} \x3Cpolyline points="12 6 12 12 16 14">${_polyline} ${_svg} Expires ${d.toLocaleDateString(undefined, options)} ${_div}`; } } const revenueIdVal = generateRevenueId(linkUrl, offerName, merchantName, null); const rewrittenLinkUrl = rewriteAffiliateLink(linkUrl, area, revenueIdVal); return ` \x3Ca href="${this.escapeHTML(rewrittenLinkUrl)}" target="_blank" rel="noopener nofollow" class="tg-df-voucher-item" data-action="voucher-click" data-product-name="${this.escapeHTML(offerName)}" data-merchant-name="${this.escapeHTML(merchantName)}" data-analytics-id="${this.escapeHTML(offerObj.offer_id || offerObj.id || v.id || '')}" data-price="" data-previous-price="" data-original-link="${this.escapeHTML(linkUrl)}" data-revenue-id="${revenueIdVal}" data-index="${idx}" data-total="${offers.length}" data-in-stock="true" data-currency="USD" data-model-id="${this.escapeHTML(offerObj.model_id || v.model_id || offerObj.id || v.id || '')}" data-merchant-id="${this.escapeHTML(offerObj.merchant_id || offerObj.merchant?.id || '')}" > ${logoHtml} \x3Cdiv class="tg-df-voucher-content"> \x3Ch4 class="tg-df-voucher-title">${this.escapeHTML(offerName)}${_h4} ${codeHtml} ${expiryHtml} ${_div} ${_a} `; }).join(''); // Attach copy functionality const copyBtns = content.querySelectorAll('[data-action="copy-code"]'); copyBtns.forEach(btn => { btn.addEventListener('click', async (e) => { e.preventDefault(); e.stopPropagation(); const code = btn.getAttribute('data-code'); if (!code) return; try { const copyToClipboard = async (text) => { if (window.navigator.clipboard && window.isSecureContext) { try { await window.navigator.clipboard.writeText(text); return; } catch (e) {} } const textArea = document.createElement("textarea"); textArea.value = text; textArea.style.position = "fixed"; document.body.appendChild(textArea); textArea.focus(); textArea.select(); document.execCommand('copy'); textArea.remove(); }; await copyToClipboard(code); // Visual feedback btn.classList.add('copied'); const textSpan = btn.querySelector('.tg-df-voucher-code-text'); const iconSvg = btn.querySelector('.tg-df-voucher-copy-icon'); const origText = textSpan.innerText; const origIcon = iconSvg.innerHTML; textSpan.innerText = 'Copied!'; iconSvg.innerHTML = `\x3Cpolyline points="20 6 9 17 4 12">${_polyline}`; setTimeout(() => { if (btn) { btn.classList.remove('copied'); if (textSpan) textSpan.innerText = origText; if (iconSvg) iconSvg.innerHTML = origIcon; } }, 2000); trackElementInteraction({ id: 'voucher-code-copy', name: 'Copy Voucher Code', label: `Copied ${code} for ${merchantName}` }); } catch (err) { console.warn('Failed to copy text: ', err); } }); }); // Attach voucher click tracking const voucherBtns = content.querySelectorAll('[data-action="voucher-click"]'); voucherBtns.forEach(btn => { btn.addEventListener('click', (e) => { if (e.target.closest('[data-action="copy-code"]')) return; const productName = btn.getAttribute('data-product-name'); const merchantNameAttr = btn.getAttribute('data-merchant-name'); const productId = btn.getAttribute('data-analytics-id'); const price = parseFloat(btn.getAttribute('data-price')) || null; const prevPriceStr = btn.getAttribute('data-previous-price'); const previousPrice = prevPriceStr ? parseFloat(prevPriceStr) : null; const originalLink = btn.getAttribute('data-original-link'); const rewrittenLink = btn.getAttribute('href'); const revenueId = btn.getAttribute('data-revenue-id'); const index = parseInt(btn.getAttribute('data-index'), 10) || 0; const inStock = btn.getAttribute('data-in-stock') === 'true'; const totalText = btn.getAttribute('data-total'); const totalDeals = parseInt(totalText, 10) || 0; const productCategoryName = 'deals'; const trackingParams = { widgetId: this.widgetId, productCategoryName: productCategoryName, product: { modelId: btn.getAttribute('data-model-id') || null, matchId: btn.getAttribute('data-match-id') || null, brand: btn.getAttribute('data-model-brand') || null, parent: btn.getAttribute('data-model-parent') || null, name: productName, price: price, previousPrice: previousPrice, link: rewrittenLink, originalLink: originalLink, inStock: inStock }, zeroBasedProductIndexOrNull: index, totalDealsOrProducts: totalDeals, merchant: { id: btn.getAttribute('data-merchant-id') || null, network: btn.getAttribute('data-merchant-network') || null, url: btn.getAttribute('data-merchant-url') || null, name: merchantNameAttr }, revenueId: revenueId, widgetTypeName: this.widgetTypeName, isoCurrencyCode: btn.getAttribute('data-currency') || 'USD' }; if (typeof trackDealClick === 'function') { trackDealClick(trackingParams); } }); }); } catch (e) { console.warn(e); content.innerHTML = `\x3Cdiv class="tg-df-message">Failed to load vouchers.${_div}`; } } render() { try { if (this.getViewMode() === 'savings_squad' && this.airedaleTags.length > 0) { if (this.categoryFilterWrapper) { this.categoryFilterWrapper.style.display = 'flex'; } if (this.categoryFilter) { const _option = ''; let optionsHtml = `\x3Coption value="all">All Categories${_option}`; this.airedaleTags.forEach(tag => { const isSelected = this.activeDealTag === tag ? 'selected' : ''; optionsHtml += `\x3Coption value="${this.escapeHTML(tag)}" ${isSelected}>${this.escapeHTML(tag)}${_option}`; }); this.categoryFilter.innerHTML = optionsHtml; this.categoryFilter.value = this.activeDealTag || 'all'; } } else { if (this.categoryFilterWrapper) { this.categoryFilterWrapper.style.display = 'none'; } } const displayDeals = this.getFilteredDeals(); // HACK: Hide closing tags from the CMS HTML sanitizer so it doesn't strip them during in-page injection const _div = ''; const _span = ''; const _a = ''; const _h3 = ''; const _p = ''; const _strong = ''; const _sup = ''; const _button = ''; if (displayDeals.length === 0) { if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) { if (this.deals.length > 0) { this.grid.innerHTML = `\x3Cdiv class="tg-df-message"> No deals match your selected filters. ${_div}`; } else if (this.getViewMode() === 'savings_squad' && this.currentQuery.length No exact matches found for "\x3Cstrong>${this.escapeHTML(this.currentQuery)}${_strong}". Try adjusting your search term. ${_div}`; } } else { this.grid.innerHTML = `\x3Cdiv class="tg-df-message"> Search product or category names to discover the best deals from across the web. ${_div}`; } return; } let dealsHtml = displayDeals.slice(0, this.displayLimit).map((deal, index) => { try { const currencySym = this.escapeHTML(deal.currency); const isoCurrencyCode = normalizeCurrency(currencySym); const escapedPrice = this.escapeHTML(deal.price); const escapedMsrp = this.escapeHTML(deal.msrp); const areaCode = this.getAreaCode(); const revenueId = generateRevenueId(deal.url, deal.title, deal.merchant, null); const originalLink = deal.url; const rewrittenLink = rewriteAffiliateLink(deal.url, areaCode, revenueId); const productCategoryName = 'deals'; const dataAttr = ` data-action="${deal.isCheckPrice ? 'view-similar-click' : 'deal-click'}" data-analytics-id="${this.escapeHTML(deal.externalProductId || deal.id || '')}" data-product-name="${this.escapeHTML(deal.title)}" data-merchant-name="${this.escapeHTML(deal.merchant)}" data-price="${deal.rawPrice || ''}" data-previous-price="${deal.rawMsrp || ''}" data-original-link="${this.escapeHTML(originalLink)}" data-revenue-id="${revenueId}" data-index="${index}" data-total="${displayDeals.length}" data-in-stock="${deal.inStock !== false}" data-currency="${this.escapeHTML(isoCurrencyCode)}" data-model-id="${this.escapeHTML(deal.modelId || '')}" data-product-key="${this.escapeHTML(deal.productKey || '')}" data-merchant-id="${this.escapeHTML(deal.merchantId || '')}" `; let priceGroupHtml = ''; let isSavingsSquadMode = this.getViewMode() === 'savings_squad'; let ctaText = 'View Deal'; let formattedPrice = ''; let msrpHtml = ''; if (deal.isCheckPrice) { ctaText = 'View Deal'; if (isSavingsSquadMode) { priceGroupHtml = ``; } else { priceGroupHtml = ` \x3Cdiv class="tg-df-card-price-group"> \x3Cspan class="tg-df-card-price" style="font-size: 15px; font-weight: 500; font-style: italic;">See price at retailer${_span} ${_div} `; } } else { // Format Price formattedPrice = escapedPrice.includes(currencySym) ? escapedPrice : `${currencySym}${escapedPrice}`; // Format MSRP msrpHtml = deal.msrp && deal.rawMsrp > deal.rawPrice ? `\x3Cspan class="tg-df-card-msrp">${escapedMsrp.includes(currencySym) ? escapedMsrp : currencySym + escapedMsrp}${_span}` : ''; priceGroupHtml = isSavingsSquadMode ? `` : ` \x3Cdiv class="tg-df-card-price-group"> \x3Cspan class="tg-df-card-price">${formattedPrice}${_span} ${msrpHtml} ${_div} `; } const discountBadgeHtml = deal.savingLabel && !deal.isCheckPrice ? `\x3Cspan class="tg-df-card-discount-badge">${this.escapeHTML(deal.savingLabel)}${_span}` : ''; // HACK for CMS const _button = ''; const _svg = ''; const _path = ''; const _rect = ''; const _circle = ''; const _polyline = ''; const _line = ''; let badgesHtml = ''; const primeBadge = deal.isPrime ? ` \x3Cspan class="tg-df-tag tg-df-tag-prime"> \x3Csvg width="12" height="12" viewBox="0 0 24 24" fill="currentColor"> \x3Cpath d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z">${_path} ${_svg} Prime ${_span} ` : ''; const couponsBadge = deal.merchant && deal.merchant.toLowerCase().includes('amazon') ? '' : ` \x3Cdiv class="tg-df-coupon-wrapper" data-merchant="${this.escapeHTML(deal.merchant)}" style="display:inline-flex; align-items:center;"> \x3Cdiv class="tg-df-coupon-spinner">${_div} \x3Cbutton type="button" class="tg-df-tag tg-df-tag-coupons" data-action="coupons-click" data-merchant="${this.escapeHTML(deal.merchant)}" style="display:none;"> \x3Csvg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> \x3Cpath d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z">${_path} \x3Cline x1="7" y1="7" x2="7.01" y2="7">${_line} ${_svg} Coupons ${_button} ${_div} `; // Note: We always add coupons badge if there's a chance, but to allow 3-line titles we check wrapper display state badgesHtml = ` \x3Cdiv class="tg-df-card-badges"> ${primeBadge} ${couponsBadge} ${_div} `; const _linearGradient = ''; const _polygon = ''; const _stop = ''; const _defs = ''; let starHtml = ''; if (deal.starRating) { let rating = deal.starRating; if (rating > 0) { const fullStars = Math.floor(rating); const halfStar = (rating - fullStars) >= 0.5 ? 1 : 0; const emptyStars = Math.max(0, 5 - fullStars - halfStar); const blue = '#1f69ff'; // Tom's guide brand color from VIEW DEAL button const gray = '#cbd5e1'; const starSvgFull = `\x3Csvg width="14" height="14" viewBox="0 0 24 24" fill="${blue}" stroke="${blue}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">\x3Cpolygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26">${_polygon}${_svg}`; const gradId = 'half_grad_' + Math.floor(Math.random()*1000000); const starSvgHalf = `\x3Csvg width="14" height="14" viewBox="0 0 24 24" stroke="${blue}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">\x3Cdefs>\x3ClinearGradient id="${gradId}" x1="0" x2="1" y1="0" y2="0">\x3Cstop offset="50%" stop-color="${blue}">${_stop}\x3Cstop offset="50%" stop-color="transparent">${_stop}${_linearGradient}${_defs} \x3Cpolygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26" fill="url(#${gradId})">${_polygon}${_svg}`; const starSvgEmpty = `\x3Csvg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="${gray}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">\x3Cpolygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26">${_polygon}${_svg}`; let stars = []; for (let i=0; i<fullStars; i++) stars.push(starSvgFull); if (halfStar) stars.push(starSvgHalf); for (let i=0; i \x3Cspan style="margin-right:6px;">Tom's Guide:${_span} \x3Cdiv style="display:flex;gap:2px;"> ${stars.join('')} ${_div} ${_div}`; } } let htmlOutput = ''; if (isSavingsSquadMode) { htmlOutput += ` \x3Cdiv class="hawk-deal-widget-container tg-df-mobile-only" data-collapsible="true"> ${this.editorMode ? `\x3Cinput type="checkbox" class="tg-df-deal-checkbox" data-id="${this.escapeHTML(deal.id)}" ${this.selectedDeals.has(deal.id) ? 'checked' : ''} style="margin-bottom: 10px;">` : ''} \x3Cdiv class="hawk-deal-widget-wrap"> \x3Cdiv class="hawk-deal-widget-image-container"> \x3Ca data-google-interstitial="false" aria-label="View ${this.escapeHTML(deal.title)} on ${this.escapeHTML(deal.merchant)}" href="${this.escapeHTML(rewrittenLink)}" rel="sponsored noopener" target="_blank" class="hawk-affiliate-link-deal-widget" ${dataAttr}> \x3Cimg ${deal.image ? `src="${this.escapeHTML(deal.image)}"` : ''} alt="${this.escapeHTML(deal.title)}" class="hawk-lazy-image-deal-widget" loading="lazy" width="140" height="160" onerror="${deal.fallbackImage ? `if(!this.dataset.fb) { this.dataset.fb='1'; this.src='${this.escapeHTML(deal.fallbackImage)}'; } else { this.style.opacity='0'; }` : `this.style.opacity='0';`}"> ${_a} ${_div} \x3Cdiv class="hawk-deal-widget-text-cta-container"> \x3Cdiv class="hawk-deal-widget-text-body-container"> \x3Cdiv class="hawk-deal-widget-text-body-main"> \x3Ca data-google-interstitial="false" aria-label="View ${this.escapeHTML(deal.title)} on ${this.escapeHTML(deal.merchant)}" href="${this.escapeHTML(rewrittenLink)}" class="hawk-affiliate-link-container" rel="sponsored noopener" target="_blank" ${dataAttr}> ${deal.isCheckPrice ? ` \x3Cspan class="hawk-deal-widget-title-product-title">${this.escapeHTML(deal.title)}${_span} ` : ` \x3Cspan class="hawk-deal-widget-title-product-title">${deal.brand ? this.escapeHTML(deal.brand) + ' ' : ''}${this.escapeHTML(deal.productName || deal.title || '')}:${_span} `} ${_a} ${!deal.isCheckPrice && deal.rawMsrp && deal.rawMsrp > deal.rawPrice ? ` \x3Ca data-google-interstitial="false" aria-label="View ${this.escapeHTML(deal.title)} on ${this.escapeHTML(deal.merchant)}" href="${this.escapeHTML(rewrittenLink)}" class="hawk-affiliate-link-container" rel="sponsored noopener" target="_blank" ${dataAttr}> \x3Cspan class="hawk-deal-widget-title-was-price">was ${currencySym}${escapedMsrp}${_span} ${_a} ` : ''} \x3Ca data-google-interstitial="false" aria-label="View ${this.escapeHTML(deal.title)} on ${this.escapeHTML(deal.merchant)}" href="${this.escapeHTML(rewrittenLink)}" class="hawk-affiliate-link-container" rel="sponsored noopener" target="_blank" ${dataAttr}> \x3Cspan class="hawk-deal-widget-title-retailer-price"> ${!deal.isCheckPrice ? ` \x3Cspan class="hawk-deal-widget-title-price">now ${formattedPrice}${_span} \x3Cspan class="hawk-deal-widget-title-retailer"> at ${this.escapeHTML(deal.merchant)}${_span} ` : ` \x3Cspan class="hawk-deal-widget-title-price">See price at ${this.escapeHTML(deal.merchant)}${_span} `} ${_span} ${_a} ${deal.description ? `\x3Cdiv class="hawk-deal-widget-text-body-description tg-df-card-desc-container" style="margin-bottom: 12px; position: relative;"> \x3Cp class="tg-df-card-desc-content" style="font-size: 13px; color: var(--tg-df-text-muted); margin-bottom: 0; line-height: 1.4; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden;">${this.escapeHTML(deal.description)}${_p} \x3Cbutton type="button" class="tg-df-card-desc-btn" style="display: none; appearance: none; border: none; color: #000000; font-size: 11px; font-weight: 700; text-transform: uppercase; cursor: pointer; font-family: inherit; position: absolute; bottom: 2px; right: 0; background: linear-gradient(to right, transparent, #fff 20%, #fff); padding: 0 0 0 16px;" onclick=" var c = this.parentNode; if (this.dataset.expanded === 'true') { var pd = (c.tagName === 'P') ? c : this.previousElementSibling; if (c.tagName === 'P') { c.parentNode.appendChild(this); pd = c; } pd.style.display = '-webkit-box'; pd.style.webkitLineClamp = '3'; this.textContent = 'READ MORE'; this.style.position = 'absolute'; this.style.background = 'linear-gradient(to right, transparent, #fff 20%, #fff)'; this.style.paddingLeft = '16px'; this.dataset.expanded = 'false'; } else { var pd = this.previousElementSibling; pd.style.display = 'inline'; pd.style.webkitLineClamp = 'unset'; this.textContent = 'READ LESS'; this.style.position = 'static'; this.style.background = 'transparent'; this.style.paddingLeft = '4px'; this.dataset.expanded = 'true'; pd.appendChild(this); } ">READ MORE${_button} \x3C/div>` : ''} ${_div} ${_div} ${deal.authorName ? ` \x3Cdiv class="tg-df-author-line-mobile" style="padding: 0 0 12px 0; background: transparent;"> \x3Cdiv style="display: flex; align-items: center; gap: 12px;"> ${deal.authorImage ? `\x3Cimg src="${this.escapeHTML(deal.authorImage)}" alt="${this.escapeHTML(deal.authorName)}" class="tg-df-author-img" width="40" height="40" style="border-radius: 50%; object-fit: cover;">` : ''} \x3Cdiv style="display: flex; flex-direction: column;"> \x3Cdiv style="font-size: 10px; color: var(--tg-df-text-muted); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 2px; font-weight: 600;">\x3Cspan style="color: #FF6600;">${this.escapeHTML(deal.merchant)}${_span} deal recommended by:${_div} \x3Cdiv style="font-size: 11px; color: var(--tg-df-text); line-height: 1.3;"> \x3Cstrong>\x3Ca href="https://www.tomsguide.com/${this.escapeHTML(deal.documentUrl || '').replace(/^\/+/, '')}" target="_blank" rel="noopener nofollow" style="text-decoration: none; color: inherit; border-bottom: 1px dotted var(--tg-df-text-muted);">${this.escapeHTML(deal.authorName)}${_a}${_strong} ${deal.authorRole && !['null', 'nul', 'undefined'].includes(String(deal.authorRole).toLowerCase()) ? ` • ${this.escapeHTML(deal.authorRole)}` : ''} ${deal.modifiedDate ? `\x3Cdiv style="color: var(--tg-df-text-muted); margin-top: 2px;">${getTimeAgo(deal.modifiedDate)}${_div}` : ''} ${_div} ${_div} ${_div} ${_div} ` : ''} \x3Cdiv class="hawk-deal-widget-footer"> \x3Cdiv class="hawk-deal-widget-button-wrapper"> \x3Cdiv class="hawk-deal-widget-preferred-partner-wrapper"> \x3Ca data-google-interstitial="false" aria-label="View ${this.escapeHTML(deal.title)} on ${this.escapeHTML(deal.merchant)}" href="${this.escapeHTML(rewrittenLink)}" class="hawk-affiliate-link-deal-button" rel="sponsored noopener" target="_blank" ${dataAttr}> \x3Cspan>View Deal${_span} ${_a} ${_div} ${_div} ${_div} ${_div} ${_div} ${_div} `; } htmlOutput += ` \x3Cdiv class="tg-df-card ${isSavingsSquadMode ? 'tg-df-desktop-only' : ''}"> ${this.editorMode ? `\x3Cinput type="checkbox" class="tg-df-deal-checkbox" data-id="${this.escapeHTML(deal.id)}" ${this.selectedDeals.has(deal.id) ? 'checked' : ''}>` : ''} \x3Cdiv class="tg-df-card-image-box"> ${discountBadgeHtml} \x3Ca href="${this.escapeHTML(rewrittenLink)}" ${dataAttr} target="_blank" rel="noopener nofollow" style="display: flex; align-items: center; justify-content: center; width: 100%; height: 100%;"> \x3Cimg ${deal.image ? `src="${this.escapeHTML(deal.image)}"` : ''} alt="${this.escapeHTML(deal.title)}" class="tg-df-card-image" loading="lazy" onerror="${deal.fallbackImage ? `if(!this.dataset.fb) { this.dataset.fb='1'; this.src='${this.escapeHTML(deal.fallbackImage)}'; } else { this.style.opacity='0'; }` : `this.style.opacity='0';`}"> ${_a} \x3Cdiv class="tg-df-card-merchant-wrapper" style="position: absolute; bottom: 0; right: 0; background: transparent; padding: 8px 12px; z-index: 10;"> \x3Cspan class="tg-df-card-merchant-pill" style="text-align: right; margin-bottom: 0;" title="${this.escapeHTML(deal.merchant)}">${this.escapeHTML(deal.merchant)}${_span} ${_div} ${_div} \x3Cdiv class="tg-df-card-body"> ${starHtml} ${badgesHtml} \x3Ch3 class="tg-df-card-title tg-df-custom-savings-squad-title" title="${this.escapeHTML(deal.title)}"> \x3Ca href="${this.escapeHTML(rewrittenLink)}" disable-tracking="true" target="_blank" rel="noopener nofollow" style="text-decoration: none; color: inherit;"> ${isSavingsSquadMode ? (deal.isCheckPrice ? (deal.title && deal.title.includes(':') ? `\x3Cstrong>${this.escapeHTML(deal.title.substring(0, deal.title.indexOf(':') + 1))}${_strong}\x3Cspan style="color: #1f69ff; font-weight: normal;">${this.escapeHTML(deal.title.substring(deal.title.indexOf(':') + 1))}${_span}` : this.escapeHTML(deal.title) ) : `\x3Cstrong>${deal.brand ? this.escapeHTML(deal.brand) + ' ' : ''}${this.escapeHTML(deal.productName || deal.title || '')}:${_strong} ${deal.rawMsrp && deal.rawMsrp > deal.rawPrice ? `\x3Cspan style="color: #d0021b; text-decoration: line-through; font-weight: normal; margin-right: 4px;">was ${currencySym}${escapedMsrp}${_span} ` : ''}\x3Cspan style="color: #1f69ff; font-weight: normal;">now ${formattedPrice} at ${this.escapeHTML(deal.merchant)}${_span}` ) : this.escapeHTML(deal.title) } ${_a} ${_h3} ${deal.description ? `\x3Cdiv class="tg-df-card-desc-container" style="margin-bottom: 12px; position: relative;"> \x3Cp class="tg-df-card-desc-content" style="font-size: 13px; color: var(--tg-df-text-muted); margin-bottom: 0; line-height: 1.4; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden;">${this.escapeHTML(deal.description)}${_p} \x3Cbutton type="button" class="tg-df-card-desc-btn" style="display: none; appearance: none; border: none; color: #000000; font-size: 11px; font-weight: 700; text-transform: uppercase; cursor: pointer; font-family: inherit; position: absolute; bottom: 2px; right: 0; background: linear-gradient(to right, transparent, #fff 20%, #fff); padding: 0 0 0 16px;" onclick=" var c = this.parentNode; if (this.dataset.expanded === 'true') { var pd = (c.tagName === 'P') ? c : this.previousElementSibling; if (c.tagName === 'P') { c.parentNode.appendChild(this); pd = c; } pd.style.display = '-webkit-box'; pd.style.webkitLineClamp = '3'; this.textContent = 'READ MORE'; this.style.position = 'absolute'; this.style.background = 'linear-gradient(to right, transparent, #fff 20%, #fff)'; this.style.paddingLeft = '16px'; this.dataset.expanded = 'false'; } else { var pd = this.previousElementSibling; pd.style.display = 'inline'; pd.style.webkitLineClamp = 'unset'; this.textContent = 'READ LESS'; this.style.position = 'static'; this.style.background = 'transparent'; this.style.paddingLeft = '4px'; this.dataset.expanded = 'true'; pd.appendChild(this); } ">READ MORE${_button} \x3C/div>` : ''} \x3Cdiv class="tg-df-card-footer"> ${deal.authorName ? ` \x3Cdiv class="tg-df-author-line-desktop" style="padding: 0 0 ${isSavingsSquadMode ? 0 : 12}px 0;"> \x3Cdiv style="display: flex; align-items: center; gap: 10px;"> ${deal.authorImage ? `\x3Cimg src="${this.escapeHTML(deal.authorImage)}" alt="${this.escapeHTML(deal.authorName)}" class="tg-df-author-img" width="36" height="36" style="border-radius: 50%; object-fit: cover;">` : ''} \x3Cdiv style="display: flex; flex-direction: column;"> \x3Cdiv style="font-size: 10px; color: var(--tg-df-text-muted); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 2px; font-weight: 600;">Recommended by:${_div} \x3Cdiv style="font-size: 11px; color: var(--tg-df-text); line-height: 1.2;"> \x3Cstrong>\x3Ca href="https://www.tomsguide.com/${this.escapeHTML(deal.documentUrl || '').replace(/^\/+/, '')}" target="_blank" rel="noopener nofollow" style="text-decoration: none; color: inherit; border-bottom: 1px dotted var(--tg-df-text-muted);">${this.escapeHTML(deal.authorName)}${_a}${_strong} ${deal.authorRole && !['null', 'nul', 'undefined'].includes(String(deal.authorRole).toLowerCase()) ? ` • ${this.escapeHTML(deal.authorRole)}` : ''} ${deal.modifiedDate ? `\x3Cspan style="color: var(--tg-df-text-muted);"> • ${getTimeAgo(deal.modifiedDate)}${_span}` : ''} ${_div} ${_div} ${_div} ${_div} ` : ''} ${priceGroupHtml} ${_div} ${_div} \x3Ca href="${this.escapeHTML(rewrittenLink)}" ${dataAttr} target="_blank" rel="noopener nofollow" class="tg-df-card-cta" style="text-decoration: none; border-radius: 0;">${ctaText}${_a} ${_div} `; return htmlOutput; } catch (e) { console.log("Error rendering deal in map for index", index, typeof deal === 'object' ? JSON.stringify(deal) : deal, "MSG:", e.message); return ''; } }).join(''); if (displayDeals.length > this.displayLimit) { dealsHtml += ` \x3Cdiv style="width: 100%; display: flex; justify-content: center; margin-top: 16px; grid-column: 1 / -1;"> \x3Cbutton type="button" class="tg-df-tag-outline tg-df-load-more" style="padding: 8px 24px; border-radius: 100px; font-weight: 600; font-size: 14px; cursor: pointer;">Load More${_button} ${_div} `; } this.grid.innerHTML = dealsHtml; // Inject JSON-LD try { let targetNode = this.hostContainer || document.head; let jsonLdScript = targetNode.querySelector('#tg-df-json-ld-' + this.widgetId); if (!jsonLdScript) { jsonLdScript = document.createElement('script'); jsonLdScript.type = 'application/ld+json'; jsonLdScript.id = 'tg-df-json-ld-' + this.widgetId; targetNode.appendChild(jsonLdScript); } const jsonLdData = { "@context": "https://schema.org", "@type": "ItemList", "itemListElement": displayDeals.slice(0, this.displayLimit).map((deal, index) => { let isoCurrency = "USD"; if (deal.currency === '£') isoCurrency = "GBP"; else if (deal.currency === '€') isoCurrency = "EUR"; else if (deal.currency === 'A$') isoCurrency = "AUD"; else if (deal.currency === 'CA$') isoCurrency = "CAD"; const areaCode = typeof this.getAreaCode === 'function' ? this.getAreaCode() : 'US'; const revenueId = typeof generateRevenueId === 'function' ? generateRevenueId(deal.url, deal.title, deal.merchant, null) : ''; const rewrittenLink = typeof rewriteAffiliateLink === 'function' ? rewriteAffiliateLink(deal.url, areaCode, revenueId) : deal.url; return { "@type": "ListItem", "position": index + 1, "item": { "@type": "Product", "name": deal.title, "image": deal.image || "", "description": deal.description || "", "brand": { "@type": "Brand", "name": deal.brand || "" }, "offers": { "@type": "Offer", "priceCurrency": isoCurrency, "price": deal.rawPrice || 0, "url": rewrittenLink, "seller": { "@type": "Organization", "name": deal.merchant || "" } } } }; }).filter(item => item.item.name) }; jsonLdScript.textContent = JSON.stringify(jsonLdData); } catch(e) { console.warn("JSON-LD generation failed", e); } setTimeout(() => { const contents = this.root.querySelectorAll('.tg-df-card-desc-content'); contents.forEach(p => { if (p.scrollHeight > p.clientHeight || p.scrollHeight > 60) { if (p.nextElementSibling) { p.nextElementSibling.style.display = 'block'; } } }); // Allow hawklinks.js to discover and rewrite our widget links // by appending the .article-body class and manually triggering processArticle. let container = this.root.classList.contains('tg-df-container') ? this.root : this.root.querySelector('.tg-df-container'); if (container && !container.classList.contains('article-body')) { container.classList.add('article-body'); } if (this.grid && !this.grid.classList.contains('article-body')) this.grid.classList.add('article-body'); if (!this.processArticleFired) { this.processArticleFired = true; document.dispatchEvent(new CustomEvent('processArticle', { detail: { element: this.root } })); } }, 50); const loadMoreBtn = this.grid.querySelector('.tg-df-load-more'); if (loadMoreBtn) { loadMoreBtn.addEventListener('click', () => { if (typeof trackElementInteraction === 'function') { trackElementInteraction({ id: 'load-more', name: 'Load more', label: 'Load More Results' }); } this.displayLimit += 12; this.render(); }); } this.bindCouponButtons(); this.checkAndUpdateCoupons(); } catch(e) { console.warn("Widget render error", e); } } async checkAndUpdateCoupons() { const wrappers = Array.from(this.root.querySelectorAll('.tg-df-coupon-wrapper')); if (wrappers.length === 0) return; const merchants = [...new Set(wrappers.map(w => w.getAttribute('data-merchant')).filter(Boolean))]; if (merchants.length === 0) return; const couponResultsMap = await this.checkMerchantsCouponsBulk(merchants); for (const merchant of merchants) { const hasCoupons = !!couponResultsMap[merchant]; const merchantWrappers = wrappers.filter(w => w.getAttribute('data-merchant') === merchant); merchantWrappers.forEach(wrapper => { const spinner = wrapper.querySelector('.tg-df-coupon-spinner'); const btn = wrapper.querySelector('.tg-df-tag-coupons'); if (spinner) spinner.style.display = 'none'; if (hasCoupons && btn) { btn.style.display = 'inline-flex'; } else if (!hasCoupons) { wrapper.style.display = 'none'; } }); } } updateFloatingCopyBar() { if (!this.editorBar || !this.editorSelectedCount) return; if (this.editorMode && this.selectedDeals.size > 0) { this.editorBar.style.display = 'flex'; this.editorSelectedCount.innerText = this.selectedDeals.size; } else { this.editorBar.style.display = 'none'; } } async copySelectedDealsToCMS() { function htmlToSlate(htmlString) { if (!htmlString) return [{ type: 'paragraph', children: [{ text: '' }] }]; let doc; if (typeof window !== 'undefined' && window.DOMParser) { doc = new DOMParser().parseFromString(htmlString, 'text/html'); } else { doc = document.implementation.createHTMLDocument(''); doc.body.innerHTML = htmlString; } function parseNode(node, marks = {}) { if (node.nodeType === 3) { const text = node.textContent; if (!text) return null; return { text: text, ...marks }; } if (node.nodeType === 1) { const tagName = node.tagName.toLowerCase(); if (tagName === 'br') { return { type: 'line-break', children: [{ text: '' }] }; } if (tagName === 'p') { let children = Array.from(node.childNodes).map(child => parseNode(child, marks)).flat().filter(Boolean); if (children.length === 0) children.push({ text: "" }); return { type: 'paragraph', children }; } if (tagName === 'strong' || tagName === 'b') { const newMarks = { ...marks, bold: true }; return Array.from(node.childNodes).map(child => parseNode(child, newMarks)).flat().filter(Boolean); } if (tagName === 'em' || tagName === 'i') { const newMarks = { ...marks, italic: true }; return Array.from(node.childNodes).map(child => parseNode(child, newMarks)).flat().filter(Boolean); } if (tagName === 'a') { const href = node.getAttribute('href') || ''; let children = Array.from(node.childNodes).map(child => parseNode(child, marks)).flat().filter(Boolean); if (children.length === 0) children.push({ text: "" }); return { type: 'link', url: href, isNoFollow: (node.getAttribute('rel') || '').includes('nofollow'), isSponsored: (node.getAttribute('rel') || '').includes('sponsored'), isOpenNewTab: node.getAttribute('target') === '_blank', isPreventDataRewrite: false, children: children }; } return Array.from(node.childNodes).map(child => parseNode(child, marks)).flat().filter(Boolean); } return null; } let blocksArray = []; let currentParagraphChildren = []; function flushParagraph() { if (currentParagraphChildren.length > 0) { blocksArray.push({ type: 'paragraph', children: currentParagraphChildren }); currentParagraphChildren = []; } } Array.from(doc.body.childNodes).forEach(node => { const parsed = parseNode(node, {}); const parsedItems = Array.isArray(parsed) ? parsed : (parsed ? [parsed] : []); parsedItems.forEach(item => { if (item.type === 'paragraph') { flushParagraph(); blocksArray.push(item); } else { currentParagraphChildren.push(item); } }); }); flushParagraph(); if (blocksArray.length === 0) { blocksArray = [{ type: 'paragraph', children: [{ text: '' }] }]; } return blocksArray; } const blocks = []; this.editorCopyBtn.innerHTML = '\x3Cspan class="tg-df-coupon-spinner" style="display:inline-block; margin-right:8px; border-top-color:#fff;">' + ' Copying...'; for (const deal of Array.from(this.selectedDeals.values())) { const url = deal.url; const merchant = deal.merchant; const title = deal.title; const image = deal.image; const currentPrice = deal.currency + deal.rawPrice; const wasPrice = deal.hasWasPrice && deal.rawMsrp > deal.rawPrice ? deal.currency + deal.rawMsrp : ''; let couponsChildren = []; try { const area = this.getAreaCode(); const apiUrl = new URL('https://search-api.fie.future.net.uk/widget.php'); apiUrl.searchParams.append('model_name', 'Everything'); apiUrl.searchParams.append('language', 'en-GB'); apiUrl.searchParams.append('area', area); apiUrl.searchParams.append('combine_product_types', '1'); apiUrl.searchParams.append('filter_merchant_name', merchant); apiUrl.searchParams.append('all_filters', 'false'); apiUrl.searchParams.append('exclude_unlabelled', 'false'); apiUrl.searchParams.append('include_specs', 'false'); apiUrl.searchParams.append('sort', 'voucher'); apiUrl.searchParams.append('distinct_merchants', 'natural'); apiUrl.searchParams.append('filter_product_types', 'vouchers,offer_deals,newsletter'); apiUrl.searchParams.append('rows', '3'); apiUrl.searchParams.append('origin', 'widgets-clientside'); let res; try { res = await fetch(apiUrl.toString()); } catch (e) { return; } if (res.ok) { const data = await res.json(); let offers = []; if (data && data.widget && data.widget.data && Array.isArray(data.widget.data.offers)) { offers = data.widget.data.offers; } else if (data && data.data && Array.isArray(data.data.offers)) { offers = data.data.offers; } if (offers.length > 0) { couponsChildren.push({ text: "Also check out these coupons: ", bold: true }); offers.slice(0, 3).forEach((offer, idx) => { const actualOffer = offer.offer || offer; const offerName = actualOffer.name || actualOffer.title || offer.model_name || offer.title || offer.name || 'Coupon'; const linkUrl = actualOffer.link || actualOffer.url || actualOffer.offer_link || '#'; couponsChildren.push({ type: "line-break", children: [{ text: "" }] }); couponsChildren.push({ text: "🎟️ " }); couponsChildren.push({ type: "link", url: linkUrl, isNoFollow: true, isSponsored: false, isOpenNewTab: true, isPreventDataRewrite: false, children: [{ text: offerName, bold: true }] }); }); } } } catch (err) { console.warn('Failed to fetch coupons for', merchant, err); } let descriptionValue = []; if (deal.text) { descriptionValue = htmlToSlate(deal.text); } else { const dealDescriptions = [ `Don't miss out on this fantastic deal for the ${title}. It is currently available at ${merchant} for a highly competitive price.`, `We've spotted an excellent price drop on the ${title}. Grab it now at ${merchant} before it's gone.`, `The ${title} is currently seeing a generous discount over at ${merchant}. This is a perfect time to buy if you've been holding out.`, `If you're in the market for the ${title}, ${merchant} has just the deal for you.`, `Score the ${title} for less at ${merchant} right now. This is a rare chance to save big.`, `Upgrade your setup with the ${title}, now available at a stellar price via ${merchant}.` ]; const randomDescription = dealDescriptions[Math.floor(Math.random() * dealDescriptions.length)]; descriptionValue = [ { type: "paragraph", children: [{ text: randomDescription }] } ]; } if (couponsChildren.length > 0) { let lastBlock = descriptionValue[descriptionValue.length - 1]; if (lastBlock && lastBlock.type === 'paragraph') { lastBlock.children.push({ type: "line-break", children: [{ text: "" }] }); lastBlock.children.push({ type: "line-break", children: [{ text: "" }] }); lastBlock.children.push({ text: "Also check out these coupons: ", bold: true }); lastBlock.children.push({ type: "line-break", children: [{ text: "" }] }); lastBlock.children = lastBlock.children.concat(couponsChildren); } else { descriptionValue.push({ type: "paragraph", children: [ { type: "line-break", children: [{ text: "" }] }, { type: "line-break", children: [{ text: "" }] }, { text: "Also check out these coupons: ", bold: true }, { type: "line-break", children: [{ text: "" }] }, ...couponsChildren ] }); } } function normalizeCurrencyToISO(symbol) { const map = { '£': 'GBP', '$': 'USD', 'A$': 'AUD', 'CA$': 'CAD', '€': 'EUR' }; return map[symbol] || symbol; } const isoCurrency = normalizeCurrencyToISO(deal.currency); blocks.push({ id: (window.crypto && window.crypto.randomUUID) ? window.crypto.randomUUID() : 'cms-' + Date.now() + Math.random(), blockTypeName: "deal", excludeFrom: [], collapsible: false, props: { description: { value: descriptionValue, touched: false, validationMessage: "" }, image: { value: { credit: [{ type: "paragraph", children: [{ text: merchant }] }], dateCreated: Date.now(), dateModified: Date.now(), distribution: [], fileSize: 0, height: 1000, id: deal.id, imageRights: "", src: image, name: title + ".jpg", tags: [], width: 1000 }, touched: false, validationMessage: "" }, showDealButton: { value: true, touched: false, validationMessage: "" }, isPreferredPartner: { value: false, touched: false, validationMessage: "" }, linkHref: { value: url, touched: false, validationMessage: "" }, linkLabel: { value: "", touched: false, validationMessage: "" }, linkIsNoFollow: { value: true, touched: false, validationMessage: "" }, linkIsSponsored: { value: false, touched: false, validationMessage: "" }, linkIsOpenNewWindow: { value: true, touched: false, validationMessage: "" }, customPromoFlags: { value: [], touched: false, validationMessage: "" }, showStarDeal: { value: false, touched: false, validationMessage: "" }, savingType: { value: "none", touched: false, validationMessage: "" }, starDealPromoFlag: { value: "", touched: false, validationMessage: "" }, showEditorsChoice: { value: false, touched: false, validationMessage: "" }, editorsChoiceTitle: { value: "", touched: false, validationMessage: "" }, hawkPriceCurrency: { value: { value: isoCurrency, label: isoCurrency }, touched: false, validationMessage: "" }, hawkPrice: { value: deal.hasWasPrice ? String(deal.rawMsrp) : String(deal.rawPrice), touched: false, validationMessage: "" }, hawkSalePrice: { value: String(deal.rawPrice), touched: false, validationMessage: "" }, lastCheckedPriceDate: { value: "", touched: false, validationMessage: "" }, hawkModel: { touched: false, validationMessage: "" }, productId: { value: "", touched: false, validationMessage: "" }, voucherId: { value: "", touched: false, validationMessage: "" }, brand: { value: deal.brand || merchant, touched: false, validationMessage: "" }, productName: { value: title, touched: false, validationMessage: "" }, label: { value: "", touched: false, validationMessage: "" }, retailer: { value: merchant, touched: false, validationMessage: "" }, priceCheckError: false }, failedFetchError: "" }); } const payload = { type: "articleBuilderPages", data: blocks }; const jsonStr = JSON.stringify(payload); if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(jsonStr).then(() => { this.editorCopyBtn.innerHTML = 'Copied!'; setTimeout(() => { this.editorCopyBtn.innerHTML = '\x3Csvg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 6px;">\x3Crect x="9" y="9" width="13" height="13" rx="2" ry="2">\x3Cpath d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"> Copy to CMS'; }, 2000); }).catch(err => { console.warn('Failed to copy text: ', err); alert('Failed to copy deals to clipboard. See console.'); }); } else { // Fallback const textArea = document.createElement("textarea"); textArea.value = jsonStr; document.body.appendChild(textArea); textArea.focus(); textArea.select(); try { document.execCommand('copy'); this.editorCopyBtn.innerHTML = 'Copied!'; setTimeout(() => { this.editorCopyBtn.innerHTML = '\x3Csvg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 6px;">\x3Crect x="9" y="9" width="13" height="13" rx="2" ry="2">\x3Cpath d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"> Copy to CMS'; }, 2000); } catch (err) { console.warn('Fallback: Oops, unable to copy', err); alert('Fallback: Failed to copy deals to clipboard.'); } document.body.removeChild(textArea); } } } // Initialize the Widget if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => new DealsFinderWidget({ rootId: 'signal-deals-finder-root', rootNode: shadowRoot, hostContainer: hostContainer })); } else { new DealsFinderWidget({ rootId: 'signal-deals-finder-root', rootNode: shadowRoot, hostContainer: hostContainer }); } })();

This is a preview from the original publisher. Continue reading at the source:

Read Full Article on tomsguide.com →

More News