Add active-product-count-driven automation
- Products gain `active`/`spotlight*`/`updatedAt` on the base Product type (folded in from the now-removed separate SpotlightProduct type) so shop grid, spotlight, and related-products can each filter `.active` from the same already-fetched list — cart/checkout/order-confirmation/product- detail pages keep resolving any product regardless of active status. - getSpotlightProduct() now derives from getProducts() instead of its own Payload query: with exactly 1 active product, that one IS the spotlight (overriding any `spotlight` flag elsewhere); otherwise same most-recently-updated tie-break as before, just computed client-side. - ProductGrid drops the already-dead SHOP_GRID_EXCLUDE_IDS list in favor of the same `active` filter, with an empty-state message if 0 active. - RelatedProducts gates on >=2 active products regardless of cart contents or how many display slots would otherwise resolve. - Navbar's "Shop" link becomes an anchor to the homepage spotlight section (id="spotlight") instead of a real /shop navigation whenever exactly 1 product is active — passed down from the now-async root layout, which fetches the catalog once for this decision.
This commit is contained in:
@@ -40,16 +40,20 @@ function pickWithFallback(allIds: string[], excludeIds: string[], keep: string[]
|
||||
export function RelatedProducts() {
|
||||
const cart = useCart();
|
||||
const products = useProducts();
|
||||
// Cart/checkout resolve any product regardless of `active` (see
|
||||
// Product's own comment in lib/payload.ts) — this is the one discovery
|
||||
// surface among the useProducts() consumers, so it filters here itself.
|
||||
const activeProducts = useMemo(() => products.filter((p) => p.active), [products]);
|
||||
const hasItems = cart.length > 0;
|
||||
const cartKey = cart
|
||||
.map((i) => i.id)
|
||||
.sort()
|
||||
.join(",");
|
||||
// useMemo, not a plain .map() — .map() would return a new array
|
||||
// reference on every render regardless of whether `products` itself
|
||||
// reference on every render regardless of whether `activeProducts` itself
|
||||
// changed, which would make the effect below re-run (and re-pick) every
|
||||
// single render if `productIds` were listed as its dependency.
|
||||
const productIds = useMemo(() => products.map((p) => p.id), [products]);
|
||||
const productIds = useMemo(() => activeProducts.map((p) => p.id), [activeProducts]);
|
||||
|
||||
// Starts empty — the catalog itself is now fetched (useProducts()), so
|
||||
// there's nothing to pick a random set from until that resolves. The
|
||||
@@ -94,10 +98,15 @@ export function RelatedProducts() {
|
||||
}, [cartKey, productIds]);
|
||||
|
||||
const displayProducts = displayIds
|
||||
.map((id) => products.find((p) => p.id === id))
|
||||
.map((id) => activeProducts.find((p) => p.id === id))
|
||||
.filter((p): p is NonNullable<typeof p> => Boolean(p));
|
||||
|
||||
if (displayProducts.length === 0) return null;
|
||||
// Section-wide gate, independent of cart contents or how many
|
||||
// displayProducts happen to resolve: with only 1 active product,
|
||||
// pickWithFallback's cart-item-reuse fallback could still populate a
|
||||
// card, but a "related products" section makes no sense with fewer than
|
||||
// 2 real alternatives to offer.
|
||||
if (activeProducts.length < 2 || displayProducts.length === 0) return null;
|
||||
|
||||
return (
|
||||
<section className="w-full bg-bg-base flex flex-col gap-8 items-center py-12 md:py-16 px-[var(--layout-padding-x)]">
|
||||
|
||||
Reference in New Issue
Block a user