Files
moalem-shop/AGENTS.md
T
amirhosein.ashourlooandClaude Sonnet 5 db7d00e473 Add about/contact/installments pages, fix category slot bugs, wire live branding
- Enlarge category carousel thumbnails (90px -> 104-140px responsive) and
  confirm the slider/categories/banner static-image slots are all reading
  correctly from the CMS (verified live, not a wiring bug)
- Fix installments page dropping "هندزفری و هدفون": it's a level-3 category,
  not level-2, so level2.find() silently missed it - use findNode() over the
  whole tree instead
- Extend gen-category-placeholders.mjs to generate a placeholder for every
  category at every level (was level-2 only), needed for the fix above
- Add /about, /contact, /installments pages
  - About: real copy transcribed from the live moalem.shop theme, real
    product photos from the API, text-only brand strip (no scraped logos)
  - Contact: ContactForm.tsx posts to the real POST /contact-submissions,
    branch cards with key-less Google Maps embeds
  - Installments: flat brand-colored hero (no photo/gradient), category
    highlights reusing the same categories slot images as the homepage
- Replace the placeholder header/footer phone number with the real one, add
  real branch addresses and Instagram/Telegram links (src/data/site.ts),
  pulled from the live theme's footer
- Wire logo/favicon to GET /website/favicon (getFavicon() in api.ts) instead
  of the static bundled files, so a dashboard upload now shows automatically;
  layout.tsx metadata is now generateMetadata() to support this. The tenant's
  uploaded logo/favicon currently belongs to a different business - the
  wiring is correct and will self-correct once that's fixed in the dashboard

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

12 KiB

moalem.shop — context for AI agents and tooling

Storefront for فروشگاه موبایل معلم built on the Meshkee CMS contract. Keep this file current: every prompt that changes structure, data sources or fallbacks should update it.

Stack

  • Next.js 16.3 (App Router, Turbopack) · React 19 · TypeScript
  • Tailwind CSS v4 (@theme inline tokens in src/app/globals.css)
  • Swiper 14 for every slider/carousel
  • fa / RTL, flat design, no gradients

Data — Meshkee Website API

  • Base: https://api.meshkee.com/api/v1, docs: https://api.meshkee.com/docs/website
  • Tenant: moalem.shop (business id 27, specialProductsSource: store_item)
  • Every public read goes through src/lib/api.ts; wrap calls in safe() so an empty or failing module degrades one section instead of the page.
  • Never invent CMS/admin endpoints. If OpenAPI and any prose conflict, OpenAPI wins.

What the tenant actually returns today (2026-09-06)

Endpoint State
enabledModules (GET /tenants/moalem.shop) ["products", "blog", "finance"] — workshops is not enabled. Do not build a workshops page/nav entry until it is.
/products 479 items, real titles, images, categories
/categories?entityType=product 48 items, 3 levels under «کالای دیجیتال». Products are filed on leaf categories only — a parent category page/carousel must query its whole subtree (subtreeIds/subtreeIdsAll in src/lib/categoryTree.ts).
/store-items, /store-specials empty → no prices, no discounts, everything inStock:false. /store-items/by-product/{id} returns a single { storeItem } (or null) — for a product's full variant list use /store-items?productId=&pageSize=100 ({ items }) instead, see getStoreItemsByProduct in src/lib/api.ts.
/website/sliders, /website/category-groups empty
/website/static-images filled — slider (2), categories (8 of 15), one/two/three-banner-bg all have real images. Verified live and rendering correctly; if a future check finds otherwise, suspect a stale build/deploy before suspecting the wiring.
/blogs empty (module enabled)
/website/business-info phones/addresses/socialMedia still empty; logoUrl/faviconUrl are now set but point to a different business's mark (see "Brand")
/products/by-id/{id}/technical-info { form: null, values: [] } — empty on every product checked so far
/comments empty, but POST works (no bearer required by the API itself)

Fallbacks in place until the CMS is filled

