feat: payload cms
This commit is contained in:
@@ -429,6 +429,26 @@ jobs:
|
||||
- name: Install dependencies
|
||||
id: deps
|
||||
run: pnpm install --frozen-lockfile
|
||||
- name: 🔍 Install Chromium (for Asset Scan)
|
||||
run: |
|
||||
rm -f /etc/apt/apt.conf.d/docker-clean
|
||||
apt-get update
|
||||
apt-get install -y gnupg wget ca-certificates
|
||||
OS_ID=$(. /etc/os-release && echo $ID)
|
||||
CODENAME=$(. /etc/os-release && echo $VERSION_CODENAME)
|
||||
if [ "$OS_ID" = "debian" ]; then
|
||||
apt-get install -y chromium
|
||||
else
|
||||
mkdir -p /etc/apt/keyrings
|
||||
KEY_ID="82BB6851C64F6880"
|
||||
wget -qO- "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x$KEY_ID" | gpg --dearmor > /etc/apt/keyrings/xtradeb.gpg
|
||||
echo "deb [signed-by=/etc/apt/keyrings/xtradeb.gpg] http://ppa.launchpad.net/xtradeb/apps/ubuntu $CODENAME main" > /etc/apt/sources.list.d/xtradeb-ppa.list
|
||||
printf "Package: *\nPin: release o=LP-PPA-xtradeb-apps\nPin-Priority: 1001\n" > /etc/apt/preferences.d/xtradeb
|
||||
apt-get update
|
||||
apt-get install -y --allow-downgrades chromium
|
||||
fi
|
||||
[ -f /usr/bin/chromium ] && ln -sf /usr/bin/chromium /usr/bin/google-chrome
|
||||
[ -f /usr/bin/chromium ] && ln -sf /usr/bin/chromium /usr/bin/chromium-browser
|
||||
|
||||
# ── Critical Smoke Tests (MUST pass) ──────────────────────────────────
|
||||
- name: 🚀 OG Image Check
|
||||
@@ -477,6 +497,8 @@ jobs:
|
||||
env:
|
||||
NEXT_PUBLIC_BASE_URL: ${{ needs.prepare.outputs.next_public_url }}
|
||||
GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD || 'klz2026' }}
|
||||
PUPPETEER_EXECUTABLE_PATH: /usr/bin/chromium
|
||||
CHROME_PATH: /usr/bin/chromium
|
||||
run: pnpm check:assets
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
@@ -568,6 +590,7 @@ jobs:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
steps:
|
||||
- name: 🔔 Gotify
|
||||
shell: bash
|
||||
run: |
|
||||
DEPLOY="${{ needs.deploy.result }}"
|
||||
SMOKE="${{ needs.post_deploy_checks.result }}"
|
||||
|
||||
1
.npmrc
1
.npmrc
@@ -1,3 +1,2 @@
|
||||
@mintel:registry=https://npm.infra.mintel.me/
|
||||
//npm.infra.mintel.me/:_authToken=${NPM_TOKEN}
|
||||
always-auth=true
|
||||
|
||||
@@ -22,6 +22,8 @@ import { StrikethroughFeatureClient as StrikethroughFeatureClient_e70f5e05f09f93
|
||||
import { UnderlineFeatureClient as UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { BoldFeatureClient as BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { ItalicFeatureClient as ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||
import { default as default_9ed509b5e5f7d08a16335393f27586cc } from '../../../src/payload/components/Icon'
|
||||
import { default as default_5470ea90f7a8fd882c2fe59ff2b1c5b9 } from '../../../src/payload/components/Logo'
|
||||
import { CollectionCards as CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1 } from '@payloadcms/next/rsc'
|
||||
|
||||
export const importMap = {
|
||||
@@ -49,5 +51,7 @@ export const importMap = {
|
||||
"@payloadcms/richtext-lexical/client#UnderlineFeatureClient": UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#BoldFeatureClient": BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"@payloadcms/richtext-lexical/client#ItalicFeatureClient": ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||
"/src/payload/components/Icon#default": default_9ed509b5e5f7d08a16335393f27586cc,
|
||||
"/src/payload/components/Logo#default": default_5470ea90f7a8fd882c2fe59ff2b1c5b9,
|
||||
"@payloadcms/next/rsc#CollectionCards": CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1
|
||||
}
|
||||
|
||||
@@ -1 +1,151 @@
|
||||
/* Custom Payload CMS admin styles can go here. Do not import payloadcms/ui/scss/app.scss as it is handled by @payloadcms/next/css */
|
||||
/* =================================================================
|
||||
KLZ Cables – Payload Admin Theme
|
||||
Strictly follows docs/STYLEGUIDE.md & tailwind.config.cjs
|
||||
|
||||
IMPORTANT: We use `html` selector (not `:root`) because Payload's
|
||||
own CSS defines variables on `:root` and loads AFTER this file.
|
||||
`html` has higher specificity than `:root`, so our values win.
|
||||
================================================================= */
|
||||
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap');
|
||||
|
||||
/* =================================================================
|
||||
COLOR OVERRIDES
|
||||
Payload internally maps:
|
||||
--theme-elevation-* → --color-base-*
|
||||
--theme-success-* → --color-success-*
|
||||
We override the SOURCE variables on `html` to beat Payload's `:root`.
|
||||
================================================================= */
|
||||
|
||||
html {
|
||||
/* ---------------------------------------------------------------
|
||||
KLZ Primary Blue (#011dff) → Buttons, links, active states
|
||||
--------------------------------------------------------------- */
|
||||
--color-success-50: #eef0ff !important;
|
||||
--color-success-100: #dfe2ff !important;
|
||||
--color-success-150: #cdd2ff !important;
|
||||
--color-success-200: #b8bfff !important;
|
||||
--color-success-250: #a0a9ff !important;
|
||||
--color-success-300: #8892ff !important;
|
||||
--color-success-350: #707bff !important;
|
||||
--color-success-400: #5564ff !important;
|
||||
--color-success-450: #3a4dff !important;
|
||||
--color-success-500: #011dff !important;
|
||||
/* KLZ Primary */
|
||||
--color-success-550: #0119e6 !important;
|
||||
--color-success-600: #0116cc !important;
|
||||
--color-success-650: #0112b3 !important;
|
||||
--color-success-700: #000e99 !important;
|
||||
--color-success-750: #000b80 !important;
|
||||
--color-success-800: #000866 !important;
|
||||
--color-success-850: #00054d !important;
|
||||
--color-success-900: #000333 !important;
|
||||
--color-success-950: #00011a !important;
|
||||
|
||||
/* ---------------------------------------------------------------
|
||||
KLZ "Foundation Neutrals" → Backgrounds, cards, borders, text
|
||||
Based on tailwind.config.cjs: neutral.light=#fff,
|
||||
neutral.DEFAULT=#f8f9fa, neutral.dark=#263336, neutral.black=#0a0a0a
|
||||
text.primary=#1a1a1a, text.secondary=#6c757d, text.light=#adb5bd
|
||||
--------------------------------------------------------------- */
|
||||
--color-base-0: #ffffff !important;
|
||||
--color-base-50: #f8f9fa !important;
|
||||
--color-base-100: #f1f3f5 !important;
|
||||
--color-base-150: #e9ecef !important;
|
||||
--color-base-200: #dee2e6 !important;
|
||||
--color-base-250: #ced4da !important;
|
||||
--color-base-300: #adb5bd !important;
|
||||
--color-base-350: #9ba3ab !important;
|
||||
--color-base-400: #868e96 !important;
|
||||
--color-base-450: #6c757d !important;
|
||||
--color-base-500: #5c636a !important;
|
||||
--color-base-550: #4d5358 !important;
|
||||
--color-base-600: #3d4246 !important;
|
||||
--color-base-650: #343a40 !important;
|
||||
--color-base-700: #2b3035 !important;
|
||||
--color-base-750: #263336 !important;
|
||||
--color-base-800: #212529 !important;
|
||||
--color-base-850: #1a1a1a !important;
|
||||
--color-base-900: #121212 !important;
|
||||
--color-base-950: #0a0a0a !important;
|
||||
--color-base-1000: #000000 !important;
|
||||
|
||||
/* Typography */
|
||||
--font-body: 'Inter', system-ui, -apple-system, sans-serif !important;
|
||||
--font-headings: 'Inter', system-ui, -apple-system, sans-serif !important;
|
||||
}
|
||||
|
||||
/* Base Body Application */
|
||||
body {
|
||||
font-family: var(--font-body) !important;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
/* =================================================================
|
||||
Login / Setup Page
|
||||
================================================================= */
|
||||
.template-default.template-default--has-bg {
|
||||
background: radial-gradient(circle at top right, #e6ebf5 0%, #f8f9fa 60%, #f3f4f6 100%) !important;
|
||||
}
|
||||
|
||||
.login__wrap,
|
||||
.create-first-user__wrap {
|
||||
border-top: none !important;
|
||||
padding: 3rem !important;
|
||||
background: rgba(255, 255, 255, 0.85) !important;
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border: 1px solid var(--theme-elevation-150) !important;
|
||||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.15) !important;
|
||||
border-radius: 1.5rem !important;
|
||||
}
|
||||
|
||||
/* =================================================================
|
||||
Buttons – override Payload's dark buttons with KLZ Blue
|
||||
Payload uses .btn--style-primary { --bg-color: var(--theme-elevation-800) }
|
||||
which makes all primary buttons near-black. We override to KLZ Blue.
|
||||
================================================================= */
|
||||
.btn--style-primary,
|
||||
.btn--style-pill {
|
||||
--bg-color: #011dff !important;
|
||||
--color: #ffffff !important;
|
||||
--hover-bg: #0116cc !important;
|
||||
--hover-color: #ffffff !important;
|
||||
}
|
||||
|
||||
.btn--style-primary.btn--disabled,
|
||||
.btn--style-pill.btn--disabled {
|
||||
--bg-color: #b8bfff !important;
|
||||
--color: #ffffff !important;
|
||||
--hover-bg: #b8bfff !important;
|
||||
}
|
||||
|
||||
/* Sidebar Active Items */
|
||||
[class*="nav-group__link--active"],
|
||||
[class*="nav__link--active"] {
|
||||
--theme-elevation-800: #011dff !important;
|
||||
color: #011dff !important;
|
||||
border-left-color: #011dff !important;
|
||||
}
|
||||
|
||||
.btn--style-secondary {
|
||||
--box-shadow: inset 0 0 0 1px #011dff !important;
|
||||
--color: #011dff !important;
|
||||
--hover-color: #0116cc !important;
|
||||
--hover-box-shadow: inset 0 0 0 1px #0116cc !important;
|
||||
}
|
||||
|
||||
/* =================================================================
|
||||
Logo & Icon
|
||||
================================================================= */
|
||||
.klz-admin-logo,
|
||||
.klz-admin-icon {
|
||||
display: block !important;
|
||||
visibility: visible !important;
|
||||
opacity: 1 !important;
|
||||
height: 32px !important;
|
||||
width: auto !important;
|
||||
max-width: 100% !important;
|
||||
object-fit: contain !important;
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { Container, Badge, Heading } from '@/components/ui';
|
||||
import { getTranslations, setRequestLocale } from 'next-intl/server';
|
||||
import { Metadata } from 'next';
|
||||
import { getPageBySlug, getAllPages } from '@/lib/pages';
|
||||
import { mapSlugToFileSlug, mapFileSlugToTranslated } from '@/lib/slugs';
|
||||
import PayloadRichText from '@/components/PayloadRichText';
|
||||
import { SITE_URL } from '@/lib/schema';
|
||||
import TrackedLink from '@/components/analytics/TrackedLink';
|
||||
@@ -20,15 +21,19 @@ export async function generateMetadata({ params }: PageProps): Promise<Metadata>
|
||||
|
||||
if (!pageData) return {};
|
||||
|
||||
const fileSlug = await mapSlugToFileSlug(slug, locale);
|
||||
const deSlug = await mapFileSlugToTranslated(fileSlug, 'de');
|
||||
const enSlug = await mapFileSlugToTranslated(fileSlug, 'en');
|
||||
|
||||
return {
|
||||
title: pageData.frontmatter.title,
|
||||
description: pageData.frontmatter.excerpt || '',
|
||||
alternates: {
|
||||
canonical: `${SITE_URL}/${locale}/${slug}`,
|
||||
languages: {
|
||||
de: `${SITE_URL}/de/${slug}`,
|
||||
en: `${SITE_URL}/en/${slug}`,
|
||||
'x-default': `${SITE_URL}/en/${slug}`,
|
||||
de: `${SITE_URL}/de/${deSlug}`,
|
||||
en: `${SITE_URL}/en/${enSlug}`,
|
||||
'x-default': `${SITE_URL}/en/${enSlug}`,
|
||||
},
|
||||
},
|
||||
openGraph: {
|
||||
@@ -54,6 +59,16 @@ export default async function StandardPage({ params }: PageProps) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
// Full-bleed pages render blocks edge-to-edge without the generic article wrapper
|
||||
if (pageData.frontmatter.layout === 'fullBleed') {
|
||||
return (
|
||||
<div className="flex flex-col min-h-screen">
|
||||
<PayloadRichText data={pageData.content} className="" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Default article layout with hero, content, and support CTA
|
||||
return (
|
||||
<div className="flex flex-col min-h-screen bg-white">
|
||||
{/* Hero Section */}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ImageResponse } from 'next/og';
|
||||
import { getProductBySlug } from '@/lib/mdx';
|
||||
import { getProductBySlug } from '@/lib/products';
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
import { OGImageTemplate } from '@/components/OGImageTemplate';
|
||||
import { NextRequest } from 'next/server';
|
||||
|
||||
@@ -67,6 +67,9 @@ export default async function BlogIndex({ params }: BlogIndexProps) {
|
||||
alt={featuredPost.frontmatter.title}
|
||||
fill
|
||||
className="absolute inset-0 w-full h-full object-cover opacity-40 md:opacity-60"
|
||||
style={{
|
||||
objectPosition: `${featuredPost.frontmatter.focalX ?? 50}% ${featuredPost.frontmatter.focalY ?? 50}%`,
|
||||
}}
|
||||
sizes="100vw"
|
||||
priority
|
||||
/>
|
||||
@@ -168,6 +171,9 @@ export default async function BlogIndex({ params }: BlogIndexProps) {
|
||||
alt={post.frontmatter.title}
|
||||
fill
|
||||
className="w-full h-full object-cover transition-transform duration-1000 group-hover:scale-110"
|
||||
style={{
|
||||
objectPosition: `${post.frontmatter.focalX ?? 50}% ${post.frontmatter.focalY ?? 50}%`,
|
||||
}}
|
||||
sizes="(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 33vw"
|
||||
/>
|
||||
<div className="absolute inset-0 image-overlay-gradient opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
|
||||
@@ -192,10 +198,10 @@ export default async function BlogIndex({ params }: BlogIndexProps) {
|
||||
</span>
|
||||
{(new Date(post.frontmatter.date) > new Date() ||
|
||||
post.frontmatter.public === false) && (
|
||||
<span className="px-1.5 py-0.5 border border-current rounded-sm text-[9px] md:text-xs">
|
||||
Draft
|
||||
</span>
|
||||
)}
|
||||
<span className="px-1.5 py-0.5 border border-current rounded-sm text-[9px] md:text-xs">
|
||||
Draft
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<h3 className="text-lg md:text-2xl font-bold text-primary mb-3 md:mb-6 group-hover:text-accent-dark transition-colors line-clamp-3 md:line-clamp-4 leading-tight">
|
||||
{post.frontmatter.title}
|
||||
|
||||
@@ -7,7 +7,7 @@ import RelatedProducts from '@/components/RelatedProducts';
|
||||
import DatasheetDownload from '@/components/DatasheetDownload';
|
||||
import { Badge, Card, Container, Heading, Section } from '@/components/ui';
|
||||
import { getDatasheetPath } from '@/lib/datasheets';
|
||||
import { getAllProducts, getProductBySlug } from '@/lib/mdx';
|
||||
import { getAllProducts, getProductBySlug } from '@/lib/products';
|
||||
import { mapFileSlugToTranslated, mapSlugToFileSlug } from '@/lib/slugs';
|
||||
import { Metadata } from 'next';
|
||||
import { getTranslations, setRequestLocale } from 'next-intl/server';
|
||||
|
||||
@@ -94,6 +94,16 @@ export async function sendContactFormAction(formData: FormData) {
|
||||
logger.info('Notification email sent successfully', {
|
||||
messageId: notificationResult.messageId,
|
||||
});
|
||||
} else {
|
||||
logger.error('Notification email FAILED', {
|
||||
error: notificationResult.error,
|
||||
subject: notificationSubject,
|
||||
email,
|
||||
});
|
||||
services.errors.captureException(
|
||||
new Error(`Notification email failed: ${notificationResult.error}`),
|
||||
{ action: 'sendContactFormAction_notification', email },
|
||||
);
|
||||
}
|
||||
|
||||
// 2b. Send confirmation to Customer (branded as KLZ Cables)
|
||||
@@ -115,6 +125,16 @@ export async function sendContactFormAction(formData: FormData) {
|
||||
logger.info('Confirmation email sent successfully', {
|
||||
messageId: confirmationResult.messageId,
|
||||
});
|
||||
} else {
|
||||
logger.error('Confirmation email FAILED', {
|
||||
error: confirmationResult.error,
|
||||
subject: confirmationSubject,
|
||||
to: email,
|
||||
});
|
||||
services.errors.captureException(
|
||||
new Error(`Confirmation email failed: ${confirmationResult.error}`),
|
||||
{ action: 'sendContactFormAction_confirmation', email },
|
||||
);
|
||||
}
|
||||
|
||||
// Notify via Gotify (Internal)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { config } from '@/lib/config';
|
||||
import { MetadataRoute } from 'next';
|
||||
import { getAllProductsMetadata } from '@/lib/mdx';
|
||||
import { getAllProductsMetadata } from '@/lib/products';
|
||||
import { getAllPostsMetadata } from '@/lib/blog';
|
||||
import { getAllPagesMetadata } from '@/lib/pages';
|
||||
import { mapFileSlugToTranslated } from '@/lib/slugs';
|
||||
|
||||
@@ -137,7 +137,7 @@ export default function Header() {
|
||||
];
|
||||
|
||||
const headerClass = cn(
|
||||
'fixed top-0 left-0 right-0 z-50 transition-all duration-500 safe-area-p transform-gpu animate-in fade-in slide-in-from-top-12 fill-mode-both',
|
||||
'fixed top-0 left-0 right-0 z-50 transition-all duration-500 safe-area-p transform-gpu fill-mode-both',
|
||||
{
|
||||
'bg-primary/95 backdrop-blur-md md:bg-transparent py-3 md:py-8 shadow-2xl md:shadow-none':
|
||||
isHomePage && !isScrolled && !isMobileMenuOpen,
|
||||
@@ -153,8 +153,7 @@ export default function Header() {
|
||||
<header className={headerClass} style={{ animationDuration: '800ms' }}>
|
||||
<div className="container mx-auto px-4 md:px-12 lg:px-16 max-w-7xl flex items-center justify-between">
|
||||
<div
|
||||
className="flex-shrink-0 group touch-target animate-in fade-in zoom-in-90 fill-mode-both"
|
||||
style={{ animationDuration: '600ms', animationDelay: '100ms' }}
|
||||
className="flex-shrink-0 group touch-target fill-mode-both"
|
||||
>
|
||||
<Link
|
||||
href={`/${currentLocale}`}
|
||||
@@ -173,6 +172,9 @@ export default function Header() {
|
||||
style={{ width: 'auto' }}
|
||||
className="h-10 md:h-14 w-auto transition-all duration-500 group-hover:scale-110"
|
||||
priority
|
||||
fetchPriority="high"
|
||||
loading="eager"
|
||||
decoding="sync"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { defaultJSXConverters, RichText } from '@payloadcms/richtext-lexical/react';
|
||||
import type { JSXConverters } from '@payloadcms/richtext-lexical/react';
|
||||
import Image from 'next/image';
|
||||
import { Suspense } from 'react';
|
||||
|
||||
// Import all custom React components that were previously mapped via MDX
|
||||
import StickyNarrative from '@/components/blog/StickyNarrative';
|
||||
@@ -16,6 +17,24 @@ import Stats from '@/components/blog/Stats';
|
||||
import SplitHeading from '@/components/blog/SplitHeading';
|
||||
import ProductTabs from '@/components/ProductTabs';
|
||||
import ProductTechnicalData from '@/components/ProductTechnicalData';
|
||||
import ContactForm from '@/components/ContactForm';
|
||||
import ContactMap from '@/components/ContactMap';
|
||||
import Gallery from '@/components/team/Gallery';
|
||||
import Reveal from '@/components/Reveal';
|
||||
import { Badge, Container, Heading, Section, Card } from '@/components/ui';
|
||||
import TrackedLink from '@/components/analytics/TrackedLink';
|
||||
import { useLocale } from 'next-intl';
|
||||
|
||||
import HomeHero from '@/components/home/Hero';
|
||||
import ProductCategories from '@/components/home/ProductCategories';
|
||||
import WhatWeDo from '@/components/home/WhatWeDo';
|
||||
import RecentPosts from '@/components/home/RecentPosts';
|
||||
import Experience from '@/components/home/Experience';
|
||||
import WhyChooseUs from '@/components/home/WhyChooseUs';
|
||||
import MeetTheTeam from '@/components/home/MeetTheTeam';
|
||||
import GallerySection from '@/components/home/GallerySection';
|
||||
import VideoSection from '@/components/home/VideoSection';
|
||||
import CTA from '@/components/home/CTA';
|
||||
|
||||
const jsxConverters: JSXConverters = {
|
||||
...defaultJSXConverters,
|
||||
@@ -255,6 +274,372 @@ const jsxConverters: JSXConverters = {
|
||||
{node.fields.content && <RichText data={node.fields.content} converters={jsxConverters} />}
|
||||
</ProductTabs>
|
||||
),
|
||||
// ─── New Page Blocks ───────────────────────────────────────────
|
||||
heroSection: ({ node }: any) => {
|
||||
const f = node.fields;
|
||||
const bgSrc = f.backgroundImage?.sizes?.card?.url || f.backgroundImage?.url;
|
||||
return (
|
||||
<Reveal>
|
||||
<section className={`relative min-h-[50vh] md:min-h-[70vh] flex items-center pt-32 pb-20 md:pt-40 md:pb-32 overflow-hidden bg-primary-dark ${f.alignment === 'center' ? 'justify-center text-center' : ''}`}>
|
||||
{bgSrc && (
|
||||
<>
|
||||
<div className="absolute inset-0 z-0">
|
||||
<Image
|
||||
src={bgSrc}
|
||||
alt={f.title}
|
||||
fill
|
||||
className="object-cover opacity-30 md:opacity-40"
|
||||
style={{ objectPosition: `${f.backgroundImage?.focalX ?? 50}% ${f.backgroundImage?.focalY ?? 50}%` }}
|
||||
sizes="100vw"
|
||||
priority
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-primary-dark/80 via-primary-dark/40 to-primary-dark/80" />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<Container className={`relative z-10 ${f.alignment === 'center' ? 'max-w-5xl' : ''}`}>
|
||||
<div className={`max-w-4xl ${f.alignment === 'center' ? 'mx-auto' : ''}`}>
|
||||
{f.badge && <Badge variant="saturated" className="mb-4 md:mb-8 shadow-lg">{f.badge}</Badge>}
|
||||
<Heading level={1} className="text-white mb-4 md:mb-8">{f.title}</Heading>
|
||||
{f.subtitle && <p className="text-lg md:text-2xl text-white/70 font-medium leading-relaxed max-w-2xl">{f.subtitle}</p>}
|
||||
{f.ctaLabel && f.ctaHref && (
|
||||
<div className="mt-8">
|
||||
<a href={f.ctaHref} className="inline-flex items-center px-8 py-4 bg-accent text-primary-dark font-bold rounded-full hover:bg-white transition-all duration-300 group">
|
||||
{f.ctaLabel}
|
||||
<span className="ml-3 transition-transform group-hover:translate-x-2">→</span>
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Container>
|
||||
</section>
|
||||
</Reveal>
|
||||
);
|
||||
},
|
||||
'block-heroSection': ({ node }: any) => {
|
||||
const f = node.fields;
|
||||
const bgSrc = f.backgroundImage?.sizes?.card?.url || f.backgroundImage?.url;
|
||||
return (
|
||||
<Reveal>
|
||||
<section className={`relative min-h-[50vh] md:min-h-[70vh] flex items-center pt-32 pb-20 md:pt-40 md:pb-32 overflow-hidden bg-primary-dark ${f.alignment === 'center' ? 'justify-center text-center' : ''}`}>
|
||||
{bgSrc && (
|
||||
<>
|
||||
<div className="absolute inset-0 z-0">
|
||||
<Image src={bgSrc} alt={f.title} fill className="object-cover opacity-30 md:opacity-40" style={{ objectPosition: `${f.backgroundImage?.focalX ?? 50}% ${f.backgroundImage?.focalY ?? 50}%` }} sizes="100vw" priority />
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-primary-dark/80 via-primary-dark/40 to-primary-dark/80" />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<Container className={`relative z-10 ${f.alignment === 'center' ? 'max-w-5xl' : ''}`}>
|
||||
<div className={`max-w-4xl ${f.alignment === 'center' ? 'mx-auto' : ''}`}>
|
||||
{f.badge && <Badge variant="saturated" className="mb-4 md:mb-8 shadow-lg">{f.badge}</Badge>}
|
||||
<Heading level={1} className="text-white mb-4 md:mb-8">{f.title}</Heading>
|
||||
{f.subtitle && <p className="text-lg md:text-2xl text-white/70 font-medium leading-relaxed max-w-2xl">{f.subtitle}</p>}
|
||||
{f.ctaLabel && f.ctaHref && (
|
||||
<div className="mt-8">
|
||||
<a href={f.ctaHref} className="inline-flex items-center px-8 py-4 bg-accent text-primary-dark font-bold rounded-full hover:bg-white transition-all duration-300 group">{f.ctaLabel}<span className="ml-3 transition-transform group-hover:translate-x-2">→</span></a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Container>
|
||||
</section>
|
||||
</Reveal>
|
||||
);
|
||||
},
|
||||
teamProfile: ({ node }: any) => {
|
||||
const f = node.fields;
|
||||
const imgSrc = f.image?.sizes?.card?.url || f.image?.url;
|
||||
const isDark = f.colorScheme === 'dark';
|
||||
const isImageRight = f.layout === 'imageRight';
|
||||
return (
|
||||
<article className="relative bg-white overflow-hidden">
|
||||
<div className="flex flex-col lg:flex-row">
|
||||
<Reveal className={`w-full lg:w-1/2 p-6 md:p-24 lg:p-32 flex flex-col justify-center relative ${isImageRight ? 'order-2 lg:order-1' : 'order-2'} ${isDark ? 'bg-primary-dark text-white' : 'bg-neutral-light text-saturated'}`}>
|
||||
<div className="relative z-10">
|
||||
<Badge variant={isDark ? 'accent' : 'saturated'} className="mb-4 md:mb-8">{f.role}</Badge>
|
||||
<Heading level={2} className={`${isDark ? 'text-white' : 'text-saturated'} mb-6 md:mb-10 text-3xl md:text-5xl`}>{f.name}</Heading>
|
||||
{f.quote && (
|
||||
<div className="relative mb-6 md:mb-12">
|
||||
<div className={`absolute -left-4 md:-left-8 top-0 bottom-0 w-1 md:w-1.5 ${isDark ? 'bg-accent' : 'bg-saturated'} rounded-full`} />
|
||||
<p className={`text-lg md:text-2xl font-bold italic leading-relaxed pl-5 md:pl-8 ${isDark ? 'text-white/90' : 'text-text-secondary'}`}>{f.quote}</p>
|
||||
</div>
|
||||
)}
|
||||
{f.description && <p className={`text-base md:text-xl leading-relaxed mb-6 md:mb-12 max-w-xl ${isDark ? 'text-white/70' : 'text-text-secondary'}`}>{f.description}</p>}
|
||||
{f.linkedinUrl && (
|
||||
<TrackedLink
|
||||
href={f.linkedinUrl}
|
||||
className={`inline-flex items-center px-8 py-4 font-bold rounded-full transition-all duration-300 group ${isDark ? 'bg-accent text-primary-dark hover:bg-white' : 'bg-saturated text-white hover:bg-primary'}`}
|
||||
eventProperties={{ type: 'social_linkedin', person: f.name, location: 'team_page' }}
|
||||
>
|
||||
{f.linkedinLabel || 'LinkedIn'}
|
||||
<span className="ml-3 transition-transform group-hover:translate-x-2">→</span>
|
||||
</TrackedLink>
|
||||
)}
|
||||
</div>
|
||||
</Reveal>
|
||||
<Reveal className={`w-full lg:w-1/2 relative min-h-[400px] md:min-h-[600px] lg:min-h-screen overflow-hidden ${isImageRight ? 'order-1 lg:order-2' : 'order-1'}`}>
|
||||
{imgSrc && (
|
||||
<>
|
||||
<Image
|
||||
src={imgSrc}
|
||||
alt={f.name}
|
||||
fill
|
||||
className="object-cover scale-105 hover:scale-100 transition-transform duration-1000"
|
||||
style={{ objectPosition: `${f.image?.focalX ?? 50}% ${f.image?.focalY ?? 50}%` }}
|
||||
sizes="(max-width: 1024px) 100vw, 50vw"
|
||||
/>
|
||||
<div className={`absolute inset-0 ${isDark ? 'bg-gradient-to-t from-primary-dark/60 lg:bg-gradient-to-r lg:from-primary-dark/20 to-transparent' : 'bg-gradient-to-t from-white/60 lg:bg-gradient-to-l lg:from-primary-dark/20 to-transparent'}`} />
|
||||
</>
|
||||
)}
|
||||
</Reveal>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
},
|
||||
'block-teamProfile': ({ node }: any) => {
|
||||
const f = node.fields;
|
||||
const imgSrc = f.image?.sizes?.card?.url || f.image?.url;
|
||||
const isDark = f.colorScheme === 'dark';
|
||||
const isImageRight = f.layout === 'imageRight';
|
||||
return (
|
||||
<article className="relative bg-white overflow-hidden">
|
||||
<div className="flex flex-col lg:flex-row">
|
||||
<Reveal className={`w-full lg:w-1/2 p-6 md:p-24 lg:p-32 flex flex-col justify-center relative ${isImageRight ? 'order-2 lg:order-1' : 'order-2'} ${isDark ? 'bg-primary-dark text-white' : 'bg-neutral-light text-saturated'}`}>
|
||||
<div className="relative z-10">
|
||||
<Badge variant={isDark ? 'accent' : 'saturated'} className="mb-4 md:mb-8">{f.role}</Badge>
|
||||
<Heading level={2} className={`${isDark ? 'text-white' : 'text-saturated'} mb-6 md:mb-10 text-3xl md:text-5xl`}>{f.name}</Heading>
|
||||
{f.quote && (
|
||||
<div className="relative mb-6 md:mb-12">
|
||||
<div className={`absolute -left-4 md:-left-8 top-0 bottom-0 w-1 md:w-1.5 ${isDark ? 'bg-accent' : 'bg-saturated'} rounded-full`} />
|
||||
<p className={`text-lg md:text-2xl font-bold italic leading-relaxed pl-5 md:pl-8 ${isDark ? 'text-white/90' : 'text-text-secondary'}`}>{f.quote}</p>
|
||||
</div>
|
||||
)}
|
||||
{f.description && <p className={`text-base md:text-xl leading-relaxed mb-6 md:mb-12 max-w-xl ${isDark ? 'text-white/70' : 'text-text-secondary'}`}>{f.description}</p>}
|
||||
{f.linkedinUrl && (
|
||||
<TrackedLink href={f.linkedinUrl} className={`inline-flex items-center px-8 py-4 font-bold rounded-full transition-all duration-300 group ${isDark ? 'bg-accent text-primary-dark hover:bg-white' : 'bg-saturated text-white hover:bg-primary'}`} eventProperties={{ type: 'social_linkedin', person: f.name, location: 'team_page' }}>
|
||||
{f.linkedinLabel || 'LinkedIn'}<span className="ml-3 transition-transform group-hover:translate-x-2">→</span>
|
||||
</TrackedLink>
|
||||
)}
|
||||
</div>
|
||||
</Reveal>
|
||||
<Reveal className={`w-full lg:w-1/2 relative min-h-[400px] md:min-h-[600px] lg:min-h-screen overflow-hidden ${isImageRight ? 'order-1 lg:order-2' : 'order-1'}`}>
|
||||
{imgSrc && (<><Image src={imgSrc} alt={f.name} fill className="object-cover scale-105 hover:scale-100 transition-transform duration-1000" style={{ objectPosition: `${f.image?.focalX ?? 50}% ${f.image?.focalY ?? 50}%` }} sizes="(max-width: 1024px) 100vw, 50vw" /><div className={`absolute inset-0 ${isDark ? 'bg-gradient-to-t from-primary-dark/60 lg:bg-gradient-to-r lg:from-primary-dark/20 to-transparent' : 'bg-gradient-to-t from-white/60 lg:bg-gradient-to-l lg:from-primary-dark/20 to-transparent'}`} /></>)}
|
||||
</Reveal>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
},
|
||||
contactSection: ({ node }: any) => {
|
||||
const f = node.fields;
|
||||
return (
|
||||
<Section className="bg-neutral-light py-12 md:py-28">
|
||||
<Container>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 md:gap-16">
|
||||
{f.showForm && (
|
||||
<div className="lg:col-span-7">
|
||||
<Suspense fallback={<div className="animate-pulse bg-neutral-medium h-96 rounded-2xl md:rounded-3xl" />}>
|
||||
<ContactForm />
|
||||
</Suspense>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Container>
|
||||
{f.showMap && (
|
||||
<section className="mt-12 h-[300px] md:h-[500px] bg-neutral-medium relative overflow-hidden grayscale hover:grayscale-0 transition-all duration-1000">
|
||||
<Suspense fallback={<div className="h-full w-full bg-neutral-medium animate-pulse" />}>
|
||||
<ContactMap address="Raiffeisenstraße 22\n73630 Remshalden" lat={48.8144} lng={9.4144} />
|
||||
</Suspense>
|
||||
</section>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
},
|
||||
'block-contactSection': ({ node }: any) => {
|
||||
const f = node.fields;
|
||||
return (
|
||||
<Section className="bg-neutral-light py-12 md:py-28">
|
||||
<Container>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 md:gap-16">
|
||||
{f.showForm && (
|
||||
<div className="lg:col-span-7">
|
||||
<Suspense fallback={<div className="animate-pulse bg-neutral-medium h-96 rounded-2xl md:rounded-3xl" />}>
|
||||
<ContactForm />
|
||||
</Suspense>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Container>
|
||||
{f.showMap && (
|
||||
<section className="mt-12 h-[300px] md:h-[500px] bg-neutral-medium relative overflow-hidden grayscale hover:grayscale-0 transition-all duration-1000">
|
||||
<Suspense fallback={<div className="h-full w-full bg-neutral-medium animate-pulse" />}>
|
||||
<ContactMap address="Raiffeisenstraße 22\n73630 Remshalden" lat={48.8144} lng={9.4144} />
|
||||
</Suspense>
|
||||
</section>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
},
|
||||
imageGallery: ({ node }: any) => <Gallery />,
|
||||
'block-imageGallery': ({ node }: any) => <Gallery />,
|
||||
categoryGrid: ({ node }: any) => {
|
||||
const cats = node.fields.categories || [];
|
||||
return (
|
||||
<Section className="bg-neutral-light relative">
|
||||
<Container>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 md:gap-8 lg:gap-12">
|
||||
{cats.map((cat: any, idx: number) => {
|
||||
const imgSrc = cat.image?.sizes?.card?.url || cat.image?.url;
|
||||
const iconSrc = cat.icon?.url;
|
||||
return (
|
||||
<Reveal key={idx} delay={idx * 100}>
|
||||
<a href={cat.href} className="group block">
|
||||
<Card className="h-full border-none shadow-sm hover:shadow-2xl transition-all duration-500 rounded-[24px] md:rounded-[48px] overflow-hidden bg-white active:scale-[0.98]">
|
||||
<div className="relative h-[200px] md:h-[400px] overflow-hidden">
|
||||
{imgSrc && (
|
||||
<Image src={imgSrc} alt={cat.title} fill className="object-cover transition-transform duration-1000 group-hover:scale-105" style={{ objectPosition: `${cat.image?.focalX ?? 50}% ${cat.image?.focalY ?? 50}%` }} sizes="(max-width: 768px) 100vw, 50vw" />
|
||||
)}
|
||||
<div className="absolute inset-0 image-overlay-gradient opacity-80 group-hover:opacity-90 transition-opacity duration-500" />
|
||||
{iconSrc && (
|
||||
<div className="absolute top-3 right-3 md:top-8 md:right-8 w-10 h-10 md:w-20 md:h-20 bg-white/10 backdrop-blur-md rounded-xl md:rounded-[24px] flex items-center justify-center border border-white/20 shadow-2xl transition-all duration-500 group-hover:scale-110 group-hover:bg-white/20">
|
||||
<Image src={iconSrc} alt="" width={24} height={24} className="w-6 h-6 md:w-12 md:h-12 brightness-0 invert opacity-80" />
|
||||
</div>
|
||||
)}
|
||||
<div className="absolute bottom-4 left-4 md:bottom-10 md:left-10 right-4 md:right-10">
|
||||
<h2 className="text-xl md:text-4xl font-bold text-white leading-tight">{cat.title}</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-5 md:p-10">
|
||||
{cat.description && <p className="text-text-secondary text-sm md:text-lg leading-relaxed mb-4 md:mb-8 line-clamp-2 md:line-clamp-none">{cat.description}</p>}
|
||||
<div className="flex items-center text-saturated font-bold text-base md:text-lg group-hover:text-accent-dark transition-colors">
|
||||
<span className="border-b-2 border-saturated/10 group-hover:border-accent-dark transition-colors pb-1">{cat.ctaLabel || 'View Products'}</span>
|
||||
<div className="ml-3 md:ml-4 w-8 h-8 md:w-10 md:h-10 rounded-full bg-primary-light flex items-center justify-center text-saturated group-hover:bg-accent group-hover:text-primary-dark transition-all duration-300 shadow-sm">
|
||||
<svg className="w-4 h-4 md:w-5 md:h-5 transition-transform group-hover:translate-x-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 8l4 4m0 0l-4 4m4-4H3" /></svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</a>
|
||||
</Reveal>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Container>
|
||||
</Section>
|
||||
);
|
||||
},
|
||||
'block-categoryGrid': ({ node }: any) => {
|
||||
const cats = node.fields.categories || [];
|
||||
return (
|
||||
<Section className="bg-neutral-light relative">
|
||||
<Container>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 md:gap-8 lg:gap-12">
|
||||
{cats.map((cat: any, idx: number) => {
|
||||
const imgSrc = cat.image?.sizes?.card?.url || cat.image?.url;
|
||||
const iconSrc = cat.icon?.url;
|
||||
return (
|
||||
<Reveal key={idx} delay={idx * 100}>
|
||||
<a href={cat.href} className="group block">
|
||||
<Card className="h-full border-none shadow-sm hover:shadow-2xl transition-all duration-500 rounded-[24px] md:rounded-[48px] overflow-hidden bg-white active:scale-[0.98]">
|
||||
<div className="relative h-[200px] md:h-[400px] overflow-hidden">
|
||||
{imgSrc && (<Image src={imgSrc} alt={cat.title} fill className="object-cover transition-transform duration-1000 group-hover:scale-105" style={{ objectPosition: `${cat.image?.focalX ?? 50}% ${cat.image?.focalY ?? 50}%` }} sizes="(max-width: 768px) 100vw, 50vw" />)}
|
||||
<div className="absolute inset-0 image-overlay-gradient opacity-80 group-hover:opacity-90 transition-opacity duration-500" />
|
||||
{iconSrc && (<div className="absolute top-3 right-3 md:top-8 md:right-8 w-10 h-10 md:w-20 md:h-20 bg-white/10 backdrop-blur-md rounded-xl md:rounded-[24px] flex items-center justify-center border border-white/20"><Image src={iconSrc} alt="" width={24} height={24} className="w-6 h-6 md:w-12 md:h-12 brightness-0 invert opacity-80" /></div>)}
|
||||
<div className="absolute bottom-4 left-4 md:bottom-10 md:left-10 right-4 md:right-10"><h2 className="text-xl md:text-4xl font-bold text-white leading-tight">{cat.title}</h2></div>
|
||||
</div>
|
||||
<div className="p-5 md:p-10">
|
||||
{cat.description && <p className="text-text-secondary text-sm md:text-lg leading-relaxed mb-4 md:mb-8">{cat.description}</p>}
|
||||
<div className="flex items-center text-saturated font-bold text-base md:text-lg group-hover:text-accent-dark transition-colors"><span>{cat.ctaLabel || 'View Products'}</span></div>
|
||||
</div>
|
||||
</Card>
|
||||
</a>
|
||||
</Reveal>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Container>
|
||||
</Section>
|
||||
);
|
||||
},
|
||||
manifestoGrid: ({ node }: any) => {
|
||||
const f = node.fields;
|
||||
return (
|
||||
<Section className="bg-white text-primary py-16 md:py-28">
|
||||
<Container>
|
||||
<div className="sticky-narrative-container">
|
||||
<div className="sticky-narrative-sidebar mb-8 lg:mb-0">
|
||||
<div className="lg:sticky lg:top-32">
|
||||
{f.title && <Heading level={2} subtitle={f.subtitle}>{f.title}</Heading>}
|
||||
{f.tagline && <p className="text-base md:text-xl text-text-secondary leading-relaxed">{f.tagline}</p>}
|
||||
</div>
|
||||
</div>
|
||||
<ul className="sticky-narrative-content grid grid-cols-1 md:grid-cols-2 gap-4 md:gap-10 list-none p-0 m-0">
|
||||
{(f.items || []).map((item: any, idx: number) => (
|
||||
<li key={idx} className="p-6 md:p-10 bg-neutral-light rounded-2xl md:rounded-[40px] border border-transparent hover:border-accent hover:bg-white hover:shadow-2xl transition-all duration-500 group active:scale-[0.98]">
|
||||
<div className="w-10 h-10 md:w-16 md:h-16 bg-white rounded-xl md:rounded-2xl flex items-center justify-center mb-4 md:mb-8 shadow-sm group-hover:bg-accent transition-colors duration-500">
|
||||
<span className="text-primary font-extrabold text-lg md:text-2xl group-hover:text-primary-dark">0{idx + 1}</span>
|
||||
</div>
|
||||
<h3 className="text-lg md:text-2xl font-bold mb-2 md:mb-4 text-primary">{item.title}</h3>
|
||||
<p className="text-sm md:text-lg text-text-secondary leading-relaxed">{item.description}</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</Container>
|
||||
</Section>
|
||||
);
|
||||
},
|
||||
'block-manifestoGrid': ({ node }: any) => {
|
||||
const f = node.fields;
|
||||
return (
|
||||
<Section className="bg-white text-primary py-16 md:py-28">
|
||||
<Container>
|
||||
<div className="sticky-narrative-container">
|
||||
<div className="sticky-narrative-sidebar mb-8 lg:mb-0">
|
||||
<div className="lg:sticky lg:top-32">
|
||||
{f.title && <Heading level={2} subtitle={f.subtitle}>{f.title}</Heading>}
|
||||
{f.tagline && <p className="text-base md:text-xl text-text-secondary leading-relaxed">{f.tagline}</p>}
|
||||
</div>
|
||||
</div>
|
||||
<ul className="sticky-narrative-content grid grid-cols-1 md:grid-cols-2 gap-4 md:gap-10 list-none p-0 m-0">
|
||||
{(f.items || []).map((item: any, idx: number) => (
|
||||
<li key={idx} className="p-6 md:p-10 bg-neutral-light rounded-2xl md:rounded-[40px] border border-transparent hover:border-accent hover:bg-white hover:shadow-2xl transition-all duration-500 group active:scale-[0.98]">
|
||||
<div className="w-10 h-10 md:w-16 md:h-16 bg-white rounded-xl md:rounded-2xl flex items-center justify-center mb-4 md:mb-8 shadow-sm group-hover:bg-accent transition-colors duration-500">
|
||||
<span className="text-primary font-extrabold text-lg md:text-2xl group-hover:text-primary-dark">0{idx + 1}</span>
|
||||
</div>
|
||||
<h3 className="text-lg md:text-2xl font-bold mb-2 md:mb-4 text-primary">{item.title}</h3>
|
||||
<p className="text-sm md:text-lg text-text-secondary leading-relaxed">{item.description}</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</Container>
|
||||
</Section>
|
||||
);
|
||||
},
|
||||
homeHero: ({ node }: any) => {
|
||||
console.log('[PayloadRichText] Rendering homeHero block');
|
||||
return <HomeHero data={node?.fields} />;
|
||||
},
|
||||
'block-homeHero': ({ node }: any) => {
|
||||
console.log('[PayloadRichText] Rendering block-homeHero block');
|
||||
return <HomeHero data={node?.fields} />;
|
||||
},
|
||||
homeProductCategories: ({ node }: any) => <Reveal><ProductCategories data={node?.fields} /></Reveal>,
|
||||
'block-homeProductCategories': ({ node }: any) => <Reveal><ProductCategories data={node?.fields} /></Reveal>,
|
||||
homeWhatWeDo: ({ node }: any) => <Reveal><WhatWeDo data={node?.fields} /></Reveal>,
|
||||
'block-homeWhatWeDo': ({ node }: any) => <Reveal><WhatWeDo data={node?.fields} /></Reveal>,
|
||||
homeExperience: ({ node }: any) => <Reveal><Experience data={node?.fields} /></Reveal>,
|
||||
'block-homeExperience': ({ node }: any) => <Reveal><Experience data={node?.fields} /></Reveal>,
|
||||
homeWhyChooseUs: ({ node }: any) => <Reveal><WhyChooseUs data={node?.fields} /></Reveal>,
|
||||
'block-homeWhyChooseUs': ({ node }: any) => <Reveal><WhyChooseUs data={node?.fields} /></Reveal>,
|
||||
homeMeetTheTeam: ({ node }: any) => <Reveal><MeetTheTeam data={node?.fields} /></Reveal>,
|
||||
'block-homeMeetTheTeam': ({ node }: any) => <Reveal><MeetTheTeam data={node?.fields} /></Reveal>,
|
||||
homeGallery: ({ node }: any) => <Reveal><GallerySection data={node?.fields} /></Reveal>,
|
||||
'block-homeGallery': ({ node }: any) => <Reveal><GallerySection data={node?.fields} /></Reveal>,
|
||||
homeVideo: ({ node }: any) => <Reveal><VideoSection data={node?.fields} /></Reveal>,
|
||||
'block-homeVideo': ({ node }: any) => <Reveal><VideoSection data={node?.fields} /></Reveal>,
|
||||
homeCTA: ({ node }: any) => <Reveal className="content-visibility-auto"><CTA data={node?.fields} /></Reveal>,
|
||||
'block-homeCTA': ({ node }: any) => <Reveal className="content-visibility-auto"><CTA data={node?.fields} /></Reveal>,
|
||||
},
|
||||
// Custom converter for the Payload "upload" Lexical node (Media collection)
|
||||
// This natively reconstructs Next.js <Image /> tags pointing to the focal-point cropped sizes
|
||||
@@ -297,12 +682,29 @@ const jsxConverters: JSXConverters = {
|
||||
},
|
||||
};
|
||||
|
||||
export default function PayloadRichText({ data }: { data: any }) {
|
||||
export default function PayloadRichText({
|
||||
data,
|
||||
className = 'article-content max-w-none',
|
||||
}: {
|
||||
data: any;
|
||||
className?: string;
|
||||
}) {
|
||||
const locale = useLocale();
|
||||
|
||||
if (!data) return null;
|
||||
|
||||
const dynamicConverters: JSXConverters = {
|
||||
...jsxConverters,
|
||||
blocks: {
|
||||
...jsxConverters.blocks,
|
||||
homeRecentPosts: () => <Reveal><RecentPosts locale={locale} /></Reveal>,
|
||||
'block-homeRecentPosts': () => <Reveal><RecentPosts locale={locale} /></Reveal>,
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="article-content max-w-none">
|
||||
<RichText data={data} converters={jsxConverters} />
|
||||
<div className={className}>
|
||||
<RichText data={data} converters={dynamicConverters} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getAllProducts } from '@/lib/mdx';
|
||||
import { getAllProducts } from '@/lib/products';
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
import Image from 'next/image';
|
||||
import { RelatedProductLink } from './RelatedProductLink';
|
||||
|
||||
@@ -30,8 +30,11 @@ export default function PostNavigation({
|
||||
{/* Background Image */}
|
||||
{prev.frontmatter.featuredImage ? (
|
||||
<div
|
||||
className="absolute inset-0 bg-cover bg-center transition-transform duration-700 group-hover:scale-110"
|
||||
style={{ backgroundImage: `url(${prev.frontmatter.featuredImage.split('?')[0]})` }}
|
||||
className="absolute inset-0 z-0 bg-cover transition-transform duration-1000 group-hover:scale-105"
|
||||
style={{
|
||||
backgroundImage: `url(${prev.frontmatter.featuredImage.split('?')[0]})`,
|
||||
backgroundPosition: `${prev.frontmatter.focalX ?? 50}% ${prev.frontmatter.focalY ?? 50}%`,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div className="absolute inset-0 bg-neutral-100" />
|
||||
@@ -81,8 +84,11 @@ export default function PostNavigation({
|
||||
{/* Background Image */}
|
||||
{next.frontmatter.featuredImage ? (
|
||||
<div
|
||||
className="absolute inset-0 bg-cover bg-center transition-transform duration-700 group-hover:scale-110"
|
||||
style={{ backgroundImage: `url(${next.frontmatter.featuredImage.split('?')[0]})` }}
|
||||
className="absolute inset-0 z-0 bg-cover transition-transform duration-1000 group-hover:scale-105"
|
||||
style={{
|
||||
backgroundImage: `url(${next.frontmatter.featuredImage.split('?')[0]})`,
|
||||
backgroundPosition: `${next.frontmatter.focalX ?? 50}% ${next.frontmatter.focalY ?? 50}%`,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div className="absolute inset-0 bg-neutral-100" />
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import { useTranslations, useLocale } from 'next-intl';
|
||||
import { Section, Container, Button, Heading } from '../../components/ui';
|
||||
|
||||
export default function CTA() {
|
||||
export default function CTA({ data }: { data?: any }) {
|
||||
const t = useTranslations('Home.cta');
|
||||
const locale = useLocale();
|
||||
|
||||
@@ -10,20 +10,20 @@ export default function CTA() {
|
||||
<Section className="bg-primary text-white py-32 relative overflow-hidden">
|
||||
<div className="absolute top-0 right-0 w-1/3 h-full bg-accent/5 -skew-x-12 translate-x-1/2" />
|
||||
<div className="absolute bottom-0 left-0 w-1/4 h-1/2 bg-primary/10 rounded-full blur-3xl -translate-x-1/2 translate-y-1/2" />
|
||||
|
||||
|
||||
<Container className="relative z-10">
|
||||
<div className="flex flex-col lg:flex-row items-center justify-between gap-16">
|
||||
<div className="max-w-3xl text-center lg:text-left">
|
||||
<Heading level={2} subtitle={t('subtitle')} className="text-white mb-6">
|
||||
<span className="text-white">{t('title')}</span>
|
||||
<Heading level={2} subtitle={data?.subtitle || t('subtitle')} className="text-white mb-6">
|
||||
<span className="text-white">{data?.title || t('title')}</span>
|
||||
</Heading>
|
||||
<p className="text-lg md:text-xl text-white/70 leading-relaxed">
|
||||
{t('description')}
|
||||
{data?.description || t('description')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex-shrink-0">
|
||||
<Button href={`/${locale}/contact`} variant="accent" size="xl" className="group px-12">
|
||||
{t('button')}
|
||||
{data?.buttonLabel || t('button')}
|
||||
<span className="ml-3 transition-transform group-hover:translate-x-2">→</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@ import Image from 'next/image';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { Section, Container, Heading } from '../../components/ui';
|
||||
|
||||
export default function Experience() {
|
||||
export default function Experience({ data }: { data?: any }) {
|
||||
const t = useTranslations('Home.experience');
|
||||
|
||||
return (
|
||||
@@ -11,7 +11,7 @@ export default function Experience() {
|
||||
<div className="absolute inset-0 z-0">
|
||||
<Image
|
||||
src="/uploads/2024/12/1694273920124-copy-2.webp"
|
||||
alt={t('subtitle')}
|
||||
alt={data?.subtitle || t('subtitle')}
|
||||
fill
|
||||
className="object-cover object-center scale-105 animate-slow-zoom"
|
||||
sizes="100vw"
|
||||
@@ -22,31 +22,31 @@ export default function Experience() {
|
||||
|
||||
<Container className="relative z-10">
|
||||
<div className="max-w-3xl">
|
||||
<Heading level={2} subtitle={t('subtitle')} className="text-white">
|
||||
<span className="text-white">{t('title')}</span>
|
||||
<Heading level={2} subtitle={data?.subtitle || t('subtitle')} className="text-white">
|
||||
<span className="text-white">{data?.title || t('title')}</span>
|
||||
</Heading>
|
||||
<div className="space-y-8 text-lg md:text-xl text-white/90 leading-relaxed font-medium">
|
||||
<p className="border-l-4 border-accent pl-8 py-2 bg-white/5 backdrop-blur-sm rounded-r-xl">
|
||||
{t('p1')}
|
||||
{data?.paragraph1 || t('p1')}
|
||||
</p>
|
||||
<p className="pl-9">{t('p2')}</p>
|
||||
<p className="pl-9">{data?.paragraph2 || t('p2')}</p>
|
||||
</div>
|
||||
|
||||
<dl className="mt-16 grid grid-cols-1 md:grid-cols-2 gap-12">
|
||||
<div className="animate-fade-in">
|
||||
<dt className="text-2xl md:text-3xl font-extrabold text-accent mb-4">
|
||||
{t('certifiedQuality')}
|
||||
{data?.badge1 || t('certifiedQuality')}
|
||||
</dt>
|
||||
<dd className="text-base md:text-lg font-bold uppercase tracking-widest text-white/60">
|
||||
{t('vdeApproved')}
|
||||
{data?.badge1Text || t('vdeApproved')}
|
||||
</dd>
|
||||
</div>
|
||||
<div className="animate-fade-in" style={{ animationDelay: '100ms' }}>
|
||||
<dt className="text-2xl md:text-3xl font-extrabold text-accent mb-4">
|
||||
{t('fullSpectrum')}
|
||||
{data?.badge2 || t('fullSpectrum')}
|
||||
</dt>
|
||||
<dd className="text-base md:text-lg font-bold uppercase tracking-widest text-white/60">
|
||||
{t('solutionsRange')}
|
||||
{data?.badge2Text || t('solutionsRange')}
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
@@ -7,7 +7,7 @@ import dynamic from 'next/dynamic';
|
||||
const Lightbox = dynamic(() => import('../Lightbox'), { ssr: false });
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
|
||||
export default function GallerySection() {
|
||||
export default function GallerySection({ data }: { data?: any }) {
|
||||
const t = useTranslations('Home.gallery');
|
||||
const searchParams = useSearchParams();
|
||||
const images = [
|
||||
@@ -26,8 +26,8 @@ export default function GallerySection() {
|
||||
return (
|
||||
<Section className="bg-white text-white py-32">
|
||||
<Container>
|
||||
<Heading level={2} subtitle={t('subtitle')} align="center">
|
||||
{t('title')}
|
||||
<Heading level={2} subtitle={data?.subtitle || t('subtitle')} align="center">
|
||||
{data?.title || t('title')}
|
||||
</Heading>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
|
||||
@@ -8,7 +8,7 @@ import { useAnalytics } from '../analytics/useAnalytics';
|
||||
import { AnalyticsEvents } from '../analytics/analytics-events';
|
||||
const HeroIllustration = dynamic(() => import('./HeroIllustration'), { ssr: false });
|
||||
|
||||
export default function Hero() {
|
||||
export default function Hero({ data }: { data?: any }) {
|
||||
const t = useTranslations('Home.hero');
|
||||
const locale = useLocale();
|
||||
const { trackEvent } = useAnalytics();
|
||||
@@ -22,24 +22,28 @@ export default function Hero() {
|
||||
level={1}
|
||||
className="text-center md:text-left mb-6 md:mb-8 md:max-w-none text-white text-4xl sm:text-5xl md:text-7xl font-extrabold [text-shadow:_-2px_-2px_0_#002b49,_2px_-2px_0_#002b49,_-2px_2px_0_#002b49,_2px_2px_0_#002b49,_-2px_0_0_#002b49,_2px_0_0_#002b49,_0_-2px_0_#002b49,_0_2px_0_#002b49]"
|
||||
>
|
||||
{t.rich('title', {
|
||||
green: (chunks) => (
|
||||
<span className="relative inline-block">
|
||||
<span className="relative z-10 text-accent italic inline-block">{chunks}</span>
|
||||
<div
|
||||
className="w-[140%] h-[140%] -top-[20%] -left-[20%] text-accent/30 hidden md:block absolute -z-10 animate-in fade-in zoom-in-0 duration-1000 ease-out fill-mode-both"
|
||||
style={{ animationDelay: '500ms' }}
|
||||
>
|
||||
<Scribble variant="circle" />
|
||||
</div>
|
||||
</span>
|
||||
),
|
||||
})}
|
||||
{data?.title ? (
|
||||
<span dangerouslySetInnerHTML={{ __html: data.title.replace(/<green>/g, '<span class="relative inline-block"><span class="relative z-10 text-accent italic inline-block">').replace(/<\/green>/g, '</span><div class="w-[140%] h-[140%] -top-[20%] -left-[20%] text-accent/30 hidden md:block absolute -z-10 animate-in fade-in zoom-in-0 duration-1000 ease-out fill-mode-both" style="animation-delay: 500ms;"><Scribble variant="circle" /></div></span>') }} />
|
||||
) : (
|
||||
t.rich('title', {
|
||||
green: (chunks) => (
|
||||
<span className="relative inline-block">
|
||||
<span className="relative z-10 text-accent italic inline-block">{chunks}</span>
|
||||
<div
|
||||
className="w-[140%] h-[140%] -top-[20%] -left-[20%] text-accent/30 hidden md:block absolute -z-10 animate-in fade-in zoom-in-0 duration-1000 ease-out fill-mode-both"
|
||||
style={{ animationDelay: '500ms' }}
|
||||
>
|
||||
<Scribble variant="circle" />
|
||||
</div>
|
||||
</span>
|
||||
),
|
||||
})
|
||||
)}
|
||||
</Heading>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-lg md:text-xl text-white leading-relaxed max-w-2xl mb-10 md:mb-12">
|
||||
{t('subtitle')}
|
||||
{data?.subtitle || t('subtitle')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col sm:flex-row justify-center md:justify-start gap-4 md:gap-6">
|
||||
@@ -51,12 +55,12 @@ export default function Hero() {
|
||||
className="group w-full sm:w-auto h-14 md:h-16 px-8 md:px-10 text-base md:text-lg hover:scale-105 transition-transform"
|
||||
onClick={() =>
|
||||
trackEvent(AnalyticsEvents.BUTTON_CLICK, {
|
||||
label: t('cta'),
|
||||
label: data?.ctaLabel || t('cta'),
|
||||
location: 'home_hero_primary',
|
||||
})
|
||||
}
|
||||
>
|
||||
{t('cta')}
|
||||
{data?.ctaLabel || t('cta')}
|
||||
<span className="transition-transform group-hover/btn:translate-x-1 ml-2">
|
||||
→
|
||||
</span>
|
||||
@@ -70,12 +74,12 @@ export default function Hero() {
|
||||
className="group w-full sm:w-auto h-14 md:h-16 px-8 md:px-10 text-base md:text-lg md:bg-white md:text-primary md:hover:bg-neutral-light md:border-none hover:scale-105 transition-transform"
|
||||
onClick={() =>
|
||||
trackEvent(AnalyticsEvents.BUTTON_CLICK, {
|
||||
label: t('exploreProducts'),
|
||||
label: data?.secondaryCtaLabel || t('exploreProducts'),
|
||||
location: 'home_hero_secondary',
|
||||
})
|
||||
}
|
||||
>
|
||||
{t('exploreProducts')}
|
||||
{data?.secondaryCtaLabel || t('exploreProducts')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@ import Image from 'next/image';
|
||||
import { useTranslations, useLocale } from 'next-intl';
|
||||
import { Section, Container, Button, Heading } from '../../components/ui';
|
||||
|
||||
export default function MeetTheTeam() {
|
||||
export default function MeetTheTeam({ data }: { data?: any }) {
|
||||
const t = useTranslations('Home.meetTheTeam');
|
||||
const teamT = useTranslations('Team');
|
||||
const locale = useLocale();
|
||||
@@ -13,7 +13,7 @@ export default function MeetTheTeam() {
|
||||
<div className="absolute inset-0 z-0">
|
||||
<Image
|
||||
src="/uploads/2024/12/DSC08036-Large.webp"
|
||||
alt={t('subtitle')}
|
||||
alt={data?.subtitle || t('subtitle')}
|
||||
fill
|
||||
className="object-cover scale-105 animate-slow-zoom"
|
||||
sizes="100vw"
|
||||
@@ -24,20 +24,20 @@ export default function MeetTheTeam() {
|
||||
|
||||
<Container className="relative z-10">
|
||||
<div className="max-w-3xl text-white animate-slide-up">
|
||||
<Heading level={2} subtitle={t('subtitle')} className="text-white mb-8">
|
||||
<span className="text-white">{t('title')}</span>
|
||||
<Heading level={2} subtitle={data?.subtitle || t('subtitle')} className="text-white mb-8">
|
||||
<span className="text-white">{data?.title || t('title')}</span>
|
||||
</Heading>
|
||||
|
||||
<div className="relative mb-12">
|
||||
<div className="absolute -left-8 top-0 bottom-0 w-1 bg-accent rounded-full" />
|
||||
<p className="text-xl md:text-2xl leading-relaxed font-medium italic text-white/90 pl-8">
|
||||
"{t('description')}"
|
||||
"{data?.description || t('description')}"
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-8 items-center">
|
||||
<Button href={`/${locale}/team`} variant="accent" size="xl" className="group">
|
||||
{t('cta')}
|
||||
{data?.ctaLabel || t('cta')}
|
||||
<span className="ml-3 transition-transform group-hover:translate-x-2">→</span>
|
||||
</Button>
|
||||
|
||||
@@ -61,7 +61,7 @@ export default function MeetTheTeam() {
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-white/60 font-bold text-xs md:text-sm uppercase tracking-widest">
|
||||
{t('andNetwork')}
|
||||
{data?.networkLabel || t('andNetwork')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,7 @@ import Image from 'next/image';
|
||||
import { useTranslations, useLocale } from 'next-intl';
|
||||
import { Section } from '../../components/ui';
|
||||
|
||||
export default function ProductCategories() {
|
||||
export default function ProductCategories({ data }: { data?: any }) {
|
||||
const t = useTranslations('Products');
|
||||
const locale = useLocale();
|
||||
|
||||
@@ -43,9 +43,13 @@ export default function ProductCategories() {
|
||||
|
||||
return (
|
||||
<Section className="bg-neutral-light py-0 md:py-0 lg:py-0 -mt-px">
|
||||
{t.has('title') && (
|
||||
{(data?.title || t.has('title')) && (
|
||||
<h2 className="sr-only">
|
||||
{t.rich('title', { green: (chunks: any) => <span>{chunks}</span> })}
|
||||
{data?.title ? (
|
||||
<span dangerouslySetInnerHTML={{ __html: data.title }} />
|
||||
) : (
|
||||
t.rich('title', { green: (chunks: any) => <span>{chunks}</span> })
|
||||
)}
|
||||
</h2>
|
||||
)}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4">
|
||||
|
||||
@@ -7,34 +7,38 @@ import { Section, Container, Heading, Card, Badge } from '../../components/ui';
|
||||
|
||||
interface RecentPostsProps {
|
||||
locale: string;
|
||||
data?: any;
|
||||
}
|
||||
|
||||
export default async function RecentPosts({ locale }: RecentPostsProps) {
|
||||
export default async function RecentPosts({ locale, data }: RecentPostsProps) {
|
||||
const t = await getTranslations('Blog');
|
||||
const posts = await getAllPosts(locale);
|
||||
const recentPosts = posts.slice(0, 3);
|
||||
|
||||
if (recentPosts.length === 0) return null;
|
||||
|
||||
const title = data?.title || t('allArticles');
|
||||
const subtitle = data?.subtitle || t('latestNews');
|
||||
|
||||
return (
|
||||
<Section className="bg-neutral py-16 md:py-24">
|
||||
<Container>
|
||||
<div className="flex flex-col md:flex-row items-start md:items-end justify-between mb-12 md:mb-16 gap-6">
|
||||
<Heading level={2} subtitle={t('latestNews')} className="mb-0 text-primary">
|
||||
{t('allArticles')}
|
||||
<Heading level={2} subtitle={subtitle} className="mb-0 text-primary">
|
||||
{title}
|
||||
</Heading>
|
||||
<Link
|
||||
href={`/${locale}/blog`}
|
||||
className="group flex items-center text-primary font-bold text-base md:text-lg touch-target"
|
||||
>
|
||||
{t('allArticles')}
|
||||
{title}
|
||||
<span className="ml-2 transition-transform group-hover:translate-x-2">→</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<ul className="grid grid-cols-1 md:grid-cols-3 gap-8 md:gap-10 list-none p-0 m-0">
|
||||
{recentPosts.map((post) => (
|
||||
<li key={post.slug}>
|
||||
{recentPosts.map((post, idx) => (
|
||||
<li key={`${post.slug}-${idx}`}>
|
||||
<Link href={`/${locale}/blog/${post.slug}`} className="group block h-full">
|
||||
<Card
|
||||
tag="article"
|
||||
@@ -47,6 +51,9 @@ export default async function RecentPosts({ locale }: RecentPostsProps) {
|
||||
alt={post.frontmatter.title}
|
||||
fill
|
||||
className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-110"
|
||||
style={{
|
||||
objectPosition: `${post.frontmatter.focalX ?? 50}% ${post.frontmatter.focalY ?? 50}%`,
|
||||
}}
|
||||
sizes="(max-width: 768px) 100vw, 33vw"
|
||||
loading="lazy"
|
||||
/>
|
||||
|
||||
@@ -4,7 +4,7 @@ import React, { useState, useEffect, useRef } from 'react';
|
||||
import Scribble from '@/components/Scribble';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
export default function VideoSection() {
|
||||
export default function VideoSection({ data }: { data?: any }) {
|
||||
const t = useTranslations('Home.video');
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const sectionRef = useRef<HTMLElement>(null);
|
||||
@@ -40,17 +40,21 @@ export default function VideoSection() {
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-primary/60 via-transparent to-primary/60 flex items-center justify-center pointer-events-none">
|
||||
<div className="max-w-5xl px-6 text-center animate-slide-up pointer-events-auto">
|
||||
<h2 className="text-3xl md:text-4xl lg:text-5xl font-extrabold text-white leading-[1.1]">
|
||||
{t.rich('title', {
|
||||
future: (chunks) => (
|
||||
<span className="relative inline-block mx-2">
|
||||
<span className="relative z-10 italic text-accent">{chunks}</span>
|
||||
<Scribble
|
||||
variant="underline"
|
||||
className="w-full h-4 -bottom-2 left-0 text-accent/40"
|
||||
/>
|
||||
</span>
|
||||
),
|
||||
})}
|
||||
{data?.title ? (
|
||||
<span dangerouslySetInnerHTML={{ __html: data.title.replace(/<future>/g, '<span class="relative inline-block mx-2"><span class="relative z-10 italic text-accent">').replace(/<\/future>/g, '</span><Scribble variant="underline" class="w-full h-4 -bottom-2 left-0 text-accent/40" /></span>') }} />
|
||||
) : (
|
||||
t.rich('title', {
|
||||
future: (chunks) => (
|
||||
<span className="relative inline-block mx-2">
|
||||
<span className="relative z-10 italic text-accent">{chunks}</span>
|
||||
<Scribble
|
||||
variant="underline"
|
||||
className="w-full h-4 -bottom-2 left-0 text-accent/40"
|
||||
/>
|
||||
</span>
|
||||
),
|
||||
})
|
||||
)}
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,30 +2,35 @@ import React from 'react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { Section, Container, Heading } from '../../components/ui';
|
||||
|
||||
export default function WhatWeDo() {
|
||||
export default function WhatWeDo({ data }: { data?: any }) {
|
||||
const t = useTranslations('Home.whatWeDo');
|
||||
|
||||
const items = data?.items?.length ? data.items : [0, 1, 2, 3].map(idx => ({
|
||||
title: t(`items.${idx}.title`),
|
||||
description: t(`items.${idx}.description`)
|
||||
}));
|
||||
|
||||
return (
|
||||
<Section className="bg-white">
|
||||
<Container>
|
||||
<div className="sticky-narrative-container">
|
||||
<div className="sticky-narrative-sidebar">
|
||||
<div className="lg:sticky lg:top-32">
|
||||
<Heading level={2} subtitle={t('expertise')} className="text-primary-dark">
|
||||
{t('title')}
|
||||
<Heading level={2} subtitle={data?.expertiseLabel || t('expertise')} className="text-primary-dark">
|
||||
{data?.title || t('title')}
|
||||
</Heading>
|
||||
<p className="text-base md:text-lg text-text-secondary leading-relaxed">
|
||||
{t('subtitle')}
|
||||
{data?.subtitle || t('subtitle')}
|
||||
</p>
|
||||
<div className="mt-8 md:mt-12 p-6 md:p-8 bg-saturated/10 rounded-2xl border border-saturated/10">
|
||||
<p className="text-saturated font-bold text-base md:text-base italic">
|
||||
"{t('quote')}"
|
||||
"{data?.quote || t('quote')}"
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="sticky-narrative-content grid grid-cols-1 md:grid-cols-2 gap-x-8 md:gap-x-12 gap-y-12 md:gap-y-20">
|
||||
{[0, 1, 2, 3].map((idx) => (
|
||||
{items.map((item: any, idx: number) => (
|
||||
<div key={idx} className="group">
|
||||
<div className="flex items-center gap-4 mb-4 md:mb-6">
|
||||
<span className="flex items-center justify-center w-10 h-10 md:w-12 md:h-12 rounded-full bg-saturated text-white font-bold text-base md:text-lg shadow-lg shadow-saturated/20 group-hover:scale-110 transition-transform">
|
||||
@@ -33,8 +38,8 @@ export default function WhatWeDo() {
|
||||
</span>
|
||||
<div className="h-px flex-grow bg-neutral-medium" />
|
||||
</div>
|
||||
<h3 className="text-lg md:text-xl font-bold mb-3 md:mb-4 text-primary-dark group-hover:text-accent-dark transition-colors">{t(`items.${idx}.title`)}</h3>
|
||||
<p className="text-text-secondary text-base md:text-base leading-relaxed">{t(`items.${idx}.description`)}</p>
|
||||
<h3 className="text-lg md:text-xl font-bold mb-3 md:mb-4 text-primary-dark group-hover:text-accent-dark transition-colors">{item.title}</h3>
|
||||
<p className="text-text-secondary text-base md:text-base leading-relaxed">{item.description}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -2,24 +2,27 @@ import React from 'react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { Section, Container, Heading } from '../../components/ui';
|
||||
|
||||
export default function WhyChooseUs() {
|
||||
export default function WhyChooseUs({ data }: { data?: any }) {
|
||||
const t = useTranslations('Home.whyChooseUs');
|
||||
|
||||
const features = data?.features?.length ? data.features.map((f: any) => f.feature) : [0, 1, 2, 3].map(i => t(`features.${i}`));
|
||||
const items = data?.items?.length ? data.items : [0, 1, 2, 3].map(idx => ({ title: t(`items.${idx}.title`), description: t(`items.${idx}.description`) }));
|
||||
|
||||
return (
|
||||
<Section className="bg-neutral-light">
|
||||
<Container>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-16 lg:gap-24">
|
||||
<div className="lg:col-span-4 order-1 lg:order-2">
|
||||
<div className="sticky top-32">
|
||||
<Heading level={2} subtitle={t('whyKlz')} className="text-primary-dark">
|
||||
{t('title')}
|
||||
<Heading level={2} subtitle={data?.tagline || t('whyKlz')} className="text-primary-dark">
|
||||
{data?.title || t('title')}
|
||||
</Heading>
|
||||
<p className="text-base md:text-lg text-text-secondary leading-relaxed">
|
||||
{t('subtitle')}
|
||||
{data?.subtitle || t('subtitle')}
|
||||
</p>
|
||||
|
||||
<ul className="mt-12 space-y-6 list-none p-0">
|
||||
{[0, 1, 2, 3].map((i) => (
|
||||
{features.map((featureText: string, i: number) => (
|
||||
<li key={i} className="flex items-center gap-4">
|
||||
<div className="flex-shrink-0 w-6 h-6 rounded-full bg-accent flex items-center justify-center">
|
||||
<svg
|
||||
@@ -38,7 +41,7 @@ export default function WhyChooseUs() {
|
||||
</svg>
|
||||
</div>
|
||||
<span className="font-bold text-primary-dark text-base md:text-base">
|
||||
{t(`features.${i}`)}
|
||||
{featureText}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
@@ -46,7 +49,7 @@ export default function WhyChooseUs() {
|
||||
</div>
|
||||
</div>
|
||||
<ul className="lg:col-span-8 grid grid-cols-1 md:grid-cols-2 gap-8 order-2 lg:order-1 list-none p-0 m-0">
|
||||
{[0, 1, 2, 3].map((idx) => (
|
||||
{items.map((item: any, idx: number) => (
|
||||
<li
|
||||
key={idx}
|
||||
className="p-10 bg-white rounded-3xl border border-neutral-medium hover:border-accent transition-all duration-500 hover:shadow-xl group"
|
||||
@@ -57,10 +60,10 @@ export default function WhyChooseUs() {
|
||||
</span>
|
||||
</div>
|
||||
<h3 className="text-xl font-bold mb-4 text-primary-dark">
|
||||
{t(`items.${idx}.title`)}
|
||||
{item.title}
|
||||
</h3>
|
||||
<p className="text-text-secondary text-base md:text-base leading-relaxed">
|
||||
{t(`items.${idx}.description`)}
|
||||
{item.description}
|
||||
</p>
|
||||
</li>
|
||||
))}
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
console.log('DEBUG SCRIPT STARTING...');
|
||||
|
||||
async function debug() {
|
||||
console.log('Importing dependencies...');
|
||||
try {
|
||||
const { getAllProductsMetadata } = await import('./lib/mdx');
|
||||
const { getAllPostsMetadata } = await import('./lib/blog');
|
||||
const { getAllPagesMetadata } = await import('./lib/pages');
|
||||
|
||||
console.log('Dependencies imported.');
|
||||
|
||||
const locales = ['de', 'en'];
|
||||
for (const locale of locales) {
|
||||
console.log(`--- Locale: ${locale} ---`);
|
||||
|
||||
try {
|
||||
const products = await getAllProductsMetadata(locale);
|
||||
console.log(`Products (${locale}): ${products.length}`);
|
||||
} catch (e) {
|
||||
console.error(`Failed to get products for ${locale}:`, e);
|
||||
}
|
||||
|
||||
try {
|
||||
const posts = await getAllPostsMetadata(locale);
|
||||
console.log(`Posts (${locale}): ${posts.length}`);
|
||||
} catch (e) {
|
||||
console.error(`Failed to get posts for ${locale}:`, e);
|
||||
}
|
||||
|
||||
try {
|
||||
const pages = await getAllPagesMetadata(locale);
|
||||
console.log(`Pages (${locale}): ${pages.length}`);
|
||||
} catch (e) {
|
||||
console.error(`Failed to get pages for ${locale}:`, e);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Debug failed during setup/imports:', err);
|
||||
}
|
||||
console.log('DEBUG SCRIPT FINISHED.');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
debug().catch((err) => {
|
||||
console.error('Unhandled retransmission error in debug():', err);
|
||||
process.exit(1);
|
||||
});
|
||||
14
lib/blog.ts
14
lib/blog.ts
@@ -35,7 +35,6 @@ export interface PostFrontmatter {
|
||||
focalX?: number;
|
||||
focalY?: number;
|
||||
category?: string;
|
||||
locale: string;
|
||||
public?: boolean;
|
||||
}
|
||||
|
||||
@@ -65,9 +64,9 @@ export async function getPostBySlug(slug: string, locale: string): Promise<PostM
|
||||
collection: 'posts',
|
||||
where: {
|
||||
slug: { equals: slug },
|
||||
locale: { equals: locale },
|
||||
...(!isDev ? { _status: { equals: 'published' } } : {}),
|
||||
},
|
||||
locale: locale as any,
|
||||
draft: isDev,
|
||||
limit: 1,
|
||||
});
|
||||
@@ -83,7 +82,6 @@ export async function getPostBySlug(slug: string, locale: string): Promise<PostM
|
||||
date: doc.date,
|
||||
excerpt: doc.excerpt || '',
|
||||
category: doc.category || '',
|
||||
locale: doc.locale,
|
||||
featuredImage:
|
||||
typeof doc.featuredImage === 'object' && doc.featuredImage !== null
|
||||
? doc.featuredImage.sizes?.card?.url || doc.featuredImage.url
|
||||
@@ -113,11 +111,9 @@ export async function getAllPosts(locale: string): Promise<PostMdx[]> {
|
||||
const { docs } = await payload.find({
|
||||
collection: 'posts',
|
||||
where: {
|
||||
locale: {
|
||||
equals: locale,
|
||||
},
|
||||
...(!isDev ? { _status: { equals: 'published' } } : {}),
|
||||
},
|
||||
locale: locale as any,
|
||||
sort: '-date',
|
||||
draft: isDev,
|
||||
limit: 100,
|
||||
@@ -125,7 +121,7 @@ export async function getAllPosts(locale: string): Promise<PostMdx[]> {
|
||||
|
||||
console.log(`[Payload] getAllPosts for ${locale}: Found ${docs.length} docs`);
|
||||
|
||||
return docs.map((doc) => {
|
||||
const posts = docs.map((doc) => {
|
||||
return {
|
||||
slug: doc.slug,
|
||||
frontmatter: {
|
||||
@@ -133,7 +129,6 @@ export async function getAllPosts(locale: string): Promise<PostMdx[]> {
|
||||
date: doc.date,
|
||||
excerpt: doc.excerpt || '',
|
||||
category: doc.category || '',
|
||||
locale: doc.locale,
|
||||
featuredImage:
|
||||
typeof doc.featuredImage === 'object' && doc.featuredImage !== null
|
||||
? doc.featuredImage.sizes?.card?.url || doc.featuredImage.url
|
||||
@@ -151,6 +146,9 @@ export async function getAllPosts(locale: string): Promise<PostMdx[]> {
|
||||
content: doc.content as any,
|
||||
};
|
||||
});
|
||||
|
||||
// Integrity check: only show posts with a featured image in listings/sitemap
|
||||
return posts.filter((p) => !!p.frontmatter.featuredImage);
|
||||
} catch (error) {
|
||||
console.error(`[Payload] getAllPosts failed for ${locale}:`, error);
|
||||
return [];
|
||||
|
||||
@@ -33,6 +33,17 @@ interface SendEmailOptions {
|
||||
|
||||
export async function sendEmail({ to, replyTo, subject, html }: SendEmailOptions) {
|
||||
const recipients = to || config.mail.recipients;
|
||||
const logger = getServerAppServices().logger.child({ component: 'mailer' });
|
||||
|
||||
if (!recipients) {
|
||||
logger.error('No email recipients configured (MAIL_RECIPIENTS is empty and no "to" provided)', { subject });
|
||||
return { success: false as const, error: 'No recipients configured' };
|
||||
}
|
||||
|
||||
if (!config.mail.from) {
|
||||
logger.error('MAIL_FROM is not configured — cannot send email', { subject, recipients });
|
||||
return { success: false as const, error: 'MAIL_FROM is not configured' };
|
||||
}
|
||||
|
||||
const mailOptions = {
|
||||
from: config.mail.from,
|
||||
@@ -42,7 +53,6 @@ export async function sendEmail({ to, replyTo, subject, html }: SendEmailOptions
|
||||
html,
|
||||
};
|
||||
|
||||
const logger = getServerAppServices().logger.child({ component: 'mailer' });
|
||||
|
||||
try {
|
||||
const info = await getTransporter().sendMail(mailOptions);
|
||||
|
||||
113
lib/pages.ts
113
lib/pages.ts
@@ -5,7 +5,9 @@ export interface PageFrontmatter {
|
||||
title: string;
|
||||
excerpt: string;
|
||||
featuredImage: string | null;
|
||||
locale: string;
|
||||
focalX?: number;
|
||||
focalY?: number;
|
||||
layout?: 'default' | 'fullBleed';
|
||||
public?: boolean;
|
||||
}
|
||||
|
||||
@@ -15,6 +17,30 @@ export interface PageMdx {
|
||||
content: any; // Lexical AST Document
|
||||
}
|
||||
|
||||
function mapDoc(doc: any): PageMdx {
|
||||
return {
|
||||
slug: doc.slug,
|
||||
frontmatter: {
|
||||
title: doc.title,
|
||||
excerpt: doc.excerpt || '',
|
||||
featuredImage:
|
||||
typeof doc.featuredImage === 'object' && doc.featuredImage !== null
|
||||
? doc.featuredImage.sizes?.card?.url || doc.featuredImage.url
|
||||
: null,
|
||||
focalX:
|
||||
typeof doc.featuredImage === 'object' && doc.featuredImage !== null
|
||||
? doc.featuredImage.focalX
|
||||
: 50,
|
||||
focalY:
|
||||
typeof doc.featuredImage === 'object' && doc.featuredImage !== null
|
||||
? doc.featuredImage.focalY
|
||||
: 50,
|
||||
layout: doc.layout || 'default',
|
||||
} as PageFrontmatter,
|
||||
content: doc.content as any,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getPageBySlug(slug: string, locale: string): Promise<PageMdx | null> {
|
||||
try {
|
||||
const payload = await getPayload({ config: configPromise });
|
||||
@@ -23,30 +49,14 @@ export async function getPageBySlug(slug: string, locale: string): Promise<PageM
|
||||
collection: 'pages' as any,
|
||||
where: {
|
||||
slug: { equals: slug },
|
||||
locale: { equals: locale },
|
||||
},
|
||||
locale: locale as any,
|
||||
limit: 1,
|
||||
});
|
||||
|
||||
const docs = result.docs as any[];
|
||||
|
||||
if (!docs || docs.length === 0) return null;
|
||||
|
||||
const doc = docs[0];
|
||||
|
||||
return {
|
||||
slug: doc.slug,
|
||||
frontmatter: {
|
||||
title: doc.title,
|
||||
excerpt: doc.excerpt || '',
|
||||
locale: doc.locale,
|
||||
featuredImage:
|
||||
typeof doc.featuredImage === 'object' && doc.featuredImage !== null
|
||||
? doc.featuredImage.sizes?.card?.url || doc.featuredImage.url
|
||||
: null,
|
||||
} as PageFrontmatter,
|
||||
content: doc.content as any, // Native Lexical Editor State
|
||||
};
|
||||
return mapDoc(docs[0]);
|
||||
} catch (error) {
|
||||
console.error(`[Payload] getPageBySlug failed for ${slug}:`, error);
|
||||
return null;
|
||||
@@ -59,31 +69,11 @@ export async function getAllPages(locale: string): Promise<PageMdx[]> {
|
||||
|
||||
const result = await payload.find({
|
||||
collection: 'pages' as any,
|
||||
where: {
|
||||
locale: {
|
||||
equals: locale,
|
||||
},
|
||||
},
|
||||
locale: locale as any,
|
||||
limit: 100,
|
||||
});
|
||||
|
||||
const docs = result.docs as any[];
|
||||
|
||||
return docs.map((doc: any) => {
|
||||
return {
|
||||
slug: doc.slug,
|
||||
frontmatter: {
|
||||
title: doc.title,
|
||||
excerpt: doc.excerpt || '',
|
||||
locale: doc.locale,
|
||||
featuredImage:
|
||||
typeof doc.featuredImage === 'object' && doc.featuredImage !== null
|
||||
? doc.featuredImage.sizes?.card?.url || doc.featuredImage.url
|
||||
: null,
|
||||
} as PageFrontmatter,
|
||||
content: doc.content as any,
|
||||
};
|
||||
});
|
||||
return (result.docs as any[]).map(mapDoc);
|
||||
} catch (error) {
|
||||
console.error(`[Payload] getAllPages failed for ${locale}:`, error);
|
||||
return [];
|
||||
@@ -96,30 +86,29 @@ export async function getAllPagesMetadata(locale: string): Promise<Partial<PageM
|
||||
|
||||
const result = await payload.find({
|
||||
collection: 'pages' as any,
|
||||
where: {
|
||||
locale: {
|
||||
equals: locale,
|
||||
},
|
||||
},
|
||||
locale: locale as any,
|
||||
limit: 100,
|
||||
});
|
||||
|
||||
const docs = result.docs as any[];
|
||||
|
||||
return docs.map((doc: any) => {
|
||||
return {
|
||||
slug: doc.slug,
|
||||
frontmatter: {
|
||||
title: doc.title,
|
||||
excerpt: doc.excerpt || '',
|
||||
locale: doc.locale,
|
||||
featuredImage:
|
||||
typeof doc.featuredImage === 'object' && doc.featuredImage !== null
|
||||
? doc.featuredImage.sizes?.card?.url || doc.featuredImage.url
|
||||
: null,
|
||||
} as PageFrontmatter,
|
||||
};
|
||||
});
|
||||
return (result.docs as any[]).map((doc: any) => ({
|
||||
slug: doc.slug,
|
||||
frontmatter: {
|
||||
title: doc.title,
|
||||
excerpt: doc.excerpt || '',
|
||||
featuredImage:
|
||||
typeof doc.featuredImage === 'object' && doc.featuredImage !== null
|
||||
? doc.featuredImage.sizes?.card?.url || doc.featuredImage.url
|
||||
: null,
|
||||
focalX:
|
||||
typeof doc.featuredImage === 'object' && doc.featuredImage !== null
|
||||
? doc.featuredImage.focalX
|
||||
: 50,
|
||||
focalY:
|
||||
typeof doc.featuredImage === 'object' && doc.featuredImage !== null
|
||||
? doc.featuredImage.focalY
|
||||
: 50,
|
||||
} as PageFrontmatter,
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error(`[Payload] getAllPagesMetadata failed for ${locale}:`, error);
|
||||
return [];
|
||||
|
||||
@@ -8,11 +8,12 @@ export interface ProductFrontmatter {
|
||||
description: string;
|
||||
categories: string[];
|
||||
images: string[];
|
||||
locale: string;
|
||||
focalX?: number;
|
||||
focalY?: number;
|
||||
isFallback?: boolean;
|
||||
}
|
||||
|
||||
export interface ProductMdx {
|
||||
export interface ProductData {
|
||||
slug: string;
|
||||
frontmatter: ProductFrontmatter;
|
||||
content: any; // Lexical AST from Payload
|
||||
@@ -21,36 +22,24 @@ export interface ProductMdx {
|
||||
export async function getProductMetadata(
|
||||
slug: string,
|
||||
locale: string,
|
||||
): Promise<Partial<ProductMdx> | null> {
|
||||
): Promise<Partial<ProductData> | null> {
|
||||
const payload = await getPayload({ config: configPromise });
|
||||
const fileSlug = await mapSlugToFileSlug(slug, locale);
|
||||
|
||||
let result = await payload.find({
|
||||
const isDev = process.env.NODE_ENV === 'development';
|
||||
const result = await payload.find({
|
||||
collection: 'products',
|
||||
where: {
|
||||
and: [{ slug: { equals: fileSlug } }, { locale: { equals: locale } }],
|
||||
and: [
|
||||
{ slug: { equals: fileSlug } },
|
||||
...(!isDev ? [{ _status: { equals: 'published' } }] : []),
|
||||
],
|
||||
},
|
||||
depth: 1, // To auto-resolve Media relation (images array)
|
||||
locale: locale as any,
|
||||
depth: 1,
|
||||
limit: 1,
|
||||
});
|
||||
|
||||
let isFallback = false;
|
||||
|
||||
if (result.docs.length === 0 && locale !== 'en') {
|
||||
// Fallback to English
|
||||
result = await payload.find({
|
||||
collection: 'products',
|
||||
where: {
|
||||
and: [{ slug: { equals: fileSlug } }, { locale: { equals: 'en' } }],
|
||||
},
|
||||
depth: 1,
|
||||
limit: 1,
|
||||
});
|
||||
if (result.docs.length > 0) {
|
||||
isFallback = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (result.docs.length > 0) {
|
||||
const doc = result.docs[0];
|
||||
|
||||
@@ -69,8 +58,6 @@ export async function getProductMetadata(
|
||||
description: doc.description,
|
||||
categories: Array.isArray(doc.categories) ? doc.categories.map((c: any) => c.category) : [],
|
||||
images: resolvedImages,
|
||||
locale: doc.locale,
|
||||
isFallback,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -78,37 +65,25 @@ export async function getProductMetadata(
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function getProductBySlug(slug: string, locale: string): Promise<ProductMdx | null> {
|
||||
export async function getProductBySlug(slug: string, locale: string): Promise<ProductData | null> {
|
||||
try {
|
||||
const payload = await getPayload({ config: configPromise });
|
||||
const fileSlug = await mapSlugToFileSlug(slug, locale);
|
||||
|
||||
let result = await payload.find({
|
||||
const isDev = process.env.NODE_ENV === 'development';
|
||||
const result = await payload.find({
|
||||
collection: 'products',
|
||||
where: {
|
||||
and: [{ slug: { equals: fileSlug } }, { locale: { equals: locale } }],
|
||||
and: [
|
||||
{ slug: { equals: fileSlug } },
|
||||
...(!isDev ? [{ _status: { equals: 'published' } }] : []),
|
||||
],
|
||||
},
|
||||
depth: 1, // Auto-resolve Media logic
|
||||
locale: locale as any,
|
||||
depth: 1,
|
||||
limit: 1,
|
||||
});
|
||||
|
||||
let isFallback = false;
|
||||
|
||||
if (result.docs.length === 0 && locale !== 'en') {
|
||||
// Fallback to English
|
||||
result = await payload.find({
|
||||
collection: 'products',
|
||||
where: {
|
||||
and: [{ slug: { equals: fileSlug } }, { locale: { equals: 'en' } }],
|
||||
},
|
||||
depth: 1,
|
||||
limit: 1,
|
||||
});
|
||||
if (result.docs.length > 0) {
|
||||
isFallback = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (result.docs.length > 0) {
|
||||
const doc = result.docs[0];
|
||||
|
||||
@@ -129,10 +104,16 @@ export async function getProductBySlug(slug: string, locale: string): Promise<Pr
|
||||
? doc.categories.map((c: any) => c.category)
|
||||
: [],
|
||||
images: resolvedImages,
|
||||
locale: doc.locale,
|
||||
isFallback,
|
||||
focalX:
|
||||
Array.isArray(doc.images) && doc.images.length > 0 && typeof doc.images[0] === 'object'
|
||||
? doc.images[0].focalX
|
||||
: 50,
|
||||
focalY:
|
||||
Array.isArray(doc.images) && doc.images.length > 0 && typeof doc.images[0] === 'object'
|
||||
? doc.images[0].focalY
|
||||
: 50,
|
||||
},
|
||||
content: doc.content, // Lexical payload instead of raw MDX String
|
||||
content: doc.content,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -146,14 +127,14 @@ export async function getProductBySlug(slug: string, locale: string): Promise<Pr
|
||||
export async function getAllProductSlugs(locale: string): Promise<string[]> {
|
||||
try {
|
||||
const payload = await getPayload({ config: configPromise });
|
||||
const isDev = process.env.NODE_ENV === 'development';
|
||||
const result = await payload.find({
|
||||
collection: 'products',
|
||||
where: {
|
||||
locale: {
|
||||
equals: locale,
|
||||
},
|
||||
...(!isDev ? { _status: { equals: 'published' } } : {}),
|
||||
},
|
||||
pagination: false, // get all docs
|
||||
locale: locale as any,
|
||||
pagination: false,
|
||||
});
|
||||
|
||||
return result.docs.map((doc) => doc.slug);
|
||||
@@ -163,7 +144,7 @@ export async function getAllProductSlugs(locale: string): Promise<string[]> {
|
||||
}
|
||||
}
|
||||
|
||||
export async function getAllProducts(locale: string): Promise<ProductMdx[]> {
|
||||
export async function getAllProducts(locale: string): Promise<ProductData[]> {
|
||||
try {
|
||||
const payload = await getPayload({ config: configPromise });
|
||||
|
||||
@@ -174,13 +155,15 @@ export async function getAllProducts(locale: string): Promise<ProductMdx[]> {
|
||||
description: true,
|
||||
categories: true,
|
||||
images: true,
|
||||
locale: true,
|
||||
} as const;
|
||||
|
||||
// Get products for this locale
|
||||
const isDev = process.env.NODE_ENV === 'development';
|
||||
const result = await payload.find({
|
||||
collection: 'products',
|
||||
where: { locale: { equals: locale } },
|
||||
where: {
|
||||
...(!isDev ? { _status: { equals: 'published' } } : {}),
|
||||
},
|
||||
locale: locale as any,
|
||||
depth: 1,
|
||||
pagination: false,
|
||||
select: selectFields,
|
||||
@@ -188,7 +171,7 @@ export async function getAllProducts(locale: string): Promise<ProductMdx[]> {
|
||||
|
||||
console.log(`[Payload] getAllProducts for ${locale}: Found ${result.docs.length} docs`);
|
||||
|
||||
let products: ProductMdx[] = result.docs.map((doc) => {
|
||||
let products: ProductData[] = result.docs.map((doc) => {
|
||||
const resolvedImages = ((doc.images as any[]) || [])
|
||||
.map((img) => (typeof img === 'string' ? img : img.url))
|
||||
.filter(Boolean) as string[];
|
||||
@@ -205,55 +188,21 @@ export async function getAllProducts(locale: string): Promise<ProductMdx[]> {
|
||||
description: doc.description ? String(doc.description) : '',
|
||||
categories: plainCategories,
|
||||
images: resolvedImages,
|
||||
locale: String(doc.locale),
|
||||
focalX:
|
||||
Array.isArray(doc.images) && doc.images.length > 0 && typeof doc.images[0] === 'object'
|
||||
? doc.images[0].focalX
|
||||
: 50,
|
||||
focalY:
|
||||
Array.isArray(doc.images) && doc.images.length > 0 && typeof doc.images[0] === 'object'
|
||||
? doc.images[0].focalY
|
||||
: 50,
|
||||
},
|
||||
content: null,
|
||||
};
|
||||
});
|
||||
|
||||
// Also include English fallbacks for slugs not in this locale
|
||||
if (locale !== 'en') {
|
||||
const localeSlugs = new Set(products.map((p) => p.slug));
|
||||
const enResult = await payload.find({
|
||||
collection: 'products',
|
||||
where: { locale: { equals: 'en' } },
|
||||
depth: 1,
|
||||
pagination: false,
|
||||
select: selectFields,
|
||||
});
|
||||
|
||||
console.log(
|
||||
`[Payload] getAllProducts (en fallbacks) for ${locale}: Found ${enResult.docs.length} docs`,
|
||||
);
|
||||
|
||||
const fallbacks = enResult.docs
|
||||
.filter((doc) => !localeSlugs.has(doc.slug))
|
||||
.map((doc) => {
|
||||
const resolvedImages = ((doc.images as any[]) || [])
|
||||
.map((img) => (typeof img === 'string' ? img : img.url))
|
||||
.filter(Boolean) as string[];
|
||||
|
||||
const plainCategories = Array.isArray(doc.categories)
|
||||
? doc.categories.map((c: any) => String(c.category))
|
||||
: [];
|
||||
|
||||
return {
|
||||
slug: String(doc.slug),
|
||||
frontmatter: {
|
||||
title: String(doc.title),
|
||||
sku: doc.sku ? String(doc.sku) : '',
|
||||
description: doc.description ? String(doc.description) : '',
|
||||
categories: plainCategories,
|
||||
images: resolvedImages,
|
||||
locale: String(doc.locale),
|
||||
isFallback: true,
|
||||
},
|
||||
content: null,
|
||||
};
|
||||
});
|
||||
|
||||
products = [...products, ...fallbacks];
|
||||
}
|
||||
// Filter out products with 0 images (data integrity check to prevent 404s)
|
||||
products = products.filter((p) => p.frontmatter.images && p.frontmatter.images.length > 0);
|
||||
|
||||
return products;
|
||||
} catch (error) {
|
||||
@@ -262,7 +211,7 @@ export async function getAllProducts(locale: string): Promise<ProductMdx[]> {
|
||||
}
|
||||
}
|
||||
|
||||
export async function getAllProductsMetadata(locale: string): Promise<Partial<ProductMdx>[]> {
|
||||
export async function getAllProductsMetadata(locale: string): Promise<Partial<ProductData>[]> {
|
||||
const products = await getAllProducts(locale);
|
||||
return products.map((p) => ({
|
||||
slug: p.slug,
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"Slugs": {
|
||||
"pages": {
|
||||
"legal-notice": "legal-notice",
|
||||
"privacy-policy": "privacy-policy",
|
||||
"terms": "terms",
|
||||
"legal-notice": "impressum",
|
||||
"privacy-policy": "datenschutz",
|
||||
"terms": "agbs",
|
||||
"contact": "contact",
|
||||
"team": "team",
|
||||
"blog": "blog",
|
||||
@@ -396,4 +396,4 @@
|
||||
"cta": "Back to Safety"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -361,23 +361,23 @@ const nextConfig = {
|
||||
},
|
||||
{
|
||||
source: '/products/solar-cables/h1z2z2-k',
|
||||
destination: '/en/products/h1z2z2-k',
|
||||
destination: '/en/products/solar-cables/h1z2z2-k',
|
||||
permanent: true,
|
||||
},
|
||||
// Product redirects (German)
|
||||
{
|
||||
source: '/de/produkte/solarkabel/h1z2z2-k',
|
||||
destination: '/de/produkte/h1z2z2-k',
|
||||
source: '/de/produkte/stromkabel/solarkabel/h1z2z2-k',
|
||||
destination: '/de/produkte/solarkabel/h1z2z2-k',
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: '/de/produkte/stromkabel/niederspannungskabel/naycwy-2',
|
||||
destination: '/de/produkte/naycwy',
|
||||
destination: '/de/produkte/niederspannungskabel/naycwy',
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: '/de/produkte/stromkabel/niederspannungskabel/ny2y-2',
|
||||
destination: '/de/produkte/ny2y',
|
||||
destination: '/de/produkte/niederspannungskabel/ny2y',
|
||||
permanent: true,
|
||||
},
|
||||
// VCF redirects
|
||||
|
||||
@@ -91,7 +91,7 @@
|
||||
"vitest": "^4.0.16"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "docker network create infra 2>/dev/null || true && COMPOSE_PROJECT_NAME=klz-2026 docker-compose -f docker-compose.dev.yml up klz-app klz-db --remove-orphans",
|
||||
"dev": "bash -c 'trap \"COMPOSE_PROJECT_NAME=klz-2026 docker-compose -f docker-compose.dev.yml down\" EXIT INT TERM; docker network create infra 2>/dev/null || true && COMPOSE_PROJECT_NAME=klz-2026 docker-compose -f docker-compose.dev.yml down && COMPOSE_PROJECT_NAME=klz-2026 docker-compose -f docker-compose.dev.yml up klz-app klz-db --remove-orphans'",
|
||||
"dev:local": "bash -c 'trap \"COMPOSE_PROJECT_NAME=klz-2026 docker-compose -f docker-compose.dev.yml down\" EXIT INT TERM; COMPOSE_PROJECT_NAME=klz-2026 docker-compose -f docker-compose.dev.yml up -d klz-db klz-proxy && POSTGRES_URI=NODE_ENV=development next dev --webpack --port 3100 --hostname 0.0.0.0'",
|
||||
"dev:infra": "COMPOSE_PROJECT_NAME=klz-2026 docker-compose -f docker-compose.dev.yml up -d klz-db klz-proxy",
|
||||
"build": "next build",
|
||||
@@ -126,8 +126,10 @@
|
||||
"backup:db": "bash ./scripts/backup-db.sh",
|
||||
"restore:db": "bash ./scripts/restore-db.sh",
|
||||
"cms:push:testing": "bash ./scripts/cms-sync.sh push testing",
|
||||
"cms:push:staging": "bash ./scripts/cms-sync.sh push staging",
|
||||
"cms:push:prod": "bash ./scripts/cms-sync.sh push prod",
|
||||
"cms:pull:testing": "bash ./scripts/cms-sync.sh pull testing",
|
||||
"cms:pull:staging": "bash ./scripts/cms-sync.sh pull staging",
|
||||
"cms:pull:prod": "bash ./scripts/cms-sync.sh pull prod",
|
||||
"prepare": "husky",
|
||||
"preinstall": "npx only-allow pnpm"
|
||||
@@ -154,4 +156,4 @@
|
||||
"peerDependencies": {
|
||||
"lucide-react": "^0.563.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
263
payload-types.ts
263
payload-types.ts
@@ -94,10 +94,10 @@ export interface Config {
|
||||
db: {
|
||||
defaultIDType: number;
|
||||
};
|
||||
fallbackLocale: null;
|
||||
fallbackLocale: ('false' | 'none' | 'null') | false | null | ('de' | 'en') | ('de' | 'en')[];
|
||||
globals: {};
|
||||
globalsSelect: {};
|
||||
locale: null;
|
||||
locale: 'de' | 'en';
|
||||
user: User;
|
||||
jobs: {
|
||||
tasks: unknown;
|
||||
@@ -200,6 +200,9 @@ export interface Media {
|
||||
export interface Post {
|
||||
id: number;
|
||||
title: string;
|
||||
/**
|
||||
* Unique slug per locale (e.g. same slug can exist in DE and EN).
|
||||
*/
|
||||
slug: string;
|
||||
/**
|
||||
* A short summary for blog feed cards and SEO.
|
||||
@@ -213,7 +216,6 @@ export interface Post {
|
||||
* The primary Hero image used for headers and OpenGraph previews.
|
||||
*/
|
||||
featuredImage?: (number | null) | Media;
|
||||
locale: 'en' | 'de';
|
||||
/**
|
||||
* Used for tag bucketing (e.g. "Kabel Technologie").
|
||||
*/
|
||||
@@ -266,7 +268,6 @@ export interface Product {
|
||||
sku: string;
|
||||
slug: string;
|
||||
description: string;
|
||||
locale: 'en' | 'de';
|
||||
categories: {
|
||||
category?: string | null;
|
||||
id?: string | null;
|
||||
@@ -317,8 +318,14 @@ export interface Product {
|
||||
export interface Page {
|
||||
id: number;
|
||||
title: string;
|
||||
/**
|
||||
* The URL slug for this locale (e.g. "impressum" for DE, "imprint" for EN).
|
||||
*/
|
||||
slug: string;
|
||||
locale: 'en' | 'de';
|
||||
/**
|
||||
* Full Bleed pages render blocks edge-to-edge without a generic hero wrapper.
|
||||
*/
|
||||
layout?: ('default' | 'fullBleed') | null;
|
||||
excerpt?: string | null;
|
||||
featuredImage?: (number | null) | Media;
|
||||
content: {
|
||||
@@ -338,6 +345,7 @@ export interface Page {
|
||||
};
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
_status?: ('draft' | 'published') | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
@@ -514,7 +522,6 @@ export interface PostsSelect<T extends boolean = true> {
|
||||
excerpt?: T;
|
||||
date?: T;
|
||||
featuredImage?: T;
|
||||
locale?: T;
|
||||
category?: T;
|
||||
content?: T;
|
||||
updatedAt?: T;
|
||||
@@ -543,7 +550,6 @@ export interface ProductsSelect<T extends boolean = true> {
|
||||
sku?: T;
|
||||
slug?: T;
|
||||
description?: T;
|
||||
locale?: T;
|
||||
categories?:
|
||||
| T
|
||||
| {
|
||||
@@ -565,12 +571,13 @@ export interface ProductsSelect<T extends boolean = true> {
|
||||
export interface PagesSelect<T extends boolean = true> {
|
||||
title?: T;
|
||||
slug?: T;
|
||||
locale?: T;
|
||||
layout?: T;
|
||||
excerpt?: T;
|
||||
featuredImage?: T;
|
||||
content?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
_status?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
@@ -702,6 +709,246 @@ export interface ProductTabsBlock {
|
||||
blockName?: string | null;
|
||||
blockType: 'productTabs';
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "HomeHeroBlock".
|
||||
*/
|
||||
export interface HomeHeroBlock {
|
||||
title?: string | null;
|
||||
subtitle?: string | null;
|
||||
ctaLabel?: string | null;
|
||||
secondaryCtaLabel?: string | null;
|
||||
id?: string | null;
|
||||
blockName?: string | null;
|
||||
blockType: 'homeHero';
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "HomeProductCategoriesBlock".
|
||||
*/
|
||||
export interface HomeProductCategoriesBlock {
|
||||
title?: string | null;
|
||||
subtitle?: string | null;
|
||||
id?: string | null;
|
||||
blockName?: string | null;
|
||||
blockType: 'homeProductCategories';
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "HomeWhatWeDoBlock".
|
||||
*/
|
||||
export interface HomeWhatWeDoBlock {
|
||||
title?: string | null;
|
||||
subtitle?: string | null;
|
||||
expertiseLabel?: string | null;
|
||||
quote?: string | null;
|
||||
items?:
|
||||
| {
|
||||
title?: string | null;
|
||||
description?: string | null;
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
id?: string | null;
|
||||
blockName?: string | null;
|
||||
blockType: 'homeWhatWeDo';
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "HomeRecentPostsBlock".
|
||||
*/
|
||||
export interface HomeRecentPostsBlock {
|
||||
title?: string | null;
|
||||
subtitle?: string | null;
|
||||
id?: string | null;
|
||||
blockName?: string | null;
|
||||
blockType: 'homeRecentPosts';
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "HomeExperienceBlock".
|
||||
*/
|
||||
export interface HomeExperienceBlock {
|
||||
title?: string | null;
|
||||
subtitle?: string | null;
|
||||
paragraph1?: string | null;
|
||||
paragraph2?: string | null;
|
||||
badge1?: string | null;
|
||||
badge1Text?: string | null;
|
||||
badge2?: string | null;
|
||||
badge2Text?: string | null;
|
||||
id?: string | null;
|
||||
blockName?: string | null;
|
||||
blockType: 'homeExperience';
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "HomeWhyChooseUsBlock".
|
||||
*/
|
||||
export interface HomeWhyChooseUsBlock {
|
||||
title?: string | null;
|
||||
subtitle?: string | null;
|
||||
tagline?: string | null;
|
||||
features?:
|
||||
| {
|
||||
feature?: string | null;
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
items?:
|
||||
| {
|
||||
title?: string | null;
|
||||
description?: string | null;
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
id?: string | null;
|
||||
blockName?: string | null;
|
||||
blockType: 'homeWhyChooseUs';
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "HomeMeetTheTeamBlock".
|
||||
*/
|
||||
export interface HomeMeetTheTeamBlock {
|
||||
title?: string | null;
|
||||
subtitle?: string | null;
|
||||
description?: string | null;
|
||||
ctaLabel?: string | null;
|
||||
networkLabel?: string | null;
|
||||
id?: string | null;
|
||||
blockName?: string | null;
|
||||
blockType: 'homeMeetTheTeam';
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "HomeGalleryBlock".
|
||||
*/
|
||||
export interface HomeGalleryBlock {
|
||||
title?: string | null;
|
||||
subtitle?: string | null;
|
||||
id?: string | null;
|
||||
blockName?: string | null;
|
||||
blockType: 'homeGallery';
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "HomeVideoBlock".
|
||||
*/
|
||||
export interface HomeVideoBlock {
|
||||
title?: string | null;
|
||||
id?: string | null;
|
||||
blockName?: string | null;
|
||||
blockType: 'homeVideo';
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "HomeCTABlock".
|
||||
*/
|
||||
export interface HomeCTABlock {
|
||||
title?: string | null;
|
||||
subtitle?: string | null;
|
||||
description?: string | null;
|
||||
buttonLabel?: string | null;
|
||||
id?: string | null;
|
||||
blockName?: string | null;
|
||||
blockType: 'homeCTA';
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "CategoryGridBlock".
|
||||
*/
|
||||
export interface CategoryGridBlock {
|
||||
categories: {
|
||||
title: string;
|
||||
description?: string | null;
|
||||
image?: (number | null) | Media;
|
||||
icon?: (number | null) | Media;
|
||||
href: string;
|
||||
ctaLabel?: string | null;
|
||||
id?: string | null;
|
||||
}[];
|
||||
id?: string | null;
|
||||
blockName?: string | null;
|
||||
blockType: 'categoryGrid';
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "ContactSectionBlock".
|
||||
*/
|
||||
export interface ContactSectionBlock {
|
||||
showForm?: boolean | null;
|
||||
showMap?: boolean | null;
|
||||
showHours?: boolean | null;
|
||||
id?: string | null;
|
||||
blockName?: string | null;
|
||||
blockType: 'contactSection';
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "HeroSectionBlock".
|
||||
*/
|
||||
export interface HeroSectionBlock {
|
||||
badge?: string | null;
|
||||
title: string;
|
||||
subtitle?: string | null;
|
||||
backgroundImage?: (number | null) | Media;
|
||||
ctaLabel?: string | null;
|
||||
ctaHref?: string | null;
|
||||
alignment?: ('left' | 'center') | null;
|
||||
id?: string | null;
|
||||
blockName?: string | null;
|
||||
blockType: 'heroSection';
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "ImageGalleryBlock".
|
||||
*/
|
||||
export interface ImageGalleryBlock {
|
||||
images: {
|
||||
image: number | Media;
|
||||
alt?: string | null;
|
||||
id?: string | null;
|
||||
}[];
|
||||
id?: string | null;
|
||||
blockName?: string | null;
|
||||
blockType: 'imageGallery';
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "ManifestoGridBlock".
|
||||
*/
|
||||
export interface ManifestoGridBlock {
|
||||
title?: string | null;
|
||||
subtitle?: string | null;
|
||||
tagline?: string | null;
|
||||
items: {
|
||||
title: string;
|
||||
description: string;
|
||||
id?: string | null;
|
||||
}[];
|
||||
id?: string | null;
|
||||
blockName?: string | null;
|
||||
blockType: 'manifestoGrid';
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "TeamProfileBlock".
|
||||
*/
|
||||
export interface TeamProfileBlock {
|
||||
name: string;
|
||||
role: string;
|
||||
quote?: string | null;
|
||||
description?: string | null;
|
||||
image?: (number | null) | Media;
|
||||
linkedinUrl?: string | null;
|
||||
linkedinLabel?: string | null;
|
||||
layout?: ('imageRight' | 'imageLeft') | null;
|
||||
colorScheme?: ('dark' | 'light') | null;
|
||||
id?: string | null;
|
||||
blockName?: string | null;
|
||||
blockType: 'teamProfile';
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "auth".
|
||||
|
||||
@@ -37,6 +37,26 @@ export default buildConfig({
|
||||
importMap: {
|
||||
baseDir: path.resolve(dirname),
|
||||
},
|
||||
components: {
|
||||
graphics: {
|
||||
Logo: '/src/payload/components/Logo',
|
||||
Icon: '/src/payload/components/Icon',
|
||||
},
|
||||
},
|
||||
meta: {
|
||||
titleSuffix: ' – KLZ Cables',
|
||||
icons: [
|
||||
{ rel: 'icon', type: 'image/x-icon', url: '/favicon.ico' },
|
||||
],
|
||||
},
|
||||
},
|
||||
localization: {
|
||||
locales: [
|
||||
{ label: 'Deutsch', code: 'de' },
|
||||
{ label: 'English', code: 'en' },
|
||||
],
|
||||
defaultLocale: 'de',
|
||||
fallback: true,
|
||||
},
|
||||
collections: [Users, Media, Posts, FormSubmissions, Products, Pages],
|
||||
editor: lexicalEditor({
|
||||
|
||||
@@ -40,6 +40,7 @@ async function main() {
|
||||
console.log(`\n🕷️ Launching Puppeteer Headless Engine...`);
|
||||
const browser = await puppeteer.launch({
|
||||
headless: true,
|
||||
executablePath: process.env.PUPPETEER_EXECUTABLE_PATH || process.env.CHROME_PATH || undefined,
|
||||
args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage'],
|
||||
});
|
||||
|
||||
@@ -61,7 +62,7 @@ async function main() {
|
||||
const consoleErrorsList: Array<{ type: string; error: string; page: string }> = [];
|
||||
|
||||
// Listen for unhandled exceptions natively in the page
|
||||
page.on('pageerror', (err) => {
|
||||
page.on('pageerror', (err: any) => {
|
||||
consoleErrorsList.push({
|
||||
type: 'PAGE_ERROR',
|
||||
error: err.message,
|
||||
@@ -73,7 +74,7 @@ async function main() {
|
||||
// Listen for console.error and console.warn messages (like Next.js Image warnings, hydration errors, CSP blocks)
|
||||
page.on('console', (msg) => {
|
||||
const type = msg.type();
|
||||
if (type === 'error' || type === 'warning') {
|
||||
if (type === 'error' || type === 'warn') {
|
||||
const text = msg.text();
|
||||
|
||||
// Exclude common browser extension noise or third party tracker warnings
|
||||
|
||||
14
scripts/check-pages.ts
Normal file
14
scripts/check-pages.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { getPayload } from 'payload';
|
||||
import configPromise from '@payload-config';
|
||||
|
||||
async function run() {
|
||||
const payload = await getPayload({ config: configPromise });
|
||||
const result = await payload.find({ collection: 'pages', limit: 1 });
|
||||
const doc = result.docs[0] as any;
|
||||
console.log('Sample page:', doc.slug);
|
||||
console.log('Content structure (first 2 levels):');
|
||||
console.log(JSON.stringify(doc.content, null, 2).slice(0, 3000));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
run();
|
||||
14
scripts/check-start.ts
Normal file
14
scripts/check-start.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { getPayload } from 'payload';
|
||||
import configPromise from '@payload-config';
|
||||
|
||||
async function run() {
|
||||
const payload = await getPayload({ config: configPromise });
|
||||
const existing = await payload.find({
|
||||
collection: 'pages',
|
||||
where: { slug: { equals: 'start' }, locale: { equals: 'de' } },
|
||||
limit: 1,
|
||||
});
|
||||
console.log(JSON.stringify(existing.docs[0].content, null, 2).slice(0, 1500));
|
||||
process.exit(0);
|
||||
}
|
||||
run();
|
||||
14
scripts/check-team.ts
Normal file
14
scripts/check-team.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { getPayload } from 'payload';
|
||||
import configPromise from '@payload-config';
|
||||
|
||||
async function run() {
|
||||
const payload = await getPayload({ config: configPromise });
|
||||
const existing = await payload.find({
|
||||
collection: 'pages',
|
||||
where: { slug: { equals: 'team' }, locale: { equals: 'de' } },
|
||||
limit: 1,
|
||||
});
|
||||
console.log(JSON.stringify(existing.docs[0].content, null, 2).slice(0, 500));
|
||||
process.exit(0);
|
||||
}
|
||||
run();
|
||||
@@ -11,6 +11,28 @@
|
||||
# ────────────────────────────────────────────────────────────────────────────
|
||||
set -euo pipefail
|
||||
|
||||
SYNC_SUCCESS="false"
|
||||
LOCAL_BACKUP_FILE=""
|
||||
REMOTE_BACKUP_FILE=""
|
||||
|
||||
cleanup_on_exit() {
|
||||
local exit_code=$?
|
||||
if [ "$SYNC_SUCCESS" != "true" ] && [ $exit_code -ne 0 ]; then
|
||||
echo ""
|
||||
echo "❌ Sync aborted or failed! (Exit code: $exit_code)"
|
||||
if [ "${DIRECTION:-}" = "push" ] && [ -n "${REMOTE_BACKUP_FILE:-}" ]; then
|
||||
echo "🔄 Rolling back $TARGET database..."
|
||||
ssh "$SSH_HOST" "gunzip -c $REMOTE_BACKUP_FILE | docker exec -i $REMOTE_DB_CONTAINER psql -U $REMOTE_DB_USER -d $REMOTE_DB_NAME --quiet" || echo "⚠️ Rollback failed"
|
||||
echo "✅ Rollback complete."
|
||||
elif [ "${DIRECTION:-}" = "pull" ] && [ -n "${LOCAL_BACKUP_FILE:-}" ]; then
|
||||
echo "🔄 Rolling back local database..."
|
||||
gunzip -c "$LOCAL_BACKUP_FILE" | docker exec -i "$LOCAL_DB_CONTAINER" psql -U "$LOCAL_DB_USER" -d "$LOCAL_DB_NAME" --quiet || echo "⚠️ Rollback failed"
|
||||
echo "✅ Rollback complete."
|
||||
fi
|
||||
fi
|
||||
}
|
||||
trap 'cleanup_on_exit' EXIT
|
||||
|
||||
# Load environment variables
|
||||
if [ -f .env ]; then
|
||||
set -a; source .env; set +a
|
||||
@@ -48,6 +70,13 @@ resolve_target() {
|
||||
REMOTE_MEDIA_VOLUME="/var/lib/docker/volumes/klz-testing_klz_media_data/_data"
|
||||
REMOTE_SITE_DIR="/home/deploy/sites/testing.klz-cables.com"
|
||||
;;
|
||||
staging)
|
||||
REMOTE_PROJECT="klz-staging"
|
||||
REMOTE_DB_CONTAINER="klz-staging-klz-db-1"
|
||||
REMOTE_APP_CONTAINER="klz-staging-klz-app-1"
|
||||
REMOTE_MEDIA_VOLUME="/var/lib/docker/volumes/klz-staging_klz_media_data/_data"
|
||||
REMOTE_SITE_DIR="/home/deploy/sites/staging.klz-cables.com"
|
||||
;;
|
||||
prod|production)
|
||||
REMOTE_PROJECT="klz-cablescom"
|
||||
REMOTE_DB_CONTAINER="klz-cablescom-klz-db-1"
|
||||
@@ -57,7 +86,7 @@ resolve_target() {
|
||||
;;
|
||||
*)
|
||||
echo "❌ Unknown target: $TARGET"
|
||||
echo " Valid targets: testing, prod"
|
||||
echo " Valid targets: testing, staging, prod"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
@@ -118,6 +147,7 @@ backup_local_db() {
|
||||
echo "📦 Creating safety backup of local DB → $file"
|
||||
docker exec "$LOCAL_DB_CONTAINER" pg_dump -U "$LOCAL_DB_USER" -d "$LOCAL_DB_NAME" --clean --if-exists | gzip > "$file"
|
||||
echo "✅ Backup: $file ($(du -h "$file" | cut -f1))"
|
||||
LOCAL_BACKUP_FILE="$file"
|
||||
}
|
||||
|
||||
backup_remote_db() {
|
||||
@@ -125,6 +155,7 @@ backup_remote_db() {
|
||||
echo "📦 Creating safety backup of $TARGET DB → $SSH_HOST:$file"
|
||||
ssh "$SSH_HOST" "docker exec $REMOTE_DB_CONTAINER pg_dump -U $REMOTE_DB_USER -d $REMOTE_DB_NAME --clean --if-exists | gzip > $file"
|
||||
echo "✅ Remote backup: $file"
|
||||
REMOTE_BACKUP_FILE="$file"
|
||||
}
|
||||
|
||||
# ── PUSH: local → remote ──────────────────────────────────────────────────
|
||||
@@ -177,6 +208,7 @@ do_push() {
|
||||
rm -f "$dump"
|
||||
ssh "$SSH_HOST" "rm -f /tmp/payload_push.sql.gz"
|
||||
|
||||
SYNC_SUCCESS="true"
|
||||
echo ""
|
||||
echo "✅ Push to $TARGET complete!"
|
||||
}
|
||||
@@ -214,12 +246,13 @@ do_pull() {
|
||||
# 4. Sync media
|
||||
echo "🖼️ Syncing media files..."
|
||||
mkdir -p "$LOCAL_MEDIA_DIR"
|
||||
rsync -az --delete --info=progress2 "$SSH_HOST:$REMOTE_MEDIA_VOLUME/" "$LOCAL_MEDIA_DIR/"
|
||||
rsync -az --delete --progress "$SSH_HOST:$REMOTE_MEDIA_VOLUME/" "$LOCAL_MEDIA_DIR/"
|
||||
|
||||
# Cleanup
|
||||
rm -f "/tmp/payload_pull.sql.gz"
|
||||
ssh "$SSH_HOST" "rm -f /tmp/payload_pull.sql.gz"
|
||||
|
||||
SYNC_SUCCESS="true"
|
||||
echo ""
|
||||
echo "✅ Pull from $TARGET complete! Restart dev server to see changes."
|
||||
}
|
||||
@@ -230,8 +263,10 @@ if [ -z "$DIRECTION" ] || [ -z "$TARGET" ]; then
|
||||
echo ""
|
||||
echo "Usage:"
|
||||
echo " pnpm cms:push:testing Push local DB + media → testing"
|
||||
echo " pnpm cms:push:staging Push local DB + media → staging"
|
||||
echo " pnpm cms:push:prod Push local DB + media → production"
|
||||
echo " pnpm cms:pull:testing Pull testing DB + media → local"
|
||||
echo " pnpm cms:pull:staging Pull staging DB + media → local"
|
||||
echo " pnpm cms:pull:prod Pull production DB + media → local"
|
||||
echo ""
|
||||
echo "Safety: A backup is always created before overwriting."
|
||||
|
||||
33
scripts/create-home-blocks.js
Normal file
33
scripts/create-home-blocks.js
Normal file
@@ -0,0 +1,33 @@
|
||||
const fs = require('fs');
|
||||
|
||||
const blocks = [
|
||||
'HomeHero',
|
||||
'HomeProductCategories',
|
||||
'HomeWhatWeDo',
|
||||
'HomeRecentPosts',
|
||||
'HomeExperience',
|
||||
'HomeWhyChooseUs',
|
||||
'HomeMeetTheTeam',
|
||||
'HomeVideo',
|
||||
];
|
||||
|
||||
blocks.forEach(name => {
|
||||
const content = `import { Block } from 'payload';
|
||||
|
||||
export const ${name}: Block = {
|
||||
slug: '${name.charAt(0).toLowerCase() + name.slice(1)}',
|
||||
interfaceName: '${name}Block',
|
||||
fields: [
|
||||
{
|
||||
name: 'note',
|
||||
type: 'text',
|
||||
admin: {
|
||||
description: 'This is a dedicated layout block for the homepage wrapper. Content is managed via translation files.',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
`;
|
||||
fs.writeFileSync(\`src/payload/blocks/\${name}.ts\`, content);
|
||||
console.log(\`Created \${name}.ts\`);
|
||||
});
|
||||
260
scripts/merge-locale-duplicates.ts
Normal file
260
scripts/merge-locale-duplicates.ts
Normal file
@@ -0,0 +1,260 @@
|
||||
/**
|
||||
* merge-locale-duplicates.ts
|
||||
*
|
||||
* Merges duplicate DE/EN documents into single Payload localized documents.
|
||||
*
|
||||
* Problem: Before native localization, DE and EN were stored as separate rows.
|
||||
* Now each should be one document with locale-specific data in the *_locales tables.
|
||||
*
|
||||
* Strategy:
|
||||
* 1. Products: Match by slug → Keep DE row as canonical, copy EN data, delete EN row
|
||||
* 2. Posts: Match by slug → Same strategy
|
||||
* 3. Pages: Match by slug map (impressum↔legal-notice, blog↔blog, etc.) → Same strategy
|
||||
*/
|
||||
|
||||
import pg from 'pg';
|
||||
const { Pool } = pg;
|
||||
|
||||
const DB_URL =
|
||||
process.env.DATABASE_URI ||
|
||||
process.env.POSTGRES_URI ||
|
||||
`postgresql://payload:120in09oenaoinsd9iaidon@127.0.0.1:54322/payload`;
|
||||
|
||||
const pool = new Pool({ connectionString: DB_URL });
|
||||
|
||||
async function q<T = any>(query: string, values: unknown[] = []): Promise<T[]> {
|
||||
const result = await pool.query(query, values);
|
||||
return result.rows as T[];
|
||||
}
|
||||
|
||||
async function mergeProducts() {
|
||||
console.log('\n── PRODUCTS ───────────────────────────────────────');
|
||||
|
||||
const pairs = await q<{ de_id: number; en_id: number; slug: string }>(`
|
||||
SELECT
|
||||
de.id as de_id,
|
||||
en.id as en_id,
|
||||
de_loc.slug as slug
|
||||
FROM products de
|
||||
JOIN products_locales de_loc ON de_loc._parent_id = de.id AND de_loc._locale = 'de'
|
||||
JOIN products_locales en_loc ON en_loc.slug = de_loc.slug AND en_loc._locale = 'en'
|
||||
JOIN products en ON en.id = en_loc._parent_id
|
||||
WHERE de.id != en.id
|
||||
`);
|
||||
|
||||
console.log(`Found ${pairs.length} DE/EN product pairs to merge`);
|
||||
|
||||
for (const { de_id, en_id, slug } of pairs) {
|
||||
console.log(` Merging: ${slug} (DE id=${de_id} ← EN id=${en_id})`);
|
||||
|
||||
const [enData] = await q(`
|
||||
SELECT * FROM products_locales WHERE _parent_id = $1 AND _locale = 'en'
|
||||
`, [en_id]);
|
||||
|
||||
if (enData) {
|
||||
await q(`
|
||||
INSERT INTO products_locales (title, description, application, content, _locale, _parent_id)
|
||||
VALUES ($1, $2, $3, $4, 'en', $5)
|
||||
ON CONFLICT (_locale, _parent_id) DO UPDATE
|
||||
SET title = EXCLUDED.title,
|
||||
description = EXCLUDED.description,
|
||||
application = EXCLUDED.application,
|
||||
content = EXCLUDED.content
|
||||
`, [enData.title, enData.description, enData.application, enData.content, de_id]);
|
||||
}
|
||||
|
||||
// Move categories from EN to DE if DE has none
|
||||
await q(`
|
||||
UPDATE products_categories SET _parent_id = $1
|
||||
WHERE _parent_id = $2
|
||||
AND NOT EXISTS (SELECT 1 FROM products_categories WHERE _parent_id = $1)
|
||||
`, [de_id, en_id]);
|
||||
|
||||
// Move images (rels) from EN to DE if DE has none
|
||||
await q(`
|
||||
UPDATE products_rels SET parent = $1
|
||||
WHERE parent = $2
|
||||
AND NOT EXISTS (SELECT 1 FROM products_rels WHERE parent = $1)
|
||||
`, [de_id, en_id]);
|
||||
|
||||
// Copy featuredImage if DE is missing one
|
||||
await q(`
|
||||
UPDATE products SET featured_image_id = (
|
||||
SELECT featured_image_id FROM products WHERE id = $2
|
||||
)
|
||||
WHERE id = $1 AND featured_image_id IS NULL
|
||||
`, [de_id, en_id]);
|
||||
|
||||
// Delete EN locale entry and EN product row
|
||||
await q(`DELETE FROM products_locales WHERE _parent_id = $1`, [en_id]);
|
||||
await q(`DELETE FROM _products_v WHERE parent = $1`, [en_id]);
|
||||
await q(`DELETE FROM products WHERE id = $1`, [en_id]);
|
||||
|
||||
console.log(` ✓ ${slug}`);
|
||||
}
|
||||
|
||||
const [{ count }] = await q(`SELECT count(*) FROM products`);
|
||||
console.log(`Products remaining: ${count}`);
|
||||
}
|
||||
|
||||
async function mergePosts() {
|
||||
console.log('\n── POSTS ──────────────────────────────────────────');
|
||||
|
||||
const pairs = await q<{ de_id: number; en_id: number; slug: string }>(`
|
||||
SELECT
|
||||
de.id as de_id,
|
||||
en.id as en_id,
|
||||
de_loc.slug as slug
|
||||
FROM posts de
|
||||
JOIN posts_locales de_loc ON de_loc._parent_id = de.id AND de_loc._locale = 'de'
|
||||
JOIN posts_locales en_loc ON en_loc.slug = de_loc.slug AND en_loc._locale = 'en'
|
||||
JOIN posts en ON en.id = en_loc._parent_id
|
||||
WHERE de.id != en.id
|
||||
`);
|
||||
|
||||
console.log(`Found ${pairs.length} DE/EN post pairs to merge`);
|
||||
|
||||
for (const { de_id, en_id, slug } of pairs) {
|
||||
console.log(` Merging post: ${slug} (DE id=${de_id} ← EN id=${en_id})`);
|
||||
|
||||
const [enData] = await q(`
|
||||
SELECT * FROM posts_locales WHERE _parent_id = $1 AND _locale = 'en'
|
||||
`, [en_id]);
|
||||
|
||||
if (enData) {
|
||||
await q(`
|
||||
INSERT INTO posts_locales (title, slug, excerpt, category, content, _locale, _parent_id)
|
||||
VALUES ($1, $2, $3, $4, $5, 'en', $6)
|
||||
ON CONFLICT (_locale, _parent_id) DO UPDATE
|
||||
SET title = EXCLUDED.title, slug = EXCLUDED.slug,
|
||||
excerpt = EXCLUDED.excerpt, category = EXCLUDED.category,
|
||||
content = EXCLUDED.content
|
||||
`, [enData.title, enData.slug, enData.excerpt, enData.category, enData.content, de_id]);
|
||||
}
|
||||
|
||||
// Copy featuredImage/date from EN if DE is missing
|
||||
await q(`
|
||||
UPDATE posts SET
|
||||
featured_image_id = COALESCE(featured_image_id, (SELECT featured_image_id FROM posts WHERE id = $2)),
|
||||
date = COALESCE(date, (SELECT date FROM posts WHERE id = $2))
|
||||
WHERE id = $1
|
||||
`, [de_id, en_id]);
|
||||
|
||||
await q(`DELETE FROM posts_locales WHERE _parent_id = $1`, [en_id]);
|
||||
await q(`DELETE FROM _posts_v WHERE parent = $1`, [en_id]);
|
||||
await q(`DELETE FROM posts WHERE id = $1`, [en_id]);
|
||||
|
||||
console.log(` ✓ ${slug}`);
|
||||
}
|
||||
|
||||
const [{ count }] = await q(`SELECT count(*) FROM posts`);
|
||||
console.log(`Posts remaining: ${count}`);
|
||||
}
|
||||
|
||||
// DE slug → EN slug mapping for pages
|
||||
const PAGE_SLUG_MAP: Record<string, string> = {
|
||||
impressum: 'legal-notice',
|
||||
datenschutz: 'privacy-policy',
|
||||
agbs: 'terms',
|
||||
kontakt: 'contact',
|
||||
produkte: 'products',
|
||||
blog: 'blog',
|
||||
team: 'team',
|
||||
start: 'start',
|
||||
danke: 'thanks',
|
||||
};
|
||||
|
||||
async function mergePages() {
|
||||
console.log('\n── PAGES ──────────────────────────────────────────');
|
||||
|
||||
for (const [deSlug, enSlug] of Object.entries(PAGE_SLUG_MAP)) {
|
||||
const [dePage] = await q<{ id: number }>(`
|
||||
SELECT p.id FROM pages p
|
||||
JOIN pages_locales pl ON pl._parent_id = p.id AND pl._locale = 'de' AND pl.slug = $1
|
||||
LIMIT 1
|
||||
`, [deSlug]);
|
||||
|
||||
const [enPage] = await q<{ id: number }>(`
|
||||
SELECT p.id FROM pages p
|
||||
JOIN pages_locales pl ON pl._parent_id = p.id AND pl._locale = 'en' AND pl.slug = $1
|
||||
LIMIT 1
|
||||
`, [enSlug]);
|
||||
|
||||
if (!dePage && !enPage) {
|
||||
console.log(` ⚠ No page found for ${deSlug}/${enSlug} — skipping`);
|
||||
continue;
|
||||
}
|
||||
if (!dePage) {
|
||||
console.log(` ⚠ No DE page for "${deSlug}" — EN-only page id=${enPage!.id} kept`);
|
||||
continue;
|
||||
}
|
||||
if (!enPage) {
|
||||
console.log(` ⚠ No EN page for "${enSlug}" — DE-only page id=${dePage.id} kept`);
|
||||
continue;
|
||||
}
|
||||
if (dePage.id === enPage.id) {
|
||||
console.log(` ✓ ${deSlug}/${enSlug} already merged (id=${dePage.id})`);
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log(` Merging: ${deSlug}↔${enSlug} (DE id=${dePage.id} ← EN id=${enPage.id})`);
|
||||
|
||||
const [enData] = await q(`
|
||||
SELECT * FROM pages_locales WHERE _parent_id = $1 AND _locale = 'en'
|
||||
`, [enPage.id]);
|
||||
|
||||
if (enData) {
|
||||
await q(`
|
||||
INSERT INTO pages_locales (title, slug, excerpt, content, _locale, _parent_id)
|
||||
VALUES ($1, $2, $3, $4, 'en', $5)
|
||||
ON CONFLICT (_locale, _parent_id) DO UPDATE
|
||||
SET title = EXCLUDED.title, slug = EXCLUDED.slug,
|
||||
excerpt = EXCLUDED.excerpt, content = EXCLUDED.content
|
||||
`, [enData.title, enData.slug, enData.excerpt, enData.content, dePage.id]);
|
||||
}
|
||||
|
||||
// Copy featuredImage/layout from EN if DE is missing
|
||||
await q(`
|
||||
UPDATE pages SET
|
||||
featured_image_id = COALESCE(featured_image_id, (SELECT featured_image_id FROM pages WHERE id = $2)),
|
||||
layout = COALESCE(layout, (SELECT layout FROM pages WHERE id = $2))
|
||||
WHERE id = $1
|
||||
`, [dePage.id, enPage.id]);
|
||||
|
||||
await q(`DELETE FROM pages_locales WHERE _parent_id = $1`, [enPage.id]);
|
||||
await q(`DELETE FROM _pages_v WHERE parent = $1`, [enPage.id]);
|
||||
await q(`DELETE FROM pages WHERE id = $1`, [enPage.id]);
|
||||
|
||||
console.log(` ✓ ${deSlug}/${enSlug}`);
|
||||
}
|
||||
|
||||
const [{ count }] = await q(`SELECT count(*) FROM pages`);
|
||||
console.log(`Pages remaining: ${count}`);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('🔀 Merging duplicate locale documents into native Payload localization...');
|
||||
|
||||
try {
|
||||
await mergeProducts();
|
||||
await mergePosts();
|
||||
await mergePages();
|
||||
|
||||
console.log('\n── Final pages state ──────────────────────────────');
|
||||
const pages = await q(`
|
||||
SELECT p.id, pl._locale, pl.slug, pl.title FROM pages p
|
||||
JOIN pages_locales pl ON pl._parent_id = p.id
|
||||
ORDER BY p.id, pl._locale
|
||||
`);
|
||||
pages.forEach((r) => console.log(` [id=${r.id}] ${r._locale}: ${r.slug} — ${r.title}`));
|
||||
|
||||
console.log('\n✅ Done!');
|
||||
} finally {
|
||||
await pool.end();
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error('Fatal error:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,152 +0,0 @@
|
||||
import { getPayload } from 'payload';
|
||||
import configPromise from '@payload-config';
|
||||
import * as dotenv from 'dotenv';
|
||||
dotenv.config();
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import matter from 'gray-matter';
|
||||
import { parseMarkdownToLexical } from '../src/payload/utils/lexicalParser';
|
||||
|
||||
async function mapImageToMediaId(payload: any, imagePath: string): Promise<string | null> {
|
||||
if (!imagePath) return null;
|
||||
const filename = path.basename(imagePath);
|
||||
|
||||
const media = await payload.find({
|
||||
collection: 'media',
|
||||
where: {
|
||||
filename: {
|
||||
equals: filename,
|
||||
},
|
||||
},
|
||||
limit: 1,
|
||||
});
|
||||
|
||||
if (media.docs.length > 0) {
|
||||
return media.docs[0].id;
|
||||
}
|
||||
|
||||
// Auto-ingest missing images from legacy public/ directory
|
||||
const cleanPath = imagePath.startsWith('/') ? imagePath.slice(1) : imagePath;
|
||||
const fullPath = path.join(process.cwd(), 'public', cleanPath);
|
||||
|
||||
if (fs.existsSync(fullPath)) {
|
||||
try {
|
||||
console.log(`[Blog Migration] 📤 Ingesting missing Media into Payload: ${filename}`);
|
||||
const newMedia = await payload.create({
|
||||
collection: 'media',
|
||||
data: {
|
||||
alt: filename.replace(/[-_]/g, ' ').replace(/\.[^/.]+$/, ''), // create a human readable alt text
|
||||
},
|
||||
filePath: fullPath,
|
||||
});
|
||||
return newMedia.id;
|
||||
} catch (err: any) {
|
||||
console.error(`[Blog Migration] ❌ Failed to ingest ${filename}:`, err);
|
||||
}
|
||||
} else {
|
||||
console.warn(`[Blog Migration] ⚠️ Missing image entirely from disk: ${fullPath}`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function migrateBlogPosts() {
|
||||
console.log('[Blog Migration] 🔍 Using POSTGRES_URI:', process.env.POSTGRES_URI || 'NOT SET');
|
||||
console.log('[Blog Migration] 🔍 Using DATABASE_URI:', process.env.DATABASE_URI || 'NOT SET');
|
||||
|
||||
let payload;
|
||||
try {
|
||||
payload = await getPayload({ config: configPromise });
|
||||
} catch (err: any) {
|
||||
console.error('[Blog Migration] ❌ Failed to initialize Payload:', err);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const locales = ['en', 'de'];
|
||||
for (const locale of locales) {
|
||||
const postsDir = path.join(process.cwd(), 'data', 'blog', locale);
|
||||
if (!fs.existsSync(postsDir)) continue;
|
||||
|
||||
const files = fs.readdirSync(postsDir);
|
||||
for (const file of files) {
|
||||
if (!file.endsWith('.mdx')) continue;
|
||||
|
||||
const slug = file.replace(/\.mdx$/, '');
|
||||
const filePath = path.join(postsDir, file);
|
||||
const fileContent = fs.readFileSync(filePath, 'utf8');
|
||||
const { data, content } = matter(fileContent);
|
||||
|
||||
console.log(`Migrating ${locale}/${slug}...`);
|
||||
|
||||
const lexicalBlocks = parseMarkdownToLexical(content);
|
||||
const lexicalAST = {
|
||||
root: {
|
||||
type: 'root',
|
||||
format: '',
|
||||
indent: 0,
|
||||
version: 1,
|
||||
children: lexicalBlocks,
|
||||
direction: 'ltr',
|
||||
},
|
||||
};
|
||||
|
||||
const publishDate = data.date ? new Date(data.date).toISOString() : new Date().toISOString();
|
||||
const status = data.public === false ? 'draft' : 'published';
|
||||
|
||||
let featuredImageId = null;
|
||||
if (data.featuredImage || data.image) {
|
||||
featuredImageId = await mapImageToMediaId(payload, data.featuredImage || data.image);
|
||||
}
|
||||
|
||||
try {
|
||||
// Find existing post
|
||||
const existing = await payload.find({
|
||||
collection: 'posts',
|
||||
where: { slug: { equals: slug }, locale: { equals: locale } as any },
|
||||
});
|
||||
|
||||
if (slug.includes('welcome-to-the-future')) {
|
||||
console.log(`\n--- AST for ${slug} ---`);
|
||||
console.log(JSON.stringify(lexicalAST, null, 2));
|
||||
console.log(`-----------------------\n`);
|
||||
}
|
||||
|
||||
if (existing.docs.length > 0) {
|
||||
await payload.update({
|
||||
collection: 'posts',
|
||||
id: existing.docs[0].id,
|
||||
data: {
|
||||
content: lexicalAST as any,
|
||||
_status: status as any,
|
||||
...(featuredImageId ? { featuredImage: featuredImageId } : {}),
|
||||
},
|
||||
});
|
||||
console.log(`✅ AST Components & Image RE-INJECTED for ${slug}`);
|
||||
} else {
|
||||
await payload.create({
|
||||
collection: 'posts',
|
||||
data: {
|
||||
title: data.title,
|
||||
slug: slug,
|
||||
locale: locale,
|
||||
date: publishDate,
|
||||
category: data.category || '',
|
||||
excerpt: data.excerpt || '',
|
||||
content: lexicalAST as any,
|
||||
_status: status as any,
|
||||
...(featuredImageId ? { featuredImage: featuredImageId } : {}),
|
||||
},
|
||||
});
|
||||
console.log(`✅ Created ${slug}`);
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error(`❌ Failed ${slug}`, err.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Migration completed.');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
migrateBlogPosts().catch(console.error);
|
||||
@@ -1,156 +0,0 @@
|
||||
import { getPayload } from 'payload';
|
||||
import configPromise from '../payload.config';
|
||||
import * as dotenv from 'dotenv';
|
||||
dotenv.config();
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import matter from 'gray-matter';
|
||||
import { parseMarkdownToLexical } from '../src/payload/utils/lexicalParser';
|
||||
|
||||
async function mapImageToMediaId(payload: any, imagePath: string): Promise<string | null> {
|
||||
if (!imagePath) return null;
|
||||
const filename = path.basename(imagePath);
|
||||
|
||||
// Exact match instead of substring to avoid matching "cable-black.jpg" with "cable.jpg"
|
||||
const media = await payload.find({
|
||||
collection: 'media',
|
||||
where: {
|
||||
filename: {
|
||||
equals: filename,
|
||||
},
|
||||
},
|
||||
limit: 1,
|
||||
});
|
||||
|
||||
if (media.docs.length > 0) {
|
||||
return media.docs[0].id;
|
||||
}
|
||||
|
||||
const cleanPath = imagePath.startsWith('/') ? imagePath.slice(1) : imagePath;
|
||||
const fullPath = path.join(process.cwd(), 'public', cleanPath);
|
||||
|
||||
if (fs.existsSync(fullPath)) {
|
||||
try {
|
||||
console.log(`[Products Migration] 📤 Ingesting missing Media into Payload: ${filename}`);
|
||||
const newMedia = await payload.create({
|
||||
collection: 'media',
|
||||
data: {
|
||||
alt: filename.replace(/[-_]/g, ' ').replace(/\.[^/.]+$/, ''),
|
||||
},
|
||||
filePath: fullPath,
|
||||
});
|
||||
return newMedia.id;
|
||||
} catch (err: any) {
|
||||
console.error(`[Products Migration] ❌ Failed to ingest ${filename}:`, err);
|
||||
}
|
||||
} else {
|
||||
console.warn(`[Products Migration] ⚠️ Missing image entirely from disk: ${fullPath}`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function migrateProducts() {
|
||||
const payload = await getPayload({ config: configPromise });
|
||||
const productLocales = ['en', 'de'];
|
||||
|
||||
for (const locale of productLocales) {
|
||||
const productsDir = path.join(process.cwd(), 'data', 'products', locale);
|
||||
if (!fs.existsSync(productsDir)) continue;
|
||||
|
||||
// Recursive file finder
|
||||
const mdFiles: string[] = [];
|
||||
const walk = (dir: string) => {
|
||||
const files = fs.readdirSync(dir);
|
||||
for (const file of files) {
|
||||
const fullPath = path.join(dir, file);
|
||||
const stat = fs.statSync(fullPath);
|
||||
if (stat.isDirectory()) {
|
||||
walk(fullPath);
|
||||
} else if (file.endsWith('.mdx')) {
|
||||
mdFiles.push(fullPath);
|
||||
}
|
||||
}
|
||||
};
|
||||
walk(productsDir);
|
||||
|
||||
for (const filePath of mdFiles) {
|
||||
const fileContent = fs.readFileSync(filePath, 'utf8');
|
||||
const { data, content } = matter(fileContent);
|
||||
|
||||
console.log(`Processing Product: [${locale.toUpperCase()}] ${data.title}`);
|
||||
|
||||
// 1. Process Images
|
||||
const mediaIds = [];
|
||||
if (data.images && Array.isArray(data.images)) {
|
||||
for (const imgPath of data.images) {
|
||||
const id = await mapImageToMediaId(payload, imgPath);
|
||||
if (id) mediaIds.push(id);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Map Lexical AST for deeply nested components (like ProductTabs + Technical data)
|
||||
const lexicalContent = parseMarkdownToLexical(content);
|
||||
|
||||
const wrapLexical = (blocks: any[]) => ({
|
||||
root: {
|
||||
type: 'root',
|
||||
format: '',
|
||||
indent: 0,
|
||||
version: 1,
|
||||
children: blocks,
|
||||
direction: 'ltr',
|
||||
},
|
||||
});
|
||||
|
||||
// Payload expects category objects via the 'category' key
|
||||
const formattedCategories = Array.isArray(data.categories)
|
||||
? data.categories.map((c: string) => ({ category: c }))
|
||||
: [];
|
||||
|
||||
const productData = {
|
||||
title: data.title,
|
||||
sku: data.sku || path.basename(filePath, '.mdx'),
|
||||
slug: path.basename(filePath, '.mdx'),
|
||||
locale: locale as 'en' | 'de',
|
||||
categories: formattedCategories,
|
||||
description: data.description || '',
|
||||
featuredImage: mediaIds.length > 0 ? mediaIds[0] : undefined,
|
||||
images: mediaIds.length > 0 ? mediaIds : undefined,
|
||||
content: wrapLexical(lexicalContent) as any,
|
||||
application: data.application
|
||||
? (wrapLexical(parseMarkdownToLexical(data.application)) as any)
|
||||
: undefined,
|
||||
_status: 'published' as any,
|
||||
};
|
||||
|
||||
// Check if product exists (by sku combined with locale, since slug may differ by language)
|
||||
const existing = await payload.find({
|
||||
collection: 'products',
|
||||
where: {
|
||||
and: [{ slug: { equals: productData.slug } }, { locale: { equals: locale } }],
|
||||
},
|
||||
});
|
||||
|
||||
if (existing.docs.length > 0) {
|
||||
console.log(`Updating existing product ${productData.slug} (${locale})`);
|
||||
await payload.update({
|
||||
collection: 'products',
|
||||
id: existing.docs[0].id,
|
||||
data: productData,
|
||||
});
|
||||
} else {
|
||||
console.log(`Creating new product ${productData.slug} (${locale})`);
|
||||
await payload.create({
|
||||
collection: 'products',
|
||||
data: productData,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\n✅ Products Migration Complete!`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
migrateProducts().catch(console.error);
|
||||
@@ -86,7 +86,7 @@ async function main() {
|
||||
|
||||
// Using a more robust way to execute and capture output
|
||||
// We remove 'npx lhci upload' to keep everything local and avoid Google-hosted reports
|
||||
const lhciCommand = `npx lhci collect ${urlArgs} ${chromePathArg} --config=config/lighthouserc.json --collect.settings.extraHeaders='${extraHeaders}' && npx lhci assert --config=config/lighthouserc.json`;
|
||||
const lhciCommand = `npx lhci collect ${urlArgs} ${chromePathArg} --config=config/lighthouserc.json --collect.settings.extraHeaders='${extraHeaders}' --collect.settings.chromeFlags="--no-sandbox --disable-setuid-sandbox --disable-dev-shm-usage" && npx lhci assert --config=config/lighthouserc.json`;
|
||||
|
||||
console.log(`💻 Executing LHCI...`);
|
||||
|
||||
|
||||
131
scripts/seed-home.ts
Normal file
131
scripts/seed-home.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* Migration: Seed homepage ('start') as Lexical block content into Payload CMS.
|
||||
*
|
||||
* Usage:
|
||||
* pnpm tsx scripts/seed-home.ts
|
||||
*/
|
||||
import { getPayload } from 'payload';
|
||||
import configPromise from '@payload-config';
|
||||
|
||||
function lexicalBlock(blockType: string, fields: Record<string, any> = {}) {
|
||||
return {
|
||||
type: 'block',
|
||||
version: 2,
|
||||
fields: {
|
||||
blockType,
|
||||
...fields,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function lexicalDoc(blocks: any[]) {
|
||||
return {
|
||||
root: {
|
||||
type: 'root',
|
||||
format: '',
|
||||
indent: 0,
|
||||
version: 1,
|
||||
children: blocks,
|
||||
direction: 'ltr',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const PAGES = [
|
||||
// ─── Homepage (DE) ─────────────────────────────────────────
|
||||
{
|
||||
title: 'Startseite',
|
||||
slug: 'start',
|
||||
locale: 'de',
|
||||
layout: 'fullBleed',
|
||||
excerpt: 'Ihr Experte für hochwertige Stromkabel, Mittelspannungslösungen und Solarkabel. Zuverlässige Infrastruktur für eine grüne Energiezukunft.',
|
||||
_status: 'published',
|
||||
content: lexicalDoc([
|
||||
lexicalBlock('homeHero', { note: 'Hero section with primary video and CTA.' }),
|
||||
lexicalBlock('homeProductCategories', { note: 'Product categories overview based on CMS data.' }),
|
||||
lexicalBlock('homeWhatWeDo', { note: 'What we do / capabilities overview.' }),
|
||||
lexicalBlock('homeRecentPosts', { note: 'Latest 3 blog articles snippet.' }),
|
||||
lexicalBlock('homeExperience', { note: 'Experience and history timeline snippet.' }),
|
||||
lexicalBlock('homeWhyChooseUs', { note: 'Why choose KLZ Cables metrics and selling points.' }),
|
||||
lexicalBlock('homeMeetTheTeam', { note: 'High-level Meet the Team teaser.' }),
|
||||
lexicalBlock('homeGallery', { note: 'Image gallery from our facilities.' }),
|
||||
lexicalBlock('homeVideo', { note: 'Secondary informative background video.' }),
|
||||
lexicalBlock('homeCTA', { note: 'Bottom call to action linking to contact.' }),
|
||||
]),
|
||||
},
|
||||
// ─── Homepage (EN) ─────────────────────────────────────────
|
||||
{
|
||||
title: 'Homepage',
|
||||
slug: 'start',
|
||||
locale: 'en',
|
||||
layout: 'fullBleed',
|
||||
excerpt: 'Your expert for high-quality power cables, medium voltage solutions, and solar cables. Reliable infrastructure for a green energy future.',
|
||||
_status: 'published',
|
||||
content: lexicalDoc([
|
||||
lexicalBlock('homeHero', { note: 'Hero section with primary video and CTA.' }),
|
||||
lexicalBlock('homeProductCategories', { note: 'Product categories overview based on CMS data.' }),
|
||||
lexicalBlock('homeWhatWeDo', { note: 'What we do / capabilities overview.' }),
|
||||
lexicalBlock('homeRecentPosts', { note: 'Latest 3 blog articles snippet.' }),
|
||||
lexicalBlock('homeExperience', { note: 'Experience and history timeline snippet.' }),
|
||||
lexicalBlock('homeWhyChooseUs', { note: 'Why choose KLZ Cables metrics and selling points.' }),
|
||||
lexicalBlock('homeMeetTheTeam', { note: 'High-level Meet the Team teaser.' }),
|
||||
lexicalBlock('homeGallery', { note: 'Image gallery from our facilities.' }),
|
||||
lexicalBlock('homeVideo', { note: 'Secondary informative background video.' }),
|
||||
lexicalBlock('homeCTA', { note: 'Bottom call to action linking to contact.' }),
|
||||
]),
|
||||
},
|
||||
];
|
||||
|
||||
async function seedHome() {
|
||||
const payload = await getPayload({ config: configPromise });
|
||||
|
||||
for (const page of PAGES) {
|
||||
const existing = await payload.find({
|
||||
collection: 'pages',
|
||||
where: {
|
||||
slug: { equals: page.slug },
|
||||
locale: { equals: page.locale },
|
||||
},
|
||||
limit: 1,
|
||||
});
|
||||
|
||||
const docs = existing.docs as any[];
|
||||
|
||||
if (docs.length > 0) {
|
||||
await payload.update({
|
||||
collection: 'pages',
|
||||
id: docs[0].id,
|
||||
data: {
|
||||
title: page.title,
|
||||
layout: page.layout as any,
|
||||
excerpt: page.excerpt,
|
||||
_status: page._status as any,
|
||||
content: page.content as any,
|
||||
},
|
||||
});
|
||||
console.log(`✅ Updated: ${page.slug} (${page.locale})`);
|
||||
} else {
|
||||
await payload.create({
|
||||
collection: 'pages',
|
||||
data: {
|
||||
title: page.title,
|
||||
slug: page.slug,
|
||||
locale: page.locale,
|
||||
layout: page.layout as any,
|
||||
excerpt: page.excerpt,
|
||||
_status: page._status as any,
|
||||
content: page.content as any,
|
||||
},
|
||||
});
|
||||
console.log(`✅ Created: ${page.slug} (${page.locale})`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n🎉 Homepage seeded successfully!');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
seedHome().catch((err) => {
|
||||
console.error('❌ Seed failed:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
242
scripts/seed-pages.ts
Normal file
242
scripts/seed-pages.ts
Normal file
@@ -0,0 +1,242 @@
|
||||
/**
|
||||
* Migration: Seed team, contact, and other missing pages as Lexical block content into Payload CMS.
|
||||
*
|
||||
* Usage:
|
||||
* pnpm tsx scripts/seed-pages.ts
|
||||
*/
|
||||
import { getPayload } from 'payload';
|
||||
import configPromise from '@payload-config';
|
||||
|
||||
function lexicalBlock(blockType: string, fields: Record<string, any>) {
|
||||
return {
|
||||
type: 'block',
|
||||
version: 2,
|
||||
fields: {
|
||||
blockType,
|
||||
...fields,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function lexicalDoc(blocks: any[]) {
|
||||
return {
|
||||
root: {
|
||||
type: 'root',
|
||||
format: '',
|
||||
indent: 0,
|
||||
version: 1,
|
||||
children: blocks,
|
||||
direction: 'ltr',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const PAGES = [
|
||||
// ─── Team (DE) ────────────────────────────────────────────
|
||||
{
|
||||
title: 'Team',
|
||||
slug: 'team',
|
||||
locale: 'de',
|
||||
layout: 'fullBleed',
|
||||
excerpt: '',
|
||||
_status: 'published',
|
||||
content: lexicalDoc([
|
||||
lexicalBlock('heroSection', {
|
||||
badge: 'Das Team',
|
||||
title: 'Tradition trifft Moderne',
|
||||
subtitle: 'Zwei Generationen, eine Vision: Deutschlands zuverlässigster Partner für Kabel & Leitungen.',
|
||||
alignment: 'center',
|
||||
}),
|
||||
lexicalBlock('teamProfile', {
|
||||
name: 'Michael Bodemer',
|
||||
role: 'Geschäftsführer',
|
||||
quote: 'Innovation entsteht dort, wo Erfahrung auf frische Ideen trifft.',
|
||||
description: 'Als Geschäftsführer verbindet Michael jahrzehntelange Branchenexpertise mit einem klaren Blick für die Zukunft. Sein Fokus liegt auf nachhaltigen Lösungen und modernster Technologie.',
|
||||
linkedinUrl: 'https://www.linkedin.com/in/michael-bodemer-33b493122/',
|
||||
layout: 'imageRight',
|
||||
colorScheme: 'dark',
|
||||
}),
|
||||
lexicalBlock('stats', {
|
||||
stats: [
|
||||
{ value: '30+', label: 'Jahre Expertise' },
|
||||
{ value: 'Global', label: 'Netzwerk' },
|
||||
],
|
||||
}),
|
||||
lexicalBlock('teamProfile', {
|
||||
name: 'Klaus Mintel',
|
||||
role: 'Gründer & Berater',
|
||||
quote: 'Qualität ist kein Zufall – sie ist das Ergebnis von Engagement und Erfahrung.',
|
||||
description: 'Klaus gründete KLZ Cables und hat das Unternehmen zu einem der zuverlässigsten Partner der Kabelindustrie aufgebaut. Er bringt Jahrzehnte an Expertise ein.',
|
||||
linkedinUrl: 'https://www.linkedin.com/in/klaus-mintel-b80a8b193/',
|
||||
layout: 'imageLeft',
|
||||
colorScheme: 'light',
|
||||
}),
|
||||
lexicalBlock('manifestoGrid', {
|
||||
title: 'Unsere Werte',
|
||||
subtitle: 'Was uns antreibt',
|
||||
tagline: 'Seit der Gründung leiten uns klare Prinzipien, die wir jeden Tag leben.',
|
||||
items: [
|
||||
{ title: 'Qualität', description: 'Wir liefern nur Produkte, die höchsten Standards entsprechen.' },
|
||||
{ title: 'Zuverlässigkeit', description: 'Termingerechte Lieferung ist für uns selbstverständlich.' },
|
||||
{ title: 'Partnerschaft', description: 'Langfristige Beziehungen sind die Grundlage unseres Erfolgs.' },
|
||||
{ title: 'Innovation', description: 'Wir investieren in neue Technologien und nachhaltige Lösungen.' },
|
||||
{ title: 'Transparenz', description: 'Offene Kommunikation und faire Preise zeichnen uns aus.' },
|
||||
{ title: 'Nachhaltigkeit', description: 'Verantwortung für Umwelt und Gesellschaft ist Teil unserer DNA.' },
|
||||
],
|
||||
}),
|
||||
// Removed the imageGallery since it requires at least 1 image and we don't have media upload IDs yet.
|
||||
]),
|
||||
},
|
||||
// ─── Team (EN) ────────────────────────────────────────────
|
||||
{
|
||||
title: 'Team',
|
||||
slug: 'team',
|
||||
locale: 'en',
|
||||
layout: 'fullBleed',
|
||||
excerpt: '',
|
||||
_status: 'published',
|
||||
content: lexicalDoc([
|
||||
lexicalBlock('heroSection', {
|
||||
badge: 'The Team',
|
||||
title: 'Tradition Meets Innovation',
|
||||
subtitle: 'Two generations, one vision: Germany\'s most reliable partner for cables & wiring.',
|
||||
alignment: 'center',
|
||||
}),
|
||||
lexicalBlock('teamProfile', {
|
||||
name: 'Michael Bodemer',
|
||||
role: 'Managing Director',
|
||||
quote: 'Innovation happens where experience meets fresh ideas.',
|
||||
description: 'As Managing Director, Michael combines decades of industry expertise with a clear vision for the future. His focus is on sustainable solutions and cutting-edge technology.',
|
||||
linkedinUrl: 'https://www.linkedin.com/in/michael-bodemer-33b493122/',
|
||||
layout: 'imageRight',
|
||||
colorScheme: 'dark',
|
||||
}),
|
||||
lexicalBlock('stats', {
|
||||
stats: [
|
||||
{ value: '30+', label: 'Years of Expertise' },
|
||||
{ value: 'Global', label: 'Network' },
|
||||
],
|
||||
}),
|
||||
lexicalBlock('teamProfile', {
|
||||
name: 'Klaus Mintel',
|
||||
role: 'Founder & Advisor',
|
||||
quote: 'Quality is no accident – it is the result of commitment and experience.',
|
||||
description: 'Klaus founded KLZ Cables and built the company into one of the most reliable partners in the cable industry. He brings decades of expertise.',
|
||||
linkedinUrl: 'https://www.linkedin.com/in/klaus-mintel-b80a8b193/',
|
||||
layout: 'imageLeft',
|
||||
colorScheme: 'light',
|
||||
}),
|
||||
lexicalBlock('manifestoGrid', {
|
||||
title: 'Our Values',
|
||||
subtitle: 'What drives us',
|
||||
tagline: 'Since our founding, clear principles have guided us every day.',
|
||||
items: [
|
||||
{ title: 'Quality', description: 'We only deliver products that meet the highest standards.' },
|
||||
{ title: 'Reliability', description: 'On-time delivery is our standard.' },
|
||||
{ title: 'Partnership', description: 'Long-term relationships are the foundation of our success.' },
|
||||
{ title: 'Innovation', description: 'We invest in new technologies and sustainable solutions.' },
|
||||
{ title: 'Transparency', description: 'Open communication and fair pricing define us.' },
|
||||
{ title: 'Sustainability', description: 'Responsibility for the environment and society is part of our DNA.' },
|
||||
],
|
||||
}),
|
||||
]),
|
||||
},
|
||||
// ─── Contact (DE) ─────────────────────────────────────────
|
||||
{
|
||||
title: 'Kontakt',
|
||||
slug: 'kontakt',
|
||||
locale: 'de',
|
||||
layout: 'fullBleed',
|
||||
excerpt: '',
|
||||
_status: 'published',
|
||||
content: lexicalDoc([
|
||||
lexicalBlock('heroSection', {
|
||||
badge: 'Kontakt',
|
||||
title: 'Sprechen Sie mit uns',
|
||||
subtitle: 'Wir sind für Sie da. Kontaktieren Sie uns für Beratung, Angebote oder technische Fragen.',
|
||||
alignment: 'left',
|
||||
}),
|
||||
lexicalBlock('contactSection', {
|
||||
showForm: true,
|
||||
showMap: true,
|
||||
showHours: true,
|
||||
}),
|
||||
]),
|
||||
},
|
||||
// ─── Contact (EN) ─────────────────────────────────────────
|
||||
{
|
||||
title: 'Contact',
|
||||
slug: 'contact',
|
||||
locale: 'en',
|
||||
layout: 'fullBleed',
|
||||
excerpt: '',
|
||||
_status: 'published',
|
||||
content: lexicalDoc([
|
||||
lexicalBlock('heroSection', {
|
||||
badge: 'Contact',
|
||||
title: 'Talk to us',
|
||||
subtitle: 'We are here for you. Contact us for consulting, quotes, or technical questions.',
|
||||
alignment: 'left',
|
||||
}),
|
||||
lexicalBlock('contactSection', {
|
||||
showForm: true,
|
||||
showMap: true,
|
||||
showHours: true,
|
||||
}),
|
||||
]),
|
||||
},
|
||||
];
|
||||
|
||||
async function seedPages() {
|
||||
const payload = await getPayload({ config: configPromise });
|
||||
|
||||
for (const page of PAGES) {
|
||||
const existing = await payload.find({
|
||||
collection: 'pages',
|
||||
where: {
|
||||
slug: { equals: page.slug },
|
||||
locale: { equals: page.locale },
|
||||
},
|
||||
limit: 1,
|
||||
});
|
||||
|
||||
const docs = existing.docs as any[];
|
||||
|
||||
if (docs.length > 0) {
|
||||
await payload.update({
|
||||
collection: 'pages',
|
||||
id: docs[0].id,
|
||||
locale: page.locale,
|
||||
data: {
|
||||
title: page.title,
|
||||
layout: page.layout as any,
|
||||
_status: page._status as any,
|
||||
content: page.content as any,
|
||||
},
|
||||
});
|
||||
console.log(`✅ Updated: ${page.slug} (${page.locale})`);
|
||||
} else {
|
||||
await payload.create({
|
||||
collection: 'pages',
|
||||
locale: page.locale,
|
||||
data: {
|
||||
title: page.title,
|
||||
slug: page.slug,
|
||||
layout: page.layout as any,
|
||||
excerpt: page.excerpt,
|
||||
_status: page._status as any,
|
||||
content: page.content as any,
|
||||
},
|
||||
});
|
||||
console.log(`✅ Created: ${page.slug} (${page.locale})`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n🎉 All pages seeded successfully!');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
seedPages().catch((err) => {
|
||||
console.error('❌ Seed failed:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
18
scripts/sql/drop_version_cols.sql
Normal file
18
scripts/sql/drop_version_cols.sql
Normal file
@@ -0,0 +1,18 @@
|
||||
ALTER TABLE _products_v DROP COLUMN IF EXISTS version_title;
|
||||
ALTER TABLE _products_v DROP COLUMN IF EXISTS version_description;
|
||||
ALTER TABLE _products_v DROP COLUMN IF EXISTS version_locale;
|
||||
ALTER TABLE _products_v DROP COLUMN IF EXISTS version_application;
|
||||
ALTER TABLE _products_v DROP COLUMN IF EXISTS version_content;
|
||||
|
||||
ALTER TABLE _posts_v DROP COLUMN IF EXISTS version_title;
|
||||
ALTER TABLE _posts_v DROP COLUMN IF EXISTS version_slug;
|
||||
ALTER TABLE _posts_v DROP COLUMN IF EXISTS version_excerpt;
|
||||
ALTER TABLE _posts_v DROP COLUMN IF EXISTS version_locale;
|
||||
ALTER TABLE _posts_v DROP COLUMN IF EXISTS version_category;
|
||||
ALTER TABLE _posts_v DROP COLUMN IF EXISTS version_content;
|
||||
|
||||
ALTER TABLE _pages_v DROP COLUMN IF EXISTS version_title;
|
||||
ALTER TABLE _pages_v DROP COLUMN IF EXISTS version_slug;
|
||||
ALTER TABLE _pages_v DROP COLUMN IF EXISTS version_locale;
|
||||
ALTER TABLE _pages_v DROP COLUMN IF EXISTS version_excerpt;
|
||||
ALTER TABLE _pages_v DROP COLUMN IF EXISTS version_content;
|
||||
14
scripts/test-rich-text.js
Normal file
14
scripts/test-rich-text.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import { getPayload } from 'payload';
|
||||
import configPromise from '@payload-config';
|
||||
|
||||
async function run() {
|
||||
const payload = await getPayload({ config: configPromise });
|
||||
const existing = await payload.find({
|
||||
collection: 'pages',
|
||||
where: { slug: { equals: 'start' } },
|
||||
limit: 1,
|
||||
});
|
||||
console.log('Homepage blocks found:', existing.docs[0].content?.root?.children?.length);
|
||||
process.exit(0);
|
||||
}
|
||||
run();
|
||||
285
src/migrations/20260225_175000_native_localization.ts
Normal file
285
src/migrations/20260225_175000_native_localization.ts
Normal file
@@ -0,0 +1,285 @@
|
||||
import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres';
|
||||
|
||||
/**
|
||||
* Migration: native_localization
|
||||
*
|
||||
* Transforms the existing schema (manual `locale` select column on each row)
|
||||
* into Payload's native localization join-table structure.
|
||||
*
|
||||
* Each statement is a separate db.execute call to avoid Drizzle multi-statement issues.
|
||||
*/
|
||||
export async function up({ db }: MigrateUpArgs): Promise<void> {
|
||||
// ── 1. Global locale enum ────────────────────────────────────────────────────
|
||||
await db.execute(sql`
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE "public"."enum__locales" AS ENUM('de', 'en');
|
||||
EXCEPTION WHEN duplicate_object THEN null; END $$
|
||||
`);
|
||||
await db.execute(sql`
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE "public"."enum__posts_v_published_locale" AS ENUM('de', 'en');
|
||||
EXCEPTION WHEN duplicate_object THEN null; END $$
|
||||
`);
|
||||
await db.execute(sql`
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE "public"."enum__products_v_published_locale" AS ENUM('de', 'en');
|
||||
EXCEPTION WHEN duplicate_object THEN null; END $$
|
||||
`);
|
||||
await db.execute(sql`
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE "public"."enum__pages_v_published_locale" AS ENUM('de', 'en');
|
||||
EXCEPTION WHEN duplicate_object THEN null; END $$
|
||||
`);
|
||||
await db.execute(sql`
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE "public"."enum_pages_layout" AS ENUM('default', 'fullBleed');
|
||||
EXCEPTION WHEN duplicate_object THEN null; END $$
|
||||
`);
|
||||
await db.execute(sql`
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE "public"."enum__pages_v_version_layout" AS ENUM('default', 'fullBleed');
|
||||
EXCEPTION WHEN duplicate_object THEN null; END $$
|
||||
`);
|
||||
await db.execute(sql`
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE "public"."enum__pages_v_version_status" AS ENUM('draft', 'published');
|
||||
EXCEPTION WHEN duplicate_object THEN null; END $$
|
||||
`);
|
||||
await db.execute(sql`
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE "public"."enum__posts_v_version_status" AS ENUM('draft', 'published');
|
||||
EXCEPTION WHEN duplicate_object THEN null; END $$
|
||||
`);
|
||||
await db.execute(sql`
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE "public"."enum__products_v_version_status" AS ENUM('draft', 'published');
|
||||
EXCEPTION WHEN duplicate_object THEN null; END $$
|
||||
`);
|
||||
|
||||
// ── 2. Alter pages table ─────────────────────────────────────────────────────
|
||||
await db.execute(sql`ALTER TABLE "pages" ADD COLUMN IF NOT EXISTS "layout" "enum_pages_layout" DEFAULT 'default'`);
|
||||
await db.execute(sql`ALTER TABLE "pages" ADD COLUMN IF NOT EXISTS "_status" "enum_pages_status" DEFAULT 'draft'`);
|
||||
|
||||
// ── 3. Create pages_locales join table ───────────────────────────────────────
|
||||
await db.execute(sql`
|
||||
CREATE TABLE IF NOT EXISTS "pages_locales" (
|
||||
"title" varchar,
|
||||
"slug" varchar,
|
||||
"excerpt" varchar,
|
||||
"content" jsonb,
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"_locale" "enum__locales" NOT NULL,
|
||||
"_parent_id" integer NOT NULL
|
||||
)
|
||||
`);
|
||||
await db.execute(sql`
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "pages_locales" ADD CONSTRAINT "pages_locales_locale_parent_id_unique" UNIQUE("_locale", "_parent_id");
|
||||
EXCEPTION WHEN duplicate_object THEN null; END $$
|
||||
`);
|
||||
await db.execute(sql`
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "pages_locales" ADD CONSTRAINT "pages_locales_parent_id_fk"
|
||||
FOREIGN KEY ("_parent_id") REFERENCES "pages"("id") ON DELETE cascade;
|
||||
EXCEPTION WHEN duplicate_object THEN null; END $$
|
||||
`);
|
||||
|
||||
// ── 4. Backfill pages_locales from old pages rows ────────────────────────────
|
||||
await db.execute(sql`
|
||||
INSERT INTO "pages_locales" ("title", "slug", "excerpt", "content", "_locale", "_parent_id")
|
||||
SELECT
|
||||
p.title, p.slug, p.excerpt, p.content,
|
||||
CASE WHEN p.locale::text = 'de' THEN 'de'::"enum__locales" ELSE 'en'::"enum__locales" END,
|
||||
p.id
|
||||
FROM "pages" p
|
||||
WHERE p.locale IS NOT NULL
|
||||
ON CONFLICT ("_locale", "_parent_id") DO UPDATE
|
||||
SET "title" = EXCLUDED."title",
|
||||
"slug" = EXCLUDED."slug",
|
||||
"excerpt" = EXCLUDED."excerpt",
|
||||
"content" = EXCLUDED."content"
|
||||
`);
|
||||
|
||||
// ── 5. Drop old columns from pages ───────────────────────────────────────────
|
||||
await db.execute(sql`ALTER TABLE "pages" DROP COLUMN IF EXISTS "title"`);
|
||||
await db.execute(sql`ALTER TABLE "pages" DROP COLUMN IF EXISTS "slug"`);
|
||||
await db.execute(sql`ALTER TABLE "pages" DROP COLUMN IF EXISTS "excerpt"`);
|
||||
await db.execute(sql`ALTER TABLE "pages" DROP COLUMN IF EXISTS "content"`);
|
||||
await db.execute(sql`ALTER TABLE "pages" DROP COLUMN IF EXISTS "locale"`);
|
||||
|
||||
// ── 6. Create posts_locales join table ───────────────────────────────────────
|
||||
await db.execute(sql`
|
||||
CREATE TABLE IF NOT EXISTS "posts_locales" (
|
||||
"title" varchar,
|
||||
"slug" varchar,
|
||||
"excerpt" varchar,
|
||||
"category" varchar,
|
||||
"content" jsonb,
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"_locale" "enum__locales" NOT NULL,
|
||||
"_parent_id" integer NOT NULL
|
||||
)
|
||||
`);
|
||||
await db.execute(sql`
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "posts_locales" ADD CONSTRAINT "posts_locales_locale_parent_id_unique" UNIQUE("_locale", "_parent_id");
|
||||
EXCEPTION WHEN duplicate_object THEN null; END $$
|
||||
`);
|
||||
await db.execute(sql`
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "posts_locales" ADD CONSTRAINT "posts_locales_parent_id_fk"
|
||||
FOREIGN KEY ("_parent_id") REFERENCES "posts"("id") ON DELETE cascade;
|
||||
EXCEPTION WHEN duplicate_object THEN null; END $$
|
||||
`);
|
||||
|
||||
// ── 7. Backfill posts_locales ────────────────────────────────────────────────
|
||||
await db.execute(sql`
|
||||
INSERT INTO "posts_locales" ("title", "slug", "excerpt", "category", "content", "_locale", "_parent_id")
|
||||
SELECT
|
||||
p.title, p.slug, p.excerpt, p.category, p.content,
|
||||
CASE WHEN p.locale::text = 'de' THEN 'de'::"enum__locales" ELSE 'en'::"enum__locales" END,
|
||||
p.id
|
||||
FROM "posts" p
|
||||
WHERE p.locale IS NOT NULL
|
||||
ON CONFLICT ("_locale", "_parent_id") DO UPDATE
|
||||
SET "title" = EXCLUDED."title",
|
||||
"slug" = EXCLUDED."slug",
|
||||
"excerpt" = EXCLUDED."excerpt",
|
||||
"category" = EXCLUDED."category",
|
||||
"content" = EXCLUDED."content"
|
||||
`);
|
||||
|
||||
// ── 8. Drop old columns from posts ───────────────────────────────────────────
|
||||
await db.execute(sql`ALTER TABLE "posts" DROP COLUMN IF EXISTS "title"`);
|
||||
await db.execute(sql`ALTER TABLE "posts" DROP COLUMN IF EXISTS "slug"`);
|
||||
await db.execute(sql`ALTER TABLE "posts" DROP COLUMN IF EXISTS "excerpt"`);
|
||||
await db.execute(sql`ALTER TABLE "posts" DROP COLUMN IF EXISTS "category"`);
|
||||
await db.execute(sql`ALTER TABLE "posts" DROP COLUMN IF EXISTS "content"`);
|
||||
await db.execute(sql`ALTER TABLE "posts" DROP COLUMN IF EXISTS "locale"`);
|
||||
|
||||
// ── 9. Create products_locales join table ────────────────────────────────────
|
||||
await db.execute(sql`
|
||||
CREATE TABLE IF NOT EXISTS "products_locales" (
|
||||
"title" varchar,
|
||||
"description" varchar,
|
||||
"application" jsonb,
|
||||
"content" jsonb,
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"_locale" "enum__locales" NOT NULL,
|
||||
"_parent_id" integer NOT NULL
|
||||
)
|
||||
`);
|
||||
await db.execute(sql`
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "products_locales" ADD CONSTRAINT "products_locales_locale_parent_id_unique" UNIQUE("_locale", "_parent_id");
|
||||
EXCEPTION WHEN duplicate_object THEN null; END $$
|
||||
`);
|
||||
await db.execute(sql`
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "products_locales" ADD CONSTRAINT "products_locales_parent_id_fk"
|
||||
FOREIGN KEY ("_parent_id") REFERENCES "products"("id") ON DELETE cascade;
|
||||
EXCEPTION WHEN duplicate_object THEN null; END $$
|
||||
`);
|
||||
|
||||
// ── 10. Backfill products_locales ────────────────────────────────────────────
|
||||
// Products were separate DE/EN rows — each becomes a locale entry on its own id
|
||||
await db.execute(sql`
|
||||
INSERT INTO "products_locales" ("title", "description", "application", "content", "_locale", "_parent_id")
|
||||
SELECT
|
||||
p.title, p.description, p.application, p.content,
|
||||
CASE WHEN p.locale::text = 'de' THEN 'de'::"enum__locales" ELSE 'en'::"enum__locales" END,
|
||||
p.id
|
||||
FROM "products" p
|
||||
WHERE p.locale IS NOT NULL
|
||||
ON CONFLICT ("_locale", "_parent_id") DO NOTHING
|
||||
`);
|
||||
|
||||
// ── 11. Drop old columns from products ───────────────────────────────────────
|
||||
await db.execute(sql`ALTER TABLE "products" DROP COLUMN IF EXISTS "title"`);
|
||||
await db.execute(sql`ALTER TABLE "products" DROP COLUMN IF EXISTS "description"`);
|
||||
await db.execute(sql`ALTER TABLE "products" DROP COLUMN IF EXISTS "application"`);
|
||||
await db.execute(sql`ALTER TABLE "products" DROP COLUMN IF EXISTS "content"`);
|
||||
await db.execute(sql`ALTER TABLE "products" DROP COLUMN IF EXISTS "locale"`);
|
||||
|
||||
// ── 12. Version tables (_posts_v, _products_v, _pages_v) locale columns ──────
|
||||
await db.execute(sql`ALTER TABLE "_posts_v" ADD COLUMN IF NOT EXISTS "published_locale" "enum__posts_v_published_locale"`);
|
||||
await db.execute(sql`ALTER TABLE "_products_v" ADD COLUMN IF NOT EXISTS "published_locale" "enum__products_v_published_locale"`);
|
||||
await db.execute(sql`ALTER TABLE "_pages_v" ADD COLUMN IF NOT EXISTS "published_locale" "enum__pages_v_published_locale"`);
|
||||
|
||||
// ── 13. Create _posts_v_locales ──────────────────────────────────────────────
|
||||
await db.execute(sql`
|
||||
CREATE TABLE IF NOT EXISTS "_posts_v_locales" (
|
||||
"version_title" varchar, "version_slug" varchar, "version_excerpt" varchar,
|
||||
"version_category" varchar, "version_content" jsonb,
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"_locale" "enum__locales" NOT NULL, "_parent_id" integer NOT NULL
|
||||
)
|
||||
`);
|
||||
await db.execute(sql`
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "_posts_v_locales" ADD CONSTRAINT "_posts_v_locales_locale_parent_id_unique" UNIQUE("_locale", "_parent_id");
|
||||
EXCEPTION WHEN duplicate_object THEN null; END $$
|
||||
`);
|
||||
await db.execute(sql`
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "_posts_v_locales" ADD CONSTRAINT "_posts_v_locales_parent_id_fk"
|
||||
FOREIGN KEY ("_parent_id") REFERENCES "_posts_v"("id") ON DELETE cascade;
|
||||
EXCEPTION WHEN duplicate_object THEN null; END $$
|
||||
`);
|
||||
|
||||
// ── 14. Create _products_v_locales ───────────────────────────────────────────
|
||||
await db.execute(sql`
|
||||
CREATE TABLE IF NOT EXISTS "_products_v_locales" (
|
||||
"version_title" varchar, "version_description" varchar,
|
||||
"version_application" jsonb, "version_content" jsonb,
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"_locale" "enum__locales" NOT NULL, "_parent_id" integer NOT NULL
|
||||
)
|
||||
`);
|
||||
await db.execute(sql`
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "_products_v_locales" ADD CONSTRAINT "_products_v_locales_locale_parent_id_unique" UNIQUE("_locale", "_parent_id");
|
||||
EXCEPTION WHEN duplicate_object THEN null; END $$
|
||||
`);
|
||||
await db.execute(sql`
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "_products_v_locales" ADD CONSTRAINT "_products_v_locales_parent_id_fk"
|
||||
FOREIGN KEY ("_parent_id") REFERENCES "_products_v"("id") ON DELETE cascade;
|
||||
EXCEPTION WHEN duplicate_object THEN null; END $$
|
||||
`);
|
||||
|
||||
// ── 15. Create _pages_v_locales ──────────────────────────────────────────────
|
||||
await db.execute(sql`
|
||||
CREATE TABLE IF NOT EXISTS "_pages_v_locales" (
|
||||
"version_title" varchar, "version_slug" varchar,
|
||||
"version_excerpt" varchar, "version_content" jsonb,
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"_locale" "enum__locales" NOT NULL, "_parent_id" integer NOT NULL
|
||||
)
|
||||
`);
|
||||
await db.execute(sql`
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "_pages_v_locales" ADD CONSTRAINT "_pages_v_locales_locale_parent_id_unique" UNIQUE("_locale", "_parent_id");
|
||||
EXCEPTION WHEN duplicate_object THEN null; END $$
|
||||
`);
|
||||
await db.execute(sql`
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "_pages_v_locales" ADD CONSTRAINT "_pages_v_locales_parent_id_fk"
|
||||
FOREIGN KEY ("_parent_id") REFERENCES "_pages_v"("id") ON DELETE cascade;
|
||||
EXCEPTION WHEN duplicate_object THEN null; END $$
|
||||
`);
|
||||
|
||||
// ── 16. Drop the now-redundant old locale enum ───────────────────────────────
|
||||
await db.execute(sql`DROP TYPE IF EXISTS "public"."enum_pages_locale"`);
|
||||
await db.execute(sql`DROP TYPE IF EXISTS "public"."enum_posts_locale"`);
|
||||
await db.execute(sql`DROP TYPE IF EXISTS "public"."enum_products_locale"`);
|
||||
}
|
||||
|
||||
export async function down({ db }: MigrateDownArgs): Promise<void> {
|
||||
await db.execute(sql`DROP TABLE IF EXISTS "pages_locales" CASCADE`);
|
||||
await db.execute(sql`DROP TABLE IF EXISTS "_pages_v_locales" CASCADE`);
|
||||
await db.execute(sql`DROP TABLE IF EXISTS "posts_locales" CASCADE`);
|
||||
await db.execute(sql`DROP TABLE IF EXISTS "_posts_v_locales" CASCADE`);
|
||||
await db.execute(sql`DROP TABLE IF EXISTS "products_locales" CASCADE`);
|
||||
await db.execute(sql`DROP TABLE IF EXISTS "_products_v_locales" CASCADE`);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as migration_20260223_195005_products_collection from './20260223_195005_products_collection';
|
||||
import * as migration_20260223_195151_remove_sku_unique from './20260223_195151_remove_sku_unique';
|
||||
import * as migration_20260225_003500_add_pages_collection from './20260225_003500_add_pages_collection';
|
||||
import * as migration_20260225_175000_native_localization from './20260225_175000_native_localization';
|
||||
|
||||
export const migrations = [
|
||||
{
|
||||
@@ -18,4 +19,9 @@ export const migrations = [
|
||||
down: migration_20260225_003500_add_pages_collection.down,
|
||||
name: '20260225_003500_add_pages_collection',
|
||||
},
|
||||
{
|
||||
up: migration_20260225_175000_native_localization.up,
|
||||
down: migration_20260225_175000_native_localization.down,
|
||||
name: '20260225_175000_native_localization',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -22,27 +22,43 @@ import {
|
||||
pgEnum,
|
||||
} from '@payloadcms/db-postgres/drizzle/pg-core';
|
||||
import { sql, relations } from '@payloadcms/db-postgres/drizzle';
|
||||
export const enum_posts_locale = pgEnum('enum_posts_locale', ['en', 'de']);
|
||||
export const enum__locales = pgEnum('enum__locales', ['de', 'en']);
|
||||
export const enum_posts_status = pgEnum('enum_posts_status', ['draft', 'published']);
|
||||
export const enum__posts_v_version_locale = pgEnum('enum__posts_v_version_locale', ['en', 'de']);
|
||||
export const enum__posts_v_version_status = pgEnum('enum__posts_v_version_status', [
|
||||
'draft',
|
||||
'published',
|
||||
]);
|
||||
export const enum__posts_v_published_locale = pgEnum('enum__posts_v_published_locale', [
|
||||
'de',
|
||||
'en',
|
||||
]);
|
||||
export const enum_form_submissions_type = pgEnum('enum_form_submissions_type', [
|
||||
'contact',
|
||||
'product_quote',
|
||||
]);
|
||||
export const enum_products_locale = pgEnum('enum_products_locale', ['en', 'de']);
|
||||
export const enum_products_status = pgEnum('enum_products_status', ['draft', 'published']);
|
||||
export const enum__products_v_version_locale = pgEnum('enum__products_v_version_locale', [
|
||||
'en',
|
||||
'de',
|
||||
]);
|
||||
export const enum__products_v_version_status = pgEnum('enum__products_v_version_status', [
|
||||
'draft',
|
||||
'published',
|
||||
]);
|
||||
export const enum__products_v_published_locale = pgEnum('enum__products_v_published_locale', [
|
||||
'de',
|
||||
'en',
|
||||
]);
|
||||
export const enum_pages_layout = pgEnum('enum_pages_layout', ['default', 'fullBleed']);
|
||||
export const enum_pages_status = pgEnum('enum_pages_status', ['draft', 'published']);
|
||||
export const enum__pages_v_version_layout = pgEnum('enum__pages_v_version_layout', [
|
||||
'default',
|
||||
'fullBleed',
|
||||
]);
|
||||
export const enum__pages_v_version_status = pgEnum('enum__pages_v_version_status', [
|
||||
'draft',
|
||||
'published',
|
||||
]);
|
||||
export const enum__pages_v_published_locale = pgEnum('enum__pages_v_published_locale', [
|
||||
'de',
|
||||
'en',
|
||||
]);
|
||||
|
||||
export const users_sessions = pgTable(
|
||||
'users_sessions',
|
||||
@@ -130,18 +146,12 @@ export const media = pgTable(
|
||||
sizes_card_mimeType: varchar('sizes_card_mime_type'),
|
||||
sizes_card_filesize: numeric('sizes_card_filesize', { mode: 'number' }),
|
||||
sizes_card_filename: varchar('sizes_card_filename'),
|
||||
sizes_hero_url: varchar('sizes_hero_url'),
|
||||
sizes_hero_width: numeric('sizes_hero_width', { mode: 'number' }),
|
||||
sizes_hero_height: numeric('sizes_hero_height', { mode: 'number' }),
|
||||
sizes_hero_mimeType: varchar('sizes_hero_mime_type'),
|
||||
sizes_hero_filesize: numeric('sizes_hero_filesize', { mode: 'number' }),
|
||||
sizes_hero_filename: varchar('sizes_hero_filename'),
|
||||
sizes_hero_mobile_url: varchar('sizes_hero_mobile_url'),
|
||||
sizes_hero_mobile_width: numeric('sizes_hero_mobile_width', { mode: 'number' }),
|
||||
sizes_hero_mobile_height: numeric('sizes_hero_mobile_height', { mode: 'number' }),
|
||||
sizes_hero_mobile_mimeType: varchar('sizes_hero_mobile_mime_type'),
|
||||
sizes_hero_mobile_filesize: numeric('sizes_hero_mobile_filesize', { mode: 'number' }),
|
||||
sizes_hero_mobile_filename: varchar('sizes_hero_mobile_filename'),
|
||||
sizes_tablet_url: varchar('sizes_tablet_url'),
|
||||
sizes_tablet_width: numeric('sizes_tablet_width', { mode: 'number' }),
|
||||
sizes_tablet_height: numeric('sizes_tablet_height', { mode: 'number' }),
|
||||
sizes_tablet_mimeType: varchar('sizes_tablet_mime_type'),
|
||||
sizes_tablet_filesize: numeric('sizes_tablet_filesize', { mode: 'number' }),
|
||||
sizes_tablet_filename: varchar('sizes_tablet_filename'),
|
||||
},
|
||||
(columns) => [
|
||||
index('media_updated_at_idx').on(columns.updatedAt),
|
||||
@@ -151,10 +161,7 @@ export const media = pgTable(
|
||||
columns.sizes_thumbnail_filename,
|
||||
),
|
||||
index('media_sizes_card_sizes_card_filename_idx').on(columns.sizes_card_filename),
|
||||
index('media_sizes_hero_sizes_hero_filename_idx').on(columns.sizes_hero_filename),
|
||||
index('media_sizes_hero_mobile_sizes_hero_mobile_filename_idx').on(
|
||||
columns.sizes_hero_mobile_filename,
|
||||
),
|
||||
index('media_sizes_tablet_sizes_tablet_filename_idx').on(columns.sizes_tablet_filename),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -162,16 +169,10 @@ export const posts = pgTable(
|
||||
'posts',
|
||||
{
|
||||
id: serial('id').primaryKey(),
|
||||
title: varchar('title'),
|
||||
slug: varchar('slug'),
|
||||
excerpt: varchar('excerpt'),
|
||||
date: timestamp('date', { mode: 'string', withTimezone: true, precision: 3 }),
|
||||
featuredImage: integer('featured_image_id').references(() => media.id, {
|
||||
onDelete: 'set null',
|
||||
}),
|
||||
locale: enum_posts_locale('locale').default('en'),
|
||||
category: varchar('category'),
|
||||
content: jsonb('content'),
|
||||
updatedAt: timestamp('updated_at', { mode: 'string', withTimezone: true, precision: 3 })
|
||||
.defaultNow()
|
||||
.notNull(),
|
||||
@@ -181,7 +182,6 @@ export const posts = pgTable(
|
||||
_status: enum_posts_status('_status').default('draft'),
|
||||
},
|
||||
(columns) => [
|
||||
uniqueIndex('posts_slug_idx').on(columns.slug),
|
||||
index('posts_featured_image_idx').on(columns.featuredImage),
|
||||
index('posts_updated_at_idx').on(columns.updatedAt),
|
||||
index('posts_created_at_idx').on(columns.createdAt),
|
||||
@@ -189,6 +189,28 @@ export const posts = pgTable(
|
||||
],
|
||||
);
|
||||
|
||||
export const posts_locales = pgTable(
|
||||
'posts_locales',
|
||||
{
|
||||
title: varchar('title'),
|
||||
slug: varchar('slug'),
|
||||
excerpt: varchar('excerpt'),
|
||||
category: varchar('category'),
|
||||
content: jsonb('content'),
|
||||
id: serial('id').primaryKey(),
|
||||
_locale: enum__locales('_locale').notNull(),
|
||||
_parentID: integer('_parent_id').notNull(),
|
||||
},
|
||||
(columns) => [
|
||||
uniqueIndex('posts_locales_locale_parent_id_unique').on(columns._locale, columns._parentID),
|
||||
foreignKey({
|
||||
columns: [columns['_parentID']],
|
||||
foreignColumns: [posts.id],
|
||||
name: 'posts_locales_parent_id_fk',
|
||||
}).onDelete('cascade'),
|
||||
],
|
||||
);
|
||||
|
||||
export const _posts_v = pgTable(
|
||||
'_posts_v',
|
||||
{
|
||||
@@ -196,16 +218,10 @@ export const _posts_v = pgTable(
|
||||
parent: integer('parent_id').references(() => posts.id, {
|
||||
onDelete: 'set null',
|
||||
}),
|
||||
version_title: varchar('version_title'),
|
||||
version_slug: varchar('version_slug'),
|
||||
version_excerpt: varchar('version_excerpt'),
|
||||
version_date: timestamp('version_date', { mode: 'string', withTimezone: true, precision: 3 }),
|
||||
version_featuredImage: integer('version_featured_image_id').references(() => media.id, {
|
||||
onDelete: 'set null',
|
||||
}),
|
||||
version_locale: enum__posts_v_version_locale('version_locale').default('en'),
|
||||
version_category: varchar('version_category'),
|
||||
version_content: jsonb('version_content'),
|
||||
version_updatedAt: timestamp('version_updated_at', {
|
||||
mode: 'string',
|
||||
withTimezone: true,
|
||||
@@ -223,21 +239,46 @@ export const _posts_v = pgTable(
|
||||
updatedAt: timestamp('updated_at', { mode: 'string', withTimezone: true, precision: 3 })
|
||||
.defaultNow()
|
||||
.notNull(),
|
||||
snapshot: boolean('snapshot'),
|
||||
publishedLocale: enum__posts_v_published_locale('published_locale'),
|
||||
latest: boolean('latest'),
|
||||
},
|
||||
(columns) => [
|
||||
index('_posts_v_parent_idx').on(columns.parent),
|
||||
index('_posts_v_version_version_slug_idx').on(columns.version_slug),
|
||||
index('_posts_v_version_version_featured_image_idx').on(columns.version_featuredImage),
|
||||
index('_posts_v_version_version_updated_at_idx').on(columns.version_updatedAt),
|
||||
index('_posts_v_version_version_created_at_idx').on(columns.version_createdAt),
|
||||
index('_posts_v_version_version__status_idx').on(columns.version__status),
|
||||
index('_posts_v_created_at_idx').on(columns.createdAt),
|
||||
index('_posts_v_updated_at_idx').on(columns.updatedAt),
|
||||
index('_posts_v_snapshot_idx').on(columns.snapshot),
|
||||
index('_posts_v_published_locale_idx').on(columns.publishedLocale),
|
||||
index('_posts_v_latest_idx').on(columns.latest),
|
||||
],
|
||||
);
|
||||
|
||||
export const _posts_v_locales = pgTable(
|
||||
'_posts_v_locales',
|
||||
{
|
||||
version_title: varchar('version_title'),
|
||||
version_slug: varchar('version_slug'),
|
||||
version_excerpt: varchar('version_excerpt'),
|
||||
version_category: varchar('version_category'),
|
||||
version_content: jsonb('version_content'),
|
||||
id: serial('id').primaryKey(),
|
||||
_locale: enum__locales('_locale').notNull(),
|
||||
_parentID: integer('_parent_id').notNull(),
|
||||
},
|
||||
(columns) => [
|
||||
uniqueIndex('_posts_v_locales_locale_parent_id_unique').on(columns._locale, columns._parentID),
|
||||
foreignKey({
|
||||
columns: [columns['_parentID']],
|
||||
foreignColumns: [_posts_v.id],
|
||||
name: '_posts_v_locales_parent_id_fk',
|
||||
}).onDelete('cascade'),
|
||||
],
|
||||
);
|
||||
|
||||
export const form_submissions = pgTable(
|
||||
'form_submissions',
|
||||
{
|
||||
@@ -283,13 +324,11 @@ export const products = pgTable(
|
||||
'products',
|
||||
{
|
||||
id: serial('id').primaryKey(),
|
||||
title: varchar('title'),
|
||||
sku: varchar('sku'),
|
||||
slug: varchar('slug'),
|
||||
description: varchar('description'),
|
||||
locale: enum_products_locale('locale').default('de'),
|
||||
application: jsonb('application'),
|
||||
content: jsonb('content'),
|
||||
featuredImage: integer('featured_image_id').references(() => media.id, {
|
||||
onDelete: 'set null',
|
||||
}),
|
||||
updatedAt: timestamp('updated_at', { mode: 'string', withTimezone: true, precision: 3 })
|
||||
.defaultNow()
|
||||
.notNull(),
|
||||
@@ -299,13 +338,34 @@ export const products = pgTable(
|
||||
_status: enum_products_status('_status').default('draft'),
|
||||
},
|
||||
(columns) => [
|
||||
uniqueIndex('products_sku_idx').on(columns.sku),
|
||||
index('products_featured_image_idx').on(columns.featuredImage),
|
||||
index('products_updated_at_idx').on(columns.updatedAt),
|
||||
index('products_created_at_idx').on(columns.createdAt),
|
||||
index('products__status_idx').on(columns._status),
|
||||
],
|
||||
);
|
||||
|
||||
export const products_locales = pgTable(
|
||||
'products_locales',
|
||||
{
|
||||
title: varchar('title'),
|
||||
description: varchar('description'),
|
||||
application: jsonb('application'),
|
||||
content: jsonb('content'),
|
||||
id: serial('id').primaryKey(),
|
||||
_locale: enum__locales('_locale').notNull(),
|
||||
_parentID: integer('_parent_id').notNull(),
|
||||
},
|
||||
(columns) => [
|
||||
uniqueIndex('products_locales_locale_parent_id_unique').on(columns._locale, columns._parentID),
|
||||
foreignKey({
|
||||
columns: [columns['_parentID']],
|
||||
foreignColumns: [products.id],
|
||||
name: 'products_locales_parent_id_fk',
|
||||
}).onDelete('cascade'),
|
||||
],
|
||||
);
|
||||
|
||||
export const products_rels = pgTable(
|
||||
'products_rels',
|
||||
{
|
||||
@@ -360,13 +420,11 @@ export const _products_v = pgTable(
|
||||
parent: integer('parent_id').references(() => products.id, {
|
||||
onDelete: 'set null',
|
||||
}),
|
||||
version_title: varchar('version_title'),
|
||||
version_sku: varchar('version_sku'),
|
||||
version_slug: varchar('version_slug'),
|
||||
version_description: varchar('version_description'),
|
||||
version_locale: enum__products_v_version_locale('version_locale').default('de'),
|
||||
version_application: jsonb('version_application'),
|
||||
version_content: jsonb('version_content'),
|
||||
version_featuredImage: integer('version_featured_image_id').references(() => media.id, {
|
||||
onDelete: 'set null',
|
||||
}),
|
||||
version_updatedAt: timestamp('version_updated_at', {
|
||||
mode: 'string',
|
||||
withTimezone: true,
|
||||
@@ -384,20 +442,48 @@ export const _products_v = pgTable(
|
||||
updatedAt: timestamp('updated_at', { mode: 'string', withTimezone: true, precision: 3 })
|
||||
.defaultNow()
|
||||
.notNull(),
|
||||
snapshot: boolean('snapshot'),
|
||||
publishedLocale: enum__products_v_published_locale('published_locale'),
|
||||
latest: boolean('latest'),
|
||||
},
|
||||
(columns) => [
|
||||
index('_products_v_parent_idx').on(columns.parent),
|
||||
index('_products_v_version_version_sku_idx').on(columns.version_sku),
|
||||
index('_products_v_version_version_featured_image_idx').on(columns.version_featuredImage),
|
||||
index('_products_v_version_version_updated_at_idx').on(columns.version_updatedAt),
|
||||
index('_products_v_version_version_created_at_idx').on(columns.version_createdAt),
|
||||
index('_products_v_version_version__status_idx').on(columns.version__status),
|
||||
index('_products_v_created_at_idx').on(columns.createdAt),
|
||||
index('_products_v_updated_at_idx').on(columns.updatedAt),
|
||||
index('_products_v_snapshot_idx').on(columns.snapshot),
|
||||
index('_products_v_published_locale_idx').on(columns.publishedLocale),
|
||||
index('_products_v_latest_idx').on(columns.latest),
|
||||
],
|
||||
);
|
||||
|
||||
export const _products_v_locales = pgTable(
|
||||
'_products_v_locales',
|
||||
{
|
||||
version_title: varchar('version_title'),
|
||||
version_description: varchar('version_description'),
|
||||
version_application: jsonb('version_application'),
|
||||
version_content: jsonb('version_content'),
|
||||
id: serial('id').primaryKey(),
|
||||
_locale: enum__locales('_locale').notNull(),
|
||||
_parentID: integer('_parent_id').notNull(),
|
||||
},
|
||||
(columns) => [
|
||||
uniqueIndex('_products_v_locales_locale_parent_id_unique').on(
|
||||
columns._locale,
|
||||
columns._parentID,
|
||||
),
|
||||
foreignKey({
|
||||
columns: [columns['_parentID']],
|
||||
foreignColumns: [_products_v.id],
|
||||
name: '_products_v_locales_parent_id_fk',
|
||||
}).onDelete('cascade'),
|
||||
],
|
||||
);
|
||||
|
||||
export const _products_v_rels = pgTable(
|
||||
'_products_v_rels',
|
||||
{
|
||||
@@ -425,6 +511,118 @@ export const _products_v_rels = pgTable(
|
||||
],
|
||||
);
|
||||
|
||||
export const pages = pgTable(
|
||||
'pages',
|
||||
{
|
||||
id: serial('id').primaryKey(),
|
||||
layout: enum_pages_layout('layout').default('default'),
|
||||
featuredImage: integer('featured_image_id').references(() => media.id, {
|
||||
onDelete: 'set null',
|
||||
}),
|
||||
updatedAt: timestamp('updated_at', { mode: 'string', withTimezone: true, precision: 3 })
|
||||
.defaultNow()
|
||||
.notNull(),
|
||||
createdAt: timestamp('created_at', { mode: 'string', withTimezone: true, precision: 3 })
|
||||
.defaultNow()
|
||||
.notNull(),
|
||||
_status: enum_pages_status('_status').default('draft'),
|
||||
},
|
||||
(columns) => [
|
||||
index('pages_featured_image_idx').on(columns.featuredImage),
|
||||
index('pages_updated_at_idx').on(columns.updatedAt),
|
||||
index('pages_created_at_idx').on(columns.createdAt),
|
||||
index('pages__status_idx').on(columns._status),
|
||||
],
|
||||
);
|
||||
|
||||
export const pages_locales = pgTable(
|
||||
'pages_locales',
|
||||
{
|
||||
title: varchar('title'),
|
||||
slug: varchar('slug'),
|
||||
excerpt: varchar('excerpt'),
|
||||
content: jsonb('content'),
|
||||
id: serial('id').primaryKey(),
|
||||
_locale: enum__locales('_locale').notNull(),
|
||||
_parentID: integer('_parent_id').notNull(),
|
||||
},
|
||||
(columns) => [
|
||||
uniqueIndex('pages_locales_locale_parent_id_unique').on(columns._locale, columns._parentID),
|
||||
foreignKey({
|
||||
columns: [columns['_parentID']],
|
||||
foreignColumns: [pages.id],
|
||||
name: 'pages_locales_parent_id_fk',
|
||||
}).onDelete('cascade'),
|
||||
],
|
||||
);
|
||||
|
||||
export const _pages_v = pgTable(
|
||||
'_pages_v',
|
||||
{
|
||||
id: serial('id').primaryKey(),
|
||||
parent: integer('parent_id').references(() => pages.id, {
|
||||
onDelete: 'set null',
|
||||
}),
|
||||
version_layout: enum__pages_v_version_layout('version_layout').default('default'),
|
||||
version_featuredImage: integer('version_featured_image_id').references(() => media.id, {
|
||||
onDelete: 'set null',
|
||||
}),
|
||||
version_updatedAt: timestamp('version_updated_at', {
|
||||
mode: 'string',
|
||||
withTimezone: true,
|
||||
precision: 3,
|
||||
}),
|
||||
version_createdAt: timestamp('version_created_at', {
|
||||
mode: 'string',
|
||||
withTimezone: true,
|
||||
precision: 3,
|
||||
}),
|
||||
version__status: enum__pages_v_version_status('version__status').default('draft'),
|
||||
createdAt: timestamp('created_at', { mode: 'string', withTimezone: true, precision: 3 })
|
||||
.defaultNow()
|
||||
.notNull(),
|
||||
updatedAt: timestamp('updated_at', { mode: 'string', withTimezone: true, precision: 3 })
|
||||
.defaultNow()
|
||||
.notNull(),
|
||||
snapshot: boolean('snapshot'),
|
||||
publishedLocale: enum__pages_v_published_locale('published_locale'),
|
||||
latest: boolean('latest'),
|
||||
},
|
||||
(columns) => [
|
||||
index('_pages_v_parent_idx').on(columns.parent),
|
||||
index('_pages_v_version_version_featured_image_idx').on(columns.version_featuredImage),
|
||||
index('_pages_v_version_version_updated_at_idx').on(columns.version_updatedAt),
|
||||
index('_pages_v_version_version_created_at_idx').on(columns.version_createdAt),
|
||||
index('_pages_v_version_version__status_idx').on(columns.version__status),
|
||||
index('_pages_v_created_at_idx').on(columns.createdAt),
|
||||
index('_pages_v_updated_at_idx').on(columns.updatedAt),
|
||||
index('_pages_v_snapshot_idx').on(columns.snapshot),
|
||||
index('_pages_v_published_locale_idx').on(columns.publishedLocale),
|
||||
index('_pages_v_latest_idx').on(columns.latest),
|
||||
],
|
||||
);
|
||||
|
||||
export const _pages_v_locales = pgTable(
|
||||
'_pages_v_locales',
|
||||
{
|
||||
version_title: varchar('version_title'),
|
||||
version_slug: varchar('version_slug'),
|
||||
version_excerpt: varchar('version_excerpt'),
|
||||
version_content: jsonb('version_content'),
|
||||
id: serial('id').primaryKey(),
|
||||
_locale: enum__locales('_locale').notNull(),
|
||||
_parentID: integer('_parent_id').notNull(),
|
||||
},
|
||||
(columns) => [
|
||||
uniqueIndex('_pages_v_locales_locale_parent_id_unique').on(columns._locale, columns._parentID),
|
||||
foreignKey({
|
||||
columns: [columns['_parentID']],
|
||||
foreignColumns: [_pages_v.id],
|
||||
name: '_pages_v_locales_parent_id_fk',
|
||||
}).onDelete('cascade'),
|
||||
],
|
||||
);
|
||||
|
||||
export const payload_kv = pgTable(
|
||||
'payload_kv',
|
||||
{
|
||||
@@ -466,6 +664,7 @@ export const payload_locked_documents_rels = pgTable(
|
||||
postsID: integer('posts_id'),
|
||||
'form-submissionsID': integer('form_submissions_id'),
|
||||
productsID: integer('products_id'),
|
||||
pagesID: integer('pages_id'),
|
||||
},
|
||||
(columns) => [
|
||||
index('payload_locked_documents_rels_order_idx').on(columns.order),
|
||||
@@ -478,6 +677,7 @@ export const payload_locked_documents_rels = pgTable(
|
||||
columns['form-submissionsID'],
|
||||
),
|
||||
index('payload_locked_documents_rels_products_id_idx').on(columns.productsID),
|
||||
index('payload_locked_documents_rels_pages_id_idx').on(columns.pagesID),
|
||||
foreignKey({
|
||||
columns: [columns['parent']],
|
||||
foreignColumns: [payload_locked_documents.id],
|
||||
@@ -508,6 +708,11 @@ export const payload_locked_documents_rels = pgTable(
|
||||
foreignColumns: [products.id],
|
||||
name: 'payload_locked_documents_rels_products_fk',
|
||||
}).onDelete('cascade'),
|
||||
foreignKey({
|
||||
columns: [columns['pagesID']],
|
||||
foreignColumns: [pages.id],
|
||||
name: 'payload_locked_documents_rels_pages_fk',
|
||||
}).onDelete('cascade'),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -590,14 +795,31 @@ export const relations_users = relations(users, ({ many }) => ({
|
||||
}),
|
||||
}));
|
||||
export const relations_media = relations(media, () => ({}));
|
||||
export const relations_posts = relations(posts, ({ one }) => ({
|
||||
export const relations_posts_locales = relations(posts_locales, ({ one }) => ({
|
||||
_parentID: one(posts, {
|
||||
fields: [posts_locales._parentID],
|
||||
references: [posts.id],
|
||||
relationName: '_locales',
|
||||
}),
|
||||
}));
|
||||
export const relations_posts = relations(posts, ({ one, many }) => ({
|
||||
featuredImage: one(media, {
|
||||
fields: [posts.featuredImage],
|
||||
references: [media.id],
|
||||
relationName: 'featuredImage',
|
||||
}),
|
||||
_locales: many(posts_locales, {
|
||||
relationName: '_locales',
|
||||
}),
|
||||
}));
|
||||
export const relations__posts_v = relations(_posts_v, ({ one }) => ({
|
||||
export const relations__posts_v_locales = relations(_posts_v_locales, ({ one }) => ({
|
||||
_parentID: one(_posts_v, {
|
||||
fields: [_posts_v_locales._parentID],
|
||||
references: [_posts_v.id],
|
||||
relationName: '_locales',
|
||||
}),
|
||||
}));
|
||||
export const relations__posts_v = relations(_posts_v, ({ one, many }) => ({
|
||||
parent: one(posts, {
|
||||
fields: [_posts_v.parent],
|
||||
references: [posts.id],
|
||||
@@ -608,6 +830,9 @@ export const relations__posts_v = relations(_posts_v, ({ one }) => ({
|
||||
references: [media.id],
|
||||
relationName: 'version_featuredImage',
|
||||
}),
|
||||
_locales: many(_posts_v_locales, {
|
||||
relationName: '_locales',
|
||||
}),
|
||||
}));
|
||||
export const relations_form_submissions = relations(form_submissions, () => ({}));
|
||||
export const relations_products_categories = relations(products_categories, ({ one }) => ({
|
||||
@@ -617,6 +842,13 @@ export const relations_products_categories = relations(products_categories, ({ o
|
||||
relationName: 'categories',
|
||||
}),
|
||||
}));
|
||||
export const relations_products_locales = relations(products_locales, ({ one }) => ({
|
||||
_parentID: one(products, {
|
||||
fields: [products_locales._parentID],
|
||||
references: [products.id],
|
||||
relationName: '_locales',
|
||||
}),
|
||||
}));
|
||||
export const relations_products_rels = relations(products_rels, ({ one }) => ({
|
||||
parent: one(products, {
|
||||
fields: [products_rels.parent],
|
||||
@@ -629,10 +861,18 @@ export const relations_products_rels = relations(products_rels, ({ one }) => ({
|
||||
relationName: 'media',
|
||||
}),
|
||||
}));
|
||||
export const relations_products = relations(products, ({ many }) => ({
|
||||
export const relations_products = relations(products, ({ one, many }) => ({
|
||||
categories: many(products_categories, {
|
||||
relationName: 'categories',
|
||||
}),
|
||||
featuredImage: one(media, {
|
||||
fields: [products.featuredImage],
|
||||
references: [media.id],
|
||||
relationName: 'featuredImage',
|
||||
}),
|
||||
_locales: many(products_locales, {
|
||||
relationName: '_locales',
|
||||
}),
|
||||
_rels: many(products_rels, {
|
||||
relationName: '_rels',
|
||||
}),
|
||||
@@ -647,6 +887,13 @@ export const relations__products_v_version_categories = relations(
|
||||
}),
|
||||
}),
|
||||
);
|
||||
export const relations__products_v_locales = relations(_products_v_locales, ({ one }) => ({
|
||||
_parentID: one(_products_v, {
|
||||
fields: [_products_v_locales._parentID],
|
||||
references: [_products_v.id],
|
||||
relationName: '_locales',
|
||||
}),
|
||||
}));
|
||||
export const relations__products_v_rels = relations(_products_v_rels, ({ one }) => ({
|
||||
parent: one(_products_v, {
|
||||
fields: [_products_v_rels.parent],
|
||||
@@ -668,10 +915,57 @@ export const relations__products_v = relations(_products_v, ({ one, many }) => (
|
||||
version_categories: many(_products_v_version_categories, {
|
||||
relationName: 'version_categories',
|
||||
}),
|
||||
version_featuredImage: one(media, {
|
||||
fields: [_products_v.version_featuredImage],
|
||||
references: [media.id],
|
||||
relationName: 'version_featuredImage',
|
||||
}),
|
||||
_locales: many(_products_v_locales, {
|
||||
relationName: '_locales',
|
||||
}),
|
||||
_rels: many(_products_v_rels, {
|
||||
relationName: '_rels',
|
||||
}),
|
||||
}));
|
||||
export const relations_pages_locales = relations(pages_locales, ({ one }) => ({
|
||||
_parentID: one(pages, {
|
||||
fields: [pages_locales._parentID],
|
||||
references: [pages.id],
|
||||
relationName: '_locales',
|
||||
}),
|
||||
}));
|
||||
export const relations_pages = relations(pages, ({ one, many }) => ({
|
||||
featuredImage: one(media, {
|
||||
fields: [pages.featuredImage],
|
||||
references: [media.id],
|
||||
relationName: 'featuredImage',
|
||||
}),
|
||||
_locales: many(pages_locales, {
|
||||
relationName: '_locales',
|
||||
}),
|
||||
}));
|
||||
export const relations__pages_v_locales = relations(_pages_v_locales, ({ one }) => ({
|
||||
_parentID: one(_pages_v, {
|
||||
fields: [_pages_v_locales._parentID],
|
||||
references: [_pages_v.id],
|
||||
relationName: '_locales',
|
||||
}),
|
||||
}));
|
||||
export const relations__pages_v = relations(_pages_v, ({ one, many }) => ({
|
||||
parent: one(pages, {
|
||||
fields: [_pages_v.parent],
|
||||
references: [pages.id],
|
||||
relationName: 'parent',
|
||||
}),
|
||||
version_featuredImage: one(media, {
|
||||
fields: [_pages_v.version_featuredImage],
|
||||
references: [media.id],
|
||||
relationName: 'version_featuredImage',
|
||||
}),
|
||||
_locales: many(_pages_v_locales, {
|
||||
relationName: '_locales',
|
||||
}),
|
||||
}));
|
||||
export const relations_payload_kv = relations(payload_kv, () => ({}));
|
||||
export const relations_payload_locked_documents_rels = relations(
|
||||
payload_locked_documents_rels,
|
||||
@@ -706,6 +1000,11 @@ export const relations_payload_locked_documents_rels = relations(
|
||||
references: [products.id],
|
||||
relationName: 'products',
|
||||
}),
|
||||
pagesID: one(pages, {
|
||||
fields: [payload_locked_documents_rels.pagesID],
|
||||
references: [pages.id],
|
||||
relationName: 'pages',
|
||||
}),
|
||||
}),
|
||||
);
|
||||
export const relations_payload_locked_documents = relations(
|
||||
@@ -739,27 +1038,39 @@ export const relations_payload_preferences = relations(payload_preferences, ({ m
|
||||
export const relations_payload_migrations = relations(payload_migrations, () => ({}));
|
||||
|
||||
type DatabaseSchema = {
|
||||
enum_posts_locale: typeof enum_posts_locale;
|
||||
enum__locales: typeof enum__locales;
|
||||
enum_posts_status: typeof enum_posts_status;
|
||||
enum__posts_v_version_locale: typeof enum__posts_v_version_locale;
|
||||
enum__posts_v_version_status: typeof enum__posts_v_version_status;
|
||||
enum__posts_v_published_locale: typeof enum__posts_v_published_locale;
|
||||
enum_form_submissions_type: typeof enum_form_submissions_type;
|
||||
enum_products_locale: typeof enum_products_locale;
|
||||
enum_products_status: typeof enum_products_status;
|
||||
enum__products_v_version_locale: typeof enum__products_v_version_locale;
|
||||
enum__products_v_version_status: typeof enum__products_v_version_status;
|
||||
enum__products_v_published_locale: typeof enum__products_v_published_locale;
|
||||
enum_pages_layout: typeof enum_pages_layout;
|
||||
enum_pages_status: typeof enum_pages_status;
|
||||
enum__pages_v_version_layout: typeof enum__pages_v_version_layout;
|
||||
enum__pages_v_version_status: typeof enum__pages_v_version_status;
|
||||
enum__pages_v_published_locale: typeof enum__pages_v_published_locale;
|
||||
users_sessions: typeof users_sessions;
|
||||
users: typeof users;
|
||||
media: typeof media;
|
||||
posts: typeof posts;
|
||||
posts_locales: typeof posts_locales;
|
||||
_posts_v: typeof _posts_v;
|
||||
_posts_v_locales: typeof _posts_v_locales;
|
||||
form_submissions: typeof form_submissions;
|
||||
products_categories: typeof products_categories;
|
||||
products: typeof products;
|
||||
products_locales: typeof products_locales;
|
||||
products_rels: typeof products_rels;
|
||||
_products_v_version_categories: typeof _products_v_version_categories;
|
||||
_products_v: typeof _products_v;
|
||||
_products_v_locales: typeof _products_v_locales;
|
||||
_products_v_rels: typeof _products_v_rels;
|
||||
pages: typeof pages;
|
||||
pages_locales: typeof pages_locales;
|
||||
_pages_v: typeof _pages_v;
|
||||
_pages_v_locales: typeof _pages_v_locales;
|
||||
payload_kv: typeof payload_kv;
|
||||
payload_locked_documents: typeof payload_locked_documents;
|
||||
payload_locked_documents_rels: typeof payload_locked_documents_rels;
|
||||
@@ -769,15 +1080,23 @@ type DatabaseSchema = {
|
||||
relations_users_sessions: typeof relations_users_sessions;
|
||||
relations_users: typeof relations_users;
|
||||
relations_media: typeof relations_media;
|
||||
relations_posts_locales: typeof relations_posts_locales;
|
||||
relations_posts: typeof relations_posts;
|
||||
relations__posts_v_locales: typeof relations__posts_v_locales;
|
||||
relations__posts_v: typeof relations__posts_v;
|
||||
relations_form_submissions: typeof relations_form_submissions;
|
||||
relations_products_categories: typeof relations_products_categories;
|
||||
relations_products_locales: typeof relations_products_locales;
|
||||
relations_products_rels: typeof relations_products_rels;
|
||||
relations_products: typeof relations_products;
|
||||
relations__products_v_version_categories: typeof relations__products_v_version_categories;
|
||||
relations__products_v_locales: typeof relations__products_v_locales;
|
||||
relations__products_v_rels: typeof relations__products_v_rels;
|
||||
relations__products_v: typeof relations__products_v;
|
||||
relations_pages_locales: typeof relations_pages_locales;
|
||||
relations_pages: typeof relations_pages;
|
||||
relations__pages_v_locales: typeof relations__pages_v_locales;
|
||||
relations__pages_v: typeof relations__pages_v;
|
||||
relations_payload_kv: typeof relations_payload_kv;
|
||||
relations_payload_locked_documents_rels: typeof relations_payload_locked_documents_rels;
|
||||
relations_payload_locked_documents: typeof relations_payload_locked_documents;
|
||||
|
||||
48
src/payload/blocks/CategoryGrid.ts
Normal file
48
src/payload/blocks/CategoryGrid.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { Block } from 'payload';
|
||||
|
||||
export const CategoryGrid: Block = {
|
||||
slug: 'categoryGrid',
|
||||
interfaceName: 'CategoryGridBlock',
|
||||
fields: [
|
||||
{
|
||||
name: 'categories',
|
||||
type: 'array',
|
||||
required: true,
|
||||
minRows: 1,
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
type: 'textarea',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'image',
|
||||
type: 'upload',
|
||||
relationTo: 'media',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'icon',
|
||||
type: 'upload',
|
||||
relationTo: 'media',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'href',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'ctaLabel',
|
||||
type: 'text',
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
54
src/payload/blocks/CompanyHeritage.ts
Normal file
54
src/payload/blocks/CompanyHeritage.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { Block } from 'payload';
|
||||
|
||||
export const TeamLegacySection: Block = {
|
||||
slug: 'teamLegacySection',
|
||||
interfaceName: 'TeamLegacySectionBlock',
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'subtitle',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'paragraph1',
|
||||
type: 'textarea',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'paragraph2',
|
||||
type: 'textarea',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'expertiseTitle',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'expertiseDesc',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'networkTitle',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'networkDesc',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'backgroundImage',
|
||||
type: 'upload',
|
||||
relationTo: 'media',
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
26
src/payload/blocks/ContactSection.ts
Normal file
26
src/payload/blocks/ContactSection.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Block } from 'payload';
|
||||
|
||||
export const ContactSection: Block = {
|
||||
slug: 'contactSection',
|
||||
interfaceName: 'ContactSectionBlock',
|
||||
fields: [
|
||||
{
|
||||
name: 'showForm',
|
||||
type: 'checkbox',
|
||||
defaultValue: true,
|
||||
label: 'Show Contact Form',
|
||||
},
|
||||
{
|
||||
name: 'showMap',
|
||||
type: 'checkbox',
|
||||
defaultValue: true,
|
||||
label: 'Show Map',
|
||||
},
|
||||
{
|
||||
name: 'showHours',
|
||||
type: 'checkbox',
|
||||
defaultValue: true,
|
||||
label: 'Show Opening Hours',
|
||||
},
|
||||
],
|
||||
};
|
||||
48
src/payload/blocks/HeroSection.ts
Normal file
48
src/payload/blocks/HeroSection.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { Block } from 'payload';
|
||||
|
||||
export const HeroSection: Block = {
|
||||
slug: 'heroSection',
|
||||
interfaceName: 'HeroSectionBlock',
|
||||
fields: [
|
||||
{
|
||||
name: 'badge',
|
||||
type: 'text',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'subtitle',
|
||||
type: 'textarea',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'backgroundImage',
|
||||
type: 'upload',
|
||||
relationTo: 'media',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'ctaLabel',
|
||||
type: 'text',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'ctaHref',
|
||||
type: 'text',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'alignment',
|
||||
type: 'select',
|
||||
defaultValue: 'left',
|
||||
options: [
|
||||
{ label: 'Left', value: 'left' },
|
||||
{ label: 'Center', value: 'center' },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
141
src/payload/blocks/HomeBlocks.ts
Normal file
141
src/payload/blocks/HomeBlocks.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
import { Block } from 'payload';
|
||||
|
||||
export const HomeHero: Block = {
|
||||
slug: 'homeHero',
|
||||
interfaceName: 'HomeHeroBlock',
|
||||
fields: [
|
||||
{ name: 'title', type: 'text', localized: true },
|
||||
{ name: 'subtitle', type: 'text', localized: true },
|
||||
{ name: 'ctaLabel', type: 'text', localized: true },
|
||||
{ name: 'secondaryCtaLabel', type: 'text', localized: true },
|
||||
],
|
||||
};
|
||||
|
||||
export const HomeProductCategories: Block = {
|
||||
slug: 'homeProductCategories',
|
||||
interfaceName: 'HomeProductCategoriesBlock',
|
||||
fields: [
|
||||
{ name: 'title', type: 'text', localized: true },
|
||||
{ name: 'subtitle', type: 'text', localized: true },
|
||||
],
|
||||
};
|
||||
|
||||
export const HomeWhatWeDo: Block = {
|
||||
slug: 'homeWhatWeDo',
|
||||
interfaceName: 'HomeWhatWeDoBlock',
|
||||
fields: [
|
||||
{ name: 'title', type: 'text', localized: true },
|
||||
{ name: 'subtitle', type: 'text', localized: true },
|
||||
{ name: 'expertiseLabel', type: 'text', localized: true },
|
||||
{ name: 'quote', type: 'textarea', localized: true },
|
||||
{
|
||||
name: 'items',
|
||||
type: 'array',
|
||||
localized: true,
|
||||
fields: [
|
||||
{ name: 'title', type: 'text' },
|
||||
{ name: 'description', type: 'textarea' },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const HomeRecentPosts: Block = {
|
||||
slug: 'homeRecentPosts',
|
||||
interfaceName: 'HomeRecentPostsBlock',
|
||||
fields: [
|
||||
{ name: 'title', type: 'text', localized: true },
|
||||
{ name: 'subtitle', type: 'text', localized: true },
|
||||
],
|
||||
};
|
||||
|
||||
export const HomeExperience: Block = {
|
||||
slug: 'homeExperience',
|
||||
interfaceName: 'HomeExperienceBlock',
|
||||
fields: [
|
||||
{ name: 'title', type: 'text', localized: true },
|
||||
{ name: 'subtitle', type: 'text', localized: true },
|
||||
{ name: 'paragraph1', type: 'textarea', localized: true },
|
||||
{ name: 'paragraph2', type: 'textarea', localized: true },
|
||||
{ name: 'badge1', type: 'text', localized: true },
|
||||
{ name: 'badge1Text', type: 'text', localized: true },
|
||||
{ name: 'badge2', type: 'text', localized: true },
|
||||
{ name: 'badge2Text', type: 'text', localized: true },
|
||||
],
|
||||
};
|
||||
|
||||
export const HomeWhyChooseUs: Block = {
|
||||
slug: 'homeWhyChooseUs',
|
||||
interfaceName: 'HomeWhyChooseUsBlock',
|
||||
fields: [
|
||||
{ name: 'title', type: 'text', localized: true },
|
||||
{ name: 'subtitle', type: 'text', localized: true },
|
||||
{ name: 'tagline', type: 'text', localized: true },
|
||||
{
|
||||
name: 'features',
|
||||
type: 'array',
|
||||
localized: true,
|
||||
fields: [{ name: 'feature', type: 'text' }],
|
||||
},
|
||||
{
|
||||
name: 'items',
|
||||
type: 'array',
|
||||
localized: true,
|
||||
fields: [
|
||||
{ name: 'title', type: 'text' },
|
||||
{ name: 'description', type: 'textarea' },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const HomeMeetTheTeam: Block = {
|
||||
slug: 'homeMeetTheTeam',
|
||||
interfaceName: 'HomeMeetTheTeamBlock',
|
||||
fields: [
|
||||
{ name: 'title', type: 'text', localized: true },
|
||||
{ name: 'subtitle', type: 'text', localized: true },
|
||||
{ name: 'description', type: 'textarea', localized: true },
|
||||
{ name: 'ctaLabel', type: 'text', localized: true },
|
||||
{ name: 'networkLabel', type: 'text', localized: true },
|
||||
],
|
||||
};
|
||||
|
||||
export const HomeGallery: Block = {
|
||||
slug: 'homeGallery',
|
||||
interfaceName: 'HomeGalleryBlock',
|
||||
fields: [
|
||||
{ name: 'title', type: 'text', localized: true },
|
||||
{ name: 'subtitle', type: 'text', localized: true },
|
||||
],
|
||||
};
|
||||
|
||||
export const HomeVideo: Block = {
|
||||
slug: 'homeVideo',
|
||||
interfaceName: 'HomeVideoBlock',
|
||||
fields: [{ name: 'title', type: 'text', localized: true }],
|
||||
};
|
||||
|
||||
export const HomeCTA: Block = {
|
||||
slug: 'homeCTA',
|
||||
interfaceName: 'HomeCTABlock',
|
||||
fields: [
|
||||
{ name: 'title', type: 'text', localized: true },
|
||||
{ name: 'subtitle', type: 'text', localized: true },
|
||||
{ name: 'description', type: 'textarea', localized: true },
|
||||
{ name: 'buttonLabel', type: 'text', localized: true },
|
||||
],
|
||||
};
|
||||
|
||||
export const homeBlocksArray = [
|
||||
HomeHero,
|
||||
HomeProductCategories,
|
||||
HomeWhatWeDo,
|
||||
HomeRecentPosts,
|
||||
HomeExperience,
|
||||
HomeWhyChooseUs,
|
||||
HomeMeetTheTeam,
|
||||
HomeGallery,
|
||||
HomeVideo,
|
||||
HomeCTA,
|
||||
];
|
||||
27
src/payload/blocks/ImageGallery.ts
Normal file
27
src/payload/blocks/ImageGallery.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { Block } from 'payload';
|
||||
|
||||
export const ImageGallery: Block = {
|
||||
slug: 'imageGallery',
|
||||
interfaceName: 'ImageGalleryBlock',
|
||||
fields: [
|
||||
{
|
||||
name: 'images',
|
||||
type: 'array',
|
||||
required: true,
|
||||
minRows: 1,
|
||||
fields: [
|
||||
{
|
||||
name: 'image',
|
||||
type: 'upload',
|
||||
relationTo: 'media',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'alt',
|
||||
type: 'text',
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
41
src/payload/blocks/ManifestoGrid.ts
Normal file
41
src/payload/blocks/ManifestoGrid.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { Block } from 'payload';
|
||||
|
||||
export const ManifestoGrid: Block = {
|
||||
slug: 'manifestoGrid',
|
||||
interfaceName: 'ManifestoGridBlock',
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'subtitle',
|
||||
type: 'text',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'tagline',
|
||||
type: 'textarea',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'items',
|
||||
type: 'array',
|
||||
required: true,
|
||||
minRows: 1,
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
type: 'textarea',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
28
src/payload/blocks/SupportCTA.ts
Normal file
28
src/payload/blocks/SupportCTA.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { Block } from 'payload';
|
||||
|
||||
export const SupportCTA: Block = {
|
||||
slug: 'supportCTA',
|
||||
interfaceName: 'SupportCTABlock',
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
type: 'textarea',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'buttonLabel',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'buttonHref',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
62
src/payload/blocks/TeamProfile.ts
Normal file
62
src/payload/blocks/TeamProfile.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { Block } from 'payload';
|
||||
|
||||
export const TeamProfile: Block = {
|
||||
slug: 'teamProfile',
|
||||
interfaceName: 'TeamProfileBlock',
|
||||
fields: [
|
||||
{
|
||||
name: 'name',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'role',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'quote',
|
||||
type: 'textarea',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
type: 'textarea',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'image',
|
||||
type: 'upload',
|
||||
relationTo: 'media',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'linkedinUrl',
|
||||
type: 'text',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'linkedinLabel',
|
||||
type: 'text',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'layout',
|
||||
type: 'select',
|
||||
defaultValue: 'imageRight',
|
||||
options: [
|
||||
{ label: 'Image Right', value: 'imageRight' },
|
||||
{ label: 'Image Left', value: 'imageLeft' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'colorScheme',
|
||||
type: 'select',
|
||||
defaultValue: 'dark',
|
||||
options: [
|
||||
{ label: 'Dark', value: 'dark' },
|
||||
{ label: 'Light', value: 'light' },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,27 +1,41 @@
|
||||
import { AnimatedImage } from './AnimatedImage';
|
||||
import { Callout } from './Callout';
|
||||
import { CategoryGrid } from './CategoryGrid';
|
||||
import { ChatBubble } from './ChatBubble';
|
||||
import { ComparisonGrid } from './ComparisonGrid';
|
||||
import { ContactSection } from './ContactSection';
|
||||
import { HeroSection } from './HeroSection';
|
||||
import { HighlightBox } from './HighlightBox';
|
||||
import { ImageGallery } from './ImageGallery';
|
||||
import { ManifestoGrid } from './ManifestoGrid';
|
||||
import { PowerCTA } from './PowerCTA';
|
||||
import { ProductTabs } from './ProductTabs';
|
||||
import { SplitHeading } from './SplitHeading';
|
||||
import { Stats } from './Stats';
|
||||
import { StickyNarrative } from './StickyNarrative';
|
||||
import { TeamProfile } from './TeamProfile';
|
||||
import { TechnicalGrid } from './TechnicalGrid';
|
||||
import { VisualLinkPreview } from './VisualLinkPreview';
|
||||
import { homeBlocksArray } from './HomeBlocks';
|
||||
|
||||
export const payloadBlocks = [
|
||||
...homeBlocksArray,
|
||||
AnimatedImage,
|
||||
Callout,
|
||||
CategoryGrid,
|
||||
ChatBubble,
|
||||
ComparisonGrid,
|
||||
ContactSection,
|
||||
HeroSection,
|
||||
HighlightBox,
|
||||
ImageGallery,
|
||||
ManifestoGrid,
|
||||
PowerCTA,
|
||||
ProductTabs,
|
||||
SplitHeading,
|
||||
Stats,
|
||||
StickyNarrative,
|
||||
TeamProfile,
|
||||
TechnicalGrid,
|
||||
VisualLinkPreview,
|
||||
];
|
||||
|
||||
@@ -1,44 +1,65 @@
|
||||
import { CollectionConfig } from 'payload';
|
||||
import { lexicalEditor } from '@payloadcms/richtext-lexical';
|
||||
import { lexicalEditor, BlocksFeature } from '@payloadcms/richtext-lexical';
|
||||
import { payloadBlocks } from '../blocks/allBlocks';
|
||||
|
||||
export const Pages: CollectionConfig = {
|
||||
slug: 'pages',
|
||||
admin: {
|
||||
useAsTitle: 'title',
|
||||
defaultColumns: ['title', 'slug', 'locale', 'updatedAt'],
|
||||
defaultColumns: ['title', 'slug', 'layout', '_status', 'updatedAt'],
|
||||
},
|
||||
versions: {
|
||||
drafts: true,
|
||||
},
|
||||
access: {
|
||||
read: () => true,
|
||||
read: ({ req: { user } }) => {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
return true;
|
||||
}
|
||||
if (user) {
|
||||
return true;
|
||||
}
|
||||
return {
|
||||
_status: {
|
||||
equals: 'published',
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
required: true,
|
||||
localized: true,
|
||||
},
|
||||
{
|
||||
name: 'slug',
|
||||
type: 'text',
|
||||
required: true,
|
||||
localized: true,
|
||||
admin: {
|
||||
position: 'sidebar',
|
||||
description: 'The URL slug for this locale (e.g. "impressum" for DE, "imprint" for EN).',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'locale',
|
||||
name: 'layout',
|
||||
type: 'select',
|
||||
defaultValue: 'default',
|
||||
options: [
|
||||
{ label: 'English', value: 'en' },
|
||||
{ label: 'German', value: 'de' },
|
||||
{ label: 'Default (Article)', value: 'default' },
|
||||
{ label: 'Full Bleed (Blocks Only)', value: 'fullBleed' },
|
||||
],
|
||||
required: true,
|
||||
admin: {
|
||||
position: 'sidebar',
|
||||
description: 'Full Bleed pages render blocks edge-to-edge without a generic hero wrapper.',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'excerpt',
|
||||
type: 'textarea',
|
||||
localized: true,
|
||||
admin: {
|
||||
position: 'sidebar',
|
||||
},
|
||||
@@ -54,7 +75,15 @@ export const Pages: CollectionConfig = {
|
||||
{
|
||||
name: 'content',
|
||||
type: 'richText',
|
||||
editor: lexicalEditor({}),
|
||||
localized: true,
|
||||
editor: lexicalEditor({
|
||||
features: ({ defaultFeatures }) => [
|
||||
...defaultFeatures,
|
||||
BlocksFeature({
|
||||
blocks: payloadBlocks,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -19,22 +19,16 @@ export const Posts: CollectionConfig = {
|
||||
defaultColumns: ['featuredImage', 'title', 'date', 'updatedAt', '_status'],
|
||||
},
|
||||
versions: {
|
||||
drafts: true, // Enables Draft/Published workflows
|
||||
drafts: true,
|
||||
},
|
||||
access: {
|
||||
read: ({ req: { user } }) => {
|
||||
// In local development, always show everything (including Drafts and scheduled future posts)
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If an Admin user is logged in, they can view everything
|
||||
if (user) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// For public unauthenticated visitors in PROD/STAGING contexts:
|
||||
// Only serve Posts where Status = "published" AND the publish Date is in the past!
|
||||
return {
|
||||
and: [
|
||||
{
|
||||
@@ -56,19 +50,20 @@ export const Posts: CollectionConfig = {
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
required: true,
|
||||
localized: true,
|
||||
},
|
||||
{
|
||||
name: 'slug',
|
||||
type: 'text',
|
||||
required: true,
|
||||
unique: true,
|
||||
localized: true,
|
||||
admin: {
|
||||
position: 'sidebar',
|
||||
description: 'Unique slug per locale (e.g. same slug can exist in DE and EN).',
|
||||
},
|
||||
hooks: {
|
||||
beforeValidate: [
|
||||
({ value, data }) => {
|
||||
// Auto-generate slug from title if left blank
|
||||
if (value || !data?.title) return value;
|
||||
return data.title
|
||||
.toLowerCase()
|
||||
@@ -81,6 +76,7 @@ export const Posts: CollectionConfig = {
|
||||
{
|
||||
name: 'excerpt',
|
||||
type: 'text',
|
||||
localized: true,
|
||||
admin: {
|
||||
description: 'A short summary for blog feed cards and SEO.',
|
||||
},
|
||||
@@ -104,22 +100,10 @@ export const Posts: CollectionConfig = {
|
||||
description: 'The primary Hero image used for headers and OpenGraph previews.',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'locale',
|
||||
type: 'select',
|
||||
required: true,
|
||||
admin: {
|
||||
position: 'sidebar',
|
||||
},
|
||||
options: [
|
||||
{ label: 'English', value: 'en' },
|
||||
{ label: 'German', value: 'de' },
|
||||
],
|
||||
defaultValue: 'en',
|
||||
},
|
||||
{
|
||||
name: 'category',
|
||||
type: 'text',
|
||||
localized: true,
|
||||
admin: {
|
||||
position: 'sidebar',
|
||||
description: 'Used for tag bucketing (e.g. "Kabel Technologie").',
|
||||
@@ -128,6 +112,7 @@ export const Posts: CollectionConfig = {
|
||||
{
|
||||
name: 'content',
|
||||
type: 'richText',
|
||||
localized: true,
|
||||
editor: lexicalEditor({
|
||||
features: ({ defaultFeatures }) => [
|
||||
...defaultFeatures,
|
||||
|
||||
@@ -17,7 +17,7 @@ import { ProductTabs } from '../blocks/ProductTabs';
|
||||
export const Products: CollectionConfig = {
|
||||
slug: 'products',
|
||||
admin: {
|
||||
defaultColumns: ['featuredImage', 'title', 'sku', 'locale', 'updatedAt', '_status'],
|
||||
defaultColumns: ['featuredImage', 'title', 'sku', 'updatedAt', '_status'],
|
||||
},
|
||||
versions: {
|
||||
drafts: true,
|
||||
@@ -42,6 +42,7 @@ export const Products: CollectionConfig = {
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
required: true,
|
||||
localized: true,
|
||||
},
|
||||
{
|
||||
name: 'sku',
|
||||
@@ -52,6 +53,7 @@ export const Products: CollectionConfig = {
|
||||
},
|
||||
},
|
||||
{
|
||||
// slug is shared: the cable name (e.g. "n2xy") is the same in DE and EN
|
||||
name: 'slug',
|
||||
type: 'text',
|
||||
required: true,
|
||||
@@ -63,19 +65,7 @@ export const Products: CollectionConfig = {
|
||||
name: 'description',
|
||||
type: 'textarea',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'locale',
|
||||
type: 'select',
|
||||
required: true,
|
||||
admin: {
|
||||
position: 'sidebar',
|
||||
},
|
||||
options: [
|
||||
{ label: 'English', value: 'en' },
|
||||
{ label: 'German', value: 'de' },
|
||||
],
|
||||
defaultValue: 'de',
|
||||
localized: true,
|
||||
},
|
||||
{
|
||||
name: 'categories',
|
||||
@@ -112,11 +102,13 @@ export const Products: CollectionConfig = {
|
||||
{
|
||||
name: 'application',
|
||||
type: 'richText',
|
||||
localized: true,
|
||||
editor: lexicalEditor({}),
|
||||
},
|
||||
{
|
||||
name: 'content',
|
||||
type: 'richText',
|
||||
localized: true,
|
||||
editor: lexicalEditor({
|
||||
features: ({ defaultFeatures }) => [
|
||||
...defaultFeatures,
|
||||
|
||||
12
src/payload/components/Icon.tsx
Normal file
12
src/payload/components/Icon.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import React from 'react';
|
||||
|
||||
export default function Icon() {
|
||||
return (
|
||||
<img
|
||||
src="/logo-blue.svg"
|
||||
alt="KLZ"
|
||||
className="klz-admin-icon"
|
||||
style={{ maxWidth: '100%', height: 'auto', maxHeight: '32px', display: 'block' }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
12
src/payload/components/Logo.tsx
Normal file
12
src/payload/components/Logo.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import React from 'react';
|
||||
|
||||
export default function Logo() {
|
||||
return (
|
||||
<img
|
||||
src="/logo-blue.svg"
|
||||
alt="KLZ Cables"
|
||||
className="klz-admin-logo"
|
||||
style={{ maxWidth: '100%', height: 'auto', maxHeight: '40px', display: 'block' }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -29,12 +29,12 @@ export async function seedDatabase(payload: Payload) {
|
||||
payload.logger.info('📦 No products found. Creating smoke test product (NAY2Y)...');
|
||||
await payload.create({
|
||||
collection: 'products',
|
||||
locale: 'de',
|
||||
data: {
|
||||
title: 'NAY2Y Smoke Test',
|
||||
sku: 'SMOKE-TEST-001',
|
||||
slug: 'nay2y',
|
||||
description: 'A dummy product for CI/CD smoke testing and OG image verification.',
|
||||
locale: 'de',
|
||||
categories: [{ category: 'Power Cables' }],
|
||||
_status: 'published',
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user