Add Shirzad Group storefront connected to the Meshkee Website API.
Co-authored-by: Cursor <cursoragent@cursor.com>
@@ -1,7 +1,13 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{ protocol: "https", hostname: "c804387.parspack.net" },
|
||||
{ protocol: "https", hostname: "cdn.meshkee.com" },
|
||||
{ protocol: "https", hostname: "meshkee-storage.sas.amin.parminstorage.ir" },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 120 KiB |
|
After Width: | Height: | Size: 180 KiB |
|
After Width: | Height: | Size: 81 KiB |
|
After Width: | Height: | Size: 287 KiB |
|
After Width: | Height: | Size: 247 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 213 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 129 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 102 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 996 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 57 KiB |
|
After Width: | Height: | Size: 115 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 83 KiB |
|
After Width: | Height: | Size: 95 KiB |
|
After Width: | Height: | Size: 102 KiB |
|
After Width: | Height: | Size: 97 KiB |
|
After Width: | Height: | Size: 93 KiB |
|
After Width: | Height: | Size: 170 KiB |
|
After Width: | Height: | Size: 161 KiB |
|
After Width: | Height: | Size: 166 KiB |
|
After Width: | Height: | Size: 164 KiB |
|
After Width: | Height: | Size: 160 KiB |
|
After Width: | Height: | Size: 165 KiB |
|
After Width: | Height: | Size: 164 KiB |
|
After Width: | Height: | Size: 165 KiB |
|
After Width: | Height: | Size: 165 KiB |
|
After Width: | Height: | Size: 166 KiB |
|
After Width: | Height: | Size: 157 KiB |
|
After Width: | Height: | Size: 160 KiB |
|
After Width: | Height: | Size: 166 KiB |
|
After Width: | Height: | Size: 178 KiB |
|
After Width: | Height: | Size: 161 KiB |
|
After Width: | Height: | Size: 161 KiB |
|
After Width: | Height: | Size: 166 KiB |
|
After Width: | Height: | Size: 162 KiB |
|
After Width: | Height: | Size: 157 KiB |
|
After Width: | Height: | Size: 155 KiB |
|
After Width: | Height: | Size: 160 KiB |
|
After Width: | Height: | Size: 108 KiB |
|
After Width: | Height: | Size: 112 KiB |
|
After Width: | Height: | Size: 111 KiB |
@@ -0,0 +1,98 @@
|
||||
import type { Metadata } from "next";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { PageHero } from "@/components/PageHero";
|
||||
import { TrustBar } from "@/components/TrustBar";
|
||||
import { aboutCategories, aboutContent, site } from "@/data/site";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: `درباره ما | ${site.name}`,
|
||||
description: site.about,
|
||||
};
|
||||
|
||||
export default function AboutPage() {
|
||||
return (
|
||||
<>
|
||||
<PageHero
|
||||
title="دربـــــاره مـــــا"
|
||||
subtitle="درباره گروه صنعتی شیرزاد بیشتر بدانید"
|
||||
image="/images/pages/about-hero.jpg"
|
||||
crumbs={[
|
||||
{ href: "/", label: "صفحه اصلی" },
|
||||
{ label: "درباره ما" },
|
||||
]}
|
||||
/>
|
||||
|
||||
<section className="mx-auto grid max-w-7xl items-center gap-10 px-4 py-12 lg:grid-cols-2">
|
||||
<div>
|
||||
<h2 className="text-lg font-medium">{aboutContent.introTitle}</h2>
|
||||
<p className="mt-1 text-3xl font-black text-primary sm:text-4xl">
|
||||
{aboutContent.introLead}
|
||||
</p>
|
||||
<div className="mt-6 space-y-4 text-justify text-sm leading-8 text-muted">
|
||||
{aboutContent.story.map((paragraph) => (
|
||||
<p key={paragraph.slice(0, 24)}>{paragraph}</p>
|
||||
))}
|
||||
</div>
|
||||
<Link
|
||||
href="/contact"
|
||||
className="mt-8 inline-flex rounded-full bg-primary px-6 py-3 text-sm text-white hover:bg-primary-dark"
|
||||
>
|
||||
همین حالا با ما تماس بگیرید ‹
|
||||
</Link>
|
||||
</div>
|
||||
<div className="relative mx-auto min-h-[320px] w-full max-w-lg sm:min-h-[420px]">
|
||||
<Image
|
||||
src="/images/pages/about-front.png"
|
||||
alt={site.name}
|
||||
width={720}
|
||||
height={720}
|
||||
className="mr-auto w-[78%] rounded-[28px] bg-black object-contain"
|
||||
/>
|
||||
<Image
|
||||
src="/images/pages/about-back.png"
|
||||
alt=""
|
||||
width={420}
|
||||
height={420}
|
||||
className="absolute bottom-0 left-0 w-[48%] rounded-[28px] bg-black object-contain ring-4 ring-white"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="bg-[#fafafa]">
|
||||
<div className="mx-auto grid max-w-7xl gap-10 px-4 py-12 lg:grid-cols-2">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold">{site.name}</h2>
|
||||
<p className="mt-4 text-justify text-sm leading-8 text-muted">{site.about}</p>
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-lg font-bold">{aboutContent.historyTitle}</h2>
|
||||
<p className="mt-4 text-justify text-sm leading-8 text-muted">{aboutContent.history}</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mx-auto max-w-7xl px-4 py-12">
|
||||
<div className="mb-8 text-center">
|
||||
<h2 className="text-xl font-semibold">محصولات ما</h2>
|
||||
<p className="mt-1 text-sm text-muted">محصولات با کیفیت و متنوع</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4 lg:grid-cols-4">
|
||||
{aboutCategories.map((item) => (
|
||||
<Link
|
||||
key={item.title}
|
||||
href={item.href}
|
||||
className="flex flex-col items-center rounded-2xl bg-page px-4 py-8 text-center"
|
||||
>
|
||||
<Image src={item.icon} alt="" width={70} height={70} />
|
||||
<h3 className="mt-4 text-sm font-semibold">{item.title}</h3>
|
||||
<p className="mt-1 text-xs text-muted">{item.subtitle}</p>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<TrustBar />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import { CatalogImage } from "@/components/CatalogImage";
|
||||
import { getBlogBySlug, listBlogs } from "@/lib/api";
|
||||
import { site } from "@/data/site";
|
||||
import { formatDate } from "@/lib/format";
|
||||
|
||||
type Props = { params: Promise<{ slug: string }> };
|
||||
|
||||
export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
||||
const { slug } = await params;
|
||||
const blog = await getBlogBySlug(decodeURIComponent(slug));
|
||||
if (!blog) return { title: `مقاله | ${site.name}` };
|
||||
return {
|
||||
title: `${blog.title} | ${site.name}`,
|
||||
description: blog.excerpt,
|
||||
};
|
||||
}
|
||||
|
||||
export default async function ArticleDetailsPage({ params }: Props) {
|
||||
const { slug } = await params;
|
||||
const blog = await getBlogBySlug(decodeURIComponent(slug));
|
||||
if (!blog) notFound();
|
||||
|
||||
const related = (await listBlogs({ pageSize: 6 })).items.filter((item) => item.slug !== blog.slug).slice(0, 3);
|
||||
|
||||
return (
|
||||
<article className="mx-auto max-w-4xl px-4 py-10">
|
||||
<nav className="mb-6 text-xs text-muted">
|
||||
<Link href="/" className="hover:text-primary">صفحه اصلی</Link>
|
||||
<span className="mx-2">‹</span>
|
||||
<Link href="/articles" className="hover:text-primary">مقالات</Link>
|
||||
<span className="mx-2">‹</span>
|
||||
<span>{blog.title}</span>
|
||||
</nav>
|
||||
|
||||
<h1 className="text-2xl font-bold leading-10 text-primary sm:text-3xl">{blog.title}</h1>
|
||||
<p className="mt-3 text-sm text-muted">
|
||||
{[blog.author, formatDate(blog.publishedAt), blog.categoryName].filter(Boolean).join(" • ")}
|
||||
</p>
|
||||
|
||||
{blog.image ? (
|
||||
<CatalogImage
|
||||
src={blog.image}
|
||||
alt={blog.title}
|
||||
width={1200}
|
||||
height={640}
|
||||
className="mt-6 h-auto w-full rounded-2xl object-cover"
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{blog.html ? (
|
||||
<div className="rich-content mt-8" dangerouslySetInnerHTML={{ __html: blog.html }} />
|
||||
) : (
|
||||
<p className="mt-8 text-sm leading-8 text-muted">{blog.excerpt}</p>
|
||||
)}
|
||||
|
||||
{related.length > 0 ? (
|
||||
<section className="mt-12">
|
||||
<h2 className="mb-4 text-lg font-bold text-primary">مقالات مرتبط</h2>
|
||||
<div className="grid gap-4 sm:grid-cols-3">
|
||||
{related.map((item) => (
|
||||
<Link key={item.slug} href={item.href} className="overflow-hidden rounded-2xl bg-white ring-1 ring-line">
|
||||
<CatalogImage src={item.image} alt={item.title} className="h-28 w-full object-cover" />
|
||||
<p className="p-3 text-sm font-medium leading-6">{item.title}</p>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
) : null}
|
||||
</article>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import { CatalogImage } from "@/components/CatalogImage";
|
||||
import { Pagination } from "@/components/Pagination";
|
||||
import { listBlogs } from "@/lib/api";
|
||||
import { site } from "@/data/site";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: `مقالات | ${site.name}`,
|
||||
description: "مقالات آموزشی گروه صنعتی شیرزاد",
|
||||
};
|
||||
|
||||
export default async function ArticlesPage({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<{ page?: string }>;
|
||||
}) {
|
||||
const params = await searchParams;
|
||||
const page = Math.max(1, Number(params.page) || 1);
|
||||
const blogs = await listBlogs({ page, pageSize: 12 });
|
||||
|
||||
return (
|
||||
<section className="mx-auto max-w-7xl px-4 py-10">
|
||||
<h1 className="mb-6 text-2xl font-bold text-primary">مقالات ما</h1>
|
||||
{blogs.items.length === 0 ? (
|
||||
<p className="rounded-2xl bg-page px-6 py-16 text-center text-muted">مقالهای یافت نشد.</p>
|
||||
) : (
|
||||
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{blogs.items.map((article) => (
|
||||
<Link
|
||||
key={article.slug}
|
||||
href={article.href}
|
||||
className="overflow-hidden rounded-2xl bg-white ring-1 ring-line"
|
||||
>
|
||||
<CatalogImage
|
||||
src={article.image}
|
||||
alt={article.title}
|
||||
width={640}
|
||||
height={400}
|
||||
className="h-44 w-full object-cover"
|
||||
/>
|
||||
<div className="p-4">
|
||||
<h2 className="font-semibold leading-7">{article.title}</h2>
|
||||
<p className="mt-2 text-sm leading-7 text-muted">{article.excerpt}</p>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<Pagination
|
||||
page={blogs.page}
|
||||
pageSize={blogs.pageSize}
|
||||
total={blogs.total}
|
||||
hrefFor={(nextPage) => (nextPage > 1 ? `/articles?page=${nextPage}` : "/articles")}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import type { Metadata } from "next";
|
||||
import { ContactForm } from "@/components/ContactForm";
|
||||
import { PageHero } from "@/components/PageHero";
|
||||
import { TrustBar } from "@/components/TrustBar";
|
||||
import { site } from "@/data/site";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: `تماس با ما | ${site.name}`,
|
||||
description: `نشانی و راههای ارتباطی ${site.name}`,
|
||||
};
|
||||
|
||||
export default function ContactPage() {
|
||||
return (
|
||||
<>
|
||||
<PageHero
|
||||
title="تمــــاس بــــا مـــــا"
|
||||
subtitle="همین حالا با ما تماس بگیرید"
|
||||
image="/images/pages/contact-hero.jpg"
|
||||
crumbs={[
|
||||
{ href: "/", label: "صفحه اصلی" },
|
||||
{ label: "تماس با ما" },
|
||||
]}
|
||||
/>
|
||||
|
||||
<section className="mx-auto grid max-w-7xl gap-10 px-4 py-12 lg:grid-cols-2">
|
||||
<div>
|
||||
<h2 className="text-xl font-bold">اطلاعات تماس با ما</h2>
|
||||
<div className="mt-8 grid gap-8 sm:grid-cols-2">
|
||||
<div>
|
||||
<h3 className="mb-3 font-medium">نشانی ما</h3>
|
||||
<p className="flex gap-3 text-sm leading-8 text-muted">
|
||||
<LocationIcon />
|
||||
<span>{site.address}</span>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="mb-3 font-medium">تماس با ما</h3>
|
||||
<ul className="space-y-3 text-sm text-muted">
|
||||
<li className="flex items-center gap-3">
|
||||
<PhoneIcon />
|
||||
<a href={`tel:${site.salesManager}`} dir="ltr">
|
||||
{site.salesManager}
|
||||
</a>
|
||||
</li>
|
||||
<li className="flex items-center gap-3">
|
||||
<PhoneIcon />
|
||||
<a href="tel:02538203572" dir="ltr">
|
||||
{site.landline}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-xl font-bold">سوالی دارید؟</h2>
|
||||
<p className="mt-2 mb-6 text-sm text-muted">با ما در میان بگذارید</p>
|
||||
<ContactForm />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="h-[380px] w-full overflow-hidden bg-page">
|
||||
<iframe
|
||||
title="موقعیت گروه صنعتی شیرزاد"
|
||||
src={site.mapEmbed}
|
||||
className="h-full w-full border-0"
|
||||
loading="lazy"
|
||||
referrerPolicy="no-referrer-when-downgrade"
|
||||
allowFullScreen
|
||||
/>
|
||||
</section>
|
||||
|
||||
<TrustBar />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function LocationIcon() {
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" className="mt-1 h-5 w-5 shrink-0 text-primary" fill="none" stroke="currentColor" strokeWidth="1.8" aria-hidden>
|
||||
<path d="M12 21s7-6.2 7-11a7 7 0 1 0-14 0c0 4.8 7 11 7 11z" />
|
||||
<circle cx="12" cy="10" r="2.2" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function PhoneIcon() {
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" className="h-5 w-5 shrink-0 text-primary" fill="none" stroke="currentColor" strokeWidth="1.8" aria-hidden>
|
||||
<path d="M6.5 3.8h3.2l1.2 3-1.8 1.2a12.5 12.5 0 0 0 6.7 6.7l1.2-1.8 3 1.2v3.2a2 2 0 0 1-2.2 2A16.8 16.8 0 0 1 4.5 6a2 2 0 0 1 2-2.2z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
Before Width: | Height: | Size: 25 KiB |
@@ -1,26 +1,79 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
:root {
|
||||
--background: #ffffff;
|
||||
--foreground: #171717;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--font-sans: var(--font-geist-sans);
|
||||
--font-mono: var(--font-geist-mono);
|
||||
--color-primary: #2f7000;
|
||||
--color-primary-dark: #47701f;
|
||||
--color-secondary: #6b9700;
|
||||
--color-tertiary: #502600;
|
||||
--color-topbar: #6b9801;
|
||||
--color-muted: #5c6873;
|
||||
--color-page: #f7f7f7;
|
||||
--color-line: #ededed;
|
||||
--font-sans: var(--font-peyda);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--background: #0a0a0a;
|
||||
--foreground: #ededed;
|
||||
}
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
background: #ffffff;
|
||||
color: #2b2b2b;
|
||||
font-family: var(--font-peyda), Tahoma, sans-serif;
|
||||
}
|
||||
|
||||
.no-scrollbar::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.no-scrollbar {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.rich-content {
|
||||
color: #3d3d3d;
|
||||
font-size: 0.95rem;
|
||||
line-height: 2;
|
||||
}
|
||||
|
||||
.rich-content h1,
|
||||
.rich-content h2,
|
||||
.rich-content h3,
|
||||
.rich-content h4 {
|
||||
color: var(--color-primary);
|
||||
font-weight: 700;
|
||||
margin: 1.5rem 0 0.75rem;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.rich-content p,
|
||||
.rich-content div,
|
||||
.rich-content li {
|
||||
margin: 0.6rem 0;
|
||||
}
|
||||
|
||||
.rich-content ul,
|
||||
.rich-content ol {
|
||||
padding-right: 1.25rem;
|
||||
}
|
||||
|
||||
.rich-content ul {
|
||||
list-style: disc;
|
||||
}
|
||||
|
||||
.rich-content ol {
|
||||
list-style: decimal;
|
||||
}
|
||||
|
||||
.rich-content img {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
margin: 1rem auto;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.rich-content a {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
@@ -1,29 +1,42 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import localFont from "next/font/local";
|
||||
import { Header } from "@/components/Header";
|
||||
import { Footer } from "@/components/Footer";
|
||||
import { WhatsAppButton } from "@/components/WhatsAppButton";
|
||||
import { getLevelTwoProductCategories } from "@/lib/api";
|
||||
import { site } from "@/data/site";
|
||||
import "./globals.css";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
variable: "--font-geist-mono",
|
||||
subsets: ["latin"],
|
||||
const peyda = localFont({
|
||||
src: [
|
||||
{ path: "./fonts/peyda-regular.ttf", weight: "400", style: "normal" },
|
||||
{ path: "./fonts/peyda-medium.ttf", weight: "500", style: "normal" },
|
||||
{ path: "./fonts/peyda-semibold.ttf", weight: "600", style: "normal" },
|
||||
{ path: "./fonts/peyda-bold.ttf", weight: "700", style: "normal" },
|
||||
],
|
||||
variable: "--font-peyda",
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Create Next App",
|
||||
description: "Generated by create next app",
|
||||
title: `${site.name} | صفحه اصلی`,
|
||||
description: site.about,
|
||||
icons: { icon: "/favicon.png" },
|
||||
};
|
||||
|
||||
export default function RootLayout({ children }: LayoutProps<"/">) {
|
||||
export default async function RootLayout({
|
||||
children,
|
||||
}: Readonly<{ children: React.ReactNode }>) {
|
||||
const productCategories = await getLevelTwoProductCategories();
|
||||
|
||||
return (
|
||||
<html
|
||||
lang="en"
|
||||
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
|
||||
>
|
||||
<body className="min-h-full flex flex-col">{children}</body>
|
||||
<html lang="fa" dir="rtl" className={`${peyda.variable} h-full antialiased`}>
|
||||
<body className="min-h-full flex flex-col font-sans bg-white text-[#2b2b2b]">
|
||||
<Header productCategories={productCategories} />
|
||||
<main className="flex-1">{children}</main>
|
||||
<Footer />
|
||||
<WhatsAppButton />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,69 +1,55 @@
|
||||
import Image from "next/image";
|
||||
import { ArticleCarousel } from "@/components/ArticleCarousel";
|
||||
import { CategoryRow } from "@/components/CategoryRow";
|
||||
import { Hero } from "@/components/Hero";
|
||||
import { ProductCarousel } from "@/components/ProductCarousel";
|
||||
import { DiscountBanner, PromoBanners } from "@/components/PromoBanners";
|
||||
import { TrustBar } from "@/components/TrustBar";
|
||||
import { listBlogs, listCatalog } from "@/lib/api";
|
||||
import { getHomeStaticImageSlots, HOME_SLOT_KEYS, slotByKey } from "@/lib/static-images";
|
||||
|
||||
const FEED_CATEGORY_ID = "488";
|
||||
const EQUIPMENT_CATEGORY_ID = "377";
|
||||
|
||||
export default async function HomePage() {
|
||||
const [slots, catalog, feed, equipment, blogs] = await Promise.all([
|
||||
getHomeStaticImageSlots(),
|
||||
listCatalog({ pageSize: 12 }),
|
||||
listCatalog({ categoryId: FEED_CATEGORY_ID, pageSize: 12 }),
|
||||
listCatalog({ categoryId: EQUIPMENT_CATEGORY_ID, pageSize: 12 }),
|
||||
listBlogs({ pageSize: 12 }),
|
||||
]);
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div className="flex flex-col flex-1 items-center justify-center bg-zinc-50 font-sans dark:bg-black">
|
||||
<main className="flex flex-1 w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
|
||||
<Image
|
||||
className="dark:invert h-5 w-[100px]"
|
||||
src="/next.svg"
|
||||
alt="Next.js logo"
|
||||
width={100}
|
||||
height={20}
|
||||
priority
|
||||
/>
|
||||
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
|
||||
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
|
||||
To get started, edit the{" "}
|
||||
<code className="rounded bg-black/[.06] px-1.5 py-0.5 font-mono text-[0.9em] dark:bg-white/[.08]">
|
||||
page.tsx
|
||||
</code>{" "}
|
||||
file.
|
||||
</h1>
|
||||
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
|
||||
Looking for a starting point or more instructions? Head over to{" "}
|
||||
<a
|
||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
className="font-medium text-zinc-950 dark:text-zinc-50"
|
||||
>
|
||||
Templates
|
||||
</a>{" "}
|
||||
or the{" "}
|
||||
<a
|
||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
className="font-medium text-zinc-950 dark:text-zinc-50"
|
||||
>
|
||||
Learning
|
||||
</a>{" "}
|
||||
center.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
|
||||
<a
|
||||
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
|
||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
className="dark:invert h-[14px] w-4"
|
||||
src="/vercel.svg"
|
||||
alt="Vercel logomark"
|
||||
width={16}
|
||||
height={14}
|
||||
/>
|
||||
Deploy Now
|
||||
</a>
|
||||
<a
|
||||
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
|
||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Documentation
|
||||
</a>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<>
|
||||
<Hero
|
||||
slider={slotByKey(slots, HOME_SLOT_KEYS.slider)}
|
||||
beside={slotByKey(slots, HOME_SLOT_KEYS.besideSlider)}
|
||||
/>
|
||||
<CategoryRow />
|
||||
<ProductCarousel
|
||||
title="محصولات ما"
|
||||
subtitle="محصولات جدید و متنوع ما"
|
||||
icon="/images/icons/products.png"
|
||||
products={feed.items.length ? feed.items : catalog.items}
|
||||
href={feed.items.length ? `/products?categoryId=${FEED_CATEGORY_ID}` : "/products"}
|
||||
/>
|
||||
<PromoBanners slot={slotByKey(slots, HOME_SLOT_KEYS.twoBannerRow)} />
|
||||
<ProductCarousel
|
||||
title="تجهیزات پرورش"
|
||||
subtitle="دانخوری و تجهیزات مرغداری"
|
||||
icon="/images/icons/bag.png"
|
||||
products={equipment.items}
|
||||
href={`/products?categoryId=${EQUIPMENT_CATEGORY_ID}`}
|
||||
/>
|
||||
<DiscountBanner slot={slotByKey(slots, HOME_SLOT_KEYS.oneBannerRow)} />
|
||||
<ProductCarousel
|
||||
title="پرفروش ترین ها"
|
||||
subtitle="پرفروش ترین محصولات ما"
|
||||
icon="/images/icons/bestsellers.png"
|
||||
products={catalog.items}
|
||||
/>
|
||||
<ArticleCarousel articles={blogs.items} />
|
||||
<TrustBar />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import { CatalogImage } from "@/components/CatalogImage";
|
||||
import { ProductCarousel } from "@/components/ProductCarousel";
|
||||
import { CUSTOMER_URL, site } from "@/data/site";
|
||||
import { getCatalogItem, listCatalog } from "@/lib/api";
|
||||
import { formatPrice } from "@/lib/format";
|
||||
|
||||
type Props = { params: Promise<{ slug: string }> };
|
||||
|
||||
export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
||||
const { slug } = await params;
|
||||
const product = await getCatalogItem(decodeURIComponent(slug));
|
||||
if (!product) return { title: `محصول | ${site.name}` };
|
||||
return {
|
||||
title: `${product.title} | ${site.name}`,
|
||||
description: product.summary || site.about,
|
||||
};
|
||||
}
|
||||
|
||||
export default async function ProductDetailsPage({ params }: Props) {
|
||||
const { slug } = await params;
|
||||
const product = await getCatalogItem(decodeURIComponent(slug));
|
||||
if (!product) notFound();
|
||||
|
||||
const related = product.categoryId
|
||||
? (await listCatalog({ categoryId: product.categoryId, pageSize: 12 })).items.filter(
|
||||
(item) => item.id !== product.id && item.slug !== product.slug,
|
||||
)
|
||||
: [];
|
||||
|
||||
return (
|
||||
<section className="mx-auto max-w-7xl px-4 py-10">
|
||||
<nav className="mb-6 text-xs text-muted">
|
||||
<Link href="/" className="hover:text-primary">صفحه اصلی</Link>
|
||||
<span className="mx-2">‹</span>
|
||||
<Link href="/products" className="hover:text-primary">محصولات</Link>
|
||||
{product.categoryId && product.categoryName ? (
|
||||
<>
|
||||
<span className="mx-2">‹</span>
|
||||
<Link href={`/products?categoryId=${product.categoryId}`} className="hover:text-primary">
|
||||
{product.categoryName}
|
||||
</Link>
|
||||
</>
|
||||
) : null}
|
||||
<span className="mx-2">‹</span>
|
||||
<span>{product.title}</span>
|
||||
</nav>
|
||||
|
||||
<div className="grid gap-10 lg:grid-cols-[1.1fr_1fr]">
|
||||
<div className="rounded-2xl bg-white p-6 ring-1 ring-line">
|
||||
<CatalogImage
|
||||
src={product.images[0] || product.image}
|
||||
alt={product.title}
|
||||
width={800}
|
||||
height={800}
|
||||
className="mx-auto max-h-[460px] w-full object-contain"
|
||||
/>
|
||||
{product.images.length > 1 ? (
|
||||
<div className="mt-4 flex flex-wrap gap-2">
|
||||
{product.images.slice(0, 6).map((image) => (
|
||||
<CatalogImage
|
||||
key={image}
|
||||
src={image}
|
||||
alt=""
|
||||
width={120}
|
||||
height={120}
|
||||
className="h-16 w-16 rounded-xl object-contain ring-1 ring-line"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold leading-10 text-primary">{product.title}</h1>
|
||||
{product.categoryName ? <p className="mt-2 text-sm text-muted">{product.categoryName}</p> : null}
|
||||
<p className="mt-6 text-xl font-semibold text-primary">{formatPrice(product.price)}</p>
|
||||
{product.inStock === false ? (
|
||||
<p className="mt-2 text-sm text-tertiary">در حال حاضر موجود نیست</p>
|
||||
) : null}
|
||||
<a
|
||||
href={CUSTOMER_URL}
|
||||
className="mt-6 inline-flex rounded-full bg-primary px-8 py-3 text-sm text-white hover:bg-primary-dark"
|
||||
>
|
||||
{product.price != null ? "افزودن به سبد خرید ‹" : "ورود به حساب کاربری ‹"}
|
||||
</a>
|
||||
{product.summary ? <p className="mt-6 text-sm leading-8 text-muted">{product.summary}</p> : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{product.descriptionHtml ? (
|
||||
<div className="mt-10 rounded-2xl bg-white p-6 ring-1 ring-line">
|
||||
<h2 className="mb-4 text-lg font-bold text-primary">توضیحات محصول</h2>
|
||||
<div className="rich-content" dangerouslySetInnerHTML={{ __html: product.descriptionHtml }} />
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<ProductCarousel
|
||||
title="محصولات مرتبط"
|
||||
subtitle="دیگر محصولات این دسته"
|
||||
icon="/images/icons/products.png"
|
||||
products={related}
|
||||
href={product.categoryId ? `/products?categoryId=${product.categoryId}` : "/products"}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import { Pagination } from "@/components/Pagination";
|
||||
import { ProductCard } from "@/components/ProductCard";
|
||||
import { categoryLabel, getCategoryById, getLevelTwoProductCategories, listCatalog } from "@/lib/api";
|
||||
import { site } from "@/data/site";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: `محصولات | ${site.name}`,
|
||||
description: "فروشگاه محصولات گروه صنعتی شیرزاد",
|
||||
};
|
||||
|
||||
type Search = {
|
||||
categoryId?: string;
|
||||
q?: string;
|
||||
page?: string;
|
||||
};
|
||||
|
||||
export default async function ProductsPage({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<Search>;
|
||||
}) {
|
||||
const params = await searchParams;
|
||||
const page = Math.max(1, Number(params.page) || 1);
|
||||
const categoryId = params.categoryId;
|
||||
const q = params.q?.trim();
|
||||
const [catalog, categories, activeCategory] = await Promise.all([
|
||||
listCatalog({ page, pageSize: 20, categoryId, name: q }),
|
||||
getLevelTwoProductCategories(),
|
||||
getCategoryById(categoryId),
|
||||
]);
|
||||
|
||||
const title = q
|
||||
? `نتایج جستجو برای «${q}»`
|
||||
: activeCategory
|
||||
? categoryLabel(activeCategory)
|
||||
: "محصولات ما";
|
||||
|
||||
function hrefFor(nextPage: number) {
|
||||
const search = new URLSearchParams();
|
||||
if (categoryId) search.set("categoryId", categoryId);
|
||||
if (q) search.set("q", q);
|
||||
if (nextPage > 1) search.set("page", String(nextPage));
|
||||
const value = search.toString();
|
||||
return value ? `/products?${value}` : "/products";
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="mx-auto max-w-7xl px-4 py-10">
|
||||
<h1 className="mb-6 text-2xl font-bold text-primary">{title}</h1>
|
||||
{categories.length > 0 ? (
|
||||
<div className="mb-8 flex flex-wrap gap-2">
|
||||
<Link
|
||||
href="/products"
|
||||
className={`rounded-full px-4 py-1.5 text-sm ${
|
||||
!categoryId ? "bg-primary text-white" : "bg-page text-muted hover:text-primary"
|
||||
}`}
|
||||
>
|
||||
همه
|
||||
</Link>
|
||||
{categories.map((category) => (
|
||||
<Link
|
||||
key={category.id}
|
||||
href={`/products?categoryId=${category.id}`}
|
||||
className={`rounded-full px-4 py-1.5 text-sm ${
|
||||
categoryId === category.id ? "bg-primary text-white" : "bg-page text-muted hover:text-primary"
|
||||
}`}
|
||||
>
|
||||
{categoryLabel(category)}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{catalog.items.length === 0 ? (
|
||||
<p className="rounded-2xl bg-page px-6 py-16 text-center text-muted">محصولی در این بخش یافت نشد.</p>
|
||||
) : (
|
||||
<div className="flex flex-wrap justify-center gap-4">
|
||||
{catalog.items.map((product) => (
|
||||
<ProductCard key={product.id} product={product} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Pagination
|
||||
page={catalog.page}
|
||||
pageSize={catalog.pageSize}
|
||||
total={catalog.total}
|
||||
hrefFor={hrefFor}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
}
|
||||