Gap Fallback Remove when
No prices src/lib/demoPricing.ts, gated by NEXT_PUBLIC_DEMO_PRICING=1 — deterministic price per product id store-items exist
No blog posts src/data/fallbackBlogs.ts — the real posts from moalem.shop/articles, shown on the homepage and /blog as non-clickable cards (no real detail page exists for them) /blogs returns items
No category art past slot position 8 placeholder SVGs in public/images/categories/{id}.svg, generated by scripts/gen-category-placeholders.mjs for every category at every level, not just level-2 the categories slot has an image for that position
Wrong logo/favicon in API getFavicon() is wired live (see "Brand") but the uploaded asset itself is wrong; no code fallback needed once the dashboard upload is fixed the correct asset is uploaded

Brand

Colors taken from the live moalem.shop theme, not invented: primary #E51E22, dark #B10123, ink #2E2E2E, body #7C7C7C. Fonts: IRANYekan (fa, local public/fonts) + Montserrat (latin, next/font).

  • Logo/favicon: getFavicon() in src/lib/api.ts reads GET /tenants/moalem.shop/website/favicon — the documented single source — and Header.tsx/Footer.tsx/generateMetadata() in layout.tsx all use it, falling back to the bundled /images/logo.png / /favicon.png only when the API field is null. As of 2026-09-06 the dashboard has a logo/ favicon uploaded, but it's the wrong business's (a home-appliances store's mark, not موبایل معلم) — the wiring is correct and will pick up the right asset automatically the moment it's replaced in the dashboard; don't "fix" this by hardcoding the local fallback again.
  • Contact data (phone, mobile, branch addresses, Instagram/Telegram) in src/data/site.ts is real, pulled from the live theme's footer — GET /website/business-info still returns empty phoneNumbers/addresses/ socialMedia for this tenant, so these stay hand-maintained here until that's filled in. Update both together when it is.

Layout conventions

  • Content width --container-boxed: 1200px, applied with the boxed utility.
  • Homepage order: promo strip → header → hero slider → category carousel → hot deals → product carousels (interleaved with promo banners) → magazine carousel → footer. Exact interleaving is in "Static-image slots" below.
  • Section header (title + one-line desc on the right, «مشاهده همه» pill on the left) is src/components/ui/SectionHeader.tsx — reuse it for every carousel.
  • Product card: ui/ProductCard.tsx, 8px radius, discount badge top-right, strikethrough old price above the current one. The price block is a fixed 44px row and the old-price line is always rendered (empty when there is no discount) so every card in a carousel is exactly the same height — keep that invariant when adding badges or meta lines.
  • Carousel arrows: ui/CarouselNav.tsx, passed to SectionHeader as nav so they sit beside the «مشاهده همه» pill on desktop; the copy under the track is lg:hidden for thumbs.
  • Prices are Toman and rendered with Persian digits (src/lib/format.ts).

Navigation

  • Desktop (lg+): full header — logo, search, account/cart, then the product mega menu and the main nav.
  • Mobile: no hamburger. The header carries the search box alone; everything else lives in MobileBottomNav.tsx, a fixed bottom bar with خانه / سبد خرید / دسته‌بندی‌ها / ورود (or the account menu once signed in). «دسته‌بندی‌ها» opens a bottom sheet with the level-2 categories and an accordion for their children. Any new mobile entry point belongs in that bar, not in a drawer.

Auth / account menu

Login itself happens off-site, on the customer portal (customer.{domain}) — this storefront never renders a login form. For the header/bottom-nav account menu to show "نام عزیز" instead of "ورود | ثبت‌نام", the customer portal needs to set a non-HttpOnly cookie on the shared parent domain (.{domain}) named AUTH_TOKEN_COOKIE (src/lib/config.ts, currently "meshkee_token") holding the bearer access token. Until that exists, getSession() in src/lib/auth.ts always resolves to null and the anonymous view is all anyone will see — this is expected, not a bug, and nothing else breaks.

  • src/lib/auth.ts — server-only (next/headers): getSession() reads the cookie and calls GET /auth/me.
  • src/lib/authUser.ts — pure helpers (displayName, isAdmin), safe to import from client components. Never import auth.ts from a client component — it pulls in next/headers and breaks the build; that's the entire reason this file is split out.
  • Admin detection (isAdmin()) reads user.isAdmin/user.role defensively — the OpenAPI schema doesn't document a role field on the user object, so this is a best guess. Update it once the real shape is confirmed.
  • Cart ("افزودن به سبد خرید" in ProductBuyPanel.tsx) needs a real storeItemVariantId, which only exists once a product has store-items — today that's none of them, so the button always shows "این محصول هنوز برای خرید آنلاین فعال نشده است" regardless of login state. That's correct given the data, not a bug either.

Static-image slots

Slot catalog: src/app/meshkee/static-image-slots/route.ts. Keys and where each is consumed:

Key Kind Consumed by
slider list HeroSlider.tsx — wins over FALLBACK_SLIDES when it has images
categories list CategoryCarousel.tsx — images matched positionally to the category list (business fills them in the same order); a category past the end of the slot keeps its SVG placeholder
one-banner-bg single PromoBanners.tsx → OneBanner
two-banner-bg list (2) PromoBanners.tsx → TwoBanners
three-banner-bg list (3) PromoBanners.tsx → ThreeBanners

Rule: every aspectRatio/recommendedWidth in that route file must match what the component actually renders. If you change a banner's size or ratio, update its slot entry in the same commit — that file is the only thing telling the dashboard (and the designer) what to upload.

Homepage banner placement (mirrors the logilook.com reference given for this layout): hero → categories → hot deals → three-banner-bg → موبایل carousel → two-banner-bg → لوازم جانبی → لپ تاپ → one-banner-bg → کنسول بازی → مجله.

Static pages

  • /about — real copy transcribed from the live moalem.shop theme's about-us module (verified against its cached HTML, not invented); brand strip is plain text wordmarks (no scraped logo images — avoids a trademark/asset-licensing headache for names we don't have real logo files for) with real product photos pulled from the API.
  • /contact — ContactForm.tsx posts to the real POST /contact-submissions (no bearer required); branch cards use BRANCHES from src/data/site.ts plus a key-less Google Maps output=embed iframe per branch (no API key needed for a query-based embed).
  • /installments — flat, brand-colored hero (no photo, no gradient — this site stays flat) instead of the reference design's photographic banner; category highlights reuse the same categories static-image slot images as the homepage carousel, matched by the same positional index. One reference category ("خانه هوشمند") has no equivalent on this tenant and is swapped for "لوازم برقی" — see HIGHLIGHT_IDS in the page for the mapping.

