feat(blog): optional quote label + swappable related-product card per post
Both were fully hardcoded before: every blockquote showed a static "Merke dir:" label, and every post's "Passend dazu" card always linked the same flagship product. Now driven by two new Posts fields — quoteLabel (empty hides the label/icon/underline, blockquote still renders) and relatedProduct (empty hides the card entirely) — mirroring Products.spotlight but per-post instead of a single site-wide flag. README's collection table updated to match today's Payload changes (shipping-settings, the new Posts fields, trust-badges' placeholder tokens, admin sidebar grouping) — also fixed a stale claim that `media` isn't tenant-scoped; it already was.
This commit is contained in:
+32
-10
@@ -71,9 +71,21 @@ export async function getBlogPosts(limit = 3): Promise<BlogPost[]> {
|
||||
|
||||
export type PostDetail = BlogPost & {
|
||||
content: unknown;
|
||||
/** Label for every blockquote callout in this post's content (icon +
|
||||
* underline included) — empty string hides that left side entirely,
|
||||
* the blockquote itself still renders. See RichText.tsx's "quote" case. */
|
||||
quoteLabel: string;
|
||||
/** Powers the "Passend dazu" card at the end of the post — null hides
|
||||
* the card entirely, per-post choice (unlike Products.spotlight, which
|
||||
* is a single site-wide flag). */
|
||||
relatedProduct: Product | null;
|
||||
};
|
||||
|
||||
type PayloadPostDetail = PayloadPost & { content: unknown };
|
||||
type PayloadPostDetail = PayloadPost & {
|
||||
content: unknown;
|
||||
quoteLabel: string | null;
|
||||
relatedProduct: PayloadProduct | null;
|
||||
};
|
||||
|
||||
export async function getPostBySlug(slug: string): Promise<PostDetail | null> {
|
||||
const params = new URLSearchParams({
|
||||
@@ -107,6 +119,8 @@ export async function getPostBySlug(slug: string): Promise<PostDetail | null> {
|
||||
content: doc.content,
|
||||
publishedAt: doc.publishedAt,
|
||||
featured: doc.featured,
|
||||
quoteLabel: doc.quoteLabel ?? "",
|
||||
relatedProduct: doc.relatedProduct ? mapPayloadProduct(doc.relatedProduct) : null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -136,6 +150,22 @@ type PayloadProduct = {
|
||||
detailHref: string | null;
|
||||
};
|
||||
|
||||
// Shared by getProducts() and getPostBySlug()'s relatedProduct — kept in
|
||||
// one place instead of duplicating the same field mapping, which is
|
||||
// exactly the kind of drift this session's Shipping Settings work was
|
||||
// about eliminating elsewhere.
|
||||
function mapPayloadProduct(product: PayloadProduct): Product {
|
||||
return {
|
||||
id: product.slug,
|
||||
name: product.name,
|
||||
description: product.description ?? "",
|
||||
price: product.price,
|
||||
compareAtPrice: product.compareAtPrice ?? null,
|
||||
image: typeof product.image === "object" && product.image ? product.image.url : "",
|
||||
href: product.detailHref || null,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getProducts(): Promise<Product[]> {
|
||||
const params = new URLSearchParams({
|
||||
"where[tenant.slug][equals]": TENANT_SLUG,
|
||||
@@ -154,15 +184,7 @@ export async function getProducts(): Promise<Product[]> {
|
||||
|
||||
const data: { docs?: PayloadProduct[] } = await res.json();
|
||||
const docs = Array.isArray(data.docs) ? data.docs : [];
|
||||
return docs.map((product) => ({
|
||||
id: product.slug,
|
||||
name: product.name,
|
||||
description: product.description ?? "",
|
||||
price: product.price,
|
||||
compareAtPrice: product.compareAtPrice ?? null,
|
||||
image: typeof product.image === "object" && product.image ? product.image.url : "",
|
||||
href: product.detailHref || null,
|
||||
}));
|
||||
return docs.map(mapPayloadProduct);
|
||||
}
|
||||
|
||||
export async function getProductBySlug(slug: string): Promise<Product | null> {
|
||||
|
||||
Reference in New Issue
Block a user