Size
18.8 KB
Version
1.0.1
Created
Jan 28, 2026
Updated
20 days ago
1// ==UserScript==
2// @name Amazon Shoe Sale Tracker
3// @description A new extension
4// @version 1.0.1
5// @match https://*.amazon.com/*
6// @icon https://www.amazon.com/favicon.ico
7// @grant GM.getValue
8// @grant GM.setValue
9// @grant GM.deleteValue
10// ==/UserScript==
11(function() {
12 'use strict';
13
14 console.log('Amazon Shoe Sale Tracker initialized');
15
16 // Debounce function to prevent excessive calls
17 function debounce(func, wait) {
18 let timeout;
19 return function executedFunction(...args) {
20 const later = () => {
21 clearTimeout(timeout);
22 func(...args);
23 };
24 clearTimeout(timeout);
25 timeout = setTimeout(later, wait);
26 };
27 }
28
29 // Function to extract product data from a product item
30 function extractProductData(productItem) {
31 try {
32 const titleElement = productItem.querySelector('a[data-testid="product-grid-title"]');
33 const title = titleElement ? titleElement.textContent.trim() : 'Unknown Product';
34
35 const priceDiv = productItem.querySelector('div[data-testid="grid-item-buy-price"]');
36 if (!priceDiv) return null;
37
38 // Check for strikethrough price (indicates sale)
39 const strikeThroughPrice = priceDiv.querySelector('.StrikeThroughPrice__strikePrice__stBvh');
40 if (!strikeThroughPrice) return null; // Not on sale
41
42 const buyPriceElement = priceDiv.querySelector('span[data-testid^="$"]');
43 const buyPrice = buyPriceElement ? buyPriceElement.getAttribute('data-testid') : 'N/A';
44 const originalPrice = strikeThroughPrice.getAttribute('data-testid') || 'N/A';
45
46 // Get product link
47 const linkElement = productItem.querySelector('a[href*="/dp/"]');
48 const productUrl = linkElement ? 'https://www.amazon.com' + linkElement.getAttribute('href') : '';
49
50 // Get image
51 const imgElement = productItem.querySelector('img[data-testid="image"]');
52 const imageUrl = imgElement ? imgElement.src : '';
53
54 // Get brand
55 const brandElement = productItem.querySelector('div[data-testid="grid-item-info"] > div > div:first-child');
56 const brand = brandElement ? brandElement.textContent.trim() : '';
57
58 // Calculate discount percentage
59 const buyPriceNum = parseFloat(buyPrice.replace('$', ''));
60 const originalPriceNum = parseFloat(originalPrice.replace('$', ''));
61 const discountPercent = originalPriceNum > 0 ? Math.round(((originalPriceNum - buyPriceNum) / originalPriceNum) * 100) : 0;
62
63 return {
64 title,
65 brand,
66 buyPrice,
67 originalPrice,
68 discountPercent,
69 productUrl,
70 imageUrl,
71 timestamp: Date.now(),
72 id: productUrl.match(/\/dp\/([A-Z0-9]+)/)?.[1] || Date.now().toString()
73 };
74 } catch (error) {
75 console.error('Error extracting product data:', error);
76 return null;
77 }
78 }
79
80 // Function to highlight sale items
81 async function highlightSaleItems() {
82 console.log('Scanning for sale items...');
83 const productItems = document.querySelectorAll('li[data-testid="product-grid-item"]');
84 console.log(`Found ${productItems.length} product items`);
85
86 let saleCount = 0;
87 const saleProducts = [];
88
89 productItems.forEach(item => {
90 const priceDiv = item.querySelector('div[data-testid="grid-item-buy-price"]');
91 if (!priceDiv) return;
92
93 const strikeThroughPrice = priceDiv.querySelector('.StrikeThroughPrice__strikePrice__stBvh');
94 if (strikeThroughPrice) {
95 // This item is on sale
96 saleCount++;
97
98 // Add highlight border
99 if (!item.classList.contains('sale-highlighted')) {
100 item.style.border = '3px solid #ff6b6b';
101 item.style.borderRadius = '8px';
102 item.style.boxShadow = '0 0 15px rgba(255, 107, 107, 0.5)';
103 item.style.position = 'relative';
104 item.classList.add('sale-highlighted');
105
106 // Add sale badge
107 const badge = document.createElement('div');
108 badge.className = 'sale-badge';
109 badge.textContent = '🔥 ON SALE';
110 badge.style.cssText = `
111 position: absolute;
112 top: 10px;
113 right: 10px;
114 background: linear-gradient(135deg, #ff6b6b 0%, #ee5a6f 100%);
115 color: white;
116 padding: 8px 15px;
117 border-radius: 20px;
118 font-weight: bold;
119 font-size: 12px;
120 z-index: 10;
121 box-shadow: 0 2px 8px rgba(0,0,0,0.2);
122 letter-spacing: 0.5px;
123 `;
124 item.style.position = 'relative';
125 item.insertBefore(badge, item.firstChild);
126
127 // Extract and save product data
128 const productData = extractProductData(item);
129 if (productData) {
130 saleProducts.push(productData);
131 console.log('Found sale item:', productData.title, productData.buyPrice, 'was', productData.originalPrice);
132 }
133 }
134 }
135 });
136
137 // Save sale products to database
138 if (saleProducts.length > 0) {
139 await saveSaleProducts(saleProducts);
140 }
141
142 console.log(`Highlighted ${saleCount} items on sale`);
143 }
144
145 // Save sale products to database
146 async function saveSaleProducts(newProducts) {
147 try {
148 const existingData = await GM.getValue('saleProducts', '[]');
149 const existingProducts = JSON.parse(existingData);
150
151 // Create a map of existing products by ID
152 const productMap = new Map();
153 existingProducts.forEach(p => productMap.set(p.id, p));
154
155 // Add or update products
156 newProducts.forEach(p => {
157 productMap.set(p.id, p);
158 });
159
160 // Convert back to array and save
161 const updatedProducts = Array.from(productMap.values());
162 await GM.setValue('saleProducts', JSON.stringify(updatedProducts));
163 console.log(`Saved ${newProducts.length} new sale products. Total: ${updatedProducts.length}`);
164
165 // Update badge count
166 updateBadgeCount(updatedProducts.length);
167 } catch (error) {
168 console.error('Error saving sale products:', error);
169 }
170 }
171
172 // Update badge count on floating button
173 function updateBadgeCount(count) {
174 const badge = document.getElementById('sale-tracker-badge');
175 if (badge) {
176 badge.textContent = count;
177 }
178 }
179
180 // Create floating button to view saved sales
181 function createFloatingButton() {
182 const button = document.createElement('button');
183 button.id = 'sale-tracker-button';
184 button.innerHTML = `
185 <span style="font-size: 24px;">👟</span>
186 <span style="font-size: 11px; margin-top: 2px;">Sale Tracker</span>
187 <span id="sale-tracker-badge" style="
188 position: absolute;
189 top: -5px;
190 right: -5px;
191 background: #ff6b6b;
192 color: white;
193 border-radius: 50%;
194 width: 24px;
195 height: 24px;
196 display: flex;
197 align-items: center;
198 justify-content: center;
199 font-size: 11px;
200 font-weight: bold;
201 border: 2px solid white;
202 ">0</span>
203 `;
204 button.style.cssText = `
205 position: fixed;
206 bottom: 30px;
207 right: 30px;
208 width: 80px;
209 height: 80px;
210 border-radius: 50%;
211 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
212 color: white;
213 border: none;
214 cursor: pointer;
215 box-shadow: 0 4px 15px rgba(0,0,0,0.3);
216 z-index: 10000;
217 display: flex;
218 flex-direction: column;
219 align-items: center;
220 justify-content: center;
221 font-weight: bold;
222 transition: transform 0.2s, box-shadow 0.2s;
223 `;
224
225 button.addEventListener('mouseenter', () => {
226 button.style.transform = 'scale(1.1)';
227 button.style.boxShadow = '0 6px 20px rgba(0,0,0,0.4)';
228 });
229
230 button.addEventListener('mouseleave', () => {
231 button.style.transform = 'scale(1)';
232 button.style.boxShadow = '0 4px 15px rgba(0,0,0,0.3)';
233 });
234
235 button.addEventListener('click', showSalePanel);
236 document.body.appendChild(button);
237
238 // Load initial count
239 loadBadgeCount();
240 }
241
242 // Load badge count
243 async function loadBadgeCount() {
244 try {
245 const existingData = await GM.getValue('saleProducts', '[]');
246 const products = JSON.parse(existingData);
247 updateBadgeCount(products.length);
248 } catch (error) {
249 console.error('Error loading badge count:', error);
250 }
251 }
252
253 // Show sale panel
254 async function showSalePanel() {
255 console.log('Opening sale panel...');
256
257 // Check if panel already exists
258 let panel = document.getElementById('sale-tracker-panel');
259 if (panel) {
260 panel.style.display = panel.style.display === 'none' ? 'block' : 'none';
261 return;
262 }
263
264 // Create panel
265 panel = document.createElement('div');
266 panel.id = 'sale-tracker-panel';
267 panel.style.cssText = `
268 position: fixed;
269 top: 50%;
270 left: 50%;
271 transform: translate(-50%, -50%);
272 width: 90%;
273 max-width: 900px;
274 height: 80vh;
275 background: white;
276 border-radius: 15px;
277 box-shadow: 0 10px 40px rgba(0,0,0,0.3);
278 z-index: 10001;
279 display: flex;
280 flex-direction: column;
281 overflow: hidden;
282 `;
283
284 // Header
285 const header = document.createElement('div');
286 header.style.cssText = `
287 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
288 color: white;
289 padding: 20px;
290 display: flex;
291 justify-content: space-between;
292 align-items: center;
293 `;
294 header.innerHTML = `
295 <h2 style="margin: 0; font-size: 24px;">👟 Saved Sale Items</h2>
296 <div>
297 <button id="clear-sales-btn" style="
298 background: rgba(255,255,255,0.2);
299 color: white;
300 border: 1px solid white;
301 padding: 8px 15px;
302 border-radius: 5px;
303 cursor: pointer;
304 margin-right: 10px;
305 font-weight: bold;
306 ">Clear All</button>
307 <button id="close-panel-btn" style="
308 background: transparent;
309 color: white;
310 border: none;
311 font-size: 30px;
312 cursor: pointer;
313 padding: 0;
314 width: 30px;
315 height: 30px;
316 line-height: 30px;
317 ">×</button>
318 </div>
319 `;
320
321 // Content area
322 const content = document.createElement('div');
323 content.id = 'sale-panel-content';
324 content.style.cssText = `
325 flex: 1;
326 overflow-y: auto;
327 padding: 20px;
328 background: #f5f5f5;
329 `;
330
331 panel.appendChild(header);
332 panel.appendChild(content);
333 document.body.appendChild(panel);
334
335 // Load and display products
336 await loadSaleProducts();
337
338 // Event listeners
339 document.getElementById('close-panel-btn').addEventListener('click', () => {
340 panel.style.display = 'none';
341 });
342
343 document.getElementById('clear-sales-btn').addEventListener('click', async () => {
344 if (confirm('Are you sure you want to clear all saved sale items?')) {
345 await GM.setValue('saleProducts', '[]');
346 updateBadgeCount(0);
347 await loadSaleProducts();
348 }
349 });
350 }
351
352 // Load and display sale products
353 async function loadSaleProducts() {
354 try {
355 const existingData = await GM.getValue('saleProducts', '[]');
356 const products = JSON.parse(existingData);
357 const content = document.getElementById('sale-panel-content');
358
359 if (products.length === 0) {
360 content.innerHTML = `
361 <div style="
362 text-align: center;
363 padding: 60px 20px;
364 color: #666;
365 ">
366 <div style="font-size: 60px; margin-bottom: 20px;">👟</div>
367 <h3 style="margin: 0 0 10px 0;">No sale items saved yet</h3>
368 <p style="margin: 0;">Browse Amazon shoes and sale items will be automatically saved here!</p>
369 </div>
370 `;
371 return;
372 }
373
374 // Sort by discount percentage (highest first)
375 products.sort((a, b) => b.discountPercent - a.discountPercent);
376
377 content.innerHTML = `
378 <div style="
379 display: grid;
380 grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
381 gap: 20px;
382 ">
383 ${products.map(product => `
384 <div style="
385 background: white;
386 border-radius: 10px;
387 padding: 15px;
388 box-shadow: 0 2px 8px rgba(0,0,0,0.1);
389 transition: transform 0.2s;
390 cursor: pointer;
391 " onmouseover="this.style.transform='translateY(-5px)'; this.style.boxShadow='0 4px 15px rgba(0,0,0,0.2)'" onmouseout="this.style.transform='translateY(0)'; this.style.boxShadow='0 2px 8px rgba(0,0,0,0.1)'" onclick="window.open('${product.productUrl}', '_blank')">
392 <div style="
393 width: 100%;
394 height: 200px;
395 background: #f9f9f9;
396 border-radius: 8px;
397 margin-bottom: 12px;
398 display: flex;
399 align-items: center;
400 justify-content: center;
401 overflow: hidden;
402 ">
403 ${product.imageUrl ? `<img src="${product.imageUrl}" style="max-width: 100%; max-height: 100%; object-fit: contain;">` : '<div style="color: #ccc; font-size: 40px;">👟</div>'}
404 </div>
405 <div style="
406 position: absolute;
407 top: 25px;
408 right: 25px;
409 background: #ff6b6b;
410 color: white;
411 padding: 5px 10px;
412 border-radius: 15px;
413 font-weight: bold;
414 font-size: 12px;
415 ">-${product.discountPercent}%</div>
416 <div style="color: #888; font-size: 12px; margin-bottom: 5px;">${product.brand}</div>
417 <div style="
418 font-weight: bold;
419 margin-bottom: 10px;
420 font-size: 14px;
421 line-height: 1.4;
422 height: 40px;
423 overflow: hidden;
424 display: -webkit-box;
425 -webkit-line-clamp: 2;
426 -webkit-box-orient: vertical;
427 ">${product.title}</div>
428 <div style="display: flex; align-items: center; gap: 10px; margin-bottom: 8px;">
429 <span style="
430 font-size: 20px;
431 font-weight: bold;
432 color: #ff6b6b;
433 ">${product.buyPrice}</span>
434 <span style="
435 text-decoration: line-through;
436 color: #999;
437 font-size: 14px;
438 ">${product.originalPrice}</span>
439 </div>
440 <div style="
441 font-size: 11px;
442 color: #999;
443 ">Saved ${new Date(product.timestamp).toLocaleDateString()}</div>
444 </div>
445 `).join('')}
446 </div>
447 `;
448 } catch (error) {
449 console.error('Error loading sale products:', error);
450 }
451 }
452
453 // Initialize
454 function init() {
455 console.log('Initializing Amazon Shoe Sale Tracker...');
456
457 // Create floating button
458 createFloatingButton();
459
460 // Initial scan
461 setTimeout(() => {
462 highlightSaleItems();
463 }, 2000);
464
465 // Watch for DOM changes (new products loaded on scroll)
466 const debouncedHighlight = debounce(highlightSaleItems, 1000);
467 const observer = new MutationObserver(debouncedHighlight);
468
469 // Observe the product grid
470 const observeProductGrid = () => {
471 const productGrid = document.querySelector('[data-testid="product-grid-container"]');
472 if (productGrid) {
473 observer.observe(productGrid, {
474 childList: true,
475 subtree: true
476 });
477 console.log('Observing product grid for changes');
478 } else {
479 setTimeout(observeProductGrid, 1000);
480 }
481 };
482
483 observeProductGrid();
484 }
485
486 // Start when page is ready
487 if (document.readyState === 'loading') {
488 document.addEventListener('DOMContentLoaded', init);
489 } else {
490 init();
491 }
492
493})();