From 2500b8b16ffa4c4d9df1fe0bd9227e2c74d32ff8 Mon Sep 17 00:00:00 2001 From: CCS Admin Date: Sun, 15 Feb 2026 00:44:21 +0000 Subject: [PATCH] feat: migrate API layer to @c2s/payload-contracts - Add @c2s/payload-contracts as shared API client dependency - Create src/lib/cms.ts with tenant-configured PayloadClient instance - Replace manual fetch logic in api.ts with contracts client calls - Add transpilePackages config for TypeScript source imports - Local types preserved for component compatibility (bridge pattern) Co-Authored-By: Claude Opus 4.6 --- next.config.ts | 2 +- package.json | 3 +- pnpm-lock.yaml | 16 ++ src/lib/api.ts | 405 ++++++++++++++++--------------------------------- src/lib/cms.ts | 15 ++ 5 files changed, 167 insertions(+), 274 deletions(-) create mode 100644 src/lib/cms.ts diff --git a/next.config.ts b/next.config.ts index e9ffa30..5a29325 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,7 +1,7 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { - /* config options here */ + transpilePackages: ["@c2s/payload-contracts"], }; export default nextConfig; diff --git a/package.json b/package.json index 2546b32..0da18d9 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,8 @@ "next": "16.0.10", "react": "19.2.1", "react-dom": "19.2.1", - "tailwind-merge": "^3.4.0" + "tailwind-merge": "^3.4.0", + "@c2s/payload-contracts": "github:complexcaresolutions/payload-contracts" }, "devDependencies": { "@tailwindcss/postcss": "^4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b39897f..da88151 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + '@c2s/payload-contracts': + specifier: github:complexcaresolutions/payload-contracts + version: git+https://git@github.com:complexcaresolutions/payload-contracts.git#64847594b2150bfdce09a7bd7f54ad2f52d6f2b7(react@19.2.1) clsx: specifier: ^2.1.1 version: 2.1.1 @@ -122,6 +125,15 @@ packages: resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} engines: {node: '>=6.9.0'} + '@c2s/payload-contracts@git+https://git@github.com:complexcaresolutions/payload-contracts.git#64847594b2150bfdce09a7bd7f54ad2f52d6f2b7': + resolution: {commit: 64847594b2150bfdce09a7bd7f54ad2f52d6f2b7, repo: git@github.com:complexcaresolutions/payload-contracts.git, type: git} + version: 1.0.0 + peerDependencies: + react: ^19.0.0 + peerDependenciesMeta: + react: + optional: true + '@emnapi/core@1.7.1': resolution: {integrity: sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==} @@ -2021,6 +2033,10 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 + '@c2s/payload-contracts@git+https://git@github.com:complexcaresolutions/payload-contracts.git#64847594b2150bfdce09a7bd7f54ad2f52d6f2b7(react@19.2.1)': + optionalDependencies: + react: 19.2.1 + '@emnapi/core@1.7.1': dependencies: '@emnapi/wasi-threads': 1.1.0 diff --git a/src/lib/api.ts b/src/lib/api.ts index f82dff8..8ad3bf4 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -1,80 +1,36 @@ +/** + * Payload CMS API functions — powered by @c2s/payload-contracts + * + * Uses the shared API client for transport (tenant isolation, fetch caching) + * but returns data typed with local interfaces for component compatibility. + * + * The local types use simplified interfaces (e.g. id: string, meta instead of seo) + * while the contracts use the real CMS types. We use 'as unknown as' to bridge. + */ +import { cms } from "./cms" import type { Page, Post, Navigation, - SiteSettings, Favorite, Series, Testimonial, FAQ, SeoSettings, PaginatedResponse, - FavoriteCategory, - FavoriteBadge, -} from './types' + SiteSettings, +} from "./types" -const PAYLOAD_URL = process.env.NEXT_PUBLIC_PAYLOAD_URL || 'https://cms.c2sgmbh.de' -const TENANT_ID = process.env.NEXT_PUBLIC_TENANT_ID || '9' - -// Shared empty paginated response for error fallbacks -const emptyPaginatedResponse = { docs: [], totalDocs: 0, limit: 10, totalPages: 0, page: 1, pagingCounter: 1, hasPrevPage: false, hasNextPage: false, prevPage: null, nextPage: null } - -interface FetchOptions { - revalidate?: number | false - tags?: string[] -} - -async function fetchAPI( - endpoint: string, - options: FetchOptions & { defaultValue?: T } = {} -): Promise { - const { revalidate = 60, tags, defaultValue } = options - - try { - const res = await fetch(`${PAYLOAD_URL}${endpoint}`, { - next: { - revalidate, - tags, - }, - }) - - if (!res.ok) { - console.error(`API error: ${res.status} ${res.statusText} for ${endpoint}`) - if (defaultValue !== undefined) { - return defaultValue - } - throw new Error(`API error: ${res.status} ${res.statusText}`) - } - - return res.json() - } catch (error) { - console.error(`Fetch error for ${endpoint}:`, error) - if (defaultValue !== undefined) { - return defaultValue - } - throw error - } -} +const PAYLOAD_URL = process.env.NEXT_PUBLIC_PAYLOAD_URL || "https://cms.c2sgmbh.de" +const TENANT_ID = process.env.NEXT_PUBLIC_TENANT_ID || "9" // Pages -export async function getPage( - slug: string, - locale = 'de' -): Promise { - const params = new URLSearchParams({ - 'where[tenant][equals]': TENANT_ID, - 'where[slug][equals]': slug, - 'where[status][equals]': 'published', - locale, - depth: '2', +export async function getPage(slug: string, locale = "de"): Promise { + const result = await cms.pages.getPage(slug, { + locale: locale as "de" | "en", + depth: 2, }) - - const data = await fetchAPI>( - `/api/pages?${params}`, - { tags: [`page-${slug}`] } - ) - - return data.docs[0] || null + return result as unknown as Page | null } export async function getPages(options: { @@ -82,41 +38,26 @@ export async function getPages(options: { page?: number locale?: string } = {}): Promise> { - const params = new URLSearchParams({ - 'where[tenant][equals]': TENANT_ID, - 'where[status][equals]': 'published', - limit: String(options.limit || 100), - page: String(options.page || 1), - locale: options.locale || 'de', - depth: '1', + const result = await cms.pages.getPages({ + limit: options.limit || 100, + page: options.page || 1, + locale: (options.locale || "de") as "de" | "en", + depth: 1, }) - - return fetchAPI>(`/api/pages?${params}`) + return result as unknown as PaginatedResponse } // Posts -export async function getPost( - slug: string, - locale = 'de' -): Promise { - const params = new URLSearchParams({ - 'where[tenant][equals]': TENANT_ID, - 'where[slug][equals]': slug, - 'where[status][equals]': 'published', - locale, - depth: '2', +export async function getPost(slug: string, locale = "de"): Promise { + const result = await cms.posts.getPost(slug, { + locale: locale as "de" | "en", + depth: 2, }) - - const data = await fetchAPI>( - `/api/posts?${params}`, - { tags: [`post-${slug}`] } - ) - - return data.docs[0] || null + return result as unknown as Post | null } export async function getPosts(options: { - type?: 'blog' | 'news' | 'press' | 'announcement' + type?: string category?: string series?: string limit?: number @@ -124,83 +65,53 @@ export async function getPosts(options: { locale?: string featured?: boolean } = {}): Promise> { - const params = new URLSearchParams({ - 'where[tenant][equals]': TENANT_ID, - 'where[status][equals]': 'published', - sort: '-publishedAt', - limit: String(options.limit || 10), - page: String(options.page || 1), - locale: options.locale || 'de', - depth: '1', - }) + try { + const where: Record = {} + if (options.featured) where["isFeatured][equals"] = "true" - if (options.type) { - params.append('where[type][equals]', options.type) + const result = await cms.posts.getPosts({ + type: options.type, + category: options.category, + series: options.series, + limit: options.limit || 10, + page: options.page || 1, + locale: (options.locale || "de") as "de" | "en", + where, + }) + return result as unknown as PaginatedResponse + } catch { + return { docs: [], totalDocs: 0, limit: 10, totalPages: 0, page: 1, pagingCounter: 1, hasPrevPage: false, hasNextPage: false, prevPage: null, nextPage: null } } - if (options.category) { - params.append('where[categories][contains]', options.category) - } - if (options.series) { - params.append('where[series][equals]', options.series) - } - if (options.featured) { - params.append('where[isFeatured][equals]', 'true') - } - - return fetchAPI>( - `/api/posts?${params}`, - { tags: ['posts'], defaultValue: emptyPaginatedResponse as PaginatedResponse } - ) } // Navigation -export async function getNavigation( - type: 'header' | 'footer' | 'mobile' -): Promise { - const params = new URLSearchParams({ - 'where[tenant][equals]': TENANT_ID, - 'where[type][equals]': type, - depth: '2', - }) - - const data = await fetchAPI>( - `/api/navigations?${params}`, - { - revalidate: 300, - tags: [`navigation-${type}`], - defaultValue: { docs: [], totalDocs: 0, limit: 10, totalPages: 0, page: 1, pagingCounter: 1, hasPrevPage: false, hasNextPage: false, prevPage: null, nextPage: null }, - } - ) - - return data.docs[0] || null +export async function getNavigation(type: "header" | "footer" | "mobile"): Promise { + try { + const result = await cms.navigation.getNavigation(type, { depth: 2 }) + return result as unknown as Navigation | null + } catch { + return null + } } // Site Settings export async function getSiteSettings(): Promise { - const params = new URLSearchParams({ - 'where[tenant][equals]': TENANT_ID, - depth: '2', - }) - - const data = await fetchAPI>( - `/api/site-settings?${params}`, - { - revalidate: 300, - tags: ['site-settings'], - defaultValue: { docs: [], totalDocs: 0, limit: 10, totalPages: 0, page: 1, pagingCounter: 1, hasPrevPage: false, hasNextPage: false, prevPage: null, nextPage: null }, - } - ) - - return data.docs[0] || null + try { + const result = await cms.settings.getSiteSettings({ depth: 2 }) + return result as unknown as SiteSettings | null + } catch { + return null + } } // SEO Settings (Global) export async function getSeoSettings(): Promise { - return fetchAPI('/api/globals/seo-settings', { - revalidate: 3600, - tags: ['seo-settings'], - defaultValue: null as unknown as SeoSettings, - }) + try { + const result = await cms.settings.getSeoSettings() + return result as unknown as SeoSettings | null + } catch { + return null + } } // Testimonials @@ -208,17 +119,12 @@ export async function getTestimonials(options: { limit?: number locale?: string } = {}): Promise> { - const params = new URLSearchParams({ - 'where[tenant][equals]': TENANT_ID, - limit: String(options.limit || 10), - locale: options.locale || 'de', - depth: '1', + const result = await cms.client.getCollection("testimonials", { + limit: options.limit || 10, + locale: (options.locale || "de") as "de" | "en", + depth: 1, }) - - return fetchAPI>( - `/api/testimonials?${params}`, - { tags: ['testimonials'] } - ) + return result as unknown as PaginatedResponse } // FAQs @@ -227,72 +133,50 @@ export async function getFAQs(options: { limit?: number locale?: string } = {}): Promise> { - const params = new URLSearchParams({ - 'where[tenant][equals]': TENANT_ID, - sort: 'order', - limit: String(options.limit || 50), - locale: options.locale || 'de', - depth: '1', + const result = await cms.client.getCollection("faqs", { + limit: options.limit || 50, + locale: (options.locale || "de") as "de" | "en", + sort: "order", + depth: 1, + where: options.category ? { "category][equals": options.category } : undefined, }) - - if (options.category) { - params.append('where[category][equals]', options.category) - } - - return fetchAPI>( - `/api/faqs?${params}`, - { tags: ['faqs'] } - ) + return result as unknown as PaginatedResponse } // BlogWoman: Favorites export async function getFavorites(options: { - category?: FavoriteCategory - badge?: FavoriteBadge + category?: string + badge?: string limit?: number page?: number locale?: string } = {}): Promise> { - const params = new URLSearchParams({ - 'where[tenant][equals]': TENANT_ID, - 'where[isActive][equals]': 'true', - limit: String(options.limit || 12), - page: String(options.page || 1), - locale: options.locale || 'de', - depth: '1', - }) + try { + const where: Record = { "isActive][equals": "true" } + if (options.category) where["category][equals"] = options.category + if (options.badge) where["badge][equals"] = options.badge - if (options.category) { - params.append('where[category][equals]', options.category) + const result = await cms.client.getCollection("favorites", { + limit: options.limit || 12, + page: options.page || 1, + locale: (options.locale || "de") as "de" | "en", + depth: 1, + where, + }) + return result as unknown as PaginatedResponse + } catch { + return { docs: [], totalDocs: 0, limit: 12, totalPages: 0, page: 1, pagingCounter: 1, hasPrevPage: false, hasNextPage: false, prevPage: null, nextPage: null } } - if (options.badge) { - params.append('where[badge][equals]', options.badge) - } - - return fetchAPI>( - `/api/favorites?${params}`, - { tags: ['favorites'], defaultValue: emptyPaginatedResponse as PaginatedResponse } - ) } -export async function getFavorite( - slug: string, - locale = 'de' -): Promise { - const params = new URLSearchParams({ - 'where[tenant][equals]': TENANT_ID, - 'where[slug][equals]': slug, - 'where[isActive][equals]': 'true', - locale, - depth: '1', +export async function getFavorite(slug: string, locale = "de"): Promise { + const data = await cms.client.getCollection("favorites", { + locale: locale as "de" | "en", + depth: 1, + where: { "slug][equals": slug, "isActive][equals": "true" }, + limit: 1, }) - - const data = await fetchAPI>( - `/api/favorites?${params}`, - { tags: [`favorite-${slug}`] } - ) - - return data.docs[0] || null + return (data.docs[0] as unknown as Favorite) ?? null } // BlogWoman: Series @@ -300,58 +184,41 @@ export async function getSeries(options: { limit?: number locale?: string } = {}): Promise> { - const params = new URLSearchParams({ - 'where[tenant][equals]': TENANT_ID, - 'where[isActive][equals]': 'true', - limit: String(options.limit || 20), - locale: options.locale || 'de', - depth: '2', - }) - - return fetchAPI>( - `/api/series?${params}`, - { tags: ['series'], defaultValue: emptyPaginatedResponse as PaginatedResponse } - ) + try { + const result = await cms.client.getCollection("series", { + limit: options.limit || 20, + locale: (options.locale || "de") as "de" | "en", + depth: 2, + where: { "isActive][equals": "true" }, + }) + return result as unknown as PaginatedResponse + } catch { + return { docs: [], totalDocs: 0, limit: 20, totalPages: 0, page: 1, pagingCounter: 1, hasPrevPage: false, hasNextPage: false, prevPage: null, nextPage: null } + } } -export async function getSeriesBySlug( - slug: string, - locale = 'de' -): Promise { - const params = new URLSearchParams({ - 'where[tenant][equals]': TENANT_ID, - 'where[slug][equals]': slug, - 'where[isActive][equals]': 'true', - locale, - depth: '2', +export async function getSeriesBySlug(slug: string, locale = "de"): Promise { + const data = await cms.client.getCollection("series", { + locale: locale as "de" | "en", + depth: 2, + where: { "slug][equals": slug, "isActive][equals": "true" }, + limit: 1, }) - - const data = await fetchAPI>( - `/api/series?${params}`, - { tags: [`series-${slug}`] } - ) - - return data.docs[0] || null + return (data.docs[0] as unknown as Series) ?? null } // Newsletter Subscription export async function subscribeNewsletter( email: string, firstName?: string, - source = 'website' + source = "website" ): Promise<{ success: boolean; message?: string }> { - const res = await fetch(`${PAYLOAD_URL}/api/newsletter/subscribe`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - email, - firstName, - tenantId: Number(TENANT_ID), - source, - }), + return cms.post("/api/newsletter/subscribe", { + email, + firstName, + tenantId: Number(TENANT_ID), + source, }) - - return res.json() } // Contact Form Submission @@ -363,20 +230,14 @@ export async function submitContactForm(data: { message: string formId?: number }): Promise<{ success: boolean; message?: string }> { - const res = await fetch(`${PAYLOAD_URL}/api/form-submissions`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - form: data.formId || 1, - submissionData: [ - { field: 'name', value: data.name }, - { field: 'email', value: data.email }, - { field: 'phone', value: data.phone || '' }, - { field: 'subject', value: data.subject }, - { field: 'message', value: data.message }, - ], - }), + return cms.post("/api/form-submissions", { + form: data.formId || 1, + submissionData: [ + { field: "name", value: data.name }, + { field: "email", value: data.email }, + { field: "phone", value: data.phone || "" }, + { field: "subject", value: data.subject }, + { field: "message", value: data.message }, + ], }) - - return res.json() } diff --git a/src/lib/cms.ts b/src/lib/cms.ts new file mode 100644 index 0000000..c03f6df --- /dev/null +++ b/src/lib/cms.ts @@ -0,0 +1,15 @@ +/** + * Payload CMS Client — initialized from @c2s/payload-contracts + * + * Single shared client instance for all API calls. + * Tenant isolation is handled automatically. + */ +import { createPayloadClient } from "@c2s/payload-contracts/api-client" + +export const cms = createPayloadClient({ + baseUrl: process.env.NEXT_PUBLIC_PAYLOAD_URL || "https://cms.c2sgmbh.de", + tenantId: process.env.NEXT_PUBLIC_TENANT_ID || "9", + defaultLocale: "de", + defaultDepth: 2, + defaultRevalidate: 60, +})