Skip to content

Commit 7ac6a71

Browse files
committed
Reworked API fetching
1 parent 5e3f14e commit 7ac6a71

File tree

2 files changed

+179
-112
lines changed

2 files changed

+179
-112
lines changed

scripts/fetchStock.js

Lines changed: 124 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { loadFavourites } from './favourites.js';
22
import lastInStockTimes from './lastInStockTimes.js';
33

44
window.currentLocale = localStorage.getItem("selectedLocale") || "en-gb";
5+
let isLocaleChanging = false;
56

67
// Helper function to format display time
78
function formatDisplayTime(isoString) {
@@ -30,22 +31,58 @@ function updateLastInStockTime(productModel, locale) {
3031
if (!window.fetchWorker) {
3132
window.fetchWorker = new Worker('./scripts/fetchWorker.js');
3233
window.fetchWorker.addEventListener('message', (event) => {
33-
const data = event.data;
34-
if (data?.searchedProducts?.productDetails) {
35-
updateStockStatus(data.searchedProducts.productDetails);
34+
if (event.data?.success) {
35+
updateStockStatus(event.data.data);
3636
isApiDown = false;
3737
} else {
38+
console.error('Fetch error:', event.data?.error || 'Unknown error');
3839
if (!isApiDown) {
3940
isApiDown = true;
4041
playNotificationSound();
4142
}
43+
document.getElementById("fetch-time").textContent = `Error: ${event.data?.error || 'API request failed'}`;
4244
}
45+
isLocaleChanging = false;
46+
});
47+
}
48+
49+
function showLoadingSpinners() {
50+
const gpuRows = document.querySelectorAll("tbody tr");
51+
const spinnerHTML = `
52+
<svg class="loading-spinner" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
53+
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2">
54+
<path stroke-dasharray="16" stroke-dashoffset="16" d="M12 3c4.97 0 9 4.03 9 9">
55+
<animate fill="freeze" attributeName="stroke-dashoffset" dur="0.3s" values="16;0"/>
56+
<animateTransform attributeName="transform" dur="1.5s" repeatCount="indefinite" type="rotate" values="0 12 12;360 12 12"/>
57+
</path>
58+
<path stroke-dasharray="64" stroke-dashoffset="64" stroke-opacity="0.3" d="M12 3c4.97 0 9 4.03 9 9c0 4.97 -4.03 9 -9 9c-4.97 0 -9 -4.03 -9 -9c0 -4.97 4.03 -9 9 -9Z">
59+
<animate fill="freeze" attributeName="stroke-dashoffset" dur="1.2s" values="64;0"/>
60+
</path>
61+
</g>
62+
</svg>`;
63+
64+
gpuRows.forEach(row => {
65+
const cells = [
66+
row.querySelector(".stock-status"),
67+
row.querySelector(".product-price"),
68+
row.querySelector(".product-link")
69+
];
70+
71+
cells.forEach(cell => {
72+
if (cell) {
73+
cell.innerHTML = spinnerHTML;
74+
cell.style.color = ""; // Reset to default
75+
cell.className = cell.className.split(' ')[0]; // Keep base class
76+
}
77+
});
4378
});
4479
}
4580

46-
// Fetch stock data
4781
export function fetchStockData() {
4882
if (window.fetchWorker) {
83+
if (isLocaleChanging) {
84+
showLoadingSpinners();
85+
}
4986
window.fetchWorker.postMessage({ type: 'fetch', locale: window.currentLocale });
5087
}
5188
}
@@ -59,117 +96,109 @@ function formatLastFetchTime() {
5996
let lastFetchTime = null;
6097
let isApiDown = false;
6198

62-
// Update table data
63-
export function updateStockStatus(products) {
64-
console.log("Updating table data:", products);
99+
export function updateStockStatus(inventoryData) {
65100
const gpuRows = document.querySelectorAll("tbody tr");
66-
67-
// Update the last fetch time
68101
lastFetchTime = formatLastFetchTime();
69102
document.getElementById("fetch-time").textContent = `Last fetch: ${lastFetchTime}`;
70103

71-
// Create a set of GPU models from the fetched data
72-
const fetchedGpuModels = new Set(products.map(product => product.displayName));
73-
74-
// Clear previous table data
75-
gpuRows.forEach(row => {
76-
const statusCell = row.querySelector(".stock-status");
77-
const priceCell = row.querySelector(".product-price");
78-
const linkCell = row.querySelector(".product-link");
79-
const skuSpan = row.querySelector(".product-sku");
80-
81-
if (statusCell) {
82-
statusCell.textContent = "";
83-
statusCell.classList.remove("in-stock", "out-of-stock", "unknown-status");
84-
}
85-
if (priceCell) priceCell.textContent = "";
86-
if (linkCell) linkCell.innerHTML = "";
87-
if (skuSpan) skuSpan.textContent = "";
88-
});
89-
90-
// Update the table with the fetched data
91-
products.forEach(product => {
92-
if (product.manufacturer === "NVIDIA") {
93-
gpuRows.forEach(row => {
94-
const modelNameSpan = row.querySelector(".model-name");
95-
const rowModelName = modelNameSpan?.textContent;
96-
97-
if (rowModelName && product.displayName === rowModelName) {
98-
const statusCell = row.querySelector(".stock-status");
99-
const priceCell = row.querySelector(".product-price");
100-
const linkCell = row.querySelector(".product-link");
101-
const skuSpan = row.querySelector(".product-sku");
102-
const alertIcon = row.querySelector(".alert-icon");
103-
104-
const isFavourited = alertIcon?.getAttribute("data-favourite") === "true";
105-
const previousStatus = statusCell?.textContent;
106-
107-
// Update stock status
108-
if (statusCell) {
109-
let stockStatus = "";
110-
if (product.prdStatus === "buy_now") {
111-
stockStatus = "In Stock";
112-
statusCell.classList.add("in-stock");
113-
114-
// Update last in-stock time
115-
updateLastInStockTime(product.displayName, window.currentLocale);
116-
117-
if (isFavourited && previousStatus !== "In Stock") {
118-
playNotificationSound();
119-
}
120-
} else if (product.prdStatus === "out_of_stock") {
121-
stockStatus = "Out of Stock";
122-
statusCell.classList.add("out-of-stock");
123-
}
124-
statusCell.textContent = stockStatus;
125-
126-
// Set tooltip with last in-stock time
127-
const lastTime = lastInStockTimes[product.displayName]?.[window.currentLocale];
128-
statusCell.setAttribute("data-tooltip", `Last In Stock: ${formatDisplayTime(lastTime)}`);
129-
}
130-
131-
// Update other cells
132-
if (priceCell && product.productPrice) {
133-
// Create a temporary element to decode HTML entities
134-
const tempElement = document.createElement('div');
135-
tempElement.innerHTML = product.productPrice;
136-
// Set the decoded text content
137-
priceCell.textContent = tempElement.textContent;
138-
priceCell.style.color = "";
139-
tempElement.remove();
140-
}
141-
if (linkCell && product.internalLink) {
142-
linkCell.innerHTML = `<a href="${product.internalLink}" target="_blank" rel="noopener noreferrer">View<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><path fill="currentColor" d="M5 17.59L15.59 7H9V5h10v10h-2V8.41L6.41 19z"/></svg></a>`;
143-
linkCell.style.color = "";
144-
}
145-
if (skuSpan && product.productSKU) {
146-
skuSpan.textContent = `SKU: ${product.productSKU}`;
147-
}
148-
}
149-
});
150-
}
104+
const inventoryMap = {};
105+
inventoryData.forEach(item => {
106+
inventoryMap[item.displayName] = item;
151107
});
152108

153-
// Set "Unknown" status for GPUs not found in API response
154109
gpuRows.forEach(row => {
155110
const modelNameSpan = row.querySelector(".model-name");
156111
const rowModelName = modelNameSpan?.textContent;
112+
if (!rowModelName) return;
113+
157114
const statusCell = row.querySelector(".stock-status");
158115
const priceCell = row.querySelector(".product-price");
159116
const linkCell = row.querySelector(".product-link");
160117
const skuSpan = row.querySelector(".product-sku");
118+
const alertIcon = row.querySelector(".alert-icon");
119+
120+
const item = inventoryMap[rowModelName];
121+
const isFavourited = alertIcon?.getAttribute("data-favourite") === "true";
122+
const previousStatus = statusCell?.textContent;
123+
124+
// Reset cell styling
125+
[statusCell, priceCell, linkCell, skuSpan].forEach(cell => {
126+
if (cell) {
127+
cell.className = cell.className.split(' ')[0]; // Keep base class
128+
cell.style.color = "";
129+
}
130+
});
161131

162-
if (rowModelName && !fetchedGpuModels.has(rowModelName)) {
132+
if (!item) {
163133
if (statusCell) {
164134
statusCell.textContent = "Not Available";
165135
statusCell.classList.add("unknown-status");
166136
const lastTime = lastInStockTimes[rowModelName]?.[window.currentLocale];
167137
statusCell.setAttribute("data-tooltip", `Last In Stock: ${formatDisplayTime(lastTime)}`);
168138
}
169-
if (priceCell) priceCell.textContent = "N/A";
139+
if (priceCell) {
140+
priceCell.textContent = "N/A";
170141
priceCell.style.color = "#666";
171-
if (linkCell) linkCell.innerHTML = `<a href="#" style="color:#666;" rel="noopener noreferrer">N/A</a>`;
172-
if (skuSpan) skuSpan.textContent = "SKU: N/A";
142+
}
143+
if (linkCell) {
144+
linkCell.innerHTML = '<a href="#" style="color:#666;">N/A</a>';
145+
}
146+
if (skuSpan) {
147+
skuSpan.textContent = "SKU: N/A";
148+
}
149+
return;
150+
}
151+
152+
// Update status cell with full styling
153+
if (statusCell) {
154+
const isInStock = item.inventory?.listMap?.some(i => i.is_active === "true");
155+
statusCell.textContent = isInStock ? "In Stock" : "Out of Stock";
156+
statusCell.classList.add(isInStock ? "in-stock" : "out-of-stock");
157+
158+
if (isInStock) {
159+
updateLastInStockTime(rowModelName, window.currentLocale);
160+
if (isFavourited && previousStatus !== "In Stock") {
161+
playNotificationSound();
162+
}
163+
}
164+
165+
const lastTime = lastInStockTimes[rowModelName]?.[window.currentLocale];
166+
statusCell.setAttribute("data-tooltip", `Last In Stock: ${formatDisplayTime(lastTime)}`);
167+
}
168+
169+
// Update price cell with full styling
170+
if (priceCell) {
171+
if (item.productPrice) {
172+
const temp = document.createElement('div');
173+
temp.innerHTML = item.productPrice;
174+
priceCell.textContent = temp.textContent;
175+
priceCell.style.color = "";
176+
} else {
177+
priceCell.textContent = "N/A";
178+
priceCell.style.color = "#666";
179+
}
180+
}
181+
182+
// Update link cell with full styling
183+
if (linkCell) {
184+
const url = item.inventory?.listMap?.[0]?.product_url || item.internalLink;
185+
if (url) {
186+
linkCell.innerHTML = `
187+
<a href="${url}" target="_blank" rel="noopener noreferrer">
188+
View
189+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24">
190+
<path fill="currentColor" d="M5 17.59L15.59 7H9V5h10v10h-2V8.41L6.41 19z"/>
191+
</svg>
192+
</a>`;
193+
linkCell.style.color = "";
194+
} else {
195+
linkCell.innerHTML = '<a href="#" style="color:#666;">N/A</a>';
196+
}
197+
}
198+
199+
// Update SKU
200+
if (skuSpan) {
201+
skuSpan.textContent = item.productSKU ? `SKU: ${item.productSKU}` : "SKU: N/A";
173202
}
174203
});
175204

@@ -204,6 +233,7 @@ document.addEventListener("DOMContentLoaded", () => {
204233
if (localeDropdown) {
205234
localeDropdown.value = window.currentLocale;
206235
localeDropdown.addEventListener("change", (event) => {
236+
isLocaleChanging = true;
207237
window.currentLocale = event.target.value;
208238
localStorage.setItem("selectedLocale", window.currentLocale);
209239
fetchStockData();

scripts/fetchWorker.js

Lines changed: 55 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,62 @@
1-
self.addEventListener('message', (event) => {
2-
1+
self.addEventListener('message', async (event) => {
32
if (event.data.type === 'fetch') {
43
const locale = event.data.locale || 'en-gb';
5-
64
console.log('Fetching data for locale:', locale);
75

8-
// Fetch data from the API with the selected locale
9-
fetch(`https://api.nvidia.partners/edge/product/search?locale=${locale}&page=1&limit=12&manufacturer=NVIDIA&category=GPU`)
10-
.then(response => {
11-
if (!response.ok) {
12-
throw new Error(`Network response was not OK: ${response.statusText}`);
6+
try {
7+
// 1. Get product data (for prices and SKUs)
8+
const productResponse = await fetch(
9+
`https://api.nvidia.partners/edge/product/search?locale=${locale}&page=1&limit=12&manufacturer=NVIDIA&category=GPU`
10+
);
11+
12+
if (!productResponse.ok) {
13+
throw new Error(`Product API failed: ${productResponse.statusText}`);
14+
}
15+
16+
const productData = await productResponse.json();
17+
18+
if (!productData?.searchedProducts?.productDetails) {
19+
throw new Error('No product details found');
20+
}
21+
22+
// 2. Process each product to get inventory status
23+
const results = [];
24+
for (const product of productData.searchedProducts.productDetails) {
25+
if (!product.productSKU) continue;
26+
27+
try {
28+
const inventoryResponse = await fetch(
29+
`https://api.store.nvidia.com/partner/v1/feinventory?status=1&skus=${product.productSKU}&locale=${locale}`
30+
);
31+
32+
const inventoryData = inventoryResponse.ok
33+
? await inventoryResponse.json()
34+
: null;
35+
36+
results.push({
37+
displayName: product.displayName,
38+
productSKU: product.productSKU,
39+
productPrice: product.productPrice, // From original API
40+
internalLink: product.internalLink, // From original API
41+
inventory: inventoryData // From inventory API
42+
});
43+
} catch (err) {
44+
console.warn(`Error processing ${product.productSKU}:`, err);
45+
// Still include the product with basic info
46+
results.push({
47+
displayName: product.displayName,
48+
productSKU: product.productSKU,
49+
productPrice: product.productPrice,
50+
internalLink: product.internalLink,
51+
inventory: null
52+
});
1353
}
14-
return response.json();
15-
})
16-
.then(data => {
17-
console.log('Data fetched successfully:', data);
18-
self.postMessage(data);
19-
})
20-
.catch(error => {
21-
console.error('Fetch error:', error);
22-
self.postMessage({});
23-
});
54+
}
55+
56+
self.postMessage({ success: true, data: results });
57+
} catch (error) {
58+
console.error('Main fetch error:', error);
59+
self.postMessage({ success: false, error: error.message });
60+
}
2461
}
2562
});

0 commit comments

Comments
 (0)