feat: unify catalog and improve detail pages
This commit is contained in:
+59
-7
@@ -156,6 +156,11 @@ persistent memory as "for all the user's sites", not just this one)
|
||||
|
||||
- **Breadcrumb on every page except the homepage** — above the `<h1>`, via
|
||||
`src/components/ui/Breadcrumb.tsx`.
|
||||
- Detail pages use `src/components/ui/PageShell.tsx` so the max width, page
|
||||
padding, and breadcrumb position cannot drift between product and blog
|
||||
detail layouts. The blog detail page also renders category-matched related
|
||||
posts in the collapsible `SimilarArticles` sidebar; its DOM position follows
|
||||
the article so it naturally moves below the article body on smaller screens.
|
||||
- **Product detail layout**: 3-column grid (desktop) — gallery
|
||||
(`ProductGallery`, ~1/3, includes hover-zoom + click-to-open lightbox) →
|
||||
title + variation pickers (~5/12) → sticky buy-box (~1/4). One
|
||||
@@ -227,6 +232,44 @@ aspect ratio change during development, update the matching
|
||||
`STATIC_IMAGE_SLOT_CATALOG` entry (`aspectRatio` and `recommendedWidth`) in the
|
||||
same change so `/meshkee/static-image-slots` stays accurate for the dashboard.
|
||||
|
||||
## Store-item filters
|
||||
|
||||
`/store-items` has a desktop sidebar and a mobile filter drawer implemented by
|
||||
`components/product/ProductFilters.tsx`. Categories are rendered as a nested
|
||||
accordion (CMS parent categories with their children), not one long flat list.
|
||||
The three currently supported filters
|
||||
are category (`categoryId`), price range (`minPrice` / `maxPrice`), and in-stock
|
||||
only (`inStock=true`). They are submitted as URL search params and passed to
|
||||
`getStoreItems`, so filtering happens in the API before pagination. Pagination
|
||||
links must preserve every active filter. Categories come from `getCategoryTree`
|
||||
and therefore follow CMS data rather than a hardcoded list. On mobile the
|
||||
"فیلتر محصولات" button opens a right-side modal; its submit/reset actions stay
|
||||
fixed at the bottom while filter fields scroll. Desktop filters apply in real
|
||||
time: category and stock changes submit immediately, while price inputs use a
|
||||
short debounce; only the mobile drawer keeps an explicit apply button. Desktop
|
||||
submissions use `scroll={false}` and the client component must not be keyed by
|
||||
active filters, otherwise both the page and the category list jump back to the
|
||||
top. When a parent category or one of its children becomes active, its matching
|
||||
`details` element is opened imperatively without closing other accordions.
|
||||
|
||||
Do not simulate technical-property filters by fetching every product's
|
||||
`technical-info`: the public API currently exposes technical schema/values only
|
||||
per product, and `/store-items` does not accept technical field filters. When a
|
||||
faceted category-filter endpoint and technical-field query parameters are added
|
||||
to the API, render those returned fields as additional accordions in the same
|
||||
component.
|
||||
|
||||
## Product detail tabs and technical specifications
|
||||
|
||||
`ProductTabs` wraps the specifications, review, and comments UI in one white,
|
||||
rounded, bordered card. Technical specifications render as a responsive
|
||||
two-column table (field label and submitted value), not a loose card grid.
|
||||
`getProductTechnicalInfo()` must join `values[].fieldId` to
|
||||
`form.fields[].id`, resolve option labels from each field's `options`, and sort
|
||||
by the CMS field `sortOrder`; technical `values` do not reliably carry their
|
||||
own labels. If the form or submitted values are empty, keep the existing empty
|
||||
state rather than fabricating rows.
|
||||
|
||||
## Homepage product carousels — fully dynamic, don't hardcode a name/key
|
||||
|
||||
`GET /tenants/{domain}/store-specials` returns **every** admin-managed named
|
||||
@@ -282,14 +325,24 @@ styled like the site's other primary buttons (filled `bg-red`, white text,
|
||||
plain text link.
|
||||
|
||||
**"مشاهده همه" (view all)**: each carousel's action button links to
|
||||
`/products?specialId={group.id}`. `store-specials` groups are admin-curated,
|
||||
`/store-items?specialId={group.id}`. `store-specials` groups are admin-curated,
|
||||
already-complete lists (the endpoint has no pagination) — there's no
|
||||
"more items than the carousel shows" to fetch, so `/products` with
|
||||
"more items than the carousel shows" to fetch, so `/store-items` with
|
||||
`specialId` set just re-renders that exact group's `items` in the full grid
|
||||
instead of calling `getProducts()`. This is why the pattern is safe to keep
|
||||
beside the same filter UI used by the main catalog. This is why the pattern is safe to keep
|
||||
fully generic (any group id works, forever) rather than needing a
|
||||
per-carousel filter mapping — there isn't a filter to map to; it's the same
|
||||
list either way.
|
||||
list either way. The header's "فروش ویژه" link is `/store-items?special=sale`, and
|
||||
the store-items page resolves the real CMS group at request time with the shared
|
||||
`isSpecialSaleGroup()` matcher (`فروش ویژه` or `special-sale`) before rendering
|
||||
that group's exact items. Do not hardcode a group id in the header.
|
||||
|
||||
`/store-items` is the one canonical catalog route. `/products` permanently
|
||||
redirects there while preserving all query parameters. Product detail URLs are
|
||||
also canonical under `/store-items/{variant-id|product-slug|product-id}`; the
|
||||
single detail page resolves all three forms and keeps an exact variant selected
|
||||
when a variant id was supplied. Legacy `/products/[slug]` and
|
||||
`/products/id/[id]` routes only redirect for backwards compatibility.
|
||||
|
||||
## Branding: logo & favicon are dynamic, not static files
|
||||
|
||||
@@ -346,9 +399,8 @@ un-draw is the distinctive part, not just "a border that appears."
|
||||
| Route | Notes |
|
||||
|---|---|
|
||||
| `/` | Homepage — all the carousels/banners |
|
||||
| `/products`, `/products/[slug]` | Catalog browsing, category filter via `?categoryId=`, one store-specials group via `?specialId=` (see above) |
|
||||
| `/products/id/[id]` | Redirect-only resolver, see "Group-source id vs productId" above |
|
||||
| `/store-items`, `/store-items/[id]` | Variant-level browsing/detail, see above |
|
||||
| `/store-items`, `/store-items/[id]` | Canonical catalog and product detail; filters include category, price and stock; curated groups use `?specialId=` |
|
||||
| `/products`, `/products/[slug]`, `/products/id/[id]` | Backwards-compatible redirects to the canonical `/store-items` routes |
|
||||
| `/blog`, `/blog/[slug]` | Blog listing/detail |
|
||||
| `/contact` | Contact form → `POST /contact-submissions`, toast + reset on success |
|
||||
|
||||
|
||||
Reference in New Issue
Block a user