Move trust badges, shipping/payment methods, Werkzeuge cards, product spotlight into CMS
New Payload collections, all editable without a code deploy: - TrustBadges: the horizontal Schneller-Versand/Versandkostenfrei/Mit- Liebe-verpackt row (TrustRow.tsx, now an async server component). - CartTrustBadges: the Sichere-Zahlung/14-Tage-Rückgaberecht/Nachhaltig- verpackt sidebar bullets — shared by /cart (title only) and /checkout (title + description), which previously had two different hardcoded bullet lists for what's conceptually the same content. - ShippingMethods: /checkout's Versandart radios. Each method has its own optional freeShippingThreshold — omitted means "never free" (Express), not "always free". /cart's FreeShippingBanner now targets the lowest threshold among active methods instead of a single global constant, and hides entirely if no active method has one. - PaymentMethods: /checkout's Zahlungsart radios, icons as an array (Kreditkarte shows 3 logos, PayPal/Überweisung show 1). - Products: new spotlight/spotlightHeadline/spotlightText/spotlightImage/ compareAtPrice fields. ProductSpotlight.tsx (homepage) now shows whichever product has `spotlight` checked instead of being hardcoded to ToDo-Karten, with its own marketing copy separate from the plain catalog name/description. AddToCartButton takes an explicit productId prop now instead of a hardcoded "todo-karten" constant. - WerkzeugeCards: the homepage's "Meine Werkzeuge" 3-card grid (Tools.tsx, now async). Icons use a uniform box instead of the previous per-card hand-tuned width/height/rotation, which only worked for 3 known, upside-down-authored SVGs — those were re-exported as pre-flipped PNGs. lib/payload.ts gained getTrustBadges/getCartTrustBadges/getShippingMethods/ getPaymentMethods/getWerkzeugeCards/getSpotlightProduct, all with the same graceful-empty-array-on-fetch-failure pattern as the existing functions. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -118,6 +118,7 @@ export type Product = {
|
||||
name: string;
|
||||
description: string;
|
||||
price: number;
|
||||
compareAtPrice: number | null;
|
||||
image: string;
|
||||
href: string | null;
|
||||
};
|
||||
@@ -128,6 +129,7 @@ type PayloadProduct = {
|
||||
slug: string;
|
||||
description: string | null;
|
||||
price: number;
|
||||
compareAtPrice: number | null;
|
||||
image: { url: string } | number | null;
|
||||
detailHref: string | null;
|
||||
};
|
||||
@@ -155,6 +157,7 @@ export async function getProducts(): Promise<Product[]> {
|
||||
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,
|
||||
}));
|
||||
@@ -165,6 +168,219 @@ export async function getProductBySlug(slug: string): Promise<Product | null> {
|
||||
return products.find((p) => p.id === slug) ?? null;
|
||||
}
|
||||
|
||||
export type SpotlightProduct = Product & {
|
||||
spotlightHeadline: string | null;
|
||||
spotlightText: string | null;
|
||||
spotlightImage: string | null;
|
||||
};
|
||||
|
||||
type PayloadSpotlightProduct = PayloadProduct & {
|
||||
spotlightHeadline: string | null;
|
||||
spotlightText: string | null;
|
||||
spotlightImage: { url: string } | number | null;
|
||||
};
|
||||
|
||||
// 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,
|
||||
spotlightHeadline: doc.spotlightHeadline || null,
|
||||
spotlightText: doc.spotlightText || null,
|
||||
spotlightImage:
|
||||
typeof doc.spotlightImage === "object" && doc.spotlightImage ? doc.spotlightImage.url : null,
|
||||
};
|
||||
}
|
||||
|
||||
export type TrustBadge = { id: number; title: string; description: string; icon: string };
|
||||
|
||||
type PayloadTrustBadge = { id: number; title: string; description: string; icon: { url: string } | number | null };
|
||||
|
||||
async function fetchTrustBadgeList(collection: "trust-badges" | "cart-trust-badges"): Promise<TrustBadge[]> {
|
||||
const params = new URLSearchParams({
|
||||
"where[tenant.slug][equals]": TENANT_SLUG,
|
||||
sort: "sortOrder",
|
||||
depth: "1",
|
||||
limit: "50",
|
||||
});
|
||||
|
||||
const res = await fetch(`${PAYLOAD_URL}/api/${collection}?${params}`, {
|
||||
next: { revalidate: 60 },
|
||||
});
|
||||
if (!res.ok) {
|
||||
console.error(`fetchTrustBadgeList(${collection}): Payload returned ${res.status} ${res.statusText}`);
|
||||
return [];
|
||||
}
|
||||
|
||||
const data: { docs?: PayloadTrustBadge[] } = await res.json();
|
||||
const docs = Array.isArray(data.docs) ? data.docs : [];
|
||||
return docs.map((doc) => ({
|
||||
id: doc.id,
|
||||
title: doc.title,
|
||||
description: doc.description,
|
||||
icon: typeof doc.icon === "object" && doc.icon ? doc.icon.url : "",
|
||||
}));
|
||||
}
|
||||
|
||||
// Powers TrustRow.tsx — the horizontal "Schneller Versand /
|
||||
// Versandkostenfrei / Mit Liebe verpackt" row.
|
||||
export async function getTrustBadges(): Promise<TrustBadge[]> {
|
||||
return fetchTrustBadgeList("trust-badges");
|
||||
}
|
||||
|
||||
// Powers the "Sichere Zahlung / 14 Tage Rückgaberecht / Nachhaltig
|
||||
// verpackt" sidebar bullets on /cart (title only) and /checkout
|
||||
// (title + description) — a different list from TrustBadges.
|
||||
export async function getCartTrustBadges(): Promise<TrustBadge[]> {
|
||||
return fetchTrustBadgeList("cart-trust-badges");
|
||||
}
|
||||
|
||||
export type ShippingMethod = {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string;
|
||||
price: number;
|
||||
freeShippingThreshold: number | null;
|
||||
};
|
||||
|
||||
type PayloadShippingMethod = ShippingMethod & { active: boolean };
|
||||
|
||||
export async function getShippingMethods(): Promise<ShippingMethod[]> {
|
||||
const params = new URLSearchParams({
|
||||
"where[tenant.slug][equals]": TENANT_SLUG,
|
||||
"where[active][equals]": "true",
|
||||
sort: "sortOrder",
|
||||
limit: "20",
|
||||
});
|
||||
|
||||
const res = await fetch(`${PAYLOAD_URL}/api/shipping-methods?${params}`, {
|
||||
next: { revalidate: 60 },
|
||||
});
|
||||
if (!res.ok) {
|
||||
console.error(`getShippingMethods: Payload returned ${res.status} ${res.statusText}`);
|
||||
return [];
|
||||
}
|
||||
|
||||
const data: { docs?: PayloadShippingMethod[] } = await res.json();
|
||||
const docs = Array.isArray(data.docs) ? data.docs : [];
|
||||
return docs.map((doc) => ({
|
||||
id: doc.id,
|
||||
title: doc.title,
|
||||
description: doc.description,
|
||||
price: doc.price,
|
||||
freeShippingThreshold: doc.freeShippingThreshold ?? null,
|
||||
}));
|
||||
}
|
||||
|
||||
export type PaymentMethod = { id: number; title: string; icons: string[] };
|
||||
|
||||
type PayloadPaymentMethod = {
|
||||
id: number;
|
||||
title: string;
|
||||
active: boolean;
|
||||
icons: { icon: { url: string } | number | null }[];
|
||||
};
|
||||
|
||||
export async function getPaymentMethods(): Promise<PaymentMethod[]> {
|
||||
const params = new URLSearchParams({
|
||||
"where[tenant.slug][equals]": TENANT_SLUG,
|
||||
"where[active][equals]": "true",
|
||||
sort: "sortOrder",
|
||||
depth: "1",
|
||||
limit: "20",
|
||||
});
|
||||
|
||||
const res = await fetch(`${PAYLOAD_URL}/api/payment-methods?${params}`, {
|
||||
next: { revalidate: 60 },
|
||||
});
|
||||
if (!res.ok) {
|
||||
console.error(`getPaymentMethods: Payload returned ${res.status} ${res.statusText}`);
|
||||
return [];
|
||||
}
|
||||
|
||||
const data: { docs?: PayloadPaymentMethod[] } = await res.json();
|
||||
const docs = Array.isArray(data.docs) ? data.docs : [];
|
||||
return docs.map((doc) => ({
|
||||
id: doc.id,
|
||||
title: doc.title,
|
||||
icons: (doc.icons ?? [])
|
||||
.map((row) => (typeof row.icon === "object" && row.icon ? row.icon.url : null))
|
||||
.filter((url): url is string => Boolean(url)),
|
||||
}));
|
||||
}
|
||||
|
||||
export type WerkzeugeCard = {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string;
|
||||
icon: string;
|
||||
ctaLabel: string;
|
||||
ctaHref: string;
|
||||
};
|
||||
|
||||
type PayloadWerkzeugeCard = {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string;
|
||||
icon: { url: string } | number | null;
|
||||
ctaLabel: string;
|
||||
ctaHref: string;
|
||||
};
|
||||
|
||||
export async function getWerkzeugeCards(): Promise<WerkzeugeCard[]> {
|
||||
const params = new URLSearchParams({
|
||||
"where[tenant.slug][equals]": TENANT_SLUG,
|
||||
sort: "sortOrder",
|
||||
depth: "1",
|
||||
limit: "20",
|
||||
});
|
||||
|
||||
const res = await fetch(`${PAYLOAD_URL}/api/werkzeuge-cards?${params}`, {
|
||||
next: { revalidate: 60 },
|
||||
});
|
||||
if (!res.ok) {
|
||||
console.error(`getWerkzeugeCards: Payload returned ${res.status} ${res.statusText}`);
|
||||
return [];
|
||||
}
|
||||
|
||||
const data: { docs?: PayloadWerkzeugeCard[] } = await res.json();
|
||||
const docs = Array.isArray(data.docs) ? data.docs : [];
|
||||
return docs.map((doc) => ({
|
||||
id: doc.id,
|
||||
title: doc.title,
|
||||
description: doc.description,
|
||||
icon: typeof doc.icon === "object" && doc.icon ? doc.icon.url : "",
|
||||
ctaLabel: doc.ctaLabel,
|
||||
ctaHref: doc.ctaHref,
|
||||
}));
|
||||
}
|
||||
|
||||
export type LegalPageType = "impressum" | "datenschutz" | "agb" | "widerruf";
|
||||
|
||||
export type LegalPage = {
|
||||
|
||||
Reference in New Issue
Block a user