Document product categories and the new /shop filter sidebar

Products table gains the categories field; Search & filters section
covers the price slider, category checkboxes, availability toggle,
the blog's client-side category filter, and the RevealGroup key fix
both grids needed to stop going blank after a filter change.
This commit is contained in:
Marco
2026-08-01 08:37:52 +00:00
parent cc17311d03
commit 9a0729adef
+19 -3
View File
@@ -162,7 +162,7 @@ explains what does have access instead.
| Collection (slug) | Used for | Key fields |
|---|---|---|
| `products` | `/shop` grid, homepage spotlight, cart, checkout | `name`, `slug` (cart item id — **not** Payload's numeric id, so existing localStorage carts survive catalog changes), `description`, `price`, `compareAtPrice` (optional strikethrough), `image`, `detailHref`, `sortOrder`, `active` (hides a product from the shop grid/spotlight/related-products only — cart/checkout/its own detail page still resolve it regardless, see Discount codes section below for the same opt-in-filtering principle), `spotlight` + `spotlightEyebrow`/`spotlightHeadline`/`spotlightText`/`spotlightImage` (homepage "Neu im Shop" section — falls back to `image` if no dedicated spotlight image is set; forced onto the sole active product when exactly 1 exists, see `getSpotlightProduct()`), `taxRatePercent` (optional per-product VAT override, see "Product bundles & per-product tax rates" below), `bundleItems` (optional — makes this product a bundle), `sku` (optional, even without variants — variants carry their own; snapshotted onto each order item at checkout and shown as "Art.-Nr." on the invoice PDF, the order-confirmation email, and `/konto/bestellungen/[orderNumber]`) |
| `products` | `/shop` grid, homepage spotlight, cart, checkout | `name`, `slug` (cart item id — **not** Payload's numeric id, so existing localStorage carts survive catalog changes), `description`, `price`, `compareAtPrice` (optional strikethrough), `image`, `detailHref`, `sortOrder`, `active` (hides a product from the shop grid/spotlight/related-products only — cart/checkout/its own detail page still resolve it regardless, see Discount codes section below for the same opt-in-filtering principle), `categories` (optional, `hasMany` relation to the backend's `product-categories` collection — own collection, not shared with blog's `categories`, so a "Karten" product category and a same-named blog category never collide; not required, so an untagged product still shows everywhere, just matches every `/shop` category filter instead of being excluded — see `Product.categories`' own comment in `lib/payload.ts`), `spotlight` + `spotlightEyebrow`/`spotlightHeadline`/`spotlightText`/`spotlightImage` (homepage "Neu im Shop" section — falls back to `image` if no dedicated spotlight image is set; forced onto the sole active product when exactly 1 exists, see `getSpotlightProduct()`), `taxRatePercent` (optional per-product VAT override, see "Product bundles & per-product tax rates" below), `bundleItems` (optional — makes this product a bundle), `sku` (optional, even without variants — variants carry their own; snapshotted onto each order item at checkout and shown as "Art.-Nr." on the invoice PDF, the order-confirmation email, and `/konto/bestellungen/[orderNumber]`) |
| `discount-codes` | Cart discount input (`/cart`, display-only on `/checkout`) | `code`, `type` (`percent`/`fixed`), `value`, `validFrom`/`validUntil`, `minOrderValue`, `maxRedemptions`, `redemptionCount` (server-incremented only), `active`. **Not public-read** — see Discount codes section below |
| `posts` | `/blog`, `/blog/[slug]` | `title`, `slug`, `categories` (`hasMany` relation to `categories` — a post can have several, joined with `", "` wherever rendered), `excerpt`, `thumbnail`, `content` (richText, supports custom Lexical blocks — Bild/Bildergalerie/Video/Zitat — rendered via `app/components/RichText.tsx`'s `@payloadcms/richtext-lexical/react` renderer with custom `JSXConverters`), `readTime` (auto-calculated on save from word count), `featured` (shown as the `/blog` hero post; most-recently-published wins if several are marked), `publishedAt`, `quoteLabel` (label + icon + underline shown next to every blockquote in `content`, default `"Merke dir:"` — leave empty to hide that framing, the blockquote text itself still renders), `relatedProduct` (optional relation to `products`, powers the "Passend dazu" card at the end of the post — leave empty to hide that card, or empty if the linked product has no `detailHref`), `seoTitle`/`seoDescription`/`seoImage` (each falls back to `title`/`excerpt`/`thumbnail` when empty, consumed by `/blog/[slug]`'s `generateMetadata()`) |
| `categories` | Blog post categorization | `name`, `slug` (unique per tenant, not globally) |
@@ -1401,8 +1401,24 @@ treatment as the wishlist icon).
**Filters** are URL-search-param-driven everywhere (shareable/bookmarkable,
no client-side-only state): `/konto/bestellungen`'s status/paymentStatus/year
dropdowns (`OrderFilters.tsx`, `CustomSelect.tsx`); `/blog`'s category
toggle chips (`buildCategoryHref()`, plain server-rendered `<Link>`s);
`/shop`'s price min/max range (`PriceRangeFilter.tsx`).
toggle chips (`BlogCategoryFilter.tsx` — a Client Component using
`useRouter()` + `useTransition()`, chips disabled while a navigation is
pending so rapid clicks can't fire overlapping RSC navigations that
commit out of order); `/shop`'s left sidebar (`lg:+`) / filter bar
(`<lg`, both rendered by `ProductGrid.tsx`) — a dual-handle price range
slider (`PriceRangeFilter.tsx`, drag-to-filter, commits on release not
per drag-frame), category checkboxes (`CategoryFilter.tsx`, union match
— a product with no category tagged still matches every filter rather
than disappearing, see `Product.categories`' own comment in
`lib/payload.ts`), and a "Nur verfügbare Produkte" availability toggle
(same component). Sidebar styled to match `AccountNav.tsx`'s label/
spacing treatment. Both `/blog` and `/shop`'s grids key their
`RevealGroup` on the actually-rendered item set — `whileInView` only
fires once per component instance, and a filter change re-renders the
same page in place rather than remounting it, so without the key a
freshly filtered list could mount into an already-settled `RevealGroup`
and stay stuck at `opacity: 0` (reported as the grid "going blank"
after filtering).
**`CustomSelect.tsx`** (`app/components/`) is a fully custom-styled
dropdown (own trigger + `role="listbox"` panel, keyboard nav,
click-outside-to-close) used wherever a native `<select>`'s un-stylable