Shopping

Best 4th of July sales 2026: the deals our shopping experts are buying

Best 4th of July sales 2026: the deals our shopping experts are buying
Image: tomsguide.com

We're just days away from the 4th of July and while it's not summer's biggest retail holiday, 4th of July sales are still a major opportunity to save on any purchases you need to make. Major retailers like Walmart, Best Buy, and Home Depot have already launched their sales with discounts on everything from outdoor grills to 4K TVs.

One of my favorite deals this week comes courtesy of Walmart. For a limited time, you can get Walmart Plus for just $49/year. That's 50% off its full price and a rare sale that Walmart only offers twice a year.

The Northeast part of the country is expecting a major heatwave this week and right now Amazon has fans/cooling devices on sale from $9. Many items are available to ship immediately for Prime members and with summer just getting started, this week's heatwave is likely just one of many we can expect this summer.

Along with the help of the Tom's Guide Savings Squad, we'll be listing the best 4th of July sales as their released, so make sure to bookmark this page and check back often for more deals.

Quick Links

My Top Deals

Walmart Plus: was $98 now $49/year @ Walmart
50% off! Walmart Plus is a subscription service from Walmart. Priced at $98 per year (or $12.95/month), it's now on sale for just $49/year. This deal is available to new members only. Membership includes perks like unlimited free delivery on more than 160,000 items as well as fuel discounts at more than 2,000 Walmart and Murphy USA fuel stations. View Deal

Adidas sale: deals from $6 @ Amazon
From t-shirts and sneakers to hats and socks, Amazon is offering a wide variety of Adidas deals on men's, women's and children's apparel. After discount, deals start as low as $6. View Deal

Crocs sale: deals from $18 @ Amazon
It's officially Crocs season! From clogs to sandals, Amazon is discounting a wide range of Crocs shoes that are perfect for the warmer weather months. We're seeing markdowns of up to 50% off stylish and functional Crocs for the entire family. View Deal

This rechargeable neck fan designed to blow cool air, comes with five powerful speeds. Simply press the power button repeatedly, to adjust to your needs. Designed to keep you cool in comfort,it’s ultra-lights and made of anti-greasy ABS and silicone. It’s also quiet enough to hear background noise, and its charge lasts up to 16 hours. View Deal

YETI sale: deals from $17 @ Amazon
Amazon is currently knocking 30% off YETI coolers, tumblers and water bottles. If you're a fan of YETI, you know that a sale like this doesn't come around often enough. That's why now is the perfect opportunity to save big on drinkware deals starting at just $17. View Deal

Skechers shoes/apparel: from $24 @ Amazon
I've been wearing Skechers sneakers for years. In addition to being vert comfortable, I like that Skechers sneakers are generally affordable. Right now, Amazon is having a massive Skechers sale. The sale includes sneakers and apparel for men, women, and children.View Deal

Patio and garden event: up to 50% off @ Walmart
If your patio is looking drab, Walmart has a major savings event around outdoor furniture. If you're looking to upgrade your deck or graden consider Walmart's outdoor living section your one-stop shop. The retailer is offering discounts up to 50% on stylish and functional outdoor items from string lights and fire pits to lounge chairs, garden beds and more.View Deal

Amazon Haul: deals from $5 @ Amazon
Amazon Haul is an Amazon storefront featuring inexpensive deals on fashion, home, lifestyle, and more. It features over 300 million products across more than 35 product categories including brands like Columbia, Under Armour, Adidas, and more. Right now, you can get name-brand apparel from $5.View Deal

Laptop sale: from $159 @ Best Buy
Back to school season is underway and Best Buy has some of the best laptop deals I've seen this month. You can get Chromebooks from $159 or Windows machines from $339. With RAM prices spiking, this is a great opportunity to score a new laptop on the cheap.View Deal

This Dell XPS 13 with a Core Ultra 7-256V chip boasts incredible performance in a well-crafted, lightweight design. Its bright, 13.4-inch 2K screen with 120Hz refresh rate will make visuals shine. Throw in its 16GB of RAM, 512GB SSD, and exceptional battery life, lasting close to 20 hours, and you have yourself an amazing Copilot+ PC on your hands — especially with this discount. View Deal

^ Back to top

TVs

This 55-inch display gives you 4K clarity with HDR, and it keeps the same super simple Roku interface so everything loads fast. Ideal if you want a straightforward TV that just gets you into streaming without overcomplicating things.View Deal

The QM5K has built-in Google TV features for all of your streaming needs, plus Dolby Vision support. Its handiest feature is its Mini-LED backlighting, which is difficult to find at this price point. If you're going to shop in this price range, I recommend going with the QM5K over a super-cheap TV from a brand like Insignia or Onn.View Deal

The 48-inch LG B5 OLED is the cheapest OLED TV we've seen this year. We already thought it was great value at full price in our LG B5 OLED review, but now you can get it for the relatively tiny price of $599. You get amazing contrast, a colorful picture and HDMI 2.1 for 4K gaming at 120Hz.View Deal

Saving big on a new TV this 4th of July doesn’t require compromise. The U7 features thousands of MiniLED backlights that deliver precise, vibrant and lifelike pictures with pinpoint control. Every frame features bright highlights and deep blacks while advanced motion technology handles gaming with maximum impact. From high-octane gaming moments to fast-paced sports action, the U7’s display is built for clarity with no blur, lag, or stutter so that you never miss a beat.View Deal

The Samsung S90F OLED TV is an enticing option, offering a 144Hz refresh rate on one of Samsung's best upscaling processers. It also has Tizen OS, which is built with tons of free channels, not to mention several cloud gaming services, and a full Gaming Hub for all your settings in one place. The S90F also has tons of AI baked into it, like an AI Sound Pro that boosts audio dramatically.
Price check: $1,197 @ AmazonView Deal

We showered this OLED TV with praise for its sensational performance, its class-leading selection of features, and its approachable price point. The C5 delivers all of the benefits we've come to expect from OLED displays, but its design, software and overall experience is the result of LG's excellent engineering.View Deal

^ Back to top

Mattresses

The DreamCloud Classic is an exceptionally well-priced mattress, even at its usual sale price. However, there's a discount over at DreamCloud right now, which takes a queen down to $699. DreamCloud mattress sales are usually pretty static, but this is one we'd recommend jumping on before it's gone.View Deal

The Saatva Classic scored an unprecedented 5 stars in our review, hitting all the right notes for comfort and support for practically every type of sleeper. Our semi-exclusive sale access unlocks $400 off all sizes of the Classic in the current Saatva mattress sale, taking a queen down to $1,829 from $2,229 . While that is an excellent deal, we have tracked a small MSRP increase on the classic, which, until a few days ago, was priced at $2,139. Still $400 is the best deal you'll get on the best hybrid mattress we've tested.View Deal

