# 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 `