11 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 inlinetokens insrc/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 insafe()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-08-31)
| 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/static-images, /website/category-groups |
empty |
/blogs |
empty (module enabled) |
/website/business-info, /website/favicon |
present but blank (no logo, no phone, no socials) |
/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 sliders | src/app/page.tsx's local FALLBACK_SLIDES (banners downloaded from the live moalem.shop theme into public/images/banners/) — used only when the slider static-image slot is also empty, see "Static-image slots" below |
/website/sliders or the slider slot returns items |
| 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 | placeholder SVGs in public/images/categories/{id}.svg, generated by scripts/gen-category-placeholders.mjs — overridden per-slide by the categories static-image slot when it has images |
real thumbnails are dropped in, or the categories slot is filled |
| No branding in API | logo/favicon copied from the live theme into public/ |
logoUrl is set on the tenant |
No banner art (one/two/three-banner-bg) |
src/components/PromoBanners.tsx renders a flat dashed placeholder per slot cell, labelled with the recommended size/ratio |
the matching static-image slot is filled |
Brand
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).
Layout conventions
- Content width
--container-boxed: 1200px, applied with theboxedutility. - 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 toSectionHeaderasnavso they sit beside the «مشاهده همه» pill on desktop; the copy under the track islg:hiddenfor 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 callsGET /auth/me.src/lib/authUser.ts— pure helpers (displayName,isAdmin), safe to import from client components. Never importauth.tsfrom a client component — it pulls innext/headersand breaks the build; that's the entire reason this file is split out.- Admin detection (
isAdmin()) readsuser.isAdmin/user.roledefensively — 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 realstoreItemVariantId, 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 → کنسول بازی → مجله.
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 fromrelatedProductson 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[]fromtechnical-infocarries no label, only afieldId— never render it directly. Join againstform.fieldsfirst viasrc/lib/technicalInfo.ts#buildSpecRows. - Comments:
POST /commentsdoesn't require a bearer token at the API level, butCommentSection.tsxgates the form behindgetSession()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 buttonGET /meshkee/sitemap-config.json— static pages only, auto-scanned fromsrc/appat 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.tsorapp/robots.ts: nginx proxies/sitemap.xmland/robots.txtto 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