first deployment of kikomiko with url photos

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
shayan khoshrou
2026-10-19 19:10:57 +02:00
co-authored by Cursor
parent 8d98304137
commit ae3ed7c7b7
159 changed files with 59509 additions and 0 deletions
+180
View File
@@ -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;
}