diff --git a/.claude/launch.json b/.claude/launch.json index b209e6a..b061b90 100644 --- a/.claude/launch.json +++ b/.claude/launch.json @@ -5,7 +5,8 @@ "name": "sanihome-dev", "runtimeExecutable": "npm", "runtimeArgs": ["run", "dev"], - "port": 3000 + "port": 3000, + "autoPort": true } ] } diff --git a/.gitignore b/.gitignore index 5ef6a52..59764df 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,4 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts +banners.psd \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index 43c994c..ea1f895 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1 +1,2 @@ @AGENTS.md +@CONTEXT.md diff --git a/CONTEXT.md b/CONTEXT.md new file mode 100644 index 0000000..782dfe6 --- /dev/null +++ b/CONTEXT.md @@ -0,0 +1,230 @@ +# sanihome.ir — project context + +> **Keep this file current.** Whenever a change touches architecture, conventions, +> API integration, or adds/removes a major feature, update the relevant section +> here in the same session. This file exists so any AI assistant or developer +> picking up the repo cold — without the conversation history that produced it — +> can get oriented in minutes instead of re-deriving everything from the code +> and from trial-and-error against the live API. + +## What this is + +A Next.js 16 (App Router, Turbopack) storefront for **سانیهوم (Sanihome)**, a +Persian-language (RTL) home-appliance retailer, backed entirely by the +**Meshkee** commerce platform's public API. There is no local database — +every page reads live from `https://api.meshkee.com/api/v1`, tenant domain +`sanihome.ir`. See [`AGENTS.md`](AGENTS.md) — Next.js 16 has breaking changes +from what most training data assumes; read `node_modules/next/dist/docs/` +before touching App Router APIs you're not 100% sure of. + +**Meshkee API docs** (always check before assuming a field/endpoint shape): +- Hub: https://api.meshkee.com/docs/website +- OpenAPI: https://api.meshkee.com/docs/website/openapi.json +- AI brief: https://api.meshkee.com/docs/website/AI_PROMPT.md +- The OpenAPI spec's `responses` are often undocumented (just a one-line + description like `{ cart }`) — when a shape isn't documented, `curl` the + live endpoint rather than guessing. Real examples discovered this way are + noted below. + +## Stack + +- Next.js 16, Turbopack, TypeScript, Tailwind v4 (`@tailwindcss/typography` for + rich HTML content). +- Swiper for carousels (`src/components/ui/Carousel.tsx` for + auto-width/free-mode rows; `FeaturedCategories.tsx` for a fixed + slides-per-view row). +- Two local font files supplied by the client — `public/fonts/*.woff` + (IRANYekan Light + Bold), loaded via `next/font/local` in + `src/app/layout.tsx`. Body text uses the light weight; **every heading tag + (`h1`–`h6`) uses the bold weight** via a plain CSS rule in `globals.css` — + don't reintroduce a Google-fonted Vazirmatn/etc., this was deliberately + replaced. +- No local dev-server port is guaranteed — `.claude/launch.json` has + `"autoPort": true` because other projects on this machine also default to + port 3000. + +## Data layer (`src/lib/`) + +- **`meshkee.ts`** — all public, unauthenticated reads (and the two + unauthenticated writes: comments, contact form) live here. Every + `normalize*` function reads defensively from multiple possible field names + because different endpoints shape the "same" concept differently (see + Data quirks below). Prefer extending an existing `normalize*`/type over + adding a parallel one. +- **`auth.ts`** — client-only (uses `localStorage`) Meshkee customer auth: + register/login/OTP/refresh. Access tokens last **~15 minutes** — this is + short enough that you *will* hit expiry during normal use, so every + authenticated call must self-heal (see Auth below), not just fail. +- **`cart.ts`** — cart/checkout, needs a Bearer token + `businessId` + (`GET /tenants/{domain}` and `.../website/business-info` both return the + same numeric id as their `id` field — `getBusinessInfo().id` is enough, + no separate tenant-resolve call needed). + +## Auth + +Real in-site login (not a redirect to `customer.sanihome.ir` — that link +still exists in the header/footer as a legacy "ورود" pointer but is +independent of this system). `AuthProvider` (`src/context/AuthContext.tsx`) ++ `LoginModal` (`src/components/auth/LoginModal.tsx`), mounted once in +`src/context/Providers.tsx` inside the root layout. + +- Login methods: password (`POST /auth/login`) and SMS OTP + (`POST /auth/send-otp` → `POST /auth/login-otp`). Register: + `POST /auth/register` (requires `domain: "sanihome.ir"`). +- **Confirmed live response shape** (undocumented in OpenAPI, verified by + actually registering a test account): `{ accessToken, refreshToken, user }` + from register/login/login-otp/refresh alike. `GET /auth/me` returns `{ user }`. +- **Password login requires a verified phone number** + (`cellNumber is not verified` 401) — a freshly-registered account can still + use the tokens *from that register response* immediately, but if the + session is lost before verifying, password login is blocked until OTP + verification. There's no verify-OTP UI built yet (see Known gaps). +- **Token refresh race**: `AuthContext` and `CartContext` each independently + notice a 401 and try to refresh. Without coordination this fires two + concurrent `/auth/refresh` calls and the second one fails (likely + refresh-token rotation server-side) — observed as 500/400 errors. Fixed + with an in-flight-promise singleton in `auth.ts`'s `refreshAccessToken()` + — **do not** add another independent refresh call anywhere; always go + through that function. +- `cart.ts` functions take no token parameter — they read the freshest token + from storage internally and self-refresh-and-retry once on 401. Gate UI on + `useAuth().user` (session identity), not on a captured `accessToken` value. + +## Cart & checkout + +`CartProvider` (`src/context/CartContext.tsx`) fetches the real backend cart +whenever `user` changes and exposes `addItem/updateItem/removeItem/refresh`. +`CartButton` + `CartPopup` (`src/components/cart/`) live in the header via +`Providers`. `/checkout` (`src/app/checkout/page.tsx`) posts to +`POST /businesses/{businessId}/cart/checkout` with `payment.type` of `cash` +or `transfer` only — no online payment gateway (Zarinpal/Mellat/etc.) is +wired up, by explicit scope decision (see Known gaps). + +**Important:** a successful checkout clears the cart server-side. Any flow +that calls `checkout()` must also call `useCart().refresh()` afterward, or +the header badge and cart state go stale until next reload. + +**Which store-item variant does "add to cart" target?** +- On the product/store-item **detail** pages, `ProductPurchasePanel` always + resolves a `matchedVariant` from `storeVariants` (even for a single-SKU + product with no selectable variations — it just uses the one variant) and + adds `matchedVariant.id`. +- On **cards** (`ProductCard`), only sources that already carry a specific + variant id can quick-add: `Product.variantId`, populated in + `normalizeProduct` from `raw.variants[0].id` (store-specials, brand-groups). + Cards from the plain `/products` list endpoint have no variant info, so the + button renders as a link to the detail page instead of a cart action — + never fabricate a variant id to force a quick-add. + +## Comments + +`GET/POST /tenants/{domain}/comments` needs no auth on Meshkee's side +(`authorName`/`authorEmail` are plain body fields) — the login requirement is +a **product decision by this site**, not an API constraint. Gate the comment +form in `ProductTabs`' comments panel on `useAuth().user`; when posting, use +the logged-in user's name/email. `GET /comments` only returns **approved** +comments, so a freshly-posted comment won't reappear on refetch until an +admin approves it — the UI appends it optimistically to local state instead +of re-fetching, which is correct, not a bug. + +## Data quirks (verified against the live catalog) + +- **Persian name always wins**: every `name`-ish field prefers `nameFa` / + `productNameFa` over the English `title`/`name` — this is a Persian + storefront front-to-back. +- **Price/discount fields differ by source.** Some endpoints (plain + `/products` list) only ever give `store.minPrice/maxPrice` with no + original/discounted distinction. Others (store-specials, brand-groups, + store-items) nest `price`/`discountedPrice` one level down, in + `variants[0]` or on the item directly. `normalizeProduct` computes a + `discount` **percent** from real price/oldPrice numbers when the API + doesn't send one explicitly — never invent a percent without two real + prices behind it. +- **Most of the catalog has no price at all** (`store.minPrice: null`) and + **`technical-info` is empty for almost every product** — this is real data + state on the client's side, not a bug. The honest-empty-state path (a + disabled/"تماس با فروشگاه" control, or an `EmptyState` message) is the one + actually exercised for most products; don't "fix" it by fabricating values. +- **Group-source `id` vs `productId`**: store-specials/brand-groups/store-items + wrap a store-item row whose own `id` is *not* the product id — the real + product id is `raw.productId`. `normalizeProduct` prefers `productId`, and + cards/links that only have this id (no slug) route through + `/products/id/[id]` (`src/app/products/id/[id]/page.tsx`), which resolves + `GET /products/by-id/{id}` and redirects to the canonical slug URL. +- **Store-items vs Products**: a "store item" is one purchasable + variant/SKU (id = `storeItemVariantId` for the cart API). `/store-items` + and `/store-items/[id]` (variant-level browsing/detail) are a separate + surface from `/products` and `/products/[slug]` (product-level, with + descriptions/technical-info/full variation picker) — the store-item detail + page cross-references its parent product (`getProductById`) for + images/description/specs, since the variant endpoint alone only has + price/stock/selections. + +## UI conventions (apply these by default; also noted in this session's +persistent memory as "for all the user's sites", not just this one) + +- **Breadcrumb on every page except the homepage** — above the `
سفارش شما با موفقیت ثبت شد ✓
+شماره سفارش: {order.orderNumber}
++ مبلغ کل: {formatPrice(order.total)} تومان +
+ + بازگشت به فروشگاه ← + ++ سوال یا پیشنهادی دارید؟ فرم زیر را پر کنید تا در اسرع وقت پاسخ دهیم. +
+ + + +