Product / blog pages

  • /products — full catalog, ?name= search, ?page=.
  • /products/category/{id}/{slug} — category listing; merges every leaf in the subtree (see the leaf-category note above) and paginates in memory.
  • /products/{id}/{slug} — detail: ProductGallery.tsx (click-to-lightbox + hover-zoom via a background-position overlay, desktop only), ProductBuyPanel.tsx (variants, price, auth-aware cart), ProductInfoTabs.tsx (توضیحات / مشخصات فنی / دیدگاه کاربران), related products from relatedProducts on the by-id response.
  • /blog, /blog/{id}/{slug} — same shape, no variants/tabs; just content + CommentSection.tsx. Fallback posts (see table above) only exist in the listing grid, not as real detail pages.
  • Technical specs: values[] from technical-info carries no label, only a fieldId — never render it directly. Join against form.fields first via src/lib/technicalInfo.ts#buildSpecRows.
  • Comments: POST /comments doesn't require a bearer token at the API level, but CommentSection.tsx gates the form behind getSession() per the page brief — anonymous visitors get a login prompt instead of a textarea.

URLs (Meshkee canonical shapes)

  • Product /products/{id}/{nameFaSlug}
  • Category /products/category/{id}/{nameFaSlug}
  • Blog /blog/{id}/{titleSlug}
  • Helpers live in src/lib/slug.ts — build links with those, never by hand.

Published for the platform

  • GET /meshkee/static-image-slots — slot catalog for the dashboard Refresh button
  • GET /meshkee/sitemap-config.json — static pages only, auto-scanned from src/app at request time (src/app/meshkee/sitemap-config.json/route.ts) rather than hand-listed, so it can never advertise a page that doesn't exist yet. Product/category/blog/portfolio detail pages are excluded on purpose — Meshkee resolves those straight from the API.
  • Do not add app/sitemap.ts or app/robots.ts: nginx proxies /sitemap.xml and /robots.txt to the API.

SEO rules for every new page

Unique <title> + meta description, exactly one <h1>, real <h2> structure, non-empty alt on every content image.

Scripts

npm run dev     # localhost:3000
npm run build
node scripts/gen-category-placeholders.mjs   # refresh category thumb placeholders