commit cfabf3e3273a3e7df9dab824801da0a4aa11164b Author: Alireza Hassani Date: Fri Sep 4 15:00:41 2026 +0330 Initial commit: Hoomat storefront with Meshkee API and static-image slots. Co-authored-by: Cursor diff --git a/.cursor/rules/static-image-slots.mdc b/.cursor/rules/static-image-slots.mdc new file mode 100644 index 0000000..c787775 --- /dev/null +++ b/.cursor/rules/static-image-slots.mdc @@ -0,0 +1,14 @@ +--- +description: Keep Meshkee static-image slot catalog aspect ratios in sync with UI +globs: lib/meshkee/static-slots.ts,app/meshkee/static-image-slots/**,app/page.tsx,app/about-us/**,app/components/hero-slider.tsx,app/components/slot-media.tsx,app/components/product-carousel.tsx +alwaysApply: false +--- + +# Static image slots + +When homepage/about (or other) UI image sizes or aspect ratios change during development, update `STATIC_IMAGE_SLOT_CATALOG` in `lib/meshkee/static-slots.ts` (and thus `GET /meshkee/static-image-slots`) so Meshkee’s static-image reader stays accurate. + +- Prefer API `slot.aspectRatio` / catalog values; do not invent upload or CMS write APIs. +- Empty or failed slot fetches must keep local placeholders. +- List slots use all `slot.images`; single slots use the first image with a `url`. +- Wrap with `` when `linkUrl` is present; map `titleFa` / `titleEn` / `subtext` to the existing UI copy fields. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0f8e1a7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +.next +node_modules +out +dist +.env* +*.log +coverage +.DS_Store 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..43c994c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/CONTEXT.md b/CONTEXT.md new file mode 100644 index 0000000..2fcc31f --- /dev/null +++ b/CONTEXT.md @@ -0,0 +1,109 @@ +# Hoomat Store Context + +## Current Prompt + +Source prompt: `https://api.meshkee.com/api/v1/public/ai-prompts/hoomat-store/images-placeholder/28488577c9163eaa8fdb8e86` + +Goal: wire homepage/about static-image slots from Meshkee (`GET /tenants/hoomatstore.com/website/static-images`) and publish the slot catalog at `/meshkee/static-image-slots`. Empty/fail → keep local placeholders. + +Previous prompts: +- Backend: `…/backend/114f70b804d97953f91ad26a` (API auth/cart/store/blog/contact) +- Basic create: `…/basic-create/d3ca3d1229f0e5c8e532b073` (Persian RTL flat-design clone) + +## Meshkee Backend + +- Docs: `https://api.meshkee.com/docs/website` +- OpenAPI: `https://api.meshkee.com/docs/website/openapi.json` +- API base: `https://api.meshkee.com/api/v1` +- Apex domain: `hoomatstore.com` (business id currently `35`) +- Customer app: `https://customer.hoomatstore.com` +- Business admin: `https://business.hoomatstore.com` +- Tenant resolve: `GET /tenants/hoomatstore.com` +- Public content: `/tenants/hoomatstore.com/...` +- Cart/orders: `/businesses/{businessId}/...` with Bearer JWT +- Auth token keys (localStorage): `meshkee_customer_access_token`, `meshkee_customer_refresh_token` +- Guest cart key: `meshkee-guest-cart` +- Cart continue: authenticated users go to `customer.hoomatstore.com/checkout/cart`; guests go to customer login with `redirect=/checkout/cart` and optional `#guestCart=` payload (Meshkee customer dashboard handoff) + +## Static image slots + +Catalog source: `lib/meshkee/static-slots.ts` → `GET /meshkee/static-image-slots` + +| key | kind | pageKey | count | UI | +|-----|------|---------|-------|----| +| `slider` | list | home | n | homepage hero | +| `categories-icon` | list | home | 6 | quick category icons | +| `special-sale-banner` | single | home | 1 | sale promo beside product carousel | +| `product-features-image` | single | home | 1 | features sofa | +| `contactus-image` | single | home | 1 | homepage contact guarantee panel | +| `two-banner-row` | list | home | 2 | dual promo banners | +| `aboutus-image` | single | home | 1 | homepage about panel | +| `three-banner-row` | list | home | 3 | triple promo banners | +| `about-us-image` | list | about | 2 | about page panels | + +Rules: use `slot.images` (all if list, first if single); `url` → img/bg; `linkUrl` → wrap link; map `titleFa` / `titleEn` / `subtext` to UI; if UI aspect/size changes, update the catalog. Cursor rule: `.cursor/rules/static-image-slots.mdc`. + +## Source Website Data + +- Main site: `https://www.hoomatstore.com/` +- About page: `https://www.hoomatstore.com/about-us` +- Contact page: `https://www.hoomatstore.com/contactus` +- Footer inspiration for bottom Meshkee row: `https://safeteb.com/` + +## Design Requirements + +- Language: Persian (`fa`) +- Direction: RTL +- Framework: Next.js app router +- Style: flat design, furniture-store feel, responsive layout +- Header: fixed with blurry/translucent background; stronger glass after scroll +- Header now includes user menu, cart icon/badge + drawer, and products mega menu (1st + 2nd category levels) +- Palette: + - Primary: `#00234B` + - Secondary: `#CC9966` + - Tertiary: `#883F3F` +- Fonts: Iran Sans (FA), Josefin Sans (EN titles), Montserrat fallback +- Logo local fallback: `public/assets/hoomat-logo.png` +- Favicon: from `GET /tenants/{domain}/website/favicon` when available; otherwise local logo + +## Implemented Routes + +- `/` homepage (static-image slots + local fallbacks; product carousels still sample) +- `/about-us` (about-us-image ×2 + local fallbacks) +- `/contactus` (contact form posts to Meshkee contact-submissions) +- `/store` store-items listing +- `/store/[variantId]/[[...slug]]` store item detail +- `/blog` blog listing +- `/blog/[id]/[slug]` blog detail +- `/meshkee/static-image-slots` slot catalog JSON for Meshkee dashboard + +## Auth / Cart UX + +- Logged out user icon → customer login +- Logged in: `{firstName} عزیز` with dropdown: پروفایل من, خروج, and مدیریت وبسایت (admins only, handoff to business app) +- Cart badge shows total quantity; drawer continue follows Meshkee customer checkout flow above + +## Key Code Locations + +- API client: `lib/meshkee/api.ts` (`getStaticImages`, `getStaticImageSlot`) +- Slot catalog/helpers: `lib/meshkee/static-slots.ts` +- Slot UI helpers: `app/components/slot-media.tsx` +- Config/domain: `lib/meshkee/config.ts` +- Auth/cart/toast providers: `app/providers/*` +- Header + cart drawer + mega menu: `app/components/header.tsx` and related components +- Shared static homepage content still in `lib/site-data.ts` +- Global styles: `app/globals.css` + +## Data Notes + +- Store-items for this tenant may be empty until inventory is published; blogs/categories are live. +- Static-images for this tenant may be empty until CMS slots are filled; UI keeps local placeholders. +- Product description/gallery/technical/comments for a store variant come from the linked catalog product (`productId`). +- Homepage carousels still use local sample products until a later prompt wires them to API. + +## Maintenance Notes + +- Update this file on every Meshkee prompt. +- Prefer Meshkee Website API docs over inventing CMS/admin endpoints. +- Keep RTL/FA patterns and existing visual language when adding pages. +- When UI image size/aspect changes, update `STATIC_IMAGE_SLOT_CATALOG` so `/meshkee/static-image-slots` stays correct. diff --git a/app/about-us/page.tsx b/app/about-us/page.tsx new file mode 100644 index 0000000..531c85a --- /dev/null +++ b/app/about-us/page.tsx @@ -0,0 +1,107 @@ +import Link from "next/link"; +import { SlotLinkedImage } from "@/app/components/slot-media"; +import { getStaticImageSlot, getStaticImages } from "@/lib/meshkee/api"; +import { slotImages } from "@/lib/meshkee/static-slots"; +import type { StaticImageItem, StaticImageSlot } from "@/lib/meshkee/types"; +import { aboutCopy, features, site } from "@/lib/site-data"; + +export const metadata = { + title: "درباره ما | داستان فروشگاه اینترنتی مبلمان هومت", + description: "با داستان، دیدگاه و خدمات مبلمان هومت بیشتر آشنا شوید.", +}; + +async function loadAboutImages(): Promise { + const aboutSlots = await getStaticImages("about").catch( + () => [] as StaticImageSlot[], + ); + const fromPage = slotImages(aboutSlots, "about-us-image"); + if (fromPage.length) return fromPage; + + const slot = await getStaticImageSlot("about-us-image"); + return slot ? slotImages([slot], "about-us-image") : []; +} + +export default async function AboutPage() { + const aboutImages = await loadAboutImages(); + const primaryImage = aboutImages[0]; + const secondaryImage = aboutImages[1]; + + return ( +
+
+

{aboutCopy.eyebrow}

+

{aboutCopy.title}

+

{aboutCopy.quote}

+
+ +
+
+

Story

+

{aboutCopy.storyTitle}

+

{aboutCopy.story}

+

+ هومت با تکیه بر تولید مستقیم، تلاش می‌کند تجربه خرید مبلمان را برای + خانواده‌ها ساده‌تر کند؛ از انتخاب محصول تا مشاوره، سفارش و تحویل. +

+
+ +
+

Vision

+

{aboutCopy.visionTitle}

+

{aboutCopy.vision}

+
    + {features.map((feature) => ( +
  • + {feature.title} - {feature.subtitle} +
  • + ))} +
+ + تماس با هومت + +
+
+ +
+
+ +
+

ONLINE FURNITURE SHOP

+

فروشگاه آنلاین خرید مبلمان منزل با تنوع بالا

+

+ مجموعه هومت محصولات اصلی مورد نیاز دکوراسیون منزل را پوشش می‌دهد: + مبلمان راحتی، کلاسیک، مینیمال، مبل تخت‌خواب‌شو، میز ناهارخوری، + صندلی، سرویس خواب و میزهای کاربردی. +

+
+
+
+ +
+
+ +
+

SHOWROOM

+

از انتخاب مدل تا چیدمان نهایی

+

+ در نمایشگاه و کارخانه هومت می‌توانید پارچه، رنگ و مدل را از نزدیک + بررسی کنید و با راهنمایی مشاوران، ترکیبی هماهنگ برای منزل خود بسازید. +

+
+
+
+
+ ); +} diff --git a/app/blog/[id]/[slug]/page.tsx b/app/blog/[id]/[slug]/page.tsx new file mode 100644 index 0000000..9789977 --- /dev/null +++ b/app/blog/[id]/[slug]/page.tsx @@ -0,0 +1,67 @@ +import type { Metadata } from "next"; +import Image from "next/image"; +import { notFound } from "next/navigation"; +import { getBlogById } from "@/lib/meshkee/api"; +import { formatDate } from "@/lib/meshkee/format"; + +type PageProps = { + params: Promise<{ id: string; slug: string }>; +}; + +export async function generateMetadata({ params }: PageProps): Promise { + const { id } = await params; + try { + const post = await getBlogById(id); + return { + title: `${post.title} | مبلمان هومت`, + description: post.abstract || post.title, + }; + } catch { + return { title: "مقاله | مبلمان هومت" }; + } +} + +export default async function BlogDetailPage({ params }: PageProps) { + const { id } = await params; + let post; + try { + post = await getBlogById(id); + } catch { + notFound(); + } + + return ( +
+
+
+

{post.categoryName || "Article"}

+

{post.title}

+ +
+ + {post.titleImageUrl ? ( +
+ {post.title} +
+ ) : null} + + {post.abstract ?

{post.abstract}

: null} + + {post.mainTextHtml ? ( +
+ ) : ( +

متن مقاله در دسترس نیست.

+ )} +
+
+ ); +} diff --git a/app/blog/page.tsx b/app/blog/page.tsx new file mode 100644 index 0000000..b9bfc10 --- /dev/null +++ b/app/blog/page.tsx @@ -0,0 +1,74 @@ +import Image from "next/image"; +import Link from "next/link"; +import { listBlogs } from "@/lib/meshkee/api"; +import { formatDate } from "@/lib/meshkee/format"; +import { blogHref } from "@/lib/meshkee/urls"; + +type SearchParams = Promise<{ page?: string }>; + +export const metadata = { + title: "مجله دکوراسیون | مبلمان هومت", + description: "مقالات و راهنمای خرید مبلمان و دکوراسیون از مبلمان هومت", +}; + +export default async function BlogPage({ + searchParams, +}: { + searchParams: SearchParams; +}) { + const params = await searchParams; + const page = Math.max(1, Number(params.page) || 1); + const data = await listBlogs({ page, pageSize: 12 }); + + return ( +
+
+

Blog

+

مجله دکوراســـــــــیون

+

راهنماها و مقالات تخصصی برای انتخاب بهتر مبلمان و چیدمان خانه.

+
+ +
+ {data.items.length === 0 ? ( +

مقاله‌ای برای نمایش وجود ندارد.

+ ) : ( +
+ {data.items.map((post) => ( + +
+ {post.titleImageUrl ? ( + {post.title} + ) : ( +
+ )} +
+
+ +

{post.title}

+ {post.abstract ?

{post.abstract}

: null} +
+ + ))} +
+ )} + + {data.total > data.pageSize ? ( +
+ {page > 1 ? قبلی : null} + + {page} + + {page * data.pageSize < data.total ? ( + بعدی + ) : null} +
+ ) : null} +
+
+ ); +} diff --git a/app/components/article-carousel.tsx b/app/components/article-carousel.tsx new file mode 100644 index 0000000..dcb2969 --- /dev/null +++ b/app/components/article-carousel.tsx @@ -0,0 +1,79 @@ +"use client"; + +import Image from "next/image"; +import Link from "next/link"; +import { useRef } from "react"; + +type Article = { + title: string; + date: string; + image: string; + href?: string; +}; + +export function ArticleCarousel({ + articles, + ariaLabel, +}: { + articles: Article[]; + ariaLabel: string; +}) { + const carouselRef = useRef(null); + + const scrollCarousel = (direction: "next" | "previous") => { + const amount = carouselRef.current + ? carouselRef.current.clientWidth * 0.7 + : 320; + carouselRef.current?.scrollBy({ + left: direction === "next" ? -amount : amount, + behavior: "smooth", + }); + }; + + return ( +
+ +
+ {articles.map((article) => ( + +
+ {article.title} +
+
+ +

{article.title}

+

ادامه

+
+ + ))} +
+ +
+ ); +} diff --git a/app/components/cart-drawer.tsx b/app/components/cart-drawer.tsx new file mode 100644 index 0000000..b43ee7d --- /dev/null +++ b/app/components/cart-drawer.tsx @@ -0,0 +1,112 @@ +"use client"; + +import Image from "next/image"; +import { useCart } from "@/app/providers/cart-provider"; +import { useAuth } from "@/app/providers/auth-provider"; +import { formatPrice } from "@/lib/meshkee/format"; +import { customerContinueCheckoutUrl } from "@/lib/meshkee/urls"; + +export function CartDrawer() { + const { cart, open, setOpen, updateQuantity, removeItem, guestItems, loading } = + useCart(); + const { isAuthenticated } = useAuth(); + + if (!open) return null; + + const continueUrl = customerContinueCheckoutUrl( + isAuthenticated, + isAuthenticated ? [] : guestItems, + ); + + return ( +
+ +
+ +
+ {cart.items.length === 0 ? ( +

هنوز محصولی به سبد اضافه نشده است.

+ ) : ( + cart.items.map((item) => ( +
+
+ {item.image ? ( + {item.name} + ) : ( +
+ )} +
+
+

{item.name}

+ {item.optionLabel ?

{item.optionLabel}

: null} + + {formatPrice(item.price)} + +
+ + + {item.quantity} + + + +
+
+
+ )) + )} +
+ +
+
+ جمع + + {formatPrice(cart.total)} + +
+
+ ادامه + +
+ + + ); +} diff --git a/app/components/contact-form.tsx b/app/components/contact-form.tsx new file mode 100644 index 0000000..54655ac --- /dev/null +++ b/app/components/contact-form.tsx @@ -0,0 +1,69 @@ +"use client"; + +import { useState } from "react"; +import { submitContact } from "@/lib/meshkee/api"; +import { useToast } from "@/app/providers/toast-provider"; + +export function ContactForm() { + const { showToast } = useToast(); + const [name, setName] = useState(""); + const [cellNumber, setCellNumber] = useState(""); + const [text, setText] = useState(""); + const [successText, setSuccessText] = useState(null); + const [submitting, setSubmitting] = useState(false); + + async function onSubmit(event: React.FormEvent) { + event.preventDefault(); + setSubmitting(true); + setSuccessText(null); + try { + await submitContact({ + title: "تماس از وبسایت هومت", + name: name.trim(), + cellNumber: cellNumber.trim() || undefined, + text: text.trim(), + }); + setName(""); + setCellNumber(""); + setText(""); + setSuccessText("پیام شما با موفقیت ارسال شد. به زودی با شما تماس می‌گیریم."); + showToast("پیام با موفقیت ارسال شد"); + } catch (error) { + showToast(error instanceof Error ? error.message : "ارسال پیام ناموفق بود"); + } finally { + setSubmitting(false); + } + } + + return ( +
void onSubmit(event)}> + setName(event.target.value)} + /> + setCellNumber(event.target.value)} + /> +