From 8e14748b48b0d827577d2329d574bc4fa545a04e Mon Sep 17 00:00:00 2001 From: Alireza Hassani Date: Fri, 21 Aug 2026 16:04:31 +0330 Subject: [PATCH] Wire Meshkee catalog: products, blog, mega menu, and product search. Catalog-only (no cart); search targets store items and products pages. Co-authored-by: Cursor --- Ai-data/prompt-connect-backend.txt | 6 +- CONTEXT.md | 150 ++++++++++---------- app/api/search/route.ts | 19 +++ app/blog/[slug]/page.tsx | 52 ++++++- app/blog/page.tsx | 89 +++++++++--- app/globals.css | 30 ++++ app/layout.tsx | 7 +- app/page.tsx | 35 ++++- app/products/[id]/page.tsx | 119 ++++++++++++++++ app/products/page.tsx | 88 ++++++++++++ components/home/BlogCarousel.tsx | 73 +++++++--- components/home/ProductCarousel.tsx | 71 ++++++++++ components/layout/Header.tsx | 122 +++++----------- components/layout/HeaderSearch.tsx | 25 ---- components/layout/InstantSearch.tsx | 125 +++++++++++++++++ components/layout/MegaMenu.tsx | 44 ++++++ components/product/ProductGallery.tsx | 86 ++++++++++++ components/product/ProductTabs.tsx | 89 ++++++++++++ components/ui/ProductCard.tsx | 60 ++++++++ lib/blog.ts | 46 ++++++ lib/categories.ts | 65 +++++++++ lib/comments.ts | 16 +++ lib/data.ts | 36 +++-- lib/format.ts | 14 ++ lib/meshkee.ts | 28 ++++ lib/store.ts | 193 ++++++++++++++++++++++++++ public/placeholders/blog.svg | 6 + public/placeholders/category.svg | 5 + public/placeholders/product.svg | 5 + 29 files changed, 1460 insertions(+), 244 deletions(-) create mode 100644 app/api/search/route.ts create mode 100644 app/products/[id]/page.tsx create mode 100644 app/products/page.tsx create mode 100644 components/home/ProductCarousel.tsx delete mode 100644 components/layout/HeaderSearch.tsx create mode 100644 components/layout/InstantSearch.tsx create mode 100644 components/layout/MegaMenu.tsx create mode 100644 components/product/ProductGallery.tsx create mode 100644 components/product/ProductTabs.tsx create mode 100644 components/ui/ProductCard.tsx create mode 100644 lib/blog.ts create mode 100644 lib/categories.ts create mode 100644 lib/comments.ts create mode 100644 lib/format.ts create mode 100644 lib/meshkee.ts create mode 100644 lib/store.ts create mode 100644 public/placeholders/blog.svg create mode 100644 public/placeholders/category.svg create mode 100644 public/placeholders/product.svg diff --git a/Ai-data/prompt-connect-backend.txt b/Ai-data/prompt-connect-backend.txt index 6722842..df5464e 100644 --- a/Ai-data/prompt-connect-backend.txt +++ b/Ai-data/prompt-connect-backend.txt @@ -16,8 +16,8 @@ Create blog page and details >> connect to blog Product Menu should be a mega menu with level 1 and 2 of categories Search input should be instant search - activate when type more than 2 char -First carousel >> special-sale -Second carousel >> multi-vitamin -Third carousel >> body-building-supplement +First carousel >> consumeables +Second carousel >> refrigrator-parts +Third carousel >> washing-machine-parts update context md for better use in other Ai and systems - update it on every prompt (If required) \ No newline at end of file diff --git a/CONTEXT.md b/CONTEXT.md index fe09974..db60b62 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -3,84 +3,95 @@ Persian RTL storefront for **brb-store.com** (فروشگاه و تعمیرگاه لوازم خانگی برادران برقعی، قم). Next.js App Router rebuild of the original WillaMall/BeTheme site. Keep this file current — other AI tools read it first. -Live DNS for `www.brb-store.com` currently serves a different tenant; source of truth for copy, images, and palette is the archived original at `https://www.brb-store.ir/` (Wayback 2022) plus `Ai-data/prompt.txt`. - ## Stack - Next.js 16 (App Router, Turbopack) + React 19 + TypeScript + Tailwind v4 - Locale: `fa`, direction: `rtl` -- Fonts: Vazirmatn (Persian) + Montserrat (English kickers). Original site used **BYekan-Edited** + **IRANSans** + **Montserrat**; those files were not recoverable from Wayback (`WillaMall_SP/assets/Fonts/...` 404). -- Icons: Lucide, tinted with brand red (original used WillaMall `WMi-*` icon font, not archived). +- Fonts: Vazirmatn (Persian) + Montserrat (English kickers) +- Icons: Lucide - Dev: `npm run dev` ## Brand -| Token | Value | Source | -| --- | --- | --- | -| Brand red | `#e7432b` | BeTheme `color-red` | -| Brand dark | `#c93621` | hover | -| Ink | `#1a1a1a` | | -| Muted | `#6c6363` | original about text | -| Paper | `#f6f3f1` | | -| Footer copy bar | `#efefef` | original `.footer_copy` | - -Logo: oval **BRB** (black top / red bottom, “Borgheei Brothers”) at `public/images/brand/logo.png` and `src/assets/images/logo/logo.png`. Favicon from original `.ico`. - -Design language comes from the original BeTheme Carpenter homepage, not a generic banner stack: - -- Full-bleed kitchen photography with a **white 92% caption card** (the original slider’s 430px white panel), red 3px bar, Montserrat kicker. -- «قطعات ما» header: red bar + English kicker, title, short catalog note. -- Parts grid: **4 columns**, image cards with a **frosted glass** caption box on the photo (`backdrop-filter`), hover zoom + search icon. -- Red (`#e7432b`) as accent — numbers, icons, underlines — not a wash over every photo. -- Story: فروش / قطعات / تعمیرات, ۳۰+ years, two Qom branches, wholesale. -- Homepage **blog carousel** after the three-banner row: snap-scroll cards, arrows, link to `/blog`. Copy lives in `lib/data.ts` (`blogPosts`) until Meshkee. -- Original **category hover carousel** (`CategoryShowcase`): centered «دسته بندی ها» header, tall photo cards, dark bottom bar that expands on hover with «تماس با ما». Data: `showcaseCategories`. - -Product tiles stay on **white backgrounds**. Lifestyle extras: `public/images/lifestyle/`. - -## Routes (this pass) - -Clone **only** these pages: - -| Path | Content | +| Token | Value | | --- | --- | -| `/` | Homepage + static image slots | -| `/about` | About us (original `/AboutUs`) | -| `/contact` | Contact us (original `/ContactUs`) | -| `/blog` | Article list (local placeholders until Meshkee blog is wired) | -| `/blog/[slug]` | Article detail | -| `/meshkee/static-image-slots` | Slot catalog for the dashboard Refresh button | +| Brand red | `#e7432b` | +| Brand dark | `#c93621` | +| Ink | `#161616` | +| Muted | `#6c6363` | +| Paper | `#f4f0ec` | -Account / user icon → `https://customer.brb-store.com` (`NEXT_PUBLIC_CUSTOMER_PORTAL_URL`). Do not build a local login page. +Logo: `public/images/brand/logo.png`. Dark glossy caption bars: `lib/glossy.ts`. + +## Meshkee API + +- Docs: https://api.meshkee.com/docs/website +- OpenAPI: https://api.meshkee.com/docs/website/openapi.json +- Base: `https://api.meshkee.com/api/v1` +- Tenant domain: `brb-store.com` (`NEXT_PUBLIC_WEBSITE_DOMAIN` / `WEBSITE_DOMAIN`) +- Customer portal: `https://customer.brb-store.com` (`NEXT_PUBLIC_CUSTOMER_PORTAL_URL`) +- Public reads: `/tenants/{domain}/...` (no auth). +- Do not invent CMS, upload, or dashboard APIs. + +This site is **catalog-only** (no shopping cart). Login / user icon goes to the customer portal. Do not build a local login page. + +## Routes + +| Path | Source | +| --- | --- | +| `/` | Homepage + static image slots + store specials + latest blogs | +| `/products` | Store items (`GET /tenants/{domain}/store-items`) | +| `/products?categoryId=` | Same, filtered | +| `/products/[variantId]` | Store item variant + product description/gallery + comments | +| `/blog` | `GET /tenants/{domain}/blogs` (falls back to `lib/data.ts` `blogPosts`) | +| `/blog/[slug]` | Blog details + comments | +| `/about` | About us | +| `/contact` | Contact us | +| `/meshkee/static-image-slots` | Slot catalog for the dashboard Refresh button | +| `/api/search?q=` | Instant product search (`store-items?name=`, min 3 chars) | ## Homepage order -`Hero` → `Services` (3 icons) → `CategoryShowcase` → `AboutPreview` → five-banner (2-col + 3-col) → `BlogCarousel` → `two-banner-row` → `Stats` → `WideBanner` → `Branches`. +`Hero` → `Services` → three product carousels → `CategoryShowcase` → `AboutPreview` → five-banner (2+3) → `BlogCarousel` → `two-banner-row` → `Stats` → `WideBanner` → `Branches`. -Caption bars on photos use dark glossy glass (`lib/glossy.ts` `glossyBar`). Hover the card to expand the bar and show `subtext`. +## Homepage carousels (store specials) -## Static image slots (wired) +`GET /tenants/{domain}/store-specials` matched by `key`: -`GET /tenants/brb-store.com/website/static-images?pageKey=home`. Catalog: `GET /meshkee/static-image-slots` from `STATIC_IMAGE_SLOTS` in `lib/static-images.ts`. Client: `overlaySlotImages` / `pickSlot`. Empty/fail → local placeholder. `url` → ``, `linkUrl` → wrap in a link, `titleFa` / `titleEn` / `subtext` → caption. Use `slot.aspectRatio`. +1. `consumeables` — اقلام مصرفی +2. `refrigrator-parts` — قطعات یخچال +3. `washing-machine-parts` — قطعات ماشین لباسشویی -If a slot’s CSS size or ratio changes, update `aspectRatio` and `recommendedWidth` in `STATIC_IMAGE_SLOTS` in the same change. +If a special has no `items`, keep sample products from `lib/data.ts`. Use `variants[0].id` as the product URL. -| key | kind | itemCount | ratio | Local fallback | -| --- | --- | --- | --- | --- | -| `slider` | list | null | 2:1 | `slides` peek carousel, center `1280px` | -| `three-icon-image` | list | 3 | 1:1 | Lucide icons if slot empty | -| `categories` | list | null | 14:23 | `showcaseCategories` tall cards | -| `about-us-image` | single | 1 | 4:3 | `public/images/about/store.jpg` | -| `five-banner-section` | list | 5 | 2:1 | `twoBannerRow` + `threeBannerRow` | -| `two-banner-row` | list | 2 | 2:1 | `bannersBelowSlider` (after blog) | -| `full-row-image` | single | 1 | 32:9 | `public/images/banners/wide.jpg` | -| `two-branch-image` | list | 2 | 2:1 | `branches` photos | +## Catalog -Do not invent upload APIs. +- Mega menu: product categories **level 1 and 2** (`GET /tenants/{domain}/categories?entityType=product`). Links: `/products?categoryId={id}`. +- Instant search: header input searches **products** when query length **> 2** (`/api/search` → `store-items?name=`). Enter or “مشاهده همه” → `/products?q=`. +- Product pages are display-only (price, gallery, description, comments). No cart / add-to-cart. +- Details gallery: product `images[].url` + thumbnail. Click = lightbox, hover = zoom. +- Details tabs: description (`descriptionHtml`) and comments (`GET /comments?entityType=product&entityId={productId}`). Unauthenticated users see a login box to the customer portal. -## Original business data (valid — do not invent replacements) +## Static image slots -**About:** 30+ years. Sells home appliances, spare parts, and repairs small appliances (vacuum, meat grinder, fan, juicer, mill, steam iron, kettle, mixer, blender, microwave, …). Wholesale and retail, Iran and abroad. Open to manufacturers/traders. +`GET /tenants/{domain}/website/static-images?pageKey=home`. Catalog: `GET /meshkee/static-image-slots` from `STATIC_IMAGE_SLOTS` in `lib/static-images.ts`. + +| key | kind | itemCount | +| --- | --- | --- | +| `slider` | list | null | +| `three-icon-image` | list | 3 | +| `categories` | list | null | +| `about-us-image` | single | 1 | +| `five-banner-section` | list | 5 | +| `two-banner-row` | list | 2 | +| `full-row-image` | single | 1 | +| `two-branch-image` | list | 2 | + +`url` → img, `linkUrl` → wrap in a link, empty/fail → placeholder. Use `slot.aspectRatio`. If a slot’s size/ratio changes in CSS, update `STATIC_IMAGE_SLOTS` in the same change. + +## Original business data (do not invent replacements) + +**About:** 30+ years. Sells home appliances, spare parts, and repairs small appliances. Wholesale and retail. **Branches** @@ -89,26 +100,11 @@ Do not invent upload APIs. **Phones:** 0912-309-3407، 0912-651-7619، 025-36604454، 025-32404070، تلفکس 0253-6638866 **Telegram:** https://t.me/borgheeibrothers -**Email display:** Alireza borghei20@gmail.com (`alirezaborghei20@gmail.com`) +**Email:** `alirezaborghei20@gmail.com` **Map:** 34.6564, 50.8696 -**Part categories (homepage):** vacuum, juicer/blender/mill, steam cleaner, meat grinder, iron, microwave/toaster, kettle, fan/evaporative cooler, food processor/chopper/hand blender/mixer, battery, bearing, collector. +## Local fallbacks -Appliance mega-menu (household + industrial) is copied from the original header; links currently go to `/#parts` until the catalog is connected. - -## Meshkee - -Docs: https://api.meshkee.com/docs/website -OpenAPI: https://api.meshkee.com/docs/website/openapi.json -Base: `https://api.meshkee.com/api/v1` -Tenant domain: `brb-store.com` (`NEXT_PUBLIC_WEBSITE_DOMAIN` / `WEBSITE_DOMAIN`). - -Wired: homepage static images (`Ai-data/prompt-images-slot.txt`). - -Later: `Ai-data/prompt-connect-backend.txt` — products, blog, portfolio, customer portal. - -Public reads only. Do not invent CMS/upload/dashboard APIs. Product images are white-bg and will be read from the backend. - -## Local content - -All copy and nav live in `lib/data.ts`. Downloaded originals are under `public/images/`. Sample lifestyle photos: `assets/sample images/` (copied to `public/images/lifestyle/`). +- Copy / nav / sample products / local blog: `lib/data.ts` +- Empty catalog art: `/public/placeholders/*.svg` +- Downloaded originals: `public/images/` diff --git a/app/api/search/route.ts b/app/api/search/route.ts new file mode 100644 index 0000000..a6c2a60 --- /dev/null +++ b/app/api/search/route.ts @@ -0,0 +1,19 @@ +import { meshkeeApiBase, websiteDomain } from "@/lib/config"; + +export async function GET(request: Request) { + const url = new URL(request.url); + const query = url.searchParams.get("q")?.trim() ?? ""; + if (query.length <= 2) { + return Response.json({ items: [] }); + } + + try { + const response = await fetch( + `${meshkeeApiBase}/tenants/${encodeURIComponent(websiteDomain)}/store-items?name=${encodeURIComponent(query)}&pageSize=8`, + { cache: "no-store" }, + ); + return Response.json(await response.json()); + } catch { + return Response.json({ items: [] }); + } +} diff --git a/app/blog/[slug]/page.tsx b/app/blog/[slug]/page.tsx index 58c2945..9fc6d1d 100644 --- a/app/blog/[slug]/page.tsx +++ b/app/blog/[slug]/page.tsx @@ -3,11 +3,14 @@ import Link from "next/link"; import { notFound } from "next/navigation"; import { Container } from "@/components/Container"; import { PageHero } from "@/components/layout/PageHero"; -import { blogPosts, getBlogPost } from "@/lib/data"; +import { ProductTabs } from "@/components/product/ProductTabs"; +import { Placeholder } from "@/components/Placeholder"; +import { blogImage, fetchBlog } from "@/lib/blog"; +import { fetchComments } from "@/lib/comments"; +import { getBlogPost } from "@/lib/data"; +import { formatFaDate } from "@/lib/format"; -export function generateStaticParams() { - return blogPosts.map((post) => ({ slug: post.slug })); -} +export const revalidate = 60; export async function generateMetadata({ params, @@ -15,9 +18,11 @@ export async function generateMetadata({ params: Promise<{ slug: string }>; }): Promise { const { slug } = await params; - const post = getBlogPost(slug); - if (!post) return { title: "مقاله" }; - return { title: post.title, description: post.excerpt }; + const apiPost = await fetchBlog(slug); + if (apiPost) return { title: apiPost.title, description: apiPost.abstract ?? undefined }; + const local = getBlogPost(slug); + if (!local) return { title: "مقاله" }; + return { title: local.title, description: local.excerpt }; } export default async function BlogPostPage({ @@ -26,6 +31,36 @@ export default async function BlogPostPage({ params: Promise<{ slug: string }>; }) { const { slug } = await params; + const apiPost = await fetchBlog(slug); + + if (apiPost) { + const comments = await fetchComments("blog", apiPost.id); + return ( + <> + +
+ +

