// ==UserScript==
// @name Beerizer Systembolaget export
// @namespace https://github.com/Row/beerizer-export-systembolaget
// @version 0.8
// @description Adds an Systembolaget export button to the top of the Beerizer.com cart.
// The export result can be verifed in the Systembolaget.se cart.
// @author Row
// @match https://beerizer.com/*
// @match https://www.systembolaget.se/*
// @grant GM.setValue
// @grant GM.getValue
// @run-at document-body
// ==/UserScript==
const STATE_KEY = 'STATE_KEY';
const STATE_UNDEF = null;
const STATE_INIT = 'INIT';
const STATE_PENDING = 'PENDING';
const STATE_DONE = 'DONE';
const STATE_ERROR = 'ERROR';
const STATE_CANCEL = 'CANCEL';
const INITIAL_STATE = {
state: STATE_UNDEF,
index: 0,
beers: [],
};
const PROGRESS_ID = 'beerizer-progress';
const makeTag = tag => parent => parent.appendChild(document.createElement(tag));
const a = makeTag('a');
const button = makeTag('button');
const div = makeTag('div');
const table = makeTag('table');
const td = makeTag('td');
const tr = makeTag('tr');
const aLink = (parent, { href, title }) => {
let el;
if (href) {
el = a(parent);
el.setAttribute('href', href);
} else {
el = parent;
}
el.innerText = title || 'Unknown';
return el;
};
const tdr = parent => {
const t = td(parent);
t.style.padding = '0.3em';
return t;
};
const getElementByXpath = (xpath) =>
document.evaluate(
xpath,
document,
null,
XPathResult.FIRST_ORDERED_NODE_TYPE,
null,
).singleNodeValue;
const waitForElement = (xpath, timeout = 5000, interval = 100, shouldInverse = false) => {
const start = (new Date()).getTime();
return new Promise((resolve, reject) => {
const tryElement = () => {
const element = getElementByXpath(xpath);
if ((!!element) !== shouldInverse) {
resolve(element);
return;
}
if (((new Date()).getTime() - start) > timeout) {
reject(xpath);
}
window.setTimeout(tryElement, interval);
};
tryElement();
});
};
// Systembolaget related
const URL_START = 'https://www.systembolaget.se';
const URL_CART = 'https://www.systembolaget.se/varukorg';
const XPATH_CART_INS = '//h1[./span[text()="Varukorg"] or text()="Varukorg"]';
const XPATH_CART = `//div[
text()="Varukorgen är tom."
or (starts-with(text(), "Du har ") and contains(text(), "varor i korgen"))]`;
const XPATH_CONFIRM_AGE = '//button[text()="Jag har fyllt 20 år"]';
const XPATH_CONFIRM_COOKIE = '//button[text()="Slå på och acceptera alla kakor"]';
const XPATH_ADD_TO_CART_BTN = '//button[text()="Lägg i varukorg"]';
const XPATH_VERIFY_ADD = '//button[text()="Tillagd"]';
const XPATH_MODAL = '//button[@id="initialTgmFocus"]';
const XPATH_BEER_TITLE = '//h1[./p]';
const XPATH_SHIP_METHOD = '//div[text()="Välj leveranssätt "]';
const cancelExport = async (state) => {
const { index, beers } = state;
for (let i = index; i < beers.length; i += 1) {
beers[i].state = STATE_CANCEL;
beers[i].error = 'cancelled';
}
doneSystemBolaget({ ...state, index: beers.length - 1 });
};
const renderProgress = (state) => {
const overlay = div(document.body);
overlay.id = PROGRESS_ID;
overlay.style.cssText = `
align-items: center;
background: #FFF;
display: flex;
flex-flow: column;
height: 100vh;
justify-content: center;
left: 0;
position: fixed;
top: 0;
transition: height 0.3s;
width: 100vw;
z-index: 1337;
`;
const done = state.beers.filter(({ state }) => state !== STATE_INIT).length;
const total = state.beers.length;
const percent = (done / total) * 100;
const bar = div(overlay);
bar.style.cssText = `
margin: 0 20em;
background: lightgrey;
`;
const progress = div(bar);
progress.style.cssText = `
background: #fbd533;
color: #fff;
overflow: visible;
padding: 1em;
text-align: right;
text-shadow: rgb(95 92 92) 1px 1px 2px;
white-space: nowrap;
width: ${percent}%;
`;
progress.innerText = `EXPORTING BEER ${done} OF ${total}`;
const cancelButton = button(overlay);
cancelButton.innerText = 'Cancel export';
cancelButton.style.cssText = `
background: white;
border: 1px solid red;
color: red;
cursor: pointer;
font-size: 0.7rem;
margin-top: 1rem;
`;
cancelButton.addEventListener('click', () => cancelExport(state));
};
const renderResult = async (state) => {
const div = document.createElement('div');
await waitForElement(XPATH_CART);
const insertElement = await waitForElement(XPATH_CART_INS);
insertElement.after(div);
div.innerHTML = `<h2>Beerizer exported ${state.beers.length} beers</h2>`;
const exportTable = table(div);
state.beers.map(({
beerizerHref,
beerizerTitle,
error,
state,
systemBolagetHref,
systemBolagetTitle,
}, index) => {
const row = tr(exportTable);
tdr(row).innerText = index + 1;
aLink(tdr(row), {
href: beerizerHref,
title: beerizerTitle,
});
tdr(row).innerText = '➜';
aLink(tdr(row), {
href: systemBolagetHref,
title: systemBolagetTitle,
});
tdr(row).innerText = state === STATE_DONE ? '✅' : '⚠️';
tdr(row).innerText = error ? error : state;
});
};
const doneSystemBolaget = async (state) => {
GM.setValue(STATE_KEY, { ...state, state: STATE_DONE });
window.location.href = URL_CART;
};
const initSystemBolaget = async (state) => {
if (state.beers.length === 0) {
await doneSystemBolaget(state);
} else {
try {
const btn = await waitForElement(XPATH_CONFIRM_AGE, 2000);
btn.click();
} catch (e) {
console.log('tried to accept age');
}
try {
const btn = await waitForElement(XPATH_CONFIRM_COOKIE, 2000);
btn.click();
} catch (e) {
console.log('tried to accept cookie');
}
await GM.setValue(STATE_KEY, { ...state, state: STATE_PENDING });
window.location.href = state.beers[0].href;
}
};
const addBeerSystembolaget = async (state) => {
const { index, beers } = state;
const beer = state.beers[index];
beer.systemBolagetHref = window.location.href;
try {
const beerHeader = await waitForElement(XPATH_BEER_TITLE);
if (!beerHeader) {
throw Error('Beer not found?');
}
beer.systemBolagetTitle = beerHeader.innerText;
const cartBtn = await waitForElement(XPATH_ADD_TO_CART_BTN);
cartBtn.click();
try {
await waitForElement(XPATH_VERIFY_ADD, 2000, 100);
} catch (e) {
if (!getElementByXpath(XPATH_SHIP_METHOD)) throw e;
const progress = document.getElementById(PROGRESS_ID);
progress.style.height = '100px';
await waitForElement(XPATH_MODAL, 1000 * 120, 100, true);
const cartBtn = await waitForElement(XPATH_ADD_TO_CART_BTN);
cartBtn.click();
await waitForElement(XPATH_VERIFY_ADD, 2000, 100);
progress.style.height = '100vh';
}
beer.state = STATE_DONE;
} catch (error) {
beer.state = STATE_ERROR;
beer.error = error.message;
}
const nextIndex = index + 1;
if (beers.length <= nextIndex) {
await doneSystemBolaget(state);
} else {
await GM.setValue(STATE_KEY, { ...state, index: nextIndex });
window.location.href = beers[nextIndex].href;
}
};
const handleSystembolaget = async () => {
const state = await GM.getValue(STATE_KEY, INITIAL_STATE);
if (state.beers.length > 0 && state.state !== STATE_DONE) {
renderProgress(state);
}
if (state.state === STATE_INIT) {
await initSystemBolaget(state);
} else if (state.state === STATE_PENDING) {
window.addEventListener('load', () => {
addBeerSystembolaget(state);
});
}
if (window.location.pathname === '/varukorg/') {
renderResult(state);
}
};
// Beerizer parts
const XPATH_CART_MENU_BUTTON = '//a[@class="cart-link" and ./span[text()="Share"]]';
const SELECT_OPEN_CART_BUTTON = 'div.cart-wrapper.collapsed>div.summary';
const SELECT_SB_REF_LINKS = 'a[title="To Systembolaget"]';
const SELECT_TITLE = '.CartProductTable td.name>a';
const exportCart = async () => {
const titles = [...document.querySelectorAll(SELECT_TITLE)].map(l => ({
beerizerHref: l.href,
beerizerTitle: l.innerText,
}));
const hrefs = [...document.querySelectorAll(SELECT_SB_REF_LINKS)].map(l => l.href);
const beers = [...new Set(hrefs)].map((href, i) => ({
...titles[i],
href,
state: STATE_INIT,
}));
const state = {
...INITIAL_STATE,
state: STATE_INIT,
beers,
};
await GM.setValue(STATE_KEY, state);
const w = window.open('', 'systembolaget');
w.location = URL_START;
};
const renderButton = () => {
const cl = getElementByXpath(XPATH_CART_MENU_BUTTON);
if (!cl) return;
const e = cl.cloneNode(2);
cl.after(e);
e.querySelector('span').innerText = 'Export Systembolaget';
e.addEventListener('click', exportCart);
};
const handleBeerizer = () => {
const btnEl = document.querySelector(SELECT_OPEN_CART_BUTTON);
btnEl.addEventListener('click', () => {
window.setTimeout(renderButton, 100);
});
};
// initialize
(() => {
'use strict';
const hostname = window.location.hostname;
if (hostname.includes('beerizer')) {
window.addEventListener('load', () => {
handleBeerizer();
});
}
if (hostname.includes('systembolaget')) {
handleSystembolaget();
}
})();