Created
July 3, 2025 19:29
-
-
Save MarcinKilarski/f4a54997f695d607e016076406c1b471 to your computer and use it in GitHub Desktop.
Amazon Affiliate Link Localizer & Multi-Region Dropdown
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| (function () { | |
| // Tracking IDs for each amazon affiliate site | |
| // you can get them at https://affiliate-program.amazon.com/ | |
| // TODO: add tracking IDs | |
| const amazonWebsites = { | |
| // North America | |
| "amazon.com": "COM-TRACKING-ID", | |
| // South America | |
| "amazon.com.br": "BR-TRACKING-ID", | |
| // Europe | |
| "amazon.co.uk": "UK-TRACKING-ID", | |
| "amazon.de": "DE-TRACKING-ID", | |
| "amazon.fr": "FR-TRACKING-ID", | |
| "amazon.nl": "NL-TRACKING-ID", | |
| "amazon.it": "IT-TRACKING-ID", | |
| "amazon.es": "ES-TRACKING-ID", | |
| "amazon.se": "SE-TRACKING-ID", | |
| "amazon.pl": "PL-TRACKING-ID", | |
| // Asia Pacific | |
| "amazon.co.jp": "JP-TRACKING-ID", | |
| "amazon.in": "IN-TRACKING-ID", | |
| "amazon.com.au": "AU-TRACKING-ID", | |
| "amazon.sg": "SG-TRACKING-ID", | |
| // Middle East | |
| "amazon.ae": "AE-TRACKING-ID", | |
| }; | |
| // Provide alternative url when there is not product listing in specific amazon site | |
| const amazonProductLinkExceptions = { | |
| B0DZXFNVQ2: { | |
| "amazon.sg": | |
| "https://www.amazon.sg/s?k=Ecovacs+X9+Pro+Omni&crid=2RAJMGLSIR9X7&sprefix=%2Caps%2C387&ref=nb_sb_noss", | |
| "amazon.co.jp": | |
| "https://www.amazon.co.jp/s?k=Ecovacs+X9+Pro+Omni&crid=9BUJYR27DH92&sprefix=%2Caps%2C174&ref=nb_sb_noss_2", | |
| "amazon.pl": | |
| "https://www.amazon.pl/s?k=Ecovacs+X9+Pro+Omni&__mk_pl_PL=%C3%85M%C3%85%C5%BD%C3%95%C3%91&crid=2D0LSAM4TPF6Q&sprefix=ecovacs+x9+pro+omni%2Caps%2C126&ref=nb_sb_noss_1", | |
| }, | |
| B0F1CJD8MQ: { | |
| "amazon.pl": | |
| "https://www.amazon.pl/ECOVACS-DEEBOT-T80-18000Pa-Odkurzacz/dp/B0DZ661PGD/?", | |
| "amazon.co.jp": | |
| "https://www.amazon.co.jp/s?k=Ecovacs+T80+Omni&crid=3N49BNCUWNWS9&sprefix=ecovacs+t80+omni%2Caps%2C171&ref=nb_sb_noss_1", | |
| }, | |
| }; | |
| const countryCodeToAmazonDomain = { | |
| // North America | |
| US: "amazon.com", | |
| CA: "amazon.com", | |
| MX: "amazon.com", | |
| // South America | |
| BR: "amazon.com.br", | |
| // Europe | |
| GB: "amazon.co.uk", | |
| IE: "amazon.co.uk", | |
| DE: "amazon.de", | |
| AT: "amazon.de", | |
| CH: "amazon.de", | |
| FR: "amazon.fr", | |
| BE: "amazon.fr", | |
| LU: "amazon.fr", | |
| IT: "amazon.it", | |
| SM: "amazon.it", | |
| VA: "amazon.it", | |
| ES: "amazon.es", | |
| AD: "amazon.es", | |
| SE: "amazon.se", | |
| PL: "amazon.pl", | |
| NL: "amazon.nl", | |
| // Asia Pacific | |
| JP: "amazon.co.jp", | |
| IN: "amazon.in", | |
| AU: "amazon.com.au", | |
| SG: "amazon.sg", | |
| // Middle East | |
| AE: "amazon.ae", | |
| SA: "amazon.ae", | |
| QA: "amazon.ae", | |
| KW: "amazon.ae", | |
| BH: "amazon.ae", | |
| OM: "amazon.ae", | |
| }; | |
| // Constants for styling and configuration | |
| const STYLES = { | |
| dropdown: { | |
| backgroundColor: "#f9f9f9", | |
| minWidth: "200px", | |
| boxShadow: "0px 8px 16px 0px rgba(0,0,0,0.2)", | |
| zIndex: "1", | |
| }, | |
| dropdownButton: { | |
| default: { | |
| color: "#666", | |
| strokeWidth: "3", | |
| }, | |
| hover: { | |
| color: "#333", | |
| strokeWidth: "3", | |
| }, | |
| active: { | |
| color: "#000", | |
| strokeWidth: "4", | |
| }, | |
| }, | |
| searchInput: { | |
| width: "100%", | |
| padding: "8px", | |
| boxSizing: "border-box", | |
| border: "1px solid #ddd", | |
| marginBottom: "4px", | |
| }, | |
| dropdownLink: { | |
| color: "black", | |
| padding: "12px 16px", | |
| textDecoration: "none", | |
| textAlign: "left", | |
| display: "block", | |
| }, | |
| }; | |
| // Function to extract product ID from an Amazon URL | |
| function getAmazonProductId(url) { | |
| const match = url.match(/\/dp\/([A-Z0-9]+)/); | |
| return match ? match[1] : null; | |
| } | |
| // Function to get user's country | |
| async function getUserCountry() { | |
| try { | |
| // TODO: Replace it with Cloudflare IP geolocation | |
| // If browser language doesn't provide country, use IP geolocation | |
| // Free usage: 1000 request per day | |
| // Pricing: https://ipapi.co/#pricing | |
| const response = await fetch("https://ipapi.co/json/"); | |
| const data = await response.json(); | |
| return data.country_code; | |
| } catch (error) { | |
| console.warn("Could not detect country:", error); | |
| return null; | |
| } | |
| } | |
| // Function to get Amazon domain based on country code | |
| function getAmazonDomainForCountry(countryCode) { | |
| return countryCodeToAmazonDomain[countryCode] || "amazon.com"; | |
| } | |
| // Function to get localized Amazon link | |
| function getLocalizedAmazonLink(domain, tag, productId) { | |
| if ( | |
| domain && | |
| amazonProductLinkExceptions[productId] && | |
| amazonProductLinkExceptions[productId][domain] | |
| ) { | |
| const newURL = amazonProductLinkExceptions[productId][domain]; | |
| return `${newURL}&tag=${tag}`; | |
| } | |
| return `https://www.${domain}/dp/${productId}?tag=${tag}`; | |
| } | |
| // Function to create and style the dropdown button SVG | |
| function createDropdownButtonSvg() { | |
| const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); | |
| svg.setAttribute("width", "18"); | |
| svg.setAttribute("height", "18"); | |
| svg.setAttribute("viewBox", "0 0 24 24"); | |
| svg.setAttribute("fill", "none"); | |
| svg.setAttribute("stroke", "currentColor"); | |
| svg.setAttribute("stroke-width", STYLES.dropdownButton.default.strokeWidth); | |
| svg.setAttribute("stroke-linecap", "round"); | |
| svg.setAttribute("stroke-linejoin", "round"); | |
| svg.style.transition = "transform 0.2s ease, stroke-width 0.2s ease"; | |
| const polyline = document.createElementNS( | |
| "http://www.w3.org/2000/svg", | |
| "polyline" | |
| ); | |
| polyline.setAttribute("points", "6 9 12 15 18 9"); | |
| svg.appendChild(polyline); | |
| return svg; | |
| } | |
| // Function to create and style a dropdown link | |
| function createDropdownLink(domain, tag, productId) { | |
| const dropdownLink = document.createElement("a"); | |
| const localizedLink = getLocalizedAmazonLink(domain, tag, productId); | |
| dropdownLink.href = localizedLink; | |
| dropdownLink.textContent = domain; | |
| dropdownLink.dataset.domain = domain; | |
| dropdownLink.target = "_blank"; | |
| dropdownLink.rel = "noopener noreferrer"; | |
| Object.assign(dropdownLink.style, STYLES.dropdownLink); | |
| return dropdownLink; | |
| } | |
| // Function to handle search input filtering | |
| function handleSearchInput(searchInput, dropdownLinks) { | |
| searchInput.addEventListener("input", (e) => { | |
| const searchTerm = e.target.value.toLowerCase(); | |
| dropdownLinks.forEach((link) => { | |
| const domain = link.dataset.domain.toLowerCase(); | |
| link.style.display = domain.includes(searchTerm) ? "block" : "none"; | |
| }); | |
| }); | |
| } | |
| // Function to handle dropdown button state changes | |
| function handleDropdownButtonState(dropdownButton, svg, state) { | |
| const styles = STYLES.dropdownButton[state]; | |
| dropdownButton.style.color = styles.color; | |
| svg.style.strokeWidth = styles.strokeWidth; | |
| } | |
| // Function to reset dropdown button state | |
| function resetDropdownButtonState(dropdownButton, svg) { | |
| handleDropdownButtonState(dropdownButton, svg, "default"); | |
| dropdownButton.classList.remove("active"); | |
| svg.style.transform = "rotate(0deg)"; | |
| } | |
| // Function to close and reset the dropdown state | |
| function closeAndResetDropdown( | |
| dropdownContent, | |
| searchInput, | |
| dropdownLinks, | |
| dropdownButton, | |
| svg | |
| ) { | |
| dropdownContent.style.display = "none"; | |
| searchInput.value = ""; | |
| dropdownLinks.forEach((link) => (link.style.display = "block")); | |
| resetDropdownButtonState(dropdownButton, svg); | |
| } | |
| // Function to create and style the dropdown button container | |
| function createDropdownButtonContainer() { | |
| const dropdownButton = document.createElement("span"); | |
| const svg = createDropdownButtonSvg(); | |
| dropdownButton.style.padding = "10px 10px 10px 5px"; | |
| dropdownButton.style.position = "absolute"; | |
| dropdownButton.style.top = "9px"; | |
| dropdownButton.style.right = "-2px"; | |
| dropdownButton.style.cursor = "pointer"; | |
| dropdownButton.style.userSelect = "none"; | |
| dropdownButton.style.display = "inline-flex"; | |
| dropdownButton.style.alignItems = "center"; | |
| dropdownButton.style.transition = "all 0.2s ease"; | |
| dropdownButton.style.color = STYLES.dropdownButton.default.color; | |
| dropdownButton.appendChild(svg); | |
| return { dropdownButton, svg }; | |
| } | |
| // Main function to add dropdown menu | |
| function addDropdownMenu(button) { | |
| button.style.columnGap = "0"; | |
| const parent = button.parentElement; | |
| const { dropdownButton, svg } = createDropdownButtonContainer(); | |
| // Create dropdown container | |
| const dropdownContent = document.createElement("div"); | |
| dropdownContent.classList.add("amazon-dropdown-links"); | |
| dropdownContent.style.display = "none"; | |
| dropdownContent.style.position = "absolute"; | |
| Object.assign(dropdownContent.style, STYLES.dropdown); | |
| const searchInputStyle = Object.entries(STYLES.searchInput) | |
| .map( | |
| ([key, value]) => | |
| `${key.replace(/([A-Z])/g, "-$1").toLowerCase()}:${value}` | |
| ) | |
| .join(";"); | |
| dropdownContent.innerHTML = ` | |
| <input type="text" placeholder="Search Amazon sites..." style="${searchInputStyle}"> | |
| <div class="links-container" style="max-height: 300px; overflow-y: auto;"></div> | |
| `; | |
| const searchInput = dropdownContent.querySelector("input"); | |
| const linksContainer = dropdownContent.querySelector(".links-container"); | |
| // Get product ID and create dropdown links | |
| const productId = getAmazonProductId(button.href); | |
| const dropdownLinks = Object.entries(amazonWebsites).map( | |
| ([domain, tag]) => { | |
| const link = createDropdownLink(domain, tag, productId); | |
| linksContainer.appendChild(link); | |
| return link; | |
| } | |
| ); | |
| // Set up search functionality | |
| handleSearchInput(searchInput, dropdownLinks); | |
| // Add event listeners for dropdown button | |
| dropdownButton.addEventListener("mouseenter", () => | |
| handleDropdownButtonState(dropdownButton, svg, "hover") | |
| ); | |
| dropdownButton.addEventListener("mouseleave", () => { | |
| if (!dropdownButton.classList.contains("active")) { | |
| handleDropdownButtonState(dropdownButton, svg, "default"); | |
| } | |
| }); | |
| dropdownButton.addEventListener("mousedown", () => { | |
| handleDropdownButtonState(dropdownButton, svg, "active"); | |
| dropdownButton.classList.add("active"); | |
| }); | |
| dropdownButton.addEventListener("mouseup", () => { | |
| if (dropdownButton.classList.contains("active")) { | |
| handleDropdownButtonState(dropdownButton, svg, "hover"); | |
| } | |
| }); | |
| // Toggle dropdown on dropdown button click | |
| dropdownButton.addEventListener("click", (event) => { | |
| event.preventDefault(); | |
| event.stopPropagation(); | |
| const isVisible = dropdownContent.style.display === "block"; | |
| if (isVisible) { | |
| closeAndResetDropdown( | |
| dropdownContent, | |
| searchInput, | |
| dropdownLinks, | |
| dropdownButton, | |
| svg | |
| ); | |
| } else { | |
| // Show dropdown | |
| dropdownContent.style.display = "block"; | |
| searchInput.focus(); | |
| handleDropdownButtonState(dropdownButton, svg, "hover"); | |
| dropdownButton.classList.add("active"); | |
| svg.style.transform = "rotate(180deg)"; | |
| } | |
| }); | |
| // Close dropdown when clicking outside | |
| document.addEventListener("click", (event) => { | |
| if ( | |
| dropdownContent.style.display === "block" && | |
| !parent.contains(event.target) | |
| ) { | |
| closeAndResetDropdown( | |
| dropdownContent, | |
| searchInput, | |
| dropdownLinks, | |
| dropdownButton, | |
| svg | |
| ); | |
| } | |
| }); | |
| // Position dropdown | |
| const buttonRect = button.getBoundingClientRect(); | |
| const parentRect = parent.getBoundingClientRect(); | |
| dropdownContent.style.top = `${buttonRect.bottom - parentRect.top}px`; | |
| dropdownContent.style.left = `${buttonRect.left - parentRect.left}px`; | |
| // Append elements to DOM | |
| parent.style.position = "relative"; | |
| button.appendChild(dropdownButton); | |
| parent.appendChild(dropdownContent); | |
| } | |
| // Initialize dropdowns for all Amazon links | |
| async function initializeAmazonDropdowns() { | |
| const userCountry = await getUserCountry(); | |
| if (!userCountry) { | |
| console.warn("Could not detect country"); | |
| return; | |
| } | |
| const amazonDomain = getAmazonDomainForCountry(userCountry); | |
| const amazonTag = amazonWebsites[amazonDomain]; | |
| const mainContainer = document.querySelector("#main"); | |
| if (!mainContainer) { | |
| console.warn('No element with id "main" found'); | |
| return; | |
| } | |
| const amazonLinks = mainContainer.querySelectorAll( | |
| "a[href^='https://www.amazon.com']" | |
| ); | |
| if (amazonLinks.length === 0) { | |
| console.warn("No Amazon links found to process."); | |
| return; | |
| } | |
| amazonLinks.forEach((link) => { | |
| const productId = getAmazonProductId(link.href); | |
| if (productId) { | |
| const localizedLink = getLocalizedAmazonLink( | |
| amazonDomain, | |
| amazonTag, | |
| productId | |
| ); | |
| link.href = localizedLink; | |
| addDropdownMenu(link); | |
| } | |
| }); | |
| } | |
| // Function to apply custom styles | |
| function applyCustomStylesToAmazonButtons() { | |
| const gbButtons = document.querySelectorAll(".gb-button"); | |
| gbButtons.forEach((button) => { | |
| button.style.paddingRight = "30px"; | |
| }); | |
| } | |
| // Start the initialization | |
| applyCustomStylesToAmazonButtons(); | |
| initializeAmazonDropdowns(); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment