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
+167
View File
@@ -0,0 +1,167 @@
import {
displayName,
escapeSearchHtml,
formatSearchPrice,
loadSearchCatalog,
searchCatalogProducts,
} from "/js/search-catalog.js?v=persianCopyRtl2";
import { initCardShadePickers } from "/js/buy.js?v=priceSort2";
import { refreshProductFilters } from "/js/filters.js?v=priceSort2";
import { loveYourEyesPillHtml } from "/js/love-your-eyes.js?v=lyeOff";
function shadeDots(p) {
const variants = p.colorVariants;
if (!variants || variants.length < 2) return "";
const shown = variants.slice(0, 5);
const extra = variants.length - shown.length;
return `<div class="shade-dots" role="listbox" aria-label="رنگ">${shown
.map((v) => {
const active = v.slug === p.slug ? " is-active" : "";
const bg = v.heroImage
? `background-image:url('${escapeSearchHtml(v.heroImage)}')`
: "";
const href = `/products/${escapeSearchHtml(v.slug)}/`;
return `<button
type="button"
class="shade-dot${active}"
style="${bg}"
title="${escapeSearchHtml(v.shade)}"
aria-label="${escapeSearchHtml(v.shade)}"
aria-selected="${active ? "true" : "false"}"
data-card-shade
data-slug="${escapeSearchHtml(v.slug)}"
data-image="${escapeSearchHtml(v.heroImage || "")}"
data-price="${v.price}"
data-compare="${v.compareAtPrice ?? ""}"
data-href="${href}"
></button>`;
})
.join("")}${
extra > 0
? `<span class="shade-dot-more">+${extra.toLocaleString("fa-IR")} عدد</span>`
: ""
}</div>`;
}
function cardIsInStock(p) {
const variants = p.colorVariants;
if (variants && variants.length > 1) {
return variants.some((v) => Number(v.stock) > 0);
}
return Number(p.stock) > 0;
}
function stockBadge(p) {
if (cardIsInStock(p)) return "";
return `<span class="stock-badge stock-badge--oos">نا موجود</span>`;
}
function filterAttrs(p) {
const updated = p.updatedAt ? new Date(p.updatedAt).getTime() : 0;
const discount =
p.compareAtPrice && p.compareAtPrice > p.price
? Math.round((1 - p.price / p.compareAtPrice) * 100)
: 0;
const prices = [Number(p.price) || 0];
for (const v of p.colorVariants || []) {
const n = Number(v.price);
if (Number.isFinite(n)) prices.push(n);
}
const min = Math.min(...prices);
const max = Math.max(...prices);
return [
`data-category="${escapeSearchHtml(p.category || "")}"`,
`data-category-fa="${escapeSearchHtml(p.categoryFa || "")}"`,
`data-price="${p.price}"`,
`data-price-min="${min}"`,
`data-price-max="${max}"`,
`data-name="${escapeSearchHtml(p.name)}"`,
`data-updated="${Number.isFinite(updated) ? updated : 0}"`,
`data-rating="${p.ratingAvg || 0}"`,
`data-reviews="${p.reviewCount || 0}"`,
`data-featured="${p.featured ? 1 : 0}"`,
`data-in-stock="${cardIsInStock(p) ? 1 : 0}"`,
`data-discount="${discount}"`,
].join(" ");
}
function productCardHtml(p) {
const title = displayName(p);
const href = `/products/${escapeSearchHtml(p.slug)}/`;
const discount =
p.compareAtPrice && p.compareAtPrice > p.price
? Math.round((1 - p.price / p.compareAtPrice) * 100)
: 0;
const badge =
discount >= 1 ? `<span class="offer-badge">−${discount}%</span>` : "";
const priceHtml =
discount >= 1
? `<p class="price price--offer" data-card-price>
<span class="price-now">${formatSearchPrice(p.price)}</span>
<span class="price-was">${formatSearchPrice(p.compareAtPrice)}</span>
</p>`
: `<p class="price" data-card-price>${formatSearchPrice(p.price)}</p>`;
return `<article class="product-card${discount >= 1 ? " product-card--offer" : ""}" data-product-card ${filterAttrs(p)}>
<a class="thumb" href="${href}" data-card-link>
${badge}
${stockBadge(p)}
${
p.heroImage
? `<img src="${escapeSearchHtml(p.heroImage)}" alt="${escapeSearchHtml(title)}" loading="lazy" data-card-image />`
: ""
}
</a>
<div class="meta">
${loveYourEyesPillHtml(p.slug)}
<p class="name"><a href="${href}" data-card-link>${escapeSearchHtml(title)}</a></p>
${shadeDots(p)}
<div class="card-action-row">
${priceHtml}
<button type="button" class="card-add-btn" data-add-to-cart data-product-id="${escapeSearchHtml(p.id)}" data-slug="${escapeSearchHtml(p.slug)}" data-name="${escapeSearchHtml(title)}" data-price="${p.price}" data-image="${escapeSearchHtml(p.heroImage || "")}" data-category="${escapeSearchHtml(p.category || "")}" data-category-fa="${escapeSearchHtml(p.categoryFa || "")}" aria-label="افزودن به سبد خرید">
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z"/><line x1="3" y1="6" x2="21" y2="6"/><path d="M16 10a4 4 0 0 1-8 0"/></svg>
</button>
</div>
</div>
</article>`;
}
const params = new URLSearchParams(location.search);
const q = (params.get("q") || "").trim();
const titleEl = document.querySelector("[data-search-title]");
const resultsEl = document.querySelector("[data-search-results]");
const emptyEl = document.querySelector("[data-search-empty]");
const countEl = document.querySelector("[data-listing-count]");
const resultPill = document.querySelector("[data-filters-result-count]");
if (titleEl && q) titleEl.textContent = `نتایج برای «${q}»`;
try {
const products = await loadSearchCatalog();
const hits = searchCatalogProducts(products, q);
if (!resultsEl) {
/* no-op */
} else if (!hits.length) {
if (emptyEl) emptyEl.hidden = false;
resultsEl.innerHTML = "";
if (countEl) countEl.textContent = "۰";
if (resultPill) resultPill.textContent = "۰";
} else {
if (emptyEl) emptyEl.hidden = true;
resultsEl.innerHTML = hits.map(productCardHtml).join("");
const n = hits.length.toLocaleString("fa-IR");
if (countEl) countEl.textContent = n;
if (resultPill) resultPill.textContent = n;
initCardShadePickers();
refreshProductFilters();
}
} catch {
if (emptyEl) {
emptyEl.hidden = false;
emptyEl.querySelector("p") &&
(emptyEl.querySelector("p").textContent = "بارگذاری نتایج ممکن نشد.");
}
}