WFDASH-WEBAPP/scripts/nss-scrape-bundle.js

132 lines
5.3 KiB
JavaScript
Raw Normal View History

// Run in the browser console on nssfasteners.co.za after login.
// Fetches all Grade 8 (101 items) + A2 (21 items) hex nut DIN934 product pages,
// parses the hidden unit prices out of the HTML, builds PosDescription strings,
// and triggers a CSV download.
(async () => {
const BASE = 'https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html';
const FILTERS = [
{ grade: '8', gradeId: '771', expectedCount: 101 },
{ grade: 'A2', gradeId: '773', expectedCount: 21 },
];
// ---- helpers ----
const fetchHtml = async (url) => {
const res = await fetch(url, { credentials: 'include' });
if (!res.ok) throw new Error(`HTTP ${res.status} for ${url}`);
return await res.text();
};
const parsePage = (html) => {
const doc = new DOMParser().parseFromString(html, 'text/html');
const items = Array.from(doc.querySelectorAll('.product-item'));
return items.map(item => {
const titleEl = item.querySelector('.items-center.justify-center.text-primary');
const title = titleEl ? titleEl.textContent.trim() : '';
const sku = title.includes(' - ') ? title.split(' - ').pop().trim() : '';
const linkEl = item.querySelector('a.product-item-photo, a.product.photo');
const url = linkEl ? linkEl.getAttribute('href') : '';
// pull the description block (Material / Diameter / Thread / Grade / Finish / Pack)
const block = (item.textContent || '').replace(/\s+/g, ' ');
const get = (label) => {
const m = block.match(new RegExp(label + '\\s*:?\\s*([^\\s].*?)(?=\\s+(?:Diameter|Thread|Grade|Finish|Sold|Metric|Hi-Tensile|Stainless|Mild|Brass|$))', 'i'));
return m ? m[1].trim() : '';
};
const material = /Hi-Tensile/i.test(block) ? 'Hi-Tensile'
: /Stainless Steel/i.test(block) ? 'Stainless Steel'
: /Mild Steel/i.test(block) ? 'Mild Steel'
: /Brass/i.test(block) ? 'Brass'
: '';
const diameter = get('Diameter');
const thread = get('Thread');
const grade = get('Grade');
const finish = get('Finish');
const pack = /Sold as a Single Piece/i.test(block) ? 'Single Piece'
: /Sold as a Box/i.test(block) ? 'Box'
: /Sold as a Pack/i.test(block) ? 'Pack'
: '';
// Unit price is in the HTML but hidden with display:none
const unitPriceEl = item.querySelector('.unit-price');
const priceMatch = unitPriceEl ? unitPriceEl.textContent.match(/R\s*([\d.]+)/) : null;
const unitPrice = priceMatch ? priceMatch[1] : '';
// Description is the parent product type (e.g. "Hex Nut DIN934")
const desc = title.includes(' - ') ? title.split(' - ')[0].trim() : '';
// Build PosDescription like: "M39 NUT 8 PLN"
// diameter -> "M{value}", normalised
const sizeMatch = diameter.match(/(\d+(?:\.\d+)?)\s*mm/i);
const size = sizeMatch ? 'M' + sizeMatch[1].replace(/\.0$/, '') : diameter;
const gradeShort = grade === 'Stainless' ? '' : grade; // "8" or "A2"
const posDescription = [size, 'NUT', gradeShort, finish.toUpperCase() === 'PLAIN' ? 'PLN' : finish.toUpperCase()]
.filter(Boolean).join(' ');
return {
sku,
posDescription,
description: desc,
material,
grade,
diameter,
thread,
finish,
pack,
unitPrice: unitPrice ? 'R ' + unitPrice : '',
url,
};
});
};
const allRows = [];
for (const f of FILTERS) {
// First page
const firstUrl = `${BASE}?gradematerial=${f.gradeId}`;
const firstHtml = await fetchHtml(firstUrl);
const firstRows = parsePage(firstHtml);
allRows.push(...firstRows);
// Discover total page count from toolbar
const doc = new DOMParser().parseFromString(firstHtml, 'text/html');
const m = (doc.querySelector('.toolbar-amount')?.textContent || '').match(/of\s+(\d+)/);
const total = m ? parseInt(m[1], 10) : f.expectedCount;
const pages = Math.ceil(total / 10);
console.log(`Grade ${f.grade}: ${firstRows.length} items on page 1, ${pages} pages total, expected ${total}`);
// Remaining pages
for (let p = 2; p <= pages; p++) {
const html = await fetchHtml(`${BASE}?gradematerial=${f.gradeId}&p=${p}`);
const rows = parsePage(html);
allRows.push(...rows);
console.log(`Grade ${f.grade}: page ${p}/${pages} -> ${rows.length} items`);
}
}
console.log(`TOTAL ROWS: ${allRows.length}`);
// Build CSV
const headers = ['SKU','PosDescription','Description','Material','Grade','Diameter','Thread','Finish','Pack','UnitPrice','SourceURL'];
const esc = (v) => {
const s = (v ?? '').toString().replace(/"/g, '""');
return /[",\n]/.test(s) ? `"${s}"` : s;
};
const csv = [headers.join(',')]
.concat(allRows.map(r => headers.map(h => esc(r[h.toLowerCase()])).join(',')))
.join('\n');
// Trigger download
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'nss-hex-nut-din934-grade8-a2.csv';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
return { count: allRows.length, sample: allRows.slice(0, 3) };
})();