Skip to content

Instantly share code, notes, and snippets.

@wolph
Last active February 26, 2025 00:13
Show Gist options
  • Save wolph/1e245ffc144af232be5d79fc9b44dc43 to your computer and use it in GitHub Desktop.
Save wolph/1e245ffc144af232be5d79fc9b44dc43 to your computer and use it in GitHub Desktop.
Albert Heijn AH.nl kortingspercentage en prijs per kg script
// ==UserScript==
// @name Albert Heijn Korting Updated (with Promotion Parsing)
// @namespace https://wol.ph/
// @version 1.0.6
// @description Add price per unit and discount percentage to products and promotion cards (updated for new page structures)
// @author wolph
// @match https://www.ah.nl/*
// @icon https://icons.duckduckgo.com/ip2/ah.nl.ico
// @grant none
// @license BSD
// @downloadURL https://update.greasyfork.org/scripts/515067/Albert%20Heijn%20Korting.user.js
// @updateURL https://update.greasyfork.org/scripts/515067/Albert%20Heijn%20Korting.meta.js
// ==/UserScript==
const DEBUG = false;
(function () {
'use strict';
// Set to keep track of processed products and cards to avoid duplicate processing.
const processedProducts = new Set();
function updateProductInfo() {
console.log("Starting updateProductInfo");
// --- PROCESS PRODUCT CARDS ---
let productCards = document.querySelectorAll('article[data-testhook="product-card"]');
console.log(`Found ${productCards.length} product cards`);
productCards.forEach(function (productCard) {
// Use a unique identifier for the product
let productId =
productCard.getAttribute("data-product-id") ||
productCard.querySelector('a[class*="link_root__"]')?.getAttribute("href");
if (!productId) {
console.log("Product ID not found, skipping product-card");
return;
}
if (processedProducts.has(productId)) {
return;
}
console.log(`Processing product ${productId}`);
// --- Extract current price ---
// Look for the highlighted price element
let priceElement = productCard.querySelector('[data-testhook="price-amount"].price-amount_highlight__ekL92');
if (!priceElement) {
console.log("Highlighted price element not found, skipping product-card");
return;
}
// Extract integer and fractional parts
let priceInt = priceElement.querySelector('[class*="price-amount_integer__"]');
let priceFrac = priceElement.querySelector('[class*="price-amount_fractional__"]');
if (!priceInt || !priceFrac) {
console.log("Price integer or fractional part not found, skipping product-card");
return;
}
let price = parseFloat(priceInt.textContent + "." + priceFrac.textContent);
console.log(`Price: ${price}`);
// --- Extract unit size ---
let unitSizeElement = productCard.querySelector('[data-testhook="product-unit-size"]');
if (!unitSizeElement) {
console.log("Unit size element not found, skipping product-card");
return;
}
let unitSizeText = unitSizeElement.textContent.trim(); // e.g., "ca. 755 g" or "750 g"
console.log(`Unit size text: ${unitSizeText}`);
// Parse unit size (supports weight and count).
let unitMatch = unitSizeText.match(/(ca\.?\s*)?([\d.,]+)\s*(g|kg)/i);
let countMatch = unitSizeText.match(/(\d+)\s*(stuk|stuks)/i);
if (!unitMatch && !countMatch) {
console.log("Weight and count not found in unit size text, skipping product-card");
return;
}
let pricePerUnit = 0;
let unit = "";
if (unitMatch) {
let weight = parseFloat(unitMatch[2].replace(",", "."));
unit = unitMatch[3].toLowerCase();
console.log(`Weight: ${weight}, Unit: ${unit}`);
// Convert weight to grams if needed; we calculate per kg
if (unit === "kg") {
weight = weight * 1000;
console.log(`Converted weight to grams: ${weight}`);
} else {
// We display per kg even if provided in grams.
unit = "kg";
}
// Calculate price per kg
pricePerUnit = (price * 1000) / weight;
}
if (countMatch) {
let count = parseInt(countMatch[1]);
unit = countMatch[2].toLowerCase();
console.log(`Count: ${count}`);
// Calculate price per item
pricePerUnit = price / count;
}
console.log(`Price per ${unit}: €${pricePerUnit.toFixed(2)}`);
// --- Extract old price, if available ---
let oldPrice = null;
let oldPriceElement = productCard.querySelector('[data-testhook="price-amount"]:not(.price-amount_highlight__ekL92)');
if (oldPriceElement) {
let oldPriceInt = oldPriceElement.querySelector('[class*="price-amount_integer__"]');
let oldPriceFrac = oldPriceElement.querySelector('[class*="price-amount_fractional__"]');
if (oldPriceInt && oldPriceFrac) {
oldPrice = parseFloat(oldPriceInt.textContent + "." + oldPriceFrac.textContent);
console.log(`Old price: ${oldPrice}`);
}
}
// --- Calculate discount percentage ---
let discountPercentage = null;
if (oldPrice && oldPrice > price) {
discountPercentage = (((oldPrice - price) / oldPrice) * 100).toFixed(1);
console.log(`Calculated discount from prices: ${discountPercentage}%`);
} else {
// Try extracting discount percentage from shield promotion text
let shieldElement = productCard.querySelector('[data-testhook="product-shield"]');
if (shieldElement) {
let shieldTextElement = shieldElement.querySelector('[class*="shield_text__"]');
if (shieldTextElement) {
let shieldText = shieldTextElement.textContent.trim();
discountPercentage = parsePromotionText(shieldText, price) || "0";
console.log(`Extracted discount percentage from shield: ${discountPercentage}%`);
} else {
discountPercentage = "0";
console.log("No shield text found, setting discount to 0%");
}
} else {
discountPercentage = "0";
console.log("Shield element not found, setting discount to 0%");
}
}
// --- Create or update shield element ---
let shieldElement = productCard.querySelector('[data-testhook="product-shield"]');
if (!shieldElement) {
console.log("Creating shield element for product-card");
shieldElement = document.createElement("div");
shieldElement.className = "shield_root__SmhpN";
shieldElement.setAttribute("data-testhook", "product-shield");
let newShieldTextElement = document.createElement("span");
newShieldTextElement.className = "shield_text__kNeiW";
shieldElement.appendChild(newShieldTextElement);
let shieldContainer = productCard.querySelector('[class*="product-card-portrait_shieldProperties__"]');
if (!shieldContainer) {
console.log("Creating shield container for product-card");
shieldContainer = document.createElement("div");
shieldContainer.className = "product-card-portrait_shieldProperties__+JZJI";
let header = productCard.querySelector('[class*="header_root__"]');
if (header === null)
return;
header.appendChild(shieldContainer);
}
shieldContainer.appendChild(shieldElement);
}
let shieldTextElement = shieldElement.querySelector('[class*="shield_text__"]');
if (!shieldTextElement) {
shieldTextElement = document.createElement("span");
shieldTextElement.className = "shield_text__kNeiW";
shieldElement.appendChild(shieldTextElement);
}
// Update the shield text with discount percentage
shieldTextElement.textContent = `${discountPercentage}%`;
// Set background and text color based on discount percentage
let { backgroundColor, textColor } = getDiscountColors(discountPercentage);
shieldElement.style.backgroundColor = backgroundColor;
shieldElement.style.color = textColor;
// --- Update price element to include price per unit info ---
let priceContainer = priceElement.parentElement;
if (priceContainer) {
let pricePerUnitElement = priceContainer.querySelector(".price-per-unit");
if (!pricePerUnitElement) {
pricePerUnitElement = document.createElement("div");
pricePerUnitElement.className = "price-per-unit";
pricePerUnitElement.style.fontSize = "smaller";
priceContainer.appendChild(pricePerUnitElement);
}
pricePerUnitElement.textContent = `€${pricePerUnit.toFixed(2)} per ${unit}`;
}
// Mark this product as processed
processedProducts.add(productId);
console.log(`Product ${productId} processed`);
});
// --- PROCESS PROMOTION CARDS (NEW STRUCTURE) ---
let promotionCards = document.querySelectorAll('a.promotion-card_root__tQA3z');
console.log(`Found ${promotionCards.length} promotion cards`);
promotionCards.forEach(function (card) {
// Use the element's id or href as a unique identifier
let cardId = card.getAttribute("id") || card.getAttribute("href");
if (!cardId) {
console.log("Promotion card unique id not found, skipping");
return;
}
if (processedProducts.has(cardId)) {
return;
}
console.log(`Processing promotion card ${cardId}`);
// --- Extract current price ---
let priceElem = card.querySelector('[data-testhook="price"]');
if (!priceElem) {
console.log("Promotion price element not found, skipping promotion card");
return;
}
let priceNow = priceElem.getAttribute("data-testpricenow");
if (!priceNow) {
console.log("Promotion current price attribute missing, skipping");
return;
}
let currentPrice = parseFloat(priceNow);
console.log(`Promotion current price: ${currentPrice}`);
// --- Extract old price, if available ---
let oldPriceAttr = priceElem.getAttribute("data-testpricewas");
let oldPrice = oldPriceAttr ? parseFloat(oldPriceAttr) : null;
if (oldPrice) {
console.log(`Promotion old price: ${oldPrice}`);
}
// --- Parse unit size from title ---
// We assume that the card title contains unit information (e.g. "400 g", "1 l", "2 stuks")
let titleElem = card.querySelector('[data-testhook="card-title"]');
if (!titleElem) {
console.log("Promotion card title not found, skipping unit extraction");
return;
}
let titleText = titleElem.textContent.trim();
console.log(`Promotion card title: "${titleText}"`);
let unitMatch = titleText.match(/(ca\.?\s*)?([\d.,]+)\s*(g|kg)/i);
let countMatch = titleText.match(/(\d+)\s*(stuk|stuks)/i);
let ppu = 0;
let unit = "";
if (unitMatch) {
let weight = parseFloat(unitMatch[2].replace(",", "."));
unit = unitMatch[3].toLowerCase();
console.log(`Parsed weight: ${weight} ${unit}`);
if (unit === "kg") {
weight *= 1000;
} else {
unit = "kg"; // we display per kg even if provided in grams
}
ppu = (currentPrice * 1000) / weight;
} else if (countMatch) {
let count = parseInt(countMatch[1]);
unit = "stuk";
console.log(`Parsed count: ${count} ${unit}(s)`);
ppu = currentPrice / count;
} else {
console.log("No unit size found in promotion card title, skipping price per unit calculation");
}
// --- Calculate discount percentage ---
let discountPercentage = null;
if (oldPrice && oldPrice > currentPrice) {
discountPercentage = (((oldPrice - currentPrice) / oldPrice) * 100).toFixed(1);
console.log(`Calculated discount from promotion prices: ${discountPercentage}%`);
} else {
discountPercentage = "0";
console.log("No discount available on promotion card");
}
// --- Update shield element ---
// Promotion cards usually include a shield container with promotion texts.
let shieldElem = card.querySelector('[data-testhook="promotion-shields"]');
if (shieldElem) {
// Look for an element that displays promotion text within shield.
let shieldTextElem = shieldElem.querySelector('[data-testhook="promotion-text"]');
if (shieldTextElem) {
shieldTextElem.textContent = discountPercentage + "%";
} else {
shieldTextElem = document.createElement("span");
shieldTextElem.setAttribute("data-testhook", "promotion-text");
shieldTextElem.textContent = discountPercentage + "%";
shieldElem.appendChild(shieldTextElem);
}
let colors = getDiscountColors(discountPercentage);
shieldElem.style.backgroundColor = colors.backgroundColor;
shieldElem.style.color = colors.textColor;
}
// --- Update or create price-per-unit element ---
let priceContainer = priceElem.parentElement;
if (priceContainer) {
let ppuElem = priceContainer.querySelector(".price-per-unit");
if (!ppuElem) {
ppuElem = document.createElement("div");
ppuElem.className = "price-per-unit";
ppuElem.style.fontSize = "smaller";
priceContainer.appendChild(ppuElem);
}
if (ppu > 0) {
ppuElem.textContent = "€" + ppu.toFixed(2) + " per " + unit;
}
}
processedProducts.add(cardId);
console.log(`Promotion card ${cardId} processed`);
});
console.log("Finished updateProductInfo");
}
// Helper function to parse promotion text (e.g., "1 + 1 gratis")
function parsePromotionText(shieldText, pricePerItem) {
shieldText = shieldText.toLowerCase();
let discountPercentage = null;
if (shieldText.includes("%")) {
let discountMatch = shieldText.match(/(\d+)%\s*korting/i);
if (discountMatch) {
discountPercentage = parseFloat(discountMatch[1]);
}
} else if (shieldText.includes("gratis")) {
if (shieldText.includes("1+1") || shieldText.includes("1 + 1")) {
discountPercentage = 50;
} else if (shieldText.includes("2+1") || shieldText.includes("2 + 1")) {
discountPercentage = 33.33;
}
} else if (shieldText.includes("2e halve prijs")) {
discountPercentage = 25;
}
return discountPercentage;
}
function getDiscountColors(discountPercentage) {
let backgroundColor, textColor;
discountPercentage = parseFloat(discountPercentage);
if (discountPercentage >= 80) {
backgroundColor = "#008000"; // Dark Green
textColor = "#FFFFFF"; // White
} else if (discountPercentage >= 60) {
backgroundColor = "#32CD32"; // Lime Green
textColor = "#000000"; // Black
} else if (discountPercentage >= 40) {
backgroundColor = "#FFFF00"; // Yellow
textColor = "#000000"; // Black
} else if (discountPercentage >= 20) {
backgroundColor = "#FFA500"; // Orange
textColor = "#000000"; // Black
} else {
backgroundColor = "#FF0000"; // Red
textColor = "#FFFFFF"; // White
}
return { backgroundColor, textColor };
}
// Initial run and periodic update
window.setTimeout(updateProductInfo, 1000);
if (!DEBUG) setInterval(updateProductInfo, 5000);
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment