first deployment of kikomiko with url photos
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
8d98304137
commit
ae3ed7c7b7
Vendored
+180
@@ -0,0 +1,180 @@
|
||||
/**
|
||||
* Live EUR→toman pricing (TGJU free-market EUR).
|
||||
* Markups: <3€ +100%, 3–5€ +80%, >5€ +65%.
|
||||
*/
|
||||
|
||||
const FALLBACK_EUR_TOMAN = 302700;
|
||||
const EUR_DETECT_MAX = 500;
|
||||
const CACHE_KEY = "kikomiko-eur-toman-v1";
|
||||
const CACHE_TTL_MS = 5 * 60 * 1000;
|
||||
|
||||
let cachedRate = 0;
|
||||
let inflight = null;
|
||||
|
||||
export function markupForEuro(eur) {
|
||||
if (!Number.isFinite(eur) || eur <= 0) return 1;
|
||||
if (eur < 3) return 2;
|
||||
if (eur <= 5) return 1.8;
|
||||
return 1.65;
|
||||
}
|
||||
|
||||
function parseTgjuRial(raw) {
|
||||
const n = Number(String(raw ?? "").replace(/,/g, "").trim());
|
||||
return Number.isFinite(n) && n > 0 ? n : 0;
|
||||
}
|
||||
|
||||
function rialToToman(rial) {
|
||||
return rial > 0 ? rial / 10 : 0;
|
||||
}
|
||||
|
||||
function readBodyFallback() {
|
||||
const raw = Number(document.body?.dataset?.eurRate || "");
|
||||
return Number.isFinite(raw) && raw > 0 ? raw : FALLBACK_EUR_TOMAN;
|
||||
}
|
||||
|
||||
function readSessionCache() {
|
||||
try {
|
||||
const raw = sessionStorage.getItem(CACHE_KEY);
|
||||
if (!raw) return 0;
|
||||
const parsed = JSON.parse(raw);
|
||||
if (
|
||||
parsed &&
|
||||
Number(parsed.rate) > 0 &&
|
||||
Date.now() - Number(parsed.at || 0) < CACHE_TTL_MS
|
||||
) {
|
||||
return Number(parsed.rate);
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function writeSessionCache(rate) {
|
||||
try {
|
||||
sessionStorage.setItem(
|
||||
CACHE_KEY,
|
||||
JSON.stringify({ rate, at: Date.now() })
|
||||
);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
|
||||
function applyRateToDom(rate) {
|
||||
if (!(rate > 0) || !document.body) return;
|
||||
document.body.dataset.eurRate = String(Math.round(rate));
|
||||
document.querySelectorAll("[data-eur-rate-display]").forEach((el) => {
|
||||
el.textContent = new Intl.NumberFormat("fa-IR", {
|
||||
maximumFractionDigits: 0,
|
||||
}).format(Math.round(rate));
|
||||
});
|
||||
}
|
||||
|
||||
/** Prefer same-origin proxy (avoids CORS); fall back to TGJU ajax JSON. */
|
||||
async function fetchTgjuEurRial() {
|
||||
const endpoints = ["/api/fx/eur", "https://call1.tgju.org/ajax.json"];
|
||||
let lastErr;
|
||||
for (const url of endpoints) {
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
credentials: "omit",
|
||||
headers: { Accept: "application/json" },
|
||||
});
|
||||
if (!res.ok) throw new Error(`FX HTTP ${res.status}`);
|
||||
const data = await res.json();
|
||||
const row = data?.current?.price_eur || data?.price_eur;
|
||||
const rial = parseTgjuRial(row?.p ?? row);
|
||||
if (rial > 0) return rial;
|
||||
throw new Error("TGJU price_eur missing");
|
||||
} catch (err) {
|
||||
lastErr = err;
|
||||
}
|
||||
}
|
||||
throw lastErr || new Error("FX fetch failed");
|
||||
}
|
||||
|
||||
export function getEurToTomanRate() {
|
||||
if (cachedRate > 0) return cachedRate;
|
||||
const fromSession = readSessionCache();
|
||||
if (fromSession > 0) {
|
||||
cachedRate = fromSession;
|
||||
return cachedRate;
|
||||
}
|
||||
return readBodyFallback();
|
||||
}
|
||||
|
||||
export async function ensureEurRate() {
|
||||
if (cachedRate > 0) return cachedRate;
|
||||
const fromSession = readSessionCache();
|
||||
if (fromSession > 0) {
|
||||
cachedRate = fromSession;
|
||||
applyRateToDom(cachedRate);
|
||||
return cachedRate;
|
||||
}
|
||||
if (!inflight) {
|
||||
inflight = fetchTgjuEurRial()
|
||||
.then((rial) => {
|
||||
const toman = rialToToman(rial);
|
||||
cachedRate = toman > 0 ? toman : readBodyFallback();
|
||||
writeSessionCache(cachedRate);
|
||||
applyRateToDom(cachedRate);
|
||||
return cachedRate;
|
||||
})
|
||||
.catch(() => {
|
||||
cachedRate = readBodyFallback();
|
||||
applyRateToDom(cachedRate);
|
||||
return cachedRate;
|
||||
})
|
||||
.finally(() => {
|
||||
inflight = null;
|
||||
});
|
||||
}
|
||||
return inflight;
|
||||
}
|
||||
|
||||
export function euroBase(item) {
|
||||
if (!item) return null;
|
||||
for (const candidate of [item.priceEur, item.sourcePrice]) {
|
||||
const n = Number(candidate);
|
||||
if (Number.isFinite(n) && n > 0 && n < EUR_DETECT_MAX) return n;
|
||||
}
|
||||
const cur = String(item.sourceCurrency || "").toUpperCase();
|
||||
const price = Number(item.price);
|
||||
if (
|
||||
(cur === "EUR" || cur === "EU" || cur === "€") &&
|
||||
Number.isFinite(price) &&
|
||||
price > 0 &&
|
||||
price < EUR_DETECT_MAX
|
||||
) {
|
||||
return price;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function eurToTomanSell(eur, rate = getEurToTomanRate()) {
|
||||
const r = rate > 0 ? rate : FALLBACK_EUR_TOMAN;
|
||||
return Math.round(Number(eur) * r * markupForEuro(Number(eur)));
|
||||
}
|
||||
|
||||
export function sellPriceFromItem(item, rate = getEurToTomanRate()) {
|
||||
const eur = euroBase(item);
|
||||
if (eur != null) return eurToTomanSell(eur, rate);
|
||||
const n = Number(item?.price);
|
||||
return Number.isFinite(n) && n > 0 ? Math.round(n) : 0;
|
||||
}
|
||||
|
||||
export function sellCompareFromItem(item, rate = getEurToTomanRate()) {
|
||||
const eur = euroBase(item);
|
||||
const compare = Number(item?.compareAtPrice);
|
||||
if (
|
||||
eur != null &&
|
||||
Number.isFinite(compare) &&
|
||||
compare > 0 &&
|
||||
compare < EUR_DETECT_MAX
|
||||
) {
|
||||
return eurToTomanSell(compare, rate);
|
||||
}
|
||||
if (Number.isFinite(compare) && compare > 0) return Math.round(compare);
|
||||
return null;
|
||||
}
|
||||
Reference in New Issue
Block a user