const PAYLOAD_URL = process.env.PAYLOAD_URL || "https://payload.mk360.de"; const TENANT_SLUG = "einfach-produktiv"; export type BlogPost = { id: number; title: string; slug: string; category: string; readTime: number; excerpt: string; thumbnail: string | null; publishedAt: string; featured: boolean; }; type PayloadPost = { id: number; title: string; slug: string; category: { name: string } | number | null; readTime: number; excerpt: string; thumbnail: { url: string } | number | null; publishedAt: string; featured: boolean; }; // sort: "-featured,-publishedAt" — Payload's multi-field sort puts any // featured post(s) first, most-recently-published first among those, then // the rest by date. Callers that just want posts[0] as "the" featured post // (e.g. /blog) get a deterministic pick even if more than one post was // accidentally marked featured — no special-casing needed here. export async function getBlogPosts(limit = 3): Promise { const params = new URLSearchParams({ "where[tenant.slug][equals]": TENANT_SLUG, sort: "-featured,-publishedAt", depth: "2", limit: String(limit), }); const res = await fetch(`${PAYLOAD_URL}/api/posts?${params}`, { next: { revalidate: 60 }, }); if (!res.ok) { console.error(`getBlogPosts: Payload returned ${res.status} ${res.statusText}`); return []; } const data: { docs?: PayloadPost[] } = await res.json(); const docs = Array.isArray(data.docs) ? data.docs : []; return docs.map((post) => ({ id: post.id, title: post.title, slug: post.slug, category: typeof post.category === "object" && post.category ? post.category.name : "", readTime: post.readTime, excerpt: post.excerpt, thumbnail: typeof post.thumbnail === "object" && post.thumbnail ? post.thumbnail.url : null, publishedAt: post.publishedAt, featured: post.featured, })); } export type PostDetail = BlogPost & { content: unknown; }; type PayloadPostDetail = PayloadPost & { content: unknown }; export async function getPostBySlug(slug: string): Promise { const params = new URLSearchParams({ "where[tenant.slug][equals]": TENANT_SLUG, "where[slug][equals]": slug, depth: "2", limit: "1", }); const res = await fetch(`${PAYLOAD_URL}/api/posts?${params}`, { next: { revalidate: 60 }, }); if (!res.ok) { console.error(`getPostBySlug: Payload returned ${res.status} ${res.statusText}`); return null; } const data: { docs?: PayloadPostDetail[] } = await res.json(); const doc = data.docs?.[0]; if (!doc) return null; return { id: doc.id, title: doc.title, slug: doc.slug, category: typeof doc.category === "object" && doc.category ? doc.category.name : "", readTime: doc.readTime, excerpt: doc.excerpt, thumbnail: typeof doc.thumbnail === "object" && doc.thumbnail ? doc.thumbnail.url : null, content: doc.content, publishedAt: doc.publishedAt, featured: doc.featured, }; } // Frontend-facing shape — `id` is Payload's `slug` field, not its numeric // row id. Cart items are stored in localStorage keyed by this string (see // lib/cart.ts), so slugs were chosen in the Products collection to match // the ids the old hardcoded catalog used ("todo-karten" etc.) — switching // to numeric ids here would silently orphan every existing shopper's cart. export type Product = { id: string; name: string; description: string; price: number; compareAtPrice: number | null; image: string; href: string | null; }; type PayloadProduct = { id: number; name: string; slug: string; description: string | null; price: number; compareAtPrice: number | null; image: { url: string } | number | null; detailHref: string | null; }; export async function getProducts(): Promise { const params = new URLSearchParams({ "where[tenant.slug][equals]": TENANT_SLUG, sort: "sortOrder", depth: "2", limit: "100", }); const res = await fetch(`${PAYLOAD_URL}/api/products?${params}`, { next: { revalidate: 60 }, }); if (!res.ok) { console.error(`getProducts: Payload returned ${res.status} ${res.statusText}`); return []; } 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, })); } export async function getProductBySlug(slug: string): Promise { const products = await getProducts(); 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 = { type: LegalPageType; title: string; content: unknown; attachment: { url: string; title: string } | null; }; type PayloadLegalPage = { type: LegalPageType; title: string; content: unknown; attachment: { url: string; title: string } | number | null; }; export async function getLegalPage(type: LegalPageType): Promise { const params = new URLSearchParams({ "where[tenant.slug][equals]": TENANT_SLUG, "where[type][equals]": type, depth: "1", limit: "1", }); const res = await fetch(`${PAYLOAD_URL}/api/legal-pages?${params}`, { next: { revalidate: 60 }, }); if (!res.ok) { console.error(`getLegalPage: Payload returned ${res.status} ${res.statusText}`); return null; } const data: { docs?: PayloadLegalPage[] } = await res.json(); const doc = data.docs?.[0]; if (!doc) return null; return { type: doc.type, title: doc.title, content: doc.content, attachment: typeof doc.attachment === "object" && doc.attachment ? { url: doc.attachment.url, title: doc.attachment.title } : null, }; }