Amazon’s 4th of July deals are just as good as Prime Day — here’s 37 deals worth shopping this weekend

4th of July weekend is officially here! And although Independence Day is a day to celebrate, it's also the perfect time to secure some deals. Retailers everywhere are already pulling out all the stops when it comes to sales — and Amazon is no exception.
The retail giant is slashing prices on popular products from top brands like Brooks, Bose, Ninja, YETI and more — and the discounts actually rival prices we saw during last week's Prime Day event. Even better, there are plenty of great finds under $50, making it easier than ever to stay in budget.
Whether you're after some outdoor cooking tools for those backyard barbecues, a pair of new running shoes for back to school or beach essentials for upcoming vacations, I've picked the best Amazon 4th of July sale you can get. Keep scrolling to check out my favorite picks.
Also, check out the Tom's Guide Savings Squad for more ways to save.
Quick Links
- shop Amazon's 4th of July sale
- Amazon Haul: deals from $5
- Lego sale: deals from $11
- YETI sale: deals from $17
- Amazon grill sale: deals from $39
- Skechers sale: deals from $24
- Garmin: deals from $149
- Brightech Outdoor String Lights: was $47 now $21
- Bose SoundLink Flex (Gen 2): was $159 now $99
- Brooks Ghost 17: was $149 now $109
- Ninja FrostVault 30QT: was $279 now $229
- Weber Spirit II E-210 Propane Grill: was $549 now $399
My Favorite Deals

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 a Skechers fan for years. In addition to being super comfortable, there's a Skechers sneaker for pretty much any style. Amazon is having a massive Skechers sale. The sale includes sneakers and apparel for men, women, and children.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

Garmin: deals from $149 @ Amazon
Amazon is slashing the price of select Garmin watches. After discount, prices start as low as $149. The sale includes a wide variety of models like the Fenix, Instinct, Descent, and Enduro. View Deal

The Glycerin 22 is a comfortable daily trainer that uses Brooks' top DNA Tuned midsole foam. It's not the latest model of the shoe, but the Glycerin 23 is not a huge update and it costs $70 more at the moment, so grabbing this deal on the black color of the older shoe is a major saving.View Deal

The $200 discount on a king-sized EGOHOME Black 12" Memory Foam Mattress is one of the best mattress sales you'll find this month. Over at EGOHOME, the size is $499.99 (down from $699.99) so Amazon really does have the edge in the price department. However, EGOHOME's 100-night sleep trial doesn't seem to be available at Amazon, but a 10-year warranty and a 90-day return policy does apply. We gave this mattress a 4-star rating in our EGOHOME Black 12" Mattress review.View Deal
Deals Under $50

This portable fan has three power settings: 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

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

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

The perfect compact cooler, now with 26% off. This Igloo cooler has a 25qt capacity, space for 36 cans. The non-slip handles make it easy to carry, and there's even a fish ruler on top to measure your catch.View Deal

There's nothing like sitting back and relaxing by the campfire in a comfortable chair. This portable folding chair will hold and cool your drinks — making it great for outdoor gatherings.View Deal

Need a new bag for beach trip? This is the one to buy. This large beach bag is lightweight, waterproof, structured and easy to clean. It also has detachable water-resistant zipper pocket to hold phones or small items. Plus, it comes in a ton of fun colors! View Deal
Outdoor Cooking Deals

Grilling season is here! Pick up these BBQ tongs for even more delicious results. The 17-inch tongs are extra long to keep your hands safe from the heat, and they're made from stainless steel. The acacia wood handles make for an extra stylish look.View Deal

Even if you're short on space, you can still cook up a feast with this electric griddle. There's a 10 x 20 Inch cooking surface and a slide out drip tray for easy cleanup. View Deal

Need a portable grill that's ready to hit the road? This Coleman grill is over $100 off on Amazon. Ideal for camping, tailgating and roadtrips, the versatile appliance has quick-fold legs and wheels, as well as an easy setup and tear-down. It's perfect to bring along on any outdoor adventure. View Deal

If you’re a fan of cooking outdoors, this is the deal for you. This outdoor pizza oven offers 8 functions, including pizza, max roast, specialty roast and smoker amongst others, so it’s by no means limited to just pizza. Powered by electricity, it can reach up to 700°F, plus there’s the option to add wood pellets if you want smoked food too. The capacity can hold a 12 lb turkey or a 12-inch pizza, which is generous enough. View Deal

Weber's Spirit line offers a mid price but high performance that's the perfect middle-ground for most homes. Its portability doesn't make it feel flimsy, and the two burners are the ideal size for most families. View Deal

