- 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>
13 KiB
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):
- 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
responsesare often undocumented (just a one-line description like{ cart }) — when a shape isn't documented,curlthe live endpoint rather than guessing. Real examples discovered this way are noted below.
Stack
- Next.js 16, Turbopack, TypeScript, Tailwind v4 (
@tailwindcss/typographyfor rich HTML content). - Swiper for carousels (
src/components/ui/Carousel.tsxfor auto-width/free-mode rows;FeaturedCategories.tsxfor a fixed slides-per-view row). - Two local font files supplied by the client —
public/fonts/*.woff(IRANYekan Light + Bold), loaded vianext/font/localinsrc/app/layout.tsx. Body text uses the light weight; every heading tag (h1–h6) uses the bold weight via a plain CSS rule inglobals.css— don't reintroduce a Google-fonted Vazirmatn/etc., this was deliberately replaced. - No local dev-server port is guaranteed —
.claude/launch.jsonhas"autoPort": truebecause 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. Everynormalize*function reads defensively from multiple possible field names because different endpoints shape the "same" concept differently (see Data quirks below). Prefer extending an existingnormalize*/type over adding a parallel one.auth.ts— client-only (useslocalStorage) 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-infoboth return the same numeric id as theiridfield —getBusinessInfo().idis 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 insrc/context/Providers.tsxinside the root layout.
- Login methods: password (
POST /auth/login) and SMS OTP (POST /auth/send-otp→POST /auth/login-otp). Register:POST /auth/register(requiresdomain: "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/mereturns{ user }. - Password login requires a verified phone number
(
cellNumber is not verified401) — 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:
AuthContextandCartContexteach independently notice a 401 and try to refresh. Without coordination this fires two concurrent/auth/refreshcalls and the second one fails (likely refresh-token rotation server-side) — observed as 500/400 errors. Fixed with an in-flight-promise singleton inauth.ts'srefreshAccessToken()— do not add another independent refresh call anywhere; always go through that function. cart.tsfunctions take no token parameter — they read the freshest token from storage internally and self-refresh-and-retry once on 401. Gate UI onuseAuth().user(session identity), not on a capturedaccessTokenvalue.
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,
ProductPurchasePanelalways resolves amatchedVariantfromstoreVariants(even for a single-SKU product with no selectable variations — it just uses the one variant) and addsmatchedVariant.id. - On cards (
ProductCard), only sources that already carry a specific variant id can quick-add:Product.variantId, populated innormalizeProductfromraw.variants[0].id(store-specials, brand-groups). Cards from the plain/productslist 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 prefersnameFa/productNameFaover the Englishtitle/name— this is a Persian storefront front-to-back. - Price/discount fields differ by source. Some endpoints (plain
/productslist) only ever givestore.minPrice/maxPricewith no original/discounted distinction. Others (store-specials, brand-groups, store-items) nestprice/discountedPriceone level down, invariants[0]or on the item directly.normalizeProductcomputes adiscountpercent 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) andtechnical-infois 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 anEmptyStatemessage) is the one actually exercised for most products; don't "fix" it by fabricating values. - Group-source
idvsproductId: store-specials/brand-groups/store-items wrap a store-item row whose ownidis not the product id — the real product id israw.productId.normalizeProductprefersproductId, and cards/links that only have this id (no slug) route through/products/id/[id](src/app/products/id/[id]/page.tsx), which resolvesGET /products/by-id/{id}and redirects to the canonical slug URL. - Store-items vs Products: a "store item" is one purchasable
variant/SKU (id =
storeItemVariantIdfor the cart API)./store-itemsand/store-items/[id](variant-level browsing/detail) are a separate surface from/productsand/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>, viasrc/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). OneProductPurchasePanelclient component owns the shared selection/quantity/cart state and renders as aFragmentwith 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 inCarousel.tsx: override the<SwiperSlide>toheight: "auto"via inline style (wins over the stylesheet regardless of import order) so normal flex-stretch equalizes the row; pair withflex h-full flex-colon 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 underdir="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.