Compare commits
No commits in common. "c916ab1793c5b9a1d79db8bc1140ebc02bc2d15e" and "982800821b2cdfc7ebade9c2ad75619d199abf6a" have entirely different histories.
c916ab1793
...
982800821b
4 changed files with 205 additions and 273 deletions
|
|
@ -1,49 +1,49 @@
|
||||||
export const COUNTRIES = [
|
export const COUNTRIES = [
|
||||||
{ countryCode: "ww", countryName: "Worldwide", currencyCode: "usd" },
|
{ countryCode: "ww", currencyCode: "usd" },
|
||||||
{ countryCode: "ca", countryName: "Canada", currencyCode: "cad" },
|
{ countryCode: "ca", currencyCode: "cad" },
|
||||||
{ countryCode: "us", countryName: "United States", currencyCode: "usd" },
|
{ countryCode: "us", currencyCode: "usd" },
|
||||||
{ countryCode: "in", countryName: "India", currencyCode: "inr" },
|
{ countryCode: "in", currencyCode: "inr" },
|
||||||
{ countryCode: "za", countryName: "South Africa", currencyCode: "zar" },
|
{ countryCode: "za", currencyCode: "zar" },
|
||||||
{ countryCode: "au", countryName: "Australia", currencyCode: "aud" },
|
{ countryCode: "au", currencyCode: "aud" },
|
||||||
{ countryCode: "hk", countryName: "Hong Kong", currencyCode: "hkd" },
|
{ countryCode: "hk", currencyCode: "hkd" },
|
||||||
{ countryCode: "jp", countryName: "Japan", currencyCode: "jpy" },
|
{ countryCode: "jp", currencyCode: "jpy" },
|
||||||
{ countryCode: "my", countryName: "Malaysia", currencyCode: "myr" },
|
{ countryCode: "my", currencyCode: "myr" },
|
||||||
{ countryCode: "nz", countryName: "New Zealand", currencyCode: "nzd" },
|
{ countryCode: "nz", currencyCode: "nzd" },
|
||||||
{ countryCode: "ph", countryName: "Philippines", currencyCode: "php" },
|
{ countryCode: "ph", currencyCode: "php" },
|
||||||
{ countryCode: "sg", countryName: "Singapore", currencyCode: "sgd" },
|
{ countryCode: "sg", currencyCode: "sgd" },
|
||||||
{ countryCode: "tw", countryName: "Taiwan", currencyCode: "twd" },
|
{ countryCode: "tw", currencyCode: "twd" },
|
||||||
{ countryCode: "th", countryName: "Thailand", currencyCode: "usd" },
|
{ countryCode: "th", currencyCode: "usd" },
|
||||||
{ countryCode: "at", countryName: "Austria", currencyCode: "eur" },
|
{ countryCode: "at", currencyCode: "eur" },
|
||||||
{ countryCode: "be", countryName: "Belgium", currencyCode: "eur" },
|
{ countryCode: "be", currencyCode: "eur" },
|
||||||
{ countryCode: "cy", countryName: "Cyprus", currencyCode: "eur" },
|
{ countryCode: "cy", currencyCode: "eur" },
|
||||||
{ countryCode: "cz", countryName: "Czech Republic", currencyCode: "czk" },
|
{ countryCode: "cz", currencyCode: "czk" },
|
||||||
{ countryCode: "dk", countryName: "Denmark", currencyCode: "dkk" },
|
{ countryCode: "dk", currencyCode: "dkk" },
|
||||||
{ countryCode: "ee", countryName: "Estonia", currencyCode: "eur" },
|
{ countryCode: "ee", currencyCode: "eur" },
|
||||||
{ countryCode: "fi", countryName: "Finland", currencyCode: "eur" },
|
{ countryCode: "fi", currencyCode: "eur" },
|
||||||
{ countryCode: "fr", countryName: "France", currencyCode: "eur" },
|
{ countryCode: "fr", currencyCode: "eur" },
|
||||||
{ countryCode: "de", countryName: "Germany", currencyCode: "eur" },
|
{ countryCode: "de", currencyCode: "eur" },
|
||||||
{ countryCode: "gr", countryName: "Greece", currencyCode: "eur" },
|
{ countryCode: "gr", currencyCode: "eur" },
|
||||||
{ countryCode: "ie", countryName: "Ireland", currencyCode: "eur" },
|
{ countryCode: "ie", currencyCode: "eur" },
|
||||||
{ countryCode: "it", countryName: "Italy", currencyCode: "eur" },
|
{ countryCode: "it", currencyCode: "eur" },
|
||||||
{ countryCode: "lt", countryName: "Lithuania", currencyCode: "eur" },
|
{ countryCode: "lt", currencyCode: "eur" },
|
||||||
{ countryCode: "lu", countryName: "Luxembourg", currencyCode: "eur" },
|
{ countryCode: "lu", currencyCode: "eur" },
|
||||||
{ countryCode: "mt", countryName: "Malta", currencyCode: "eur" },
|
{ countryCode: "mt", currencyCode: "eur" },
|
||||||
{ countryCode: "nl", countryName: "Netherlands", currencyCode: "eur" },
|
{ countryCode: "nl", currencyCode: "eur" },
|
||||||
{ countryCode: "no", countryName: "Norway", currencyCode: "nok" },
|
{ countryCode: "no", currencyCode: "nok" },
|
||||||
{ countryCode: "pl", countryName: "Poland", currencyCode: "pln" },
|
{ countryCode: "pl", currencyCode: "pln" },
|
||||||
{ countryCode: "pt", countryName: "Portugal", currencyCode: "eur" },
|
{ countryCode: "pt", currencyCode: "eur" },
|
||||||
{ countryCode: "ro", countryName: "Romania", currencyCode: "ron" },
|
{ countryCode: "ro", currencyCode: "ron" },
|
||||||
{ countryCode: "sk", countryName: "Slovakia", currencyCode: "eur" },
|
{ countryCode: "sk", currencyCode: "eur" },
|
||||||
{ countryCode: "si", countryName: "Slovenia", currencyCode: "eur" },
|
{ countryCode: "si", currencyCode: "eur" },
|
||||||
{ countryCode: "es", countryName: "Spain", currencyCode: "eur" },
|
{ countryCode: "es", currencyCode: "eur" },
|
||||||
{ countryCode: "se", countryName: "Sweden", currencyCode: "sek" },
|
{ countryCode: "se", currencyCode: "sek" },
|
||||||
{ countryCode: "ch", countryName: "Switzerland", currencyCode: "chf" },
|
{ countryCode: "ch", currencyCode: "chf" },
|
||||||
{ countryCode: "tr", countryName: "Turkey", currencyCode: "try" },
|
{ countryCode: "tr", currencyCode: "try" },
|
||||||
{ countryCode: "gb", countryName: "United Kingdom", currencyCode: "gbp" },
|
{ countryCode: "gb", currencyCode: "gbp" },
|
||||||
{ countryCode: "ar", countryName: "Argentina", currencyCode: "usd" },
|
{ countryCode: "ar", currencyCode: "usd" },
|
||||||
{ countryCode: "br", countryName: "Brazil", currencyCode: "brl" },
|
{ countryCode: "br", currencyCode: "brl" },
|
||||||
{ countryCode: "cl", countryName: "Chile", currencyCode: "clp" },
|
{ countryCode: "cl", currencyCode: "clp" },
|
||||||
{ countryCode: "co", countryName: "Colombia", currencyCode: "cop" },
|
{ countryCode: "co", currencyCode: "cop" },
|
||||||
{ countryCode: "mx", countryName: "Mexico", currencyCode: "mxn" },
|
{ countryCode: "mx", currencyCode: "mxn" },
|
||||||
{ countryCode: "pe", countryName: "Peru", currencyCode: "pen" },
|
{ countryCode: "pe", currencyCode: "pen" },
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -1,37 +1,13 @@
|
||||||
const timeout = (duration) => new Promise((r) => setTimeout(r, duration));
|
const timeout = (duration) => new Promise((r) => setTimeout(r, duration));
|
||||||
|
|
||||||
// Selectors for both old and new Kobo layouts
|
|
||||||
const PRICE_SELECTORS = [
|
|
||||||
// Old layout (pre-2025 redesign)
|
|
||||||
".primary-right-container .pricing-details .active-price span",
|
|
||||||
// New layout (Tailwind, data-testid based)
|
|
||||||
'[data-testid="price"] .text-title-m-bold',
|
|
||||||
];
|
|
||||||
|
|
||||||
const findPrice = (page) => {
|
|
||||||
for (const sel of PRICE_SELECTORS) {
|
|
||||||
const el = page?.querySelector(sel);
|
|
||||||
if (el?.textContent?.trim()) {
|
|
||||||
return el.textContent.trim();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const observePriceOnPage = (page) =>
|
const observePriceOnPage = (page) =>
|
||||||
new Promise((res, rej) => {
|
new Promise((res, rej) => {
|
||||||
timeout(5000).then(() => rej("price not found"));
|
timeout(5000).then(() => rej("price not found"));
|
||||||
|
|
||||||
// Check immediately first
|
var observer = new MutationObserver(() => {
|
||||||
const immediate = findPrice(page);
|
const price = page?.querySelector(
|
||||||
if (immediate) {
|
".primary-right-container .pricing-details .active-price span",
|
||||||
l("found price (immediate)", immediate);
|
)?.textContent;
|
||||||
res(immediate);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const observer = new MutationObserver(() => {
|
|
||||||
const price = findPrice(page);
|
|
||||||
|
|
||||||
if (price) {
|
if (price) {
|
||||||
l("found price", price);
|
l("found price", price);
|
||||||
|
|
|
||||||
350
lib/index.js
350
lib/index.js
|
|
@ -12,241 +12,197 @@ TODO:
|
||||||
- readme how to use and debug
|
- readme how to use and debug
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const isInLibrary = () =>
|
const createPricesContainer = () => {
|
||||||
// Old layout
|
const pricingActionContainers = document.querySelectorAll(".pricing-details");
|
||||||
document.querySelectorAll(".read-now").length > 0 ||
|
|
||||||
// New layout
|
l("all pricing containers", pricingActionContainers);
|
||||||
document.querySelectorAll('[data-testid="buypad-readnow"]').length > 0;
|
|
||||||
|
let visible;
|
||||||
|
for (const node of pricingActionContainers) {
|
||||||
|
if (node.checkVisibility()) {
|
||||||
|
l("found visible pricing container", node);
|
||||||
|
visible = node;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return visible.parentNode.insertBefore(
|
||||||
|
document.createElement("div"),
|
||||||
|
visible,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isInLibrary = () => document.querySelectorAll(".read-now").length;
|
||||||
|
|
||||||
const formatPrice = (countryPrice, convertedPrice, isSelected) => {
|
const formatPrice = (countryPrice, convertedPrice, isSelected) => {
|
||||||
if (!isSelected) return "SKIP";
|
if (!isSelected) return "SKIP";
|
||||||
|
|
||||||
if (countryPrice == undefined) {
|
if (countryPrice == undefined) {
|
||||||
return "NOT LOADED";
|
return "NOT LOADED";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (convertedPrice) {
|
if (convertedPrice) {
|
||||||
return `${countryPrice} => ${convertedPrice?.formatted}`;
|
return `${countryPrice} => ${convertedPrice?.formatted}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return "NOT FOUND";
|
return "NOT FOUND";
|
||||||
};
|
};
|
||||||
|
|
||||||
const Price = ({ price, toggleCountry, isSelected }) => {
|
const Price = ({ price, toggleCountry, isSelected }) => {
|
||||||
const { convertedPrice, countryCode, countryName, countryPrice } = price;
|
const { convertedPrice, countryCode, countryPrice } = price;
|
||||||
const [isHovered, setHover] = useState(false);
|
const [isHovered, setHover] = useState(false);
|
||||||
|
|
||||||
const hover = useCallback(() => setHover(true));
|
const hover = useCallback(() => setHover(true));
|
||||||
const leave = useCallback(() => setHover(false));
|
const leave = useCallback(() => setHover(false));
|
||||||
|
|
||||||
const style = {
|
const style = {
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
backgroundColor: isHovered ? "#d7d7d7" : "",
|
backgroundColor: isHovered ? "#d7d7d7" : "",
|
||||||
padding: "4px 10px",
|
padding: "0 10px",
|
||||||
};
|
};
|
||||||
|
|
||||||
const checkbox = h("input", {
|
const link = h(
|
||||||
type: "checkbox",
|
"a",
|
||||||
checked: isSelected,
|
{
|
||||||
onChange: () => toggleCountry(),
|
style: { textDecoration: "underline" },
|
||||||
});
|
href: bookUrl(countryCode),
|
||||||
|
target: "_blank",
|
||||||
|
},
|
||||||
|
countryCode.toUpperCase(),
|
||||||
|
);
|
||||||
|
|
||||||
const link = h(
|
const checkbox = h("input", {
|
||||||
"a",
|
type: "checkbox",
|
||||||
{
|
checked: isSelected,
|
||||||
style: { textDecoration: "underline", marginLeft: "8px" },
|
onChange: () => toggleCountry(),
|
||||||
href: bookUrl(countryCode),
|
});
|
||||||
target: "_blank",
|
|
||||||
},
|
|
||||||
countryName || countryCode.toUpperCase(),
|
|
||||||
);
|
|
||||||
|
|
||||||
const priceLabel = h(
|
const priceLabel = h(
|
||||||
"p",
|
"p",
|
||||||
{ style: { fontWeight: "bold", marginLeft: "auto" } },
|
{ style: { fontWeight: "bold" } },
|
||||||
formatPrice(countryPrice, convertedPrice, isSelected),
|
formatPrice(countryPrice, convertedPrice, isSelected),
|
||||||
);
|
);
|
||||||
|
|
||||||
return h(
|
return h(
|
||||||
"label",
|
"label",
|
||||||
{ style, onMouseEnter: hover, onMouseLeave: leave },
|
{ style, onMouseEnter: hover, onMouseLeave: leave },
|
||||||
checkbox,
|
h("div", { style: { display: "flex" } }, checkbox, link),
|
||||||
link,
|
priceLabel,
|
||||||
priceLabel,
|
);
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const InLibrary = () =>
|
const InLibrary = () =>
|
||||||
h(
|
h(
|
||||||
"div",
|
"div",
|
||||||
{
|
{
|
||||||
style: {
|
style: {
|
||||||
padding: "10px",
|
display: "flex",
|
||||||
textAlign: "center",
|
flexDirection: "column",
|
||||||
},
|
border: "1px solid black",
|
||||||
},
|
textAlign: "center",
|
||||||
h("h2", null, "Already in Library!"),
|
},
|
||||||
);
|
},
|
||||||
|
h("h2", null, "Already in Library!"),
|
||||||
|
);
|
||||||
|
|
||||||
const Percent = (percent) => {
|
const Percent = (percent) => {
|
||||||
const spinner = useRef();
|
const spinner = useRef();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
spinner.current.animate(
|
spinner.current.animate(
|
||||||
[{ transform: "rotate(0deg)" }, { transform: "rotate(360deg)" }],
|
[{ transform: "rotate(0deg)" }, { transform: "rotate(360deg)" }],
|
||||||
{ iterations: Infinity, duration: 1000 },
|
{ iterations: Infinity, duration: 1000 },
|
||||||
);
|
);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const style = { padding: "10px 10px 0 10px", textAlign: "center" };
|
const style = { padding: "10px 10px 0 10px", textAlign: "center" };
|
||||||
|
|
||||||
if (percent == 100) return h("h2", { style }, "all prices are loaded!");
|
if (percent == 100) return h("h2", { style }, "all prices are loaded!");
|
||||||
|
|
||||||
return h(
|
return h(
|
||||||
"h2",
|
"h2",
|
||||||
{ style },
|
{ style },
|
||||||
`${percent}%`,
|
`${percent}%`,
|
||||||
h("span", {
|
h("span", {
|
||||||
ref: spinner,
|
ref: spinner,
|
||||||
style: {
|
style: {
|
||||||
width: "16px",
|
width: "16px",
|
||||||
height: "16px",
|
height: "16px",
|
||||||
borderRadius: "50%",
|
borderRadius: "50%",
|
||||||
display: "inline-block",
|
display: "inline-block",
|
||||||
borderTop: "3px solid black",
|
borderTop: "3px solid black",
|
||||||
borderRight: "3px solid transparent",
|
borderRight: "3px solid transparent",
|
||||||
boxSizing: "border-box",
|
boxSizing: "border-box",
|
||||||
marginLeft: "15px",
|
marginLeft: "15px",
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const Header = (state, percentChecked) => {
|
const Header = (state, percentChecked) => {
|
||||||
if (state === "loading") return Percent(percentChecked);
|
if (state === "loading") return Percent(percentChecked);
|
||||||
|
|
||||||
if (state === "done")
|
if (state === "done")
|
||||||
return h(
|
return h(
|
||||||
"h2",
|
"h2",
|
||||||
{ style: { textAlign: "center" } },
|
{ style: { textAlign: "center" } },
|
||||||
"All prices are loaded!",
|
"All prices are loaded!",
|
||||||
);
|
);
|
||||||
|
|
||||||
return h(
|
return h(
|
||||||
"h2",
|
"h2",
|
||||||
{ style: { textAlign: "center" } },
|
{ style: { textAlign: "center" } },
|
||||||
'Select countries or leave as is and press "Load prices"',
|
'Select countries or leave as is and press "load"',
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const Load = (state, load) => {
|
const Load = (state, load) => {
|
||||||
return h(
|
return h(
|
||||||
"button",
|
"button",
|
||||||
{
|
{
|
||||||
disabled: state === "loading",
|
disabled: state === "loading",
|
||||||
onClick: load,
|
onClick: load,
|
||||||
type: "button",
|
type: "button",
|
||||||
style: { backgroundColor: "#91ff91" },
|
style: { backgroundColor: "#91ff91" },
|
||||||
},
|
},
|
||||||
state === "loading" ? "Loading..." : "Load prices",
|
state === "loading" ? "Loading..." : "Load prices",
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
const { isSelected, toggleCountry, selected } = useSelectedCountries();
|
const { isSelected, toggleCountry, selected } = useSelectedCountries();
|
||||||
const { prices, percentChecked, state, load } = usePrices(selected);
|
const { prices, percentChecked, state, load } = usePrices(selected);
|
||||||
const [expanded, setExpanded] = useState(false);
|
|
||||||
|
|
||||||
l(selected);
|
l(selected);
|
||||||
|
const style = {
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
border: "1px solid black",
|
||||||
|
};
|
||||||
|
|
||||||
const toggle = useCallback(() => setExpanded((v) => !v));
|
if (isInLibrary()) {
|
||||||
|
return InLibrary();
|
||||||
|
}
|
||||||
|
|
||||||
if (isInLibrary()) {
|
return h(
|
||||||
return h(
|
"div",
|
||||||
"div",
|
{ style },
|
||||||
{
|
Header(state, percentChecked),
|
||||||
style: {
|
...prices.map((price) =>
|
||||||
position: "fixed",
|
h(Price, {
|
||||||
top: "10px",
|
price,
|
||||||
left: "10px",
|
toggleCountry: () => toggleCountry(price.countryCode),
|
||||||
zIndex: 99999,
|
isSelected: isSelected(price.countryCode),
|
||||||
background: "white",
|
isLoading: state,
|
||||||
border: "1px solid black",
|
}),
|
||||||
fontFamily: "sans-serif",
|
),
|
||||||
fontSize: "14px",
|
Load(state, load),
|
||||||
},
|
);
|
||||||
},
|
|
||||||
InLibrary(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const panelStyle = {
|
|
||||||
position: "fixed",
|
|
||||||
bottom: "0",
|
|
||||||
left: "0",
|
|
||||||
zIndex: 99999,
|
|
||||||
background: "white",
|
|
||||||
border: "1px solid black",
|
|
||||||
fontFamily: "sans-serif",
|
|
||||||
fontSize: "14px",
|
|
||||||
maxHeight: expanded ? "100vh" : "auto",
|
|
||||||
overflowY: expanded ? "auto" : "hidden",
|
|
||||||
minWidth: "100%",
|
|
||||||
};
|
|
||||||
|
|
||||||
const buttonStyle = {
|
|
||||||
height: "2.25rem",
|
|
||||||
background: "white",
|
|
||||||
border: "1px solid black",
|
|
||||||
// padding: "6px 12px",
|
|
||||||
cursor: "pointer",
|
|
||||||
fontSize: "14px",
|
|
||||||
fontWeight: "bold",
|
|
||||||
width: "100%",
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!expanded) {
|
|
||||||
return h(
|
|
||||||
"div",
|
|
||||||
{ style: panelStyle },
|
|
||||||
h("button", { style: buttonStyle, onClick: toggle }, "PRICES"),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const listStyle = {
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
};
|
|
||||||
|
|
||||||
return h(
|
|
||||||
"div",
|
|
||||||
{ style: panelStyle },
|
|
||||||
h("button", { style: buttonStyle, onClick: toggle }, "CLOSE"),
|
|
||||||
h(
|
|
||||||
"div",
|
|
||||||
{ style: listStyle },
|
|
||||||
...[
|
|
||||||
Header(state, percentChecked),
|
|
||||||
Load(state, load),
|
|
||||||
...prices.map((price) =>
|
|
||||||
h(Price, {
|
|
||||||
price,
|
|
||||||
toggleCountry: () => toggleCountry(price.countryCode),
|
|
||||||
isSelected: isSelected(price.countryCode),
|
|
||||||
isLoading: state,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
l("starting...");
|
l("starting...");
|
||||||
initCache();
|
initCache();
|
||||||
|
render(createElement(App), createPricesContainer());
|
||||||
const container = document.createElement("div");
|
|
||||||
document.body.appendChild(container);
|
|
||||||
render(createElement(App), container);
|
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
Loading…
Add table
Reference in a new issue