Files
sanihome/CONTEXT.md
T
amirhosein.ashourlooandClaude Sonnet 5 5152b16d61 Revert to guest-cart architecture, drop in-site auth/checkout
Per api.meshkee.com/docs/website/AI_PROMPT.md's hard rules (which an
earlier pass on this branch missed): login, register, OTP, the real
cart, addresses, and payment are the customer dashboard's job
(customer.sanihome.ir), not this storefront's. This site should only
ever run a local guest mini-cart and hand off to the dashboard.

- Remove: AuthContext, LoginModal, lib/auth.ts, lib/cart.ts (the real
  /businesses/{id}/cart integration), and the /checkout page entirely.
- Add lib/guestCart.ts: localStorage + shared cookie
  (meshkee-guest-cart, Domain=.sanihome.ir) cart, storeItemVariantId
  keyed, matching the spec's line shape and ~3500-char cookie-size
  cutoff.
- CartContext is now a thin React wrapper over the guest cart (no
  server round-trip, so add/update/remove are synchronous); CartPopup's
  "Continue" builds the exact customer.sanihome.ir/checkout/cart
  (or /login?redirect=...) URL from the spec, base64url-encoding the
  cart and detecting an existing dashboard session via the shared
  meshkee_customer_access_token cookie.
- ProductCard and ProductPurchasePanel add to the guest cart directly;
  no login gate on add-to-cart anymore.
- Comments panel is a plain guest name/email/text form again (the
  comments API never required auth — gating it on the now-removed
  login system was wrong regardless).
- CONTEXT.md rewritten to document the guest-cart contract explicitly
  and flag this exact mistake so it doesn't get reintroduced.

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

12 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.
  • guestCart.ts — the storefront's entire cart story (see next section). No token, no backend call, no businessId — just localStorage + a shared cookie.

Auth, cart & checkout: this site does none of it

This was built wrong once already — an earlier pass added real in-site login (password/OTP), a real backend cart (/businesses/{businessId}/cart), and a /checkout page. That's explicitly not how Meshkee storefronts work, per https://api.meshkee.com/docs/website/AI_PROMPT.md's hard rules: login, register, OTP, the real cart, addresses, and payment all belong to https://customer.<domain> (the customer dashboard) — a separate app, not a page on this site. If you're about to add a /login, /register, /checkout, /cart, or /account route, or call /auth/* or /businesses/{id}/cart* from this codebase: stop, re-read the AI_PROMPT.md "Shopping cart on the storefront" and "Shared login" sections first.

  • Detecting a logged-in shopper: read the meshkee_customer_access_token cookie (written by the customer dashboard with Domain=.sanihome.ir, so it's readable here too) — see isCustomerLoggedIn() in guestCart.ts. Never build a login UI or call /auth/login from this site.
  • Guest cart (guestCart.ts): lines are { id, name, slug, price, originalPrice?, image, quantity } where id is always a storeItemVariantId — never a product id or store-item id. Persisted to localStorage (key meshkee-guest-cart) and a cookie of the same name (Domain=.sanihome.ir, skipped if the encoded JSON is

    ~3500 chars — rely on the URL param instead). CartProvider (src/context/CartContext.tsx) just wraps this in React state; there is no server round-trip, so addItem/updateItem/removeItem are synchronous.

  • "Continue" in the mini-cart (CartPopup) builds a URL with continueToCheckoutUrl(): the guest cart, base64url-encoded (exact algorithm from the spec) into a guestCart query param, pointed at https://customer.sanihome.ir/checkout/cart?guestCart=... if the shopper looks logged in, or .../login?redirect=... (wrapping that same cart URL) if not. The dashboard is responsible for decoding it and syncing it into the real server cart after login — this site's job ends at building that URL correctly.
  • Which variant does "add to cart" target?
    • On the product/store-item detail pages, ProductPurchasePanel 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 links to the detail page instead — never fabricate a variant id to force a quick-add.
    • The spec's fuller flow (GET /store-items/by-product/{productId}, then: hide the button if no in-stock variant exists, force a picker when more than one is in stock) isn't separately re-implemented per card — the existing variation-picker UI on the detail page already satisfies "shopper must choose before adding."

Comments

GET/POST /tenants/{domain}/comments needs no auth on Meshkee's side — authorName/authorEmail are plain body fields, same as the contact form. ProductTabs' comments panel is a guest name+email(optional)+text form, no login involved (this also isn't gated on the dashboard cookie — keep it that way; there's no reason to require a customer-dashboard session just to leave a comment). 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
/contact Contact form → POST /contact-submissions, toast + reset on success

No /login, /register, /checkout, /cart, or /account route exists here on purpose — see "Auth, cart & checkout" above.

Known gaps / explicit scope decisions

  • Login, registration, OTP, the real cart, addresses, and payment are all out of scope for this codebase by design — they live on customer.sanihome.ir. Don't reintroduce them here.
  • A throwaway test customer account was created against the live business database while an earlier (since-reverted) pass tested a real backend cart: +989120000001 / "Test Dev". Safe to delete from the Meshkee admin panel — it's not used by anything in this codebase anymore.