2209 lines
93 KiB
TypeScript
2209 lines
93 KiB
TypeScript
import { formatPrice } from "../../src/lib/utils";
|
||
import type { Dictionary } from "../../src/lib/i18n/dictionaries";
|
||
import type { MegaMenuRoot } from "../../src/lib/mega-menu";
|
||
import { SEARCH_SORT_OPTIONS, type SearchSort } from "../../src/lib/search-sort";
|
||
import {
|
||
displayNameWithoutShade,
|
||
variantGalleryImages,
|
||
type ColorVariant,
|
||
} from "../../src/lib/color-variants";
|
||
import { productCategoryLink, withPersianCategory, persianCategoryLabel } from "../../src/lib/categories/kiko";
|
||
import { loveYourEyesPillHtml } from "../../src/lib/promos/love-your-eyes";
|
||
import { headSeoTags, STORE_FAQS } from "../../src/lib/seo";
|
||
|
||
|
||
function esc(s: unknown) {
|
||
return String(s ?? "")
|
||
.replaceAll("&", "&")
|
||
.replaceAll("<", "<")
|
||
.replaceAll(">", ">")
|
||
.replaceAll('"', """);
|
||
}
|
||
|
||
/** English/original product name with Persian category beside it. */
|
||
function productDisplayTitle(
|
||
p: { name: string; nameFa?: string | null; category?: string | null },
|
||
hasVariants = false
|
||
) {
|
||
const base = displayNameWithoutShade(p.name, hasVariants);
|
||
return withPersianCategory(base, p.category);
|
||
}
|
||
|
||
function icon(name: "bag" | "menu" | "x" | "chevron" | "chevron-end" | "chevron-start") {
|
||
if (name === "bag") {
|
||
// Lucide-style shopping bag (matches original storefront)
|
||
return `<svg class="icon icon-bag" viewBox="0 0 24 24" aria-hidden="true"><path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4Z"/><path d="M3 6h18"/><path d="M16 10a4 4 0 0 1-8 0"/></svg>`;
|
||
}
|
||
if (name === "menu") {
|
||
return `<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M4 7h16M4 12h16M4 17h16"/></svg>`;
|
||
}
|
||
if (name === "x") {
|
||
return `<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18"/></svg>`;
|
||
}
|
||
if (name === "chevron-end") {
|
||
// Points toward the right edge (hide in RTL layout)
|
||
return `<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M10 6.5 15.5 12 10 17.5"/></svg>`;
|
||
}
|
||
if (name === "chevron-start") {
|
||
return `<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M14 6.5 8.5 12 14 17.5"/></svg>`;
|
||
}
|
||
return `<svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M6 9l6 6 6-6"/></svg>`;
|
||
}
|
||
|
||
const INSTAGRAM_URL = "https://www.instagram.com/kikomiko.ir/";
|
||
const INSTAGRAM_LABEL = "اینستاگرام ما";
|
||
|
||
function instagramFooterIcon() {
|
||
return `<a class="footer-instagram" href="${INSTAGRAM_URL}" target="_blank" rel="noopener noreferrer" aria-label="${INSTAGRAM_LABEL}">
|
||
<img class="footer-ig-icon" src="/images/icons/instagram.png" alt="" width="40" height="40" />
|
||
</a>`;
|
||
}
|
||
|
||
const CONTACT_ADDRESS_LABEL =
|
||
"Italy, Torino, Via Luigi Capriolo 58, 10139";
|
||
const CONTACT_ADDRESS_HREF =
|
||
"https://www.google.com/maps/search/?api=1&query=" +
|
||
encodeURIComponent("Via Luigi Capriolo 58, 10139 Torino, Italy");
|
||
const CONTACT_SUPPORT = [
|
||
{ display: "۰۹۱۲۱۸۵۹۷۱۲", tel: "+989121859712" },
|
||
{ display: "۰۹۳۸۷۲۴۷۲۳۵", tel: "+989387247235" },
|
||
] as const;
|
||
const CONTACT_WHATSAPP_DISPLAY = "+۳۹۳۵۲۵۶۶۶۰۷";
|
||
const CONTACT_WHATSAPP_HREF = "https://wa.me/39352566607";
|
||
|
||
export function contactPageBody() {
|
||
const promises = [
|
||
"محصولات اورجینال KIKO Milano",
|
||
"واردات مستقیم از ایتالیا",
|
||
"تضمین اصالت کالا",
|
||
"ارسال به سراسر ایران",
|
||
];
|
||
return `<div class="page-pad contact-page" dir="rtl">
|
||
<section class="contact-hero container-luxury">
|
||
<p class="label-caps text-muted">کیکومیکو</p>
|
||
<h1 class="page-title">با کیکومیکو در ارتباط باشید</h1>
|
||
<p class="contact-lead">ما اینجا هستیم تا تجربهای مطمئنتر و لذتبخشتر از خرید محصولات KIKO Milano داشته باشید.</p>
|
||
<p class="contact-copy">اگر درباره محصولات، انتخاب شید و رنگ، وضعیت سفارش، ارسال، اصالت کالا یا هر موضوع دیگری سوالی دارید، تیم کیکومیکو با خوشحالی پاسخگوی شماست.</p>
|
||
</section>
|
||
|
||
<section class="contact-hours" aria-label="ساعات پشتیبانی">
|
||
<div class="contact-hours-inner container-luxury">
|
||
<article class="contact-hour-card">
|
||
<p class="contact-hour-label">پشتیبانی واتساپ</p>
|
||
<p class="contact-hour-value">همه روزه از ۹ صبح تا ۱۲ شب</p>
|
||
</article>
|
||
<article class="contact-hour-card">
|
||
<p class="contact-hour-label">پشتیبانی تلفنی</p>
|
||
<p class="contact-hour-value">از ۹ صبح تا ۵–۷ بعد از ظهر</p>
|
||
</article>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="contact-details container-luxury" aria-label="راههای ارتباطی">
|
||
<h2 class="contact-section-title">راههای تماس</h2>
|
||
<ul class="contact-list">
|
||
<li>
|
||
<span class="contact-label">آدرس :</span>
|
||
<a class="contact-link" href="${esc(CONTACT_ADDRESS_HREF)}" target="_blank" rel="noopener noreferrer">${esc(CONTACT_ADDRESS_LABEL)}</a>
|
||
</li>
|
||
<li>
|
||
<span class="contact-label">شماره پشتیبانی (ایران) :</span>
|
||
<span class="contact-numbers">
|
||
${CONTACT_SUPPORT.map(
|
||
(n) =>
|
||
`<a class="contact-link" href="tel:${n.tel}" dir="ltr">${esc(n.display)}</a>`
|
||
).join('<span class="contact-sep"> و </span>')}
|
||
</span>
|
||
</li>
|
||
<li>
|
||
<span class="contact-label">واتساپ (ایتالیا) :</span>
|
||
<a class="contact-link" href="${esc(CONTACT_WHATSAPP_HREF)}" target="_blank" rel="noopener noreferrer" dir="ltr">${esc(CONTACT_WHATSAPP_DISPLAY)}</a>
|
||
</li>
|
||
</ul>
|
||
</section>
|
||
|
||
<section class="contact-guide container-luxury">
|
||
<h2 class="contact-section-title">چطور پیام بدهید؟</h2>
|
||
<p class="contact-copy">سوال یا درخواست خود را از طریق واتساپ برای ما ارسال کنید. پیام شما در سریعترین زمان ممکن بررسی و پاسخ داده خواهد شد.</p>
|
||
<p class="contact-copy">همینطور برای پیگیری سفارش میتوانید از طریق تماس تلفنی یا واتساپ کیکومیکو با ما در ارتباط باشید.</p>
|
||
<div class="contact-actions">
|
||
<a class="btn lux-btn-primary" href="${esc(CONTACT_WHATSAPP_HREF)}" target="_blank" rel="noopener noreferrer">پیام در واتساپ</a>
|
||
<a class="btn" href="tel:${CONTACT_SUPPORT[0].tel}">تماس تلفنی</a>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="contact-brand" aria-label="کیکومیکو">
|
||
<div class="contact-brand-inner container-luxury">
|
||
<p class="label-caps text-muted">کیکومیکو</p>
|
||
<h2 class="contact-brand-title">زیبایی اصیل ایتالیایی، در ایران.</h2>
|
||
<ul class="contact-promises">
|
||
${promises
|
||
.map(
|
||
(item) =>
|
||
`<li><span class="contact-promise-check" aria-hidden="true">✓</span>${esc(item)}</li>`
|
||
)
|
||
.join("")}
|
||
</ul>
|
||
</div>
|
||
</section>
|
||
</div>`;
|
||
}
|
||
|
||
export function aboutPageBody() {
|
||
return `<div class="page-pad about-page" dir="rtl">
|
||
<section class="contact-hero container-luxury">
|
||
<p class="label-caps text-muted">کیکومیکو</p>
|
||
<h1 class="page-title">درباره کیکو میکو</h1>
|
||
<p class="contact-lead">کیکو میکو فروشگاه تخصصی محصولات KIKO Milano در ایران است. هدف ما این است که دسترسی به محصولات و جدیدترین کالکشنهای کیکو میلانو ایتالیا را برای علاقهمندان به لوازم آرایشی در ایران سادهتر کنیم.</p>
|
||
<p class="contact-copy">محصولات کیکو میکو مستقیماً از ایتالیا تهیه و به ایران وارد میشوند. به همین دلیل میتوانید بسیاری از محصولات محبوب، کالکشنهای جدید، محصولات فصلی و نسخههای Limited Edition برند KIKO Milano را بدون نیاز به خرید مستقیم از فروشگاههای خارجی سفارش دهید.</p>
|
||
<p class="contact-copy">در فروشگاه کیکو میکو میتوانید انواع لوازم آرایشی KIKO Milano از جمله رژ لب، لیپ گلاس، ریمل، خط چشم، مداد چشم، سایه چشم، محصولات آرایش صورت، رژگونه، برنزر، هایلایتر، محصولات مراقبت از لب، براش و اکسسوریهای آرایشی کیکو را پیدا کنید.</p>
|
||
</section>
|
||
|
||
<section class="about-band" aria-hidden="true">
|
||
<div class="about-band-line container-luxury"></div>
|
||
</section>
|
||
|
||
<section class="contact-guide container-luxury">
|
||
<h2 class="contact-section-title">خرید محصولات KIKO Milano از ایتالیا</h2>
|
||
<p class="contact-copy">یکی از تفاوتهای اصلی کیکو میکو این است که محصولات از ایتالیا خریداری شده و سپس برای مشتریان داخل ایران ارسال میشوند. به همین دلیل امکان ارائه محصولات و کالکشنهایی وجود دارد که ممکن است در بازار ایران بهراحتی در دسترس نباشند.</p>
|
||
<p class="contact-copy">با توجه به اینکه سفارشها از ایتالیا وارد ایران میشوند، زمان تحویل سفارش معمولاً بین یک تا سه هفته کاری است. این بازه شامل تهیه محصول، ارسال بینالمللی و مراحل ورود کالا به ایران میشود.</p>
|
||
<p class="contact-copy">در بعضی شرایط مانند تأخیر در حملونقل بینالمللی، تعطیلات، شرایط گمرکی یا عوامل خارج از کنترل، ممکن است زمان تحویل کمی بیشتر شود. تلاش ما این است که سفارشها در سریعترین زمان ممکن تهیه و ارسال شوند و مشتری از روند سفارش خود اطلاع داشته باشد.</p>
|
||
</section>
|
||
|
||
<section class="contact-guide container-luxury">
|
||
<h2 class="contact-section-title">جدیدترین کالکشنهای کیکو میلانو</h2>
|
||
<p class="contact-copy">KIKO Milano بهصورت مداوم کالکشنها و محصولات جدیدی عرضه میکند. در کیکو میکو تلاش میکنیم جدیدترین محصولات و کالکشنهای KIKO Milano را نیز در کنار محصولات محبوب و پرفروش این برند در اختیار شما قرار دهیم.</p>
|
||
<p class="contact-copy">اگر محصول خاصی از کیکو، یک رنگ مشخص یا محصولی از یک کالکشن جدید مدنظر شما باشد، میتوانید موجودی و امکان تهیه آن از ایتالیا را نیز بررسی کنید.</p>
|
||
</section>
|
||
|
||
<section class="contact-guide container-luxury">
|
||
<h2 class="contact-section-title">چرا کیکو میکو؟</h2>
|
||
<p class="contact-copy">کیکو میکو برای کسانی ساخته شده که به دنبال خرید محصولات کیکو میلانو در ایران هستند و میخواهند به تنوع بیشتری از محصولات KIKO Milano ایتالیا دسترسی داشته باشند.</p>
|
||
<p class="contact-copy">از محصولات آرایش چشم و لب گرفته تا آرایش صورت، اکسسوریها و کالکشنهای محدود، هدف ما ایجاد یک مسیر سادهتر برای سفارش محصولات این برند از ایتالیا و دریافت آنها در ایران است.</p>
|
||
</section>
|
||
|
||
<section class="contact-brand" aria-label="کیکومیکو">
|
||
<div class="contact-brand-inner container-luxury">
|
||
<p class="label-caps text-muted">کیکومیکو</p>
|
||
<h2 class="contact-brand-title">کیکو میکو؛ محصولات KIKO Milano، از ایتالیا تا ایران.</h2>
|
||
<div class="contact-actions">
|
||
<a class="btn lux-btn-primary" href="/collection/">مشاهده محصولات</a>
|
||
<a class="btn" href="/contact/">تماس با ما</a>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</div>`;
|
||
}
|
||
|
||
export function authenticityPageBody() {
|
||
return `<div class="page-pad about-page authenticity-page" dir="rtl">
|
||
<section class="contact-hero container-luxury">
|
||
<p class="label-caps text-muted">کیکومیکو</p>
|
||
<h1 class="page-title">اصالت کالا</h1>
|
||
<p class="contact-lead">کیکومیکو فروشگاه تخصصی و واردکننده مستقل محصولات اورجینال KIKO Milano از ایتالیا به ایران است؛ متعلق به برند کیکو میلانو نیستیم، اما محصولات را از کانالهای معتبر ایتالیا تهیه میکنیم.</p>
|
||
<p class="contact-copy">هدف ما این است که هنگام خرید لوازم آرایشی کیکو در ایران، بدانید از کجا آمدهاند، چطور تضمین اصالت میدهیم، و اگر سوالی داشتید چطور با ما در تماس باشید.</p>
|
||
</section>
|
||
|
||
<section class="about-band" aria-hidden="true">
|
||
<div class="about-band-line container-luxury"></div>
|
||
</section>
|
||
|
||
<section class="contact-guide container-luxury">
|
||
<h2 class="contact-section-title">محصولات را چطور تهیه میکنیم؟</h2>
|
||
<p class="contact-copy">سفارشها از ایتالیا تهیه میشوند؛ از فروشگاهها و کانالهای رسمی KIKO Milano در ایتالیا خریداری و سپس برای مشتریان داخل ایران ارسال میگردند.</p>
|
||
<p class="contact-copy">به همین دلیل بسیاری از محصولات، شیدها و کالکشنهای محدودی که در بازار ایران بهراحتی پیدا نمیشوند، از طریق کیکومیکو قابل سفارش هستند.</p>
|
||
</section>
|
||
|
||
<section class="contact-guide container-luxury">
|
||
<h2 class="contact-section-title">تضمین اصالت کالا یعنی چه؟</h2>
|
||
<p class="contact-copy">ما متعهدیم محصولات عرضهشده اورجینال KIKO Milano باشند. اگر ثابت شود کالای دریافتی غیراصل است، پس از بررسی از طریق <a class="contact-link" href="/contact/">تماس با ما</a>، مطابق قوانین فروشگاه اقدام به تعویض یا استرداد میکنیم.</p>
|
||
<p class="contact-copy">برای پیگیری هرگونه نگرانی درباره اصالت، بستهبندی یا کد محصول، با پشتیبانی واتساپ یا تلفن در صفحه تماس در ارتباط باشید.</p>
|
||
</section>
|
||
|
||
<section class="contact-guide container-luxury">
|
||
<h2 class="contact-section-title">زمان ارسال از ایتالیا</h2>
|
||
<p class="contact-copy">چون سفارشها از ایتالیا وارد ایران میشوند، زمان تحویل معمولاً بین یک تا سه هفته کاری است. این بازه شامل تهیه محصول، ارسال بینالمللی و مراحل ورود کالا به ایران میشود.</p>
|
||
<p class="contact-copy">در شرایط خاص مانند تأخیر حملونقل، تعطیلات یا عوامل گمرکی، ممکن است زمان تحویل کمی بیشتر شود؛ تلاش ما اطلاعرسانی شفاف و ارسال در کوتاهترین زمان ممکن است.</p>
|
||
</section>
|
||
|
||
<section class="contact-guide container-luxury">
|
||
<h2 class="contact-section-title">سوال درباره اصالت دارید؟</h2>
|
||
<p class="contact-copy">جزئیات آدرس، شماره پشتیبانی ایران و واتساپ ایتالیا در صفحه <a class="contact-link" href="/contact/">تماس با ما</a> آمده است. پیام یا تماس شما درباره اصالت کالا در اولویت بررسی قرار میگیرد.</p>
|
||
<div class="contact-actions">
|
||
<a class="btn lux-btn-primary" href="/contact/">تماس با ما</a>
|
||
<a class="btn" href="${esc(CONTACT_WHATSAPP_HREF)}" target="_blank" rel="noopener noreferrer">پیام در واتساپ</a>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="contact-brand" aria-label="کیکومیکو">
|
||
<div class="contact-brand-inner container-luxury">
|
||
<p class="label-caps text-muted">کیکومیکو</p>
|
||
<h2 class="contact-brand-title">واردکننده مستقل محصولات اورجینال KIKO Milano از ایتالیا.</h2>
|
||
<div class="contact-actions">
|
||
<a class="btn lux-btn-primary" href="/collection/">مشاهده محصولات</a>
|
||
<a class="btn" href="/about/">درباره ما</a>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</div>`;
|
||
}
|
||
|
||
/** Persian label for catalog sync timestamp (prices/stock from KIKO Italy). */
|
||
export function formatCatalogSyncTime(iso: string) {
|
||
const d = new Date(iso);
|
||
if (Number.isNaN(d.getTime())) return null;
|
||
return new Intl.DateTimeFormat("fa-IR", {
|
||
dateStyle: "medium",
|
||
timeStyle: "short",
|
||
timeZone: "Asia/Tehran",
|
||
}).format(d);
|
||
}
|
||
|
||
export function layout(opts: {
|
||
title: string;
|
||
dict: Dictionary;
|
||
megaMenu: MegaMenuRoot[];
|
||
body: string;
|
||
description?: string;
|
||
whatsapp?: string;
|
||
ordersEndpoint?: string;
|
||
/** ISO timestamp from data/site.json generatedAt */
|
||
catalogUpdatedAt?: string;
|
||
/** EUR→toman sell rate used for catalog pricing (from data/site.json) */
|
||
eurToTomanRate?: number;
|
||
/** Extra tags injected into <head> (preload, etc.) */
|
||
headExtra?: string;
|
||
/** Site-relative path for canonical + Open Graph (e.g. /collection/). */
|
||
canonicalPath?: string;
|
||
ogImage?: string | null;
|
||
noindex?: boolean;
|
||
nofollow?: boolean;
|
||
ogType?: "website" | "product";
|
||
}) {
|
||
const {
|
||
title,
|
||
dict,
|
||
megaMenu,
|
||
body,
|
||
description,
|
||
whatsapp,
|
||
ordersEndpoint,
|
||
catalogUpdatedAt,
|
||
eurToTomanRate,
|
||
headExtra = "",
|
||
canonicalPath,
|
||
ogImage,
|
||
noindex,
|
||
nofollow,
|
||
ogType,
|
||
} = opts;
|
||
const seoTags = canonicalPath
|
||
? headSeoTags({
|
||
path: canonicalPath,
|
||
title,
|
||
description,
|
||
image: ogImage,
|
||
noindex,
|
||
nofollow,
|
||
ogType,
|
||
})
|
||
: "";
|
||
const year = new Date().getFullYear();
|
||
const wa = whatsapp || process.env.WHATSAPP_NUMBER || "393520566607";
|
||
const orders = ordersEndpoint || process.env.ORDERS_ENDPOINT || "";
|
||
const googlePlacesKey = (process.env.GOOGLE_PLACES_API_KEY || "").trim();
|
||
const eurRateAttr =
|
||
typeof eurToTomanRate === "number" &&
|
||
Number.isFinite(eurToTomanRate) &&
|
||
eurToTomanRate > 0
|
||
? ` data-eur-rate="${esc(String(Math.round(eurToTomanRate)))}"`
|
||
: "";
|
||
|
||
const megaPanes = megaMenu
|
||
.map((root) => {
|
||
const children = root.children
|
||
.map(
|
||
(c, index) =>
|
||
`<li class="mega-child">
|
||
<span class="mega-idx">|${String(index + 1).padStart(2, "0")}|</span>
|
||
<a href="${esc(c.href)}">${esc(c.label)}<span class="mega-count">(${esc(c.count)})</span></a>
|
||
</li>`
|
||
)
|
||
.join("");
|
||
const featured = root.featured
|
||
.map(
|
||
(p) => `
|
||
<a class="mega-card" href="/products/${esc(p.slug)}">
|
||
${p.heroImage ? `<img src="${esc(p.heroImage)}" alt="${esc(productDisplayTitle(p))}" loading="lazy" />` : `<div class="mega-card-ph"></div>`}
|
||
<p class="mega-card-name">${esc(productDisplayTitle(p))}</p>
|
||
<p class="mega-card-price">${esc(formatPrice(p.price))}</p>
|
||
</a>`
|
||
)
|
||
.join("");
|
||
return `<div class="mega-pane" data-mega-pane="${esc(root.key)}" hidden>
|
||
<div class="mega-grid">
|
||
<div class="mega-col">
|
||
<p class="mega-label">دستهبندیهای ${esc(root.label)}</p>
|
||
<ul class="mega-children">
|
||
<li><a class="mega-all" href="${esc(root.href)}">مشاهده همه</a></li>
|
||
${children || `<li class="text-muted">هنوز محصولی در این دسته نیست.</li>`}
|
||
</ul>
|
||
</div>
|
||
<div class="mega-col mega-col-mid">
|
||
<p class="mega-label">مجموعه</p>
|
||
<a class="mega-title" href="${esc(root.href)}">${esc(root.label)}</a>
|
||
<p class="mega-desc">${esc(root.description)}</p>
|
||
</div>
|
||
<div class="mega-col">
|
||
<p class="mega-label">منتخب</p>
|
||
<div class="mega-featured">${featured || `<p class="text-muted">تصویری برای نمایش موجود نیست.</p>`}</div>
|
||
</div>
|
||
</div>
|
||
</div>`;
|
||
})
|
||
.join("");
|
||
|
||
const sideNav = [
|
||
`<a class="side-nav-link is-active" href="/">${esc(dict.nav.home)}</a>`,
|
||
...megaMenu.map(
|
||
(r) =>
|
||
`<button type="button" class="side-nav-link" data-mega-key="${esc(r.key)}">${esc(r.label)}</button>`
|
||
),
|
||
`<a class="side-nav-link" href="/offers/">${esc(dict.withDiscount)}</a>`,
|
||
`<a class="side-nav-link" href="/about/">درباره ما</a>`,
|
||
`<a class="side-nav-link" href="/authenticity/">اصالت کالا</a>`,
|
||
`<a class="side-nav-link" href="/contact/">تماس با ما</a>`,
|
||
`<a class="side-nav-link" href="${INSTAGRAM_URL}" target="_blank" rel="noopener noreferrer">${INSTAGRAM_LABEL}</a>`,
|
||
].join("");
|
||
|
||
const mobileNav = [
|
||
`<a href="/">${esc(dict.nav.home)}</a>`,
|
||
...megaMenu.map(
|
||
(r) => `
|
||
<div class="mobile-cat">
|
||
<div class="mobile-cat-row">
|
||
<a href="${esc(r.href)}">${esc(r.label)}</a>
|
||
${
|
||
r.children.length
|
||
? `<button type="button" data-mobile-expand="${esc(r.key)}" aria-label="زیرمجموعههای ${esc(r.label)}">${icon("chevron")}</button>`
|
||
: ""
|
||
}
|
||
</div>
|
||
<div class="mobile-sub" data-mobile-sub="${esc(r.key)}">
|
||
${r.children.map((c) => `<a href="${esc(c.href)}">${esc(c.label)} <span>(${esc(c.count)})</span></a>`).join("")}
|
||
</div>
|
||
</div>`
|
||
),
|
||
`<a href="/offers/">${esc(dict.withDiscount)}</a>`,
|
||
`<a href="/about/">درباره ما</a>`,
|
||
`<a href="/authenticity/">اصالت کالا</a>`,
|
||
`<a href="/contact/">تماس با ما</a>`,
|
||
`<a href="${INSTAGRAM_URL}" target="_blank" rel="noopener noreferrer">${INSTAGRAM_LABEL}</a>`,
|
||
].join("");
|
||
|
||
return `<!DOCTYPE html>
|
||
<html lang="fa" dir="rtl">
|
||
<head>
|
||
<meta charset="utf-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||
${catalogUpdatedAt ? `<meta name="catalog-updated-at" content="${esc(catalogUpdatedAt)}" />` : ""}
|
||
<meta name="enamad" content="60162903">
|
||
<!-- Google tag (gtag.js) — deferred until after load / idle so it does not contend with LCP/INP -->
|
||
<script>
|
||
window.dataLayer = window.dataLayer || [];
|
||
function gtag(){dataLayer.push(arguments);}
|
||
function __kikoLoadGtag(){
|
||
if (window.__kikoGtagLoaded) return;
|
||
window.__kikoGtagLoaded = true;
|
||
var s = document.createElement('script');
|
||
s.async = true;
|
||
s.src = 'https://www.googletagmanager.com/gtag/js?id=G-LP2TBBKMBP';
|
||
s.onload = function(){
|
||
gtag('js', new Date());
|
||
gtag('config', 'G-LP2TBBKMBP');
|
||
};
|
||
document.head.appendChild(s);
|
||
}
|
||
function __kikoScheduleGtag(){
|
||
if ('requestIdleCallback' in window) {
|
||
requestIdleCallback(__kikoLoadGtag, { timeout: 4000 });
|
||
} else {
|
||
setTimeout(__kikoLoadGtag, 1);
|
||
}
|
||
}
|
||
if (document.readyState === 'complete') __kikoScheduleGtag();
|
||
else window.addEventListener('load', __kikoScheduleGtag, { once: true });
|
||
</script>
|
||
<title>${esc(title)}</title>
|
||
${description ? `<meta name="description" content="${esc(description)}" />` : ""}
|
||
${seoTags}
|
||
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
||
<link rel="apple-touch-icon" href="/favicon.svg" />
|
||
<link rel="preload" as="font" href="/fonts/yekan-bakh/YekanBakh_FaNum_Regular.woff" type="font/woff" crossorigin />
|
||
<link rel="stylesheet" href="/css/site.css?v=brandInv1" />
|
||
${headExtra}</head>
|
||
<body data-whatsapp="${esc(wa)}" data-orders-endpoint="${esc(orders)}"${googlePlacesKey ? ` data-google-places-key="${esc(googlePlacesKey)}"` : ""}${eurRateAttr}>
|
||
<header class="site-header" dir="ltr">
|
||
<div class="header-bar">
|
||
<a class="site-logo" href="/" aria-label="${esc(dict.brand)}">
|
||
<span class="site-logo-mark">Kiko</span><span class="site-logo-tail">Miko</span>
|
||
</a>
|
||
|
||
<div class="header-actions">
|
||
<div class="cart-anchor" data-cart-menu>
|
||
<button
|
||
type="button"
|
||
class="cart-toggle"
|
||
data-cart-trigger
|
||
aria-label="${esc(dict.cart)}"
|
||
aria-haspopup="true"
|
||
aria-expanded="false"
|
||
aria-controls="cart-preview-panel"
|
||
>
|
||
${icon("bag")}
|
||
<span class="cart-label">${esc(dict.cart)}</span>
|
||
<span class="cart-badge" data-cart-count>0</span>
|
||
</button>
|
||
<div class="cart-preview" data-cart-preview hidden>
|
||
<div class="cart-preview-panel" id="cart-preview-panel" dir="rtl">
|
||
<p class="cart-preview-title">${esc(dict.cart)}</p>
|
||
<div class="cart-preview-list" data-cart-preview-list></div>
|
||
<p class="cart-preview-empty" data-cart-preview-empty>${esc(dict.emptyCart)}</p>
|
||
<div class="cart-preview-foot" data-cart-preview-foot hidden>
|
||
<p class="cart-preview-total">
|
||
<span>جمع</span>
|
||
<strong data-cart-preview-total>۰ تومان</strong>
|
||
</p>
|
||
<a class="cart-preview-cta" href="/cart/">${esc(dict.cart)}</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<form class="search-form" action="/search/" method="get" role="search">
|
||
<div class="search-shell">
|
||
<input type="search" name="q" dir="rtl" placeholder="${esc(dict.searchPlaceholder)}" aria-label="${esc(dict.searchPlaceholder)}" autocomplete="off" aria-expanded="false" aria-controls="header-search-suggest" aria-autocomplete="list" />
|
||
<div class="search-suggest" id="header-search-suggest" data-search-suggest role="listbox" hidden></div>
|
||
</div>
|
||
</form>
|
||
|
||
<button type="button" class="menu-toggle" data-menu-open aria-label="باز کردن منو" aria-expanded="false" aria-controls="mobile-menu">${icon("menu")}</button>
|
||
</div>
|
||
|
||
<nav class="side-nav" dir="rtl" aria-label="دستهبندیها">
|
||
<button
|
||
type="button"
|
||
class="side-nav-toggle"
|
||
data-side-nav-toggle
|
||
aria-expanded="true"
|
||
aria-controls="side-nav-card"
|
||
aria-label="پنهان کردن منو"
|
||
title="پنهان کردن منو"
|
||
>
|
||
<span class="side-nav-toggle-hide" aria-hidden="true">${icon("chevron-end")}</span>
|
||
<span class="side-nav-toggle-show" aria-hidden="true">منو</span>
|
||
</button>
|
||
<div class="side-nav-card" id="side-nav-card">${sideNav}</div>
|
||
</nav>
|
||
|
||
<div class="mega-panel" dir="rtl">${megaPanes}</div>
|
||
</header>
|
||
|
||
<button type="button" class="mega-backdrop" data-mega-close hidden aria-label="بستن منو"></button>
|
||
|
||
<div class="mobile-drawer" aria-hidden="true">
|
||
<button type="button" class="mobile-backdrop" data-menu-close aria-label="بستن منو"></button>
|
||
<aside class="mobile-panel" dir="rtl" role="dialog" aria-label="منوی موبایل">
|
||
<div class="mobile-panel-head">
|
||
<p>${esc(dict.brand)}</p>
|
||
<button type="button" data-menu-close aria-label="بستن منو">${icon("x")}</button>
|
||
</div>
|
||
<form class="mobile-search" action="/search/" method="get">
|
||
<input type="search" name="q" placeholder="${esc(dict.searchPlaceholder)}" aria-label="${esc(dict.searchPlaceholder)}" autocomplete="off" aria-expanded="false" aria-autocomplete="list" />
|
||
</form>
|
||
<nav class="mobile-nav" aria-label="منوی موبایل">${mobileNav}
|
||
<a class="mobile-cart" href="/cart/">${icon("bag")} ${esc(dict.cart)}</a>
|
||
</nav>
|
||
</aside>
|
||
</div>
|
||
|
||
<main>${body}</main>
|
||
|
||
${siteFooter(dict, megaMenu, year, catalogUpdatedAt, eurToTomanRate)}
|
||
|
||
<script type="module" src="/js/main.js?v=priceSort2"></script></body>
|
||
</html>`;
|
||
}
|
||
|
||
function siteFooter(
|
||
dict: Dictionary,
|
||
megaMenu: MegaMenuRoot[],
|
||
year: number,
|
||
catalogUpdatedAt?: string,
|
||
eurToTomanRate?: number
|
||
) {
|
||
const shopLinks = [
|
||
{ href: "/", label: dict.nav.home },
|
||
{ href: "/collection/", label: dict.seeAllProducts },
|
||
{ href: "/offers/", label: dict.withDiscount },
|
||
{ href: "/about/", label: "درباره ما" },
|
||
{ href: "/authenticity/", label: "اصالت کالا" },
|
||
{ href: "/contact/", label: "تماس با ما" },
|
||
{ href: INSTAGRAM_URL, label: INSTAGRAM_LABEL, external: true },
|
||
{ href: "/cart/", label: dict.cart },
|
||
];
|
||
|
||
const menuCols = megaMenu
|
||
.map((root) => {
|
||
const rootHref = root.href.endsWith("/") ? root.href : `${root.href}/`;
|
||
const children = [
|
||
`<li><a href="${esc(rootHref)}">مشاهده همه</a></li>`,
|
||
...root.children.map((c) => {
|
||
const href = c.href.endsWith("/") ? c.href : `${c.href}/`;
|
||
return `<li><a href="${esc(href)}">${esc(c.label)}</a></li>`;
|
||
}),
|
||
].join("");
|
||
return `<details class="footer-col" data-footer-col>
|
||
<summary class="footer-col-title">${esc(root.label)}</summary>
|
||
<ul class="footer-links">${children}</ul>
|
||
</details>`;
|
||
})
|
||
.join("");
|
||
|
||
const syncLabel = catalogUpdatedAt
|
||
? formatCatalogSyncTime(catalogUpdatedAt)
|
||
: null;
|
||
const rateOk =
|
||
typeof eurToTomanRate === "number" &&
|
||
Number.isFinite(eurToTomanRate) &&
|
||
eurToTomanRate > 0;
|
||
const syncParts = [
|
||
syncLabel
|
||
? `<p class="footer-catalog-sync">آخرین بروزرسانی قیمت و موجودی: <time datetime="${esc(catalogUpdatedAt)}">${esc(syncLabel)}</time></p>`
|
||
: "",
|
||
rateOk
|
||
? `<p class="footer-catalog-sync footer-catalog-fx">نرخ یورو: ${esc(formatPrice(eurToTomanRate))}</p>`
|
||
: "",
|
||
].filter(Boolean);
|
||
const syncHtml = syncParts.join("\n ");
|
||
|
||
return `<footer class="site-footer" dir="rtl">
|
||
<div class="footer-inner">
|
||
<div class="footer-top">
|
||
<div class="footer-brand">
|
||
<a class="brand" href="/">${esc(dict.brand)}</a>
|
||
<p class="footer-tagline">واردکننده تخصصی محصولات اورجینال KIKO Milano از ایتالیا — تضمین اصالت و تجربه خرید لوکس.</p>
|
||
<ul class="footer-promises">
|
||
<li>واردات مستقیم از ایتالیا</li>
|
||
<li>تضمین اصالت کالا</li>
|
||
<li>مشاوره تخصصی زیبایی</li>
|
||
</ul>
|
||
${instagramFooterIcon()}
|
||
</div>
|
||
<div class="footer-newsletter">
|
||
<h2 class="footer-newsletter-title">${esc(dict.newsletter)}</h2>
|
||
<p class="footer-newsletter-hint">از کالکشنهای جدید و پیشنهادهای ویژه باخبر شوید.</p>
|
||
<form class="footer-newsletter-form" data-footer-newsletter>
|
||
<label class="sr-only" for="footer-email">${esc(dict.newsletterPlaceholder)}</label>
|
||
<input id="footer-email" type="email" name="email" required autocomplete="email" placeholder="${esc(dict.newsletterPlaceholder)}" />
|
||
<button type="submit" class="label-caps">${esc(dict.newsletterCta)}</button>
|
||
<p class="footer-newsletter-status" data-footer-newsletter-status hidden></p>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<nav class="footer-nav" aria-label="پیوندهای سایت">
|
||
${menuCols}
|
||
<details class="footer-col" data-footer-col>
|
||
<summary class="footer-col-title"><span>فروشگاه</span></summary>
|
||
<ul class="footer-links">
|
||
${shopLinks
|
||
.map((l) => {
|
||
const extra = l.external ? ` target="_blank" rel="noopener noreferrer"` : "";
|
||
return `<li><a href="${esc(l.href)}"${extra}>${esc(l.label)}</a></li>`;
|
||
})
|
||
.join("")}
|
||
</ul>
|
||
</details>
|
||
</nav>
|
||
|
||
<div class="footer-bottom">
|
||
<a class="footer-enamad" href="https://trustseal.enamad.ir/?id=7947600&Code=sNYtwbTKfknWRpuvOh3DdHaeBuD4PWVu" target="_blank" rel="noopener noreferrer" referrerpolicy="origin" aria-label="مشاهده نماد اعتماد الکترونیکی کیکو میکو">
|
||
<img referrerpolicy="origin" src="https://trustseal.enamad.ir/logo.aspx?id=7947600&Code=sNYtwbTKfknWRpuvOh3DdHaeBuD4PWVu" alt="نماد اعتماد الکترونیکی" width="96" height="96" loading="lazy" decoding="async" code="sNYtwbTKfknWRpuvOh3DdHaeBuD4PWVu" />
|
||
</a>
|
||
<p>© ${year} ${esc(dict.brand)} · زیبایی اصیل ایتالیایی در ایران</p>
|
||
${syncHtml}
|
||
</div>
|
||
</div>
|
||
</footer>`;
|
||
}
|
||
|
||
export function sortSelect(current: SearchSort, opts?: { hash?: string; preserve?: Record<string, string | undefined> }) {
|
||
const hidden = Object.entries(opts?.preserve || {})
|
||
.filter(([, v]) => v)
|
||
.map(([k, v]) => `<input type="hidden" name="${esc(k)}" value="${esc(v)}" />`)
|
||
.join("");
|
||
const options = SEARCH_SORT_OPTIONS.map(
|
||
(o) =>
|
||
`<option value="${esc(o.value)}" ${o.value === current ? "selected" : ""}>${esc(o.label)}</option>`
|
||
).join("");
|
||
return `<form method="get" data-hash="${esc(opts?.hash || "")}" hidden class="sort-form-legacy">
|
||
${hidden}
|
||
<div style="display:flex;flex-direction:column;gap:0.5rem">
|
||
<label for="sort-select" class="text-muted" style="font-size:0.75rem">مرتبسازی</label>
|
||
<select id="sort-select" name="sort" data-sort-select>${options}</select>
|
||
</div>
|
||
</form>`;
|
||
}
|
||
|
||
function filterIconSvg() {
|
||
return `<svg class="filters-icon" viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" aria-hidden="true">
|
||
<line x1="4" y1="7" x2="20" y2="7"/><circle cx="9" cy="7" r="2.25" fill="currentColor" stroke="none"/>
|
||
<line x1="4" y1="12" x2="20" y2="12"/><circle cx="15" cy="12" r="2.25" fill="currentColor" stroke="none"/>
|
||
<line x1="4" y1="17" x2="20" y2="17"/><circle cx="11" cy="17" r="2.25" fill="currentColor" stroke="none"/>
|
||
</svg>`;
|
||
}
|
||
|
||
export type ProductListingFiltersOpts = {
|
||
megaMenu: MegaMenuRoot[];
|
||
productCount: number;
|
||
currentSort?: SearchSort;
|
||
/** Category root slug when on a category page */
|
||
categorySlug?: string | null;
|
||
currentSub?: string | null;
|
||
mode: "collection" | "category" | "search" | "offers" | "home";
|
||
preserve?: Record<string, string | undefined>;
|
||
hash?: string;
|
||
/** Prefer client-side apply (static builds) */
|
||
clientMode?: boolean;
|
||
};
|
||
|
||
/** KIKO-style filter trigger + overlay for product listing pages. */
|
||
export function productListingFilters(opts: ProductListingFiltersOpts) {
|
||
const sort = opts.currentSort || "newest";
|
||
const countFa = opts.productCount.toLocaleString("fa-IR");
|
||
const client = opts.clientMode ? "1" : "0";
|
||
const preserveJson = esc(JSON.stringify(opts.preserve || {}));
|
||
|
||
const categoryOptions: string[] = [];
|
||
if (opts.mode === "category" && opts.categorySlug) {
|
||
const root = opts.megaMenu.find((r) => r.key === opts.categorySlug);
|
||
if (root) {
|
||
const rootSelected = !opts.currentSub ? " is-selected" : "";
|
||
categoryOptions.push(
|
||
`<button type="button" class="filters-option${rootSelected}" data-filter-option="category" data-href="${esc(root.href)}" data-label="${esc(root.label)}" data-root="${esc(root.key)}" data-count="${root.children.reduce((n, c) => n + c.count, 0) || ""}">
|
||
<span class="filters-option-check" aria-hidden="true"></span>
|
||
<span class="filters-option-label">همهٔ ${esc(root.label)}</span>
|
||
</button>`
|
||
);
|
||
for (const child of root.children) {
|
||
const selected =
|
||
opts.currentSub &&
|
||
(opts.currentSub === child.label ||
|
||
opts.currentSub === child.key ||
|
||
child.href.includes(encodeURIComponent(opts.currentSub)))
|
||
? " is-selected"
|
||
: "";
|
||
categoryOptions.push(
|
||
`<button type="button" class="filters-option${selected}" data-filter-option="category" data-href="${esc(child.href)}" data-label="${esc(child.label)}" data-count="${child.count}" data-root="${esc(root.key)}" data-sub="${esc(child.key)}">
|
||
<span class="filters-option-check" aria-hidden="true"></span>
|
||
<span class="filters-option-label">${esc(child.label)}</span>
|
||
<span class="filters-option-meta">(${esc(child.count.toLocaleString("fa-IR"))})</span>
|
||
</button>`
|
||
);
|
||
}
|
||
// Allow jumping to other roots
|
||
for (const other of opts.megaMenu) {
|
||
if (other.key === opts.categorySlug) continue;
|
||
categoryOptions.push(
|
||
`<button type="button" class="filters-option filters-option--root" data-filter-option="category" data-href="${esc(other.href)}" data-label="${esc(other.label)}" data-root="${esc(other.key)}">
|
||
<span class="filters-option-check" aria-hidden="true"></span>
|
||
<span class="filters-option-label">${esc(other.label)}</span>
|
||
</button>`
|
||
);
|
||
}
|
||
}
|
||
} else {
|
||
for (const root of opts.megaMenu) {
|
||
categoryOptions.push(
|
||
`<button type="button" class="filters-option filters-option--root" data-filter-option="category" data-href="${esc(root.href)}" data-label="${esc(root.label)}" data-root="${esc(root.key)}">
|
||
<span class="filters-option-check" aria-hidden="true"></span>
|
||
<span class="filters-option-label">${esc(root.label)}</span>
|
||
</button>`
|
||
);
|
||
for (const child of root.children) {
|
||
categoryOptions.push(
|
||
`<button type="button" class="filters-option filters-option--child" data-filter-option="category" data-href="${esc(child.href)}" data-label="${esc(child.label)}" data-count="${child.count}" data-root="${esc(root.key)}" data-sub="${esc(child.key)}">
|
||
<span class="filters-option-check" aria-hidden="true"></span>
|
||
<span class="filters-option-label">${esc(child.label)}</span>
|
||
<span class="filters-option-meta">(${esc(child.count.toLocaleString("fa-IR"))})</span>
|
||
</button>`
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
const sortOptions = SEARCH_SORT_OPTIONS.map((o) => {
|
||
const selected = o.value === sort ? " is-selected" : "";
|
||
return `<button type="button" class="filters-option${selected}" data-filter-option="sort" data-value="${esc(o.value)}" data-label="${esc(o.label)}">
|
||
<span class="filters-option-check" aria-hidden="true"></span>
|
||
<span class="filters-option-label">${esc(o.label)}</span>
|
||
</button>`;
|
||
}).join("");
|
||
|
||
const stockAccordion =
|
||
opts.mode === "offers"
|
||
? ""
|
||
: `<details class="filters-acc">
|
||
<summary class="filters-acc-summary">
|
||
<span>موجودی</span>
|
||
<span class="filters-acc-chevron" aria-hidden="true">${icon("chevron")}</span>
|
||
</summary>
|
||
<div class="filters-acc-panel">
|
||
<button type="button" class="filters-option" data-filter-option="stock" data-value="in-stock">
|
||
<span class="filters-option-check" aria-hidden="true"></span>
|
||
<span class="filters-option-label">فقط کالاهای موجود</span>
|
||
</button>
|
||
</div>
|
||
</details>`;
|
||
|
||
const legacySort = sortSelect(sort, {
|
||
hash: opts.hash,
|
||
preserve: opts.preserve,
|
||
});
|
||
|
||
return `<div
|
||
class="listing-filters"
|
||
data-product-listing
|
||
data-listing-mode="${esc(opts.mode)}"
|
||
data-client-mode="${client}"
|
||
data-category-slug="${esc(opts.categorySlug || "")}"
|
||
data-current-sub="${esc(opts.currentSub || "")}"
|
||
data-current-sort="${esc(sort)}"
|
||
data-preserve="${preserveJson}"
|
||
data-hash="${esc(opts.hash || "")}"
|
||
>
|
||
<div class="listing-toolbar">
|
||
<p class="listing-count text-muted"><span data-listing-count>${countFa}</span> محصول</p>
|
||
<button type="button" class="filters-trigger" data-filters-open>
|
||
${filterIconSvg()}
|
||
<span>فیلترها</span>
|
||
</button>
|
||
</div>
|
||
${legacySort}
|
||
|
||
<div class="filters-overlay" data-filters-overlay hidden>
|
||
<button type="button" class="filters-backdrop" data-filters-close aria-label="بستن فیلترها"></button>
|
||
<div class="filters-sheet" role="dialog" aria-modal="true" aria-labelledby="filters-sheet-title" dir="rtl">
|
||
<header class="filters-sheet-head">
|
||
<div class="filters-sheet-brand">
|
||
${filterIconSvg()}
|
||
<h2 id="filters-sheet-title" class="filters-sheet-title">فیلترها</h2>
|
||
</div>
|
||
<button type="button" class="filters-sheet-close" data-filters-close aria-label="بستن">${icon("x")}</button>
|
||
</header>
|
||
|
||
<div class="filters-sheet-body">
|
||
<details class="filters-acc" open>
|
||
<summary class="filters-acc-summary">
|
||
<span>دستهبندی</span>
|
||
<span class="filters-acc-chevron" aria-hidden="true">${icon("chevron")}</span>
|
||
</summary>
|
||
<div class="filters-acc-panel" data-filter-group="category">
|
||
${categoryOptions.join("") || `<p class="filters-empty text-muted">دستهای موجود نیست.</p>`}
|
||
</div>
|
||
</details>
|
||
|
||
<details class="filters-acc">
|
||
<summary class="filters-acc-summary">
|
||
<span>مرتبسازی</span>
|
||
<span class="filters-acc-chevron" aria-hidden="true">${icon("chevron")}</span>
|
||
</summary>
|
||
<div class="filters-acc-panel" data-filter-group="sort">
|
||
${sortOptions}
|
||
</div>
|
||
</details>
|
||
|
||
${stockAccordion}
|
||
</div>
|
||
|
||
<footer class="filters-sheet-foot">
|
||
<button type="button" class="filters-btn filters-btn--clear" data-filters-clear>پاک کردن همه</button>
|
||
<button type="button" class="filters-btn filters-btn--apply" data-filters-apply>
|
||
<span>مشاهده نتایج</span>
|
||
<span class="filters-count-pill" data-filters-result-count>${countFa}</span>
|
||
</button>
|
||
</footer>
|
||
</div>
|
||
</div>
|
||
</div>`;
|
||
}
|
||
|
||
function productHref(slug: string, trailingSlash = false) {
|
||
return `/products/${esc(slug)}${trailingSlash ? "/" : ""}`;
|
||
}
|
||
|
||
function cardIsInStock(p: {
|
||
stock?: number;
|
||
colorVariants?: ColorVariant[];
|
||
}): boolean {
|
||
if (p.colorVariants && p.colorVariants.length > 1) {
|
||
return p.colorVariants.some((v) => v.stock > 0);
|
||
}
|
||
return (p.stock ?? 0) > 0;
|
||
}
|
||
|
||
function stockBadgeHtml(p: {
|
||
stock?: number;
|
||
colorVariants?: ColorVariant[];
|
||
}) {
|
||
if (cardIsInStock(p)) return "";
|
||
return `<span class="stock-badge stock-badge--oos">نا موجود</span>`;
|
||
}
|
||
|
||
function cardShadeDots(
|
||
variants: ColorVariant[] | undefined,
|
||
activeSlug: string | undefined,
|
||
opts?: { trailingSlash?: boolean }
|
||
) {
|
||
if (!variants || variants.length < 2) return "";
|
||
// Mobile cards: show up to 5 shades, then +N عدد for the rest
|
||
const shown = variants.slice(0, 5);
|
||
const extra = variants.length - shown.length;
|
||
const slash = opts?.trailingSlash ? "/" : "";
|
||
return `<div class="shade-dots" role="listbox" aria-label="رنگ">
|
||
${shown
|
||
.map((v) => {
|
||
const active = activeSlug && v.slug === activeSlug ? " is-active" : "";
|
||
const bg = v.heroImage
|
||
? `background-image:url('${esc(v.heroImage)}')`
|
||
: "";
|
||
const href = `/products/${esc(v.slug)}${slash}`;
|
||
return `<button
|
||
type="button"
|
||
class="shade-dot${active}"
|
||
style="${bg}"
|
||
title="${esc(v.shade)}"
|
||
aria-label="${esc(v.shade)}"
|
||
aria-selected="${active ? "true" : "false"}"
|
||
data-card-shade
|
||
data-slug="${esc(v.slug)}"
|
||
data-image="${esc(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>`;
|
||
}
|
||
|
||
type CardFilterMeta = {
|
||
featured?: boolean;
|
||
updatedAt?: Date | string | null;
|
||
ratingAvg?: number;
|
||
reviewCount?: number;
|
||
reviews?: Array<{ rating: number }>;
|
||
_count?: { reviews?: number };
|
||
compareAtPrice?: number | null;
|
||
discountPercent?: number;
|
||
};
|
||
|
||
function cardRating(p: CardFilterMeta) {
|
||
if (typeof p.ratingAvg === "number") return p.ratingAvg;
|
||
if (p.reviews?.length) {
|
||
const sum = p.reviews.reduce((a, r) => a + r.rating, 0);
|
||
return sum / p.reviews.length;
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
function cardReviewCount(p: CardFilterMeta) {
|
||
if (typeof p.reviewCount === "number") return p.reviewCount;
|
||
if (typeof p._count?.reviews === "number") return p._count.reviews;
|
||
return p.reviews?.length ?? 0;
|
||
}
|
||
|
||
function cardUpdatedMs(p: CardFilterMeta) {
|
||
if (!p.updatedAt) return 0;
|
||
const t = new Date(p.updatedAt).getTime();
|
||
return Number.isFinite(t) ? t : 0;
|
||
}
|
||
|
||
function cardPriceBounds(p: {
|
||
price: number;
|
||
colorVariants?: ColorVariant[];
|
||
}) {
|
||
const prices = [Number(p.price) || 0];
|
||
for (const v of p.colorVariants || []) {
|
||
const n = Number(v.price);
|
||
if (Number.isFinite(n)) prices.push(n);
|
||
}
|
||
return { min: Math.min(...prices), max: Math.max(...prices) };
|
||
}
|
||
|
||
function cardFilterDataAttrs(
|
||
p: {
|
||
name: string;
|
||
price: number;
|
||
category: string | null;
|
||
stock?: number;
|
||
colorVariants?: ColorVariant[];
|
||
} & CardFilterMeta
|
||
) {
|
||
const discount =
|
||
p.discountPercent ??
|
||
(p.compareAtPrice && p.compareAtPrice > p.price
|
||
? Math.round((1 - p.price / p.compareAtPrice) * 100)
|
||
: 0);
|
||
const { min, max } = cardPriceBounds(p);
|
||
return [
|
||
`data-category="${esc(p.category || "")}"`,
|
||
`data-category-fa="${esc(persianCategoryLabel(p.category, p.name) || "")}"`,
|
||
`data-price="${p.price}"`,
|
||
`data-price-min="${min}"`,
|
||
`data-price-max="${max}"`,
|
||
`data-name="${esc(p.name)}"`,
|
||
`data-updated="${cardUpdatedMs(p)}"`,
|
||
`data-rating="${cardRating(p)}"`,
|
||
`data-reviews="${cardReviewCount(p)}"`,
|
||
`data-featured="${p.featured ? 1 : 0}"`,
|
||
`data-in-stock="${cardIsInStock(p) ? 1 : 0}"`,
|
||
`data-discount="${discount}"`,
|
||
].join(" ");
|
||
}
|
||
|
||
function discountPercent(price: number, compareAt: number | null | undefined) {
|
||
if (!compareAt || compareAt <= price) return 0;
|
||
return Math.round((1 - price / compareAt) * 100);
|
||
}
|
||
|
||
/** Highest visible discount for a listing card (canonical + shade dots). */
|
||
function listingDiscountPercent(
|
||
p: CardFilterMeta & { price: number; colorVariants?: ColorVariant[] }
|
||
) {
|
||
if (p.discountPercent && p.discountPercent >= 1) return p.discountPercent;
|
||
let best = discountPercent(p.price, p.compareAtPrice ?? null);
|
||
for (const v of p.colorVariants || []) {
|
||
best = Math.max(best, discountPercent(v.price, v.compareAtPrice ?? null));
|
||
}
|
||
return best;
|
||
}
|
||
|
||
/** Badge + strike price — used on category, home, search, and offers cards. */
|
||
function cardDiscountBadgeHtml(pct: number) {
|
||
if (pct < 1) return "";
|
||
return `<span class="offer-badge">−${pct}%</span>`;
|
||
}
|
||
|
||
/** PDP gallery with optional discount pill on the hero image. */
|
||
export function pdpGallerySection(images: string[], discountPct: number) {
|
||
if (!images.length) return "";
|
||
const badge =
|
||
discountPct >= 1
|
||
? `<span class="offer-badge" data-pdp-discount-badge>−${discountPct}%</span>`
|
||
: `<span class="offer-badge" data-pdp-discount-badge hidden></span>`;
|
||
return `<div class="pdp-gallery-shell"${
|
||
discountPct >= 1 ? ` data-pdp-discount="${discountPct}"` : ""
|
||
}>
|
||
${badge}
|
||
<div class="gallery-grid" data-variant-gallery>
|
||
${images
|
||
.map((src) => `<img src="${esc(src)}" alt="" loading="lazy" />`)
|
||
.join("")}
|
||
</div>
|
||
</div>`;
|
||
}
|
||
|
||
function cardPriceHtml(price: number, compareAt?: number | null) {
|
||
const pct = discountPercent(price, compareAt);
|
||
if (pct < 1 || !compareAt) {
|
||
return `<p class="price" data-card-price>${esc(formatPrice(price))}</p>`;
|
||
}
|
||
return `<p class="price price--offer" data-card-price>
|
||
<span class="price-now">${esc(formatPrice(price))}</span>
|
||
<span class="price-was">${esc(formatPrice(compareAt))}</span>
|
||
</p>`;
|
||
}
|
||
|
||
export function productGridCard(
|
||
p: {
|
||
id: string;
|
||
name: string;
|
||
nameFa?: string | null;
|
||
slug: string;
|
||
price: number;
|
||
heroImage: string | null;
|
||
category: string | null;
|
||
stock?: number;
|
||
colorVariants?: ColorVariant[];
|
||
} & CardFilterMeta,
|
||
opts?: { trailingSlash?: boolean }
|
||
) {
|
||
const hasVariants = (p.colorVariants?.length || 0) > 1;
|
||
const title = productDisplayTitle(p, hasVariants);
|
||
const href = productHref(p.slug, opts?.trailingSlash);
|
||
const pct = listingDiscountPercent(p);
|
||
return `<article class="product-card${pct >= 1 ? " product-card--offer" : ""}" data-product-card ${cardFilterDataAttrs({ ...p, discountPercent: pct })}>
|
||
<a class="thumb" href="${href}" data-card-link>
|
||
${cardDiscountBadgeHtml(pct)}
|
||
${stockBadgeHtml(p)}
|
||
${
|
||
p.heroImage
|
||
? `<img src="${esc(p.heroImage)}" alt="${esc(title)}" loading="lazy" data-card-image />`
|
||
: ""
|
||
}</a>
|
||
<div class="meta">
|
||
${loveYourEyesPillHtml(p.slug)}
|
||
<p class="name"><a href="${href}" data-card-link>${esc(title)}</a></p>
|
||
${cardShadeDots(p.colorVariants, p.slug, opts)}
|
||
<div class="card-action-row">
|
||
${cardPriceHtml(p.price, p.compareAtPrice)}
|
||
<button type="button" class="card-add-btn" data-add-to-cart data-product-id="${esc(p.id)}" data-slug="${esc(p.slug)}" data-name="${esc(title)}" data-price="${p.price}" data-image="${esc(p.heroImage || "")}" data-category="${esc(p.category || "")}" data-category-fa="${esc(persianCategoryLabel(p.category, p.name) || "")}" 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>`;
|
||
}
|
||
|
||
/** Compact top matches on the search results page (first 4 hits). */
|
||
export function searchQuickPreviewCard(
|
||
p: {
|
||
name: string;
|
||
slug: string;
|
||
price: number;
|
||
heroImage: string | null;
|
||
category?: string | null;
|
||
colorVariants?: ColorVariant[];
|
||
},
|
||
opts?: { trailingSlash?: boolean }
|
||
) {
|
||
const hasVariants = (p.colorVariants?.length || 0) > 1;
|
||
const title = productDisplayTitle(p, hasVariants);
|
||
const href = productHref(p.slug, opts?.trailingSlash);
|
||
return `<a class="search-quick-card" href="${href}">
|
||
<span class="search-quick-thumb">${
|
||
p.heroImage
|
||
? `<img src="${esc(p.heroImage)}" alt="${esc(title)}" loading="lazy" />`
|
||
: ""
|
||
}</span>
|
||
<span class="search-quick-meta">
|
||
<span class="search-quick-name">${esc(title)}</span>
|
||
<span class="search-quick-price">${esc(formatPrice(p.price))}</span>
|
||
</span>
|
||
</a>`;
|
||
}
|
||
|
||
/** Related products on PDP — same shade-dot behavior as listing cards. */
|
||
export function relatedProductCard(
|
||
p: {
|
||
id: string;
|
||
name: string;
|
||
nameFa?: string | null;
|
||
slug: string;
|
||
price: number;
|
||
compareAtPrice?: number | null;
|
||
heroImage: string | null;
|
||
category: string | null;
|
||
tagline?: string | null;
|
||
stock?: number;
|
||
colorVariants?: ColorVariant[];
|
||
},
|
||
opts?: { trailingSlash?: boolean }
|
||
) {
|
||
const hasVariants = (p.colorVariants?.length || 0) > 1;
|
||
const title = productDisplayTitle(p, hasVariants);
|
||
const href = productHref(p.slug, opts?.trailingSlash);
|
||
const pct = listingDiscountPercent(p);
|
||
return `<article class="related-card product-card${pct >= 1 ? " product-card--offer" : ""} reveal" data-product-card>
|
||
<a class="thumb" href="${href}" data-card-link>
|
||
${cardDiscountBadgeHtml(pct)}
|
||
${stockBadgeHtml(p)}
|
||
${
|
||
p.heroImage
|
||
? `<img src="${esc(p.heroImage)}" alt="${esc(title)}" loading="lazy" data-card-image />`
|
||
: ""
|
||
}
|
||
</a>
|
||
<div class="meta">
|
||
${loveYourEyesPillHtml(p.slug)}
|
||
<h3 class="name"><a href="${href}" data-card-link>${esc(title)}</a></h3>
|
||
${cardShadeDots(p.colorVariants, p.slug, opts)}
|
||
${
|
||
p.tagline && !hasVariants
|
||
? `<p class="text-muted card-tagline">${esc(p.tagline)}</p>`
|
||
: ""
|
||
}
|
||
<div class="card-action-row">
|
||
${cardPriceHtml(p.price, p.compareAtPrice)}
|
||
</div>
|
||
</div>
|
||
</article>`;
|
||
}
|
||
|
||
export function listProductCard(
|
||
p: {
|
||
id: string;
|
||
name: string;
|
||
nameFa?: string | null;
|
||
slug: string;
|
||
price: number;
|
||
heroImage: string | null;
|
||
category?: string | null;
|
||
tagline?: string | null;
|
||
stock?: number;
|
||
colorVariants?: ColorVariant[];
|
||
} & CardFilterMeta,
|
||
opts?: { trailingSlash?: boolean }
|
||
) {
|
||
// Same card chrome as home grid: shade dots, aligned price, add-to-cart
|
||
return productGridCard(
|
||
{
|
||
id: p.id,
|
||
name: p.name,
|
||
nameFa: p.nameFa,
|
||
slug: p.slug,
|
||
price: p.price,
|
||
heroImage: p.heroImage,
|
||
category: p.category ?? null,
|
||
stock: p.stock,
|
||
colorVariants: p.colorVariants,
|
||
featured: p.featured,
|
||
updatedAt: p.updatedAt,
|
||
ratingAvg: p.ratingAvg,
|
||
reviewCount: p.reviewCount,
|
||
reviews: p.reviews,
|
||
_count: p._count,
|
||
compareAtPrice: p.compareAtPrice,
|
||
discountPercent: p.discountPercent,
|
||
},
|
||
opts
|
||
);
|
||
}
|
||
|
||
/** Horizontal “most selling” strip before the full collection grid. */
|
||
export function bestsellersSection(
|
||
title: string,
|
||
products: Array<{
|
||
id: string;
|
||
name: string;
|
||
slug: string;
|
||
price: number;
|
||
compareAtPrice?: number | null;
|
||
heroImage: string | null;
|
||
category: string | null;
|
||
discountPercent?: number;
|
||
colorVariants?: ColorVariant[];
|
||
} & CardFilterMeta>,
|
||
opts?: { trailingSlash?: boolean }
|
||
) {
|
||
if (!products.length) return "";
|
||
return `<section class="bestsellers-wrap" id="bestsellers" data-product-slider aria-label="${esc(title)}">
|
||
<div class="bestsellers-head">
|
||
<h2 class="bestsellers-title">${esc(title)}</h2>
|
||
</div>
|
||
<div class="offers-slider">
|
||
<div class="bestsellers-rail" data-slider-rail>
|
||
${products.map((p) => productGridCard(p, opts)).join("")}
|
||
</div>
|
||
<div class="slider-dots" data-slider-dots role="tablist" aria-label="صفحههای اسلایدر"></div>
|
||
</div>
|
||
</section>`;
|
||
}
|
||
|
||
export function offerProductCard(
|
||
p: {
|
||
id: string;
|
||
name: string;
|
||
slug: string;
|
||
price: number;
|
||
compareAtPrice?: number | null;
|
||
heroImage: string | null;
|
||
category: string | null;
|
||
stock?: number;
|
||
discountPercent?: number;
|
||
colorVariants?: ColorVariant[];
|
||
} & CardFilterMeta,
|
||
opts?: { trailingSlash?: boolean }
|
||
) {
|
||
return productGridCard(p, opts);
|
||
}
|
||
|
||
/** Discounted products slider with under-rail dots. */
|
||
export function offersSliderSection(
|
||
title: string,
|
||
products: Array<{
|
||
id: string;
|
||
name: string;
|
||
slug: string;
|
||
price: number;
|
||
compareAtPrice?: number | null;
|
||
heroImage: string | null;
|
||
category: string | null;
|
||
discountPercent?: number;
|
||
colorVariants?: ColorVariant[];
|
||
}>,
|
||
opts?: { trailingSlash?: boolean }
|
||
) {
|
||
if (!products.length) return "";
|
||
return `<section class="bestsellers-wrap offers-wrap" data-product-slider aria-label="${esc(title)}">
|
||
<div class="bestsellers-head">
|
||
<h2 class="bestsellers-title">${esc(title)}</h2>
|
||
</div>
|
||
<div class="offers-slider">
|
||
<div class="bestsellers-rail offers-rail" data-slider-rail>
|
||
${products.map((p) => offerProductCard(p, opts)).join("")}
|
||
</div>
|
||
<div class="slider-dots" data-slider-dots role="tablist" aria-label="صفحههای اسلایدر"></div>
|
||
</div>
|
||
</section>`;
|
||
}
|
||
|
||
/** Luxury editorial hero — now rendered inside sticky scrub (kept for compatibility) */
|
||
export function luxuryHeroHtml() {
|
||
return "";
|
||
}
|
||
|
||
/** Sticky scroll-scrub: motion video (left) + 3 editorial story cards (right) */
|
||
export function atelierHeroHtml(_images: string[] = []) {
|
||
const stories: Array<{
|
||
id: string;
|
||
titleHtml: string;
|
||
body: string;
|
||
cta: string;
|
||
href: string;
|
||
}> = [
|
||
{
|
||
id: "0",
|
||
titleHtml: "درخشش تکرار نشدنی که<br /><strong>از ایتالیا میآید</strong>",
|
||
body: "بافتهای شفاف، نور نرم روی پوست — زیبایی که مثل خاطرهای از میلان میماند.",
|
||
cta: "ورود به دنیای کالکشن",
|
||
href: "/collection/",
|
||
},
|
||
{
|
||
id: "1",
|
||
titleHtml: "اورجینال،<br /><strong>بیواسطه و ناب</strong>",
|
||
body: "مستقیم از فروشگاه رسمی KIKO — هر شید، همان جادویی که در ایتالیا لمس میشود.",
|
||
cta: "کشف محصولات اورجینال",
|
||
href: "/collection/",
|
||
},
|
||
{
|
||
id: "2",
|
||
titleHtml: "لحظههایی که<br /><strong>میدرخشند</strong>",
|
||
body: "از برق لب تا نگاه ماندگار — محبوبترین انتخابها برای روزهای روشن و شبهای خاص.",
|
||
cta: "دیدن پرفروشترینها",
|
||
href: "#bestsellers",
|
||
},
|
||
];
|
||
|
||
const cards = stories
|
||
.map(
|
||
(s, i) => `<article
|
||
class="atelier-story${i === 0 ? " is-active" : ""}"
|
||
data-atelier-feat="${s.id}"
|
||
aria-hidden="${i === 0 ? "false" : "true"}"
|
||
>
|
||
<h2 class="atelier-story-title">${s.titleHtml}</h2>
|
||
<p class="atelier-story-body">${esc(s.body)}</p>
|
||
<a class="atelier-story-cta" href="${esc(s.href)}"><span>${esc(s.cta)}</span></a>
|
||
</article>`
|
||
)
|
||
.join("");
|
||
|
||
const dots = stories
|
||
.map(
|
||
(_, i) =>
|
||
`<span class="atelier-dot${i === 0 ? " is-active" : ""}" data-atelier-dot="${i}" aria-hidden="true"></span>`
|
||
)
|
||
.join("");
|
||
|
||
return `<section class="atelier-hero atelier-hero--split atelier-hero--story" data-atelier data-atelier-story aria-label="معرفی فروشگاه و موشن">
|
||
<div class="atelier-pin">
|
||
<div class="atelier-bg" aria-hidden="true"></div>
|
||
|
||
<div class="atelier-rail" aria-hidden="true">
|
||
<div class="atelier-rail-line"><div class="atelier-rail-fill" data-atelier-fill></div></div>
|
||
<div class="atelier-dots">${dots}</div>
|
||
</div>
|
||
|
||
<div class="atelier-split">
|
||
<div class="atelier-split-video" aria-hidden="true">
|
||
<div class="atelier-motion" data-atelier-motion>
|
||
<div class="atelier-motion-screen">
|
||
<video
|
||
data-atelier-video
|
||
data-src="/videos/kiko-hero-motion-v3.mp4"
|
||
src="/videos/kiko-hero-motion-v3.mp4"
|
||
poster="/videos/kiko-hero-poster.jpg?v=3"
|
||
muted
|
||
playsinline
|
||
webkit-playsinline
|
||
disablepictureinpicture
|
||
controlslist="nodownload nofullscreen noremoteplayback"
|
||
preload="metadata"
|
||
aria-label="موشن معرفی برند"
|
||
></video>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="atelier-split-story" dir="rtl">
|
||
<div class="atelier-story-stack">${cards}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<p class="atelier-hint" data-atelier-hint>اسکرول کنید تا موشن پخش شود</p>
|
||
</div>
|
||
<div class="atelier-track atelier-track--story" aria-hidden="true"></div>
|
||
</section>`;
|
||
}
|
||
|
||
/** Trust / selling points */
|
||
export function trustSection() {
|
||
const items = [
|
||
{ t: "واردات مستقیم از ایتالیا", d: "بدون واسطه، از مبدأ تا درب منزل" },
|
||
{ t: "تضمین اصالت کالا", d: "واردات از ایتالیا با تعهد به اصالت KIKO" },
|
||
{ t: "ارسال سریع", d: "بستهبندی امن و تحویل بهموقع" },
|
||
{ t: "پشتیبانی تخصصی", d: "مشاوره زیبایی توسط تیم متخصص" },
|
||
];
|
||
return `<section class="trust-sec" aria-label="اعتماد و اصالت">
|
||
<div class="trust-grid">
|
||
${items
|
||
.map(
|
||
(i) => `<div class="trust-item">
|
||
<span class="trust-check" aria-hidden="true">✓</span>
|
||
<div>
|
||
<h3 class="trust-title">${i.t}</h3>
|
||
<p class="trust-desc">${i.d}</p>
|
||
</div>
|
||
</div>`
|
||
)
|
||
.join("")}
|
||
</div>
|
||
</section>`;
|
||
}
|
||
|
||
/** Why us — advantages */
|
||
export function whyUsSection() {
|
||
const rows = [
|
||
{
|
||
n: "۰۱",
|
||
t: "متخصص کیکو میلانو",
|
||
d: "تمرکز کامل ما روی برند KIKO است؛ شناخت عمیق شیدها، فرمولها و کالکشنهای جدید.",
|
||
},
|
||
{
|
||
n: "۰۲",
|
||
t: "قیمت رقابتی",
|
||
d: "با واردات مستقیم، ارزش واقعی محصول اروپایی را با قیمت منصفانه دریافت میکنید.",
|
||
},
|
||
{
|
||
n: "۰۳",
|
||
t: "تجربه خرید لوکس",
|
||
d: "از انتخاب رنگ تا بستهبندی، هر جزئیات برای حسی شبیه بوتیکهای زیبایی طراحی شده است.",
|
||
},
|
||
];
|
||
return `<section class="why-sec" aria-label="چرا کیکومیکو">
|
||
<div class="why-inner">
|
||
<header class="sec-head">
|
||
<h2 class="sec-title">چرا <strong>کیکومیکو</strong>؟</h2>
|
||
<p class="sec-sub">اعتماد، اصالت و زیبایی — سه اصل تجربه خرید شما.</p>
|
||
</header>
|
||
<div class="why-grid">
|
||
${rows
|
||
.map(
|
||
(r) => `<article class="why-card">
|
||
<span class="why-num">${r.n}</span>
|
||
<h3>${r.t}</h3>
|
||
<p>${r.d}</p>
|
||
</article>`
|
||
)
|
||
.join("")}
|
||
</div>
|
||
</div>
|
||
</section>`;
|
||
}
|
||
|
||
/** Persian customer reviews */
|
||
export function reviewsSection() {
|
||
const reviews = [
|
||
{
|
||
q: "سایه چشم متالیکشون روی پلک پخش نمیشه و رنگش دقیقاً همون چیزیه که تو عکس دیدم. بستهبندی هم خیلی شیک بود.",
|
||
n: "الهام ن.",
|
||
c: "کرج",
|
||
},
|
||
{
|
||
q: "برای اولین بار شید کانسیلر رو درست انتخاب کردم؛ عکسای سایت واقعی بودن و مشاوره واتساپ هم خیلی کمکم کرد.",
|
||
n: "یاسمن ف.",
|
||
c: "مشهد",
|
||
},
|
||
{
|
||
q: "پک مراقبت پوست که سفارش دادم اورجینال رسید، پلمب سالم. حس میکنم مستقیم از بوتیک میلان اومده.",
|
||
n: "آیدا ح.",
|
||
c: "رشت",
|
||
},
|
||
];
|
||
return `<section class="reviews-sec" aria-label="نظرات مشتریان">
|
||
<header class="sec-head">
|
||
<h2 class="sec-title">تجربه <strong>واقعی</strong></h2>
|
||
</header>
|
||
<div class="reviews-grid">
|
||
${reviews
|
||
.map(
|
||
(r) => `<blockquote class="review-card">
|
||
<p>«${esc(r.q)}»</p>
|
||
<footer><strong>${esc(r.n)}</strong> · ${esc(r.c)}</footer>
|
||
</blockquote>`
|
||
)
|
||
.join("")}
|
||
</div>
|
||
</section>`;
|
||
}
|
||
|
||
/** FAQ accordion — pure HTML details */
|
||
export function faqSection() {
|
||
return `<section class="faq-sec" aria-label="سوالات متداول">
|
||
<header class="sec-head">
|
||
<h2 class="sec-title">سوالات <strong>متداول</strong></h2>
|
||
</header>
|
||
<div class="faq-list">
|
||
${STORE_FAQS
|
||
.map(
|
||
(f) => `<details class="faq-item">
|
||
<summary>${f.q}</summary>
|
||
<p>${f.a}</p>
|
||
</details>`
|
||
)
|
||
.join("")}
|
||
</div>
|
||
</section>`;
|
||
}
|
||
|
||
/**
|
||
* Collaboration / campaign slider — Cavalli, Candy Crush, Club K.
|
||
* Visually distinct from the full-bleed promo hero under the atelier section.
|
||
*/
|
||
export function collabBannersSection(opts?: { trailingSlash?: boolean }) {
|
||
const slash = opts?.trailingSlash ? "/" : "";
|
||
const slides: Array<{
|
||
id: string;
|
||
theme: "cavalli" | "candy" | "club";
|
||
titleHtml: string;
|
||
sub: string;
|
||
cta: string;
|
||
href: string;
|
||
durationMs: number;
|
||
media: { src: string; mobileSrc?: string; alt: string };
|
||
}> = [
|
||
{
|
||
id: "club-k",
|
||
theme: "club",
|
||
titleHtml: "Club K × KIKO",
|
||
sub: "کالکشن Club K — ارزون ترین لوازم آرایشی که میتونی از کیکو بخری",
|
||
cta: "خرید محصولات Club K",
|
||
href: `/search/?q=${encodeURIComponent("Club K")}`,
|
||
durationMs: 7000,
|
||
media: {
|
||
alt: "Club K by KIKO Milano",
|
||
src: "https://images.kikocosmetics.com/lf2wbbijeo86/5DskZohflWHTTr99mVUKjU/a895a497ac86bf484d6f2cac7f6c4bcc/Launch_ClubK_HP_Slider_Desktop.jpg?w=1920&fm=webp&q=90",
|
||
mobileSrc:
|
||
"https://images.kikocosmetics.com/lf2wbbijeo86/4tclBcGmSsaaMO87jj5jlp/5d1eeb0e56e3f644c8c307c13908503f/Launch_ClubK_HP_Slider-Mobile.jpg?w=1080&fm=webp&q=90",
|
||
},
|
||
},
|
||
{
|
||
id: "candy-crush",
|
||
theme: "candy",
|
||
titleHtml: "Candy Crush × KIKO",
|
||
sub: "کالکشن محدود کندی کراش — رنگ و بازی",
|
||
cta: "خرید کالکشن کندی کراش",
|
||
href: `/search/?q=${encodeURIComponent("Candy Crush")}`,
|
||
durationMs: 7000,
|
||
media: {
|
||
alt: "Candy Crush x KIKO Milano limited edition",
|
||
src: "/images/collab/candy-crush-banner.jpg",
|
||
mobileSrc: "/images/collab/candy-crush-banner-mobile.jpg",
|
||
},
|
||
},
|
||
{
|
||
id: "cavalli-fusion",
|
||
theme: "cavalli",
|
||
titleHtml: "Roberto Cavalli × KIKO",
|
||
sub: "جاست کاوالی فیوژن گلو — نسخه لیمیت ادیشن",
|
||
cta: "خرید کالکشن کاوالی",
|
||
href: "/search/?q=just+cavalli",
|
||
durationMs: 7000,
|
||
media: {
|
||
alt: "Just Cavalli x KIKO Milano limited edition",
|
||
src: "https://images.kikocosmetics.com/lf2wbbijeo86/6iTfXHCAkMpJo8KbpMtua0/113a1ec89f923f6339c2c7d8457eac7d/LimitedEdition_JustCavali_HP_Slider_Desktop_Products_03.webp?w=1920&fm=webp&q=90",
|
||
mobileSrc:
|
||
"https://images.kikocosmetics.com/lf2wbbijeo86/2LayvhGDaFD0rDvendGRxi/3a29ed6812d0b7910e29c3d70b651c60/LimitedEdition_JustCavallixKIKO_HP_Slider_Mobile.gif?w=1080&fm=webp&q=90",
|
||
},
|
||
},
|
||
];
|
||
|
||
const slidesHtml = slides
|
||
.map((s, i) => {
|
||
const media = `<picture>
|
||
${
|
||
s.media.mobileSrc
|
||
? `<source media="(max-width: 767px)" srcset="${esc(s.media.mobileSrc)}" />`
|
||
: ""
|
||
}
|
||
<img
|
||
class="cbs-media-el"
|
||
src="${esc(s.media.src)}"
|
||
alt="${esc(s.media.alt)}"
|
||
width="1920"
|
||
height="1080"
|
||
decoding="async"
|
||
${i === 0 ? 'fetchpriority="high"' : 'loading="lazy"'}
|
||
/>
|
||
</picture>`;
|
||
|
||
return `<article
|
||
class="cbs-slide cbs-slide--${s.theme}${i === 0 ? " is-active" : ""}"
|
||
data-collab-slide="${i}"
|
||
data-cbs-duration="${s.durationMs}"
|
||
data-cbs-id="${esc(s.id)}"
|
||
aria-hidden="${i === 0 ? "false" : "true"}"
|
||
>
|
||
<div class="cbs-media" aria-hidden="true">${media}</div>
|
||
<div class="cbs-band" dir="rtl">
|
||
<div class="cbs-band-copy">
|
||
<h2 class="cbs-title">${s.titleHtml}</h2>
|
||
<p class="cbs-sub">${esc(s.sub)}</p>
|
||
</div>
|
||
<a class="cbs-cta" href="${esc(s.href)}">${esc(s.cta)}</a>
|
||
</div>
|
||
</article>`;
|
||
})
|
||
.join("");
|
||
|
||
const dots = slides
|
||
.map(
|
||
(_, i) =>
|
||
`<button type="button" class="slider-dot cbs-dot${i === 0 ? " is-active" : ""}" data-collab-goto="${i}" aria-label="اسلاید ${i + 1}" aria-current="${i === 0 ? "true" : "false"}"></button>`
|
||
)
|
||
.join("");
|
||
|
||
return `<section class="collab-banners" data-collab-banners aria-roledescription="carousel" aria-label="همکاری کیکو با سایر برندها - نسخههای لیمیت ادیشن">
|
||
<div class="cbs-frame">
|
||
<header class="cbs-caption" dir="rtl">
|
||
<h2 class="cbs-caption-text">همکاری کیکو با سایر برندها - نسخههای لیمیت ادیشن</h2>
|
||
</header>
|
||
<div class="cbs-track">${slidesHtml}</div>
|
||
<div class="cbs-controls">
|
||
<button type="button" class="cbs-prev" data-collab-prev aria-label="اسلاید قبلی">${icon("chevron-start")}</button>
|
||
</div>
|
||
</div>
|
||
<div class="cbs-dots slider-dots" role="tablist" aria-label="انتخاب اسلاید">${dots}</div>
|
||
</section>`;
|
||
}
|
||
|
||
/** Magazine-style beauty inspiration (static, CSS-only) */
|
||
export function beautyInspirationBanner() {
|
||
return `<section class="inspire-sec" aria-label="الهام زیبایی">
|
||
<div class="inspire-inner">
|
||
<div class="inspire-copy">
|
||
<h2 class="inspire-title">آرایش ایتالیایی،<br /><strong>سبک ایرانی</strong></h2>
|
||
<p class="inspire-sub">از بافتهای شفاف لب تا سایههای مخملی چشم — کالکشنهایی که برای درخشش روزمره و شبهای خاص ساخته شدهاند.</p>
|
||
<a class="btn lux-btn-primary" href="/collection/">کشف کالکشن</a>
|
||
</div>
|
||
<div class="inspire-panels" aria-hidden="true">
|
||
<div class="inspire-panel inspire-panel--a"><span>GLOSS</span></div>
|
||
<div class="inspire-panel inspire-panel--b"><span>NUDE</span></div>
|
||
<div class="inspire-panel inspire-panel--c"><span>GOLD</span></div>
|
||
<div class="inspire-panel inspire-panel--d"><span>ROSE</span></div>
|
||
<div class="inspire-panel inspire-panel--e"><span>BERRY</span></div>
|
||
<div class="inspire-panel inspire-panel--f"><span>IVORY</span></div>
|
||
</div>
|
||
</div>
|
||
</section>`;
|
||
}
|
||
|
||
/** KIKO homepage “Shop by Category”: Lips, Eyes, Face, Skin, Accessories */
|
||
export function kikoShopByCategoryItems(
|
||
megaMenu: Array<{
|
||
key: string;
|
||
featured: Array<{ heroImage: string | null; category: string | null }>;
|
||
children?: Array<{ key: string; href: string }>;
|
||
}>
|
||
): Array<{ label: string; href: string; img?: string | null }> {
|
||
const makeup = megaMenu.find((m) => m.key === "make-up");
|
||
const skin = megaMenu.find((m) => m.key === "skin-care");
|
||
const acc = megaMenu.find((m) => m.key === "accessories");
|
||
|
||
const imgFor = (
|
||
root:
|
||
| { featured: Array<{ heroImage: string | null; category: string | null }> }
|
||
| undefined,
|
||
needles: string | string[]
|
||
) => {
|
||
const list = Array.isArray(needles) ? needles : [needles];
|
||
const featured = root?.featured ?? [];
|
||
for (const needle of list) {
|
||
const upper = needle.toUpperCase();
|
||
const hit = featured.find((p) =>
|
||
(p.category || "").toUpperCase().includes(upper)
|
||
);
|
||
if (hit?.heroImage) return hit.heroImage;
|
||
}
|
||
return featured.find((p) => p.heroImage)?.heroImage ?? null;
|
||
};
|
||
|
||
return [
|
||
{
|
||
label: "لب",
|
||
href: "/category/make-up/sub/lips-make-up/",
|
||
img: imgFor(makeup, ["LIPS MAKE-UP", "LIPS", "LIP"]),
|
||
},
|
||
{
|
||
label: "چشم",
|
||
href: "/category/make-up/sub/eyes-make-up/",
|
||
img: imgFor(makeup, ["EYES MAKE-UP", "EYES", "EYE"]),
|
||
},
|
||
{
|
||
label: "صورت",
|
||
href: "/category/make-up/sub/face-make-up/",
|
||
img: imgFor(makeup, ["FACE MAKE-UP", "FACE"]),
|
||
},
|
||
{
|
||
label: "مراقبت پوست",
|
||
href: "/category/skin-care/",
|
||
img: imgFor(skin, ["SKIN CARE", "SKIN", "FACE CARE"]),
|
||
},
|
||
{
|
||
label: "اکسسوری",
|
||
href: "/category/accessories/",
|
||
img: imgFor(acc, ["ACCESSORIES", "ACCESSORY", "TOOLS"]),
|
||
},
|
||
];
|
||
}
|
||
|
||
/** KIKO-style homepage promo carousel (4 slots — video + campaign stills) */
|
||
export function promoHeroSliderHtml() {
|
||
const slides: Array<{
|
||
id: string;
|
||
tone: "burgundy" | "frost" | "frost-dark";
|
||
titleHtml: string;
|
||
sub: string;
|
||
cta: string;
|
||
href: string;
|
||
durationMs: number;
|
||
media:
|
||
| { kind: "image"; src: string; mobileSrc?: string }
|
||
| { kind: "video"; src: string; poster?: string };
|
||
}> = [
|
||
{
|
||
id: "kiko-sale",
|
||
tone: "frost",
|
||
titleHtml: "حراج کیکو",
|
||
sub: "تا ۷۰٪ تخفیف",
|
||
cta: "خرید کنید",
|
||
href: "/offers/",
|
||
durationMs: 6500,
|
||
media: {
|
||
kind: "image",
|
||
src: "https://images.kikocosmetics.com/lf2wbbijeo86/1YlokesjJQV77nKOfSbOpb/9d442692328ba645e37cfd75534e3cab/SummerSale_Bestseller_hp_slider_desktop.webp?w=1920&fm=webp",
|
||
mobileSrc:
|
||
"https://images.kikocosmetics.com/lf2wbbijeo86/2l2GIj2z5o76TxQVr7KP8p/ded199ce8a5a7c65cc7dd45e95c5de29/SummerSale_Bestseller_hp_slider_mobile.webp?w=900&fm=webp",
|
||
},
|
||
},
|
||
{
|
||
id: "matte-diaries",
|
||
tone: "burgundy",
|
||
titleHtml: "کالکشن محدود<br />Matte Diaries",
|
||
sub: "Not Shiny. Not Sorry.",
|
||
cta: "خرید کنید",
|
||
href: `/search/?q=${encodeURIComponent("Matte Diaries")}`,
|
||
durationMs: 7000,
|
||
media: {
|
||
kind: "image",
|
||
src: "https://images.kikocosmetics.com/lf2wbbijeo86/2ql2Sb1MDpfjVtUHCYLeKb/59b0faa4737f6f1fc457540c09de7a47/MatteDiaries_HP_Slider_Mobile.gif?w=1600&fm=webp",
|
||
},
|
||
},
|
||
{
|
||
id: "madonna",
|
||
tone: "frost",
|
||
titleHtml: "LET THE SHOW BEGIN",
|
||
sub: "کمپین جدید کیکو اینجاست.",
|
||
cta: "کشف کنید",
|
||
href: "/collection/",
|
||
durationMs: 8000,
|
||
media: {
|
||
kind: "video",
|
||
src: "https://videos.kikocosmetics.com/lf2wbbijeo86/2fvGzw53OyqL2Ij40WnXMY/0e60e3ea00e28156a2d7396bbd29ce13/KIKO_BRAND_CAMPAIGN_6s_HP_MOBILE_TEASER_9MB.mp4",
|
||
},
|
||
},
|
||
{
|
||
id: "just-cavalli",
|
||
tone: "frost-dark",
|
||
titleHtml: "Just Cavalli X Kiko Milano",
|
||
sub: "کالکشن محدود تابستانی",
|
||
cta: "خرید کنید",
|
||
href: "/search/?q=just+cavalli",
|
||
durationMs: 6500,
|
||
media: {
|
||
kind: "image",
|
||
src: "https://images.kikocosmetics.com/lf2wbbijeo86/6iTfXHCAkMpJo8KbpMtua0/113a1ec89f923f6339c2c7d8457eac7d/LimitedEdition_JustCavali_HP_Slider_Desktop_Products_03.webp?w=1920&fm=webp",
|
||
mobileSrc:
|
||
"https://images.kikocosmetics.com/lf2wbbijeo86/2LayvhGDaFD0rDvendGRxi/3a29ed6812d0b7910e29c3d70b651c60/LimitedEdition_JustCavallixKIKO_HP_Slider_Mobile.gif?w=900&fm=webp",
|
||
},
|
||
},
|
||
];
|
||
|
||
const slidesHtml = slides
|
||
.map((s, i) => {
|
||
const media =
|
||
s.media.kind === "video"
|
||
? `<video
|
||
class="phs-media-el"
|
||
data-phs-video
|
||
muted
|
||
loop
|
||
playsinline
|
||
preload="${i === 0 ? "auto" : "metadata"}"
|
||
src="${esc(s.media.src)}"
|
||
></video>`
|
||
: `<picture>
|
||
${
|
||
s.media.mobileSrc
|
||
? `<source media="(max-width: 767px)" srcset="${esc(s.media.mobileSrc)}" />`
|
||
: ""
|
||
}
|
||
<img class="phs-media-el" src="${esc(s.media.src)}" alt="${esc(`${s.titleHtml.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim()} — ${s.sub}`)}" decoding="async" width="1920" height="1080" ${i === 0 ? "" : 'loading="lazy"'} />
|
||
</picture>`;
|
||
|
||
return `<article
|
||
class="phs-slide${i === 0 ? " is-active" : ""}"
|
||
data-phs-slide="${i}"
|
||
data-phs-duration="${s.durationMs}"
|
||
data-phs-id="${esc(s.id)}"
|
||
aria-hidden="${i === 0 ? "false" : "true"}"
|
||
>
|
||
<div class="phs-media" aria-hidden="true">${media}</div>
|
||
<div class="phs-card phs-card--${s.tone}">
|
||
<h2 class="phs-title">${s.titleHtml}</h2>
|
||
<p class="phs-sub">${esc(s.sub)}</p>
|
||
<a class="phs-cta" href="${esc(s.href)}">${esc(s.cta)}</a>
|
||
</div>
|
||
</article>`;
|
||
})
|
||
.join("");
|
||
|
||
const bars = slides
|
||
.map(
|
||
(_s, i) =>
|
||
`<button type="button" class="phs-bar${i === 0 ? " is-active" : ""}" data-phs-goto="${i}" aria-label="اسلاید ${i + 1}">
|
||
<span class="phs-bar-fill" data-phs-fill></span>
|
||
</button>`
|
||
)
|
||
.join("");
|
||
|
||
return `<section class="promo-hero-slider" data-promo-slider aria-roledescription="carousel" aria-label="پیشنهادهای ویژه کیکو">
|
||
<div class="phs-stage">
|
||
<div class="phs-track">${slidesHtml}</div>
|
||
<div class="phs-ui">
|
||
<div class="phs-bars" role="tablist" aria-label="انتخاب اسلاید">${bars}</div>
|
||
<button type="button" class="phs-toggle" data-phs-toggle aria-label="توقف اسلایدر" aria-pressed="false">
|
||
<span class="phs-icon-pause" aria-hidden="true"></span>
|
||
<span class="phs-icon-play" aria-hidden="true" hidden></span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</section>`;
|
||
}
|
||
|
||
/** Top Categories — KIKO Italy “Top Categorie” smear tiles */
|
||
export function topCategoriesSection(categories: Array<{ label: string; href: string; img?: string | null }>) {
|
||
if (!categories.length) return "";
|
||
const items = categories.map((cat, i) => `
|
||
<a class="top-cat top-cat--${i}" href="${esc(cat.href)}" data-top-cat="${i}">
|
||
<div class="top-cat-visual" aria-hidden="true">
|
||
<span class="top-cat-smear"></span>
|
||
${cat.img ? `<img class="top-cat-img" src="${esc(cat.img)}" alt="${esc(cat.label)}" loading="lazy" />` : ""}
|
||
</div>
|
||
<span class="top-cat-label">${esc(cat.label)}</span>
|
||
</a>`).join("");
|
||
return `<section class="top-cats" id="categories" aria-label="برترین دستهبندیها">
|
||
<header class="sec-head">
|
||
<h2 class="top-cats-title">برترین <strong>دستهبندیها</strong></h2>
|
||
</header>
|
||
<div class="top-cats-rail">${items}</div>
|
||
</section>`;
|
||
}
|
||
|
||
/** Trust strip (compact) — kept for compatibility; prefer trustSection() */
|
||
export function perksStrip() {
|
||
return trustSection();
|
||
}
|
||
|
||
/** Editorial promo banner between rails and collection */
|
||
export function promoBanner(opts: { title: string; titleBold: string; sub: string; ctaLabel: string; ctaHref: string; img?: string }) {
|
||
return `<div class="promo-banner">
|
||
<div class="promo-banner-body">
|
||
<h2 class="promo-banner-title">${opts.title} <strong>${opts.titleBold}</strong></h2>
|
||
<p class="promo-banner-sub">${opts.sub}</p>
|
||
<a class="promo-banner-cta" href="${opts.ctaHref}">${opts.ctaLabel}</a>
|
||
</div>
|
||
${opts.img ? `<div class="promo-banner-img"><img src="${opts.img}" alt="${opts.titleBold}" loading="lazy" /></div>` : ""}
|
||
</div>`;
|
||
}
|
||
|
||
/**
|
||
* Love Your Eyes (2+1) promo PLP — retired; kept only if needed for redirects.
|
||
* Prefer omitting the route entirely (promo ended).
|
||
*/
|
||
export function loveYourEyesPromoBody(
|
||
_products: Array<Parameters<typeof listProductCard>[0]>,
|
||
_opts?: { trailingSlash?: boolean; emptyLabel?: string }
|
||
) {
|
||
return "";
|
||
}
|
||
|
||
function atelierIcon(name: "italy" | "shield" | "ship" | "pay" | "support" | "trust") {
|
||
const common = `class="atelier-ico" viewBox="0 0 24 24" aria-hidden="true"`;
|
||
if (name === "italy") {
|
||
return `<svg ${common}><path d="M12 21s7-4.4 7-10a5 5 0 0 0-10 0c0 5.6 3 10 3 10Z"/><circle cx="12" cy="11" r="2.2"/></svg>`;
|
||
}
|
||
if (name === "shield") {
|
||
return `<svg ${common}><path d="M12 3 5 6v5c0 4.5 3 7.8 7 9 4-1.2 7-4.5 7-9V6l-7-3Z"/><path d="m9.2 12 1.9 1.9 3.7-3.7"/></svg>`;
|
||
}
|
||
if (name === "ship") {
|
||
return `<svg ${common}><path d="M4 8h10v9H4zM14 11h3.5L20 14v3h-6"/><circle cx="7.5" cy="18.5" r="1.4"/><circle cx="16.5" cy="18.5" r="1.4"/><path d="M7 8V5.5h4V8"/></svg>`;
|
||
}
|
||
if (name === "pay") {
|
||
return `<svg ${common}><rect x="3" y="6" width="18" height="12" rx="2.2"/><path d="M3 10h18M7 14h5"/></svg>`;
|
||
}
|
||
if (name === "support") {
|
||
return `<svg ${common}><path d="M4 12a8 8 0 0 1 16 0"/><path d="M4 12v3.5A1.5 1.5 0 0 0 5.5 17H7v-5H4Zm16 0v3.5a1.5 1.5 0 0 1-1.5 1.5H17v-5h3Z"/><path d="M12 17v2.5a2 2 0 0 0 2 2h2"/></svg>`;
|
||
}
|
||
return `<svg ${common}><path d="M12 3.4 13.7 9H19l-4.2 3.1L16.5 18 12 14.8 7.5 18l1.7-5.9L5 9h5.3L12 3.4Z"/></svg>`;
|
||
}
|
||
|
||
/**
|
||
* Luxury Italian showroom hero — floating product compositions + exhibition features.
|
||
* No phone. No device video. Scroll advances scenes only.
|
||
*/
|
||
|
||
/** Editorial intro after campaign video — HTML/CSS only */
|
||
export function italyIntroSections(images: string[] = []) {
|
||
const img = (i: number) => images[i] || "";
|
||
const bubble = (src: string, alt: string) =>
|
||
`<div class="kiko-bubble" aria-hidden="${src ? "false" : "true"}">
|
||
<span class="kiko-bubble-glow"></span>
|
||
${src ? `<img src="${esc(src)}" alt="${esc(alt)}" loading="lazy" />` : `<span class="kiko-bubble-fallback">KIKO</span>`}
|
||
</div>`;
|
||
|
||
const steps = [
|
||
{
|
||
n: "1/",
|
||
title: "اصالت میلان",
|
||
product: "ORIGINAL FROM ITALY",
|
||
body: "هر محصول مستقیم از اکوسیستم KIKO Milano انتخاب میشود؛ همان رنگها، بافتها و استاندارد اروپایی که در ایتالیا عرضه میشود.",
|
||
href: "/category/make-up",
|
||
image: img(0),
|
||
flip: false,
|
||
},
|
||
{
|
||
n: "2/",
|
||
title: "انتخاب هوشمند",
|
||
product: "CURATED DISCOUNTS",
|
||
body: "بهجای گشتن در هزاران SKU، کالکشن تخفیفدار و پرفروش را برای شما فیلتر کردهایم تا خرید از ایتالیا سریع و مطمئن باشد.",
|
||
href: "/#collection",
|
||
image: img(1),
|
||
flip: true,
|
||
},
|
||
{
|
||
n: "3/",
|
||
title: "تجربه زیبایی",
|
||
product: "MADE FOR YOUR RITUAL",
|
||
body: "از آرایش تا مراقبت پوست؛ محصولات را مثل ابزار تناسب پوست در نظر بگیرید—روال روزمره با کیفیت میلان، به زبان و قیمت تومان.",
|
||
href: "/category/skin-care",
|
||
image: img(2),
|
||
flip: false,
|
||
},
|
||
];
|
||
|
||
const stepHtml = steps
|
||
.map(
|
||
(s) => `<article class="kiko-step${s.flip ? " kiko-step--flip" : ""} reveal">
|
||
<span class="kiko-step-num" dir="ltr" aria-hidden="true">${esc(s.n)}</span>
|
||
<div class="kiko-step-media">${bubble(s.image, s.title)}</div>
|
||
<div class="kiko-step-copy">
|
||
<h3>${esc(s.title)}</h3>
|
||
<p class="kiko-step-sku label-caps">${esc(s.product)}</p>
|
||
<p class="kiko-step-body">${esc(s.body)}</p>
|
||
<a class="kiko-step-link" href="${esc(s.href)}">مشاهده محصولات</a>
|
||
</div>
|
||
</article>`
|
||
)
|
||
.join("");
|
||
|
||
return `
|
||
<section class="kiko-philosophy" aria-labelledby="kiko-philosophy-title">
|
||
<p class="kiko-philosophy-label reveal"><span class="kiko-dot" aria-hidden="true"></span> فلسفه ما</p>
|
||
<h2 id="kiko-philosophy-title" class="kiko-philosophy-title reveal">
|
||
محصولات کیکو را مثل ابزار تناسب پوست، مستقیم از ایتالیا انتخاب میکنیم
|
||
</h2>
|
||
<div class="kiko-philosophy-stage reveal">
|
||
<div class="kiko-philosophy-product">
|
||
${bubble(img(0), "KIKO Milano")}
|
||
</div>
|
||
<aside class="kiko-callout">
|
||
<span class="kiko-callout-line" aria-hidden="true"></span>
|
||
<p class="kiko-callout-kicker">پشتیبانی</p>
|
||
<p class="kiko-callout-text">از پوست در برابر استرس روزمره و انتخابهای بیکیفیت</p>
|
||
</aside>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="kiko-steps" aria-labelledby="kiko-steps-title">
|
||
<header class="kiko-steps-head reveal">
|
||
<h2 id="kiko-steps-title">مراحل ضروری</h2>
|
||
<p class="kiko-steps-sub">سه قدم ساده برای خرید مطمئن محصولات اصل KIKO از ایتالیا</p>
|
||
<span class="kiko-steps-watermark" dir="ltr" aria-hidden="true">3</span>
|
||
</header>
|
||
${stepHtml}
|
||
</section>`;
|
||
}
|
||
|
||
function parseJson<T>(raw: string, fallback: T): T {
|
||
try {
|
||
return JSON.parse(raw) as T;
|
||
} catch {
|
||
return fallback;
|
||
}
|
||
}
|
||
|
||
export function renderBlocks(
|
||
blocks: Array<{ type: string; content: string }>,
|
||
productImages: string[]
|
||
) {
|
||
return blocks
|
||
.filter((b) => b.type !== "HERO")
|
||
.map((block) => {
|
||
const c = parseJson<Record<string, unknown>>(block.content, {});
|
||
if (block.type === "FAQ") {
|
||
const items = (c.items as Array<{ question: string; answer: string }>) || [];
|
||
return `<section class="block-section container-luxury reveal">
|
||
${c.title ? `<h2>${esc(c.title)}</h2>` : `<h2>پرسشها</h2>`}
|
||
<div style="max-width:48rem;margin-inline:auto">
|
||
${items
|
||
.map(
|
||
(item) => `<details class="faq-item"><summary>${esc(item.question)}</summary><p>${esc(item.answer)}</p></details>`
|
||
)
|
||
.join("")}
|
||
</div>
|
||
</section>`;
|
||
}
|
||
if (block.type === "FEATURES" || block.type === "BENEFITS") {
|
||
const features =
|
||
(c.features as Array<{ title: string; description: string }>) ||
|
||
(c.items as Array<{ title: string; description: string }>) ||
|
||
[];
|
||
return `<section class="block-section container-luxury">
|
||
${c.title ? `<h2 class="reveal">${esc(c.title)}</h2>` : ""}
|
||
<div class="features-grid">
|
||
${features
|
||
.map(
|
||
(f) => `<article class="feature-card reveal"><h3>${esc(f.title)}</h3><p class="text-muted">${esc(f.description)}</p></article>`
|
||
)
|
||
.join("")}
|
||
</div>
|
||
</section>`;
|
||
}
|
||
if (block.type === "GALLERY") {
|
||
const images =
|
||
(c.images as string[])?.length
|
||
? (c.images as string[])
|
||
: productImages;
|
||
return `<section class="block-section container-luxury">
|
||
<div class="gallery-grid reveal" data-variant-gallery>
|
||
${images.map((src) => `<img src="${esc(src)}" alt="" loading="lazy" />`).join("")}
|
||
</div>
|
||
</section>`;
|
||
}
|
||
if (block.type === "IMAGE_TEXT") {
|
||
return `<section class="block-section container-luxury reveal">
|
||
<div style="display:grid;gap:2rem;align-items:center" class="image-text">
|
||
${c.image ? `<img src="${esc(c.image)}" alt="${esc(c.title || "")}" style="width:100%;aspect-ratio:4/3;object-fit:cover" />` : ""}
|
||
<div>
|
||
${c.title ? `<h2 style="text-align:start;margin:0 0 1rem">${esc(c.title)}</h2>` : ""}
|
||
<p class="text-muted" style="line-height:1.8">${esc(c.body || "")}</p>
|
||
${c.ctaText && c.ctaLink ? `<p style="margin-top:1.5rem"><a class="btn" href="${esc(String(c.ctaLink).startsWith("#") || String(c.ctaLink).startsWith("/") ? c.ctaLink : `#${c.ctaLink}`)}">${esc(c.ctaText)}</a></p>` : ""}
|
||
</div>
|
||
</div>
|
||
</section>`;
|
||
}
|
||
if (block.type === "CTA") {
|
||
return `<section class="block-section" style="background:#0a0a0a;color:#fff;text-align:center">
|
||
<div class="container-luxury reveal">
|
||
<h2 style="color:#fff">${esc(c.headline || "")}</h2>
|
||
${c.subheadline ? `<p style="opacity:0.8;max-width:36rem;margin:1rem auto 0">${esc(c.subheadline)}</p>` : ""}
|
||
${c.buttonText && c.buttonLink ? `<p style="margin-top:2rem"><a class="btn btn-light" href="${esc(c.buttonLink)}">${esc(c.buttonText)}</a></p>` : ""}
|
||
</div>
|
||
</section>`;
|
||
}
|
||
if (block.type === "SPECIFICATIONS") {
|
||
const rows =
|
||
(c.items as Array<{ label: string; value: string }>) ||
|
||
(c.rows as Array<{ label: string; value: string }>) ||
|
||
[];
|
||
return `<section class="block-section container-luxury reveal">
|
||
${c.title ? `<h2>${esc(c.title)}</h2>` : ""}
|
||
<dl style="max-width:40rem;margin-inline:auto">
|
||
${rows
|
||
.map(
|
||
(r) => `<div style="display:flex;justify-content:space-between;gap:1rem;border-bottom:1px solid #ebebeb;padding:0.9rem 0"><dt class="text-muted">${esc(r.label)}</dt><dd style="margin:0">${esc(r.value)}</dd></div>`
|
||
)
|
||
.join("")}
|
||
</dl>
|
||
</section>`;
|
||
}
|
||
if (block.type === "TESTIMONIAL" || block.type === "TRUST") {
|
||
const items =
|
||
(c.items as Array<{ quote?: string; text?: string; author?: string; title?: string; name?: string; description?: string }>) ||
|
||
[];
|
||
return `<section class="block-section container-luxury">
|
||
${c.title ? `<h2 class="reveal">${esc(c.title)}</h2>` : ""}
|
||
<div class="features-grid">
|
||
${items
|
||
.map((t) => {
|
||
const body = t.quote || t.text || t.description || "";
|
||
const who = t.author || t.name || "";
|
||
return `<blockquote class="reveal" style="margin:0"><p>${esc(body)}</p>${who ? `<footer class="text-muted" style="margin-top:0.75rem">${esc(who)}${t.title ? ` — ${esc(t.title)}` : ""}</footer>` : ""}</blockquote>`;
|
||
})
|
||
.join("")}
|
||
</div>
|
||
</section>`;
|
||
}
|
||
return "";
|
||
})
|
||
.join("\n");
|
||
}
|
||
|
||
export function buyPanel(opts: {
|
||
product: {
|
||
id: string;
|
||
slug: string;
|
||
name: string;
|
||
nameFa?: string | null;
|
||
category?: string | null;
|
||
tagline: string | null;
|
||
price: number;
|
||
compareAtPrice: number | null;
|
||
heroImage: string | null;
|
||
stock: number;
|
||
lowStockThreshold: number;
|
||
};
|
||
dict: Dictionary;
|
||
colorVariants?: ColorVariant[];
|
||
trailingSlash?: boolean;
|
||
}) {
|
||
const { product: p, dict, colorVariants } = opts;
|
||
const inStock = p.stock > 0;
|
||
const discount =
|
||
p.compareAtPrice && p.compareAtPrice > p.price
|
||
? Math.round(((p.compareAtPrice - p.price) / p.compareAtPrice) * 100)
|
||
: null;
|
||
const variants = colorVariants || [];
|
||
const hasVariants = variants.length > 1;
|
||
const current = variants.find((v) => v.id === p.id) || variants.find((v) => v.slug === p.slug);
|
||
const title = productDisplayTitle(p, hasVariants);
|
||
const categoryLink = productCategoryLink(p.name, p.category);
|
||
|
||
const shadePicker = !hasVariants
|
||
? ""
|
||
: `<div class="shade-picker" data-shade-picker role="listbox" aria-label="${esc(dict.color)}">
|
||
<p class="shade-picker-label">
|
||
<span class="label-caps text-muted">${esc(dict.color)}</span>
|
||
<span class="shade-picker-current" data-shade-label>${esc(current?.shade || "")}</span>
|
||
</p>
|
||
<div class="shade-swatches">
|
||
${variants
|
||
.map((v) => {
|
||
const active = v.id === p.id || v.slug === p.slug;
|
||
const bg = v.heroImage
|
||
? `background-image:url('${esc(v.heroImage)}')`
|
||
: "";
|
||
const oos = v.stock <= 0 ? " is-oos" : "";
|
||
const variantName = v.name.replace(/\s+/g, " ").trim();
|
||
const variantTitle = withPersianCategory(variantName, p.category);
|
||
const variantIntro = `${persianCategoryLabel(p.category, variantName)} کیکو میلانو مدل ${variantName}${v.shade ? `، رنگ ${v.shade}` : ""}.`;
|
||
return `<button
|
||
type="button"
|
||
class="shade-swatch${active ? " is-active" : ""}${oos}"
|
||
data-shade-select
|
||
data-id="${esc(v.id)}"
|
||
data-slug="${esc(v.slug)}"
|
||
data-name="${esc(variantTitle)}"
|
||
data-shade="${esc(v.shade)}"
|
||
data-intro="${esc(variantIntro)}"
|
||
data-price="${v.price}"
|
||
data-compare="${v.compareAtPrice ?? ""}"
|
||
data-image="${esc(v.heroImage || "")}"
|
||
data-images="${esc(JSON.stringify(variantGalleryImages(v)))}"
|
||
data-stock="${v.stock}"
|
||
role="option"
|
||
aria-selected="${active ? "true" : "false"}"
|
||
title="${esc(v.shade)}"
|
||
${active ? 'aria-current="true"' : ""}
|
||
><span class="shade-swatch-img" style="${bg}"></span><span class="shade-swatch-code">${esc(v.shade)}</span></button>`;
|
||
})
|
||
.join("")}
|
||
</div>
|
||
</div>`;
|
||
|
||
return `<div class="buy-panel" id="buy" data-buy-panel data-category="${esc(p.category || "")}" data-category-fa="${esc(persianCategoryLabel(p.category, p.name) || "")}" data-low-stock="${p.lowStockThreshold}" data-label-add="${esc(dict.addToCart)}" data-label-oos="${esc(dict.notifyMe)}" data-label-limited="${esc(dict.limitedStock)}" data-label-sold="${esc(dict.outOfStock)}">
|
||
<p class="label-caps text-muted">${esc(dict.exclusive)}</p>
|
||
${loveYourEyesPillHtml(p.slug)}
|
||
<h1 data-buy-title>${esc(title)}</h1>
|
||
${categoryLink ? `<p class="text-muted" style="margin:0.6rem 0 0;font-size:0.875rem">دستهبندی: <a href="${esc(categoryLink.href)}" style="text-decoration:underline">${esc(categoryLink.label)}</a></p>` : ""}
|
||
${p.tagline ? `<p class="tagline">${esc(p.tagline)}</p>` : ""}
|
||
<div class="price-row" data-buy-price>
|
||
<span class="now">${esc(formatPrice(p.price))}</span>
|
||
${
|
||
p.compareAtPrice && p.compareAtPrice > p.price
|
||
? `<span class="was">${esc(formatPrice(p.compareAtPrice))}</span><span class="offer-badge offer-badge--buy" data-buy-discount>−${discount}%</span>`
|
||
: ""
|
||
}
|
||
</div>
|
||
<p class="buy-cta-wrap">
|
||
<button
|
||
type="button"
|
||
class="btn btn-block"
|
||
data-add-to-cart
|
||
data-product-id="${esc(p.id)}"
|
||
data-slug="${esc(p.slug)}"
|
||
data-name="${esc(title)}"
|
||
data-price="${p.price}"
|
||
data-image="${esc(p.heroImage || "")}"
|
||
data-category="${esc(p.category || "")}"
|
||
data-category-fa="${esc(persianCategoryLabel(p.category, p.name) || "")}"
|
||
data-locale="fa"
|
||
${inStock ? "" : "disabled"}
|
||
>${inStock ? esc(dict.addToCart) : esc(dict.notifyMe)}</button>
|
||
<button
|
||
type="button"
|
||
class="btn btn-block btn-share"
|
||
data-share-product
|
||
data-share-title="${esc(title)}"
|
||
aria-label="اشتراکگذاری"
|
||
>اشتراکگذاری</button>
|
||
</p>
|
||
<p class="text-muted buy-stock-msg" data-buy-stock style="font-size:0.875rem">${
|
||
!inStock
|
||
? esc(dict.outOfStock)
|
||
: inStock && p.stock <= p.lowStockThreshold
|
||
? `${esc(dict.limitedStock)} ${p.stock}`
|
||
: ""
|
||
}</p>
|
||
${shadePicker}
|
||
<ul class="buy-perks">
|
||
<li>${esc(dict.authenticity)}</li>
|
||
</ul>
|
||
</div>`;
|
||
}
|