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>
12 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.guestCart.ts— the storefront's entire cart story (see next section). No token, no backend call, nobusinessId— justlocalStorage+ 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_tokencookie (written by the customer dashboard withDomain=.sanihome.ir, so it's readable here too) — seeisCustomerLoggedIn()inguestCart.ts. Never build a login UI or call/auth/loginfrom this site. - Guest cart (
guestCart.ts): lines are{ id, name, slug, price, originalPrice?, image, quantity }whereidis always astoreItemVariantId— never a product id or store-item id. Persisted tolocalStorage(keymeshkee-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, soaddItem/updateItem/removeItemare synchronous. - "Continue" in the mini-cart (
CartPopup) builds a URL withcontinueToCheckoutUrl(): the guest cart, base64url-encoded (exact algorithm from the spec) into aguestCartquery param, pointed athttps://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,
ProductPurchasePanelresolves 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 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."
- On the product/store-item detail pages,
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 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 |
/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.