- {blogPosts.map((post) => (
-
-
-
+ {usingApi ? (
+ <>
+
+ {result.items.map((post) => (
+
+
+
+ {formatFaDate(post.publishedAt || post.createdAt)}
+
+
{post.title}
+ {post.abstract ? (
+
{post.abstract}
+ ) : null}
+
+ ))}
+
+ {pageCount > 1 ? (
+
+ {Array.from({ length: pageCount }, (_, index) => {
+ const number = index + 1;
+ return (
+
+ {faDigits(number)}
+
+ );
+ })}
-
{post.date}
-
{post.title}
-
{post.excerpt}
-
- ))}
-
+ ) : null}
+ >
+ ) : (
+
+ {blogPosts.map((post) => (
+
+
+
{post.date}
+
{post.title}
+
{post.excerpt}
+
+ ))}
+
+ )}
>
diff --git a/app/globals.css b/app/globals.css
index d30d217..eb8b8e4 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -77,3 +77,33 @@ textarea,
button {
font-family: inherit;
}
+
+.rich-text h1,
+.rich-text h2,
+.rich-text h3 {
+ margin-top: 1.25em;
+ margin-bottom: 0.5em;
+ font-weight: 700;
+ color: var(--ink);
+}
+
+.rich-text p {
+ margin-bottom: 0.85em;
+}
+
+.rich-text ul,
+.rich-text ol {
+ margin: 0.75em 0;
+ padding-inline-start: 1.25rem;
+}
+
+.rich-text a {
+ color: var(--brand);
+ text-decoration: underline;
+}
+
+.rich-text img {
+ max-width: 100%;
+ height: auto;
+ border-radius: 0.75rem;
+}
diff --git a/app/layout.tsx b/app/layout.tsx
index 95c7ad3..06ec00f 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -4,6 +4,7 @@ import "./globals.css";
import { Footer } from "@/components/layout/Footer";
import { Header } from "@/components/layout/Header";
import { TopBar } from "@/components/layout/TopBar";
+import { getNavCategories } from "@/lib/categories";
import { site } from "@/lib/data";
const vazir = Vazirmatn({
@@ -30,12 +31,14 @@ export const metadata: Metadata = {
icons: { icon: "/favicon.ico" },
};
-export default function RootLayout({ children }: { children: React.ReactNode }) {
+export default async function RootLayout({ children }: { children: React.ReactNode }) {
+ const categories = await getNavCategories();
+
return (
-
+
{children}
diff --git a/app/page.tsx b/app/page.tsx
index da2f163..8891dc4 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -4,25 +4,38 @@ import { BlogCarousel } from "@/components/home/BlogCarousel";
import { Branches } from "@/components/home/Branches";
import { CategoryShowcase } from "@/components/home/CategoryShowcase";
import { Hero } from "@/components/home/Hero";
+import { ProductCarousel } from "@/components/home/ProductCarousel";
import { Services } from "@/components/home/Services";
import { Stats } from "@/components/home/Stats";
import { WideBanner } from "@/components/home/WideBanner";
import {
about,
bannersBelowSlider,
+ blogPosts,
branches,
+ consumablesFallback,
+ refrigeratorPartsFallback,
services,
showcaseCategories,
slides,
threeBannerRow,
twoBannerRow,
+ washingMachinePartsFallback,
} from "@/lib/data";
+import { fetchBlogs } from "@/lib/blog";
+import { fetchSpecialProducts } from "@/lib/store";
import { fetchStaticImageSlots, overlaySlotImages, pickSlot } from "@/lib/static-images";
export const revalidate = 60;
export default async function HomePage() {
- const slots = await fetchStaticImageSlots("home");
+ const [slots, consumables, refrigerator, washing, blogs] = await Promise.all([
+ fetchStaticImageSlots("home"),
+ fetchSpecialProducts("consumeables"),
+ fetchSpecialProducts("refrigrator-parts"),
+ fetchSpecialProducts("washing-machine-parts"),
+ fetchBlogs({ pageSize: 8 }),
+ ]);
const slider = overlaySlotImages(
pickSlot(slots, "slider"),
@@ -122,11 +135,29 @@ export default async function HomePage() {
<>
+
+
+
-
+
diff --git a/app/products/[id]/page.tsx b/app/products/[id]/page.tsx
new file mode 100644
index 0000000..8caf13d
--- /dev/null
+++ b/app/products/[id]/page.tsx
@@ -0,0 +1,119 @@
+import type { Metadata } from "next";
+import Link from "next/link";
+import { notFound } from "next/navigation";
+import { Container } from "@/components/Container";
+import { ProductGallery } from "@/components/product/ProductGallery";
+import { ProductTabs } from "@/components/product/ProductTabs";
+import { fetchComments } from "@/lib/comments";
+import { formatPrice } from "@/lib/format";
+import {
+ brandName,
+ fetchCatalogProductByTitle,
+ fetchStoreItemByProduct,
+ fetchStoreVariant,
+ galleryUrls,
+ variantTitle,
+} from "@/lib/store";
+
+export const revalidate = 60;
+
+export async function generateMetadata({
+ params,
+}: {
+ params: Promise<{ id: string }>;
+}): Promise
{
+ const { id } = await params;
+ const variant = await fetchStoreVariant(id);
+ return {
+ title: variant ? variantTitle(variant) : "محصول",
+ };
+}
+
+export default async function ProductDetailsPage({ params }: { params: Promise<{ id: string }> }) {
+ const { id } = await params;
+ const variant = await fetchStoreVariant(id);
+ if (!variant) notFound();
+
+ const [catalog, storeItem, comments] = await Promise.all([
+ fetchCatalogProductByTitle(variant.productNameFa || variant.productTitle, variant.productId),
+ variant.productId ? fetchStoreItemByProduct(variant.productId) : Promise.resolve({ storeItem: undefined }),
+ variant.productId ? fetchComments("product", variant.productId) : Promise.resolve([]),
+ ]);
+
+ const images = galleryUrls(catalog, variant.productImage);
+ const price = variant.discountedPrice ?? variant.price;
+ const oldPrice = variant.discountedPrice != null ? variant.price : null;
+ const title = catalog?.nameFa || variantTitle(variant);
+ const brand = brandName(catalog);
+ const variants = storeItem.storeItem?.variants ?? [];
+ const inStock = variant.stockQuantity == null || variant.stockQuantity > 0;
+
+ return (
+
+
+
+
+ خانه
+
+ /
+
+ محصولات
+
+ /
+ {title}
+
+
+
+
+
+ {brand ?
{brand}
: null}
+
{title}
+ {variant.label && variant.label !== title ? (
+
{variant.label}
+ ) : null}
+
+
+ {oldPrice ?
{formatPrice(oldPrice)} تومان
: null}
+
+ {price != null ? (
+ <>
+ {formatPrice(price)}
+ تومان
+ >
+ ) : (
+ برای قیمت تماس بگیرید
+ )}
+
+
+
+ {inStock ? "موجود در فروشگاه" : "ناموجود"}
+
+
+ {variants.length > 1 ? (
+
+ {variants.map((item) => (
+
+ {item.label || variantTitle(item)}
+
+ ))}
+
+ ) : null}
+
+
+ استعلام و سفارش
+
+
+
+
+
+
+
+ );
+}
diff --git a/app/products/page.tsx b/app/products/page.tsx
new file mode 100644
index 0000000..95038fe
--- /dev/null
+++ b/app/products/page.tsx
@@ -0,0 +1,88 @@
+import type { Metadata } from "next";
+import Link from "next/link";
+import { Container } from "@/components/Container";
+import { ProductCard } from "@/components/ui/ProductCard";
+import { fetchStoreItems, variantToProduct } from "@/lib/store";
+import { getNavCategories } from "@/lib/categories";
+import { faDigits } from "@/lib/format";
+
+export const revalidate = 60;
+
+export const metadata: Metadata = {
+ title: "محصولات",
+ description: "قطعات و لوازم خانگی فروشگاه برادران برقعی",
+};
+
+export default async function ProductsPage({
+ searchParams,
+}: {
+ searchParams: Promise<{ page?: string; categoryId?: string; q?: string }>;
+}) {
+ const params = await searchParams;
+ const page = Math.max(1, Number(params.page) || 1);
+ const categoryId = params.categoryId;
+ const q = params.q?.trim();
+ const [result, nav] = await Promise.all([
+ fetchStoreItems({ page, pageSize: 20, categoryId, name: q && q.length > 2 ? q : undefined }),
+ getNavCategories(),
+ ]);
+ const products = result.items.map(variantToProduct);
+ const pageCount = Math.max(1, Math.ceil((result.total || products.length) / (result.pageSize || 20)));
+ const categoryTitle = nav
+ .flatMap((item) => [item, ...item.children.map((child) => ({ ...child, children: [] as never[] }))])
+ .find((item) => item.id === categoryId)?.title;
+
+ return (
+
+
+
+
+
+ خانه
+
+ /
+ محصولات
+
+
+ {categoryTitle || (q ? `جستجو: ${q}` : "همه محصولات")}
+
+ {result.total ?
{faDigits(result.total)} کالا
: null}
+
+
+ {products.length === 0 ? (
+
+
محصولی برای نمایش وجود ندارد
+
بهزودی موجودی فروشگاه از پنل Meshkee اینجا دیده میشود.
+
+ ) : (
+
+ {products.map((product) => (
+
+ ))}
+
+ )}
+
+ {pageCount > 1 ? (
+
+ {Array.from({ length: pageCount }, (_, index) => {
+ const number = index + 1;
+ const query = new URLSearchParams();
+ if (categoryId) query.set("categoryId", categoryId);
+ if (q) query.set("q", q);
+ query.set("page", String(number));
+ return (
+
+ {faDigits(number)}
+
+ );
+ })}
+
+ ) : null}
+
+
+ );
+}
diff --git a/components/home/BlogCarousel.tsx b/components/home/BlogCarousel.tsx
index b8d69c1..987d247 100644
--- a/components/home/BlogCarousel.tsx
+++ b/components/home/BlogCarousel.tsx
@@ -5,10 +5,26 @@ import Link from "next/link";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { Container } from "@/components/Container";
import { Placeholder } from "@/components/Placeholder";
-import { blogPosts } from "@/lib/data";
+import { blogHref, blogImage, type BlogPost } from "@/lib/blog";
+import { formatFaDate } from "@/lib/format";
-export function BlogCarousel() {
+type LocalPost = {
+ slug: string;
+ title: string;
+ excerpt: string;
+ date: string;
+ image: string;
+};
+
+export function BlogCarousel({
+ posts = [],
+ fallback = [],
+}: {
+ posts?: BlogPost[];
+ fallback?: LocalPost[];
+}) {
const scroller = useRef(null);
+ const usingApi = posts.length > 0;
const scrollByCard = (direction: number) => {
const node = scroller.current;
@@ -56,22 +72,43 @@ export function BlogCarousel() {