Initial commit: Hoomat storefront with Meshkee API and static-image slots.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+344
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user