The 4th July Bear mattress sale open to all shoppers right now knocks 35% off across the brand's mattress suite. But we've got a semi-exclusive discount code (pssst, it's TOMS40) which bags you an extra 5% off at checkout. That takes a queen size from $2,284 to $1,484 with unbeatable cooling tech, night-after-night comfort, a 120-night trial, limited lifetime warranty, and free shipping all included. View Deal

^ Back to top

Footwear

I'm absolutely loving these leopard print flip flops for spring and summer! Their effortless slip-on design fits easily for quick errands, beach strolls and everything in between. If leopard isn't your style, there's plenty more colors and patterns to choose from. View Deal

These slip-on Skechers come in a surprising amount of different colours, but this Blue is one that caught my eye. Air cooled memory foam, stretch laces and a mesh and synthetic upper make for a easy shoe to wear day-in-day-out. And it certainly doesn't hurt that there's a great saving to be made too.View Deal

The Tide 2 is all about comfort for your everyday. It has soft, springy foam to make every step feel cushioned, a flexible plate to give you a little bounce and move with ease, and a breathable mesh upper to keep your feet cool. Basically, it’s perfect for walking, running errands, or just being on your feet all day without feeling sore.View Deal

Currently 53% off, it's no wonder Tom's Guide readers are buying up the Asics Glideride. These come with GuideSole tech to help you save energy, keeping you running for longer. You also get FF Blast Max cushioning for a lightweight, soft feel.View Deal

If you’re looking for a breathable, supportive running and walking shoe, you’ve found it. With Air Zoom cushioning and plastic caging on the side, there’s plenty of support and a responsive feel, and with mesh panels and ventilation ports on the heel, air can easily flow through and keep your feet cool.View Deal

Per our testing, we think the new Brooks Ghost 18 feels very similar to the Ghost 17. So if you're a fan of Brooks' most popular trainer, I'd definitely go for this deal on the previous model and save a bit of cash.View Deal

^ Back to top

Cooling Appliances

This portable fan has three power settings, a low breeze, medium and high. On top of this, you can turn the misting setting on at any point to take advantage of the small water tank attached to it. With a handle that folds to create a stand, this fan is perfect for a bedside table, on-the-go travel or just throughout your home. It’s rechargeable and comes with a USB-C cable. Despite its small size, it boasts an impressive battery life of up to 10 hours. View Deal

The Dreo Smart Fan might not look all that big, but it's very capable of pumping out a whole lot of cold air. It's quiet at the lowest level, but will keep you cool enough as the weather gets ever hotter. It connects to the Dreo smart app as well, so you can control it with your phone.View Deal

The FlexBreeze HydroGo is reduced by 34% at Amazon right now. It dispenses water from a very fine misting mechanism which sits right at the center of the fan. And it's portable, which means you can bring it anywhere. It's available in additional colors but the white features the deepest discount of $50. View Deal

Our Editor was impressed this 42-inch smart fan helped him beat the heat. Simply control using your phone or your voice with a smart speaker, but there are also physical controls on the top. In the app, you can switch between its five included modes or create your own, turn oscillation on or off or adjust the angle the fan turns in and even change the color of its RGB night light.View Deal

^ Back to top

Appliances

While it's only a small discount, this coffee maker is certainly worth the investment, especially if you’re a fan of iced coffee. It can brew hot or iced coffee in three cup sizes, including 10, 8 and 6-oz. It includes a 36-oz water reservoir as well as an iced tumbler. View Deal

The Stilosa is De'Longhi's most affordable espresso machine. My colleague Erin Bashford, an ex-barista, gave it a lovely 4 stars in her review, and although it looks pretty plastic, it actually makes a mean cup. The model's Panarello steam wand may not be versatile enough to impress you if you're after Instagrammable latte art, though.View Deal

The self-standing PowerClean 200W Cordless Vacuum is one of Bissell's top-selling vacuums. It features powerful suction, 3-in-1 versatility, HEPA filtration, and a tangle free brush roll that's designed to take on hair and fur. It comes with three attachments: a pet upholstery tool, crevice tool, and wall mount. This is the cheapest price we've seen for it all year. View Deal

The Dreame Bladeless Tower Fan of offers whole-room cooling with the help of a built-in temperature sensor and algorithm, which automatically adjusts airflow as ambient conditions change. The smart fan can help maintain airflow and consistently support comfortable, natural cooling. It stands 3.3' tall, includes a HEPA filter, and it's suitable for rooms up to 538 sq. ft.View Deal

With 7 pre-set programs and customizable options, the Ninja CREAMi Ice Cream Maker will help you whip up gelato, milkshakes, smoothies and more, all with your favorite nuts, candy and fruit to personalize your treat. It also comes with 2 pint pots with lids. View Deal

A favorite in Dyson's cordless lineup, the V11 hits the perfect balance of power and battery life, and at 30% off, it's actually reasonable for what you're getting. The digital display, which shows remaining runtime is handy, and the auto-adjust feature means you're not constantly switching heads between floors. If you've been waiting to go cordless, this is your moment.View Deal

^ Back to top

Find more 4th of July sales

/** * 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 normalizeCurrency(symbol) { const map = { '£': 'GBP', '$': 'USD', 'A$': 'AUD', 'CA$': 'CAD', '€': 'EUR' }; return map[symbol] || symbol; } function trackElementInteraction(props) { sendEvent({ name: 'elementInteraction', data: { element: { action: props.action || "click", id: props.id || undefined, class: props.class || undefined, name: props.name || undefined, text: props.text || undefined, label: props.label || undefined, container: props.container || undefined, url: props.url || undefined, articleId: props.articleId || undefined } } }); } function generateRevenueId(url, productName, merchantName, modelId) { const str = `${window.location.href}|${productName}|${merchantName}|${modelId || ''}|${new Date().toDateString()}|tomsguide`; let hash = 0; for (let i = 0; i < str.length; i++) { const char = str.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; } let numericStr = Math.abs(hash).toString(); while (numericStr.length { let voucherPct = null; let rawPrice = parseFloat(deal.rawPrice) || parseFloat(deal.price) || null; let rawMsrp = parseFloat(deal.rawMsrp) || parseFloat(deal.msrp) || null; if (rawMsrp > rawPrice && rawPrice > 0) { voucherPct = Math.round((1 - (rawPrice / rawMsrp)) * 100); } let numId = null; if (deal.externalProductId && !isNaN(parseInt(deal.externalProductId))) { numId = parseInt(deal.externalProductId); } else if (deal.id && !isNaN(parseInt(deal.id))) { numId = parseInt(deal.id); } else { numId = deal.matchId || null; } return { product: { primary: { id: numId, name: deal.productName || deal.title || "", type: "deal", price: rawPrice, previousPrice: rawMsrp, currency: currency || 'USD', preorder: false, labels: deal.modelBrand || deal.brand ? [ { type: "brand", value: deal.modelBrand || deal.brand } ] : [], link: deal.url, originalLink: deal.url, revenueId: revenueId || null, startTime: null, endTime: null, voucherCode: null, voucherAudience: null, voucherPercentageSaving: voucherPct, voucherMoneySaving: null, voucherType: null, offerExclusive: false, offerScope: null, globalId: deal.productKey || null, inStock: deal.inStock !== false, contractProvider: null, contractMinutes: null, contractTexts: null, contractData: null, contractLength: null, contractMonthlyPrice: null, contractCurrency: currency || 'USD' } }, merchant: { id: deal.merchantId ? parseInt(deal.merchantId) : null, name: deal.merchant || "Retailer", url: deal.merchantUrl || null, network: deal.merchantNetwork || null }, model: { id: deal.modelId ? parseInt(deal.modelId) : null, brand: deal.modelBrand || deal.brand || null, name: deal.productName || deal.title || "", parent: deal.modelParent || null } }; }); trackHawkEvent({ clickType: "appeared", widgetId: widgetId, productCategoryName: "deals", zeroBasedProductIndexOrNull: null, totalDealsOrProducts: deals.length, productsArray: productsArray, queryName: queryName, widgetTypeName: widgetTypeName }); } // 1. Setup Shadow DOM Sandbox const currentScript = document.currentScript; let hostContainer = null; let template = null; if (currentScript) { let prev = currentScript.previousElementSibling; while (prev) { if (prev.tagName === 'TEMPLATE' && prev.classList.contains('tg-df-widget-template')) { template = prev; } else if (prev.tagName === 'DIV' && prev.classList.contains('tg-df-widget-host') && !prev.hasAttribute('data-initialized')) { hostContainer = prev; break; } prev = prev.previousElementSibling; } } // Fallbacks in case script is deferred if (!hostContainer) { const hosts = document.querySelectorAll('.tg-df-widget-host:not([data-initialized])'); if (hosts.length > 0) hostContainer = hosts[0]; } // Safely embedded template for CMS environments const rawTemplate = ` \x3Cstyle> /* --- Shadow DOM Base Reset --- */ *, *::before, *::after { box-sizing: border-box; } img, picture, svg, video { max-width: 100%; height: auto; display: block; } /* 1. Scoped CSS for Tom's Guide Deals Widget All classes are prefixed with \`tg-df-\` to prevent CMS style leakage. */ .tg-df-container { container-type: inline-size; container-name: tg-df; --tg-df-blue: #1F69FF; --tg-df-blue-hover: #004d8c; --tg-df-text: #222222; --tg-df-text-muted: #555555; --tg-df-bg: #ffffff; --tg-df-bg-secondary: #f4f4f4; --tg-df-border: #e2e8f0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; color: var(--tg-df-text); background-color: transparent; width: 100%; max-width: 1200px; margin: 0 auto; padding-bottom: 24px; } .tg-df-container *, .tg-df-container *::before, .tg-df-container *::after { margin: 0; padding: 0; box-sizing: border-box; } .tg-df-container img { border: none; margin: 0; padding: 0; } .tg-df-container a { text-decoration: none; color: inherit; } /* 2. Search & Filter Bar */ .tg-df-controls { display: flex; flex-direction: column; align-items: center; gap: 20px; margin-bottom: 32px; width: 100%; } .tg-df-top-bar { display: flex; width: 100%; max-width: 760px; gap: 12px; 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 { display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; } .tg-df-sort-wrapper { position: relative; display: flex; align-items: center; } .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 { 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; } .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); display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } .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; } .tg-df-container .tg-df-card-cta.tg-df-cta-savings-squad { background-color: #3c8d0d; } .tg-df-card:hover .tg-df-card-cta.tg-df-cta-savings-squad, .tg-df-card-cta.tg-df-cta-savings-squad:hover { background-color: #2b6509; } /* 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: 0; 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: 0 16px; } .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-filters { width: calc(100% + 32px); margin: 0 -16px; padding: 0 16px 4px 16px; display: flex; justify-content: flex-start; gap: 8px; flex-wrap: nowrap; overflow-x: auto; -webkit-overflow-scrolling: touch; scrollbar-width: none; } .tg-df-filters::after { content: ""; display: block; flex: 0 0 8px; } .tg-df-filters::-webkit-scrollbar { display: none; } .tg-df-sort-wrapper { flex: 0 0 max(42%, 130px); min-width: 0; } .tg-df-sort-wrapper.tg-df-price-range-wrapper { flex: 0 0 auto; min-width: max-content; } .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; -webkit-line-clamp: 3; } /* 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; -webkit-line-clamp: 3; 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; } .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: 16px; text-align: center; width: 100%; 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; } @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: #f8fafc; border-color: #cbd5e1; } .tg-df-carousel-roundels-wrapper .tg-df-carousel-scroll-left, .tg-df-carousel-roundels-wrapper .tg-df-carousel-scroll-right, .tg-df-carousel-filters-outer .tg-df-carousel-scroll-left, .tg-df-carousel-filters-outer .tg-df-carousel-scroll-right { background-color: rgba(255, 255, 255, 0.4); border: none; box-shadow: none; backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px); } .tg-df-carousel-roundels-wrapper .tg-df-carousel-scroll-left { left: 0; } .tg-df-carousel-roundels-wrapper .tg-df-carousel-scroll-right { right: 0; } .tg-df-grid-wrapper .tg-df-carousel-scroll-left { left: 0; } .tg-df-grid-wrapper .tg-df-carousel-scroll-right { right: 0; } .tg-df-carousel-roundels-wrapper .tg-df-carousel-scroll-left:hover, .tg-df-carousel-roundels-wrapper .tg-df-carousel-scroll-right:hover { background-color: rgba(255, 255, 255, 0.6); border: none; } .tg-df-container .tg-df-carousel-roundels::-webkit-scrollbar { display: none; } .tg-df-container .tg-df-carousel-roundels::after { content: ""; flex: 0 0 32px; } .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; transition: font-weight 0.2s; } .tg-df-container .tg-df-roundel.active .tg-df-roundel-label { font-weight: 700; } .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; display: inline-flex; align-items: center; justify-content: center; min-height: 36px; box-sizing: border-box; } .tg-df-container .tg-df-carousel-filter-btn svg { margin-right: 6px; } .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::after { content: ""; flex: 0 0 32px; } .tg-df-grid-wrapper { position: relative; } .tg-df-grid.carousel-compact::-webkit-scrollbar { display: none; } .tg-df-grid.carousel-compact .tg-df-load-more-card { flex: 0 0 auto; width: 100px; border-radius: 15px; box-shadow: 0 0 16px rgba(0,0,0,0.08); border: 2px solid #1e293b; background: white; color: #1e293b; display: flex; flex-direction: column; justify-content: center; align-items: center; font-weight: 600; font-size: 14px; cursor: pointer; padding: 16px; text-align: center; transition: all 0.2s; } .tg-df-grid.carousel-compact .tg-df-load-more-card:hover { background: #1e293b; color: white; } .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; -webkit-line-clamp: 2; 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 { -webkit-line-clamp: 3; } .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.45); 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: #5aaf0b; 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: #4a9109; 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; } \x3C/style> \x3C!-- Widget Container --\x3E \x3Cdiv class="tg-df-container" id="signal-deals-finder-root"> \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-roundel-skeleton">\x3Cdiv class="tg-df-roundel-img-box tg-df-skeleton">\x3C/div>\x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short" style="margin: 4px auto 0 auto; height: 13px; width: 48px;">\x3C/div>\x3C/div> \x3Cdiv class="tg-df-roundel tg-df-roundel-skeleton">\x3Cdiv class="tg-df-roundel-img-box tg-df-skeleton">\x3C/div>\x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short" style="margin: 4px auto 0 auto; height: 13px; width: 48px;">\x3C/div>\x3C/div> \x3Cdiv class="tg-df-roundel tg-df-roundel-skeleton">\x3Cdiv class="tg-df-roundel-img-box tg-df-skeleton">\x3C/div>\x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short" style="margin: 4px auto 0 auto; height: 13px; width: 48px;">\x3C/div>\x3C/div> \x3Cdiv class="tg-df-roundel tg-df-roundel-skeleton">\x3Cdiv class="tg-df-roundel-img-box tg-df-skeleton">\x3C/div>\x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short" style="margin: 4px auto 0 auto; height: 13px; width: 48px;">\x3C/div>\x3C/div> \x3Cdiv class="tg-df-roundel tg-df-roundel-skeleton">\x3Cdiv class="tg-df-roundel-img-box tg-df-skeleton">\x3C/div>\x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short" style="margin: 4px auto 0 auto; height: 13px; width: 48px;">\x3C/div>\x3C/div> \x3Cdiv class="tg-df-roundel tg-df-roundel-skeleton">\x3Cdiv class="tg-df-roundel-img-box tg-df-skeleton">\x3C/div>\x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short" style="margin: 4px auto 0 auto; height: 13px; width: 48px;">\x3C/div>\x3C/div> \x3Cdiv class="tg-df-roundel tg-df-roundel-skeleton">\x3Cdiv class="tg-df-roundel-img-box tg-df-skeleton">\x3C/div>\x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short" style="margin: 4px auto 0 auto; height: 13px; width: 48px;">\x3C/div>\x3C/div> \x3Cdiv class="tg-df-roundel tg-df-roundel-skeleton">\x3Cdiv class="tg-df-roundel-img-box tg-df-skeleton">\x3C/div>\x3Cdiv class="tg-df-skeleton tg-df-skeleton-text short" style="margin: 4px auto 0 auto; height: 13px; width: 48px;">\x3C/div>\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> \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-controls" id="tg-df-controls" style="display:flex;"> \x3Cdiv class="tg-df-top-bar"> \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> \x3Cdiv class="tg-df-settings-wrapper"> \x3Cbutton type="button" class="tg-df-settings-btn" aria-label="Settings" id="tg-df-settings-toggle"> \x3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="20" height="20"> \x3Cpath d="M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.06-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.73 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.09.63-.09.94s.02.64.06.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .43-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.49-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z"/> \x3C/svg> \x3C/button> \x3Cdiv class="tg-df-settings-dropdown-backdrop" id="tg-df-settings-backdrop">\x3C/div> \x3Cdiv class="tg-df-settings-dropdown" id="tg-df-settings-panel"> \x3Cdiv class="tg-df-setting-item"> \x3Clabel class="tg-df-setting-label">Search Region\x3C/label> \x3Cselect class="tg-df-region-select" id="tg-df-region-select"> \x3Coption value="auto">🌍 Auto-detect\x3C/option> \x3Coption value="US">🇺🇸 United States (US)\x3C/option> \x3Coption value="GB">🇬🇧 United Kingdom (UK)\x3C/option> \x3Coption value="CA">🇨🇦 Canada (CA)\x3C/option> \x3Coption value="AU">🇦🇺 Australia (AU)\x3C/option> \x3Coption value="DE">🇩🇪 Germany (DE)\x3C/option> \x3Coption value="FR">🇫🇷 France (FR)\x3C/option> \x3Coption value="IT">🇮🇹 Italy (IT)\x3C/option> \x3C/select> \x3C/div> \x3Cdiv class="tg-df-setting-item"> \x3Clabel class="tg-df-setting-label">Retailer\x3C/label> \x3Cselect class="tg-df-region-select" id="tg-df-retailer-select"> \x3Coption value="">All Retailers\x3C/option> \x3Coption value="Amazon">Amazon\x3C/option> \x3Coption value="Walmart">Walmart\x3C/option> \x3Coption value="Best Buy">Best Buy\x3C/option> \x3Coption value="Target">Target\x3C/option> \x3Coption value="John Lewis">John Lewis\x3C/option> \x3Coption value="Currys">Currys\x3C/option> \x3Coption value="Argos">Argos\x3C/option> \x3C/select> \x3C/div> \x3Cdiv class="tg-df-setting-item"> \x3Clabel class="tg-df-setting-label">Offer Type\x3C/label> \x3Cselect class="tg-df-region-select" id="tg-df-offer-type-select"> \x3Coption value="">All Offers\x3C/option> \x3Coption value="amazon_prime">Amazon Prime\x3C/option> \x3Coption value="recommended_promo">Recommended Promo\x3C/option> \x3Coption value="amazon_lightning">Amazon Lightning Deal\x3C/option> \x3C/select> \x3C/div> \x3Cdiv class="tg-df-setting-item"> \x3Clabel class="tg-df-setting-label">Result Count\x3C/label> \x3Cselect class="tg-df-region-select" id="tg-df-rows-select"> \x3Coption value="3">3 Items\x3C/option> \x3Coption value="4">4 Items\x3C/option> \x3Coption value="6">6 Items\x3C/option> \x3Coption value="12" selected>12 Items\x3C/option> \x3Coption value="24">24 Items\x3C/option> \x3Coption value="48">48 Items\x3C/option> \x3C/select> \x3C/div> \x3Cdiv class="tg-df-setting-item tg-df-dl-row"> \x3Cdiv> \x3Cspan class="tg-df-dl-row-text">Deal Mode\x3C/span> \x3Cspan class="tg-df-dl-row-subtext">Only show products with active offers or previous prices (was_price)\x3C/span> \x3C/div> \x3Clabel class="tg-df-toggle"> \x3Cinput type="checkbox" id="tg-df-deal-mode"> \x3Cspan class="tg-df-slider">\x3C/span> \x3C/label> \x3C/div> \x3Cdiv class="tg-df-setting-item tg-df-dl-row"> \x3Cdiv> \x3Cspan class="tg-df-dl-row-text">Editor Mode\x3C/span> \x3Cspan class="tg-df-dl-row-subtext">Enable multi-select to copy deals to CMS\x3C/span> \x3C/div> \x3Clabel class="tg-df-toggle"> \x3Cinput type="checkbox" id="tg-df-editor-mode"> \x3Cspan class="tg-df-slider">\x3C/span> \x3C/label> \x3C/div> \x3Cdiv class="tg-df-setting-item"> \x3Clabel class="tg-df-setting-label">View Mode\x3C/label> \x3Cselect class="tg-df-region-select" id="tg-df-view-mode-select"> \x3Coption value="auto">Auto Collection\x3C/option> \x3Coption value="carousel">Carousel\x3C/option> \x3Coption value="savings_squad">Savings Squad\x3C/option> \x3Coption value="grid">Grid (Columns)\x3C/option> \x3Coption value="row">Row (List)\x3C/option> \x3C/select> \x3C/div> \x3C/div> \x3C/div> \x3C/div> \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"> \x3Csvg class="tg-df-sort-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> \x3Cpath d="M9 3L5 6.99h3V14h2V6.99h3L9 3zm7 14.01V10h-2v7.01h-3L15 21l4-3.99h-3z"/> \x3C/svg> \x3Cselect class="tg-df-sort-select" aria-label="Sort Deals"> \x3Coption value="date_desc">Newest First\x3C/option> \x3Coption value="best_match">Sort by: Match\x3C/option> \x3Coption value="price_asc">Price Low to High\x3C/option> \x3Coption value="price_desc">Price High to Low\x3C/option> \x3Coption value="discount_desc">Biggest Discount\x3C/option> \x3C/select> \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!-- 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(); if (['carousel', 'carousel-compact', 'auto', 'grid', 'row'].includes(this.getViewMode())) { this.loadCarouselSpreadsheet(); } } getViewMode() { 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(); this.grid.classList.remove('layout-row', 'layout-grid', 'tg-df-grid-auto', 'carousel-compact', 'layout-replica-1', 'layout-replica-2'); const carouselHost = this.root.querySelector('#tg-df-carousel-host'); const controlsDiv = this.root.querySelector('#tg-df-controls'); if (mode === 'carousel' || mode === 'auto' || mode === 'grid' || mode === 'row') { if (mode === 'carousel') this.grid.classList.add('carousel-compact'); if (carouselHost) carouselHost.style.display = 'block'; if (controlsDiv) controlsDiv.style.display = 'none'; if (this.root.classList.contains('tg-df-container') && mode === 'carousel') { this.root.classList.add('is-carousel'); } else if (this.root.classList.contains('tg-df-container')) { this.root.classList.remove('is-carousel'); } } else { if (carouselHost) carouselHost.style.display = 'none'; if (controlsDiv) controlsDiv.style.display = 'flex'; if (this.root.classList.contains('tg-df-container')) { this.root.classList.remove('is-carousel'); } } if (mode === 'grid') { this.grid.classList.add('layout-grid'); } else if (mode === 'row') { this.grid.classList.add('layout-row'); } else if (mode === 'savings_squad') { this.grid.classList.add('tg-df-grid-auto', 'savings-squad-cards'); } else if (mode !== 'carousel') { this.grid.classList.add('tg-df-grid-auto'); } const settingsWrapper = this.root.querySelector('.tg-df-settings-wrapper'); if (settingsWrapper) { settingsWrapper.style.display = mode === 'auto' ? 'none' : 'block'; } if (this.customPriceWrapper) { this.customPriceWrapper.style.display = mode === 'auto' ? 'flex' : 'none'; } if (this.legacyPriceWrapper) { this.legacyPriceWrapper.style.display = mode === 'auto' ? 'none' : 'flex'; } if (this.discountFilterWrapper) { this.discountFilterWrapper.style.display = mode === 'auto' ? 'none' : 'flex'; } } initResizeObserver() { try { if (window.parent === window) return; } catch (e) { // cross origin frame check threw } const emitHeight = () => { try { const height = document.documentElement.scrollHeight || document.body.scrollHeight; const msg = { type: 'embed-size', height: height }; if (window.parent && window.parent !== window) { window.parent.postMessage(msg, '*'); window.parent.postMessage(JSON.stringify({ ...msg, sentinel: 'amp' }), '*'); } } catch (e) {} }; if (window.ResizeObserver) { try { const ro = new ResizeObserver(() => emitHeight()); ro.observe(document.body); if (this.root) ro.observe(this.root); } catch(e){ console.warn(e); } } window.addEventListener('resize', emitHeight); setTimeout(emitHeight, 300); } initCountdown() { this.cdWrapper = this.root.querySelector('#tg-df-countdown-wrapper'); let searchSource = window.location.search; if (this.hostContainer && this.hostContainer.hasAttribute('data-widget-config')) { searchSource = this.hostContainer.getAttribute('data-widget-config'); } else if (typeof window !== 'undefined' && window.__WIDGET_CONFIG__) { searchSource = window.__WIDGET_CONFIG__; } const params = new URLSearchParams(searchSource); this.showCountdown = params.get('show_countdown') === 'true'; const showHeaderDetails = params.get('show_header_details') !== 'false'; const eyebrow = this.root.querySelector('.tg-df-carousel-box-eyebrow'); const title = this.root.querySelector('.tg-df-carousel-box-title'); const subtitle = this.root.querySelector('.tg-df-carousel-box-subtitle'); if (!showHeaderDetails) { let containerElement = this.root.classList.contains('tg-df-container') ? this.root : this.root.querySelector('.tg-df-container'); if (containerElement) containerElement.classList.add('hide-header-details'); if (eyebrow) eyebrow.style.display = 'none'; if (title) title.style.display = 'none'; if (subtitle) subtitle.style.display = 'none'; } if (!this.cdWrapper) return; this.cdTitle = this.root.querySelector('#tg-df-countdown-title'); this.cdDays = this.root.querySelector('#tg-df-cd-days'); this.cdHrs = this.root.querySelector('#tg-df-cd-hrs'); this.cdMin = this.root.querySelector('#tg-df-cd-min'); this.cdSec = this.root.querySelector('#tg-df-cd-sec'); this.updateCountdown(); this.cdInterval = setInterval(() => this.updateCountdown(), 1000); } updateCountdown() { if (!this.cdWrapper) return; if (!this.showCountdown) { this.cdWrapper.style.display = 'none'; return; } const area = this.getAreaCode(); let offset = '-04:00'; if (['DE', 'FR', 'IT', 'ES', 'NL'].includes(area)) { offset = '+02:00'; } else if (['GB', 'IE', 'UK'].includes(area)) { offset = '+01:00'; } const startTime = new Date('2026-06-23T00:00:00' + offset).getTime(); const endTime = new Date('2026-06-26T00:00:00' + offset).getTime(); const now = Date.now(); let targetTime = 0; if (now < startTime) { targetTime = startTime; if (this.cdTitle) this.cdTitle.textContent = 'Prime Day starts in'; this.cdWrapper.style.display = 'flex'; } else if (now 2 || (this.getViewMode() === 'savings_squad')) { this.fetchDeals(this.currentQuery); } else { this.render(); } } updatePriceDropdownCurrency() { if (!this.priceFilter || !this.regionSelect) return; const currencySymbols = { 'US': '$', 'GB': '£', 'CA': '$CA', 'AU': '$AU', 'DE': '€', 'FR': '€', 'IT': '€', }; const area = this.getAreaCode(); const cur = currencySymbols[area || 'US'] || '$'; const options = this.priceFilter.options; for (let i = 0; i < options.length; i++) { const opt = options[i]; if (opt.value === 'all') { opt.innerText = 'All Prices'; } else if (opt.value === 'under50') { opt.innerText = `Under ${cur}50`; } else if (opt.value === '50_100') { opt.innerText = `${cur}50 - ${cur}100`; } else if (opt.value === '100_200') { opt.innerText = `${cur}100 - ${cur}200`; } else if (opt.value === '200_500') { opt.innerText = `${cur}200 - ${cur}500`; } else if (opt.value === 'over500') { opt.innerText = `Over ${cur}500`; } } } populateBrandDropdown(values) { if (!this.brandDropdown || !this.brandFilterWrapper) return; this.brandFilterWrapper.style.display = 'flex'; // show the wrapper let html = ''; const allChecked = this.selectedBrands.length === 0 ? 'checked' : ''; const _div = ''; const _span = ''; html += `\x3Cdiv class="tg-df-ms-option">\x3Cinput type="checkbox" value="" ${allChecked} class="tg-df-brand-chk"> Any Brand${_div}`; values.forEach(v => { if (!v.formatted_value || v.formatted_value === 'Any Brand') return; const isChecked = this.selectedBrands.includes(v.formatted_value) ? 'checked' : ''; html += `\x3Cdiv class="tg-df-ms-option">\x3Cinput type="checkbox" value="${this.escapeHTML(v.formatted_value)}" ${isChecked} class="tg-df-brand-chk"> ${this.escapeHTML(v.formatted_value)} \x3Cspan style="color:var(--tg-df-text-muted);font-size:12px">(${v.count || 0})${_span}${_div}`; }); this.brandDropdown.innerHTML = html; // Re-bind listeners const chks = this.brandDropdown.querySelectorAll('.tg-df-brand-chk'); chks.forEach(chk => { chk.addEventListener('change', (e) => { const val = e.target.value; if (val === '') { this.selectedBrands = []; } else { if (e.target.checked) { if (!this.selectedBrands.includes(val)) this.selectedBrands.push(val); } else { this.selectedBrands = this.selectedBrands.filter(b => b !== val); } } if (this.selectedBrands.length === 0) { this.brandTrigger.innerText = 'Any Brand'; } else if (this.selectedBrands.length === 1) { this.brandTrigger.innerText = this.selectedBrands[0]; } else { this.brandTrigger.innerText = `${this.selectedBrands.length} Brands selected`; } // Only call API if changed from UI interactions if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) { this.updateURLParams(); this.fetchDeals(this.currentQuery); } }); }); // Update button text on load if (this.selectedBrands.length === 0) { this.brandTrigger.innerText = 'Any Brand'; } else if (this.selectedBrands.length === 1) { this.brandTrigger.innerText = this.selectedBrands[0]; } else { this.brandTrigger.innerText = `${this.selectedBrands.length} Brands selected`; } } updateURLParams() { const url = new URL(window.location); if (this.currentQuery && this.currentQuery !== 'Gaming laptops') { url.searchParams.set('q', this.currentQuery); } else { url.searchParams.delete('q'); url.searchParams.delete('search'); url.searchParams.delete('query'); } if (this.regionSelect && this.regionSelect.value !== 'auto') { url.searchParams.set('region', this.regionSelect.value); } else { url.searchParams.delete('region'); } if (this.retailerSelect && this.retailerSelect.value) { url.searchParams.set('retailer', this.retailerSelect.value); } else { url.searchParams.delete('retailer'); } if (this.selectedBrands && this.selectedBrands.length > 0) { url.searchParams.set('brands', this.selectedBrands.join(',')); } else { url.searchParams.delete('brands'); } if (this.offerTypeSelect && this.offerTypeSelect.value) { url.searchParams.set('offer_type', this.offerTypeSelect.value); } else { url.searchParams.delete('offer_type'); } if (this.viewModeSelect && this.viewModeSelect.value !== 'auto') { url.searchParams.set('view_mode', this.viewModeSelect.value); } else { url.searchParams.delete('view_mode'); } if (this.rowsSelect && this.rowsSelect.value !== '12') { url.searchParams.set('rows', this.rowsSelect.value); } else { url.searchParams.delete('rows'); } const min = this.customPriceMin ? this.customPriceMin.value : ''; const max = this.customPriceMax ? this.customPriceMax.value : ''; if (min || max) { url.searchParams.set('price', `${min}_${max}`); } else if (this.priceFilter && this.priceFilter.value !== 'all') { url.searchParams.set('price', this.priceFilter.value); } else { url.searchParams.delete('price'); } if (this.discountFilter && this.discountFilter.value !== 'all' && this.discountFilter.value !== '0') { const v = parseInt(this.discountFilter.value); if (!isNaN(v) && v > 0) { const ratio = (100 - v) / 100; url.searchParams.set('min_discount_ratio', ratio.toString()); } } else { url.searchParams.delete('min_discount_ratio'); } if (this.sortSelect && this.sortSelect.value !== 'discount_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 roundels = this.root.querySelectorAll('.tg-df-carousel-cat.original-hardcoded'); 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'); } }); } let debounceTimer; if(this.searchInput) { this.searchInput.addEventListener('input', (e) => { clearTimeout(debounceTimer); const query = e.target.value.trim(); this.currentQuery = query; if (this.getViewMode() === 'savings_squad' && this.autocompleteDropdown && this.airedaleTags && query.length > 0) { const matches = this.airedaleTags.filter(t => t.toLowerCase().includes(query.toLowerCase()) && t.toLowerCase() !== query.toLowerCase()).slice(0, 5); 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'); } } else if (this.autocompleteDropdown) { this.autocompleteDropdown.classList.remove('active'); } debounceTimer = setTimeout(() => { this.updateURLParams(); if (query.length > 2) { this.fetchDeals(query); } else if (query.length === 0) { 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; this.activeDealTag = null; trackElementInteraction({ id: 'search-submit', name: 'Ask', label: 'Ask (main search)', text: query }); this.updateURLParams(); if (query.length > 2 || (this.getViewMode() === 'savings_squad')) { 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; 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 }); this.activeDealTag = null; this.currentQuery = query; this.updateURLParams(); if (query.length > 2 || (this.getViewMode() === 'savings_squad')) { this.fetchDeals(query); } }); } 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; 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 = this.viewModeSelect.value === 'savings_squad' ? 'date_desc' : 'discount_desc'; if (this.discountFilter) this.discountFilter.value = '0'; if (this.retailerSelect) this.retailerSelect.value = ''; if (this.offerTypeSelect) this.offerTypeSelect.value = ''; if (this.rowsSelect) this.rowsSelect.value = '12'; if (this.categoryFilter) this.categoryFilter.value = 'all'; this.activeDealTag = null; this.updateURLParams(); this.applyLayoutMode(); if (this.getViewMode() === 'savings_squad' || this._prevViewMode === 'savings_squad') { this.fetchDeals(this.currentQuery); } else { this.render(); } this._prevViewMode = this.viewModeSelect.value; }); } if (this.rowsSelect) { this.rowsSelect.addEventListener('change', () => { this.updateURLParams(); if (this.currentQuery.length > 2 || (this.getViewMode() === 'savings_squad')) { this.fetchDeals(this.currentQuery); } }); } if (this.dealModeToggle) { this.dealModeToggle.addEventListener('change', () => { this.updateURLParams(); this.render(); }); } if (this.editorModeToggle) { this.editorModeToggle.addEventListener('change', (e) => { this.editorMode = e.target.checked; this.render(); this.updateFloatingCopyBar(); }); } if (this.editorCopyBtn) { this.editorCopyBtn.addEventListener('click', () => { this.copySelectedDealsToCMS(); }); } if (this.editorClearBtn) { this.editorClearBtn.addEventListener('click', () => { this.selectedDeals.clear(); this.render(); this.updateFloatingCopyBar(); }); } if (this.grid) { this.grid.addEventListener('change', (e) => { if (e.target.classList.contains('tg-df-deal-checkbox')) { const dealId = e.target.getAttribute('data-id'); if (e.target.checked) { const dealObj = this.deals.find(d => d.id === dealId); if (dealObj) this.selectedDeals.set(dealId, dealObj); } else { this.selectedDeals.delete(dealId); } this.updateFloatingCopyBar(); } }); this.grid.addEventListener('click', (e) => { const dealCard = e.target.closest('[data-action="deal-click"]'); const similarCard = e.target.closest('[data-action="view-similar-click"]'); const cardLink = dealCard || similarCard; if (cardLink) { const productName = cardLink.getAttribute('data-product-name'); const merchantName = cardLink.getAttribute('data-merchant-name'); const productId = cardLink.getAttribute('data-analytics-id'); const price = parseFloat(cardLink.getAttribute('data-price')) || null; const prevPriceStr = cardLink.getAttribute('data-previous-price'); const previousPrice = prevPriceStr ? parseFloat(prevPriceStr) : null; const originalLink = cardLink.getAttribute('data-original-link'); const rewrittenLink = cardLink.getAttribute('href'); const revenueId = cardLink.getAttribute('data-revenue-id'); const index = parseInt(cardLink.getAttribute('data-index'), 10) || 0; const inStock = cardLink.getAttribute('data-in-stock') === 'true'; const totalText = cardLink.getAttribute('data-total'); const totalDeals = parseInt(totalText, 10) || 0; const productCategoryName = 'deals'; const trackingParams = { widgetId: this.widgetId, productCategoryName: productCategoryName, product: { modelId: cardLink.getAttribute('data-model-id') || null, matchId: cardLink.getAttribute('data-match-id') || null, brand: cardLink.getAttribute('data-model-brand') || null, parent: cardLink.getAttribute('data-model-parent') || null, name: productName, price: price, previousPrice: previousPrice, link: rewrittenLink, originalLink: originalLink, inStock: inStock }, zeroBasedProductIndexOrNull: index, totalDealsOrProducts: totalDeals, merchant: { id: cardLink.getAttribute('data-merchant-id') || null, network: cardLink.getAttribute('data-merchant-network') || null, url: cardLink.getAttribute('data-merchant-url') || null, name: merchantName }, revenueId: revenueId, widgetTypeName: this.widgetTypeName, isoCurrencyCode: normalizeCurrency(this.escapeHTML(cardLink.getAttribute('data-currency') || '$')) }; if (dealCard) { trackDealClick(trackingParams); } else { trackViewSimilarClick(trackingParams); } } const couponsBtn = e.target.closest('[data-action="coupons-click"]'); if (couponsBtn) { trackElementInteraction({ id: 'product-card-show-coupons', name: 'Coupons', label: `Product card coupons: ${couponsBtn.getAttribute('data-merchant')}` }); } }); } this.setupScrollListeners(); } setupScrollListeners() { const containers = [ this.root.querySelector('.tg-df-carousel-roundels'), this.root.querySelector('.tg-df-carousel-filters-wrap'), this.root.querySelector('#tg-df-grid') ]; containers.forEach(container => { if (!container) return; const checkScroll = () => { if (!container.parentElement) return; const leftBtn = container.parentElement.querySelector('.tg-df-carousel-scroll-left'); const rightBtn = container.parentElement.querySelector('.tg-df-carousel-scroll-right'); if (leftBtn) { if (container.scrollLeft <= 5) leftBtn.style.display = 'none'; else leftBtn.style.display = 'flex'; } if (rightBtn) { if (container.scrollWidth = container.scrollWidth - container.clientWidth - 5) { rightBtn.style.display = 'none'; } else { rightBtn.style.display = 'flex'; } } }; container.addEventListener('scroll', checkScroll); checkScroll(); window.addEventListener('resize', checkScroll); const observer = new MutationObserver(checkScroll); observer.observe(container, { childList: true, subtree: true, characterData: false }); }); } get widgetTypeName() { const mode = this.viewModeSelect ? this.viewModeSelect.value : (this.viewModeOverride || 'auto'); switch(mode) { case 'carousel': return 'Carousel'; case 'savings_squad': return 'Savings Squad'; case 'grid': return 'Grid'; case 'row': return 'Row'; default: return 'Auto Collection'; } } 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 loadCarouselSpreadsheet() { try { const parseCSVRow = (str) => { let result = [], cur = '', inQuotes = false; for (let i = 0; i { const rows = []; let curRow = '', inQuotes = false; for (let i = 0; i { if (rc && rc.length > 0 && rc[0]) rowsByName[rc[0]] = rc; if (rc && rc.length > 0 && rc[0] === 'Filter buttons') filterStart = i; }); const cols = []; if(rowsByName['Roundel text']) { const headerRow = rowsByName['Roundel text']; for(let col = 1; col 0) { for(let r = filterStart + 1; r 0) { const isMatched = this.carouselData.some(c => c.q === this.currentQuery || c.label === this.currentQuery); if (!isMatched) { const first = this.carouselData[0]; this.currentQuery = first.q || first.label; if (this.priceFilter) this.priceFilter.value = 'all'; if (this.customPriceMin) this.customPriceMin.value = ''; if (this.customPriceMax) this.customPriceMax.value = ''; let dPr = first.pr || 'all'; if (typeof dPr === 'string' && dPr !== 'all') { let prLower = dPr.toLowerCase(); if (prLower.includes('min') || prLower.includes('over')) { let m = dPr.match(/(\d+)/); if (m && this.customPriceMin) this.customPriceMin.value = m[1]; } else if (prLower.includes('max') || prLower.includes('under')) { let m = dPr.match(/(\d+)/); if (m && this.customPriceMax) this.customPriceMax.value = m[1]; } } let dAm = '0'; if(first.ds && typeof first.ds === 'string') { let m = first.ds.match(/(\d+)/); if(m) dAm = m[1]; } if (this.discountFilter) this.discountFilter.value = dAm; if (this.offerTypeSelect) this.offerTypeSelect.value = first.ot || ''; if (this.retailerSelect) this.retailerSelect.value = first.rt || ''; this.selectedBrands = []; if (this.brandDropdown) { const chks = this.brandDropdown.querySelectorAll('.tg-df-brand-chk'); chks.forEach(chk => chk.checked = false); } if (this.searchInput) this.searchInput.value = this.currentQuery; } } this.renderCarouselUI(); } catch(e){ console.warn(e); } } renderCarouselUI() { const roundelWrapper = this.root.querySelector('.tg-df-carousel-roundels'); if(!roundelWrapper || !this.carouselData) return; let html = ''; this.carouselData.forEach(r => { const q = r.q || r.label; const isActive = (this.currentQuery === q || this.currentQuery === r.label) ? 'active' : ''; const imgHtml = r.img ? `\x3Cimg src="${r.img}" alt="${r.label}" />` : `\x3Csvg width="32" height="32" fill="#1F69FF" viewBox="0 0 24 24">\x3Cpath d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z"/>\x3C/svg>`; html += ` \x3Cdiv class="tg-df-roundel tg-df-carousel-cat ${isActive}" data-label="${this.escapeHTML(r.label)}"> \x3Cdiv class="tg-df-roundel-img-box">${imgHtml}\x3C/div> \x3Cspan class="tg-df-roundel-label">${this.escapeHTML(r.label)}\x3C/span> \x3C/div> `; }); roundelWrapper.innerHTML = html; // Rebind clicks const roundels = this.root.querySelectorAll('.tg-df-carousel-cat'); roundels.forEach(rNode => { rNode.addEventListener('click', () => { const r = this.carouselData.find(c => c.label === rNode.getAttribute('data-label')); if(!r) return; 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: r.label, widgetTypeName: this.widgetTypeName }); } this.currentQuery = r.q || r.label; const labelTitle = this.root.querySelector('#tg-df-carousel-title-label'); if (labelTitle) labelTitle.textContent = 'Best ' + this.currentQuery; if (this.priceFilter) this.priceFilter.value = 'all'; if (this.customPriceMin) this.customPriceMin.value = ''; if (this.customPriceMax) this.customPriceMax.value = ''; let dPr = r.pr || 'all'; if (typeof dPr === 'string' && dPr !== 'all') { let prLower = dPr.toLowerCase(); if (prLower.includes('min') || prLower.includes('over')) { let m = dPr.match(/(\d+)/); if (m && this.customPriceMin) this.customPriceMin.value = m[1]; } else if (prLower.includes('max') || prLower.includes('under')) { let m = dPr.match(/(\d+)/); if (m && this.customPriceMax) this.customPriceMax.value = m[1]; } } let discountAmount = '0'; if(r.ds && typeof r.ds === 'string') { let m = r.ds.match(/(\d+)/); if(m) discountAmount = m[1]; } if (this.discountFilter) this.discountFilter.value = discountAmount; if (this.offerTypeSelect) this.offerTypeSelect.value = r.ot || ''; if (this.retailerSelect) this.retailerSelect.value = r.rt || ''; // Clear brands this.selectedBrands = []; if (this.brandDropdown) { const chks = this.brandDropdown.querySelectorAll('.tg-df-brand-chk'); chks.forEach(chk => chk.checked = false); } if (this.searchInput) this.searchInput.value = this.currentQuery; roundels.forEach(ro => ro.classList.remove('active')); if (rNode) rNode.classList.add('active'); this.renderCarouselFilters(r); this.fetchDeals(this.currentQuery); }); }); // Auto-highlight active const activeR = this.carouselData.find(c => c.q === this.currentQuery || c.label === this.currentQuery); if(activeR) this.renderCarouselFilters(activeR); } renderCarouselFilters(r) { const filtersWrap = this.root.querySelector('.tg-df-carousel-filters-wrap'); if(!filtersWrap) return; let html = `\x3Cbutton class="tg-df-carousel-filter-btn" data-type="all">All\x3C/button>`; r.filters.forEach(f => { let fL = f.toLowerCase(); let icon = ''; let logic = `data-type="custom" data-v="${this.escapeHTML(f)}"`; if (fL === 'amazon deals' || fL === 'prime deals') { html += `\x3Cbutton class="tg-df-carousel-filter-btn" ${logic}>\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>`; } else if (fL === 'lightning deals') { html += `\x3Cbutton class="tg-df-carousel-filter-btn" ${logic}>\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>`; } else { if (fL.includes('lightning')) { icon = `\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-zap">\x3Cpolygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2">\x3C/polygon>\x3C/svg>`; } else if (fL.includes('% off')) { icon = `\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>`; } else if (fL.includes('under') || fL.includes('min ')) { icon = `\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>`; } html += `\x3Cbutton class="tg-df-carousel-filter-btn" ${logic}>${icon} ${this.escapeHTML(f)}\x3C/button>`; } }); filtersWrap.innerHTML = html; const btns = filtersWrap.querySelectorAll('button'); btns.forEach(b => { b.addEventListener('click', () => { const type = b.getAttribute('data-type'); if (type === 'custom') { const v = b.getAttribute('data-v'); if (typeof trackElementInteraction === 'function') trackElementInteraction({ id: `filter-custom-${(v||'').toLowerCase().replace(/[^a-z0-9]+/g, '-')}`, name: 'Custom Filter', label: v }); } if (type === 'all') { if (typeof trackElementInteraction === 'function') trackElementInteraction({ id: 'filter-clear-all', name: 'Clear all', label: 'Clear all filters' }); // reset everything btns.forEach(btn => btn.classList.remove('active')); b.classList.add('active'); // Reset prices if (this.priceFilter) this.priceFilter.value = 'all'; if (this.customPriceMin) this.customPriceMin.value = ''; if (this.customPriceMax) this.customPriceMax.value = ''; let dPr = r.pr || 'all'; if (typeof dPr === 'string' && dPr !== 'all') { let prLower = dPr.toLowerCase(); if (prLower.includes('min') || prLower.includes('over')) { let m = dPr.match(/(\d+)/); if (m && this.customPriceMin) this.customPriceMin.value = m[1]; } else if (prLower.includes('max') || prLower.includes('under')) { let m = dPr.match(/(\d+)/); if (m && this.customPriceMax) this.customPriceMax.value = m[1]; } } let discountAmount = '0'; if(r.ds && typeof r.ds === 'string') { let m = r.ds.match(/(\d+)/); if(m) discountAmount = m[1]; } if (this.discountFilter) this.discountFilter.value = discountAmount; if (this.offerTypeSelect) this.offerTypeSelect.value = r.ot || ''; if (this.retailerSelect) this.retailerSelect.value = r.rt || ''; this.selectedBrands = []; if (this.brandDropdown) { const chks = this.brandDropdown.querySelectorAll('.tg-df-brand-chk'); chks.forEach(chk => chk.checked = false); } } else { const v = b.getAttribute('data-v'); const fL = v.toLowerCase(); let mapRet = ['amazon', 'walmart', 'best buy', 'target', 'john lewis', 'currys', 'argos']; const getCategory = (s) => { if (s === 'lightning deals' || s === 'amazon deals' || s === 'prime deals') return 'offer'; if (s.includes('% off')) return 'discount'; if (s.includes('under') || s.includes('over') || s.includes('min') || s.includes('max')) return 'price'; if (mapRet.includes(s)) return 'retailer'; return 'brand'; }; const cat = getCategory(fL); const wasActive = b.classList.contains('active'); if (cat !== 'brand') { btns.forEach(btn => { if (btn === b) return; if (btn.getAttribute('data-type') === 'all') return; const bV = btn.getAttribute('data-v'); if (!bV) return; if (getCategory(bV.toLowerCase()) === cat) btn.classList.remove('active'); }); } if (wasActive) b.classList.remove('active'); else b.classList.add('active'); let anyActive = Array.from(btns).some(btn => btn !== btns[0] && btn.classList.contains('active')); if (!anyActive) { btns[0].click(); return; } else { btns[0].classList.remove('active'); } if (this.priceFilter) this.priceFilter.value = 'all'; if (this.customPriceMin) this.customPriceMin.value = ''; if (this.customPriceMax) this.customPriceMax.value = ''; let dPr = r.pr || 'all'; if (typeof dPr === 'string' && dPr !== 'all') { let prLower = dPr.toLowerCase(); if (prLower.includes('min') || prLower.includes('over')) { let m = dPr.match(/(\d+)/); if (m && this.customPriceMin) this.customPriceMin.value = m[1]; } else if (prLower.includes('max') || prLower.includes('under')) { let m = dPr.match(/(\d+)/); if (m && this.customPriceMax) this.customPriceMax.value = m[1]; } } let discountAmount = '0'; if(r.ds && typeof r.ds === 'string') { let m = r.ds.match(/(\d+)/); if(m) discountAmount = m[1]; } if (this.discountFilter) this.discountFilter.value = discountAmount; if (this.offerTypeSelect) this.offerTypeSelect.value = r.ot || ''; if (this.retailerSelect) this.retailerSelect.value = r.rt || ''; this.selectedBrands = []; btns.forEach(btn => { if (!btn.classList.contains('active') || btn.getAttribute('data-type') === 'all') return; const vv = btn.getAttribute('data-v'); const vl = vv.toLowerCase(); if (vl === 'lightning deals') { if (this.offerTypeSelect) this.offerTypeSelect.value = 'amazon_lightning'; } else if (vl === 'amazon deals' || vl === 'prime deals') { if (this.offerTypeSelect) this.offerTypeSelect.value = 'amazon_prime'; } else if (vl.includes('% off')) { let m = vl.match(/(\d+)%/); if (m && this.discountFilter) this.discountFilter.value = m[1]; } else if (vl.includes('under') || vl.includes('max')) { let m = vl.match(/(\d+)/); if (m && this.customPriceMax) this.customPriceMax.value = m[1]; } else if (vl.includes('min') || vl.includes('over')) { let m = vl.match(/(\d+)/); if (m && this.customPriceMin) this.customPriceMin.value = m[1]; } else { let foundR = mapRet.find(x => x === vl); if (foundR) { let realR = ['Amazon', 'Walmart', 'Best Buy', 'Target', 'John Lewis', 'Currys', 'Argos'].find(x => x.toLowerCase() === vl); if (this.retailerSelect) this.retailerSelect.value = realR; } else { this.selectedBrands.push(vv); } } }); if (this.brandDropdown) { const chks = this.brandDropdown.querySelectorAll('.tg-df-brand-chk'); chks.forEach(c => c.checked = this.selectedBrands.includes(c.value)); } if (r.pr && typeof r.pr === 'string') { let prL = r.pr.toLowerCase(); if (prL.includes('under $')) { let m = prL.match(/under \$(\d+)/i); if (m && this.customPriceMax && !this.customPriceMax.value) this.customPriceMax.value = m[1]; } } } this.fetchDeals(this.currentQuery); }); }); // default to highlighting first btns[0].classList.add('active'); }async fetchDeals(query, append = false) { if (!append) { this.showLoading(); this.deals = []; this.displayLimit = this.getRowsLimit(); } else { this.displayLimit += this.getRowsLimit(); } try { if (this.getViewMode() === 'savings_squad') { await this.fetchSavingsSquad(append); } else { if (this.isBroadQuery(query)) { await this.fetchAdviserDeals(query, append); } else { await this.fetchHawkDeals(query, append); if (this.deals.length === 0) { await this.fetchAdviserDeals(query, append); } } } } catch (error) { console.warn("[Tom's Guide Widget] Fetch error:", error); this.showError(); } } async fetchSavingsSquad() { let topArticles = this.airedaleArticles; if (!topArticles) { const airedaleUrl = `https://airedale.futurecdn.net/feeds/feed_1776420579726.json?site=tomsguide&articleType=deals&limit=50`; let res; try { res = await fetch(airedaleUrl); } catch(e) { try { res = await fetch(`https://airedale.futurecdn.net/feeds/feed_1776420579726.json?site=tomsguide&articleType=deals&limit=50`); } catch (err) { console.warn("Fallback fetch failed", err); return; } } if (!res.ok) throw new Error('Airedale API Error'); const articles = await res.json(); topArticles = Array.isArray(articles) ? articles.slice(0, 50) : ((articles.data && Array.isArray(articles.data)) ? articles.data.slice(0, 50) : []); this.airedaleArticles = topArticles; let tagCounts = {}; topArticles.forEach((a) => { let articleTags = new Set(); if (a.articlecategory && Array.isArray(a.articlecategory)) { a.articlecategory.forEach((t) => articleTags.add(t)); } articleTags.forEach(t => { tagCounts[t] = (tagCounts[t] || 0) + 1; }); }); this.airedaleTags = Object.keys(tagCounts).sort((a, b) => tagCounts[b] - tagCounts[a]); this.airedaleTagCounts = tagCounts; } let targetArticles = topArticles; if (this.activeDealTag) { const cleanTag = this.activeDealTag.toLowerCase().replace(/&/g, '').replace(/[^a-z0-9]+/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, ''); const encodedTag = encodeURIComponent(cleanTag); const url = `https://airedale.futurecdn.net/feeds/feed_1776420579726.json?site=tomsguide&articleType=deals&limit=50&articleCategoryHandle=${encodedTag}`; try { const res = await fetch(url); if (res.ok) { const articles = await res.json(); targetArticles = Array.isArray(articles) ? articles.slice(0, 50) : ((articles.data && Array.isArray(articles.data)) ? articles.data.slice(0, 50) : []); } } catch(e) { console.warn("Failed to fetch by activeDealTag", e); } } let extractedDeals = []; let dynamicBrandsCounts = {}; 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(); dynamicBrandsCounts[data.brand] = (dynamicBrandsCounts[data.brand] || 0) + 1; } const externalUrl = isFeatured ? data.url : (link.href || null); let summaryTitle = isFeatured ? (data.name || data.brand) : (data.productName || link.label || article.articlename); let description = isFeatured ? (data.strapline || '') : (data.text || ''); if (!isFeatured && !data.productName && data.text) { const brSplit = data.text.split(new RegExp('\x3Cbr\\s*\\/?\\x3E', 'i')); if (brSplit.length > 1) { summaryTitle = brSplit[0].replace(/]+>/g, '').trim(); description = brSplit.slice(1).join(' ').replace(/
/gi, ' ').replace(/]*>/gi, ' ').replace(/]+>/g, '').replace(/\s+/g, ' ').trim(); } else { const match = data.text.match(/\x3Cstrong>(.*?)/); if (match) { summaryTitle = match[1].replace(/]+>/g, '').trim(); if (summaryTitle.endsWith(':')) summaryTitle = summaryTitle.slice(0, -1); } } } let imageUrl = isFeatured ? image.mos : (image.src || null); if (imageUrl && imageUrl.startsWith('//')) imageUrl = 'https:' + imageUrl; description = description.replace(/
/gi, ' ').replace(/]*>/gi, ' ').replace(/]+>/g, '').replace(/\s+/g, ' ').replace(/View Deal$/i, '').trim(); let merchantName = data.retailer || ''; if (!merchantName && externalUrl) { try { merchantName = new URL(externalUrl).hostname.replace('www.', '').split('.')[0]; merchantName = merchantName.charAt(0).toUpperCase() + merchantName.slice(1); }catch(e){ console.warn(e); } } if (!merchantName) merchantName = 'Retailer'; const q = (this.currentQuery || '').toLowerCase(); const activeTagLogic = (this.activeDealTag || '').toLowerCase(); if (q.length > 2 && q !== activeTagLogic) { const searchTarget = `${summaryTitle || ''} ${description || ''}`.toLowerCase(); if (!searchTarget.includes(q)) return; } let rawPrice = 0; let rawMsrp = 0; let currencyStr = '$'; if (isFeatured) { rawPrice = typeof data.salePrice === 'number' && data.salePrice > 0 ? data.salePrice : (typeof data.price === 'number' ? data.price : 0); rawMsrp = typeof data.salePrice === 'number' && typeof data.price === 'number' && data.price > data.salePrice ? data.price : 0; currencyStr = data.currency === 'GBP' ? '£' : '$'; } else { rawPrice = priceObj.amount ? parseFloat(priceObj.amount) : 0; rawMsrp = priceObj.amountWas ? parseFloat(priceObj.amountWas) : 0; currencyStr = priceObj.currency === 'GBP' ? '£' : '$'; } let savingAmt = 0; let savingLabel = ''; if (rawPrice > 0 && rawMsrp > rawPrice) { savingAmt = parseFloat((rawMsrp - rawPrice).toFixed(2)); savingLabel = `Save ${currencyStr}${savingAmt}`; } // Apply Brand filter if (this.selectedBrands && this.selectedBrands.length > 0) { const itemBrand = (data.brand || '').toLowerCase(); const hasMatch = this.selectedBrands.some(sb => sb.toLowerCase() === itemBrand); if (!hasMatch) return; } // Apply Price filter let priceFilterVal = null; const min = this.customPriceMin ? this.customPriceMin.value : ''; const max = this.customPriceMax ? this.customPriceMax.value : ''; if (min || max) { priceFilterVal = `${min}_${max}`; } else if (this.priceFilter && this.priceFilter.value !== 'all') { priceFilterVal = this.priceFilter.value; } if (priceFilterVal && rawPrice > 0) { if (priceFilterVal === 'under50' && rawPrice >= 50) return; if (priceFilterVal === 'over50' && rawPrice <= 50) return; if (priceFilterVal === 'over30' && rawPrice <= 30) return; if (priceFilterVal === 'over500' && rawPrice <= 500) return; if (priceFilterVal.includes('_')) { const parts = priceFilterVal.split('_'); const min = parseFloat(parts[0]); const max = parseFloat(parts[1]); if (!isNaN(min) && rawPrice max) return; } } // Apply Discount filter if (this.discountFilter && this.discountFilter.value !== 'all' && this.discountFilter.value !== '0') { const requiredDiscount = parseInt(this.discountFilter.value); if (!isNaN(requiredDiscount) && requiredDiscount > 0) { if (!rawMsrp || rawMsrp <= rawPrice) return; const ratio = Math.round((1 - (rawPrice / rawMsrp)) * 100); if (ratio 0 ? rawPrice.toString() : '', msrp: rawMsrp > 0 ? rawMsrp.toString() : '', currency: currencyStr, isCheckPrice: !rawPrice, savingLabel: savingLabel, savingType: rawMsrp > rawPrice ? 'amount' : 'none', isPrime: false, starRating: null, description: description, text: data.text || '' }); }); }); const airedaleBrandsList = Object.keys(dynamicBrandsCounts).map(b => ({ formatted_value: b, count: dynamicBrandsCounts[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, append = false) { const url = new URL(this.apiUrl); url.searchParams.append('model_name', query); const areaCode = this.getAreaCode(); if (areaCode) { url.searchParams.append('area', areaCode); } if (append && this.deals.length > 0) { url.searchParams.append('offset', this.deals.length.toString()); } 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 <= 50) return; if (priceVal === 'over30' && price <= 30) return; if (priceVal === 'over500' && price <= 500) return; if (priceVal.includes('_')) { const parts = priceVal.split('_'); if (parts[0] && price parseFloat(parts[1])) return; } } // Map Adviser schema to our widget's expected schema const mappedData = { url: dataObj.linkHREF || dataObj.dataLink || '#', image: dataObj.imageURL || (dataObj.image && dataObj.image.src) || '', title: dataObj.dataProduct || (dataObj.product && dataObj.product.name) || 'Product Deal', merchant: dataObj.dataRetailer || 'Retailer', price: dataObj.dataDiscountedPrice || 0, currency: dataObj.dataCurrency === 'USD' ? '$' : (dataObj.dataCurrency || '$'), msrp: dataObj.dataOriginalPrice || null }; const titleLow = mappedData.title.toLowerCase(); const merchLow = mappedData.merchant.toLowerCase(); // Smarter mock filtering let isMatch = false; if (q === '' || this.isBroadQuery(q)) { isMatch = true; } else if (titleLow.includes(q) || merchLow.includes(q)) { isMatch = true; } else if ((q.includes('laptop') || q.includes('mac') || q.includes('pc')) && (titleLow.includes('macbook') || titleLow.includes('laptop'))) { isMatch = true; } else if ((q.includes('tv') || q.includes('television')) && (titleLow.includes('tv') || titleLow.includes('oled') || titleLow.includes('qled'))) { isMatch = true; } else if ((q.includes('phone') || q.includes('smartphone')) && (titleLow.includes('galaxy') || titleLow.includes('phone'))) { isMatch = true; } else if ((q.match(/watch|fitness|run|shoe/)) && (titleLow.includes('forerunner') || titleLow.includes('saucony') || titleLow.includes('watch'))) { isMatch = true; } if (isMatch) { this.deals.push(this.extractDealData(mappedData)); } }); let rowLimit = 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 : (this.getViewMode() === 'savings_squad' ? 'date_desc' : 'discount_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} \x3Cdiv class="tg-df-skeleton tg-df-skeleton-text" style="height:44px; margin-top:8px;">${_div} ${_div} ${_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)} (${this.airedaleTagCounts[tag] || 0})${_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 = isSavingsSquadMode ? 'View Deal' : 'Check Price'; if (isSavingsSquadMode) { priceGroupHtml = ` \x3Cdiv class="tg-df-card-merchant-wrapper"> \x3Cspan class="tg-df-card-merchant-pill" title="${this.escapeHTML(deal.merchant)}">${this.escapeHTML(deal.merchant)}${_span} ${_div} \x3Cdiv class="tg-df-card-price-group"> ${_div} `; } else { priceGroupHtml = ` \x3Cdiv class="tg-df-card-merchant-wrapper"> \x3Cspan class="tg-df-card-merchant-pill" title="${this.escapeHTML(deal.merchant)}">${this.escapeHTML(deal.merchant)}${_span} ${_div} \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 = ` \x3Cdiv class="tg-df-card-merchant-wrapper"> \x3Cspan class="tg-df-card-merchant-pill" title="${this.escapeHTML(deal.merchant)}">${this.escapeHTML(deal.merchant)}${_span} ${_div} \x3Cdiv class="tg-df-card-price-group"> ${isSavingsSquadMode ? '' : ` \x3Cspan class="tg-df-card-price">${formattedPrice}${_span} ${msrpHtml} `} ${_div} `; } const discountBadgeHtml = deal.savingLabel && !deal.isCheckPrice ? `\x3Cspan class="tg-df-card-discount-badge">${this.escapeHTML(deal.savingLabel)}${_span}` : ''; // HACK for CMS const _button = ''; const _svg = ''; const _path = ''; const _rect = ''; const _circle = ''; const _polyline = ''; const _line = ''; let badgesHtml = ''; const primeBadge = deal.isPrime ? ` \x3Cspan class="tg-df-tag tg-df-tag-prime"> \x3Csvg width="12" height="12" viewBox="0 0 24 24" fill="currentColor"> \x3Cpath d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z">${_path} ${_svg} Prime ${_span} ` : ''; const couponsBadge = deal.merchant && deal.merchant.toLowerCase().includes('amazon') ? '' : ` \x3Cdiv class="tg-df-coupon-wrapper" data-merchant="${this.escapeHTML(deal.merchant)}" style="display:inline-flex; align-items:center;"> \x3Cdiv class="tg-df-coupon-spinner">${_div} \x3Cbutton type="button" class="tg-df-tag tg-df-tag-coupons" data-action="coupons-click" data-merchant="${this.escapeHTML(deal.merchant)}" style="display:none;"> \x3Csvg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> \x3Cpath d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z">${_path} \x3Cline x1="7" y1="7" x2="7.01" y2="7">${_line} ${_svg} Coupons ${_button} ${_div} `; // Note: We always add coupons badge if there's a chance, but to allow 3-line titles we check wrapper display state badgesHtml = ` \x3Cdiv class="tg-df-card-badges"> ${primeBadge} ${couponsBadge} ${_div} `; const _linearGradient = ''; const _polygon = ''; const _stop = ''; const _defs = ''; let starHtml = ''; if (deal.starRating) { let rating = deal.starRating; if (rating > 0) { const fullStars = Math.floor(rating); const halfStar = (rating - fullStars) >= 0.5 ? 1 : 0; const emptyStars = Math.max(0, 5 - fullStars - halfStar); const blue = '#1f69ff'; // Tom's guide brand color from VIEW DEAL button const gray = '#cbd5e1'; const starSvgFull = `\x3Csvg width="14" height="14" viewBox="0 0 24 24" fill="${blue}" stroke="${blue}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">\x3Cpolygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26">${_polygon}${_svg}`; const gradId = 'half_grad_' + Math.floor(Math.random()*1000000); const starSvgHalf = `\x3Csvg width="14" height="14" viewBox="0 0 24 24" stroke="${blue}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">\x3Cdefs>\x3ClinearGradient id="${gradId}" x1="0" x2="1" y1="0" y2="0">\x3Cstop offset="50%" stop-color="${blue}">${_stop}\x3Cstop offset="50%" stop-color="transparent">${_stop}${_linearGradient}${_defs} \x3Cpolygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26" fill="url(#${gradId})">${_polygon}${_svg}`; const starSvgEmpty = `\x3Csvg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="${gray}" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">\x3Cpolygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26">${_polygon}${_svg}`; let stars = []; for (let i=0; i<fullStars; i++) stars.push(starSvgFull); if (halfStar) stars.push(starSvgHalf); for (let i=0; i \x3Cspan style="margin-right:6px;">Tom's Guide:${_span} \x3Cdiv style="display:flex;gap:2px;"> ${stars.join('')} ${_div} ${_div}`; } } let htmlOutput = ''; if (isSavingsSquadMode) { htmlOutput += ` \x3Cdiv class="hawk-deal-widget-container tg-df-mobile-only" data-collapsible="true"> ${this.editorMode ? `\x3Cinput type="checkbox" class="tg-df-deal-checkbox" data-id="${this.escapeHTML(deal.id)}" ${this.selectedDeals.has(deal.id) ? 'checked' : ''} style="margin-bottom: 10px;">` : ''} \x3Cdiv class="hawk-deal-widget-wrap"> \x3Cdiv class="hawk-deal-widget-image-container"> \x3Ca data-google-interstitial="false" aria-label="View ${this.escapeHTML(deal.title)} on ${this.escapeHTML(deal.merchant)}" href="${this.escapeHTML(rewrittenLink)}" rel="sponsored noopener" target="_blank" class="hawk-affiliate-link-deal-widget" ${dataAttr}> \x3Cimg ${deal.image ? `src="${this.escapeHTML(deal.image)}"` : ''} alt="${this.escapeHTML(deal.title)}" class="hawk-lazy-image-deal-widget" loading="lazy" width="140" height="160" onerror="${deal.fallbackImage ? `if(!this.dataset.fb) { this.dataset.fb='1'; this.src='${this.escapeHTML(deal.fallbackImage)}'; } else { this.style.opacity='0'; }` : `this.style.opacity='0';`}"> ${_a} ${_div} \x3Cdiv class="hawk-deal-widget-text-cta-container"> \x3Cdiv class="hawk-deal-widget-text-body-container"> \x3Cdiv class="hawk-deal-widget-text-body-main"> \x3Ca data-google-interstitial="false" aria-label="View ${this.escapeHTML(deal.title)} on ${this.escapeHTML(deal.merchant)}" href="${this.escapeHTML(rewrittenLink)}" class="hawk-affiliate-link-container" rel="sponsored noopener" target="_blank" ${dataAttr}> ${deal.isCheckPrice ? ` \x3Cspan class="hawk-deal-widget-title-product-title">${this.escapeHTML(deal.title)}${_span} ` : ` \x3Cspan class="hawk-deal-widget-title-product-title">${deal.brand ? this.escapeHTML(deal.brand) + ' ' : ''}${this.escapeHTML(deal.productName || deal.title || '')}:${_span} `} ${_a} ${!deal.isCheckPrice && deal.rawMsrp && deal.rawMsrp > deal.rawPrice ? ` \x3Ca data-google-interstitial="false" aria-label="View ${this.escapeHTML(deal.title)} on ${this.escapeHTML(deal.merchant)}" href="${this.escapeHTML(rewrittenLink)}" class="hawk-affiliate-link-container" rel="sponsored noopener" target="_blank" ${dataAttr}> \x3Cspan class="hawk-deal-widget-title-was-price">was ${currencySym}${escapedMsrp}${_span} ${_a} ` : ''} \x3Ca data-google-interstitial="false" aria-label="View ${this.escapeHTML(deal.title)} on ${this.escapeHTML(deal.merchant)}" href="${this.escapeHTML(rewrittenLink)}" class="hawk-affiliate-link-container" rel="sponsored noopener" target="_blank" ${dataAttr}> \x3Cspan class="hawk-deal-widget-title-retailer-price"> ${!deal.isCheckPrice ? ` \x3Cspan class="hawk-deal-widget-title-price">now ${formattedPrice}${_span} \x3Cspan class="hawk-deal-widget-title-retailer"> at ${this.escapeHTML(deal.merchant)}${_span} ` : ` \x3Cspan class="hawk-deal-widget-title-price">See price at ${this.escapeHTML(deal.merchant)}${_span} `} ${_span} ${_a} ${deal.description ? `\x3Cdiv class="hawk-deal-widget-text-body-description">\x3Cp>${this.escapeHTML(deal.description)}${_p}${_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>${deal.isCheckPrice ? 'Check Price' : '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} ${_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 ? `\x3Cp style="font-size: 13px; color: var(--tg-df-text-muted); margin-bottom: 12px; line-height: 1.4;">${this.escapeHTML(deal.description)}${_p}` : ''} \x3Cdiv class="tg-df-card-footer"> ${priceGroupHtml} \x3Ca href="${this.escapeHTML(rewrittenLink)}" ${dataAttr} target="_blank" rel="noopener nofollow" class="tg-df-card-cta ${isSavingsSquadMode ? 'tg-df-cta-savings-squad' : ''}" style="text-decoration: none;">${ctaText}${_a} ${_div} ${_div} ${_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 || ((this.getViewMode() === 'carousel' || this.getViewMode() === 'auto') && displayDeals.length > 0 && displayDeals.length % ((this.rowsSelect && this.rowsSelect.value) ? parseInt(this.rowsSelect.value, 10) : 12) === 0)) { if (this.getViewMode() === 'carousel') { dealsHtml += ` \x3Cbutton type="button" class="tg-df-load-more-card tg-df-load-more"> \x3Csvg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-bottom: 8px;">\x3Cpath d="M5 12h14">\x3C/path>\x3Cpath d="m12 5 7 7-7 7">\x3C/path>\x3C/svg> Load More ${_button} `; } else { 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; display: flex; align-items: center;">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); } let gridWrapper = this.grid.parentElement; if (gridWrapper && gridWrapper.classList.contains('tg-df-grid-wrapper')) { let rightChevron = gridWrapper.querySelector('.tg-df-carousel-scroll-right'); let leftChevron = gridWrapper.querySelector('.tg-df-carousel-scroll-left'); if (this.getViewMode() === 'carousel') { // The observer set up in setupScrollListeners handles visibility. if (rightChevron) rightChevron.style.display = 'flex'; if (leftChevron) leftChevron.style.display = 'none'; // reset correctly } else { if (rightChevron) rightChevron.style.display = 'none'; if (leftChevron) leftChevron.style.display = 'none'; } } const loadMoreBtn = this.grid.querySelector('.tg-df-load-more'); if (loadMoreBtn) { loadMoreBtn.addEventListener('click', async () => { if (typeof trackElementInteraction === 'function') { trackElementInteraction({ id: 'load-more', name: 'Load more', label: 'Load More Results' }); } if (displayDeals.length <= this.displayLimit) { loadMoreBtn.innerHTML = ` Loading... `; loadMoreBtn.disabled = true; await this.fetchDeals(this.currentQuery, true); } else { this.displayLimit += this.getRowsLimit(); this.render(); } }); } this.bindCouponButtons(); this.checkAndUpdateCoupons(); // 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'); } setTimeout(() => { 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); } catch(e) { console.warn("Widget render error", e); } } async checkAndUpdateCoupons() { const wrappers = Array.from(this.root.querySelectorAll('.tg-df-coupon-wrapper')); if (wrappers.length === 0) return; const merchants = [...new Set(wrappers.map(w => w.getAttribute('data-merchant')).filter(Boolean))]; if (merchants.length === 0) return; const couponResultsMap = await this.checkMerchantsCouponsBulk(merchants); for (const merchant of merchants) { const hasCoupons = !!couponResultsMap[merchant]; const merchantWrappers = wrappers.filter(w => w.getAttribute('data-merchant') === merchant); merchantWrappers.forEach(wrapper => { const spinner = wrapper.querySelector('.tg-df-coupon-spinner'); const btn = wrapper.querySelector('.tg-df-tag-coupons'); if (spinner) spinner.style.display = 'none'; if (hasCoupons && btn) { btn.style.display = 'inline-flex'; } else if (!hasCoupons) { wrapper.style.display = 'none'; } }); } } updateFloatingCopyBar() { if (!this.editorBar || !this.editorSelectedCount) return; if (this.editorMode && this.selectedDeals.size > 0) { this.editorBar.style.display = 'flex'; this.editorSelectedCount.innerText = this.selectedDeals.size; } else { this.editorBar.style.display = 'none'; } } async copySelectedDealsToCMS() { function htmlToSlate(htmlString) { if (!htmlString) return [{ type: 'paragraph', children: [{ text: '' }] }]; let doc; if (typeof window !== 'undefined' && window.DOMParser) { doc = new DOMParser().parseFromString(htmlString, 'text/html'); } else { doc = document.implementation.createHTMLDocument(''); doc.body.innerHTML = htmlString; } function parseNode(node, marks = {}) { if (node.nodeType === 3) { const text = node.textContent; if (!text) return null; return { text: text, ...marks }; } if (node.nodeType === 1) { const tagName = node.tagName.toLowerCase(); if (tagName === 'br') { return { type: 'line-break', children: [{ text: '' }] }; } if (tagName === 'p') { let children = Array.from(node.childNodes).map(child => parseNode(child, marks)).flat().filter(Boolean); if (children.length === 0) children.push({ text: "" }); return { type: 'paragraph', children }; } if (tagName === 'strong' || tagName === 'b') { const newMarks = { ...marks, bold: true }; return Array.from(node.childNodes).map(child => parseNode(child, newMarks)).flat().filter(Boolean); } if (tagName === 'em' || tagName === 'i') { const newMarks = { ...marks, italic: true }; return Array.from(node.childNodes).map(child => parseNode(child, newMarks)).flat().filter(Boolean); } if (tagName === 'a') { const href = node.getAttribute('href') || ''; let children = Array.from(node.childNodes).map(child => parseNode(child, marks)).flat().filter(Boolean); if (children.length === 0) children.push({ text: "" }); return { type: 'link', url: href, isNoFollow: (node.getAttribute('rel') || '').includes('nofollow'), isSponsored: (node.getAttribute('rel') || '').includes('sponsored'), isOpenNewTab: node.getAttribute('target') === '_blank', isPreventDataRewrite: false, children: children }; } return Array.from(node.childNodes).map(child => parseNode(child, marks)).flat().filter(Boolean); } return null; } let blocksArray = []; let currentParagraphChildren = []; function flushParagraph() { if (currentParagraphChildren.length > 0) { blocksArray.push({ type: 'paragraph', children: currentParagraphChildren }); currentParagraphChildren = []; } } Array.from(doc.body.childNodes).forEach(node => { const parsed = parseNode(node, {}); const parsedItems = Array.isArray(parsed) ? parsed : (parsed ? [parsed] : []); parsedItems.forEach(item => { if (item.type === 'paragraph') { flushParagraph(); blocksArray.push(item); } else { currentParagraphChildren.push(item); } }); }); flushParagraph(); if (blocksArray.length === 0) { blocksArray = [{ type: 'paragraph', children: [{ text: '' }] }]; } return blocksArray; } const blocks = []; this.editorCopyBtn.innerHTML = '\x3Cspan class="tg-df-coupon-spinner" style="display:inline-block; margin-right:8px; border-top-color:#fff;">' + ' Copying...'; for (const deal of Array.from(this.selectedDeals.values())) { const url = deal.url; const merchant = deal.merchant; const title = deal.title; const image = deal.image; const currentPrice = deal.currency + deal.rawPrice; const wasPrice = deal.hasWasPrice && deal.rawMsrp > deal.rawPrice ? deal.currency + deal.rawMsrp : ''; let couponsChildren = []; try { const area = this.getAreaCode(); const apiUrl = new URL('https://search-api.fie.future.net.uk/widget.php'); apiUrl.searchParams.append('model_name', 'Everything'); apiUrl.searchParams.append('language', 'en-GB'); apiUrl.searchParams.append('area', area); apiUrl.searchParams.append('combine_product_types', '1'); apiUrl.searchParams.append('filter_merchant_name', merchant); apiUrl.searchParams.append('all_filters', 'false'); apiUrl.searchParams.append('exclude_unlabelled', 'false'); apiUrl.searchParams.append('include_specs', 'false'); apiUrl.searchParams.append('sort', 'voucher'); apiUrl.searchParams.append('distinct_merchants', 'natural'); apiUrl.searchParams.append('filter_product_types', 'vouchers,offer_deals,newsletter'); apiUrl.searchParams.append('rows', '3'); apiUrl.searchParams.append('origin', 'widgets-clientside'); let res; try { res = await fetch(apiUrl.toString()); } catch (e) { return; } if (res.ok) { const data = await res.json(); let offers = []; if (data && data.widget && data.widget.data && Array.isArray(data.widget.data.offers)) { offers = data.widget.data.offers; } else if (data && data.data && Array.isArray(data.data.offers)) { offers = data.data.offers; } if (offers.length > 0) { couponsChildren.push({ text: "Also check out these coupons: ", bold: true }); offers.slice(0, 3).forEach((offer, idx) => { const actualOffer = offer.offer || offer; const offerName = actualOffer.name || actualOffer.title || offer.model_name || offer.title || offer.name || 'Coupon'; const linkUrl = actualOffer.link || actualOffer.url || actualOffer.offer_link || '#'; couponsChildren.push({ type: "line-break", children: [{ text: "" }] }); couponsChildren.push({ text: "🎟️ " }); couponsChildren.push({ type: "link", url: linkUrl, isNoFollow: true, isSponsored: false, isOpenNewTab: true, isPreventDataRewrite: false, children: [{ text: offerName, bold: true }] }); }); } } } catch (err) { console.warn('Failed to fetch coupons for', merchant, err); } let descriptionValue = []; if (deal.text) { descriptionValue = htmlToSlate(deal.text); } else { const dealDescriptions = [ `Don't miss out on this fantastic deal for the ${title}. It is currently available at ${merchant} for a highly competitive price.`, `We've spotted an excellent price drop on the ${title}. Grab it now at ${merchant} before it's gone.`, `The ${title} is currently seeing a generous discount over at ${merchant}. This is a perfect time to buy if you've been holding out.`, `If you're in the market for the ${title}, ${merchant} has just the deal for you.`, `Score the ${title} for less at ${merchant} right now. This is a rare chance to save big.`, `Upgrade your setup with the ${title}, now available at a stellar price via ${merchant}.` ]; const randomDescription = dealDescriptions[Math.floor(Math.random() * dealDescriptions.length)]; descriptionValue = [ { type: "paragraph", children: [{ text: randomDescription }] } ]; } if (couponsChildren.length > 0) { let lastBlock = descriptionValue[descriptionValue.length - 1]; if (lastBlock && lastBlock.type === 'paragraph') { lastBlock.children.push({ type: "line-break", children: [{ text: "" }] }); lastBlock.children.push({ type: "line-break", children: [{ text: "" }] }); lastBlock.children.push({ text: "Also check out these coupons: ", bold: true }); lastBlock.children.push({ type: "line-break", children: [{ text: "" }] }); lastBlock.children = lastBlock.children.concat(couponsChildren); } else { descriptionValue.push({ type: "paragraph", children: [ { type: "line-break", children: [{ text: "" }] }, { type: "line-break", children: [{ text: "" }] }, { text: "Also check out these coupons: ", bold: true }, { type: "line-break", children: [{ text: "" }] }, ...couponsChildren ] }); } } function normalizeCurrencyToISO(symbol) { const map = { '£': 'GBP', '$': 'USD', 'A$': 'AUD', 'CA$': 'CAD', '€': 'EUR' }; return map[symbol] || symbol; } const isoCurrency = normalizeCurrencyToISO(deal.currency); blocks.push({ id: (window.crypto && window.crypto.randomUUID) ? window.crypto.randomUUID() : 'cms-' + Date.now() + Math.random(), blockTypeName: "deal", excludeFrom: [], collapsible: false, props: { description: { value: descriptionValue, touched: false, validationMessage: "" }, image: { value: { credit: [{ type: "paragraph", children: [{ text: merchant }] }], dateCreated: Date.now(), dateModified: Date.now(), distribution: [], fileSize: 0, height: 1000, id: deal.id, imageRights: "", src: image, name: title + ".jpg", tags: [], width: 1000 }, touched: false, validationMessage: "" }, showDealButton: { value: true, touched: false, validationMessage: "" }, isPreferredPartner: { value: false, touched: false, validationMessage: "" }, linkHref: { value: url, touched: false, validationMessage: "" }, linkLabel: { value: "", touched: false, validationMessage: "" }, linkIsNoFollow: { value: true, touched: false, validationMessage: "" }, linkIsSponsored: { value: false, touched: false, validationMessage: "" }, linkIsOpenNewWindow: { value: true, touched: false, validationMessage: "" }, customPromoFlags: { value: [], touched: false, validationMessage: "" }, showStarDeal: { value: false, touched: false, validationMessage: "" }, savingType: { value: "none", touched: false, validationMessage: "" }, starDealPromoFlag: { value: "", touched: false, validationMessage: "" }, showEditorsChoice: { value: false, touched: false, validationMessage: "" }, editorsChoiceTitle: { value: "", touched: false, validationMessage: "" }, hawkPriceCurrency: { value: { value: isoCurrency, label: isoCurrency }, touched: false, validationMessage: "" }, hawkPrice: { value: deal.hasWasPrice ? String(deal.rawMsrp) : String(deal.rawPrice), touched: false, validationMessage: "" }, hawkSalePrice: { value: String(deal.rawPrice), touched: false, validationMessage: "" }, lastCheckedPriceDate: { value: "", touched: false, validationMessage: "" }, hawkModel: { touched: false, validationMessage: "" }, productId: { value: "", touched: false, validationMessage: "" }, voucherId: { value: "", touched: false, validationMessage: "" }, brand: { value: deal.brand || merchant, touched: false, validationMessage: "" }, productName: { value: title, touched: false, validationMessage: "" }, label: { value: "", touched: false, validationMessage: "" }, retailer: { value: merchant, touched: false, validationMessage: "" }, priceCheckError: false }, failedFetchError: "" }); } const payload = { type: "articleBuilderPages", data: blocks }; const jsonStr = JSON.stringify(payload); if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(jsonStr).then(() => { this.editorCopyBtn.innerHTML = 'Copied!'; setTimeout(() => { this.editorCopyBtn.innerHTML = '\x3Csvg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 6px;">\x3Crect x="9" y="9" width="13" height="13" rx="2" ry="2">\x3Cpath d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"> Copy to CMS'; }, 2000); }).catch(err => { console.warn('Failed to copy text: ', err); alert('Failed to copy deals to clipboard. See console.'); }); } else { // Fallback const textArea = document.createElement("textarea"); textArea.value = jsonStr; document.body.appendChild(textArea); textArea.focus(); textArea.select(); try { document.execCommand('copy'); this.editorCopyBtn.innerHTML = 'Copied!'; setTimeout(() => { this.editorCopyBtn.innerHTML = '\x3Csvg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-right: 6px;">\x3Crect x="9" y="9" width="13" height="13" rx="2" ry="2">\x3Cpath d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"> Copy to CMS'; }, 2000); } catch (err) { console.warn('Fallback: Oops, unable to copy', err); alert('Fallback: Failed to copy deals to clipboard.'); } document.body.removeChild(textArea); } } } // Initialize the Widget if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => new DealsFinderWidget({ rootId: 'signal-deals-finder-root', rootNode: shadowRoot, hostContainer: hostContainer })); } else { new DealsFinderWidget({ rootId: 'signal-deals-finder-root', rootNode: shadowRoot, hostContainer: hostContainer }); } })();

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

Read Full Article on tomsguide.com →

More News