- Header/footer logo now reads from GET /tenants/{domain}/website/favicon
(logoDarkUrl on the navy bar, logoUrl on the white footer) with a local
fallback, matching the favicon's existing dynamic behavior.
- Mobile header rebuilt as its own 3-column row: hamburger right, logo
centered, cart left. Fixed a missing justify-center that left the
hamburger icon (and its X state) off-center inside its square button.
- Header action buttons (cart, user/login) restyled to solid primary
pills/circles per a reference screenshot; a search button was tried and
then removed since the site has no search backend yet.
- Navbar reverted to exactly 5 static items (home, workshops, blog, about,
contact) per instruction — a categories-driven "محصولات" dropdown was
built and then removed the same session; /products, /store, /portfolios
remain real pages, just unlinked from the header for now.
- Hero: second avatar raised so it's not hidden behind the character.
WhyChooseUs: mirrored the two bottom floating badges into symmetric
corners (were overlapping). FeatureCards: restored the 4th feature
("انتخاب زمان کلاس") that had been silently dropped, gave the icon
badges a nested white-circle treatment, and gave the cards a visible
border.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
159 lines
33 KiB
Markdown
159 lines
33 KiB
Markdown
# Ali Alemi Website Context
|
||
|
||
> Keep this file current. Whenever a prompt changes scope, content, domain, or conventions, update the relevant section below in the same turn.
|
||
|
||
## Project
|
||
|
||
- Farsi RTL storefront for استاد علی عالمی (private math/calculus tutor for the Iranian کنکور).
|
||
- Next.js 16 App Router, React 19, TypeScript, Tailwind 4 (`@theme inline`, no `tailwind.config.js`), `src/app`.
|
||
- `lang="fa" dir="rtl"` set in `src/app/layout.tsx`.
|
||
- Apex domain: **`alialemi.com`** (`src/lib/site.ts` → `SITE_DOMAIN`) — confirmed by the user. `SITE_URL`, `CUSTOMER_URL` (`customer.alialemi.com`), `BUSINESS_URL` (`business.alialemi.com`), the sitemap-config fallback, and metadata all derive from it.
|
||
- The live Meshkee tenant for this domain does not exist yet on `api.meshkee.com` (every API call currently 404s) — every fetch on the site is wrapped in `safe()` (`src/lib/meshkee/client.ts`) so pages render a friendly empty state instead of crashing. This is expected and will resolve itself once the business is provisioned on the backend; no code change needed then.
|
||
- Carousels use **Swiper** (`swiper/react`, `swiper/modules`) per explicit instruction — see `src/components/home/Testimonials.tsx` and `src/components/home/FeatureCards.tsx`.
|
||
|
||
## Homepage layout fidelity
|
||
|
||
The user asked for the homepage's structure/layout to closely match the live original (`http://ali-alemi.local:8000`, a local Meshkee web-builder preview blocked by their VPN from this environment — they instead pasted screenshots directly). Corrections made after comparing against those screenshots:
|
||
|
||
- **Header**: solid `bg-secondary` (navy) bar, not white/blurred. On desktop (`lg:` and up) logo sits first in DOM (renders on the **right** in RTL — do not add `order-*` overrides here again, that was tried and was wrong), nav links, then phone/cart/login actions, matching the real desktop header exactly. **Below `lg:` the header is a completely separate 3-column grid row** (hamburger right / logo centered / cart left — Farsi readers expect the hamburger on the reading-start side, not the logo-adjacent corner) rendered via its own `grid grid-cols-3 lg:hidden` block, not responsive classes bolted onto the desktop flex row — the two arrangements are different enough (centered logo vs. edge logo, hamburger only exists on mobile) that trying to share one row fighting `order-*`/`justify-*` was worse than two small dedicated blocks. See `Header.tsx`.
|
||
- **Hero** (`HeroSection.tsx`): character + a large dotted ring backdrop (`public/images/hero/ring.png` — the accent dots are baked into that asset, don't draw separate circle divs), a second smaller student avatar peeking from behind the character, floating grad-cap/paper-plane/shape decorations. No CTA buttons in the hero — the original doesn't have any there. Two-column grid switches at **`md:`** (768px), not `lg:` — at `lg:` the in-between tablet/small-laptop range stacked to a single column and looked broken (large blank gap), since the image side has fixed-height containers.
|
||
- **Feature cards** (`FeatureCards.tsx`): a **Swiper carousel** (prev/next arrow buttons, not pagination dots) in a two-column row — cards on the left (wide column), a "ویژگیهای ما" heading + short paragraph + the arrow buttons in a narrow **280px right-hand column**. This grid must be `md:grid-cols-[280px_minmax(0,1fr)]` — **not** `[280px_1fr]`: a bare `1fr` track let Swiper's internal width calculation blow the column out to ~16.7M px (classic CSS Grid blowout; Swiper needs `minmax(0,1fr)` to be constrained by its parent). The Swiper's own grid-item wrapper also needs `min-w-0` directly (see the `Reveal` around it) — that's the part that matters **below** `md`, where there's no explicit `grid-template-columns` at all but the item is still a grid child with `min-width: auto` by default, so the same blowout happened on mobile too until that class was added. Any future Swiper placed inside a `grid` or `flex` container needs this same `min-w-0` treatment regardless of breakpoint. Also must NOT have negative top margin overlapping the hero — that was tried first and the user flagged it as a bug ("باید با هیرو فاصله داشته باشه"), plain `py-16 md:py-24` is correct.
|
||
- **Quick-links row** ("چرا استاد علی عالمی؟", `QuickLinks.tsx`): real icon assets (`icons/play.png`, `exam.png`, `price.png`, `support.png`) in `sm:grid-cols-2 lg:grid-cols-4` — 4-in-a-row on real desktop widths, 2×2 on tablet. Ends with a "درباره ما بیشتر بدانید" CTA linking `/about-us` (that's the original frame's own link field, not invented).
|
||
- **"چرا مارا انتخاب کنید؟"** (`WhyChooseUs.tsx`): image on the **left**, text+CTA on the **right** (DOM order text-first renders right in RTL grid). Stat badges (آموزش با کیفیت / نظارت دقیق و ارزیابی عملکرد / بیش از ۱۵۰ ساعت آموزش / ۱۴,۵۰۰ نفر دانشجو+avatars) are **floating cards scattered over the image's corners** (absolute-positioned with physical `left-*`/`right-*`, not logical `start`/`end` — this composition doesn't flip with page direction), not a plain bulleted list in the text column. The two **bottom** badges must sit in mirrored corners — `۱۴,۵۰۰ نفر دانشجو` at `-bottom-6 -left-6` and `بیش از ۱۵۰ ساعت آموزش` at `-bottom-6 -right-6` — an earlier `right-1/3` on the second one drifted under the first at narrower widths (user flagged it as an overlap bug from a screenshot); symmetric corners is the fix, don't reintroduce a fractional/centered offset on either bottom badge.
|
||
- **Testimonials**: multi-per-view on desktop (`breakpoints: {768:2, 1024:3}`), pagination dots (not arrows) — opposite of the feature carousel.
|
||
- **Stats bar** (`StatsBar.tsx`): light/white background with **teal** numbers — not a navy full-bleed banner (that was tried first and was wrong).
|
||
- Scroll-reveal entrance animation on every section via `src/components/Reveal.tsx` (fade + translateY on `IntersectionObserver`, stands in for the original's WOW.js + animate.css) — requested explicitly ("انیمیشنها ... باید دقیقا شبیه سایت اصلی باشه").
|
||
- **General lesson**: any two-column `image + text` grid using a fixed-height image container should switch at `md:` not `lg:`, or verify the `lg:` in-between range doesn't leave a tall single-column stack. Applies to `WhyChooseUs.tsx`, `about/BioIntro.tsx`, `contact/ContactInfo.tsx` (already fixed) — check any *new* two-column section against this before shipping.
|
||
- **Hero second avatar** (`avatar-1.png`, the girl peeking from behind the character): positioned `top-16 end-0` (was `bottom-6 end-2`, which put almost the whole thing behind the character's body with only a sliver of hair visible) — user asked to raise it so it actually reads as a second figure. If the character illustration or its box size changes, re-check this is still clearly visible, not just technically in the DOM.
|
||
- **Feature cards** (`FeatureCards.tsx`) use `border border-ink/15 shadow-md`, not `ring-1 ring-line` — the `--color-line` token (`#ececec`) was too light to read as a border at all against the white card/white section background (flagged from a screenshot as "بوردرهاشون مشخص نیست"). If another white-on-white card ever looks edge-less, this is the fix, not a heavier shadow alone.
|
||
|
||
## Header — logo/favicon, nav, categories (round 2 polish)
|
||
|
||
- **Dynamic logo**: `RootLayout` (`src/app/layout.tsx`) now fetches `getFavicon()` once and passes `logoUrl`/`logoDarkUrl` down to both `Header` and `Footer` (in addition to the favicon it already used for `<head>` icons). `Header` prefers `logoDarkUrl` (the dashboard's dark-background variant) → `logoUrl` → the bundled local `/images/logo.png`; the `brightness-0 invert` CSS filter is applied **only** when falling back to the local asset (it's the one known-teal file that needs forcing white for the navy bar — a real CMS-uploaded logo's color is unknown, don't invert it blind). `Footer` just uses `logoUrl` → local, no invert (white background, original color is correct there). Both use `unoptimized` on `next/image` when the src is a remote CMS URL, since that host isn't (and can't be, until a real tenant exists) in `next.config.ts` `images.remotePatterns`. The favicon itself was already dynamic before this round (`generateMetadata`) — this was specifically about the header/footer `<img>` logo not following the same rule yet.
|
||
- **Active nav link + phone button styling**: active link and the phone number both render as a solid white pill (`rounded-full bg-white px-4 py-2`, dark `text-secondary`) against the navy bar, not a transparent/tinted-text treatment — this was an explicit visual ask from a screenshot annotation ("دور لینک اکتیو ... یک مستطیل سفید با ردیوس ۵۰ بذار"). `rounded-full` (Tailwind's 9999px) reads as the same fully-pill shape as a literal 50px radius at this button height, so that's the correct utility, not an arbitrary `rounded-[50px]`. Inactive links stay `text-white/80` on transparent. Applies only to the desktop nav row; the mobile dropdown panel keeps its own simpler hover-background treatment.
|
||
- **Navbar reverted to exactly 5 static items** (`NAV_LINKS` in `src/lib/site.ts`): صفحه اصلی، دورههای آموزشی، مقالات، درباره ما، تماس با ما — per explicit instruction, **not** محصولات/فروشگاه/نمونهکارها. A categories-driven dropdown for "محصولات" (`src/components/CategoriesMenu.tsx`, fetched via `getCategories({ entityType: "product" })` in the root layout) was built and then **removed again** the same session once the user clarified the nav should stay to just those 5 until real CMS content exists — `CategoriesMenu.tsx` was deleted and the `categories` fetch/prop taken back out of `layout.tsx`/`Header.tsx`. `/products`, `/store`, `/portfolios` are still real, working pages — just not linked from the header nav right now. If nav-driven category browsing is wanted again later, it needs to be rebuilt (check git history for the deleted `CategoriesMenu.tsx` rather than starting from scratch).
|
||
- **Header action buttons** (search/cart/user, right side of the desktop row): redesigned to solid `bg-primary` filled circles/pills per a reference screenshot, not outlined/ghost buttons. **Search was added then removed the same session** — the site has no search backend, so don't re-add a search icon/box to the header without an actual `/products?name=` wiring decision first. `CartButton` (`src/components/cart/CartButton.tsx`) is now always a solid primary circle with a white-on-primary badge (no more `light` prop — it was only ever used inside this navy header, the conditional was dead weight). `UserMenu` (`src/components/UserMenu.tsx`) similarly dropped its `light` prop — both the logged-out "ورود / ثبتنام" pill and the logged-in "{name} عزیز" trigger are solid primary pills now.
|
||
- **Mobile hamburger button bug**: the button was `flex items-center` but **not** `justify-center`, so the 3-line icon span (and the X it morphs into when open) hugged one edge of the 40×40 square instead of sitting centered — reported from a screenshot ("۳ خط منو داخل مربع نیستن" / X not centered when open). Fix was adding `justify-center` alongside the existing `justify-self-start` (that one's unrelated — it positions the *button itself* within the mobile header's 3-column grid row; `justify-center` centers the icon *inside* the button). If another icon-only square button ever looks off-center, check for this exact same missing class rather than touching the icon's own SVG/span markup.
|
||
- **Feature carousel must have all 4 real items**: `src/components/home/features-data.ts` had been trimmed to 3 during the earlier "match the real 3-visible-at-once desktop screenshot" pass, silently dropping "انتخاب زمان کلاس" (choose class time) — the original theme has 4 features, the 4th just wasn't visible in that one screenshot because the carousel only shows 3 at a time on desktop. Restored as the 2nd item (clock icon, secondary tone) so the carousel actually has something to scroll to. Icon badges in `FeatureCards.tsx` also got a nested treatment — an outer `h-16 w-16 rounded-2xl` tone-colored square containing an inner `h-10 w-10 rounded-full bg-white` circle with the icon colored to match the outer tone — per a "flaticon-style" reference screenshot; a flat single-layer colored-square-with-white-icon read as low-effort by comparison. `about/FeatureStrip.tsx` reuses the same `FEATURES` array and benefits from the same fix (was showing an awkward 3-in-a-4-col grid before).
|
||
|
||
## Scope (current)
|
||
|
||
Static pages: `/` (home), `/about-us`, `/contact-us` — content from `old-site` (see "Source clone" below).
|
||
|
||
Live-API-backed catalog pages (built against `api.meshkee.com`, real tenant not provisioned yet so every list/detail page currently shows its empty state — see "Meshkee API" below):
|
||
|
||
| Module | List | Detail | Notes |
|
||
|---|---|---|---|
|
||
| Products | `/products` | `/products/{id}/{nameFaSlug}` | description / technical-data / comments tabs, "محصولات مشابه" row. No cart button (informational catalog). |
|
||
| Store items | `/store` | `/store/{id}/{nameFaSlug}` | same tabs **plus** add-to-cart with quantity. `{id}` is the **product id**, not a variant id — see "Store items architecture" below. |
|
||
| Product category | — | `/products/category/{id}/{nameFaSlug}` | lists products in that category. |
|
||
| Workshops | `/workshops` | `/workshops/{slug}` | no confirmed backend endpoint yet — see "Workshops" below. |
|
||
| Portfolios | `/portfolios` | `/portfolios/{id}/{titleFaSlug}` | lightbox gallery, "نمونه کارهای مشابه" row. |
|
||
| Blog | `/blog` | `/blog/{id}/{titleSlug}` | cover image, HTML content, comments. |
|
||
|
||
There is **no** `درمانگاران` page — it was in an earlier draft of the brief and the user corrected that mid-session.
|
||
|
||
## Source clone
|
||
|
||
- Original site: a Meshkee web-builder (Blade/PHP) export that was temporarily copied into `old-site/` to port content/assets from, then **deleted per explicit instruction** once the rebuild below was complete and verified against it — it no longer exists in this repo or on disk. Everything it contained is now captured either as real content in the pages/components listed below or as extracted files under `public/`; there is no separate archive to fall back on, so treat this file's notes as the record of what came from where. (Its original structure, for history: `old-site/custom/fa/pages/*/main.php` listed each page's frame stack; `old-site/custom/fa/frames/*.php` and `old-site/frames/**/content.blade.php` held the actual field data and markup per frame.)
|
||
- `old-site/settings.php` gave the brand palette (`primaryColor #008081`, `secondaryColor #324264`, `tertiaryColor #E7E7E7`) and default font (`iran-yekan`) — both carried over as Tailwind tokens (`src/app/globals.css`) and the self-hosted font (`src/app/fonts.ts`).
|
||
- **All page copy is the real content from `old-site`**, including the lorem-ipsum placeholder paragraphs that were literally in the source (per instruction: "all data is valid"). One exception: the About page FAQ (`old-site/custom/fa/frames/public-faq-yOxMt.php`) contained a broken, irrelevant template Q&A ("چگونه میتوانم محصول شما را با PayPal ادغام کنم؟" on a math-tutor site) — that was replaced with generic, non-fabricated tutoring FAQs (`src/components/about/Faq.tsx`) rather than keeping obviously mismatched boilerplate.
|
||
- **Real, non-lorem content that was found and is now used prominently:**
|
||
- Bio/credentials (`old-site` footer frame `footer-footer-hCR1y.php`, field `text`): born 1366/12/22, mechanical engineering (Iran University of Science & Technology), aerospace M.Sc. (same university, aerospace structures), rank 243 in the national Konkur, 13 years teaching math/calculus, worked with top Konkur academies. Used in the footer bio list **and** as the About page's `Resume` section (`src/components/about/Resume.tsx`) — this is the standout real content on the site, feature it, don't bury it.
|
||
- Contact info (`public-info_1-ZWXoP.php`): address "قم، میدان مفید، ساختمان پاسارگاد، طبقه سوم", phone `0912 607 3354`, Instagram `@Alialemiteacher`. Lives in `src/lib/site.ts` → `CONTACT`.
|
||
|
||
### Asset provenance (`public/images`, `public/fonts`, `public/brand`)
|
||
|
||
- `public/fonts/iranyekanweb{light,bold}fanum.woff` — copied from the sibling Meshkee project `../moallem/public/fonts` (same font, already cleared for use across the Meshkee site fleet). `cdn.meshkee.com` (the original font CDN) was unreachable from this environment (TLS handshake blocked) — if that changes and more weights are wanted, fetch `https://cdn.meshkee.com/web-builder/fonts/iran-yekan/iran-yekan.css`.
|
||
- `public/images/logo.png` ← `old-site/assets/files/E9jXiAxciH.png` (teal "ALI ALEMI" wordmark; footer's `mk7iCwJnYx.png` is byte-identical). There's no separate white variant, so `Header.tsx` applies `brightness-0 invert` to render it white on the navy bar; `Footer.tsx` uses the original teal color on its white background.
|
||
- `public/favicon.png` / `public/images/favicon-mark.png` ← `old-site/assets/files/9B5ryAVpXy.png` (the infinity mark from the logo).
|
||
- `public/images/hero/character.png` ← `XrfpuKYbWv.png` — the 3D professor illustration (used in the hero, `WhyChooseUs`, and About `BioIntro`).
|
||
- `public/images/hero/{ring,sketch,grad-cap,paper-plane,shape-1..5,avatar-1,avatar-2}.png` ← decorative floaters from the same slider frame (`RHLYjNnYj6`, `JtkpbKjrLA`, `8d3ZPngaUQ`, `jWgzrcRljG`, `QpKmZ80xX1`, `hiDCqXhOI6`, `f8rjoyAIi9`, `wYNzYMZHz2`, `H11Q6Ni7hV`, `QeqN7XLmJY`, `yVeuSypizt`).
|
||
- `public/images/icons/{play,exam,price,support}.png` ← `YYrcDvSrtC`, `JGmzMzVo2I`, `wEkbnOuKHg`, `7HZemBIE7S` — the real gradient icons for the "چرا استاد علی عالمی؟" quick-links row.
|
||
- `public/images/icons/quote.png` ← `j6FWn5p4aw.png`; `public/images/testimonial/avatar.png` ← `IRj6Zp6N2J.png` (same avatar the original reused across all three testimonial cards — that's the real data, not a shortcut).
|
||
- `public/images/contact-photo.jpg` ← `old-site/assets/img/contact/01.jpg` (generic stock photo bundled with the original theme).
|
||
- `public/brand/meshkee-logo-white.png` — downloaded directly from `https://safeteb.com/images/footer/MeshkeeLogo-White.png` (see Footer credit below).
|
||
- **Substituted with custom inline SVG icons (`src/components/icons.tsx`) instead of a missing asset:** the four "ویژگیهای ما" feature-card icons, the three small badge icons (pencil/search/check), contact icons (location/phone/instagram/whatsapp/telegram), and the About-page Resume icons. Their source files (e.g. `files/vLZSFuYnkv.png`, `files/otHFQ7PVUQ.jpg`, `files/IyvhvDu45j.jpg`, `files/TXzEEuVLAR.png`, `files/6aCyDwfHgF.png`, `files/6r1iyxcLfi.png`) were referenced in the Blade config but **not present** in `old-site/assets/files` (never actually uploaded on the live site) — rather than inventing stock photography, those slots became flat gradient backgrounds (`StatsBar`, `PageHeader`, `CustomerClubBanner` all use a teal→navy gradient + dotted pattern instead of a missing photo) or the custom icon set.
|
||
|
||
## Footer Meshkee credit
|
||
|
||
Matches `https://safeteb.com/` exactly, per instruction — markup, hover-frame animation, and CSS copied over in `src/components/Footer.tsx` / `src/app/globals.css` (`.meshkee-credit*`, `.footer-bottom*` classes). This is the same pattern already used on the sibling `khiyal-studio` project; keep the three in sync if the reference site's credit design ever changes.
|
||
|
||
## Meshkee API layer (`src/lib/meshkee/`)
|
||
|
||
- `client.ts` — `meshkeeFetch`/`tenantFetch` (adds `/tenants/{SITE_DOMAIN}` prefix) wrap `fetch` with Next.js data-cache `revalidate` (default 60s; auth/mutating calls pass `revalidate: false`). `safe(promise, fallback)` swallows any error/404 and returns the fallback — **every** list/detail fetch on a page goes through `safe()` so a missing tenant or disabled module renders an empty state, never a crash. `MeshkeeApiError` carries the HTTP status if a caller needs to branch on it (nothing does yet).
|
||
- `types.ts` — deliberately loose interfaces. **The public OpenAPI/Postman docs document endpoint shapes but not exact field names for `product`, `store-item`/variant, `category`, or `comment` objects** (confirmed by querying both docs directly this session). Fields like `nameFa`/`name`/`title`, `titleImageUrl`/`imageUrl`, `discountedPrice`/`price` are best-effort guesses at Meshkee's conventions (matching what other modules — blogs, portfolios, user-products — *do* document). **Once a real tenant exists, fetch one live response per entity and reconcile `types.ts` + the `pickText()`/`normalize*()` accessor chains in `format.ts` against the actual field names** — don't assume the current guesses are correct.
|
||
- `format.ts` — `pickText(...)` tries field-name candidates in order; `normalizeImage`/`normalizeGallery` handle both bare URL strings and `{url}` objects (unconfirmed which shape the API actually returns); `slugify()` builds canonical-URL slugs (Farsi letters/numbers kept, spaces → `-`, other punctuation stripped) — **always build links with `slugify(name)`, never a raw `item.slug` field** (the legacy DB slug must not appear in public URLs, per instruction); `formatToman`, `formatDate`.
|
||
- One file per module: `tenant.ts`, `products.ts`, `store-items.ts`, `portfolios.ts`, `blogs.ts`, `workshops.ts`, `categories.ts`, `comments.ts`, `contact.ts`.
|
||
|
||
## Canonical URLs
|
||
|
||
Per the current brief (this supersedes anything the earlier AI_PROMPT.md fetch said about slug-only blog/portfolio routes):
|
||
|
||
- Product: `/products/{id}/{nameFaSlug}` → resolve via `GET /tenants/{domain}/products/by-id/{id}`.
|
||
- Product category: `/products/category/{id}/{nameFaSlug}` → `GET /tenants/{domain}/categories/by-id/{id}`, then list products with `categoryId`.
|
||
- Blog: `/blog/{id}/{titleSlug}` → `GET /tenants/{domain}/blogs/by-id/{id}`.
|
||
- Portfolio: `/portfolios/{id}/{titleFaSlug}` → `GET /tenants/{domain}/portfolios/by-id/{id}`.
|
||
- Store item: `/store/{id}/{nameFaSlug}` — **not** an official Meshkee canonical path (none exists for store-items in the docs); chosen to match the product pattern. `{id}` is the product id (see "Store items architecture").
|
||
- Workshop: `/workshops/{slug}` — no `by-id` resolve exists yet (no endpoint at all — see "Workshops"); kept slug-only since there's nothing to redirect against.
|
||
|
||
Every `{id}/{slug}` detail page fetches by `id`, computes `slugify(name)`, and `redirect()`s to the canonical slug if the URL's slug segment doesn't match (same pattern on all four: products, store, portfolios, blog).
|
||
|
||
## Store items architecture (read before touching `/store`)
|
||
|
||
Meshkee's own data model treats `store-items` as **the sellable variant layer under a product**, not an independent content catalog — `GET /store-items/by-product/{productId}` is the only store-item lookup AI_PROMPT.md itself documents, specifically for the mini-cart flow. There is no dedicated "rich content" (gallery/description/technical-info) on a variant; that lives on the **product**.
|
||
|
||
So `/store/{id}/{slug}` resolves `{id}` as a **product id**: it fetches the product (`getProductById`, for gallery/description/technical-info) **and** `getStoreItemByProduct(id)` (for sellable variants/pricing) in parallel, then renders one page combining both. `/store` (the list) calls `GET /store-items` directly and reads `item.product` for display fields — **this nested `product` field on a store-item list response is unconfirmed** (see types.ts note above); verify against a live tenant.
|
||
|
||
## Workshops — no confirmed backend endpoint
|
||
|
||
Checked both `openapi.json` and the Postman collection directly this session: neither documents any workshops CRUD route — only a `/tenants/{domain}/sitemap-workshops.xml` placeholder exists (tag: SEO). `src/lib/meshkee/workshops.ts` still implements `getWorkshops`/`getWorkshopBySlug`/`getWorkshopById` following Meshkee's uniform module shape (same as products/portfolios/blogs) so it's a drop-in once the backend exposes it; until then every call 404s and `safe()` renders the "دورهای برای ثبتنام باز نیست" empty state — this is expected, not a bug.
|
||
|
||
**"Registration row"**: a workshop's `productId` (falls back to the workshop's own `id`) is looked up via `getStoreItemByProduct()`, and if that returns in-stock variants, the *same* `AddToCartBar` used on `/store` renders as the registration control ("ثبتنام" = adding the workshop's product variant to the cart, then continuing to the customer-dashboard checkout — there's no separate "enrollment" API). If no store item exists for it, a "برای ثبتنام تماس بگیرید" + phone-number fallback renders instead.
|
||
|
||
## Auth (customer session)
|
||
|
||
- `src/lib/auth.ts` — server-only (`next/headers` `cookies()`). Reads `meshkee_customer_access_token`, including the **chunked** form (`{name}_n` count + `{name}_0`, `{name}_1`, ...) per AI_PROMPT.md's cookie contract. `getSession()` calls `GET /auth/me` with that token and returns `{ user, token } | null`; called once in `src/app/layout.tsx` (root layout is now `async`) and passed down as props — **no client component calls `/auth/me` itself**.
|
||
- `isAdminUser()` is a **best-effort** check (`user.isAdmin || user.role === "admin" || user.role === "owner" || user.roles?.includes("admin") || user.businessRoles?.length`) — the real shape of the `user` object returned by `/auth/me` isn't documented publicly. Verify against a live logged-in session and trim to whichever field actually indicates admin/owner.
|
||
- `src/components/UserMenu.tsx`: logged-out → "ورود / ثبتنام" linking to `customerLoginUrl()`. Logged-in → `"{firstName} عزیز"` with a dropdown: `پروفایل من` (`customer.{domain}/profile`), `مدیریت وبسایت` (`business.{domain}`, **only when `isAdmin`**), `خروج` (`customer.{domain}/logout`). All three are plain cross-origin `<a>` tags (not `next/link`) — deliberately, they leave the site.
|
||
- No local login/register/OTP UI anywhere, per the hard rule — everything auth-related redirects to `customer.{domain}`.
|
||
|
||
## Cart (guest cart, local-only)
|
||
|
||
- `src/lib/cart.ts` implements AI_PROMPT.md's guest-cart spec **exactly**: `localStorage` key `meshkee-guest-cart`, mirrored to a cookie (`Domain=.{SITE_DOMAIN}`, `Path=/`, `SameSite=Lax`, ~30 days, skipped if the encoded JSON exceeds ~3500 chars), `encodeGuestCart()` is the identical base64url encoder the customer dashboard uses. `CartLine.id` is **always** a `storeItemVariantId` — never a product id or store-item id.
|
||
- `src/components/providers/CartProvider.tsx` (client context, wraps the app in `layout.tsx`) holds cart state; reads `localStorage` in a `useEffect` deferred via `queueMicrotask` (avoids both a hydration mismatch *and* the `react-hooks/set-state-in-effect` lint error — same pattern as the sibling `moallem` project's `CartProvider`, keep them consistent if either changes) and writes back to storage on every change.
|
||
- `src/components/cart/CartButton.tsx` (header icon + quantity badge) and `MiniCart.tsx` (slide-over popup: line items, qty stepper, remove, total, **Continue**) — `Continue` uses `continueToCheckoutUrl()`: if the access-token cookie is present it links to `customer.{domain}/checkout/cart?guestCart=...`, otherwise to `customer.{domain}/login?redirect=/checkout/cart?guestCart=...`. No server cart/checkout code exists on this site at all, per the hard rule.
|
||
- `AddToCartBar.tsx` (`src/components/catalog/`) is the shared add-to-cart control used on `/store/{id}/{slug}` and workshop registration: filters variants to in-stock (`stockQuantity === null` or `> 0`), shows a `<select>` only when more than one in-stock variant exists, quantity stepper, "این محصول در حال حاضر موجود نیست" when none are in stock.
|
||
|
||
## Comments
|
||
|
||
- `GET/POST /tenants/{domain}/comments?entityType=...&entityId=...` — the public docs list `product|blog|portfolio|video` as valid `entityType` values; `workshop` and `store_item` are passed the same way as a best-effort extension (**unconfirmed** — verify once live, `blog`/`portfolio`/`product` are the safe ones). Blog and portfolio also have entity-specific `/blogs/{id}/comments` routes but the generic one is used everywhere for one code path.
|
||
- **The API itself does not require auth to POST a comment** (no security scheme on that path in the OpenAPI doc) — but the user explicitly wants comments **gated to authenticated users in the UI**: `src/components/catalog/CommentsPanel.tsx` shows the comment form only when `user` is non-null (passed from the server page's `getSession()`), otherwise a "برای ثبت نظر ابتدا وارد حساب کاربری خود شوید" box linking to `customerLoginUrl(pathname)`. When posting, `authorName` is built from the session's `firstName`/`lastName` and the access token is sent as `Authorization: Bearer` too (harmless if the API ignores it, useful if it actually does attribute comments server-side).
|
||
|
||
## SEO / sitemap
|
||
|
||
- No `public/sitemap.xml`, `public/robots.txt`, `app/sitemap.ts`, or `app/robots.ts` — Meshkee nginx proxies `/sitemap.xml` (index) → `/sitemap-main.xml` + `/sitemap-products.xml` (+ other module sitemaps once enabled).
|
||
- `src/app/meshkee/sitemap-config.json/route.ts` scans `src/app/**/page.tsx` at request time (build-time-equivalent, since it's a dynamic route) and lists every **static** route, excluding dynamic `[...]` segments automatically — so `/products/category/[id]/[slug]` etc. never appear here; Meshkee's CMS fills those in from its own data per the canonical templates above. After deploy: business dashboard → Website → Settings → Sync sitemap config.
|
||
- Every page (static and dynamic) sets a unique `<title>`/description via `metadata`/`generateMetadata`, preferring `seoMetaTitle`/`seoMetaDescription` when the API provides them. Exactly one `<h1>` per page: `PageHeader` owns it; detail-page body sections that repeat the item name use `<h2>` (fixed a duplicate-H1 bug on the products/store/workshop detail pages this session — if you add a new detail page, don't reintroduce it). All content images carry a real `alt` (item name, never empty, decorative floaters use `aria-hidden` + empty alt instead).
|
||
- `src/app/meshkee/static-image-slots/route.ts` still returns `STATIC_IMAGE_SLOT_CATALOG` from `src/lib/meshkee.ts` (note: different file from the new `src/lib/meshkee/` **directory** — `src/lib/meshkee.ts` predates this session's API layer and only holds that one constant, currently **empty**). Populate it if/when a homepage image becomes dashboard-editable.
|
||
|
||
## Contact form
|
||
|
||
`src/components/contact/ContactForm.tsx` now POSTs to `POST /tenants/{domain}/contact-submissions` (`src/lib/meshkee/contact.ts`) with `{ title, name, cellNumber, text }` (the `title` field is a fixed "پیام از فرم تماس با ما" — the endpoint appears to want a subject line, unconfirmed). On success: a toast (`useToast()`), an inline success banner inside the form for ~4s, and the form resets so the visitor can send another message without leaving the page. On failure: an error toast, fields are preserved (no reset) so nothing is lost.
|
||
|
||
## Next steps
|
||
|
||
1. Once the tenant exists on the backend: fetch one real response per entity (product, store-item, category, comment, `/auth/me` user) and reconcile field names in `src/lib/meshkee/types.ts` + the `pickText()`/`normalize*()` chains — several are educated guesses (see "Meshkee API layer" above).
|
||
2. Confirm `entityType` values `workshop`/`store_item` actually work against `/comments`, or find/request the real ones.
|
||
3. Confirm whether a `/workshops` endpoint ever ships; if it lands under a different shape (e.g. folded into `instructions` or `videos` instead of its own module), update `src/lib/meshkee/workshops.ts` and the "registration row" fallback logic accordingly.
|
||
4. Resolve the tenant (`GET /tenants/{domain}`) for `businessId` and site-wide Organization JSON-LD (`business-info.schema.jsonLd`) in the root layout — not done yet, only favicon is fetched there.
|
||
5. Decide which homepage/about sections should become dashboard-editable and populate `STATIC_IMAGE_SLOT_CATALOG` (`src/lib/meshkee.ts`) accordingly.
|
||
|
||
## Verification
|
||
|
||
- `npm run lint` and `npm run build` (standalone output at `.next/standalone/server.js`).
|
||
- Visually check every page in desktop and mobile widths; RTL layout, Swiper carousels, mobile nav toggle, cart badge/popup, user menu (logged-out state at minimum — no test account exists yet), contact form success + reset.
|
||
- Every catalog list/detail page should render its **empty state**, not crash, against the current (unprovisioned) tenant — that's the expected state until the backend is live.
|
||
- `/meshkee/sitemap-config.json` should list all static routes (`/`, `/workshops`, `/products`, `/store`, `/portfolios`, `/blog`, `/about-us`, `/contact-us`) and none of the `[id]/[slug]` dynamic ones.
|
||
- `/meshkee/static-image-slots` should return `{ "slots": [] }` until slots are added.
|