Tuesday, October 13, 2026||
MultiKhabarMultiKhabar
Home/technology/15 best affordable Asics running shoe deals with up to 56% off — including the Novablast 5, Gel-Kayano and more

15 best affordable Asics running shoe deals with up to 56% off — including the Novablast 5, Gel-Kayano and more

To
Tom's Guide
By Millie Davis-Williams
10/12/2026, 12:00:00 PM
Share:WhatsAppFacebookX (Twitter)Telegram
15 best affordable Asics running shoe deals with up to 56% off — including the Novablast 5, Gel-Kayano and more
EXECUTIVE SUMMARY

I'm sharing the best deals on Asics runinng shoes — check out the deals you can't miss to improve your PRs this fall.

Key Takeaways

  • 15 best affordable Asics running shoe deals with up to 56% off — including the Novablast 5, Gel-Kayano and more.
  • I'm sharing the best deals on Asics runinng shoes — check out the deals you can't miss to improve your PRs this fall.
  • We're just off the back of Amazon's October sale, Prime Big Deal Days.

We're just off the back of Amazon's October sale, Prime Big Deal Days. But if you missed out, don't worry — there are still plenty of Asics running shoe deals that are absolutely worth shopping.You'll find some massive discounts on certain models — like the Asics Gel-Kayano 30 for $69 (56% off.) Sure, it's an older shoe, but it still offers great support, especially if you tend to overpronate.Note: Prices can vary depending on the size and color you choose — check out any different color options in your size to find the best deals.Quick Links shop all Asics deals on AmazonAsics Novablast 5 (Men's): was $160 now from $129Asics Gel-Kayano 30 (Women's): was $160 now from $69Asics Versablast 4 Running Shoes (Women's): was $80 now from $69Asics Glideride Max (Men's): was $170 now from $80Asics Sonicblast (Men's): was $180 now from $94Asics Gel-Kayano 32 (Men's): was $165 now from $124Asics Superblast 2 (Unisex): was $199 now $149Asics ShoesNovablast 5 (Men's): was $160 now $129We voted the Asics Novablast 6 as the best running shoes you can buy right now, but before that, the Asics Novablast 5 had the top spot. They quickly stood out for their balance of comfort, support and fun, making them a pair of shoes you will actually look forward to running in. They're lightweight, durable and have a soft, springy feel underfoot that helps absorb impact while keeping runs enjoyable.View DealGel-Kayano 30 (Women's): was $160 now $69The Gel Kayano 30 running shoes are an older model, but they're amazingly cheap with 56% off. They're designed to provide advanced stability and soft cushioning for neutral runners and overpronators alike. Our Deals Editor-in-Chief, Louis Ramirez, wrote: "I've been using these shoes for almost two years now. (I loved them so much I bought a second pair months after buying my first pair). I find that they're great for casual runs on the treadmill, HIIT sessions, and long distance running outdoors."View DealVersablast 4 Running Shoes (Women's): was $80 now $69Packed with Amplifoam+ cushioning, the Asics Versablast 4 are built to be both soft and responsive — and are now a steal with this discount. The Cream/Hazy Lilac colorway is gorgeous, too.View DealGel-Contend 10 Road Running Shoes (Men's): was $80 now $69The 10th iteration of Asics' budget-friendly road running shoes have dropped in price! They feature Amplifoam+ cushioning to support your runs and have a lightweight and breathable mesh upper.View DealGel-Venture 10 (Women's): was $80 now $72These shoes are excellent for runs, with a traction pattern designed to support you on outdoor terrain. They also feature Rearfoot GEL to absorb impact and super-soft AMPLIFOAM cushioning.View DealGel-Excite 11 (Women's): was $90 now $74The Gel-Excite 11 are now on sale for a discount, bringing their price under $75. Designed for running and fitness, it brings a higher stack and a super soft and comfortable feel underfoot. The reflective details are also handy if you're working out in low light.View DealWomen's Novablast 4: was $140 now $67Old but gold. While the Novablast 6 is now available, the Novablast 4 is beloved by the Tom's Guide team — especially our Fitness Editor Jane McGuire, who wrote: "I've run over 100 miles in this shoe, and it's still one I reach for when I'm heading out for a long run."View DealGT-1000 14 Running Shoes (Women's): was $110 now $79Asics' affordable series of GT-1000 running shoes are even better with a discount. The 14th iteration of the shoe features a 3D guidance system built in to keep you stable on your feet as you run! View DealGlideride Max (Men's): was $170 now $80At 53% off, the Asics Glideride are a steal. These come with GuideSole tech to help you save energy, keeping you going for longer. You also get FF Blast Max cushioning for a lightweight, soft feel.View DealGel-Cumulus 27 (Women's): was $140 now $89The Asics Gel-Cumulus 27 often gets overlooked because of the popularity of the Novablast and Gel-Nimbus lines, but it's usually available for less than those shoes, and it's a comfortable, fairly lightweight daily trainer in its own right. We've tested the newer Gel-Cumulus 28 model, and it's not a major update on the 27, so this deal on the older shoe is much better value.View DealSonicblast Road Running Shoes (Men's): was $180 now $94Suited for both fast runs and everyday training, the Asics Sonicblast are super lightweight and propulsive. There's an Astroplate mechanism to help you shoot forward, as well as FF Blast Max and FF Turbo cushioning for a bouncy feel underfoot.View DealGel-K1011 Sneakers (Men's): was $140 now $99An iconic design and reliable comfort, what more could you want from a pair of sneakers? It takes cues from some of Asics' best shoes, with Gel-Kayano-inspired panels on the upper and the same midsole tooling system as the Gel-Cumulus 17.View DealFuji Lite 6 Trail Running Shoes (Men's): was $130 now $99The Fuji Lite 6 is a lightweight trail-running shoe that offers reliable grip and a speedy, responsive ride for your off-road runs. It's also on sale at Asics. For $99, it's a steal.View DealGel-Kayano 32 (Men's): was $165 now $124The excellent Gel Kayano 32 have been discounted to $124. These running shoes are designed to provide advanced stability and soft cushioning for neutral runners and overpronators alike. I love how they're lightweight yet provide excellent cushioning and support. In our Asics Gel-Kayano 32 review, we called the Editor's Choice shoe the most comfortable running shoes ever.View DealSuperblast 2 (Unisex): was $200 now $149In our Asics Superblast 2 review we said they were one of the best shoes of the year they released. There are newer shoes on the market, but now, you can get them for 25% off. Our testers thought the Superblast 2 was amazing for long runs, and found it to be lightweight and versatile.View Deal /** * 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 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 { 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'); 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); const showHeaderDetails = params.get('show_header_details') !== 'false'; 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' && showHeaderDetails) ? '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 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 '; 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 = 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'; } } getRowsLimit() { if (this.rowsSelect && this.rowsSelect.value) return parseInt(this.rowsSelect.value, 10); let configSource = window.location.search; if (this.hostContainer && this.hostContainer.hasAttribute('data-widget-config')) { configSource = this.hostContainer.getAttribute('data-widget-config'); } else if (typeof window !== 'undefined' && window.__WIDGET_CONFIG__) { configSource = window.__WIDGET_CONFIG__; } const params = new URLSearchParams(configSource); if (params.has('rows')) return parseInt(params.get('rows'), 10) || 12; return 12; } 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.getRowsLimit(); 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 siteParam = this.website || 'tomsguide'; const airedaleUrl = `https://airedale.futurecdn.net/feeds/feed_1781000519267.json?site=${siteParam}&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=${siteParam}&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 siteParam = this.website || 'tomsguide'; const url = `https://airedale.futurecdn.net/feeds/feed_1781000519267.json?site=${siteParam}&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 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 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'); url.searchParams.append('rows', this.getRowsLimit().toString()); 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 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 = this.getRowsLimit(); // 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 \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; flex-shrink: 0;">` : ''} \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; flex-shrink: 0;">` : ''} \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'); 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 += this.getRowsLimit(); 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 }); } })();

Share:WhatsAppFacebookX (Twitter)Telegram

Read Complete Original Article

Published originally on Tom's Guide

Visit Publisher Site →

Related News

‘डिजिटल त्रिवि’देखि ‘रिसर्च युनिभर्सिटी’सम्मः के–के हुँदैछन् नयाँ काम?
प्रविधिJust now

‘डिजिटल त्रिवि’देखि ‘रिसर्च युनिभर्सिटी’सम्मः के–के हुँदैछन् नयाँ काम?

त्रिभुवन विश्वविद्यालयले आफ्नो प्रशासनिक, शैक्षिक तथा प्राज्ञिक संरचनालाई समयानुकूल बनाउँदै डिजिटल सेवा विस्तार, अनुसन्धान प्रवर्द्धन, संस्थागत पुनःसंरचना र वित्तीय सुधारका नयाँ योजना अघि सारेको छ। त्रिविका सहकुलपति एवं शिक्षा तथा खेलकुदमन्त्री सस्मित पोखरेलको अध्यक्षतामा बसेको विश्वविद्यालय सभामा चालु आर्थिक वर्षको नीति तथा कार्यक्रम प्रस्तुत गरिएको थियो। यी योजना कार्यान्वयन भएमा विश्वविद्यालयको प्रशासनिक, शैक्षिक र प्राज्ञिक स्वरूपमा ठूलो फेरबदल आउने अपेक्षा गरिएको ...

टेक्नोलोजी खबर
इन्टरनेट प्याकेज र शुल्कमा प्राधिकरणको निगरानी, १४ सेवा प्रदायक कारबाहीमा
प्रविधिJust now

इन्टरनेट प्याकेज र शुल्कमा प्राधिकरणको निगरानी, १४ सेवा प्रदायक कारबाहीमा

काठमाडौं । नेपाल दूरसञ्चार प्राधिकरणले स्वीकृत मापदण्ड र निर्देशनविपरीत इन्टरनेट सेवा सञ्चालन गरेको भन्दै १४ वटा इन्टरनेट सेवा प्रदायक कम्पनीलाई जनही ५० हजार रुपैयाँ जरिवाना गरेको छ ।प्राधिकरणले मंगलबार जारी गरेको प्रेस विज्ञप्तिअनुसार अनुमतिपत्रका सर्त उल्लङ्घन गरेको निरीक्षण तथा जाँचबुझबाट पुष्टि भएपछि दूरसञ्चार ऐन, २०५३ को दफा ४७ (१) बमोजिम कारबाही गरिएको हो ।प्राधिकरणका प्रवक्ता मिनप्रसाद अर्यालका अनुसार स्वीकृत प्याकेजअनुसार ...

आइसिटी समाचार
नेपालमा सार्वजनिक भयो भेस्पा १५० एफआई ओबीडी २ स्टेज बी: विशेषता तथा विवरण
प्रविधिJust now

नेपालमा सार्वजनिक भयो भेस्पा १५० एफआई ओबीडी २ स्टेज बी: विशेषता तथा विवरण

२७ असोज, काठमाडौं। इटालियन स्कुटर ब्रान्ड भेस्पाले नेपाली बजारमा नयाँ १५० सिसी क्षमताको स्कुटर सार्वजनिक गरेको छ । नेपालका लागि भेस्पाको आधिकारिक वितरक डेल्टा अटोकर्प प्रालिले भेस्पा १५० एफआई ओबीडी २ स्टेज बी (Vespa 150 FI OBD 2 Stage B) सार्वजनिक गरेको हो । काठमाडौँको त्रिपुरेश्वरस्थित डेल्टा अटोकर्पको शोरुममा आयोजित विशेष कार्यक्रममार्फत नयाँ मोडल सार्वजनिक गरिएको […] The post नेपालमा सार्वजनिक ...

आइसिटी फ्रेम
आफूखुशी शुल्क बढाउने १४ इन्टरनेट सेवा प्रदायकलाई प्राधिकरणले तिरायो जरिवाना, स्वीकृति नलिई लागू गरिएका प्याकेज र शुल्क तत्काल बन्द गर्न निर्देशन
प्रविधिJust now

आफूखुशी शुल्क बढाउने १४ इन्टरनेट सेवा प्रदायकलाई प्राधिकरणले तिरायो जरिवाना, स्वीकृति नलिई लागू गरिएका प्याकेज र शुल्क तत्काल बन्द गर्न निर्देशन

नेपाल दूरसञ्चार प्राधिकरणले अनुमतिपत्रका शर्तविपरीत काम गरेको भन्दै १४ वटा इन्टरनेट सेवा प्रदायकलाई जनही ५० हजार रुपैयाँ जरिवाना गरेको छ। प्राधिकरणले आज एक विज्ञप्ति जारी गर्दै इन्टरनेट (इमेलसहित) सेवा सञ्चालन गर्न अनुमति पाएका सेवा प्रदायकले अनुमतिपत्रका सर्त तथा प्रचलित कानुनअनुसार सेवा सञ्चालन नगरेको निरीक्षण तथा जाँचबुझ प्रतिवेदनका आधारमा जरिवाना गर्ने निर्णय गरिएको जनाएको छ। दूरसञ्चार ऐन, २०५३ को दफा ४७(१) बमोजिम ती ...

टेक्नोलोजी खबर
Multikhabar App

All Information In One Place

Real-time headlines from trusted sources. Get the app now — stay updated, inspired, and ahead with breaking news!

Get it on Google Play