The Ninja FlexFlame is Ninja's first attempt at a propane grill, and it offers all the ingeniuity you'd expect from a Ninja product. It's got a pellet chamber to infuse your food with an authentic smoked flavor, but can also sear and even be converted to a griddle with the use of add-ons.View Deal
Running Sneakers

These Nike road runners are on sale starting from just $60, which is a steal. They're packed with cushioning, have a breathable mesh upper and a flexible outsole. View Deal

Currently 54% 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

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

I'm currently testing the new Brooks Ghost 18 and it feels very similar to the Ghost 17 to me, 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

The arrival of the Asics Superblast 3 means that the previous version of the shoe is going for a song in the Amazon Big Spring Sale, having been reduced to $149 in a wide range of colors and sizes. The Superblast 2 is a high-stack super-trainer that offers a rare combo of comfort and speed, and is a great addition to any runner’s wardrobe.View Deal

I found this pretty sizable saving on the New Balance FuelCell SC Elite V5, which depends on the color you're prepared to buy. The best discount (30%) is on the Boysenberry/Afterglow edition. You can still score 21% off the Urgent Red/White/Black but take my advice and grab the bigger discount. Just note that this deal is for a US Men's size 9.5 and other sizes may not have the same price cut. Or any at all, for that matter.View Deal
Outdoor Deals

Ready to jumpstart your garden this spring? Miracle Gro Garden Soil is here to help. Full of essential nutrients, it instantly feeds to grow bigger, more beautiful plants, including vegetables, trees, shrubs and houseplants. View Deal

Nothing adds outdoor ambiance like a killer playlist and the very best solar lights. This outdoor-rated pack is built with shatterproof plastic bulbs that are currently only $19 at Amazon. Plug them in when the sun sets and enjoy 27 feet-worth and up to six full hours of a soft, white glow. View Deal

This lightweight hose comes with 10 functional nozzles providing a range of sprays from jet to mist. It also has a no kink design to supply a steady water stream. The hose is durable and can withstand extreme weather conditions.View Deal

Add comfortable seating and a space to store snacks and drinks to your patio — all for just $59. The chairs come in several color options and the tabletop has a stylish textured glass finish.View Deal

Lounge in your backyard, on your patio or poolside in ultimate comfort with this set of padded, weather-resistant reclining chairs. They conform to your body while an adjustable headrest adds an extra level of comfort. They also feature a lightweight, folding design so you can bring them along with you to the beach, park or your next tailgate. View Deal
Bose Deals

The new Bose SoundLink (2nd gen) is a compact and powerful Bluetooth speaker with a durable design. This 1.2-pound speaker is water and dust proof and delivered great sound quality in our SoundLink Flex gen 2 review. With 12 hours of battery life and $59 off, you have yourself a great deal.View Deal

The Bose QuietComfort Earbuds (2024) are a fantastic pair of noise-canceling earbuds that don’t break the bank. Bose reigns supreme in terms of sound quality and active noise cancellation, and that’s very much the case with these mid-range earbuds too. You’re getting best-in-class sound quality and ANC — all for just $129.View Deal

The Bose QuietComfort Earbuds 2 take active noise cancellation to the next level. In our Bose QuietComfort Earbuds 2 review, we said these buds are a remarkable follow-up to their predecessors delivering better sound, call quality and unrivaled ANC. Sound balance and noise neutralization work extremely well and the battery life is up to 6 hours, with an extra 24 hours via the charging case.View Deal

This relatively new Bluetooth speaker is only just seeing its first discounts, and they're really good. This is a big saving on a speaker that offers great sound quality and 20 hours of battery life.View Deal

In our Bose QuietComfort Headphones review, we said these are "excellent always-on noise-cancelers with enhanced sound and battery life." They're missing spatial audio mode and touch controls, but for $180 less, these are nearly perfect. View Deal
Ninja Deals

The Ninja Blast Max is capable of taking on any challenge you throw at it, including crushing ice and blitzing berries to make yummy smoothies. It comes with multiple blend settings to make sure your blender is using the right level of power for the task at hand, and the compact size means it’s a great option for anyone with a small kitchen. View Deal

The Ninja Air Fryer comes with an XL (5.5 quart) capacity which is ideal for a large household. In addition, it can roast, reheat and dehydrate delicious meals in less time. With a handy digital display, it’s easy to use at just a touch of a button. Designed to be lightweight and easy to clean, removable parts are dishwasher safe.View Deal

If you have a smaller pack-up and can skip the wheels, Ninja's backpack cooler is just the ticket. It has a built-in cold dry zone to keep food dry and separate at under 40°F, holds ice for up to 24 hours, and has a top lid and side opening for easier access. It holds up to 24 cans, or 16 cans with ice. It even have a built in bottle opener and right now you can save 25%.View Deal

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