Create Glassmond storefront with Meshkee integration

Build the RTL Next.js experience with live catalog, journal, customer interactions, and SEO-ready canonical routes.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-08-26 00:08:00 +03:30
co-authored by Cursor
commit 98fde02322
66 changed files with 8280 additions and 0 deletions
+116
View File
@@ -0,0 +1,116 @@
import type { Metadata } from "next";
import Image from "next/image";
import { Award, CheckCircle2, Lightbulb, Users } from "lucide-react";
import { Features, PageHero, SectionHeading, VideoBanner } from "@/components/sections";
export const metadata: Metadata = {
title: "درباره ما",
description: "با داستان شکل‌گیری، اختراع‌ها، کیفیت تولید و خدمات آینه هوشمند گلسموند آشنا شوید.",
alternates: { canonical: "/about" },
openGraph: {
title: "درباره گلسموند",
description: "داستان شکل‌گیری گلسموند و اختراع آینه هوشمند ضد بخار را بخوانید.",
url: "/about",
images: ["/images/about/gallery-1.jpg"],
},
};
export default function AboutPage() {
return (
<>
<PageHero englishTitle="ABOUT US" title="درباره ما بیشتر بدانید" />
<section className="about-intro">
<div className="container about-grid">
<div className="about-gallery">
<div className="gallery-main">
<Image src="/images/about/gallery-1.jpg" alt="آینه هوشمند گلسموند در فضای مدرن" fill sizes="40vw" />
</div>
<div className="gallery-side">
<Image src="/images/about/gallery-3.jpg" alt="طراحی آینه گلسموند" fill sizes="22vw" />
</div>
<div className="about-year">
<strong>۱۳۹۳</strong>
<span>سال آغاز فعالیت</span>
</div>
</div>
<div className="about-copy">
<SectionHeading
eyebrow=""
englishTitle="ABOUT GLASSMOND"
title="درباره گلسموند"
align="start"
/>
<p>
شرکت فن‌آوران صنعت گلسمند با برند «گلسموند» در سال ۱۳۹۳ شروع به کار
کرد. بنیان‌گذار این شرکت، مهندس مهدی عرب‌پور، در سال ۱۳۹۷ برای اولین
بار موفق به ثبت اختراع و پتنت موقت آمریکا در زمینه تولید آینه‌های
هوشمند ضد بخار شد.
</p>
<p>
ویژگی‌های انحصاری این آینه‌ها نتیجه طراحی و توسعه داخلی است و محصول
امروز به‌صورت کاملا صنعتی در مجموعه گلسموند تولید می‌شود.
</p>
<div className="about-highlights">
<div><Award /><span><b>ثبت اختراع</b> فناوری انحصاری ضد بخار</span></div>
<div><Users /><span><b>خدمات گسترده</b> نمایندگی در ایران و عراق</span></div>
<div><Lightbulb /><span><b>تولید هوشمند</b> قطعات به‌روز و ایمن</span></div>
</div>
</div>
</div>
</section>
<section className="about-stats" aria-label="آمار مجموعه گلسموند">
<div className="container about-stats-grid">
<article>
<span>EXPERIENCE</span>
<strong>+۱۰</strong>
<p>سال تجربه و نوآوری</p>
</article>
<article>
<span>INNOVATION</span>
<strong>۱</strong>
<p>اختراع ثبت‌شده</p>
</article>
<article>
<span>SUPPORT</span>
<strong>۱۰</strong>
<p>سال خدمات پس از فروش</p>
</article>
</div>
</section>
<section className="why-section">
<div className="container why-grid">
<div>
<SectionHeading
eyebrow=""
englishTitle="OUR SERVICES"
title="چرا گلسموند؟"
align="start"
/>
<p>
طراحی و اختراع انحصاری، رعایت استانداردهای جهانی در طراحی و تولید،
تضمین کیفیت در ضد بخار و ضد آب بودن و قیمت‌گذاری متناسب با کیفیت،
پایه‌های اعتماد مشتریان به گلسموند هستند.
</p>
<ul className="check-list">
<li><CheckCircle2 /> طراحی و اختراع انحصاری محصول</li>
<li><CheckCircle2 /> رعایت استانداردهای جهانی و ایمنی</li>
<li><CheckCircle2 /> تضمین کیفیت و قیمت</li>
<li><CheckCircle2 /> یک سال گارانتی و ده سال خدمات پس از فروش</li>
<li><CheckCircle2 /> استفاده از مواد اولیه درجه یک</li>
</ul>
</div>
<div className="why-image">
<Image src="/images/about/gallery-4.jpg" alt="کیفیت آینه گلسموند" fill sizes="45vw" />
</div>
</div>
</section>
<Features compact />
<VideoBanner />
</>
);
}
+41
View File
@@ -0,0 +1,41 @@
import { NextResponse } from "next/server";
import {
MESHKEE_API_BASE,
WEBSITE_DOMAIN,
} from "@/lib/meshkee";
export async function POST(request: Request) {
const body = (await request.json()) as {
title?: string;
name?: string;
email?: string;
cellNumber?: string;
text?: string;
};
if (!body.title?.trim() || !body.name?.trim() || !body.text?.trim()) {
return NextResponse.json(
{ message: "نام، موضوع و متن پیام الزامی است." },
{ status: 400 },
);
}
const response = await fetch(
`${MESHKEE_API_BASE}/tenants/${WEBSITE_DOMAIN}/contact-submissions`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
title: body.title.trim(),
name: body.name.trim(),
email: body.email?.trim() || undefined,
cellNumber: body.cellNumber?.trim() || undefined,
text: body.text.trim(),
}),
cache: "no-store",
},
);
const data = await response.json().catch(() => ({}));
return NextResponse.json(data, { status: response.status });
}
+119
View File
@@ -0,0 +1,119 @@
import type { Metadata } from "next";
import Image from "next/image";
import Link from "next/link";
import { CalendarDays, UserRound } from "lucide-react";
import { notFound, redirect } from "next/navigation";
import { CommentsPanel } from "@/components/entity-comments";
import {
blogUrl,
formatPersianDate,
getBlogById,
getBlogs,
getComments,
plainText,
sanitizeCmsContent,
slugifyFa,
} from "@/lib/meshkee";
type BlogDetailProps = {
params: Promise<{ id: string; slug: string }>;
};
export async function generateStaticParams() {
const data = await getBlogs({ pageSize: 50 }).catch(() => ({ items: [] }));
return data.items.map((blog) => ({ id: blog.id, slug: slugifyFa(blog.title) }));
}
export async function generateMetadata({ params }: BlogDetailProps): Promise<Metadata> {
const { id } = await params;
const data = await getBlogById(id).catch(() => null);
if (!data) return { title: "مقاله یافت نشد", robots: { index: false } };
const { blog } = data;
const description =
plainText(blog.abstract || blog.mainTextHtml).slice(0, 155) ||
`مطالعه مقاله ${blog.title} در مجله گلسموند.`;
const canonical = blogUrl(blog);
return {
title: blog.title,
description,
alternates: { canonical },
openGraph: {
title: blog.title,
description,
type: "article",
publishedTime: blog.publishedAt,
url: canonical,
images: blog.titleImageUrl ? [blog.titleImageUrl] : [],
},
};
}
export default async function BlogDetailPage({ params }: BlogDetailProps) {
const { id, slug } = await params;
const data = await getBlogById(id).catch(() => null);
if (!data) notFound();
const { blog } = data;
const canonical = blogUrl(blog);
const expectedSlug = canonical.split("/").at(-1) || "";
let currentSlug = slug;
try {
currentSlug = decodeURIComponent(slug);
} catch {
currentSlug = slug;
}
if (currentSlug !== expectedSlug) redirect(encodeURI(canonical));
const comments = await getComments("blog", blog.id);
const content = sanitizeCmsContent(blog.mainTextHtml, blog.title);
const authorName = [blog.author?.firstName, blog.author?.lastName].filter(Boolean).join(" ");
return (
<article className="blog-detail">
<header className="blog-detail-header">
<div className="container">
<nav className="detail-breadcrumbs light" aria-label="مسیر صفحه">
<Link href="/">صفحه اصلی</Link>
<span>/</span>
<Link href="/blog">مجله</Link>
<span>/</span>
<b>{blog.title}</b>
</nav>
<span className="page-hero-label">GLASSMOND JOURNAL</span>
<h1>{blog.title}</h1>
<div className="blog-meta">
<span><CalendarDays />{formatPersianDate(blog.publishedAt)}</span>
{authorName && <span><UserRound />{authorName}</span>}
</div>
</div>
</header>
<div className="container blog-detail-image">
<Image
src={blog.titleImageUrl || "/images/articles/decorative-mirrors.jpg"}
alt={blog.title}
fill
sizes="(max-width: 900px) 100vw, 1100px"
priority
/>
</div>
<div className="container blog-content-wrap">
{blog.abstract && <p className="blog-lead">{blog.abstract}</p>}
<div className="cms-content" dangerouslySetInnerHTML={{ __html: content }} />
</div>
<section className="blog-comments">
<div className="container">
<div className="section-heading align-start">
<span className="english-title">COMMENTS</span>
<h2>دیدگاه‌ها</h2>
</div>
<CommentsPanel entityType="blog" entityId={blog.id} comments={comments.items} />
</div>
</section>
</article>
);
}
+135
View File
@@ -0,0 +1,135 @@
import type { Metadata } from "next";
import Link from "next/link";
import { ArrowLeft, ArrowRight, Search } from "lucide-react";
import { ApiBlogCard } from "@/components/api-blog-card";
import { PageHero } from "@/components/sections";
import { getBlogs, getCategories } from "@/lib/meshkee";
type BlogSearchParams = {
page?: string;
q?: string;
category?: string;
};
export async function generateMetadata({
searchParams,
}: {
searchParams: Promise<BlogSearchParams>;
}): Promise<Metadata> {
const params = await searchParams;
const page = Math.max(1, Number.parseInt(params.page || "1", 10) || 1);
const q = (params.q || "").trim();
const category = (params.category || "").trim();
const baseDescription =
"مقالات گلسموند درباره آینه هوشمند، آینه دکوراتیو، نورپردازی و کاربرد آینه در دکوراسیون داخلی.";
if (q || category) {
const title = q ? `نتایج جست‌وجوی «${q}»` : "مقالات دسته‌بندی انتخاب‌شده";
return {
title,
description: `${title} در مجله تخصصی گلسموند.`,
alternates: { canonical: "/blog" },
robots: { index: false, follow: true },
};
}
const canonical = page > 1 ? `/blog?page=${page}` : "/blog";
const title = page > 1 ? `مجله گلسموند – صفحه ${page}` : "مجله گلسموند";
return {
title,
description: page > 1 ? `${baseDescription} صفحه ${page}.` : baseDescription,
alternates: { canonical },
openGraph: {
title,
description: "مقالات تخصصی درباره آینه هوشمند، نورپردازی و دکوراسیون داخلی.",
url: canonical,
images: ["/images/articles/decorative-mirrors.jpg"],
},
};
}
function pageHref(page: number, q: string, category: string) {
const query = new URLSearchParams();
if (page > 1) query.set("page", String(page));
if (q) query.set("q", q);
if (category) query.set("category", category);
const value = query.toString();
return value ? `/blog?${value}` : "/blog";
}
export default async function BlogPage({
searchParams,
}: {
searchParams: Promise<BlogSearchParams>;
}) {
const params = await searchParams;
const page = Math.max(1, Number.parseInt(params.page || "1", 10) || 1);
const q = (params.q || "").trim();
const category = (params.category || "").trim();
const [blogs, categories] = await Promise.all([
getBlogs({ page, pageSize: 9, title: q, categoryId: category }).catch(() => ({
items: [],
total: 0,
page,
pageSize: 9,
})),
getCategories("blog").catch(() => ({ items: [] })),
]);
const totalPages = Math.max(1, Math.ceil(blogs.total / blogs.pageSize));
return (
<>
<PageHero englishTitle="JOURNAL" title="مجله گلسموند" />
<section className="blog-list-section">
<div className="container">
<form className="catalog-filters blog-filters" action="/blog" method="get">
<label>
<span>جست‌وجوی مقاله</span>
<div>
<input type="search" name="q" defaultValue={q} placeholder="عنوان مقاله..." />
<Search />
</div>
</label>
<label>
<span>دسته‌بندی</span>
<select name="category" defaultValue={category}>
<option value="">همه مقالات</option>
{categories.items.map((item) => (
<option key={item.id} value={item.id}>{item.nameFa || item.name}</option>
))}
</select>
</label>
<button className="primary-button" type="submit">جست‌وجو</button>
</form>
{blogs.items.length > 0 ? (
<div className="blog-list-grid">
{blogs.items.map((blog) => <ApiBlogCard key={blog.id} blog={blog} />)}
</div>
) : (
<div className="empty-state">
<h2>مقاله‌ای پیدا نشد</h2>
<p>عبارت جست‌وجو را تغییر دهید یا فیلترها را حذف کنید.</p>
<Link href="/blog">نمایش همه مقالات</Link>
</div>
)}
{totalPages > 1 && (
<nav className="pagination" aria-label="صفحه‌بندی مقالات">
{page > 1 && (
<Link href={pageHref(page - 1, q, category)}><ArrowRight />قبلی</Link>
)}
<span>صفحه {page.toLocaleString("fa-IR")} از {totalPages.toLocaleString("fa-IR")}</span>
{page < totalPages && (
<Link href={pageHref(page + 1, q, category)}>بعدی<ArrowLeft /></Link>
)}
</nav>
)}
</div>
</section>
</>
);
}
+93
View File
@@ -0,0 +1,93 @@
import type { Metadata } from "next";
import { Camera, Clock3, MapPin, MessageCircle, Phone } from "lucide-react";
import { ContactForm } from "@/components/contact-form";
import { PageHero, SectionHeading } from "@/components/sections";
import { contact } from "@/data/site";
export const metadata: Metadata = {
title: "تماس با ما",
description: "شماره تماس، آدرس کارخانه و فرم ارسال پیام مستقیم به تیم آینه هوشمند گلسموند.",
alternates: { canonical: "/contact" },
openGraph: {
title: "تماس با گلسموند",
description: "راه‌های ارتباط مستقیم با تیم فروش و پشتیبانی گلسموند.",
url: "/contact",
images: ["/images/hero.jpg"],
},
};
export default function ContactPage() {
return (
<>
<PageHero englishTitle="CONTACT US" title="با ما تماس بگیرید" />
<section className="contact-section">
<div className="container">
<SectionHeading
eyebrow=""
englishTitle="CONTACT INFO"
title="اطلاعات تماس با ما"
description="هفت روز هفته می‌توانید از راه‌های زیر با تیم گلسموند در ارتباط باشید."
/>
<div className="contact-cards">
<article>
<span><Phone /></span>
<h3>شماره تماس</h3>
<a href={contact.mobileHref}>{contact.mobile}</a>
</article>
<article>
<span><MapPin /></span>
<h3>آدرس کارخانه</h3>
<p>{contact.address}</p>
</article>
<article>
<span><Clock3 /></span>
<h3>زمان پاسخ‌گویی</h3>
<p>هفت روز هفته</p>
</article>
</div>
<div className="contact-main">
<div className="form-panel">
<SectionHeading
eyebrow=""
englishTitle="SEND A MESSAGE"
title="پیامی برای ما ارسال کنید"
description="فرم را تکمیل کنید تا پیام شما مستقیم برای تیم گلسموند ثبت شود."
align="start"
/>
<ContactForm />
</div>
<aside className="contact-aside">
<h3>در شبکه‌های اجتماعی همراه ما باشید</h3>
<p>نمونه محصولات تازه، خبرها و پروژه‌های اجراشده گلسموند را ببینید.</p>
<a href={contact.instagram} target="_blank" rel="noreferrer">
<Camera />
اینستاگرام گلسموند
</a>
<a href={contact.whatsapp} target="_blank" rel="noreferrer">
<MessageCircle />
گفت‌وگو در واتساپ
</a>
</aside>
</div>
</div>
</section>
<section className="map-section" aria-label="موقعیت گلسموند روی نقشه">
<iframe
src="https://www.google.com/maps/embed?pb=!1m14!1m8!1m3!1d3282.943383616102!2d50.85739!3d34.630871!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x3f93bb7deb55b5fb%3A0xc59744e996908549!2z2YXYrNiq2YXYuSDYqtis2KfYsduMINmF2LPaqdmI2YbbjCDZgdix2K_ZiNiz!5e0!3m2!1sfa!2s!4v1704776476571!5m2!1sfa!2s"
width="100%"
height="480"
style={{ border: 0 }}
allowFullScreen
loading="lazy"
referrerPolicy="no-referrer-when-downgrade"
title="موقعیت کارخانه گلسموند"
/>
</section>
</>
);
}
+129
View File
@@ -0,0 +1,129 @@
import type { Metadata } from "next";
import Image from "next/image";
import { Droplets, MousePointerClick, ShieldCheck, SunMedium } from "lucide-react";
import { Features, PageHero, SectionHeading, VideoBanner } from "@/components/sections";
export const metadata: Metadata = {
title: "ویژگی‌ها",
description:
"ویژگی‌های آینه هوشمند گلسموند؛ فناوری ضد بخار، نور استاندارد، کنترل لمسی، ایمنی ۱۲ ولت و استاندارد IP64.",
alternates: { canonical: "/features" },
openGraph: {
title: "ویژگی‌های آینه هوشمند گلسموند",
description:
"فناوری ضد بخار، نور استاندارد، کنترل لمسی و ایمنی ۱۲ ولت را بیشتر بشناسید.",
url: "/features",
images: ["/images/about/gallery-2.jpg"],
},
};
const technologies = [
{
icon: Droplets,
english: "ANTI-FOG",
title: "فناوری ضد بخار",
description:
"گرمایش یکنواخت سطح آینه، دیدی شفاف و بدون بخار را در فضای مرطوب حمام حفظ می‌کند.",
},
{
icon: SunMedium,
english: "SMART LIGHT",
title: "نورپردازی استاندارد",
description:
"نور یکدست و کاربردی برای اصلاح، آرایش و استفاده روزمره، بدون ایجاد سایه‌های مزاحم.",
},
{
icon: MousePointerClick,
english: "TOUCH CONTROL",
title: "کنترل لمسی",
description:
"کلیدهای لمسی هوشمند با دسترسی سریع و ظاهری مینیمال، هماهنگ با طراحی یکپارچه آینه.",
},
{
icon: ShieldCheck,
english: "SAFE POWER",
title: "ایمنی ۱۲ ولت",
description:
"استفاده از برق ۱۲ ولت و استاندارد IP64 برای کاهش مصرف انرژی و افزایش ایمنی در محیط مرطوب.",
},
];
export default function FeaturesPage() {
return (
<>
<PageHero englishTitle="FEATURES" title="ویژگی‌های آینه هوشمند گلسموند" />
<section className="features-intro">
<div className="container features-intro-grid">
<div className="features-intro-image">
<Image
src="/images/about/gallery-2.jpg"
alt="جزئیات آینه هوشمند گلسموند"
fill
sizes="(max-width: 860px) 100vw, 50vw"
priority
/>
<span>GLASSMOND</span>
</div>
<div className="features-intro-copy">
<SectionHeading
eyebrow=""
englishTitle="SMART TECHNOLOGY"
title="هوشمندی در تمام جزئیات"
align="start"
/>
<p>
آینه‌های گلسموند با تمرکز بر ایمنی، دوام و تجربه استفاده روزمره
طراحی شده‌اند. فناوری ضد بخار، نور استاندارد و کنترل لمسی در ساختاری
یکپارچه قرار گرفته‌اند تا محصول در عین زیبایی، کاملا کاربردی باشد.
</p>
<div className="feature-specs">
<div>
<strong>IP64</strong>
<span>مقاوم در برابر نفوذ آب و بخار</span>
</div>
<div>
<strong>۱۲V</strong>
<span>ایمن و کم‌مصرف</span>
</div>
<div>
<strong>۱۰</strong>
<span>سال خدمات پس از فروش</span>
</div>
</div>
</div>
</div>
</section>
<Features />
<section className="technology-section">
<div className="container">
<SectionHeading
eyebrow=""
englishTitle="CORE FEATURES"
title="تکنولوژی برای آسایش بیشتر"
description="هر قابلیت با هدف استفاده ساده‌تر، ایمن‌تر و ماندگارتر طراحی شده است."
/>
<div className="technology-grid">
{technologies.map((item, index) => {
const Icon = item.icon;
return (
<article key={item.english}>
<span className="technology-number">{String(index + 1).padStart(2, "0")}</span>
<Icon />
<small>{item.english}</small>
<h3>{item.title}</h3>
<p>{item.description}</p>
</article>
);
})}
</div>
</div>
</section>
<VideoBanner />
</>
);
}
+3371
View File
File diff suppressed because it is too large Load Diff
+38
View File
@@ -0,0 +1,38 @@
import type { Metadata } from "next";
import { AuthProvider } from "@/components/auth-provider";
import { Footer } from "@/components/footer";
import { Header } from "@/components/header";
import { MotionObserver } from "@/components/motion-observer";
import "./globals.css";
export const metadata: Metadata = {
metadataBase: new URL("https://glassmond.com"),
title: {
default: "آینه هوشمند گلسموند",
template: "%s | گلسموند",
},
description:
"تولیدکننده آینه‌های هوشمند، ضد بخار و آرایشی با استاندارد IP64، گارانتی یکساله و ده سال خدمات پس از فروش.",
icons: {
icon: "/images/logo.png",
},
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="fa" dir="rtl">
<body>
<AuthProvider>
<MotionObserver />
<Header />
<main>{children}</main>
<Footer />
</AuthProvider>
</body>
</html>
);
}
+9
View File
@@ -0,0 +1,9 @@
import { NextResponse } from "next/server";
import { generatedStaticPages } from "@/data/generated-sitemap-pages";
export function GET() {
return NextResponse.json({
baseUrl: "https://glassmond.com",
staticPages: generatedStaticPages,
});
}
+44
View File
@@ -0,0 +1,44 @@
import { NextResponse } from "next/server";
export function GET() {
return NextResponse.json({
slots: [
{
key: "home-hero",
label: "Homepage hero",
kind: "single",
pageKey: "home",
aspectRatio: "16:9",
itemCount: 1,
recommendedWidth: 1920,
},
{
key: "home-video-cover",
label: "Homepage video cover",
kind: "single",
pageKey: "home",
aspectRatio: "16:7",
itemCount: 1,
recommendedWidth: 1600,
},
{
key: "inner-page-hero",
label: "Inner page hero",
kind: "single",
pageKey: "global",
aspectRatio: "16:7",
itemCount: 1,
recommendedWidth: 1920,
},
{
key: "about-gallery",
label: "About gallery",
kind: "list",
pageKey: "about",
aspectRatio: "1:1",
itemCount: 4,
recommendedWidth: 1200,
},
],
});
}
+156
View File
@@ -0,0 +1,156 @@
import type { Metadata } from "next";
import Image from "next/image";
import Link from "next/link";
import { ArrowLeft, ArrowUpLeft, CheckCircle2 } from "lucide-react";
import { ProductCarousel } from "@/components/product-carousel";
import { Features, SectionHeading, VideoBanner } from "@/components/sections";
import {
blogUrl,
formatPersianDate,
getBlogs,
getProducts,
} from "@/lib/meshkee";
export const metadata: Metadata = {
title: "آینه هوشمند ضد بخار",
description:
"گلسموند، تولیدکننده آینه هوشمند ضد بخار با نور حرفه‌ای، استاندارد IP64، یک سال گارانتی و ده سال خدمات پس از فروش.",
alternates: { canonical: "/" },
openGraph: {
title: "آینه هوشمند ضد بخار گلسموند",
description:
"آینه‌های هوشمند گلسموند؛ تلفیق طراحی مدرن، نور حرفه‌ای و فناوری ضد بخار.",
url: "/",
type: "website",
locale: "fa_IR",
images: ["/images/hero.jpg"],
},
};
export default async function HomePage() {
const [productData, blogData] = await Promise.all([
getProducts({ pageSize: 12 }).catch(() => ({ items: [], total: 0, page: 1, pageSize: 12 })),
getBlogs({ pageSize: 4 }).catch(() => ({ items: [], total: 0, page: 1, pageSize: 4 })),
]);
return (
<>
<section className="home-hero">
<div className="container hero-content">
<span className="hero-kicker">اولین آینه ضد بخار در ایران</span>
<h1>
زیبایی را <strong>هوشمندانه</strong> ببینید
</h1>
<p>
آینه‌های هوشمند گلسموند؛ تلفیقی از طراحی مدرن، نور حرفه‌ای و فناوری
ضد بخار برای تجربه‌ای متفاوت.
</p>
<div className="hero-actions">
<Link href="/products" className="primary-button">
مشاهده محصولات
<ArrowLeft />
</Link>
<Link href="/about" className="text-button">
درباره گلسموند
<ArrowUpLeft />
</Link>
</div>
</div>
<div className="hero-index" aria-hidden="true">
<span>01</span>
<i />
<small>GLASSMOND</small>
</div>
<a href="#products" className="scroll-hint" aria-label="رفتن به محصولات">
<span>اسکرول کنید</span>
<i />
</a>
</section>
<section id="products" className="products-section">
<div className="container">
<SectionHeading
eyebrow=""
englishTitle="NEW PRODUCTS"
title="محصولات جدید ما"
description="اولین آینه ضد بخار در ایران"
/>
{productData.items.length > 0 ? (
<ProductCarousel products={productData.items} />
) : (
<p className="empty-state">در حال حاضر محصولی برای نمایش وجود ندارد.</p>
)}
</div>
</section>
<section className="resume-section">
<div className="container resume-grid">
<div>
<SectionHeading
eyebrow=""
englishTitle="OUR STORY"
title="یک دهه نوآوری در صنعت آینه هوشمند"
align="start"
/>
<p className="resume-text">
صادرات به کشورهای خاورمیانه، همکاری با بزرگ‌ترین شرکت واگن‌سازی و
لوکوموتیوسازی خاورمیانه (پارس مپنا) و ایجاد نمایندگی‌های معتبر و
فعال در ایران و مناطق شمال کشور عراق، بخشی از مسیر گلسموند است.
</p>
<ul className="check-list">
<li><CheckCircle2 /> ثبت اختراع آینه هوشمند ضد بخار</li>
<li><CheckCircle2 /> تولید صنعتی با استاندارد IP64</li>
<li><CheckCircle2 /> یک سال گارانتی و ده سال خدمات</li>
</ul>
</div>
<div className="resume-image">
<Image src="/images/about/gallery-2.jpg" alt="آینه هوشمند گلسموند" fill sizes="50vw" />
<div className="experience-badge">
<strong>+۱۰</strong>
<span>سال تجربه و نوآوری</span>
</div>
</div>
</div>
</section>
<Features />
<VideoBanner />
<section id="articles" className="articles-section">
<div className="container">
<SectionHeading
eyebrow=""
englishTitle="JOURNAL"
title="جدیدترین مقالات"
description="مطالب کاربردی درباره آینه، طراحی داخلی و دکوراسیون مدرن"
/>
<div className="articles-grid">
{blogData.items.map((article) => (
<article className="article-card" key={article.id}>
<div className="article-image">
<Link href={blogUrl(article)}>
<Image
src={article.titleImageUrl || "/images/articles/decorative-mirrors.jpg"}
alt={article.title}
fill
sizes="(max-width: 800px) 100vw, 25vw"
/>
</Link>
</div>
<div className="article-copy">
<span>{formatPersianDate(article.publishedAt)}</span>
<h3><Link href={blogUrl(article)}>{article.title}</Link></h3>
<p>{article.abstract}</p>
<Link href={blogUrl(article)} aria-label={`مطالعه ${article.title}`}>
ادامه مطلب
<ArrowLeft />
</Link>
</div>
</article>
))}
</div>
</div>
</section>
</>
);
}
+178
View File
@@ -0,0 +1,178 @@
import type { Metadata } from "next";
import Link from "next/link";
import { MessageCircle, Phone, ShieldCheck } from "lucide-react";
import { notFound, redirect } from "next/navigation";
import { ApiProductCard } from "@/components/api-product-card";
import { ProductTabs } from "@/components/entity-comments";
import { ProductGallery } from "@/components/product-gallery";
import {
getComments,
getProductById,
getProducts,
plainText,
productImage,
productUrl,
sanitizeCmsContent,
slugifyFa,
} from "@/lib/meshkee";
type ProductPageProps = {
params: Promise<{ id: string; slug: string }>;
};
export async function generateStaticParams() {
const data = await getProducts({ pageSize: 50 }).catch(() => ({ items: [] }));
return data.items.map((product) => ({
id: product.id,
slug: slugifyFa(product.nameFa || product.title),
}));
}
export async function generateMetadata({ params }: ProductPageProps): Promise<Metadata> {
const { id } = await params;
const data = await getProductById(id).catch(() => null);
if (!data) return { title: "محصول یافت نشد", robots: { index: false } };
const product = data.product;
const title = product.nameFa || product.title;
const description =
plainText(product.summary || product.descriptionHtml).slice(0, 155) ||
`مشخصات و تصاویر ${title} از محصولات گلسموند.`;
const image = productImage(product);
const canonical = productUrl(product);
return {
title,
description,
alternates: { canonical },
openGraph: {
title,
description,
url: canonical,
images: image ? [image] : [],
},
};
}
export default async function ProductDetailPage({ params }: ProductPageProps) {
const { id, slug } = await params;
const data = await getProductById(id).catch(() => null);
if (!data) notFound();
const { product, relatedProducts = [] } = data;
const canonical = productUrl(product);
const expectedSlug = canonical.split("/").at(-1) || "";
let currentSlug = slug;
try {
currentSlug = decodeURIComponent(slug);
} catch {
currentSlug = slug;
}
if (currentSlug !== expectedSlug) redirect(encodeURI(canonical));
const title = product.nameFa || product.title;
const mainImage = productImage(product);
const images = Array.from(
new Set([mainImage, ...(product.images || []).map((item) => item.url)].filter(Boolean)),
) as string[];
const comments = await getComments("product", product.id);
const categoryTitle = product.categoryNameFa || product.categoryName || "آینه";
const categoryHref = product.categoryId
? `/products/category/${product.categoryId}/${slugifyFa(categoryTitle)}`
: null;
const descriptionHtml = sanitizeCmsContent(
product.descriptionHtml || product.summary || "اطلاعات تکمیلی این محصول به‌زودی درج می‌شود.",
title,
);
return (
<>
<section className="product-detail-top">
<div className="container">
<nav className="detail-breadcrumbs" aria-label="مسیر صفحه">
<Link href="/">صفحه اصلی</Link>
<span>/</span>
<Link href="/products">محصولات</Link>
<span>/</span>
<b>{title}</b>
</nav>
<div className="product-detail-grid">
<ProductGallery images={images.length ? images : ["/images/hero.jpg"]} alt={title} />
<div className="product-detail-copy">
<span className="english-title">PRODUCT DETAILS</span>
<p className="product-category">{product.categoryNameFa || "آینه هوشمند"}</p>
<h1>{title}</h1>
{product.summary && <p className="product-summary">{product.summary}</p>}
<dl className="product-facts">
<div>
<dt>دسته‌بندی</dt>
<dd>
{categoryHref ? <Link href={categoryHref}>{categoryTitle}</Link> : categoryTitle}
</dd>
</div>
<div>
<dt>وضعیت سفارش</dt>
<dd>{product.store?.inStock ? "موجود" : "تماس بگیرید"}</dd>
</div>
<div>
<dt>خدمات</dt>
<dd>یک سال گارانتی و ده سال پشتیبانی</dd>
</div>
</dl>
<div className="product-assurance">
<ShieldCheck />
<p>تولید مطابق استاندارد IP64 با برق ایمن ۱۲ ولت</p>
</div>
<div className="product-detail-actions">
<a className="primary-button" href="tel:+989301293902">
<Phone />
تماس برای سفارش
</a>
<a
className="outline-dark-button"
href="https://wa.me/989301293902"
target="_blank"
rel="noreferrer"
>
<MessageCircle />
واتساپ
</a>
</div>
</div>
</div>
</div>
</section>
<section className="product-information">
<div className="container">
<ProductTabs
descriptionHtml={descriptionHtml}
entityId={product.id}
comments={comments.items}
/>
</div>
</section>
{relatedProducts.length > 0 && (
<section className="related-section">
<div className="container">
<div className="section-heading">
<span className="english-title">RELATED PRODUCTS</span>
<h2>محصولات مرتبط</h2>
</div>
<div className="catalog-grid">
{relatedProducts.slice(0, 3).map((item) => (
<ApiProductCard key={item.id} product={item} headingLevel={3} />
))}
</div>
</div>
</section>
)}
</>
);
}
+107
View File
@@ -0,0 +1,107 @@
import type { Metadata } from "next";
import { notFound, redirect } from "next/navigation";
import { ApiProductCard } from "@/components/api-product-card";
import { PageHero } from "@/components/sections";
import {
getCategories,
getCategoryById,
getProducts,
productCategoryUrl,
slugifyFa,
} from "@/lib/meshkee";
type ProductCategoryPageProps = {
params: Promise<{ id: string; slug: string }>;
};
export async function generateStaticParams() {
const data = await getCategories("product").catch(() => ({ items: [] }));
return data.items.map((category) => ({
id: category.id,
slug: slugifyFa(category.nameFa || category.name),
}));
}
export async function generateMetadata({
params,
}: ProductCategoryPageProps): Promise<Metadata> {
const { id } = await params;
const data = await getCategoryById(id).catch(() => null);
if (!data) return { title: "دسته‌بندی یافت نشد", robots: { index: false } };
const title = data.category.nameFa || data.category.name;
const canonical = productCategoryUrl(data.category);
const description =
data.category.description ||
`مشاهده محصولات دسته ${title} گلسموند، شامل آینه‌های هوشمند با طراحی مدرن و فناوری ضد بخار.`;
return {
title: `${title} | محصولات`,
description,
alternates: { canonical },
openGraph: {
title: `${title} | محصولات گلسموند`,
description,
url: canonical,
},
};
}
export default async function ProductCategoryPage({
params,
}: ProductCategoryPageProps) {
const { id, slug } = await params;
const categoryData = await getCategoryById(id).catch(() => null);
if (!categoryData) notFound();
const { category } = categoryData;
const canonical = productCategoryUrl(category);
const expectedSlug = canonical.split("/").at(-1) || "";
let currentSlug = slug;
try {
currentSlug = decodeURIComponent(slug);
} catch {
currentSlug = slug;
}
if (currentSlug !== expectedSlug) redirect(encodeURI(canonical));
const products = await getProducts({ categoryId: category.id, pageSize: 24 }).catch(() => ({
items: [],
total: 0,
page: 1,
pageSize: 24,
}));
const title = category.nameFa || category.name;
return (
<>
<PageHero englishTitle="PRODUCT CATEGORY" title={title} />
<section className="catalog-section category-catalog">
<div className="container">
<header className="category-intro">
<span className="english-title">GLASSMOND COLLECTION</span>
<h2>محصولات {title}</h2>
<p>
{category.description ||
`مجموعه محصولات ${title} گلسموند با تمرکز بر کیفیت ساخت، ایمنی و طراحی ماندگار.`}
</p>
</header>
{products.items.length > 0 ? (
<div className="catalog-grid">
{products.items.map((product) => (
<ApiProductCard key={product.id} product={product} />
))}
</div>
) : (
<div className="empty-state">
<h2>محصولی در این دسته ثبت نشده است</h2>
<p>برای مشاهده محصولات جدید دوباره به این صفحه سر بزنید.</p>
</div>
)}
</div>
</section>
</>
);
}
+165
View File
@@ -0,0 +1,165 @@
import type { Metadata } from "next";
import Link from "next/link";
import { ArrowLeft, ArrowRight, Search } from "lucide-react";
import { ApiProductCard } from "@/components/api-product-card";
import { PageHero } from "@/components/sections";
import {
getCategories,
getCategoryById,
getProducts,
productCategoryUrl,
} from "@/lib/meshkee";
type ProductsSearchParams = {
page?: string;
q?: string;
category?: string;
};
export async function generateMetadata({
searchParams,
}: {
searchParams: Promise<ProductsSearchParams>;
}): Promise<Metadata> {
const params = await searchParams;
const page = Math.max(1, Number.parseInt(params.page || "1", 10) || 1);
const q = (params.q || "").trim();
const categoryId = (params.category || "").trim();
const baseDescription =
"مشاهده محصولات آینه هوشمند، ضد بخار و آرایشی گلسموند با تصاویر، مشخصات و جزئیات کامل.";
if (q || categoryId) {
const category = categoryId
? await getCategoryById(categoryId).then((data) => data.category).catch(() => null)
: null;
const label = q
? `نتایج جست‌وجوی «${q}»`
: `محصولات ${category?.nameFa || category?.name || "دسته‌بندی انتخاب‌شده"}`;
const canonical = category && !q ? productCategoryUrl(category) : "/products";
return {
title: label,
description: `${label} در مجموعه محصولات آینه هوشمند گلسموند.`,
alternates: { canonical },
robots: { index: false, follow: true },
};
}
const canonical = page > 1 ? `/products?page=${page}` : "/products";
const title = page > 1 ? `محصولات آینه هوشمند – صفحه ${page}` : "محصولات آینه هوشمند";
return {
title,
description: page > 1 ? `${baseDescription} صفحه ${page}.` : baseDescription,
alternates: { canonical },
openGraph: {
title: "محصولات آینه هوشمند گلسموند",
description: "مجموعه آینه‌های هوشمند، ضد بخار و آرایشی گلسموند را مشاهده کنید.",
url: canonical,
images: ["/images/hero.jpg"],
},
};
}
function pageHref(page: number, q: string, category: string) {
const query = new URLSearchParams();
if (page > 1) query.set("page", String(page));
if (q) query.set("q", q);
if (category) query.set("category", category);
const value = query.toString();
return value ? `/products?${value}` : "/products";
}
export default async function ProductsPage({
searchParams,
}: {
searchParams: Promise<ProductsSearchParams>;
}) {
const params = await searchParams;
const page = Math.max(1, Number.parseInt(params.page || "1", 10) || 1);
const q = (params.q || "").trim();
const category = (params.category || "").trim();
const [products, categories] = await Promise.all([
getProducts({ page, pageSize: 12, name: q, categoryId: category }).catch(() => ({
items: [],
total: 0,
page,
pageSize: 12,
})),
getCategories("product").catch(() => ({ items: [] })),
]);
const totalPages = Math.max(1, Math.ceil(products.total / products.pageSize));
return (
<>
<PageHero englishTitle="PRODUCTS" title="محصولات گلسموند" />
<section className="catalog-section">
<div className="container">
<form className="catalog-filters" action="/products" method="get">
<label>
<span>جست‌وجوی محصول</span>
<div>
<input
type="search"
name="q"
defaultValue={q}
placeholder="نام محصول را وارد کنید..."
/>
<Search />
</div>
</label>
<label>
<span>دسته‌بندی</span>
<select name="category" defaultValue={category}>
<option value="">همه محصولات</option>
{categories.items.map((item) => (
<option key={item.id} value={item.id}>{item.nameFa || item.name}</option>
))}
</select>
</label>
<button className="primary-button" type="submit">اعمال فیلتر</button>
</form>
<div className="catalog-summary">
<p><strong>{products.total.toLocaleString("fa-IR")}</strong> محصول</p>
{(q || category) && <Link href="/products">حذف فیلترها</Link>}
</div>
{products.items.length > 0 ? (
<div className="catalog-grid">
{products.items.map((product) => (
<ApiProductCard key={product.id} product={product} />
))}
</div>
) : (
<div className="empty-state">
<h2>محصولی پیدا نشد</h2>
<p>عبارت جست‌وجو یا دسته‌بندی را تغییر دهید.</p>
</div>
)}
{totalPages > 1 && (
<nav className="pagination" aria-label="صفحه‌بندی محصولات">
{page > 1 && (
<Link href={pageHref(page - 1, q, category)}>
<ArrowRight />
قبلی
</Link>
)}
<span>صفحه {page.toLocaleString("fa-IR")} از {totalPages.toLocaleString("fa-IR")}</span>
{page < totalPages && (
<Link href={pageHref(page + 1, q, category)}>
بعدی
<ArrowLeft />
</Link>
)}
</nav>
)}
</div>
</section>
</>
);
}