added: progress, live price updates, do not check already bought books

This commit is contained in:
Gregory Tertyshny 2024-07-17 18:11:08 +03:00
parent 94b688843c
commit d7676019d0
8 changed files with 140 additions and 76 deletions

View file

@ -2,7 +2,7 @@ const timeout = (duration) => new Promise((r) => setTimeout(r, duration));
const observePriceOnPage = (page) => const observePriceOnPage = (page) =>
new Promise((res, rej) => { new Promise((res, rej) => {
timeout(10000).then(() => rej("price not found")); timeout(5000).then(() => rej("price not found"));
var observer = new MutationObserver(() => { var observer = new MutationObserver(() => {
const price = page?.querySelector( const price = page?.querySelector(

View file

@ -1,17 +1,13 @@
import "./logger"; import "./logger";
import { COUNTRIES } from "./countries";
import { initCache } from "./cache"; import { initCache } from "./cache";
import { bookPriceFor } from "./bookPriceFor"; import { createElement, h, render } from "preact";
import { usePrices } from "./usePrices";
import { useCallback, useState } from "preact/hooks";
/* /*
TODO: TODO:
- do not run for books you already bought
- more durable source for rates - more durable source for rates
- React for UI
- More informative UI, show loading progress
- clear stale cache
- readme how to use and debug - readme how to use and debug
- configuration (purge cache, change base currency)
*/ */
const createPricesContainer = () => { const createPricesContainer = () => {
@ -28,79 +24,101 @@ const createPricesContainer = () => {
} }
} }
const container = document.createElement("div"); return visible.parentNode.insertBefore(
document.createElement("div"),
container.style.display = "flex"; visible,
container.style.flexDirection = "column"; );
container.style.border = "1px solid black";
container.style.padding = "10px";
return visible.parentNode.insertBefore(container, visible);
}; };
const getPrices = async () => { const isInLibrary = () => document.querySelectorAll(".read-now").length;
const prices = [];
for (const country of COUNTRIES) { const formatPrice = (countryPrice, convertedPrice) => {
// intentionally blocking execution if (countryPrice == undefined) {
// to resolve sequentially. return `LOADING`;
// It should prevent DOS and triggering captcha
prices.push(await bookPriceFor(country));
} }
return prices; if (convertedPrice) {
return `${countryPrice} => ${convertedPrice?.formatted}`;
}
return `NOT FOUND`;
}; };
const sortPrices = (prices) => const Price = ({ props }) => {
prices.sort( const { convertedPrice, countryCode, countryPrice, url } = props;
(a, b) => const [isHovered, setHover] = useState(false);
(a?.convertedPrice?.intValue || Infinity) -
(b?.convertedPrice?.intValue || Infinity), const hover = useCallback(() => setHover(true));
const leave = useCallback(() => setHover(false));
const style = {
display: "flex",
flexDirection: "row",
justifyContent: "space-between",
backgroundColor: isHovered ? "#d7d7d7" : "",
padding: "10px 10px 0 10px",
};
return h(
"a",
{
style,
href: url,
target: "_blank",
onMouseEnter: hover,
onMouseLeave: leave,
},
[
h("p", null, countryCode.toUpperCase()),
h(
"p",
{ style: { fontWeight: "bold" } },
formatPrice(countryPrice, convertedPrice),
),
],
);
};
const InLibrary = () =>
h(
"div",
{
style: {
display: "flex",
flexDirection: "column",
border: "1px solid black",
textAlign: "center",
},
},
h("h2", null, "Already in Library!"),
); );
const showPrices = (container, prices) => { const Percent = (percent) => {
container.innerText = null; const style = { padding: "10px 10px 0 10px", textAlign: "center" };
prices.forEach((price) => {
const link = document.createElement("a"); if (percent == 100) return h("h2", { style }, "all prices are loaded!");
link.href = price.url;
link.target = "_blank"; return h("h2", { style }, `${percent}%`);
link.style.marginBottom = "5px";
link.style.display = "flex";
link.style.justifyContent = "space-between";
const oldPrice = document.createElement("p");
oldPrice.innerText = `${price.countryCode.toUpperCase()}: ${price?.countryPrice || "NO PRICE"}`;
const newPrice = document.createElement("p");
newPrice.innerText = price?.convertedPrice?.formatted ?? "NO PRICE";
newPrice.style.fontWeight = "bold";
link.appendChild(oldPrice);
link.appendChild(newPrice);
container.appendChild(link);
});
}; };
async function main() { const App = () => {
const container = createPricesContainer(); const [prices, percentChecked] = usePrices();
container.innerText = "LOADING PRICES..."; const style = {
display: "flex",
flexDirection: "column",
border: "1px solid black",
};
try { if (isInLibrary()) {
initCache(); return InLibrary();
const countriesPrice = await getPrices();
l("country prices", countriesPrice);
sortPrices(countriesPrice);
l("sorted prices", countriesPrice);
showPrices(container, countriesPrice);
} catch (e) {
l("error", e);
container.innerText = "FAILED TO LOAD PRICES. CHECK CONSOLE FOR MORE INFO";
} finally {
l("done");
} }
}
return h("div", { style }, [
Percent(percentChecked),
...prices.map((price) => h(Price, { props: price })),
]);
};
l("starting..."); l("starting...");
void main(); initCache();
render(createElement(App), createPricesContainer());

35
lib/usePrices.js Normal file
View file

@ -0,0 +1,35 @@
import { useEffect, useState } from "preact/hooks";
import { COUNTRIES } from "./countries";
import { bookPriceFor } from "./bookPriceFor";
const sortPrices = (prices) =>
prices.sort(
(a, b) =>
(a?.convertedPrice?.intValue || Infinity) -
(b?.convertedPrice?.intValue || Infinity),
);
export const usePrices = () => {
const [prices, setPrices] = useState(COUNTRIES);
useEffect(() => {
const fetchAll = async () => {
for (const [index, country] of COUNTRIES.entries()) {
// intentionally blocking execution
// to resolve sequentially.
// It should prevent DOS and triggering captcha
const found = await bookPriceFor(country);
prices[index] = found;
sortPrices(prices);
setPrices([...prices]);
}
l("DONE");
};
fetchAll();
}, []);
const percentChecked =
(prices.filter((p) => p.countryPrice != undefined).length / prices.length) *
100;
return [prices, percentChecked.toFixed(0)];
};

View file

@ -1,7 +1,7 @@
{ {
"name": "Kobo Price", "name": "Kobo Price",
"description": "Find lowest book price on kobo.com", "description": "Find lowest book price on kobo.com",
"version": "1.1.9", "version": "1.2.0",
"manifest_version": 3, "manifest_version": 3,
"content_scripts": [ "content_scripts": [
{ {

12
package-lock.json generated
View file

@ -10,7 +10,8 @@
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"currency.js": "^2.0.4", "currency.js": "^2.0.4",
"date-fns": "^3.6.0" "date-fns": "^3.6.0",
"preact": "^10.22.1"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.6.0", "@eslint/js": "^9.6.0",
@ -4868,6 +4869,15 @@
"integrity": "sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA==", "integrity": "sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA==",
"dev": true "dev": true
}, },
"node_modules/preact": {
"version": "10.22.1",
"resolved": "https://registry.npmjs.org/preact/-/preact-10.22.1.tgz",
"integrity": "sha512-jRYbDDgMpIb5LHq3hkI0bbl+l/TQ9UnkdQ0ww+lp+4MMOdqaUYdFc5qeyP+IV8FAd/2Em7drVPeKdQxsiWCf/A==",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/preact"
}
},
"node_modules/prelude-ls": { "node_modules/prelude-ls": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",

View file

@ -30,6 +30,7 @@
}, },
"dependencies": { "dependencies": {
"currency.js": "^2.0.4", "currency.js": "^2.0.4",
"date-fns": "^3.6.0" "date-fns": "^3.6.0",
"preact": "^10.22.1"
} }
} }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long