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:
+47
-49
@@ -156,6 +156,20 @@ export type Product = {
|
||||
compareAtPrice: number | null;
|
||||
image: string;
|
||||
href: string | null;
|
||||
// `active` is opt-in for callers to filter by, not applied inside
|
||||
// getProducts()/getProductBySlug() themselves — cart, checkout, order
|
||||
// confirmation, and already-linked product detail pages (e.g.
|
||||
// TodoKartenHero/Pricing calling getProductBySlug directly) all need to
|
||||
// keep resolving a product regardless of its active status, unlike the
|
||||
// shop grid / spotlight / related-products discovery surfaces, which
|
||||
// filter `.filter(p => p.active)` themselves.
|
||||
active: boolean;
|
||||
updatedAt: string;
|
||||
spotlight: boolean;
|
||||
spotlightEyebrow: string | null;
|
||||
spotlightHeadline: string | null;
|
||||
spotlightText: string | null;
|
||||
spotlightImage: string | null;
|
||||
};
|
||||
|
||||
type PayloadProduct = {
|
||||
@@ -167,6 +181,13 @@ type PayloadProduct = {
|
||||
compareAtPrice: number | null;
|
||||
image: { url: string } | number | null;
|
||||
detailHref: string | null;
|
||||
active: boolean;
|
||||
updatedAt: string;
|
||||
spotlight: boolean;
|
||||
spotlightEyebrow: string | null;
|
||||
spotlightHeadline: string | null;
|
||||
spotlightText: string | null;
|
||||
spotlightImage: { url: string } | number | null;
|
||||
};
|
||||
|
||||
// Shared by getProducts() and getPostBySlug()'s relatedProduct — kept in
|
||||
@@ -182,6 +203,14 @@ export function mapPayloadProduct(product: PayloadProduct): Product {
|
||||
compareAtPrice: product.compareAtPrice ?? null,
|
||||
image: typeof product.image === "object" && product.image ? product.image.url : "",
|
||||
href: product.detailHref || null,
|
||||
active: product.active,
|
||||
updatedAt: product.updatedAt,
|
||||
spotlight: product.spotlight,
|
||||
spotlightEyebrow: product.spotlightEyebrow || null,
|
||||
spotlightHeadline: product.spotlightHeadline || null,
|
||||
spotlightText: product.spotlightText || null,
|
||||
spotlightImage:
|
||||
typeof product.spotlightImage === "object" && product.spotlightImage ? product.spotlightImage.url : null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -211,57 +240,26 @@ export async function getProductBySlug(slug: string): Promise<Product | null> {
|
||||
return products.find((p) => p.id === slug) ?? null;
|
||||
}
|
||||
|
||||
export type SpotlightProduct = Product & {
|
||||
spotlightEyebrow: string | null;
|
||||
spotlightHeadline: string | null;
|
||||
spotlightText: string | null;
|
||||
spotlightImage: string | null;
|
||||
};
|
||||
// Derived from getProducts() (same 60s-ISR-cached fetch every other
|
||||
// discovery surface already uses) instead of its own separate Payload
|
||||
// query — also what lets the auto-spotlight rule below just be a plain
|
||||
// array check instead of a second round-trip.
|
||||
//
|
||||
// Auto-spotlight: with exactly 1 active product, that product IS the
|
||||
// spotlight, full stop — overriding any `spotlight` flag set on some
|
||||
// other (inactive) product. Confirmed product decision, not just a
|
||||
// no-manual-flag fallback. Otherwise, same deterministic tie-break as
|
||||
// before (most-recently-updated wins) among active products actually
|
||||
// flagged `spotlight`.
|
||||
export async function getSpotlightProduct(): Promise<Product | null> {
|
||||
const products = await getProducts();
|
||||
const active = products.filter((p) => p.active);
|
||||
|
||||
type PayloadSpotlightProduct = PayloadProduct & {
|
||||
spotlightEyebrow: string | null;
|
||||
spotlightHeadline: string | null;
|
||||
spotlightText: string | null;
|
||||
spotlightImage: { url: string } | number | null;
|
||||
};
|
||||
if (active.length === 1) return active[0];
|
||||
|
||||
// sort: "-spotlight,-updatedAt" — same deterministic-tie-breaker pattern
|
||||
// as getBlogPosts' featured post: if more than one product is accidentally
|
||||
// marked spotlight, the most recently updated one wins, no error.
|
||||
export async function getSpotlightProduct(): Promise<SpotlightProduct | null> {
|
||||
const params = new URLSearchParams({
|
||||
"where[tenant.slug][equals]": TENANT_SLUG,
|
||||
"where[spotlight][equals]": "true",
|
||||
sort: "-updatedAt",
|
||||
depth: "2",
|
||||
limit: "1",
|
||||
});
|
||||
|
||||
const res = await fetch(`${PAYLOAD_URL}/api/products?${params}`, {
|
||||
next: { revalidate: 60 },
|
||||
});
|
||||
if (!res.ok) {
|
||||
console.error(`getSpotlightProduct: Payload returned ${res.status} ${res.statusText}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const data: { docs?: PayloadSpotlightProduct[] } = await res.json();
|
||||
const doc = data.docs?.[0];
|
||||
if (!doc) return null;
|
||||
return {
|
||||
id: doc.slug,
|
||||
name: doc.name,
|
||||
description: doc.description ?? "",
|
||||
price: doc.price,
|
||||
compareAtPrice: doc.compareAtPrice ?? null,
|
||||
image: typeof doc.image === "object" && doc.image ? doc.image.url : "",
|
||||
href: doc.detailHref || null,
|
||||
spotlightEyebrow: doc.spotlightEyebrow || null,
|
||||
spotlightHeadline: doc.spotlightHeadline || null,
|
||||
spotlightText: doc.spotlightText || null,
|
||||
spotlightImage:
|
||||
typeof doc.spotlightImage === "object" && doc.spotlightImage ? doc.spotlightImage.url : null,
|
||||
};
|
||||
const flagged = active.filter((p) => p.spotlight);
|
||||
if (flagged.length === 0) return null;
|
||||
return flagged.reduce((latest, p) => (p.updatedAt > latest.updatedAt ? p : latest));
|
||||
}
|
||||
|
||||
export type TrustBadge = { id: number; title: string; description: string; icon: string };
|
||||
|
||||
Reference in New Issue
Block a user