From 9e2dc2c61d072a4492e4d178460f0bd04b8407eb Mon Sep 17 00:00:00 2001 From: Alireza Hassani Date: Wed, 30 Sep 2026 09:31:07 +0330 Subject: [PATCH] Wire Meshkee storefront: static images, store-items, guest cart, and auth. Connect homepage slots and specials, switch catalog to store-items, add the customer-portal mini-cart and account menu, and publish the static-image slot catalog. Co-authored-by: Cursor --- CONTEXT.md | 96 ++++++++ next.config.ts | 4 + package-lock.json | 114 --------- src/app/api/auth/handoff/route.ts | 17 ++ src/app/api/auth/me/route.ts | 10 + src/app/cart/CartPageClient.tsx | 114 --------- src/app/cart/page.tsx | 10 +- src/app/layout.tsx | 8 +- src/app/meshkee/static-image-slots/route.ts | 16 ++ src/app/page.tsx | 136 ++++++++++- src/app/products/[id]/[slug]/page.tsx | 23 +- .../products/category/[id]/[slug]/page.tsx | 12 +- src/app/products/page.tsx | 22 +- src/components/AboutDcorya.tsx | 42 +++- src/components/AddToCartBox.tsx | 105 +++++--- src/components/BannerRow.tsx | 95 +++++--- src/components/CartPopover.tsx | 93 ++++--- src/components/ConsoleCategories.tsx | 89 ++++--- src/components/DecorativeProducts.tsx | 32 --- src/components/Header.tsx | 19 +- src/components/Hero.tsx | 145 ++++++----- src/components/LatestProducts.tsx | 42 ---- src/components/ProductCard.tsx | 58 ++++- src/components/ProductCarousel.tsx | 59 +++++ src/components/ProductCategories.tsx | 89 ++++--- src/components/ProductsMegaMenu.tsx | 150 ++++++++++++ src/components/ProfileMenu.tsx | 109 +++++++-- src/components/RemoteProductCard.tsx | 62 +---- src/components/SlotMedia.tsx | 73 ++++++ src/lib/auth-context.tsx | 230 +++++++++++++----- src/lib/cart-context.tsx | 151 +++++++----- src/lib/categories.ts | 31 +++ src/lib/guest-cart.ts | 90 +++++++ src/lib/meshkee-server.ts | 38 +++ src/lib/meshkee.ts | 120 ++++++--- src/lib/providers.tsx | 4 +- src/lib/shared-auth-cookie.ts | 110 +++++++++ src/lib/specials.ts | 124 ++++++++++ src/lib/static-images.ts | 200 +++++++++++++++ src/lib/store.ts | 43 ++++ 40 files changed, 2154 insertions(+), 831 deletions(-) create mode 100644 CONTEXT.md create mode 100644 src/app/api/auth/handoff/route.ts create mode 100644 src/app/api/auth/me/route.ts delete mode 100644 src/app/cart/CartPageClient.tsx create mode 100644 src/app/meshkee/static-image-slots/route.ts delete mode 100644 src/components/DecorativeProducts.tsx delete mode 100644 src/components/LatestProducts.tsx create mode 100644 src/components/ProductCarousel.tsx create mode 100644 src/components/ProductsMegaMenu.tsx create mode 100644 src/components/SlotMedia.tsx create mode 100644 src/lib/categories.ts create mode 100644 src/lib/guest-cart.ts create mode 100644 src/lib/meshkee-server.ts create mode 100644 src/lib/shared-auth-cookie.ts create mode 100644 src/lib/specials.ts create mode 100644 src/lib/static-images.ts create mode 100644 src/lib/store.ts diff --git a/CONTEXT.md b/CONTEXT.md new file mode 100644 index 0000000..ba074f5 --- /dev/null +++ b/CONTEXT.md @@ -0,0 +1,96 @@ +# Dcorya — AI context + +Persian RTL decorative home-goods storefront for **dcorya.com** (دکوریا). Next.js App Router. + +## 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: `dcorya.com` (`WEBSITE_DOMAIN` in `src/lib/meshkee.ts`) +- Customer portal: `https://customer.dcorya.com` +- Public reads: `/tenants/{domain}/...` (no auth). Cart: `/businesses/{businessId}/...` + Bearer JWT. +- Do not invent CMS, upload, or dashboard APIs. + +## Stack + +- Next.js 16 (App Router) + React 19 + TypeScript + Tailwind CSS v4 +- `output: "standalone"` in `next.config.ts` (required for Meshkee websites VM deploy) +- Helpers: `src/lib/meshkee.ts`, static images: `src/lib/static-images.ts` + +## Routes + +| Path | Source | +| --- | --- | +| `/` | Homepage + static image slots + 3 store-special carousels | +| `/products` | Product catalog | +| `/products/[id]/[slug]` | Product details | +| `/products/category/[id]/[slug]` | Category listing | +| `/blog`, `/blog/[id]/[slug]` | Blog | +| `/about`, `/contact`, `/cart`, `/purchaseguide`, `/satisfaction` | Static / form pages | +| `/meshkee/static-image-slots` | Slot catalog for the dashboard Refresh button | +| `/meshkee/sitemap-config.json` | Sitemap config | + +## Static image slots + +Homepage images: `GET /tenants/{domain}/website/static-images?pageKey=home`. +Catalog: `GET /meshkee/static-image-slots` → `STATIC_IMAGE_SLOTS` in `src/lib/static-images.ts`. + +| key | kind | itemCount | UI section | aspectRatio | +| --- | --- | --- | --- | --- | +| `slider` | list | null | `Hero` full-bleed | `1920:1100` | +| `categories` | list | null | `ProductCategories` icons | `1:1` | +| `two-banner-below-slider` | list | 2 | `BannerRow` | `2:1` | +| `second-categories` | list | null | `ConsoleCategories` | `1:1` | +| `about-us-image` | single | 1 | `AboutDcorya` photo | `4:3` | + +Rules: +- `url` → ``; `linkUrl` → wrap in `` / `Link` +- Use `titleFa` → Persian label, `titleEn` → English subtitle, `subtext` when the original UI shows them +- Empty/fail → keep current local placeholders under `/public/images/` +- Use `slot.aspectRatio` from the API (catalog fallback if missing) +- **If a slot’s CSS size or aspect ratio changes during development, update `STATIC_IMAGE_SLOTS` in the same change** — Meshkee reads the catalog as the source of truth + +## Local fallbacks + +- Slider: `/images/slide-1.jpg` … `slide-4.jpg` +- Categories: `/images/cat-*.png` +- Banners: `/images/banner-1.png`, `/images/console-cat-4.jpg` +- Second categories: `/images/console-cat-*.png|jpg` +- About: `/images/product-2.jpg` + +## Auth / account menu + +- Login / user icon → `https://customer.dcorya.com` (`CUSTOMER_PORTAL_URL`) +- Detect session from parent-domain cookies `meshkee_customer_access_token` / `meshkee_customer_refresh_token` (storefront only reads; customer dashboard writes) +- Logged in: show `{name} عزیز` with dropdown **پروفایل من**, **خروج**, and if website admin **مدیریت وبسایت** +- Admin handoff: `POST /api/auth/handoff` → `https://business.dcorya.com?handoff=…` +- Admin = super_admin or membership on this business (`isOwner` / `teamRole`) +- Favicon: `GET /tenants/{domain}/website/favicon` → `faviconUrl` / `logoUrl`, else `/images/logo.png` + +## Shopping cart (mini-cart only) + +- Header cart icon + quantity badge + popup (lines, qty, remove, total, **ادامه خرید**) +- Guest cart key `meshkee-guest-cart` in `localStorage` + parent-domain cookie +- Line `id` = `storeItemVariantId`; price = `discountedPrice ?? price` +- Continue → `customer.dcorya.com/checkout/cart?guestCart=…` (or login redirect when logged out) +- Do **not** build storefront `/cart`, checkout, or call `/businesses/.../cart` + +## Catalog + +- Listings / carousels / search: `GET /tenants/{domain}/store-items` (not `/products` list) +- Detail still uses `GET /products/by-id/{id}` for description/gallery/comments + `store-items/by-product/{id}` for variants +- Mega menu on **محصولات**: categories level 1–2. Links: `/products/category/{id}/{slug}` +- Product card: shared `ProductCard` (1:1 image, `object-cover`); no price → **تماس بگیرید** + +## Homepage product carousels + +`GET /tenants/{domain}/store-specials` (`source` mirrors `specialProductsSource`). Shared card: `ProductCard` (1:1 image box, `object-contain`). + +| key | eyebrow | placement | +| --- | --- | --- | +| `new-collection` | NEW ARRIVALS | After categories | +| `special-sale` | SPECIAL SALE | After console categories | +| `our-handmades` | HANDCRAFTED | After about (muted bg) | + +Empty special `items` → local placeholders (`new-collection` also tries latest products first). Title comes from the special when set. diff --git a/next.config.ts b/next.config.ts index 642c3da..5394c53 100644 --- a/next.config.ts +++ b/next.config.ts @@ -5,6 +5,10 @@ const nextConfig: NextConfig = { output: "standalone", images: { remotePatterns: [ + { + protocol: "https", + hostname: "cdn.meshkee.com", + }, { protocol: "https", hostname: "c804387.parspack.net", diff --git a/package-lock.json b/package-lock.json index 50cbabd..b58cbb6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -631,9 +631,6 @@ "cpu": [ "arm" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -650,9 +647,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -669,9 +663,6 @@ "cpu": [ "ppc64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -688,9 +679,6 @@ "cpu": [ "riscv64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -707,9 +695,6 @@ "cpu": [ "s390x" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -726,9 +711,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -745,9 +727,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -764,9 +743,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -783,9 +759,6 @@ "cpu": [ "arm" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -808,9 +781,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -833,9 +803,6 @@ "cpu": [ "ppc64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -858,9 +825,6 @@ "cpu": [ "riscv64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -883,9 +847,6 @@ "cpu": [ "s390x" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -908,9 +869,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -933,9 +891,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -958,9 +913,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -1228,9 +1180,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1247,9 +1196,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1266,9 +1212,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1285,9 +1228,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1526,9 +1466,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1546,9 +1483,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1566,9 +1500,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1586,9 +1517,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2139,9 +2067,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2156,9 +2081,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2173,9 +2095,6 @@ "loong64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2190,9 +2109,6 @@ "loong64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2207,9 +2123,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2224,9 +2137,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2241,9 +2151,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2258,9 +2165,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2275,9 +2179,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2292,9 +2193,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -4980,9 +4878,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -5004,9 +4899,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -5028,9 +4920,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -5052,9 +4941,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ diff --git a/src/app/api/auth/handoff/route.ts b/src/app/api/auth/handoff/route.ts new file mode 100644 index 0000000..93365f5 --- /dev/null +++ b/src/app/api/auth/handoff/route.ts @@ -0,0 +1,17 @@ +import { getBearerToken, jsonResponse, meshkeeFetch, unauthorizedResponse } from "@/lib/meshkee-server"; + +export async function POST(request: Request) { + const token = getBearerToken(request); + if (!token) return unauthorizedResponse(); + + const res = await meshkeeFetch("/auth/handoff", { + method: "POST", + token, + }); + + if (!res.ok) { + return jsonResponse({ error: "امکان باز کردن پنل مدیریت نیست." }, res.status); + } + + return jsonResponse(res.data); +} diff --git a/src/app/api/auth/me/route.ts b/src/app/api/auth/me/route.ts new file mode 100644 index 0000000..53435fb --- /dev/null +++ b/src/app/api/auth/me/route.ts @@ -0,0 +1,10 @@ +import { getBearerToken, jsonResponse, meshkeeFetch, unauthorizedResponse } from "@/lib/meshkee-server"; + +export async function GET(request: Request) { + const token = getBearerToken(request); + if (!token) return unauthorizedResponse(); + + const res = await meshkeeFetch("/auth/me", { token }); + if (!res.ok) return jsonResponse({ error: "Unauthorized" }, res.status); + return jsonResponse(res.data); +} diff --git a/src/app/cart/CartPageClient.tsx b/src/app/cart/CartPageClient.tsx deleted file mode 100644 index fd9ef90..0000000 --- a/src/app/cart/CartPageClient.tsx +++ /dev/null @@ -1,114 +0,0 @@ -"use client"; - -import Image from "next/image"; -import Link from "next/link"; -import Header from "@/components/Header"; -import Footer from "@/components/Footer"; -import { useAuth } from "@/lib/auth-context"; -import { useCart } from "@/lib/cart-context"; - -function formatToman(value?: number) { - if (!value && value !== 0) return null; - return `${value.toLocaleString("fa-IR")} تومان`; -} - -export default function CartPageClient() { - const { user, isLoading, loginUrl } = useAuth(); - const { cart, updateItem, removeItem } = useCart(); - const items = cart?.items ?? []; - const total = items.reduce((sum, item) => sum + (item.unitPrice ?? 0) * item.quantity, 0); - - return ( - <> -
-
-

سبد خرید

- - {isLoading ? ( -

در حال بارگذاری...

- ) : !user ? ( -
-

- برای مشاهده سبد خرید ابتدا وارد حساب کاربری خود شوید. -

-
- ورود به حساب کاربری - -
- ) : items.length === 0 ? ( -
-

سبد خرید شما خالی است.

- - مشاهده محصولات - -
- ) : ( -
- {items.map((item) => ( -
-
- {item.image && ( - {item.title - )} -
-
-

{item.title ?? "کالا"}

- {formatToman(item.unitPrice) && ( -

{formatToman(item.unitPrice)}

- )} -
-
- - {item.quantity} - -
- -
- ))} - - {formatToman(total) && ( -
- جمع کل - {formatToman(total)} -
- )} -
- )} -
-