Initial commit: Hoomat storefront with Meshkee API and static-image slots.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-09-04 15:00:41 +03:30
co-authored by Cursor
commit cfabf3e327
95 changed files with 12651 additions and 0 deletions
+344
View File
@@ -0,0 +1,344 @@
import Image from "next/image";
import Link from "next/link";
import { HeroSlider, type HeroSlide } from "@/app/components/hero-slider";
import { ProductCarousel } from "@/app/components/product-carousel";
import { ArticleCarousel } from "@/app/components/article-carousel";
import { SlotBanner, SlotLinkedImage } from "@/app/components/slot-media";
import { getStaticImages, listBlogs } from "@/lib/meshkee/api";
import { formatDate } from "@/lib/meshkee/format";
import {
firstSlotImage,
imageSubtext,
imageTitleEn,
imageTitleFa,
slotImages,
} from "@/lib/meshkee/static-slots";
import type { StaticImageItem, StaticImageSlot } from "@/lib/meshkee/types";
import { blogHref } from "@/lib/meshkee/urls";
import {
aboutCopy,
articles as fallbackArticles,
categories,
categoryQuickLinks,
diningProducts,
features,
heroSlides as fallbackHeroSlides,
products,
site,
} from "@/lib/site-data";
function SectionTitle({
eyebrow,
title,
href,
}: {
eyebrow: string;
title: string;
href?: string;
}) {
return (
<div className="section-title">
<div>
<p className="eyebrow">{eyebrow}</p>
<h2>{title}</h2>
</div>
{href ? (
<Link className="section-title-chevron" href={href} aria-label="مشاهده بیشتر">
<span aria-hidden="true">‹</span>
<em>MORE</em>
</Link>
) : null}
</div>
);
}
function QuickCategoryIcon({ index }: { index: number }) {
const shapes = [
"M5 18h14M7 18v-5a4 4 0 0 1 4-4h2a4 4 0 0 1 4 4v5M8 18v2m8-2v2",
"M8 20v-7a4 4 0 0 1 8 0v7M7 13h10M9 20h6",
"M6 18h12M8 18v-8h8v8M10 10v8m4-8v8",
"M5 18h14M7 18V9h10v9M9 12h6",
"M5 12h14M8 12v8m8-8v8M7 8h10",
"M5 18V9h14v9M5 14h14M8 18v2m8-2v2",
];
return (
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d={shapes[index % shapes.length]} />
</svg>
);
}
function buildHeroSlides(images: StaticImageItem[]): HeroSlide[] {
if (!images.length) return fallbackHeroSlides;
return images.map((image, index) => {
const fallback = fallbackHeroSlides[index % fallbackHeroSlides.length];
return {
eyebrow: imageTitleEn(image) || fallback.eyebrow,
title: imageTitleFa(image) || fallback.title,
description: imageSubtext(image) || fallback.description,
image: image.url || fallback.image,
linkUrl: image.linkUrl,
};
});
}
function mergeQuickCategories(icons: StaticImageItem[]) {
return categoryQuickLinks.map((item, index) => {
const image = icons[index];
return {
title: imageTitleFa(image) || item.title,
iconImage: image?.url || item.iconImage,
href: image?.linkUrl || "#products",
};
});
}
const twoBannerFallbacks = [
{
eyebrow: "MINIMAL SOFA",
title: "خرید مبل مینیمال مدرن",
text: "طراحی ساده، دسته‌های چوبی و نشیمن راحت برای خانه‌های مدرن.",
},
{
eyebrow: "INSTALLMENT",
title: "خرید مبل اقساطی",
text: "شرایط ویژه برای خرید انواع مبلمان با تحویل و مشاوره فروش.",
},
];
const threeBannerFallbacks = [
{
title: "مبل راحتی مدرن",
text: "انتخابی مناسب برای چیدمان صمیمی و کاربردی فضای نشیمن.",
},
{
title: "سرویس خواب",
text: "مدل‌های مدرن و کلاسیک برای تکمیل دکوراسیون اتاق خواب.",
},
{
title: "میز جلو مبلی و عسلی",
text: "هماهنگ با سبک مبلمان و قابل سفارش برای خانه‌های ایرانی.",
},
];
export default async function Home() {
const [homeSlots, blogItems] = await Promise.all([
getStaticImages("home").catch(() => [] as StaticImageSlot[]),
listBlogs({ page: 1, pageSize: 8 })
.then((data) =>
data.items.map((post) => ({
title: post.title,
date: formatDate(post.publishedAt || post.createdAt),
image: post.titleImageUrl || "/assets/sample/spacejoy-c0JoR_-2x3E-unsplash.jpg",
href: blogHref(post.id, post.title),
})),
)
.catch(() =>
fallbackArticles.map((article) => ({
...article,
href: "/blog",
})),
),
]);
const heroSlides = buildHeroSlides(slotImages(homeSlots, "slider"));
const quickCategories = mergeQuickCategories(slotImages(homeSlots, "categories-icon"));
const saleBannerImage = firstSlotImage(homeSlots, "special-sale-banner");
const featuresImage = firstSlotImage(homeSlots, "product-features-image");
const contactImage = firstSlotImage(homeSlots, "contactus-image");
const aboutHomeImage = firstSlotImage(homeSlots, "aboutus-image");
const twoBanners = slotImages(homeSlots, "two-banner-row");
const threeBanners = slotImages(homeSlots, "three-banner-row");
return (
<main>
<HeroSlider slides={heroSlides} />
<section className="quick-categories-section">
<div className="container">
<div className="quick-category-frame">
{quickCategories.map((item, index) => (
<a className="quick-category-item" href={item.href} key={`${item.title}-${index}`}>
<span className="quick-category-icon">
{item.iconImage ? (
<Image src={item.iconImage} alt="" width={96} height={96} aria-hidden="true" />
) : (
<QuickCategoryIcon index={index} />
)}
</span>
<span>{item.title}</span>
</a>
))}
</div>
</div>
</section>
<section className="section">
<div className="container">
<div className="grid category-grid">
{categories.map((category) => (
<article className="category-card" key={category.title}>
<Image src={category.image} alt={category.title} width={520} height={460} />
<div>
<h3>{category.title}</h3>
<p>{category.subtitle}</p>
</div>
</article>
))}
</div>
</div>
</section>
<section className="section">
<div className="container">
<ProductCarousel
products={products}
ariaLabel="کاروسل فروش ویژه"
withSaleBanner
saleBanner={{
image: saleBannerImage?.url,
linkUrl: saleBannerImage?.linkUrl,
ariaLabel: imageTitleFa(saleBannerImage) || "فروش ویژه",
}}
/>
</div>
</section>
<section className="section">
<div className="container">
<div className="feature-showcase">
<svg className="feature-connectors" viewBox="0 0 100 100" aria-hidden="true">
<path d="M55 45 C64 31 75 25 88 17" />
<path d="M45 45 C35 28 25 24 12 17" />
<path d="M58 56 C70 70 78 81 90 86" />
<path d="M42 56 C31 73 22 80 10 86" />
</svg>
<div className="feature-sofa">
<SlotLinkedImage
image={featuresImage}
fallbackSrc="/assets/sample/sofa-real-v2.png"
alt="مبل راحتی هومت"
width={900}
height={420}
/>
</div>
{features.map((feature, index) => (
<article className={`feature-card feature-card-${index + 1}`} key={feature.title}>
<h3>{feature.title}</h3>
<p>{feature.subtitle}</p>
<small className="feature-subtext">{feature.subtext}</small>
</article>
))}
</div>
</div>
</section>
<section className="section" id="products">
<div className="container">
<SectionTitle eyebrow="Newest sofa" title="جدیدترین مبل" href="/store" />
<ProductCarousel products={products} ariaLabel="کاروسل مبل راحتی" />
</div>
</section>
<section className="section">
<div className="container contact-row">
<div className="contact-card">
<p className="eyebrow">Contact Us</p>
<h2>برای انتخاب پارچه و سفارش با ما در ارتباط باشید</h2>
<p>
مشاوران هومت هر روز از ساعت 10 صبح الی 20 شب آماده پاسخگویی و
راهنمایی برای انتخاب مدل، رنگ، پارچه و شرایط خرید هستند.
</p>
<ul className="contact-list">
<li>
واتس‌اپ / تلگرام:{" "}
<span className="ltr-number" dir="ltr" lang="en">
{site.whatsapp} _ {site.mobile}
</span>
</li>
<li>
شماره تماس:{" "}
<span className="ltr-number" dir="ltr" lang="en">
{site.phone}
</span>
</li>
</ul>
<Link className="primary-btn" href="/contactus">
ارتباط با مبلمان هومت
</Link>
</div>
<SlotBanner
image={contactImage}
className="banner contact-guarantee"
fallbackEyebrow="GUARANTEE"
fallbackTitle="ضمانت نامه کتبی مبلمان"
fallbackText="خرید با اطمینان، ارسال مبل راحتی و کلاسیک و پشتیبانی پس از فروش."
/>
</div>
</section>
<section className="section">
<div className="container">
<SectionTitle eyebrow="Dining Set Table" title="کاروسل میز ناهارخوری" />
<ProductCarousel products={diningProducts} ariaLabel="کاروسل میز ناهارخوری" />
</div>
</section>
<section className="section">
<div className="container grid banner-grid">
{twoBannerFallbacks.map((fallback, index) => (
<SlotBanner
key={fallback.title}
image={twoBanners[index]}
fallbackEyebrow={fallback.eyebrow}
fallbackTitle={fallback.title}
fallbackText={fallback.text}
/>
))}
</div>
</section>
<section className="section">
<div className="container about-panel">
<SlotLinkedImage
image={aboutHomeImage}
fallbackSrc={site.heroImages[1]}
alt="درباره مبلمان هومت"
width={720}
height={620}
/>
<div className="about-copy">
<p className="eyebrow">{aboutCopy.eyebrow}</p>
<h2>درباره هومت</h2>
<p>{aboutCopy.story}</p>
<Link className="primary-btn" href="/about-us">
بیشتر بدانید
</Link>
</div>
</div>
</section>
<section className="section">
<div className="container grid banner-grid three">
{threeBannerFallbacks.map((fallback, index) => (
<SlotBanner
key={fallback.title}
image={threeBanners[index]}
fallbackTitle={fallback.title}
fallbackText={fallback.text}
/>
))}
</div>
</section>
<section className="section" id="articles">
<div className="container">
<SectionTitle eyebrow="Latest Articles" title="آخــریـن مــقـالــات" href="/blog" />
<ArticleCarousel articles={blogItems} ariaLabel="کاروسل مقالات" />
</div>
</section>
</main>
);
}