Files
sanihome/CONTEXT.md
T
amirhosein.ashourlooandClaude Sonnet 5 66f92b634e Add customer auth, real cart/checkout, store-items pages, comments, contact form
- Real in-site login/register (password + SMS OTP) against Meshkee's
  /auth API, with self-healing access-token refresh (15 min lifetime) and
  an in-flight-refresh guard so AuthContext and CartContext don't race
  each other into a failed concurrent refresh.
- Real cart: header cart button/badge, a cart popup, and a working
  "add to cart" on both product cards (where a specific variant id is
  known) and the product/store-item detail buy-box (always resolves a
  purchasable variant, even for single-SKU products with no selectable
  variation).
- /checkout: address + cash/bank-transfer payment, posts to the real
  checkout API and creates an order; refreshes the cart afterward so the
  header badge doesn't go stale.
- New /store-items and /store-items/[id] pages (variant-level browsing,
  separate from the product-level /products catalog), cross-referencing
  the parent product for images/description/technical-info.
- Product gallery: click-to-open fullscreen lightbox alongside the
  existing hover-zoom.
- Comments tab wired to the real comments API, gated to logged-in users
  (a product decision — the API itself doesn't require auth) with a
  login prompt otherwise.
- Similar-products row on product/store-item detail pages.
- Contact page wired to the real contact-submissions API, with a toast
  and inline confirmation on success, and the form resetting.
- CONTEXT.md: a maintained architecture/conventions doc for this repo,
  now auto-loaded via CLAUDE.md, covering the Meshkee integration quirks
  and UI conventions established across this project.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 08:06:30 +03:30

231 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 `<h1>`, via
`src/components/ui/Breadcrumb.tsx`.
- **Product detail layout**: 3-column grid (desktop) — gallery
(`ProductGallery`, ~1/3, includes hover-zoom + click-to-open lightbox) →
title + variation pickers (~5/12) → sticky buy-box (~1/4). One
`ProductPurchasePanel` client component owns the shared
selection/quantity/cart state and renders as a `Fragment` with two
grid-column-span children so it can span two non-adjacent grid cells while
the server renders the static header content into it via a prop.
- **Discount price display** (cards and buy-box both): old price
strikethrough + green percent badge **on the same line, badge to the
old price's left** — current price alone, bold, on the next line. Never a
min–max range once a real variant-driven price exists.
- **Equal-height cards in a `slidesPerView: "auto"` + freeMode Swiper row**:
Swiper's own `.swiper-slide { height: 100% }` doesn't stretch to the
tallest sibling in this mode (no definite ancestor height for the
percentage to resolve against). Fix applied in `Carousel.tsx`: override
the `<SwiperSlide>` to `height: "auto"` via inline style (wins over the
stylesheet regardless of import order) so normal flex-stretch equalizes
the row; pair with `flex h-full flex-col` on the card root and put the
button last so it pins to the bottom.
- **Tabbed product-detail section** (`ProductTabs`): مشخصات فنی / نقد و
بررسی / نظرات کاربران, in that RTL order (rightmost/default-active first
in the array — first DOM child renders rightmost under `dir="rtl"`).
- Category tile images: 110px, no border, no border-radius
(`FeaturedCategories.tsx`).
## Static-image slots
Admin-managed marketing images (banners, category tiles) — not tied to real
catalog data. `STATIC_IMAGE_SLOT_CATALOG` in `meshkee.ts` is the source of
truth for which slots exist; publishing it via
`src/app/meshkee/static-image-slots/route.ts` (`/meshkee/static-image-slots`)
is how the admin dashboard's "refresh" imports new keys. Current keys:
`home-hero`, `two-banner`, `three-banner-row`, `featured-categories`. Adding
a slot here does **not** make images appear — someone still has to upload
them in the dashboard; the code path just needs to fall back to whatever the
"current" behavior was before the slot existed when it's empty (see
`FeaturedCategories.tsx` for the pattern: real slot images when present,
else the pre-existing UI unchanged).
## Route map
| Route | Notes |
|---|---|
| `/` | Homepage — all the carousels/banners |
| `/products`, `/products/[slug]` | Catalog browsing, category filter via `?categoryId=` |
| `/products/id/[id]` | Redirect-only resolver, see "Group-source id vs productId" above |
| `/store-items`, `/store-items/[id]` | Variant-level browsing/detail, see above |
| `/blog`, `/blog/[slug]` | Blog listing/detail |
| `/checkout` | Cart → order, cash/transfer only |
| `/contact` | Contact form → `POST /contact-submissions`, toast + reset on success |
## Known gaps / explicit scope decisions
- No OTP-verification UI for a registered-but-unverified phone number — if a
returning shopper's session is lost pre-verification, password login fails
with a clear error but there's no in-app way to complete verification yet
(OTP *login* still works as a workaround).
- No online payment gateway integration (Zarinpal/Mellat/etc.) — checkout
only supports cash-on-delivery and bank transfer, by explicit user choice
over building the bank-redirect + return-URL callback flow.
- A throwaway test customer account exists in the live business database
from building/verifying this integration: `+989120000001` / "Test Dev".
Safe to delete from the Meshkee admin panel.