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

13 KiB
Raw Blame History

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 — 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):

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.