diff --git a/package-lock.json b/package-lock.json
index e0fbce9..a4cba35 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -15,6 +15,7 @@
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
+ "@tailwindcss/typography": "^0.5.20",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
@@ -1605,6 +1606,19 @@
"tailwindcss": "4.3.3"
}
},
+ "node_modules/@tailwindcss/typography": {
+ "version": "0.5.20",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.20.tgz",
+ "integrity": "sha512-hwbzQuNUfcPvbegQFatVPl/MY/tcM9KLl963hQ5laJKPh81TEZ1+dNG9PirGvcaDBkp+BCshExAyKVPW91dozw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "postcss-selector-parser": "6.0.10"
+ },
+ "peerDependencies": {
+ "tailwindcss": ">=3.0.0 || >=4.0.0 || insiders"
+ }
+ },
"node_modules/@tybys/wasm-util": {
"version": "0.10.3",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz",
@@ -2801,6 +2815,19 @@
"node": ">= 8"
}
},
+ "node_modules/cssesc": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
+ "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "cssesc": "bin/cssesc"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/csstype": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
@@ -5557,6 +5584,20 @@
"node": "^10 || ^12 || >=14"
}
},
+ "node_modules/postcss-selector-parser": {
+ "version": "6.0.10",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz",
+ "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cssesc": "^3.0.0",
+ "util-deprecate": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@@ -6639,6 +6680,13 @@
"punycode": "^2.1.0"
}
},
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
diff --git a/package.json b/package.json
index 9dbf527..7215258 100644
--- a/package.json
+++ b/package.json
@@ -16,6 +16,7 @@
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
+ "@tailwindcss/typography": "^0.5.20",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
diff --git a/prompt/connect-backend-prompt.txt b/prompt/connect-backend-prompt.txt
new file mode 100644
index 0000000..91f58e2
--- /dev/null
+++ b/prompt/connect-backend-prompt.txt
@@ -0,0 +1,15 @@
+Domain: sanihome.ir
+Documentation for backend is:https://api.meshkee.com/docs/website
+
+
+Login or user icon should go to customer.sanihome.ir.com
+
+Create product page and details >> connect to store items
+Create blog page and details >> connect to blog
+
+Product Menu should be a mega menu with level 2 and 3 of categories - skip level 1
+
+Search input should be instant search - activate when type more than 2 char
+
+First carousel >> special-sale
+Second carousel >> top-selling
diff --git a/src/app/api/search/route.ts b/src/app/api/search/route.ts
new file mode 100644
index 0000000..ce17375
--- /dev/null
+++ b/src/app/api/search/route.ts
@@ -0,0 +1,12 @@
+import { NextRequest, NextResponse } from "next/server";
+import { getProducts } from "@/lib/meshkee";
+
+export async function GET(request: NextRequest) {
+ const query = request.nextUrl.searchParams.get("q")?.trim() ?? "";
+ if (query.length <= 2) {
+ return NextResponse.json({ items: [] });
+ }
+
+ const { items } = await getProducts({ name: query, pageSize: 6 });
+ return NextResponse.json({ items });
+}
diff --git a/src/app/blog/[slug]/page.tsx b/src/app/blog/[slug]/page.tsx
new file mode 100644
index 0000000..3b41633
--- /dev/null
+++ b/src/app/blog/[slug]/page.tsx
@@ -0,0 +1,64 @@
+import { notFound } from "next/navigation";
+import { Placeholder } from "@/components/ui/Placeholder";
+import { getBlog } from "@/lib/meshkee";
+
+function formatDate(iso?: string) {
+ if (!iso) return "";
+ return new Intl.DateTimeFormat("fa-IR", { dateStyle: "long" }).format(new Date(iso));
+}
+
+export default async function BlogDetailPage({
+ params,
+}: {
+ params: Promise<{ slug: string }>;
+}) {
+ const { slug } = await params;
+ const post = await getBlog(slug);
+
+ if (!post) notFound();
+
+ return (
+
+
+ {post.categoryName && {post.categoryName}}
+ {post.publishedAt && (
+ <>
+ •
+ {formatDate(post.publishedAt)}
+ >
+ )}
+ {post.author && (
+ <>
+ •
+ {post.author}
+ >
+ )}
+
+
+
+ {post.title}
+
+
+ {post.image && (
+
+ )}
+
+
+
+
+
+ );
+}
diff --git a/src/app/blog/page.tsx b/src/app/blog/page.tsx
new file mode 100644
index 0000000..c6d2dff
--- /dev/null
+++ b/src/app/blog/page.tsx
@@ -0,0 +1,97 @@
+import { Placeholder } from "@/components/ui/Placeholder";
+import { EmptyState } from "@/components/ui/EmptyState";
+import { getBlogs } from "@/lib/meshkee";
+
+const PAGE_SIZE = 12;
+
+function formatDate(iso?: string) {
+ if (!iso) return "";
+ return new Intl.DateTimeFormat("fa-IR", { dateStyle: "long" }).format(new Date(iso));
+}
+
+export default async function BlogListPage({
+ searchParams,
+}: {
+ searchParams: Promise<{ page?: string }>;
+}) {
+ const params = await searchParams;
+ const page = Math.max(1, Number(params.page ?? 1) || 1);
+ const { items, total } = await getBlogs({ page, pageSize: PAGE_SIZE });
+ const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE));
+
+ return (
+
+
وبلاگ
+
+ {items.length === 0 ? (
+
+ ) : (
+ <>
+
+
+ {totalPages > 1 && (
+
+ {page > 1 ? (
+
+ قبلی
+
+ ) : (
+
+ قبلی
+
+ )}
+
+ صفحه {page} از {totalPages}
+
+ {page < totalPages ? (
+
+ بعدی
+
+ ) : (
+
+ بعدی
+
+ )}
+
+ )}
+ >
+ )}
+
+ );
+}
diff --git a/src/app/globals.css b/src/app/globals.css
index 481eab9..323febe 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -1,4 +1,5 @@
@import "tailwindcss";
+@plugin "@tailwindcss/typography";
:root {
--background: #fafafa;
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index f9056df..1e94339 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -1,6 +1,10 @@
import type { Metadata } from "next";
import { Vazirmatn, Syne } from "next/font/google";
import "./globals.css";
+import { TopBar } from "@/components/home/TopBar";
+import { Header } from "@/components/home/Header";
+import { Footer } from "@/components/home/Footer";
+import { getBusinessInfo, getCategories, getCategoryTree } from "@/lib/meshkee";
const vazirmatn = Vazirmatn({
variable: "--font-vazirmatn",
@@ -19,14 +23,25 @@ export const metadata: Metadata = {
description: "فروشگاه آنلاین لوازم خانگی — یخچال، لباسشویی، تلویزیون و بیشتر",
};
-export default function RootLayout({ children }: LayoutProps<"/">) {
+export default async function RootLayout({ children }: LayoutProps<"/">) {
+ const [categoryTree, categories, businessInfo] = await Promise.all([
+ getCategoryTree(),
+ getCategories(),
+ getBusinessInfo(),
+ ]);
+
return (
- {children}
+
+
+
+ {children}
+
+
);
}
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 0fdf1bc..9c4ac2a 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -1,5 +1,3 @@
-import { TopBar } from "@/components/home/TopBar";
-import { Header } from "@/components/home/Header";
import { FeatureBar } from "@/components/home/FeatureBar";
import { HeroSlider } from "@/components/home/HeroSlider";
import { FeaturedCategories } from "@/components/home/FeaturedCategories";
@@ -10,11 +8,9 @@ import { ThreeBanners } from "@/components/home/ThreeBanners";
import { BrandGroupCarousels } from "@/components/home/BrandGroupCarousels";
import { AboutSanihome } from "@/components/home/AboutSanihome";
import { BlogCarousel } from "@/components/home/BlogCarousel";
-import { Footer } from "@/components/home/Footer";
import {
getBusinessInfo,
getCategories,
- getCategoryGroups,
getProducts,
getBrandGroups,
getStoreSpecials,
@@ -24,24 +20,17 @@ import {
} from "@/lib/meshkee";
export default async function Home() {
- const [
- categoryGroups,
- categories,
- storeSpecials,
- productsResult,
- brandGroups,
- businessInfo,
- staticImageSlots,
- ] = await Promise.all([
- getCategoryGroups(),
- getCategories(),
- getStoreSpecials(),
- getProducts({ pageSize: 12 }),
- getBrandGroups(),
- getBusinessInfo(),
- getStaticImageSlots(),
- ]);
+ const [categories, storeSpecials, productsResult, brandGroups, staticImageSlots, businessInfo] =
+ await Promise.all([
+ getCategories(),
+ getStoreSpecials(),
+ getProducts({ pageSize: 12 }),
+ getBrandGroups(),
+ getStaticImageSlots(),
+ getBusinessInfo(),
+ ]);
+ const contactPhone = businessInfo?.phoneNumbers[0];
const specialSaleProducts = storeSpecials[0]?.items ?? [];
const twoBannerSlot = staticImageSlots.find((slot) => slot.key === STATIC_IMAGE_KEYS.twoBanner);
const threeBannerSlot = staticImageSlots.find(
@@ -51,27 +40,22 @@ export default async function Home() {
return (
<>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
>
);
}
diff --git a/src/app/products/[slug]/page.tsx b/src/app/products/[slug]/page.tsx
new file mode 100644
index 0000000..40cfb7f
--- /dev/null
+++ b/src/app/products/[slug]/page.tsx
@@ -0,0 +1,135 @@
+import { notFound } from "next/navigation";
+import { Placeholder } from "@/components/ui/Placeholder";
+import { formatPrice } from "@/data/sample";
+import { getBusinessInfo, getProduct, getProductTechnicalInfo } from "@/lib/meshkee";
+
+export default async function ProductDetailPage({
+ params,
+}: {
+ params: Promise<{ slug: string }>;
+}) {
+ const { slug } = await params;
+ const [product, technicalInfo, businessInfo] = await Promise.all([
+ getProduct(slug),
+ getProductTechnicalInfo(slug),
+ getBusinessInfo(),
+ ]);
+ const contactPhone = businessInfo?.phoneNumbers[0];
+
+ if (!product) notFound();
+
+ const images = product.images.length > 0 ? product.images : product.image ? [product.image] : [];
+
+ return (
+
+
+
+
+ {images.length > 1 && (
+
+ {images.slice(1, 5).map((img) => (
+
+ ))}
+
+ )}
+
+
+
+ {product.brand && (
+
+ {product.brand}
+
+ )}
+
{product.name}
+ {product.categoryName && (
+
+ {product.categoryName}
+
+ )}
+
+
+ {product.minPrice != null ? (
+
+ {product.minPrice === product.maxPrice
+ ? formatPrice(product.minPrice)
+ : `${formatPrice(product.minPrice)} - ${formatPrice(product.maxPrice ?? product.minPrice)}`}
+ تومان
+
+ ) : (
+ تماس با فروشگاه
+ )}
+
+ {product.inStock ? "موجود" : "ناموجود"}
+
+
+
+ {product.minPrice != null ? (
+
+ ) : contactPhone ? (
+
+ 📞
+ تماس با فروشگاه
+
+ ) : (
+
+ )}
+
+ {product.descriptionHtml && (
+
+ )}
+
+
+
+ {technicalInfo.length > 0 && (
+
+
مشخصات فنی
+
+
+ {technicalInfo.map((field) => (
+
+ | {field.label} |
+ {field.value} |
+
+ ))}
+
+
+
+ )}
+
+ );
+}
diff --git a/src/app/products/page.tsx b/src/app/products/page.tsx
new file mode 100644
index 0000000..6a7edca
--- /dev/null
+++ b/src/app/products/page.tsx
@@ -0,0 +1,94 @@
+import { ProductCard } from "@/components/ui/ProductCard";
+import { EmptyState } from "@/components/ui/EmptyState";
+import { getBusinessInfo, getProducts, productHref } from "@/lib/meshkee";
+
+const PAGE_SIZE = 24;
+
+export default async function ProductsPage({
+ searchParams,
+}: {
+ searchParams: Promise<{ categoryId?: string; q?: string; page?: string }>;
+}) {
+ const params = await searchParams;
+ const page = Math.max(1, Number(params.page ?? 1) || 1);
+ const [{ items, total }, businessInfo] = await Promise.all([
+ getProducts({
+ categoryId: params.categoryId,
+ name: params.q,
+ page,
+ pageSize: PAGE_SIZE,
+ }),
+ getBusinessInfo(),
+ ]);
+ const contactPhone = businessInfo?.phoneNumbers[0];
+ const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE));
+
+ const pageHref = (p: number) => {
+ const sp = new URLSearchParams();
+ if (params.categoryId) sp.set("categoryId", params.categoryId);
+ if (params.q) sp.set("q", params.q);
+ sp.set("page", String(p));
+ return `/products?${sp.toString()}`;
+ };
+
+ return (
+
+
+ {params.q ? `نتایج جستجو برای «${params.q}»` : "محصولات"}
+
+
+ {items.length === 0 ? (
+
+ ) : (
+ <>
+
+ {items.map((product) => (
+
+ ))}
+
+
+ {totalPages > 1 && (
+
+ {page > 1 ? (
+
+ قبلی
+
+ ) : (
+
+ قبلی
+
+ )}
+
+ صفحه {page} از {totalPages}
+
+ {page < totalPages ? (
+
+ بعدی
+
+ ) : (
+
+ بعدی
+
+ )}
+
+ )}
+ >
+ )}
+
+ );
+}
diff --git a/src/components/home/BrandGroupCarousels.tsx b/src/components/home/BrandGroupCarousels.tsx
index 83c7797..e416664 100644
--- a/src/components/home/BrandGroupCarousels.tsx
+++ b/src/components/home/BrandGroupCarousels.tsx
@@ -2,9 +2,15 @@ import { Carousel } from "@/components/ui/Carousel";
import { ProductCard } from "@/components/ui/ProductCard";
import { SectionTitle } from "@/components/ui/SectionTitle";
import { EmptyState } from "@/components/ui/EmptyState";
-import type { ProductGroup } from "@/lib/meshkee";
+import { productHref, type ProductGroup } from "@/lib/meshkee";
-export function BrandGroupCarousels({ brandGroups }: { brandGroups: ProductGroup[] }) {
+export function BrandGroupCarousels({
+ brandGroups,
+ contactPhone,
+}: {
+ brandGroups: ProductGroup[];
+ contactPhone?: string;
+}) {
if (brandGroups.length === 0) {
return (
@@ -31,6 +37,8 @@ export function BrandGroupCarousels({ brandGroups }: { brandGroups: ProductGroup
price={product.price}
image={product.image ?? "/images/placeholder.svg"}
variant="brand"
+ href={productHref(product)}
+ contactPhone={contactPhone}
/>
))}
diff --git a/src/components/home/Header.tsx b/src/components/home/Header.tsx
index a3eaf1b..5e61cb4 100644
--- a/src/components/home/Header.tsx
+++ b/src/components/home/Header.tsx
@@ -2,8 +2,9 @@
import { useEffect, useRef, useState } from "react";
import { createPortal } from "react-dom";
-import type { Category, CategoryGroup } from "@/lib/meshkee";
+import type { Category, CategoryNode, CategoryTreeGroup } from "@/lib/meshkee";
import { EmptyState } from "@/components/ui/EmptyState";
+import { SearchBar } from "./SearchBar";
const navLinks = [
{ label: "فروش ویژه", href: "#" },
@@ -12,6 +13,8 @@ const navLinks = [
{ label: "تماس با ما", href: "#" },
];
+const LOGIN_URL = "https://customer.sanihome.ir";
+
function Logo({ large = false }: { large?: boolean }) {
return (
@@ -43,35 +46,22 @@ function ChevronDownIcon({ className = "" }: { className?: string }) {
);
}
-function SearchBar() {
- return (
-
-
-
-
- );
+function flattenLevel2(categoryTree: CategoryTreeGroup[]): CategoryNode[] {
+ return categoryTree.flatMap((group) => group.items);
}
function MobileDrawer({
open,
onClose,
- categoryGroups,
+ categoryTree,
}: {
open: boolean;
onClose: () => void;
- categoryGroups: CategoryGroup[];
+ categoryTree: CategoryTreeGroup[];
}) {
const [categoriesOpen, setCategoriesOpen] = useState(false);
const [mounted, setMounted] = useState(false);
+ const level2 = flattenLevel2(categoryTree);
useEffect(() => {
setMounted(true);
@@ -144,32 +134,37 @@ function MobileDrawer({
}`}
>
- {categoryGroups.length === 0 ? (
+ {level2.length === 0 ? (
) : (
-
- {categoryGroups.map((group) => (
-
+
+ {level2.map((node) => (
+ -
+
+ {node.nameFa}
+
+ {node.children.length > 0 && (
+
+ )}
+
))}
-
+
)}
@@ -185,13 +180,13 @@ function MobileDrawer({
))}
-
+
,
@@ -200,15 +195,16 @@ function MobileDrawer({
}
export function Header({
- categoryGroups,
+ categoryTree,
categories,
}: {
- categoryGroups: CategoryGroup[];
+ categoryTree: CategoryTreeGroup[];
categories: Category[];
}) {
const [menuOpen, setMenuOpen] = useState(false);
const [drawerOpen, setDrawerOpen] = useState(false);
const menuRef = useRef(null);
+ const level2 = flattenLevel2(categoryTree);
useEffect(() => {
const onClickOutside = (event: MouseEvent) => {
@@ -248,13 +244,13 @@ export function Header({
-
+