+ {formatFaDate(apiPost.publishedAt || apiPost.createdAt)} +

+ {apiPost.abstract ?

{apiPost.abstract}

: null} + {apiPost.mainTextHtml ? ( +
+ ) : null} + + + بازگشت به مقالات + + + +
+ + ); + } + const post = getBlogPost(slug); if (!post) notFound(); @@ -40,6 +75,9 @@ export default async function BlogPostPage({

{paragraph}

))} +
+ +
بازگشت به مقالات diff --git a/app/blog/page.tsx b/app/blog/page.tsx index 7f5b346..581350c 100644 --- a/app/blog/page.tsx +++ b/app/blog/page.tsx @@ -3,35 +3,92 @@ import Link from "next/link"; import { Container } from "@/components/Container"; import { PageHero } from "@/components/layout/PageHero"; import { Placeholder } from "@/components/Placeholder"; +import { blogHref, blogImage, fetchBlogs } from "@/lib/blog"; import { blogPosts } from "@/lib/data"; +import { faDigits, formatFaDate } from "@/lib/format"; + +export const revalidate = 60; export const metadata: Metadata = { title: "مقالات", description: "نکته‌هایی از تعمیرگاه و بورس قطعات لوازم خانگی برادران برقعی.", }; -export default function BlogPage() { +export default async function BlogPage({ + searchParams, +}: { + searchParams: Promise<{ page?: string }>; +}) { + const { page: pageParam } = await searchParams; + const page = Math.max(1, Number(pageParam) || 1); + const result = await fetchBlogs({ page, pageSize: 12 }); + const usingApi = result.items.length > 0; + const pageCount = usingApi + ? Math.max(1, Math.ceil((result.total || result.items.length) / (result.pageSize || 12))) + : 1; + return ( <>
-
- {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}