From 8f102a825023e91a89de890ddd1436354a316ba9 Mon Sep 17 00:00:00 2001 From: "amirhosein.ashourloo" Date: Wed, 16 Sep 2026 09:43:10 +0330 Subject: [PATCH] Initial commit: Ali Alemi storefront (Next.js, Meshkee API integration) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Farsi RTL Next.js 16 storefront for استاد علی عالمی, cloned from the original Meshkee web-builder site and rebuilt with Tailwind 4, live Meshkee API integration (products, store items, portfolios, blog, workshops, comments, cart, customer auth), and standalone production output for the shared-VM deploy. Co-Authored-By: Claude Sonnet 5 --- .claude/launch.json | 11 + .gitignore | 34 + AGENTS.md | 9 + CLAUDE.md | 2 + CONTEXT.md | 147 + README.md | 19 + eslint.config.mjs | 18 + next.config.ts | 8 + package-lock.json | 6802 +++++++++++++++++ package.json | 27 + postcss.config.mjs | 7 + public/brand/meshkee-logo-white.png | Bin 0 -> 33035 bytes public/favicon.png | Bin 0 -> 3276 bytes public/fonts/iranyekanwebboldfanum.woff | Bin 0 -> 33581 bytes public/fonts/iranyekanweblightfanum.woff | Bin 0 -> 33941 bytes public/images/contact-photo.jpg | Bin 0 -> 41497 bytes public/images/favicon-mark.png | Bin 0 -> 3276 bytes public/images/hero/avatar-1.png | Bin 0 -> 48269 bytes public/images/hero/avatar-2.png | Bin 0 -> 14577 bytes public/images/hero/character.png | Bin 0 -> 216508 bytes public/images/hero/grad-cap.png | Bin 0 -> 36160 bytes public/images/hero/paper-plane.png | Bin 0 -> 5627 bytes public/images/hero/ring.png | Bin 0 -> 17983 bytes public/images/hero/shape-1.png | Bin 0 -> 3062 bytes public/images/hero/shape-2.png | Bin 0 -> 3493 bytes public/images/hero/shape-3.png | Bin 0 -> 5141 bytes public/images/hero/shape-4.png | Bin 0 -> 3493 bytes public/images/hero/shape-5.png | Bin 0 -> 2666 bytes public/images/hero/sketch.png | Bin 0 -> 3202 bytes public/images/icons/exam.png | Bin 0 -> 9405 bytes public/images/icons/play.png | Bin 0 -> 8029 bytes public/images/icons/price.png | Bin 0 -> 8071 bytes public/images/icons/quote.png | Bin 0 -> 2998 bytes public/images/icons/support.png | Bin 0 -> 9738 bytes public/images/logo.png | Bin 0 -> 11226 bytes public/images/testimonial/avatar.png | Bin 0 -> 10254 bytes src/app/about-us/page.tsx | 33 + src/app/blog/[id]/[slug]/page.tsx | 83 + src/app/blog/page.tsx | 48 + src/app/contact-us/page.tsx | 22 + src/app/fonts.ts | 22 + src/app/globals.css | 285 + src/app/layout.tsx | 67 + src/app/meshkee/sitemap-config.json/route.ts | 94 + src/app/meshkee/static-image-slots/route.ts | 13 + src/app/not-found.tsx | 23 + src/app/page.tsx | 26 + src/app/portfolios/[id]/[slug]/page.tsx | 123 + src/app/portfolios/page.tsx | 47 + src/app/products/[id]/[slug]/page.tsx | 131 + .../products/category/[id]/[slug]/page.tsx | 75 + src/app/products/page.tsx | 50 + src/app/store/[id]/[slug]/page.tsx | 125 + src/app/store/page.tsx | 52 + src/app/workshops/[slug]/page.tsx | 121 + src/app/workshops/page.tsx | 46 + src/components/Footer.tsx | 106 + src/components/Header.tsx | 110 + src/components/PageHeader.tsx | 28 + src/components/Reveal.tsx | 44 + src/components/StatsBar.tsx | 61 + src/components/UserMenu.tsx | 59 + src/components/about/BioIntro.tsx | 64 + src/components/about/Faq.tsx | 55 + src/components/about/FeatureStrip.tsx | 29 + src/components/about/Resume.tsx | 43 + src/components/cart/CartButton.tsx | 26 + src/components/cart/MiniCart.tsx | 93 + src/components/catalog/AddToCartBar.tsx | 115 + src/components/catalog/CommentsPanel.tsx | 100 + src/components/catalog/EmptyState.tsx | 8 + src/components/catalog/EntityCard.tsx | 52 + src/components/catalog/Gallery.tsx | 127 + src/components/catalog/SimilarRow.tsx | 12 + src/components/catalog/Tabs.tsx | 28 + src/components/catalog/TechnicalDataTable.tsx | 49 + src/components/contact/ContactForm.tsx | 104 + src/components/contact/ContactInfo.tsx | 51 + src/components/contact/CustomerClubBanner.tsx | 28 + src/components/contact/MessageSection.tsx | 45 + src/components/home/FeatureCards.tsx | 76 + src/components/home/HeroSection.tsx | 95 + src/components/home/QuickLinks.tsx | 46 + src/components/home/Testimonials.tsx | 86 + src/components/home/WhyChooseUs.tsx | 78 + src/components/home/features-data.ts | 22 + src/components/icons.tsx | 312 + src/components/providers/CartProvider.tsx | 83 + src/components/providers/ToastProvider.tsx | 49 + src/lib/auth.ts | 60 + src/lib/cart.ts | 81 + src/lib/meshkee.ts | 13 + src/lib/meshkee/blogs.ts | 14 + src/lib/meshkee/categories.ts | 10 + src/lib/meshkee/client.ts | 70 + src/lib/meshkee/comments.ts | 29 + src/lib/meshkee/contact.ts | 17 + src/lib/meshkee/format.ts | 52 + src/lib/meshkee/portfolios.ts | 14 + src/lib/meshkee/products.ts | 18 + src/lib/meshkee/store-items.ts | 21 + src/lib/meshkee/tenant.ts | 10 + src/lib/meshkee/types.ts | 239 + src/lib/meshkee/workshops.ts | 20 + src/lib/site.ts | 49 + tsconfig.json | 34 + 106 files changed, 11605 insertions(+) create mode 100644 .claude/launch.json create mode 100644 .gitignore create mode 100644 AGENTS.md create mode 100644 CLAUDE.md create mode 100644 CONTEXT.md create mode 100644 README.md create mode 100644 eslint.config.mjs create mode 100644 next.config.ts create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 postcss.config.mjs create mode 100644 public/brand/meshkee-logo-white.png create mode 100644 public/favicon.png create mode 100644 public/fonts/iranyekanwebboldfanum.woff create mode 100644 public/fonts/iranyekanweblightfanum.woff create mode 100644 public/images/contact-photo.jpg create mode 100644 public/images/favicon-mark.png create mode 100644 public/images/hero/avatar-1.png create mode 100644 public/images/hero/avatar-2.png create mode 100644 public/images/hero/character.png create mode 100644 public/images/hero/grad-cap.png create mode 100644 public/images/hero/paper-plane.png create mode 100644 public/images/hero/ring.png create mode 100644 public/images/hero/shape-1.png create mode 100644 public/images/hero/shape-2.png create mode 100644 public/images/hero/shape-3.png create mode 100644 public/images/hero/shape-4.png create mode 100644 public/images/hero/shape-5.png create mode 100644 public/images/hero/sketch.png create mode 100644 public/images/icons/exam.png create mode 100644 public/images/icons/play.png create mode 100644 public/images/icons/price.png create mode 100644 public/images/icons/quote.png create mode 100644 public/images/icons/support.png create mode 100644 public/images/logo.png create mode 100644 public/images/testimonial/avatar.png create mode 100644 src/app/about-us/page.tsx create mode 100644 src/app/blog/[id]/[slug]/page.tsx create mode 100644 src/app/blog/page.tsx create mode 100644 src/app/contact-us/page.tsx create mode 100644 src/app/fonts.ts create mode 100644 src/app/globals.css create mode 100644 src/app/layout.tsx create mode 100644 src/app/meshkee/sitemap-config.json/route.ts create mode 100644 src/app/meshkee/static-image-slots/route.ts create mode 100644 src/app/not-found.tsx create mode 100644 src/app/page.tsx create mode 100644 src/app/portfolios/[id]/[slug]/page.tsx create mode 100644 src/app/portfolios/page.tsx create mode 100644 src/app/products/[id]/[slug]/page.tsx create mode 100644 src/app/products/category/[id]/[slug]/page.tsx create mode 100644 src/app/products/page.tsx create mode 100644 src/app/store/[id]/[slug]/page.tsx create mode 100644 src/app/store/page.tsx create mode 100644 src/app/workshops/[slug]/page.tsx create mode 100644 src/app/workshops/page.tsx create mode 100644 src/components/Footer.tsx create mode 100644 src/components/Header.tsx create mode 100644 src/components/PageHeader.tsx create mode 100644 src/components/Reveal.tsx create mode 100644 src/components/StatsBar.tsx create mode 100644 src/components/UserMenu.tsx create mode 100644 src/components/about/BioIntro.tsx create mode 100644 src/components/about/Faq.tsx create mode 100644 src/components/about/FeatureStrip.tsx create mode 100644 src/components/about/Resume.tsx create mode 100644 src/components/cart/CartButton.tsx create mode 100644 src/components/cart/MiniCart.tsx create mode 100644 src/components/catalog/AddToCartBar.tsx create mode 100644 src/components/catalog/CommentsPanel.tsx create mode 100644 src/components/catalog/EmptyState.tsx create mode 100644 src/components/catalog/EntityCard.tsx create mode 100644 src/components/catalog/Gallery.tsx create mode 100644 src/components/catalog/SimilarRow.tsx create mode 100644 src/components/catalog/Tabs.tsx create mode 100644 src/components/catalog/TechnicalDataTable.tsx create mode 100644 src/components/contact/ContactForm.tsx create mode 100644 src/components/contact/ContactInfo.tsx create mode 100644 src/components/contact/CustomerClubBanner.tsx create mode 100644 src/components/contact/MessageSection.tsx create mode 100644 src/components/home/FeatureCards.tsx create mode 100644 src/components/home/HeroSection.tsx create mode 100644 src/components/home/QuickLinks.tsx create mode 100644 src/components/home/Testimonials.tsx create mode 100644 src/components/home/WhyChooseUs.tsx create mode 100644 src/components/home/features-data.ts create mode 100644 src/components/icons.tsx create mode 100644 src/components/providers/CartProvider.tsx create mode 100644 src/components/providers/ToastProvider.tsx create mode 100644 src/lib/auth.ts create mode 100644 src/lib/cart.ts create mode 100644 src/lib/meshkee.ts create mode 100644 src/lib/meshkee/blogs.ts create mode 100644 src/lib/meshkee/categories.ts create mode 100644 src/lib/meshkee/client.ts create mode 100644 src/lib/meshkee/comments.ts create mode 100644 src/lib/meshkee/contact.ts create mode 100644 src/lib/meshkee/format.ts create mode 100644 src/lib/meshkee/portfolios.ts create mode 100644 src/lib/meshkee/products.ts create mode 100644 src/lib/meshkee/store-items.ts create mode 100644 src/lib/meshkee/tenant.ts create mode 100644 src/lib/meshkee/types.ts create mode 100644 src/lib/meshkee/workshops.ts create mode 100644 src/lib/site.ts create mode 100644 tsconfig.json diff --git a/.claude/launch.json b/.claude/launch.json new file mode 100644 index 0000000..0319124 --- /dev/null +++ b/.claude/launch.json @@ -0,0 +1,11 @@ +{ + "version": "0.0.1", + "configurations": [ + { + "name": "ali-alemi", + "runtimeExecutable": "npm", + "runtimeArgs": ["run", "dev"], + "port": 3000 + } + ] +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d0eab47 --- /dev/null +++ b/.gitignore @@ -0,0 +1,34 @@ +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# env files +.env* +!.env.example + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..643577d --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,9 @@ + + +# This is NOT the Next.js you know + +This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices. + +This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean. + + diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..ea1f895 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,2 @@ +@AGENTS.md +@CONTEXT.md diff --git a/CONTEXT.md b/CONTEXT.md new file mode 100644 index 0000000..43ca9d4 --- /dev/null +++ b/CONTEXT.md @@ -0,0 +1,147 @@ +# 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. 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/login actions, matching the real desktop header exactly. +- **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. +- **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. + +## 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 `` 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 ` setSelectedId(e.target.value)} + className="w-full rounded-xl border border-line bg-white px-4 py-3 text-sm outline-none focus:border-primary" + > + {availableVariants.map((v) => ( + + ))} + + + )} + +
+
+ + {quantity.toLocaleString("fa-IR")} + +
+ + +
+ + ); +} diff --git a/src/components/catalog/CommentsPanel.tsx b/src/components/catalog/CommentsPanel.tsx new file mode 100644 index 0000000..6b95d93 --- /dev/null +++ b/src/components/catalog/CommentsPanel.tsx @@ -0,0 +1,100 @@ +"use client"; + +import { useState } from "react"; +import { usePathname } from "next/navigation"; +import { customerLoginUrl } from "@/lib/site"; +import { postComment, type EntityType } from "@/lib/meshkee/comments"; +import { formatDate } from "@/lib/meshkee/format"; +import type { Comment, CustomerUser } from "@/lib/meshkee/types"; +import { useToast } from "@/components/providers/ToastProvider"; + +export function CommentsPanel({ + entityType, + entityId, + initialComments, + user, + token, +}: { + entityType: EntityType; + entityId: string; + initialComments: Comment[]; + user: CustomerUser | null; + token: string | null; +}) { + const [comments, setComments] = useState(initialComments); + const [text, setText] = useState(""); + const [submitting, setSubmitting] = useState(false); + const pathname = usePathname(); + const { showToast } = useToast(); + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + if (!user || !text.trim()) return; + setSubmitting(true); + try { + const authorName = [user.firstName, user.lastName].filter(Boolean).join(" ") || "کاربر"; + const { comment } = await postComment(entityType, entityId, { authorName, text: text.trim() }, token); + setComments((prev) => [comment, ...prev]); + setText(""); + showToast("نظر شما ثبت شد و پس از تایید نمایش داده می‌شود."); + } catch { + showToast("ثبت نظر با خطا مواجه شد. دوباره تلاش کنید.", "error"); + } finally { + setSubmitting(false); + } + } + + return ( +
+ {user ? ( +
+ +