The best retro gadgets to help you log off (plus the Prime Day deals I'm using to build my collection)

This past weekend, while the internet was busy arguing about the latest AI news, I was at the thrift store flipping through a box of cassette tapes. I found a B-52s album, a dodgy-looking workout mix from 1987, and a surprisingly pristine copy of Janet Jackson's Rhythm Nation. All three made it into the bag.
Then, on Sunday, I clambered into the attic to dig out my old Xbox so I could play Guitar Hero (Legends of Rock to be exact). The keys were a little sticky, but it didn't matter because the pure rush of nostalgia took over the second 'Slow Ride' started playing.
There's something genuinely lovely about tech that just does one thing and does it well. If you're feeling the pull of something a little more analog right now, you're not alone, and as it turns out, there are some brilliant retro-inspired gadgets worth picking up, several of which are on sale right now.
Here's my pick of the best.
Quick links
- YoYa Toys Aqua Rings Handheld Water Game (Set of 2) — was $15 now $12
- LEGO Creator 3-in-1 Retro Camera — was $19 now $15
- Tamagotchi Original Celebration Egg — was $29 now $23
- Cotsoco Vinyl Record Player — was $59 now $28
- Casio F91WG-9 Retro Digital Watch — was $38 now $29
- 8BitDo Retro R8 Mouse — was $49 now $31
- JAMBO 16" LED Lava Lamp — was $39 now $31
- My Arcade Pac-Man Micro Player Pro — was $44 now $32
- Nostalgia Snow Cone Machine — was $59 now $39
- ROKR 3D Wooden Puzzle Illuminated Globe — was $54 now $44
- R36MAX II Retro Handheld Game Console — was $99 now $75
- Kodak Mini 2 Retro Portable Photo Printer — was $99 now $84
- JadeAudio FiiO JM21 Hi-Res Music Player — was $179 now $169
Retro gadget deals

If you ever sat in the back of a car on a long journey as a kid, desperately trying to land plastic rings onto a peg using nothing but water pressure and sheer determination, this one will hit differently. The set comes with two (a ring toss and a basketball hoop,) which means you can either share with someone or stage a very intense competition with yourself. View Deal

Technically this is a kids' toy, but anyone who has ever sat down with a LEGO set as an adult knows that the age guidance on the box is largely advisory. This 3-in-1 set lets you build a retro film camera, a video camera, or a classic television set — all from the same bricks. The camera has a moving lens and a working film loader. The TV has an antenna. Once you're done building, they display beautifully on a shelf. Consider it mindfulness with a nostalgic twist.View Deal

The Tamagotchi was the original screen addiction, except it was wholesome because you were keeping a tiny digital creature alive. Feed it, play with it, turn its lights off at bedtime. Neglect it and face the consequences. I actually hosted a 90s-themed birthday party recently, and my Tama absolutely upstaged me.
If you want to recreate that magic (or stress), this Amazon-exclusive Celebration Egg edition is 20% off right now and comes on a chain so you can take your new dependent everywhere. Which you will need to.View Deal

If you've been vinyl-curious but put off by the price of entry, this is about as low a barrier as it gets. 53% off brings it to under $30, which is genuinely remarkable for a three-speed turntable that plays 7", 10", and 12" records and has built-in stereo speakers. It also has Bluetooth input if you want to use it as a speaker for your phone, plus AUX and RCA output. View Deal

The Casio F91W is one of those objects that has somehow never gone out of style despite being entirely unchanged since 1991. In a world of smartwatches demanding your attention, there is something deeply refreshing about strapping on something that just gets on with being a watch.View Deal

If you're going to sit at a desk all day, you may as well do it with a mouse that looks like it time-travelled from 1989. The R8 is inspired by classic retro gaming hardware and comes with a charging dock that doubles as a signal extender — but underneath the nostalgia is a genuinely serious piece of kit. It's built with a PAW 3395 optical sensor, 4K polling rate, and programmable buttons. Available in grey, green, and a C64-inspired colourway for the real purists. View Deal

Lava Lamps will always be a retro staple. This 16-inch version adds a modern twist with 17 colour-changing LED modes controlled by a wireless remote, so you can dial in exactly the right vibe for whatever the evening calls for. It comes in black or silver base and will look great on a desk, beside table, or games room. View Deal

Pac-Man. On a tiny officially licensed arcade cabinet. That fits in your hand. If you need more convincing than that, the 2.75-inch full colour display is sharp, it has a headphone jack for private play, and it can run on batteries or USB-C. Essentially, it's a collectible as much as it is a game and it also comes in Galaga, Space Invaders, Tetris, and Street Fighter versions if Pac-Man isn't your era. View Deal

Sometimes the most retro thing you can do is make snow cones in your kitchen on a Tuesday. This table-top machine uses stainless steel blades to shave ice into fluffy snow cone consistency, comes with two reusable cups and a scoop, and makes up to 20 treats per session. Stock up on syrups and you've got the most popular item at any summer gathering sorted.View Deal

The 180-piece laser-cut wooden kit builds into an 11.5-inch antique-style globe complete with a wooden stand and built-in LED light, no glue required. It's the kind of thing you put together on a slow Sunday afternoon, and then can't quite bring yourself to take apart because it looks too good on the shelf. Relaxing to build, beautiful once finished, and would make a thoughtful gift. View Deal

If your childhood gaming memories involve blowing into a cartridge and hoping for the best, this one is for you. The R36MAX II is a handheld retro console that comes pre-loaded with over 18,000 classic games across multiple eras, on a 4.5-inch HD screen with a battery that will last well beyond any journey or lunch break. It has a detachable stand for hands-free play, swappable joysticks, and a UI simple enough that you'll be playing within minutes of opening the box. It runs on Linux. View Deal

Here's the thing about phone photos: you take thousands of them and look back at almost none of them. There's something about holding an actual print that makes a moment feel real in a way that a camera roll just doesn't. The Kodak Mini 2 Retro connects to your phone via Bluetooth and prints credit-card-sized photos in about 55 seconds using dye sublimation — so no ink cartridges to replace, and prints that are waterproof and fade-resistant. This bundle comes with 38 sheets included so you can get started straight away.View Deal

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