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:
@@ -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 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
@@ -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
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user