feat: payload cms
This commit is contained in:
@@ -429,6 +429,26 @@ jobs:
|
|||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
id: deps
|
id: deps
|
||||||
run: pnpm install --frozen-lockfile
|
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) ──────────────────────────────────
|
# ── Critical Smoke Tests (MUST pass) ──────────────────────────────────
|
||||||
- name: 🚀 OG Image Check
|
- name: 🚀 OG Image Check
|
||||||
@@ -477,6 +497,8 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
NEXT_PUBLIC_BASE_URL: ${{ needs.prepare.outputs.next_public_url }}
|
NEXT_PUBLIC_BASE_URL: ${{ needs.prepare.outputs.next_public_url }}
|
||||||
GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD || 'klz2026' }}
|
GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD || 'klz2026' }}
|
||||||
|
PUPPETEER_EXECUTABLE_PATH: /usr/bin/chromium
|
||||||
|
CHROME_PATH: /usr/bin/chromium
|
||||||
run: pnpm check:assets
|
run: pnpm check:assets
|
||||||
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────────
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
@@ -568,6 +590,7 @@ jobs:
|
|||||||
image: catthehacker/ubuntu:act-latest
|
image: catthehacker/ubuntu:act-latest
|
||||||
steps:
|
steps:
|
||||||
- name: 🔔 Gotify
|
- name: 🔔 Gotify
|
||||||
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
DEPLOY="${{ needs.deploy.result }}"
|
DEPLOY="${{ needs.deploy.result }}"
|
||||||
SMOKE="${{ needs.post_deploy_checks.result }}"
|
SMOKE="${{ needs.post_deploy_checks.result }}"
|
||||||
|
|||||||
1
.npmrc
1
.npmrc
@@ -1,3 +1,2 @@
|
|||||||
@mintel:registry=https://npm.infra.mintel.me/
|
@mintel:registry=https://npm.infra.mintel.me/
|
||||||
//npm.infra.mintel.me/:_authToken=${NPM_TOKEN}
|
//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 { UnderlineFeatureClient as UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
|
||||||
import { BoldFeatureClient as BoldFeatureClient_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 { 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'
|
import { CollectionCards as CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1 } from '@payloadcms/next/rsc'
|
||||||
|
|
||||||
export const importMap = {
|
export const importMap = {
|
||||||
@@ -49,5 +51,7 @@ export const importMap = {
|
|||||||
"@payloadcms/richtext-lexical/client#UnderlineFeatureClient": UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
"@payloadcms/richtext-lexical/client#UnderlineFeatureClient": UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
"@payloadcms/richtext-lexical/client#BoldFeatureClient": BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
"@payloadcms/richtext-lexical/client#BoldFeatureClient": BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
|
||||||
"@payloadcms/richtext-lexical/client#ItalicFeatureClient": ItalicFeatureClient_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
|
"@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 { getTranslations, setRequestLocale } from 'next-intl/server';
|
||||||
import { Metadata } from 'next';
|
import { Metadata } from 'next';
|
||||||
import { getPageBySlug, getAllPages } from '@/lib/pages';
|
import { getPageBySlug, getAllPages } from '@/lib/pages';
|
||||||
|
import { mapSlugToFileSlug, mapFileSlugToTranslated } from '@/lib/slugs';
|
||||||
import PayloadRichText from '@/components/PayloadRichText';
|
import PayloadRichText from '@/components/PayloadRichText';
|
||||||
import { SITE_URL } from '@/lib/schema';
|
import { SITE_URL } from '@/lib/schema';
|
||||||
import TrackedLink from '@/components/analytics/TrackedLink';
|
import TrackedLink from '@/components/analytics/TrackedLink';
|
||||||
@@ -20,15 +21,19 @@ export async function generateMetadata({ params }: PageProps): Promise<Metadata>
|
|||||||
|
|
||||||
if (!pageData) return {};
|
if (!pageData) return {};
|
||||||
|
|
||||||
|
const fileSlug = await mapSlugToFileSlug(slug, locale);
|
||||||
|
const deSlug = await mapFileSlugToTranslated(fileSlug, 'de');
|
||||||
|
const enSlug = await mapFileSlugToTranslated(fileSlug, 'en');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: pageData.frontmatter.title,
|
title: pageData.frontmatter.title,
|
||||||
description: pageData.frontmatter.excerpt || '',
|
description: pageData.frontmatter.excerpt || '',
|
||||||
alternates: {
|
alternates: {
|
||||||
canonical: `${SITE_URL}/${locale}/${slug}`,
|
canonical: `${SITE_URL}/${locale}/${slug}`,
|
||||||
languages: {
|
languages: {
|
||||||
de: `${SITE_URL}/de/${slug}`,
|
de: `${SITE_URL}/de/${deSlug}`,
|
||||||
en: `${SITE_URL}/en/${slug}`,
|
en: `${SITE_URL}/en/${enSlug}`,
|
||||||
'x-default': `${SITE_URL}/en/${slug}`,
|
'x-default': `${SITE_URL}/en/${enSlug}`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
openGraph: {
|
openGraph: {
|
||||||
@@ -54,6 +59,16 @@ export default async function StandardPage({ params }: PageProps) {
|
|||||||
notFound();
|
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 (
|
return (
|
||||||
<div className="flex flex-col min-h-screen bg-white">
|
<div className="flex flex-col min-h-screen bg-white">
|
||||||
{/* Hero Section */}
|
{/* Hero Section */}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ImageResponse } from 'next/og';
|
import { ImageResponse } from 'next/og';
|
||||||
import { getProductBySlug } from '@/lib/mdx';
|
import { getProductBySlug } from '@/lib/products';
|
||||||
import { getTranslations } from 'next-intl/server';
|
import { getTranslations } from 'next-intl/server';
|
||||||
import { OGImageTemplate } from '@/components/OGImageTemplate';
|
import { OGImageTemplate } from '@/components/OGImageTemplate';
|
||||||
import { NextRequest } from 'next/server';
|
import { NextRequest } from 'next/server';
|
||||||
|
|||||||
@@ -67,6 +67,9 @@ export default async function BlogIndex({ params }: BlogIndexProps) {
|
|||||||
alt={featuredPost.frontmatter.title}
|
alt={featuredPost.frontmatter.title}
|
||||||
fill
|
fill
|
||||||
className="absolute inset-0 w-full h-full object-cover opacity-40 md:opacity-60"
|
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"
|
sizes="100vw"
|
||||||
priority
|
priority
|
||||||
/>
|
/>
|
||||||
@@ -168,6 +171,9 @@ export default async function BlogIndex({ params }: BlogIndexProps) {
|
|||||||
alt={post.frontmatter.title}
|
alt={post.frontmatter.title}
|
||||||
fill
|
fill
|
||||||
className="w-full h-full object-cover transition-transform duration-1000 group-hover:scale-110"
|
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"
|
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" />
|
<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>
|
</span>
|
||||||
{(new Date(post.frontmatter.date) > new Date() ||
|
{(new Date(post.frontmatter.date) > new Date() ||
|
||||||
post.frontmatter.public === false) && (
|
post.frontmatter.public === false) && (
|
||||||
<span className="px-1.5 py-0.5 border border-current rounded-sm text-[9px] md:text-xs">
|
<span className="px-1.5 py-0.5 border border-current rounded-sm text-[9px] md:text-xs">
|
||||||
Draft
|
Draft
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</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">
|
<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}
|
{post.frontmatter.title}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import RelatedProducts from '@/components/RelatedProducts';
|
|||||||
import DatasheetDownload from '@/components/DatasheetDownload';
|
import DatasheetDownload from '@/components/DatasheetDownload';
|
||||||
import { Badge, Card, Container, Heading, Section } from '@/components/ui';
|
import { Badge, Card, Container, Heading, Section } from '@/components/ui';
|
||||||
import { getDatasheetPath } from '@/lib/datasheets';
|
import { getDatasheetPath } from '@/lib/datasheets';
|
||||||
import { getAllProducts, getProductBySlug } from '@/lib/mdx';
|
import { getAllProducts, getProductBySlug } from '@/lib/products';
|
||||||
import { mapFileSlugToTranslated, mapSlugToFileSlug } from '@/lib/slugs';
|
import { mapFileSlugToTranslated, mapSlugToFileSlug } from '@/lib/slugs';
|
||||||
import { Metadata } from 'next';
|
import { Metadata } from 'next';
|
||||||
import { getTranslations, setRequestLocale } from 'next-intl/server';
|
import { getTranslations, setRequestLocale } from 'next-intl/server';
|
||||||
|
|||||||
@@ -94,6 +94,16 @@ export async function sendContactFormAction(formData: FormData) {
|
|||||||
logger.info('Notification email sent successfully', {
|
logger.info('Notification email sent successfully', {
|
||||||
messageId: notificationResult.messageId,
|
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)
|
// 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', {
|
logger.info('Confirmation email sent successfully', {
|
||||||
messageId: confirmationResult.messageId,
|
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)
|
// Notify via Gotify (Internal)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { config } from '@/lib/config';
|
import { config } from '@/lib/config';
|
||||||
import { MetadataRoute } from 'next';
|
import { MetadataRoute } from 'next';
|
||||||
import { getAllProductsMetadata } from '@/lib/mdx';
|
import { getAllProductsMetadata } from '@/lib/products';
|
||||||
import { getAllPostsMetadata } from '@/lib/blog';
|
import { getAllPostsMetadata } from '@/lib/blog';
|
||||||
import { getAllPagesMetadata } from '@/lib/pages';
|
import { getAllPagesMetadata } from '@/lib/pages';
|
||||||
import { mapFileSlugToTranslated } from '@/lib/slugs';
|
import { mapFileSlugToTranslated } from '@/lib/slugs';
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ export default function Header() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
const headerClass = cn(
|
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':
|
'bg-primary/95 backdrop-blur-md md:bg-transparent py-3 md:py-8 shadow-2xl md:shadow-none':
|
||||||
isHomePage && !isScrolled && !isMobileMenuOpen,
|
isHomePage && !isScrolled && !isMobileMenuOpen,
|
||||||
@@ -153,8 +153,7 @@ export default function Header() {
|
|||||||
<header className={headerClass} style={{ animationDuration: '800ms' }}>
|
<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="container mx-auto px-4 md:px-12 lg:px-16 max-w-7xl flex items-center justify-between">
|
||||||
<div
|
<div
|
||||||
className="flex-shrink-0 group touch-target animate-in fade-in zoom-in-90 fill-mode-both"
|
className="flex-shrink-0 group touch-target fill-mode-both"
|
||||||
style={{ animationDuration: '600ms', animationDelay: '100ms' }}
|
|
||||||
>
|
>
|
||||||
<Link
|
<Link
|
||||||
href={`/${currentLocale}`}
|
href={`/${currentLocale}`}
|
||||||
@@ -173,6 +172,9 @@ export default function Header() {
|
|||||||
style={{ width: 'auto' }}
|
style={{ width: 'auto' }}
|
||||||
className="h-10 md:h-14 w-auto transition-all duration-500 group-hover:scale-110"
|
className="h-10 md:h-14 w-auto transition-all duration-500 group-hover:scale-110"
|
||||||
priority
|
priority
|
||||||
|
fetchPriority="high"
|
||||||
|
loading="eager"
|
||||||
|
decoding="sync"
|
||||||
/>
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { defaultJSXConverters, RichText } from '@payloadcms/richtext-lexical/react';
|
import { defaultJSXConverters, RichText } from '@payloadcms/richtext-lexical/react';
|
||||||
import type { JSXConverters } from '@payloadcms/richtext-lexical/react';
|
import type { JSXConverters } from '@payloadcms/richtext-lexical/react';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
|
import { Suspense } from 'react';
|
||||||
|
|
||||||
// Import all custom React components that were previously mapped via MDX
|
// Import all custom React components that were previously mapped via MDX
|
||||||
import StickyNarrative from '@/components/blog/StickyNarrative';
|
import StickyNarrative from '@/components/blog/StickyNarrative';
|
||||||
@@ -16,6 +17,24 @@ import Stats from '@/components/blog/Stats';
|
|||||||
import SplitHeading from '@/components/blog/SplitHeading';
|
import SplitHeading from '@/components/blog/SplitHeading';
|
||||||
import ProductTabs from '@/components/ProductTabs';
|
import ProductTabs from '@/components/ProductTabs';
|
||||||
import ProductTechnicalData from '@/components/ProductTechnicalData';
|
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 = {
|
const jsxConverters: JSXConverters = {
|
||||||
...defaultJSXConverters,
|
...defaultJSXConverters,
|
||||||
@@ -255,6 +274,372 @@ const jsxConverters: JSXConverters = {
|
|||||||
{node.fields.content && <RichText data={node.fields.content} converters={jsxConverters} />}
|
{node.fields.content && <RichText data={node.fields.content} converters={jsxConverters} />}
|
||||||
</ProductTabs>
|
</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)
|
// Custom converter for the Payload "upload" Lexical node (Media collection)
|
||||||
// This natively reconstructs Next.js <Image /> tags pointing to the focal-point cropped sizes
|
// 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;
|
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 (
|
return (
|
||||||
<div className="article-content max-w-none">
|
<div className={className}>
|
||||||
<RichText data={data} converters={jsxConverters} />
|
<RichText data={data} converters={dynamicConverters} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { getAllProducts } from '@/lib/mdx';
|
import { getAllProducts } from '@/lib/products';
|
||||||
import { getTranslations } from 'next-intl/server';
|
import { getTranslations } from 'next-intl/server';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { RelatedProductLink } from './RelatedProductLink';
|
import { RelatedProductLink } from './RelatedProductLink';
|
||||||
|
|||||||
@@ -30,8 +30,11 @@ export default function PostNavigation({
|
|||||||
{/* Background Image */}
|
{/* Background Image */}
|
||||||
{prev.frontmatter.featuredImage ? (
|
{prev.frontmatter.featuredImage ? (
|
||||||
<div
|
<div
|
||||||
className="absolute inset-0 bg-cover bg-center transition-transform duration-700 group-hover:scale-110"
|
className="absolute inset-0 z-0 bg-cover transition-transform duration-1000 group-hover:scale-105"
|
||||||
style={{ backgroundImage: `url(${prev.frontmatter.featuredImage.split('?')[0]})` }}
|
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" />
|
<div className="absolute inset-0 bg-neutral-100" />
|
||||||
@@ -81,8 +84,11 @@ export default function PostNavigation({
|
|||||||
{/* Background Image */}
|
{/* Background Image */}
|
||||||
{next.frontmatter.featuredImage ? (
|
{next.frontmatter.featuredImage ? (
|
||||||
<div
|
<div
|
||||||
className="absolute inset-0 bg-cover bg-center transition-transform duration-700 group-hover:scale-110"
|
className="absolute inset-0 z-0 bg-cover transition-transform duration-1000 group-hover:scale-105"
|
||||||
style={{ backgroundImage: `url(${next.frontmatter.featuredImage.split('?')[0]})` }}
|
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" />
|
<div className="absolute inset-0 bg-neutral-100" />
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React from 'react';
|
|||||||
import { useTranslations, useLocale } from 'next-intl';
|
import { useTranslations, useLocale } from 'next-intl';
|
||||||
import { Section, Container, Button, Heading } from '../../components/ui';
|
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 t = useTranslations('Home.cta');
|
||||||
const locale = useLocale();
|
const locale = useLocale();
|
||||||
|
|
||||||
@@ -10,20 +10,20 @@ export default function CTA() {
|
|||||||
<Section className="bg-primary text-white py-32 relative overflow-hidden">
|
<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 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" />
|
<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">
|
<Container className="relative z-10">
|
||||||
<div className="flex flex-col lg:flex-row items-center justify-between gap-16">
|
<div className="flex flex-col lg:flex-row items-center justify-between gap-16">
|
||||||
<div className="max-w-3xl text-center lg:text-left">
|
<div className="max-w-3xl text-center lg:text-left">
|
||||||
<Heading level={2} subtitle={t('subtitle')} className="text-white mb-6">
|
<Heading level={2} subtitle={data?.subtitle || t('subtitle')} className="text-white mb-6">
|
||||||
<span className="text-white">{t('title')}</span>
|
<span className="text-white">{data?.title || t('title')}</span>
|
||||||
</Heading>
|
</Heading>
|
||||||
<p className="text-lg md:text-xl text-white/70 leading-relaxed">
|
<p className="text-lg md:text-xl text-white/70 leading-relaxed">
|
||||||
{t('description')}
|
{data?.description || t('description')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
<Button href={`/${locale}/contact`} variant="accent" size="xl" className="group px-12">
|
<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>
|
<span className="ml-3 transition-transform group-hover:translate-x-2">→</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import Image from 'next/image';
|
|||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
import { Section, Container, Heading } from '../../components/ui';
|
import { Section, Container, Heading } from '../../components/ui';
|
||||||
|
|
||||||
export default function Experience() {
|
export default function Experience({ data }: { data?: any }) {
|
||||||
const t = useTranslations('Home.experience');
|
const t = useTranslations('Home.experience');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -11,7 +11,7 @@ export default function Experience() {
|
|||||||
<div className="absolute inset-0 z-0">
|
<div className="absolute inset-0 z-0">
|
||||||
<Image
|
<Image
|
||||||
src="/uploads/2024/12/1694273920124-copy-2.webp"
|
src="/uploads/2024/12/1694273920124-copy-2.webp"
|
||||||
alt={t('subtitle')}
|
alt={data?.subtitle || t('subtitle')}
|
||||||
fill
|
fill
|
||||||
className="object-cover object-center scale-105 animate-slow-zoom"
|
className="object-cover object-center scale-105 animate-slow-zoom"
|
||||||
sizes="100vw"
|
sizes="100vw"
|
||||||
@@ -22,31 +22,31 @@ export default function Experience() {
|
|||||||
|
|
||||||
<Container className="relative z-10">
|
<Container className="relative z-10">
|
||||||
<div className="max-w-3xl">
|
<div className="max-w-3xl">
|
||||||
<Heading level={2} subtitle={t('subtitle')} className="text-white">
|
<Heading level={2} subtitle={data?.subtitle || t('subtitle')} className="text-white">
|
||||||
<span className="text-white">{t('title')}</span>
|
<span className="text-white">{data?.title || t('title')}</span>
|
||||||
</Heading>
|
</Heading>
|
||||||
<div className="space-y-8 text-lg md:text-xl text-white/90 leading-relaxed font-medium">
|
<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">
|
<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>
|
||||||
<p className="pl-9">{t('p2')}</p>
|
<p className="pl-9">{data?.paragraph2 || t('p2')}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<dl className="mt-16 grid grid-cols-1 md:grid-cols-2 gap-12">
|
<dl className="mt-16 grid grid-cols-1 md:grid-cols-2 gap-12">
|
||||||
<div className="animate-fade-in">
|
<div className="animate-fade-in">
|
||||||
<dt className="text-2xl md:text-3xl font-extrabold text-accent mb-4">
|
<dt className="text-2xl md:text-3xl font-extrabold text-accent mb-4">
|
||||||
{t('certifiedQuality')}
|
{data?.badge1 || t('certifiedQuality')}
|
||||||
</dt>
|
</dt>
|
||||||
<dd className="text-base md:text-lg font-bold uppercase tracking-widest text-white/60">
|
<dd className="text-base md:text-lg font-bold uppercase tracking-widest text-white/60">
|
||||||
{t('vdeApproved')}
|
{data?.badge1Text || t('vdeApproved')}
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
<div className="animate-fade-in" style={{ animationDelay: '100ms' }}>
|
<div className="animate-fade-in" style={{ animationDelay: '100ms' }}>
|
||||||
<dt className="text-2xl md:text-3xl font-extrabold text-accent mb-4">
|
<dt className="text-2xl md:text-3xl font-extrabold text-accent mb-4">
|
||||||
{t('fullSpectrum')}
|
{data?.badge2 || t('fullSpectrum')}
|
||||||
</dt>
|
</dt>
|
||||||
<dd className="text-base md:text-lg font-bold uppercase tracking-widest text-white/60">
|
<dd className="text-base md:text-lg font-bold uppercase tracking-widest text-white/60">
|
||||||
{t('solutionsRange')}
|
{data?.badge2Text || t('solutionsRange')}
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
</dl>
|
</dl>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import dynamic from 'next/dynamic';
|
|||||||
const Lightbox = dynamic(() => import('../Lightbox'), { ssr: false });
|
const Lightbox = dynamic(() => import('../Lightbox'), { ssr: false });
|
||||||
import { useSearchParams } from 'next/navigation';
|
import { useSearchParams } from 'next/navigation';
|
||||||
|
|
||||||
export default function GallerySection() {
|
export default function GallerySection({ data }: { data?: any }) {
|
||||||
const t = useTranslations('Home.gallery');
|
const t = useTranslations('Home.gallery');
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const images = [
|
const images = [
|
||||||
@@ -26,8 +26,8 @@ export default function GallerySection() {
|
|||||||
return (
|
return (
|
||||||
<Section className="bg-white text-white py-32">
|
<Section className="bg-white text-white py-32">
|
||||||
<Container>
|
<Container>
|
||||||
<Heading level={2} subtitle={t('subtitle')} align="center">
|
<Heading level={2} subtitle={data?.subtitle || t('subtitle')} align="center">
|
||||||
{t('title')}
|
{data?.title || t('title')}
|
||||||
</Heading>
|
</Heading>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
<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';
|
import { AnalyticsEvents } from '../analytics/analytics-events';
|
||||||
const HeroIllustration = dynamic(() => import('./HeroIllustration'), { ssr: false });
|
const HeroIllustration = dynamic(() => import('./HeroIllustration'), { ssr: false });
|
||||||
|
|
||||||
export default function Hero() {
|
export default function Hero({ data }: { data?: any }) {
|
||||||
const t = useTranslations('Home.hero');
|
const t = useTranslations('Home.hero');
|
||||||
const locale = useLocale();
|
const locale = useLocale();
|
||||||
const { trackEvent } = useAnalytics();
|
const { trackEvent } = useAnalytics();
|
||||||
@@ -22,24 +22,28 @@ export default function Hero() {
|
|||||||
level={1}
|
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]"
|
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', {
|
{data?.title ? (
|
||||||
green: (chunks) => (
|
<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>') }} />
|
||||||
<span className="relative inline-block">
|
) : (
|
||||||
<span className="relative z-10 text-accent italic inline-block">{chunks}</span>
|
t.rich('title', {
|
||||||
<div
|
green: (chunks) => (
|
||||||
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"
|
<span className="relative inline-block">
|
||||||
style={{ animationDelay: '500ms' }}
|
<span className="relative z-10 text-accent italic inline-block">{chunks}</span>
|
||||||
>
|
<div
|
||||||
<Scribble variant="circle" />
|
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"
|
||||||
</div>
|
style={{ animationDelay: '500ms' }}
|
||||||
</span>
|
>
|
||||||
),
|
<Scribble variant="circle" />
|
||||||
})}
|
</div>
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
})
|
||||||
|
)}
|
||||||
</Heading>
|
</Heading>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-lg md:text-xl text-white leading-relaxed max-w-2xl mb-10 md:mb-12">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col sm:flex-row justify-center md:justify-start gap-4 md:gap-6">
|
<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"
|
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={() =>
|
onClick={() =>
|
||||||
trackEvent(AnalyticsEvents.BUTTON_CLICK, {
|
trackEvent(AnalyticsEvents.BUTTON_CLICK, {
|
||||||
label: t('cta'),
|
label: data?.ctaLabel || t('cta'),
|
||||||
location: 'home_hero_primary',
|
location: 'home_hero_primary',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{t('cta')}
|
{data?.ctaLabel || t('cta')}
|
||||||
<span className="transition-transform group-hover/btn:translate-x-1 ml-2">
|
<span className="transition-transform group-hover/btn:translate-x-1 ml-2">
|
||||||
→
|
→
|
||||||
</span>
|
</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"
|
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={() =>
|
onClick={() =>
|
||||||
trackEvent(AnalyticsEvents.BUTTON_CLICK, {
|
trackEvent(AnalyticsEvents.BUTTON_CLICK, {
|
||||||
label: t('exploreProducts'),
|
label: data?.secondaryCtaLabel || t('exploreProducts'),
|
||||||
location: 'home_hero_secondary',
|
location: 'home_hero_secondary',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{t('exploreProducts')}
|
{data?.secondaryCtaLabel || t('exploreProducts')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import Image from 'next/image';
|
|||||||
import { useTranslations, useLocale } from 'next-intl';
|
import { useTranslations, useLocale } from 'next-intl';
|
||||||
import { Section, Container, Button, Heading } from '../../components/ui';
|
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 t = useTranslations('Home.meetTheTeam');
|
||||||
const teamT = useTranslations('Team');
|
const teamT = useTranslations('Team');
|
||||||
const locale = useLocale();
|
const locale = useLocale();
|
||||||
@@ -13,7 +13,7 @@ export default function MeetTheTeam() {
|
|||||||
<div className="absolute inset-0 z-0">
|
<div className="absolute inset-0 z-0">
|
||||||
<Image
|
<Image
|
||||||
src="/uploads/2024/12/DSC08036-Large.webp"
|
src="/uploads/2024/12/DSC08036-Large.webp"
|
||||||
alt={t('subtitle')}
|
alt={data?.subtitle || t('subtitle')}
|
||||||
fill
|
fill
|
||||||
className="object-cover scale-105 animate-slow-zoom"
|
className="object-cover scale-105 animate-slow-zoom"
|
||||||
sizes="100vw"
|
sizes="100vw"
|
||||||
@@ -24,20 +24,20 @@ export default function MeetTheTeam() {
|
|||||||
|
|
||||||
<Container className="relative z-10">
|
<Container className="relative z-10">
|
||||||
<div className="max-w-3xl text-white animate-slide-up">
|
<div className="max-w-3xl text-white animate-slide-up">
|
||||||
<Heading level={2} subtitle={t('subtitle')} className="text-white mb-8">
|
<Heading level={2} subtitle={data?.subtitle || t('subtitle')} className="text-white mb-8">
|
||||||
<span className="text-white">{t('title')}</span>
|
<span className="text-white">{data?.title || t('title')}</span>
|
||||||
</Heading>
|
</Heading>
|
||||||
|
|
||||||
<div className="relative mb-12">
|
<div className="relative mb-12">
|
||||||
<div className="absolute -left-8 top-0 bottom-0 w-1 bg-accent rounded-full" />
|
<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">
|
<p className="text-xl md:text-2xl leading-relaxed font-medium italic text-white/90 pl-8">
|
||||||
"{t('description')}"
|
"{data?.description || t('description')}"
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-wrap gap-8 items-center">
|
<div className="flex flex-wrap gap-8 items-center">
|
||||||
<Button href={`/${locale}/team`} variant="accent" size="xl" className="group">
|
<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>
|
<span className="ml-3 transition-transform group-hover:translate-x-2">→</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
@@ -61,7 +61,7 @@ export default function MeetTheTeam() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-white/60 font-bold text-xs md:text-sm uppercase tracking-widest">
|
<span className="text-white/60 font-bold text-xs md:text-sm uppercase tracking-widest">
|
||||||
{t('andNetwork')}
|
{data?.networkLabel || t('andNetwork')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import Image from 'next/image';
|
|||||||
import { useTranslations, useLocale } from 'next-intl';
|
import { useTranslations, useLocale } from 'next-intl';
|
||||||
import { Section } from '../../components/ui';
|
import { Section } from '../../components/ui';
|
||||||
|
|
||||||
export default function ProductCategories() {
|
export default function ProductCategories({ data }: { data?: any }) {
|
||||||
const t = useTranslations('Products');
|
const t = useTranslations('Products');
|
||||||
const locale = useLocale();
|
const locale = useLocale();
|
||||||
|
|
||||||
@@ -43,9 +43,13 @@ export default function ProductCategories() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Section className="bg-neutral-light py-0 md:py-0 lg:py-0 -mt-px">
|
<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">
|
<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>
|
</h2>
|
||||||
)}
|
)}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4">
|
<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 {
|
interface RecentPostsProps {
|
||||||
locale: string;
|
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 t = await getTranslations('Blog');
|
||||||
const posts = await getAllPosts(locale);
|
const posts = await getAllPosts(locale);
|
||||||
const recentPosts = posts.slice(0, 3);
|
const recentPosts = posts.slice(0, 3);
|
||||||
|
|
||||||
if (recentPosts.length === 0) return null;
|
if (recentPosts.length === 0) return null;
|
||||||
|
|
||||||
|
const title = data?.title || t('allArticles');
|
||||||
|
const subtitle = data?.subtitle || t('latestNews');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Section className="bg-neutral py-16 md:py-24">
|
<Section className="bg-neutral py-16 md:py-24">
|
||||||
<Container>
|
<Container>
|
||||||
<div className="flex flex-col md:flex-row items-start md:items-end justify-between mb-12 md:mb-16 gap-6">
|
<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">
|
<Heading level={2} subtitle={subtitle} className="mb-0 text-primary">
|
||||||
{t('allArticles')}
|
{title}
|
||||||
</Heading>
|
</Heading>
|
||||||
<Link
|
<Link
|
||||||
href={`/${locale}/blog`}
|
href={`/${locale}/blog`}
|
||||||
className="group flex items-center text-primary font-bold text-base md:text-lg touch-target"
|
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>
|
<span className="ml-2 transition-transform group-hover:translate-x-2">→</span>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul className="grid grid-cols-1 md:grid-cols-3 gap-8 md:gap-10 list-none p-0 m-0">
|
<ul className="grid grid-cols-1 md:grid-cols-3 gap-8 md:gap-10 list-none p-0 m-0">
|
||||||
{recentPosts.map((post) => (
|
{recentPosts.map((post, idx) => (
|
||||||
<li key={post.slug}>
|
<li key={`${post.slug}-${idx}`}>
|
||||||
<Link href={`/${locale}/blog/${post.slug}`} className="group block h-full">
|
<Link href={`/${locale}/blog/${post.slug}`} className="group block h-full">
|
||||||
<Card
|
<Card
|
||||||
tag="article"
|
tag="article"
|
||||||
@@ -47,6 +51,9 @@ export default async function RecentPosts({ locale }: RecentPostsProps) {
|
|||||||
alt={post.frontmatter.title}
|
alt={post.frontmatter.title}
|
||||||
fill
|
fill
|
||||||
className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-110"
|
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"
|
sizes="(max-width: 768px) 100vw, 33vw"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import React, { useState, useEffect, useRef } from 'react';
|
|||||||
import Scribble from '@/components/Scribble';
|
import Scribble from '@/components/Scribble';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
|
|
||||||
export default function VideoSection() {
|
export default function VideoSection({ data }: { data?: any }) {
|
||||||
const t = useTranslations('Home.video');
|
const t = useTranslations('Home.video');
|
||||||
const [isVisible, setIsVisible] = useState(false);
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
const sectionRef = useRef<HTMLElement>(null);
|
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="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">
|
<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]">
|
<h2 className="text-3xl md:text-4xl lg:text-5xl font-extrabold text-white leading-[1.1]">
|
||||||
{t.rich('title', {
|
{data?.title ? (
|
||||||
future: (chunks) => (
|
<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>') }} />
|
||||||
<span className="relative inline-block mx-2">
|
) : (
|
||||||
<span className="relative z-10 italic text-accent">{chunks}</span>
|
t.rich('title', {
|
||||||
<Scribble
|
future: (chunks) => (
|
||||||
variant="underline"
|
<span className="relative inline-block mx-2">
|
||||||
className="w-full h-4 -bottom-2 left-0 text-accent/40"
|
<span className="relative z-10 italic text-accent">{chunks}</span>
|
||||||
/>
|
<Scribble
|
||||||
</span>
|
variant="underline"
|
||||||
),
|
className="w-full h-4 -bottom-2 left-0 text-accent/40"
|
||||||
})}
|
/>
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
})
|
||||||
|
)}
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,30 +2,35 @@ import React from 'react';
|
|||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
import { Section, Container, Heading } from '../../components/ui';
|
import { Section, Container, Heading } from '../../components/ui';
|
||||||
|
|
||||||
export default function WhatWeDo() {
|
export default function WhatWeDo({ data }: { data?: any }) {
|
||||||
const t = useTranslations('Home.whatWeDo');
|
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 (
|
return (
|
||||||
<Section className="bg-white">
|
<Section className="bg-white">
|
||||||
<Container>
|
<Container>
|
||||||
<div className="sticky-narrative-container">
|
<div className="sticky-narrative-container">
|
||||||
<div className="sticky-narrative-sidebar">
|
<div className="sticky-narrative-sidebar">
|
||||||
<div className="lg:sticky lg:top-32">
|
<div className="lg:sticky lg:top-32">
|
||||||
<Heading level={2} subtitle={t('expertise')} className="text-primary-dark">
|
<Heading level={2} subtitle={data?.expertiseLabel || t('expertise')} className="text-primary-dark">
|
||||||
{t('title')}
|
{data?.title || t('title')}
|
||||||
</Heading>
|
</Heading>
|
||||||
<p className="text-base md:text-lg text-text-secondary leading-relaxed">
|
<p className="text-base md:text-lg text-text-secondary leading-relaxed">
|
||||||
{t('subtitle')}
|
{data?.subtitle || t('subtitle')}
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-8 md:mt-12 p-6 md:p-8 bg-saturated/10 rounded-2xl border border-saturated/10">
|
<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">
|
<p className="text-saturated font-bold text-base md:text-base italic">
|
||||||
"{t('quote')}"
|
"{data?.quote || t('quote')}"
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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">
|
<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 key={idx} className="group">
|
||||||
<div className="flex items-center gap-4 mb-4 md:mb-6">
|
<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">
|
<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>
|
</span>
|
||||||
<div className="h-px flex-grow bg-neutral-medium" />
|
<div className="h-px flex-grow bg-neutral-medium" />
|
||||||
</div>
|
</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>
|
<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">{t(`items.${idx}.description`)}</p>
|
<p className="text-text-secondary text-base md:text-base leading-relaxed">{item.description}</p>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,24 +2,27 @@ import React from 'react';
|
|||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
import { Section, Container, Heading } from '../../components/ui';
|
import { Section, Container, Heading } from '../../components/ui';
|
||||||
|
|
||||||
export default function WhyChooseUs() {
|
export default function WhyChooseUs({ data }: { data?: any }) {
|
||||||
const t = useTranslations('Home.whyChooseUs');
|
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 (
|
return (
|
||||||
<Section className="bg-neutral-light">
|
<Section className="bg-neutral-light">
|
||||||
<Container>
|
<Container>
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-16 lg:gap-24">
|
<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="lg:col-span-4 order-1 lg:order-2">
|
||||||
<div className="sticky top-32">
|
<div className="sticky top-32">
|
||||||
<Heading level={2} subtitle={t('whyKlz')} className="text-primary-dark">
|
<Heading level={2} subtitle={data?.tagline || t('whyKlz')} className="text-primary-dark">
|
||||||
{t('title')}
|
{data?.title || t('title')}
|
||||||
</Heading>
|
</Heading>
|
||||||
<p className="text-base md:text-lg text-text-secondary leading-relaxed">
|
<p className="text-base md:text-lg text-text-secondary leading-relaxed">
|
||||||
{t('subtitle')}
|
{data?.subtitle || t('subtitle')}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<ul className="mt-12 space-y-6 list-none p-0">
|
<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">
|
<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">
|
<div className="flex-shrink-0 w-6 h-6 rounded-full bg-accent flex items-center justify-center">
|
||||||
<svg
|
<svg
|
||||||
@@ -38,7 +41,7 @@ export default function WhyChooseUs() {
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<span className="font-bold text-primary-dark text-base md:text-base">
|
<span className="font-bold text-primary-dark text-base md:text-base">
|
||||||
{t(`features.${i}`)}
|
{featureText}
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
@@ -46,7 +49,7 @@ export default function WhyChooseUs() {
|
|||||||
</div>
|
</div>
|
||||||
</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">
|
<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
|
<li
|
||||||
key={idx}
|
key={idx}
|
||||||
className="p-10 bg-white rounded-3xl border border-neutral-medium hover:border-accent transition-all duration-500 hover:shadow-xl group"
|
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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-xl font-bold mb-4 text-primary-dark">
|
<h3 className="text-xl font-bold mb-4 text-primary-dark">
|
||||||
{t(`items.${idx}.title`)}
|
{item.title}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-text-secondary text-base md:text-base leading-relaxed">
|
<p className="text-text-secondary text-base md:text-base leading-relaxed">
|
||||||
{t(`items.${idx}.description`)}
|
{item.description}
|
||||||
</p>
|
</p>
|
||||||
</li>
|
</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;
|
focalX?: number;
|
||||||
focalY?: number;
|
focalY?: number;
|
||||||
category?: string;
|
category?: string;
|
||||||
locale: string;
|
|
||||||
public?: boolean;
|
public?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,9 +64,9 @@ export async function getPostBySlug(slug: string, locale: string): Promise<PostM
|
|||||||
collection: 'posts',
|
collection: 'posts',
|
||||||
where: {
|
where: {
|
||||||
slug: { equals: slug },
|
slug: { equals: slug },
|
||||||
locale: { equals: locale },
|
|
||||||
...(!isDev ? { _status: { equals: 'published' } } : {}),
|
...(!isDev ? { _status: { equals: 'published' } } : {}),
|
||||||
},
|
},
|
||||||
|
locale: locale as any,
|
||||||
draft: isDev,
|
draft: isDev,
|
||||||
limit: 1,
|
limit: 1,
|
||||||
});
|
});
|
||||||
@@ -83,7 +82,6 @@ export async function getPostBySlug(slug: string, locale: string): Promise<PostM
|
|||||||
date: doc.date,
|
date: doc.date,
|
||||||
excerpt: doc.excerpt || '',
|
excerpt: doc.excerpt || '',
|
||||||
category: doc.category || '',
|
category: doc.category || '',
|
||||||
locale: doc.locale,
|
|
||||||
featuredImage:
|
featuredImage:
|
||||||
typeof doc.featuredImage === 'object' && doc.featuredImage !== null
|
typeof doc.featuredImage === 'object' && doc.featuredImage !== null
|
||||||
? doc.featuredImage.sizes?.card?.url || doc.featuredImage.url
|
? 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({
|
const { docs } = await payload.find({
|
||||||
collection: 'posts',
|
collection: 'posts',
|
||||||
where: {
|
where: {
|
||||||
locale: {
|
|
||||||
equals: locale,
|
|
||||||
},
|
|
||||||
...(!isDev ? { _status: { equals: 'published' } } : {}),
|
...(!isDev ? { _status: { equals: 'published' } } : {}),
|
||||||
},
|
},
|
||||||
|
locale: locale as any,
|
||||||
sort: '-date',
|
sort: '-date',
|
||||||
draft: isDev,
|
draft: isDev,
|
||||||
limit: 100,
|
limit: 100,
|
||||||
@@ -125,7 +121,7 @@ export async function getAllPosts(locale: string): Promise<PostMdx[]> {
|
|||||||
|
|
||||||
console.log(`[Payload] getAllPosts for ${locale}: Found ${docs.length} docs`);
|
console.log(`[Payload] getAllPosts for ${locale}: Found ${docs.length} docs`);
|
||||||
|
|
||||||
return docs.map((doc) => {
|
const posts = docs.map((doc) => {
|
||||||
return {
|
return {
|
||||||
slug: doc.slug,
|
slug: doc.slug,
|
||||||
frontmatter: {
|
frontmatter: {
|
||||||
@@ -133,7 +129,6 @@ export async function getAllPosts(locale: string): Promise<PostMdx[]> {
|
|||||||
date: doc.date,
|
date: doc.date,
|
||||||
excerpt: doc.excerpt || '',
|
excerpt: doc.excerpt || '',
|
||||||
category: doc.category || '',
|
category: doc.category || '',
|
||||||
locale: doc.locale,
|
|
||||||
featuredImage:
|
featuredImage:
|
||||||
typeof doc.featuredImage === 'object' && doc.featuredImage !== null
|
typeof doc.featuredImage === 'object' && doc.featuredImage !== null
|
||||||
? doc.featuredImage.sizes?.card?.url || doc.featuredImage.url
|
? 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,
|
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) {
|
} catch (error) {
|
||||||
console.error(`[Payload] getAllPosts failed for ${locale}:`, error);
|
console.error(`[Payload] getAllPosts failed for ${locale}:`, error);
|
||||||
return [];
|
return [];
|
||||||
|
|||||||
@@ -33,6 +33,17 @@ interface SendEmailOptions {
|
|||||||
|
|
||||||
export async function sendEmail({ to, replyTo, subject, html }: SendEmailOptions) {
|
export async function sendEmail({ to, replyTo, subject, html }: SendEmailOptions) {
|
||||||
const recipients = to || config.mail.recipients;
|
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 = {
|
const mailOptions = {
|
||||||
from: config.mail.from,
|
from: config.mail.from,
|
||||||
@@ -42,7 +53,6 @@ export async function sendEmail({ to, replyTo, subject, html }: SendEmailOptions
|
|||||||
html,
|
html,
|
||||||
};
|
};
|
||||||
|
|
||||||
const logger = getServerAppServices().logger.child({ component: 'mailer' });
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const info = await getTransporter().sendMail(mailOptions);
|
const info = await getTransporter().sendMail(mailOptions);
|
||||||
|
|||||||
113
lib/pages.ts
113
lib/pages.ts
@@ -5,7 +5,9 @@ export interface PageFrontmatter {
|
|||||||
title: string;
|
title: string;
|
||||||
excerpt: string;
|
excerpt: string;
|
||||||
featuredImage: string | null;
|
featuredImage: string | null;
|
||||||
locale: string;
|
focalX?: number;
|
||||||
|
focalY?: number;
|
||||||
|
layout?: 'default' | 'fullBleed';
|
||||||
public?: boolean;
|
public?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -15,6 +17,30 @@ export interface PageMdx {
|
|||||||
content: any; // Lexical AST Document
|
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> {
|
export async function getPageBySlug(slug: string, locale: string): Promise<PageMdx | null> {
|
||||||
try {
|
try {
|
||||||
const payload = await getPayload({ config: configPromise });
|
const payload = await getPayload({ config: configPromise });
|
||||||
@@ -23,30 +49,14 @@ export async function getPageBySlug(slug: string, locale: string): Promise<PageM
|
|||||||
collection: 'pages' as any,
|
collection: 'pages' as any,
|
||||||
where: {
|
where: {
|
||||||
slug: { equals: slug },
|
slug: { equals: slug },
|
||||||
locale: { equals: locale },
|
|
||||||
},
|
},
|
||||||
|
locale: locale as any,
|
||||||
limit: 1,
|
limit: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
const docs = result.docs as any[];
|
const docs = result.docs as any[];
|
||||||
|
|
||||||
if (!docs || docs.length === 0) return null;
|
if (!docs || docs.length === 0) return null;
|
||||||
|
return mapDoc(docs[0]);
|
||||||
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
|
|
||||||
};
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`[Payload] getPageBySlug failed for ${slug}:`, error);
|
console.error(`[Payload] getPageBySlug failed for ${slug}:`, error);
|
||||||
return null;
|
return null;
|
||||||
@@ -59,31 +69,11 @@ export async function getAllPages(locale: string): Promise<PageMdx[]> {
|
|||||||
|
|
||||||
const result = await payload.find({
|
const result = await payload.find({
|
||||||
collection: 'pages' as any,
|
collection: 'pages' as any,
|
||||||
where: {
|
locale: locale as any,
|
||||||
locale: {
|
|
||||||
equals: locale,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
limit: 100,
|
limit: 100,
|
||||||
});
|
});
|
||||||
|
|
||||||
const docs = result.docs as any[];
|
return (result.docs as any[]).map(mapDoc);
|
||||||
|
|
||||||
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,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`[Payload] getAllPages failed for ${locale}:`, error);
|
console.error(`[Payload] getAllPages failed for ${locale}:`, error);
|
||||||
return [];
|
return [];
|
||||||
@@ -96,30 +86,29 @@ export async function getAllPagesMetadata(locale: string): Promise<Partial<PageM
|
|||||||
|
|
||||||
const result = await payload.find({
|
const result = await payload.find({
|
||||||
collection: 'pages' as any,
|
collection: 'pages' as any,
|
||||||
where: {
|
locale: locale as any,
|
||||||
locale: {
|
|
||||||
equals: locale,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
limit: 100,
|
limit: 100,
|
||||||
});
|
});
|
||||||
|
|
||||||
const docs = result.docs as any[];
|
return (result.docs as any[]).map((doc: any) => ({
|
||||||
|
slug: doc.slug,
|
||||||
return docs.map((doc: any) => {
|
frontmatter: {
|
||||||
return {
|
title: doc.title,
|
||||||
slug: doc.slug,
|
excerpt: doc.excerpt || '',
|
||||||
frontmatter: {
|
featuredImage:
|
||||||
title: doc.title,
|
typeof doc.featuredImage === 'object' && doc.featuredImage !== null
|
||||||
excerpt: doc.excerpt || '',
|
? doc.featuredImage.sizes?.card?.url || doc.featuredImage.url
|
||||||
locale: doc.locale,
|
: null,
|
||||||
featuredImage:
|
focalX:
|
||||||
typeof doc.featuredImage === 'object' && doc.featuredImage !== null
|
typeof doc.featuredImage === 'object' && doc.featuredImage !== null
|
||||||
? doc.featuredImage.sizes?.card?.url || doc.featuredImage.url
|
? doc.featuredImage.focalX
|
||||||
: null,
|
: 50,
|
||||||
} as PageFrontmatter,
|
focalY:
|
||||||
};
|
typeof doc.featuredImage === 'object' && doc.featuredImage !== null
|
||||||
});
|
? doc.featuredImage.focalY
|
||||||
|
: 50,
|
||||||
|
} as PageFrontmatter,
|
||||||
|
}));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`[Payload] getAllPagesMetadata failed for ${locale}:`, error);
|
console.error(`[Payload] getAllPagesMetadata failed for ${locale}:`, error);
|
||||||
return [];
|
return [];
|
||||||
|
|||||||
@@ -8,11 +8,12 @@ export interface ProductFrontmatter {
|
|||||||
description: string;
|
description: string;
|
||||||
categories: string[];
|
categories: string[];
|
||||||
images: string[];
|
images: string[];
|
||||||
locale: string;
|
focalX?: number;
|
||||||
|
focalY?: number;
|
||||||
isFallback?: boolean;
|
isFallback?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProductMdx {
|
export interface ProductData {
|
||||||
slug: string;
|
slug: string;
|
||||||
frontmatter: ProductFrontmatter;
|
frontmatter: ProductFrontmatter;
|
||||||
content: any; // Lexical AST from Payload
|
content: any; // Lexical AST from Payload
|
||||||
@@ -21,36 +22,24 @@ export interface ProductMdx {
|
|||||||
export async function getProductMetadata(
|
export async function getProductMetadata(
|
||||||
slug: string,
|
slug: string,
|
||||||
locale: string,
|
locale: string,
|
||||||
): Promise<Partial<ProductMdx> | null> {
|
): Promise<Partial<ProductData> | null> {
|
||||||
const payload = await getPayload({ config: configPromise });
|
const payload = await getPayload({ config: configPromise });
|
||||||
const fileSlug = await mapSlugToFileSlug(slug, locale);
|
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',
|
collection: 'products',
|
||||||
where: {
|
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,
|
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) {
|
if (result.docs.length > 0) {
|
||||||
const doc = result.docs[0];
|
const doc = result.docs[0];
|
||||||
|
|
||||||
@@ -69,8 +58,6 @@ export async function getProductMetadata(
|
|||||||
description: doc.description,
|
description: doc.description,
|
||||||
categories: Array.isArray(doc.categories) ? doc.categories.map((c: any) => c.category) : [],
|
categories: Array.isArray(doc.categories) ? doc.categories.map((c: any) => c.category) : [],
|
||||||
images: resolvedImages,
|
images: resolvedImages,
|
||||||
locale: doc.locale,
|
|
||||||
isFallback,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -78,37 +65,25 @@ export async function getProductMetadata(
|
|||||||
return null;
|
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 {
|
try {
|
||||||
const payload = await getPayload({ config: configPromise });
|
const payload = await getPayload({ config: configPromise });
|
||||||
const fileSlug = await mapSlugToFileSlug(slug, locale);
|
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',
|
collection: 'products',
|
||||||
where: {
|
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,
|
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) {
|
if (result.docs.length > 0) {
|
||||||
const doc = result.docs[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)
|
? doc.categories.map((c: any) => c.category)
|
||||||
: [],
|
: [],
|
||||||
images: resolvedImages,
|
images: resolvedImages,
|
||||||
locale: doc.locale,
|
focalX:
|
||||||
isFallback,
|
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[]> {
|
export async function getAllProductSlugs(locale: string): Promise<string[]> {
|
||||||
try {
|
try {
|
||||||
const payload = await getPayload({ config: configPromise });
|
const payload = await getPayload({ config: configPromise });
|
||||||
|
const isDev = process.env.NODE_ENV === 'development';
|
||||||
const result = await payload.find({
|
const result = await payload.find({
|
||||||
collection: 'products',
|
collection: 'products',
|
||||||
where: {
|
where: {
|
||||||
locale: {
|
...(!isDev ? { _status: { equals: 'published' } } : {}),
|
||||||
equals: locale,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
pagination: false, // get all docs
|
locale: locale as any,
|
||||||
|
pagination: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
return result.docs.map((doc) => doc.slug);
|
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 {
|
try {
|
||||||
const payload = await getPayload({ config: configPromise });
|
const payload = await getPayload({ config: configPromise });
|
||||||
|
|
||||||
@@ -174,13 +155,15 @@ export async function getAllProducts(locale: string): Promise<ProductMdx[]> {
|
|||||||
description: true,
|
description: true,
|
||||||
categories: true,
|
categories: true,
|
||||||
images: true,
|
images: true,
|
||||||
locale: true,
|
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// Get products for this locale
|
const isDev = process.env.NODE_ENV === 'development';
|
||||||
const result = await payload.find({
|
const result = await payload.find({
|
||||||
collection: 'products',
|
collection: 'products',
|
||||||
where: { locale: { equals: locale } },
|
where: {
|
||||||
|
...(!isDev ? { _status: { equals: 'published' } } : {}),
|
||||||
|
},
|
||||||
|
locale: locale as any,
|
||||||
depth: 1,
|
depth: 1,
|
||||||
pagination: false,
|
pagination: false,
|
||||||
select: selectFields,
|
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`);
|
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[]) || [])
|
const resolvedImages = ((doc.images as any[]) || [])
|
||||||
.map((img) => (typeof img === 'string' ? img : img.url))
|
.map((img) => (typeof img === 'string' ? img : img.url))
|
||||||
.filter(Boolean) as string[];
|
.filter(Boolean) as string[];
|
||||||
@@ -205,55 +188,21 @@ export async function getAllProducts(locale: string): Promise<ProductMdx[]> {
|
|||||||
description: doc.description ? String(doc.description) : '',
|
description: doc.description ? String(doc.description) : '',
|
||||||
categories: plainCategories,
|
categories: plainCategories,
|
||||||
images: resolvedImages,
|
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,
|
content: null,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// Also include English fallbacks for slugs not in this locale
|
// Filter out products with 0 images (data integrity check to prevent 404s)
|
||||||
if (locale !== 'en') {
|
products = products.filter((p) => p.frontmatter.images && p.frontmatter.images.length > 0);
|
||||||
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];
|
|
||||||
}
|
|
||||||
|
|
||||||
return products;
|
return products;
|
||||||
} catch (error) {
|
} 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);
|
const products = await getAllProducts(locale);
|
||||||
return products.map((p) => ({
|
return products.map((p) => ({
|
||||||
slug: p.slug,
|
slug: p.slug,
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"Slugs": {
|
"Slugs": {
|
||||||
"pages": {
|
"pages": {
|
||||||
"legal-notice": "legal-notice",
|
"legal-notice": "impressum",
|
||||||
"privacy-policy": "privacy-policy",
|
"privacy-policy": "datenschutz",
|
||||||
"terms": "terms",
|
"terms": "agbs",
|
||||||
"contact": "contact",
|
"contact": "contact",
|
||||||
"team": "team",
|
"team": "team",
|
||||||
"blog": "blog",
|
"blog": "blog",
|
||||||
@@ -396,4 +396,4 @@
|
|||||||
"cta": "Back to Safety"
|
"cta": "Back to Safety"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -361,23 +361,23 @@ const nextConfig = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
source: '/products/solar-cables/h1z2z2-k',
|
source: '/products/solar-cables/h1z2z2-k',
|
||||||
destination: '/en/products/h1z2z2-k',
|
destination: '/en/products/solar-cables/h1z2z2-k',
|
||||||
permanent: true,
|
permanent: true,
|
||||||
},
|
},
|
||||||
// Product redirects (German)
|
// Product redirects (German)
|
||||||
{
|
{
|
||||||
source: '/de/produkte/solarkabel/h1z2z2-k',
|
source: '/de/produkte/stromkabel/solarkabel/h1z2z2-k',
|
||||||
destination: '/de/produkte/h1z2z2-k',
|
destination: '/de/produkte/solarkabel/h1z2z2-k',
|
||||||
permanent: true,
|
permanent: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
source: '/de/produkte/stromkabel/niederspannungskabel/naycwy-2',
|
source: '/de/produkte/stromkabel/niederspannungskabel/naycwy-2',
|
||||||
destination: '/de/produkte/naycwy',
|
destination: '/de/produkte/niederspannungskabel/naycwy',
|
||||||
permanent: true,
|
permanent: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
source: '/de/produkte/stromkabel/niederspannungskabel/ny2y-2',
|
source: '/de/produkte/stromkabel/niederspannungskabel/ny2y-2',
|
||||||
destination: '/de/produkte/ny2y',
|
destination: '/de/produkte/niederspannungskabel/ny2y',
|
||||||
permanent: true,
|
permanent: true,
|
||||||
},
|
},
|
||||||
// VCF redirects
|
// VCF redirects
|
||||||
|
|||||||
@@ -91,7 +91,7 @@
|
|||||||
"vitest": "^4.0.16"
|
"vitest": "^4.0.16"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"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: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",
|
"dev:infra": "COMPOSE_PROJECT_NAME=klz-2026 docker-compose -f docker-compose.dev.yml up -d klz-db klz-proxy",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
@@ -126,8 +126,10 @@
|
|||||||
"backup:db": "bash ./scripts/backup-db.sh",
|
"backup:db": "bash ./scripts/backup-db.sh",
|
||||||
"restore:db": "bash ./scripts/restore-db.sh",
|
"restore:db": "bash ./scripts/restore-db.sh",
|
||||||
"cms:push:testing": "bash ./scripts/cms-sync.sh push testing",
|
"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:push:prod": "bash ./scripts/cms-sync.sh push prod",
|
||||||
"cms:pull:testing": "bash ./scripts/cms-sync.sh pull testing",
|
"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",
|
"cms:pull:prod": "bash ./scripts/cms-sync.sh pull prod",
|
||||||
"prepare": "husky",
|
"prepare": "husky",
|
||||||
"preinstall": "npx only-allow pnpm"
|
"preinstall": "npx only-allow pnpm"
|
||||||
@@ -154,4 +156,4 @@
|
|||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"lucide-react": "^0.563.0"
|
"lucide-react": "^0.563.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
263
payload-types.ts
263
payload-types.ts
@@ -94,10 +94,10 @@ export interface Config {
|
|||||||
db: {
|
db: {
|
||||||
defaultIDType: number;
|
defaultIDType: number;
|
||||||
};
|
};
|
||||||
fallbackLocale: null;
|
fallbackLocale: ('false' | 'none' | 'null') | false | null | ('de' | 'en') | ('de' | 'en')[];
|
||||||
globals: {};
|
globals: {};
|
||||||
globalsSelect: {};
|
globalsSelect: {};
|
||||||
locale: null;
|
locale: 'de' | 'en';
|
||||||
user: User;
|
user: User;
|
||||||
jobs: {
|
jobs: {
|
||||||
tasks: unknown;
|
tasks: unknown;
|
||||||
@@ -200,6 +200,9 @@ export interface Media {
|
|||||||
export interface Post {
|
export interface Post {
|
||||||
id: number;
|
id: number;
|
||||||
title: string;
|
title: string;
|
||||||
|
/**
|
||||||
|
* Unique slug per locale (e.g. same slug can exist in DE and EN).
|
||||||
|
*/
|
||||||
slug: string;
|
slug: string;
|
||||||
/**
|
/**
|
||||||
* A short summary for blog feed cards and SEO.
|
* 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.
|
* The primary Hero image used for headers and OpenGraph previews.
|
||||||
*/
|
*/
|
||||||
featuredImage?: (number | null) | Media;
|
featuredImage?: (number | null) | Media;
|
||||||
locale: 'en' | 'de';
|
|
||||||
/**
|
/**
|
||||||
* Used for tag bucketing (e.g. "Kabel Technologie").
|
* Used for tag bucketing (e.g. "Kabel Technologie").
|
||||||
*/
|
*/
|
||||||
@@ -266,7 +268,6 @@ export interface Product {
|
|||||||
sku: string;
|
sku: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
description: string;
|
description: string;
|
||||||
locale: 'en' | 'de';
|
|
||||||
categories: {
|
categories: {
|
||||||
category?: string | null;
|
category?: string | null;
|
||||||
id?: string | null;
|
id?: string | null;
|
||||||
@@ -317,8 +318,14 @@ export interface Product {
|
|||||||
export interface Page {
|
export interface Page {
|
||||||
id: number;
|
id: number;
|
||||||
title: string;
|
title: string;
|
||||||
|
/**
|
||||||
|
* The URL slug for this locale (e.g. "impressum" for DE, "imprint" for EN).
|
||||||
|
*/
|
||||||
slug: string;
|
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;
|
excerpt?: string | null;
|
||||||
featuredImage?: (number | null) | Media;
|
featuredImage?: (number | null) | Media;
|
||||||
content: {
|
content: {
|
||||||
@@ -338,6 +345,7 @@ export interface Page {
|
|||||||
};
|
};
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
|
_status?: ('draft' | 'published') | null;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* This interface was referenced by `Config`'s JSON-Schema
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
@@ -514,7 +522,6 @@ export interface PostsSelect<T extends boolean = true> {
|
|||||||
excerpt?: T;
|
excerpt?: T;
|
||||||
date?: T;
|
date?: T;
|
||||||
featuredImage?: T;
|
featuredImage?: T;
|
||||||
locale?: T;
|
|
||||||
category?: T;
|
category?: T;
|
||||||
content?: T;
|
content?: T;
|
||||||
updatedAt?: T;
|
updatedAt?: T;
|
||||||
@@ -543,7 +550,6 @@ export interface ProductsSelect<T extends boolean = true> {
|
|||||||
sku?: T;
|
sku?: T;
|
||||||
slug?: T;
|
slug?: T;
|
||||||
description?: T;
|
description?: T;
|
||||||
locale?: T;
|
|
||||||
categories?:
|
categories?:
|
||||||
| T
|
| T
|
||||||
| {
|
| {
|
||||||
@@ -565,12 +571,13 @@ export interface ProductsSelect<T extends boolean = true> {
|
|||||||
export interface PagesSelect<T extends boolean = true> {
|
export interface PagesSelect<T extends boolean = true> {
|
||||||
title?: T;
|
title?: T;
|
||||||
slug?: T;
|
slug?: T;
|
||||||
locale?: T;
|
layout?: T;
|
||||||
excerpt?: T;
|
excerpt?: T;
|
||||||
featuredImage?: T;
|
featuredImage?: T;
|
||||||
content?: T;
|
content?: T;
|
||||||
updatedAt?: T;
|
updatedAt?: T;
|
||||||
createdAt?: T;
|
createdAt?: T;
|
||||||
|
_status?: T;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* This interface was referenced by `Config`'s JSON-Schema
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
@@ -702,6 +709,246 @@ export interface ProductTabsBlock {
|
|||||||
blockName?: string | null;
|
blockName?: string | null;
|
||||||
blockType: 'productTabs';
|
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
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
* via the `definition` "auth".
|
* via the `definition` "auth".
|
||||||
|
|||||||
@@ -37,6 +37,26 @@ export default buildConfig({
|
|||||||
importMap: {
|
importMap: {
|
||||||
baseDir: path.resolve(dirname),
|
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],
|
collections: [Users, Media, Posts, FormSubmissions, Products, Pages],
|
||||||
editor: lexicalEditor({
|
editor: lexicalEditor({
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ async function main() {
|
|||||||
console.log(`\n🕷️ Launching Puppeteer Headless Engine...`);
|
console.log(`\n🕷️ Launching Puppeteer Headless Engine...`);
|
||||||
const browser = await puppeteer.launch({
|
const browser = await puppeteer.launch({
|
||||||
headless: true,
|
headless: true,
|
||||||
|
executablePath: process.env.PUPPETEER_EXECUTABLE_PATH || process.env.CHROME_PATH || undefined,
|
||||||
args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage'],
|
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 }> = [];
|
const consoleErrorsList: Array<{ type: string; error: string; page: string }> = [];
|
||||||
|
|
||||||
// Listen for unhandled exceptions natively in the page
|
// Listen for unhandled exceptions natively in the page
|
||||||
page.on('pageerror', (err) => {
|
page.on('pageerror', (err: any) => {
|
||||||
consoleErrorsList.push({
|
consoleErrorsList.push({
|
||||||
type: 'PAGE_ERROR',
|
type: 'PAGE_ERROR',
|
||||||
error: err.message,
|
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)
|
// Listen for console.error and console.warn messages (like Next.js Image warnings, hydration errors, CSP blocks)
|
||||||
page.on('console', (msg) => {
|
page.on('console', (msg) => {
|
||||||
const type = msg.type();
|
const type = msg.type();
|
||||||
if (type === 'error' || type === 'warning') {
|
if (type === 'error' || type === 'warn') {
|
||||||
const text = msg.text();
|
const text = msg.text();
|
||||||
|
|
||||||
// Exclude common browser extension noise or third party tracker warnings
|
// 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
|
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
|
# Load environment variables
|
||||||
if [ -f .env ]; then
|
if [ -f .env ]; then
|
||||||
set -a; source .env; set +a
|
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_MEDIA_VOLUME="/var/lib/docker/volumes/klz-testing_klz_media_data/_data"
|
||||||
REMOTE_SITE_DIR="/home/deploy/sites/testing.klz-cables.com"
|
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)
|
prod|production)
|
||||||
REMOTE_PROJECT="klz-cablescom"
|
REMOTE_PROJECT="klz-cablescom"
|
||||||
REMOTE_DB_CONTAINER="klz-cablescom-klz-db-1"
|
REMOTE_DB_CONTAINER="klz-cablescom-klz-db-1"
|
||||||
@@ -57,7 +86,7 @@ resolve_target() {
|
|||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
echo "❌ Unknown target: $TARGET"
|
echo "❌ Unknown target: $TARGET"
|
||||||
echo " Valid targets: testing, prod"
|
echo " Valid targets: testing, staging, prod"
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
@@ -118,6 +147,7 @@ backup_local_db() {
|
|||||||
echo "📦 Creating safety backup of local DB → $file"
|
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"
|
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))"
|
echo "✅ Backup: $file ($(du -h "$file" | cut -f1))"
|
||||||
|
LOCAL_BACKUP_FILE="$file"
|
||||||
}
|
}
|
||||||
|
|
||||||
backup_remote_db() {
|
backup_remote_db() {
|
||||||
@@ -125,6 +155,7 @@ backup_remote_db() {
|
|||||||
echo "📦 Creating safety backup of $TARGET DB → $SSH_HOST:$file"
|
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"
|
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"
|
echo "✅ Remote backup: $file"
|
||||||
|
REMOTE_BACKUP_FILE="$file"
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── PUSH: local → remote ──────────────────────────────────────────────────
|
# ── PUSH: local → remote ──────────────────────────────────────────────────
|
||||||
@@ -177,6 +208,7 @@ do_push() {
|
|||||||
rm -f "$dump"
|
rm -f "$dump"
|
||||||
ssh "$SSH_HOST" "rm -f /tmp/payload_push.sql.gz"
|
ssh "$SSH_HOST" "rm -f /tmp/payload_push.sql.gz"
|
||||||
|
|
||||||
|
SYNC_SUCCESS="true"
|
||||||
echo ""
|
echo ""
|
||||||
echo "✅ Push to $TARGET complete!"
|
echo "✅ Push to $TARGET complete!"
|
||||||
}
|
}
|
||||||
@@ -214,12 +246,13 @@ do_pull() {
|
|||||||
# 4. Sync media
|
# 4. Sync media
|
||||||
echo "🖼️ Syncing media files..."
|
echo "🖼️ Syncing media files..."
|
||||||
mkdir -p "$LOCAL_MEDIA_DIR"
|
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
|
# Cleanup
|
||||||
rm -f "/tmp/payload_pull.sql.gz"
|
rm -f "/tmp/payload_pull.sql.gz"
|
||||||
ssh "$SSH_HOST" "rm -f /tmp/payload_pull.sql.gz"
|
ssh "$SSH_HOST" "rm -f /tmp/payload_pull.sql.gz"
|
||||||
|
|
||||||
|
SYNC_SUCCESS="true"
|
||||||
echo ""
|
echo ""
|
||||||
echo "✅ Pull from $TARGET complete! Restart dev server to see changes."
|
echo "✅ Pull from $TARGET complete! Restart dev server to see changes."
|
||||||
}
|
}
|
||||||
@@ -230,8 +263,10 @@ if [ -z "$DIRECTION" ] || [ -z "$TARGET" ]; then
|
|||||||
echo ""
|
echo ""
|
||||||
echo "Usage:"
|
echo "Usage:"
|
||||||
echo " pnpm cms:push:testing Push local DB + media → testing"
|
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:push:prod Push local DB + media → production"
|
||||||
echo " pnpm cms:pull:testing Pull testing DB + media → local"
|
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 " pnpm cms:pull:prod Pull production DB + media → local"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Safety: A backup is always created before overwriting."
|
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
|
// Using a more robust way to execute and capture output
|
||||||
// We remove 'npx lhci upload' to keep everything local and avoid Google-hosted reports
|
// 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...`);
|
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_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_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_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 = [
|
export const migrations = [
|
||||||
{
|
{
|
||||||
@@ -18,4 +19,9 @@ export const migrations = [
|
|||||||
down: migration_20260225_003500_add_pages_collection.down,
|
down: migration_20260225_003500_add_pages_collection.down,
|
||||||
name: '20260225_003500_add_pages_collection',
|
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,
|
pgEnum,
|
||||||
} from '@payloadcms/db-postgres/drizzle/pg-core';
|
} from '@payloadcms/db-postgres/drizzle/pg-core';
|
||||||
import { sql, relations } from '@payloadcms/db-postgres/drizzle';
|
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_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', [
|
export const enum__posts_v_version_status = pgEnum('enum__posts_v_version_status', [
|
||||||
'draft',
|
'draft',
|
||||||
'published',
|
'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', [
|
export const enum_form_submissions_type = pgEnum('enum_form_submissions_type', [
|
||||||
'contact',
|
'contact',
|
||||||
'product_quote',
|
'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_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', [
|
export const enum__products_v_version_status = pgEnum('enum__products_v_version_status', [
|
||||||
'draft',
|
'draft',
|
||||||
'published',
|
'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(
|
export const users_sessions = pgTable(
|
||||||
'users_sessions',
|
'users_sessions',
|
||||||
@@ -130,18 +146,12 @@ export const media = pgTable(
|
|||||||
sizes_card_mimeType: varchar('sizes_card_mime_type'),
|
sizes_card_mimeType: varchar('sizes_card_mime_type'),
|
||||||
sizes_card_filesize: numeric('sizes_card_filesize', { mode: 'number' }),
|
sizes_card_filesize: numeric('sizes_card_filesize', { mode: 'number' }),
|
||||||
sizes_card_filename: varchar('sizes_card_filename'),
|
sizes_card_filename: varchar('sizes_card_filename'),
|
||||||
sizes_hero_url: varchar('sizes_hero_url'),
|
sizes_tablet_url: varchar('sizes_tablet_url'),
|
||||||
sizes_hero_width: numeric('sizes_hero_width', { mode: 'number' }),
|
sizes_tablet_width: numeric('sizes_tablet_width', { mode: 'number' }),
|
||||||
sizes_hero_height: numeric('sizes_hero_height', { mode: 'number' }),
|
sizes_tablet_height: numeric('sizes_tablet_height', { mode: 'number' }),
|
||||||
sizes_hero_mimeType: varchar('sizes_hero_mime_type'),
|
sizes_tablet_mimeType: varchar('sizes_tablet_mime_type'),
|
||||||
sizes_hero_filesize: numeric('sizes_hero_filesize', { mode: 'number' }),
|
sizes_tablet_filesize: numeric('sizes_tablet_filesize', { mode: 'number' }),
|
||||||
sizes_hero_filename: varchar('sizes_hero_filename'),
|
sizes_tablet_filename: varchar('sizes_tablet_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'),
|
|
||||||
},
|
},
|
||||||
(columns) => [
|
(columns) => [
|
||||||
index('media_updated_at_idx').on(columns.updatedAt),
|
index('media_updated_at_idx').on(columns.updatedAt),
|
||||||
@@ -151,10 +161,7 @@ export const media = pgTable(
|
|||||||
columns.sizes_thumbnail_filename,
|
columns.sizes_thumbnail_filename,
|
||||||
),
|
),
|
||||||
index('media_sizes_card_sizes_card_filename_idx').on(columns.sizes_card_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_tablet_sizes_tablet_filename_idx').on(columns.sizes_tablet_filename),
|
||||||
index('media_sizes_hero_mobile_sizes_hero_mobile_filename_idx').on(
|
|
||||||
columns.sizes_hero_mobile_filename,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -162,16 +169,10 @@ export const posts = pgTable(
|
|||||||
'posts',
|
'posts',
|
||||||
{
|
{
|
||||||
id: serial('id').primaryKey(),
|
id: serial('id').primaryKey(),
|
||||||
title: varchar('title'),
|
|
||||||
slug: varchar('slug'),
|
|
||||||
excerpt: varchar('excerpt'),
|
|
||||||
date: timestamp('date', { mode: 'string', withTimezone: true, precision: 3 }),
|
date: timestamp('date', { mode: 'string', withTimezone: true, precision: 3 }),
|
||||||
featuredImage: integer('featured_image_id').references(() => media.id, {
|
featuredImage: integer('featured_image_id').references(() => media.id, {
|
||||||
onDelete: 'set null',
|
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 })
|
updatedAt: timestamp('updated_at', { mode: 'string', withTimezone: true, precision: 3 })
|
||||||
.defaultNow()
|
.defaultNow()
|
||||||
.notNull(),
|
.notNull(),
|
||||||
@@ -181,7 +182,6 @@ export const posts = pgTable(
|
|||||||
_status: enum_posts_status('_status').default('draft'),
|
_status: enum_posts_status('_status').default('draft'),
|
||||||
},
|
},
|
||||||
(columns) => [
|
(columns) => [
|
||||||
uniqueIndex('posts_slug_idx').on(columns.slug),
|
|
||||||
index('posts_featured_image_idx').on(columns.featuredImage),
|
index('posts_featured_image_idx').on(columns.featuredImage),
|
||||||
index('posts_updated_at_idx').on(columns.updatedAt),
|
index('posts_updated_at_idx').on(columns.updatedAt),
|
||||||
index('posts_created_at_idx').on(columns.createdAt),
|
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(
|
export const _posts_v = pgTable(
|
||||||
'_posts_v',
|
'_posts_v',
|
||||||
{
|
{
|
||||||
@@ -196,16 +218,10 @@ export const _posts_v = pgTable(
|
|||||||
parent: integer('parent_id').references(() => posts.id, {
|
parent: integer('parent_id').references(() => posts.id, {
|
||||||
onDelete: 'set null',
|
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_date: timestamp('version_date', { mode: 'string', withTimezone: true, precision: 3 }),
|
||||||
version_featuredImage: integer('version_featured_image_id').references(() => media.id, {
|
version_featuredImage: integer('version_featured_image_id').references(() => media.id, {
|
||||||
onDelete: 'set null',
|
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', {
|
version_updatedAt: timestamp('version_updated_at', {
|
||||||
mode: 'string',
|
mode: 'string',
|
||||||
withTimezone: true,
|
withTimezone: true,
|
||||||
@@ -223,21 +239,46 @@ export const _posts_v = pgTable(
|
|||||||
updatedAt: timestamp('updated_at', { mode: 'string', withTimezone: true, precision: 3 })
|
updatedAt: timestamp('updated_at', { mode: 'string', withTimezone: true, precision: 3 })
|
||||||
.defaultNow()
|
.defaultNow()
|
||||||
.notNull(),
|
.notNull(),
|
||||||
|
snapshot: boolean('snapshot'),
|
||||||
|
publishedLocale: enum__posts_v_published_locale('published_locale'),
|
||||||
latest: boolean('latest'),
|
latest: boolean('latest'),
|
||||||
},
|
},
|
||||||
(columns) => [
|
(columns) => [
|
||||||
index('_posts_v_parent_idx').on(columns.parent),
|
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_featured_image_idx').on(columns.version_featuredImage),
|
||||||
index('_posts_v_version_version_updated_at_idx').on(columns.version_updatedAt),
|
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_created_at_idx').on(columns.version_createdAt),
|
||||||
index('_posts_v_version_version__status_idx').on(columns.version__status),
|
index('_posts_v_version_version__status_idx').on(columns.version__status),
|
||||||
index('_posts_v_created_at_idx').on(columns.createdAt),
|
index('_posts_v_created_at_idx').on(columns.createdAt),
|
||||||
index('_posts_v_updated_at_idx').on(columns.updatedAt),
|
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),
|
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(
|
export const form_submissions = pgTable(
|
||||||
'form_submissions',
|
'form_submissions',
|
||||||
{
|
{
|
||||||
@@ -283,13 +324,11 @@ export const products = pgTable(
|
|||||||
'products',
|
'products',
|
||||||
{
|
{
|
||||||
id: serial('id').primaryKey(),
|
id: serial('id').primaryKey(),
|
||||||
title: varchar('title'),
|
|
||||||
sku: varchar('sku'),
|
sku: varchar('sku'),
|
||||||
slug: varchar('slug'),
|
slug: varchar('slug'),
|
||||||
description: varchar('description'),
|
featuredImage: integer('featured_image_id').references(() => media.id, {
|
||||||
locale: enum_products_locale('locale').default('de'),
|
onDelete: 'set null',
|
||||||
application: jsonb('application'),
|
}),
|
||||||
content: jsonb('content'),
|
|
||||||
updatedAt: timestamp('updated_at', { mode: 'string', withTimezone: true, precision: 3 })
|
updatedAt: timestamp('updated_at', { mode: 'string', withTimezone: true, precision: 3 })
|
||||||
.defaultNow()
|
.defaultNow()
|
||||||
.notNull(),
|
.notNull(),
|
||||||
@@ -299,13 +338,34 @@ export const products = pgTable(
|
|||||||
_status: enum_products_status('_status').default('draft'),
|
_status: enum_products_status('_status').default('draft'),
|
||||||
},
|
},
|
||||||
(columns) => [
|
(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_updated_at_idx').on(columns.updatedAt),
|
||||||
index('products_created_at_idx').on(columns.createdAt),
|
index('products_created_at_idx').on(columns.createdAt),
|
||||||
index('products__status_idx').on(columns._status),
|
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(
|
export const products_rels = pgTable(
|
||||||
'products_rels',
|
'products_rels',
|
||||||
{
|
{
|
||||||
@@ -360,13 +420,11 @@ export const _products_v = pgTable(
|
|||||||
parent: integer('parent_id').references(() => products.id, {
|
parent: integer('parent_id').references(() => products.id, {
|
||||||
onDelete: 'set null',
|
onDelete: 'set null',
|
||||||
}),
|
}),
|
||||||
version_title: varchar('version_title'),
|
|
||||||
version_sku: varchar('version_sku'),
|
version_sku: varchar('version_sku'),
|
||||||
version_slug: varchar('version_slug'),
|
version_slug: varchar('version_slug'),
|
||||||
version_description: varchar('version_description'),
|
version_featuredImage: integer('version_featured_image_id').references(() => media.id, {
|
||||||
version_locale: enum__products_v_version_locale('version_locale').default('de'),
|
onDelete: 'set null',
|
||||||
version_application: jsonb('version_application'),
|
}),
|
||||||
version_content: jsonb('version_content'),
|
|
||||||
version_updatedAt: timestamp('version_updated_at', {
|
version_updatedAt: timestamp('version_updated_at', {
|
||||||
mode: 'string',
|
mode: 'string',
|
||||||
withTimezone: true,
|
withTimezone: true,
|
||||||
@@ -384,20 +442,48 @@ export const _products_v = pgTable(
|
|||||||
updatedAt: timestamp('updated_at', { mode: 'string', withTimezone: true, precision: 3 })
|
updatedAt: timestamp('updated_at', { mode: 'string', withTimezone: true, precision: 3 })
|
||||||
.defaultNow()
|
.defaultNow()
|
||||||
.notNull(),
|
.notNull(),
|
||||||
|
snapshot: boolean('snapshot'),
|
||||||
|
publishedLocale: enum__products_v_published_locale('published_locale'),
|
||||||
latest: boolean('latest'),
|
latest: boolean('latest'),
|
||||||
},
|
},
|
||||||
(columns) => [
|
(columns) => [
|
||||||
index('_products_v_parent_idx').on(columns.parent),
|
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_updated_at_idx').on(columns.version_updatedAt),
|
||||||
index('_products_v_version_version_created_at_idx').on(columns.version_createdAt),
|
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_version_version__status_idx').on(columns.version__status),
|
||||||
index('_products_v_created_at_idx').on(columns.createdAt),
|
index('_products_v_created_at_idx').on(columns.createdAt),
|
||||||
index('_products_v_updated_at_idx').on(columns.updatedAt),
|
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),
|
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(
|
export const _products_v_rels = pgTable(
|
||||||
'_products_v_rels',
|
'_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(
|
export const payload_kv = pgTable(
|
||||||
'payload_kv',
|
'payload_kv',
|
||||||
{
|
{
|
||||||
@@ -466,6 +664,7 @@ export const payload_locked_documents_rels = pgTable(
|
|||||||
postsID: integer('posts_id'),
|
postsID: integer('posts_id'),
|
||||||
'form-submissionsID': integer('form_submissions_id'),
|
'form-submissionsID': integer('form_submissions_id'),
|
||||||
productsID: integer('products_id'),
|
productsID: integer('products_id'),
|
||||||
|
pagesID: integer('pages_id'),
|
||||||
},
|
},
|
||||||
(columns) => [
|
(columns) => [
|
||||||
index('payload_locked_documents_rels_order_idx').on(columns.order),
|
index('payload_locked_documents_rels_order_idx').on(columns.order),
|
||||||
@@ -478,6 +677,7 @@ export const payload_locked_documents_rels = pgTable(
|
|||||||
columns['form-submissionsID'],
|
columns['form-submissionsID'],
|
||||||
),
|
),
|
||||||
index('payload_locked_documents_rels_products_id_idx').on(columns.productsID),
|
index('payload_locked_documents_rels_products_id_idx').on(columns.productsID),
|
||||||
|
index('payload_locked_documents_rels_pages_id_idx').on(columns.pagesID),
|
||||||
foreignKey({
|
foreignKey({
|
||||||
columns: [columns['parent']],
|
columns: [columns['parent']],
|
||||||
foreignColumns: [payload_locked_documents.id],
|
foreignColumns: [payload_locked_documents.id],
|
||||||
@@ -508,6 +708,11 @@ export const payload_locked_documents_rels = pgTable(
|
|||||||
foreignColumns: [products.id],
|
foreignColumns: [products.id],
|
||||||
name: 'payload_locked_documents_rels_products_fk',
|
name: 'payload_locked_documents_rels_products_fk',
|
||||||
}).onDelete('cascade'),
|
}).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_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, {
|
featuredImage: one(media, {
|
||||||
fields: [posts.featuredImage],
|
fields: [posts.featuredImage],
|
||||||
references: [media.id],
|
references: [media.id],
|
||||||
relationName: 'featuredImage',
|
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, {
|
parent: one(posts, {
|
||||||
fields: [_posts_v.parent],
|
fields: [_posts_v.parent],
|
||||||
references: [posts.id],
|
references: [posts.id],
|
||||||
@@ -608,6 +830,9 @@ export const relations__posts_v = relations(_posts_v, ({ one }) => ({
|
|||||||
references: [media.id],
|
references: [media.id],
|
||||||
relationName: 'version_featuredImage',
|
relationName: 'version_featuredImage',
|
||||||
}),
|
}),
|
||||||
|
_locales: many(_posts_v_locales, {
|
||||||
|
relationName: '_locales',
|
||||||
|
}),
|
||||||
}));
|
}));
|
||||||
export const relations_form_submissions = relations(form_submissions, () => ({}));
|
export const relations_form_submissions = relations(form_submissions, () => ({}));
|
||||||
export const relations_products_categories = relations(products_categories, ({ one }) => ({
|
export const relations_products_categories = relations(products_categories, ({ one }) => ({
|
||||||
@@ -617,6 +842,13 @@ export const relations_products_categories = relations(products_categories, ({ o
|
|||||||
relationName: 'categories',
|
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 }) => ({
|
export const relations_products_rels = relations(products_rels, ({ one }) => ({
|
||||||
parent: one(products, {
|
parent: one(products, {
|
||||||
fields: [products_rels.parent],
|
fields: [products_rels.parent],
|
||||||
@@ -629,10 +861,18 @@ export const relations_products_rels = relations(products_rels, ({ one }) => ({
|
|||||||
relationName: 'media',
|
relationName: 'media',
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
export const relations_products = relations(products, ({ many }) => ({
|
export const relations_products = relations(products, ({ one, many }) => ({
|
||||||
categories: many(products_categories, {
|
categories: many(products_categories, {
|
||||||
relationName: 'categories',
|
relationName: 'categories',
|
||||||
}),
|
}),
|
||||||
|
featuredImage: one(media, {
|
||||||
|
fields: [products.featuredImage],
|
||||||
|
references: [media.id],
|
||||||
|
relationName: 'featuredImage',
|
||||||
|
}),
|
||||||
|
_locales: many(products_locales, {
|
||||||
|
relationName: '_locales',
|
||||||
|
}),
|
||||||
_rels: many(products_rels, {
|
_rels: many(products_rels, {
|
||||||
relationName: '_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 }) => ({
|
export const relations__products_v_rels = relations(_products_v_rels, ({ one }) => ({
|
||||||
parent: one(_products_v, {
|
parent: one(_products_v, {
|
||||||
fields: [_products_v_rels.parent],
|
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, {
|
version_categories: many(_products_v_version_categories, {
|
||||||
relationName: '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, {
|
_rels: many(_products_v_rels, {
|
||||||
relationName: '_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_kv = relations(payload_kv, () => ({}));
|
||||||
export const relations_payload_locked_documents_rels = relations(
|
export const relations_payload_locked_documents_rels = relations(
|
||||||
payload_locked_documents_rels,
|
payload_locked_documents_rels,
|
||||||
@@ -706,6 +1000,11 @@ export const relations_payload_locked_documents_rels = relations(
|
|||||||
references: [products.id],
|
references: [products.id],
|
||||||
relationName: 'products',
|
relationName: 'products',
|
||||||
}),
|
}),
|
||||||
|
pagesID: one(pages, {
|
||||||
|
fields: [payload_locked_documents_rels.pagesID],
|
||||||
|
references: [pages.id],
|
||||||
|
relationName: 'pages',
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
export const relations_payload_locked_documents = relations(
|
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, () => ({}));
|
export const relations_payload_migrations = relations(payload_migrations, () => ({}));
|
||||||
|
|
||||||
type DatabaseSchema = {
|
type DatabaseSchema = {
|
||||||
enum_posts_locale: typeof enum_posts_locale;
|
enum__locales: typeof enum__locales;
|
||||||
enum_posts_status: typeof enum_posts_status;
|
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_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_form_submissions_type: typeof enum_form_submissions_type;
|
||||||
enum_products_locale: typeof enum_products_locale;
|
|
||||||
enum_products_status: typeof enum_products_status;
|
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_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_sessions: typeof users_sessions;
|
||||||
users: typeof users;
|
users: typeof users;
|
||||||
media: typeof media;
|
media: typeof media;
|
||||||
posts: typeof posts;
|
posts: typeof posts;
|
||||||
|
posts_locales: typeof posts_locales;
|
||||||
_posts_v: typeof _posts_v;
|
_posts_v: typeof _posts_v;
|
||||||
|
_posts_v_locales: typeof _posts_v_locales;
|
||||||
form_submissions: typeof form_submissions;
|
form_submissions: typeof form_submissions;
|
||||||
products_categories: typeof products_categories;
|
products_categories: typeof products_categories;
|
||||||
products: typeof products;
|
products: typeof products;
|
||||||
|
products_locales: typeof products_locales;
|
||||||
products_rels: typeof products_rels;
|
products_rels: typeof products_rels;
|
||||||
_products_v_version_categories: typeof _products_v_version_categories;
|
_products_v_version_categories: typeof _products_v_version_categories;
|
||||||
_products_v: typeof _products_v;
|
_products_v: typeof _products_v;
|
||||||
|
_products_v_locales: typeof _products_v_locales;
|
||||||
_products_v_rels: typeof _products_v_rels;
|
_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_kv: typeof payload_kv;
|
||||||
payload_locked_documents: typeof payload_locked_documents;
|
payload_locked_documents: typeof payload_locked_documents;
|
||||||
payload_locked_documents_rels: typeof payload_locked_documents_rels;
|
payload_locked_documents_rels: typeof payload_locked_documents_rels;
|
||||||
@@ -769,15 +1080,23 @@ type DatabaseSchema = {
|
|||||||
relations_users_sessions: typeof relations_users_sessions;
|
relations_users_sessions: typeof relations_users_sessions;
|
||||||
relations_users: typeof relations_users;
|
relations_users: typeof relations_users;
|
||||||
relations_media: typeof relations_media;
|
relations_media: typeof relations_media;
|
||||||
|
relations_posts_locales: typeof relations_posts_locales;
|
||||||
relations_posts: typeof relations_posts;
|
relations_posts: typeof relations_posts;
|
||||||
|
relations__posts_v_locales: typeof relations__posts_v_locales;
|
||||||
relations__posts_v: typeof relations__posts_v;
|
relations__posts_v: typeof relations__posts_v;
|
||||||
relations_form_submissions: typeof relations_form_submissions;
|
relations_form_submissions: typeof relations_form_submissions;
|
||||||
relations_products_categories: typeof relations_products_categories;
|
relations_products_categories: typeof relations_products_categories;
|
||||||
|
relations_products_locales: typeof relations_products_locales;
|
||||||
relations_products_rels: typeof relations_products_rels;
|
relations_products_rels: typeof relations_products_rels;
|
||||||
relations_products: typeof relations_products;
|
relations_products: typeof relations_products;
|
||||||
relations__products_v_version_categories: typeof relations__products_v_version_categories;
|
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_rels: typeof relations__products_v_rels;
|
||||||
relations__products_v: typeof relations__products_v;
|
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_kv: typeof relations_payload_kv;
|
||||||
relations_payload_locked_documents_rels: typeof relations_payload_locked_documents_rels;
|
relations_payload_locked_documents_rels: typeof relations_payload_locked_documents_rels;
|
||||||
relations_payload_locked_documents: typeof relations_payload_locked_documents;
|
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 { AnimatedImage } from './AnimatedImage';
|
||||||
import { Callout } from './Callout';
|
import { Callout } from './Callout';
|
||||||
|
import { CategoryGrid } from './CategoryGrid';
|
||||||
import { ChatBubble } from './ChatBubble';
|
import { ChatBubble } from './ChatBubble';
|
||||||
import { ComparisonGrid } from './ComparisonGrid';
|
import { ComparisonGrid } from './ComparisonGrid';
|
||||||
|
import { ContactSection } from './ContactSection';
|
||||||
|
import { HeroSection } from './HeroSection';
|
||||||
import { HighlightBox } from './HighlightBox';
|
import { HighlightBox } from './HighlightBox';
|
||||||
|
import { ImageGallery } from './ImageGallery';
|
||||||
|
import { ManifestoGrid } from './ManifestoGrid';
|
||||||
import { PowerCTA } from './PowerCTA';
|
import { PowerCTA } from './PowerCTA';
|
||||||
import { ProductTabs } from './ProductTabs';
|
import { ProductTabs } from './ProductTabs';
|
||||||
import { SplitHeading } from './SplitHeading';
|
import { SplitHeading } from './SplitHeading';
|
||||||
import { Stats } from './Stats';
|
import { Stats } from './Stats';
|
||||||
import { StickyNarrative } from './StickyNarrative';
|
import { StickyNarrative } from './StickyNarrative';
|
||||||
|
import { TeamProfile } from './TeamProfile';
|
||||||
import { TechnicalGrid } from './TechnicalGrid';
|
import { TechnicalGrid } from './TechnicalGrid';
|
||||||
import { VisualLinkPreview } from './VisualLinkPreview';
|
import { VisualLinkPreview } from './VisualLinkPreview';
|
||||||
|
import { homeBlocksArray } from './HomeBlocks';
|
||||||
|
|
||||||
export const payloadBlocks = [
|
export const payloadBlocks = [
|
||||||
|
...homeBlocksArray,
|
||||||
AnimatedImage,
|
AnimatedImage,
|
||||||
Callout,
|
Callout,
|
||||||
|
CategoryGrid,
|
||||||
ChatBubble,
|
ChatBubble,
|
||||||
ComparisonGrid,
|
ComparisonGrid,
|
||||||
|
ContactSection,
|
||||||
|
HeroSection,
|
||||||
HighlightBox,
|
HighlightBox,
|
||||||
|
ImageGallery,
|
||||||
|
ManifestoGrid,
|
||||||
PowerCTA,
|
PowerCTA,
|
||||||
ProductTabs,
|
ProductTabs,
|
||||||
SplitHeading,
|
SplitHeading,
|
||||||
Stats,
|
Stats,
|
||||||
StickyNarrative,
|
StickyNarrative,
|
||||||
|
TeamProfile,
|
||||||
TechnicalGrid,
|
TechnicalGrid,
|
||||||
VisualLinkPreview,
|
VisualLinkPreview,
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,44 +1,65 @@
|
|||||||
import { CollectionConfig } from 'payload';
|
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 = {
|
export const Pages: CollectionConfig = {
|
||||||
slug: 'pages',
|
slug: 'pages',
|
||||||
admin: {
|
admin: {
|
||||||
useAsTitle: 'title',
|
useAsTitle: 'title',
|
||||||
defaultColumns: ['title', 'slug', 'locale', 'updatedAt'],
|
defaultColumns: ['title', 'slug', 'layout', '_status', 'updatedAt'],
|
||||||
|
},
|
||||||
|
versions: {
|
||||||
|
drafts: true,
|
||||||
},
|
},
|
||||||
access: {
|
access: {
|
||||||
read: () => true,
|
read: ({ req: { user } }) => {
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (user) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
_status: {
|
||||||
|
equals: 'published',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
},
|
},
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
name: 'title',
|
name: 'title',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
required: true,
|
required: true,
|
||||||
|
localized: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'slug',
|
name: 'slug',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
required: true,
|
required: true,
|
||||||
|
localized: true,
|
||||||
admin: {
|
admin: {
|
||||||
position: 'sidebar',
|
position: 'sidebar',
|
||||||
|
description: 'The URL slug for this locale (e.g. "impressum" for DE, "imprint" for EN).',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'locale',
|
name: 'layout',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
|
defaultValue: 'default',
|
||||||
options: [
|
options: [
|
||||||
{ label: 'English', value: 'en' },
|
{ label: 'Default (Article)', value: 'default' },
|
||||||
{ label: 'German', value: 'de' },
|
{ label: 'Full Bleed (Blocks Only)', value: 'fullBleed' },
|
||||||
],
|
],
|
||||||
required: true,
|
|
||||||
admin: {
|
admin: {
|
||||||
position: 'sidebar',
|
position: 'sidebar',
|
||||||
|
description: 'Full Bleed pages render blocks edge-to-edge without a generic hero wrapper.',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'excerpt',
|
name: 'excerpt',
|
||||||
type: 'textarea',
|
type: 'textarea',
|
||||||
|
localized: true,
|
||||||
admin: {
|
admin: {
|
||||||
position: 'sidebar',
|
position: 'sidebar',
|
||||||
},
|
},
|
||||||
@@ -54,7 +75,15 @@ export const Pages: CollectionConfig = {
|
|||||||
{
|
{
|
||||||
name: 'content',
|
name: 'content',
|
||||||
type: 'richText',
|
type: 'richText',
|
||||||
editor: lexicalEditor({}),
|
localized: true,
|
||||||
|
editor: lexicalEditor({
|
||||||
|
features: ({ defaultFeatures }) => [
|
||||||
|
...defaultFeatures,
|
||||||
|
BlocksFeature({
|
||||||
|
blocks: payloadBlocks,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -19,22 +19,16 @@ export const Posts: CollectionConfig = {
|
|||||||
defaultColumns: ['featuredImage', 'title', 'date', 'updatedAt', '_status'],
|
defaultColumns: ['featuredImage', 'title', 'date', 'updatedAt', '_status'],
|
||||||
},
|
},
|
||||||
versions: {
|
versions: {
|
||||||
drafts: true, // Enables Draft/Published workflows
|
drafts: true,
|
||||||
},
|
},
|
||||||
access: {
|
access: {
|
||||||
read: ({ req: { user } }) => {
|
read: ({ req: { user } }) => {
|
||||||
// In local development, always show everything (including Drafts and scheduled future posts)
|
|
||||||
if (process.env.NODE_ENV === 'development') {
|
if (process.env.NODE_ENV === 'development') {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If an Admin user is logged in, they can view everything
|
|
||||||
if (user) {
|
if (user) {
|
||||||
return true;
|
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 {
|
return {
|
||||||
and: [
|
and: [
|
||||||
{
|
{
|
||||||
@@ -56,19 +50,20 @@ export const Posts: CollectionConfig = {
|
|||||||
name: 'title',
|
name: 'title',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
required: true,
|
required: true,
|
||||||
|
localized: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'slug',
|
name: 'slug',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
required: true,
|
required: true,
|
||||||
unique: true,
|
localized: true,
|
||||||
admin: {
|
admin: {
|
||||||
position: 'sidebar',
|
position: 'sidebar',
|
||||||
|
description: 'Unique slug per locale (e.g. same slug can exist in DE and EN).',
|
||||||
},
|
},
|
||||||
hooks: {
|
hooks: {
|
||||||
beforeValidate: [
|
beforeValidate: [
|
||||||
({ value, data }) => {
|
({ value, data }) => {
|
||||||
// Auto-generate slug from title if left blank
|
|
||||||
if (value || !data?.title) return value;
|
if (value || !data?.title) return value;
|
||||||
return data.title
|
return data.title
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
@@ -81,6 +76,7 @@ export const Posts: CollectionConfig = {
|
|||||||
{
|
{
|
||||||
name: 'excerpt',
|
name: 'excerpt',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
|
localized: true,
|
||||||
admin: {
|
admin: {
|
||||||
description: 'A short summary for blog feed cards and SEO.',
|
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.',
|
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',
|
name: 'category',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
|
localized: true,
|
||||||
admin: {
|
admin: {
|
||||||
position: 'sidebar',
|
position: 'sidebar',
|
||||||
description: 'Used for tag bucketing (e.g. "Kabel Technologie").',
|
description: 'Used for tag bucketing (e.g. "Kabel Technologie").',
|
||||||
@@ -128,6 +112,7 @@ export const Posts: CollectionConfig = {
|
|||||||
{
|
{
|
||||||
name: 'content',
|
name: 'content',
|
||||||
type: 'richText',
|
type: 'richText',
|
||||||
|
localized: true,
|
||||||
editor: lexicalEditor({
|
editor: lexicalEditor({
|
||||||
features: ({ defaultFeatures }) => [
|
features: ({ defaultFeatures }) => [
|
||||||
...defaultFeatures,
|
...defaultFeatures,
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import { ProductTabs } from '../blocks/ProductTabs';
|
|||||||
export const Products: CollectionConfig = {
|
export const Products: CollectionConfig = {
|
||||||
slug: 'products',
|
slug: 'products',
|
||||||
admin: {
|
admin: {
|
||||||
defaultColumns: ['featuredImage', 'title', 'sku', 'locale', 'updatedAt', '_status'],
|
defaultColumns: ['featuredImage', 'title', 'sku', 'updatedAt', '_status'],
|
||||||
},
|
},
|
||||||
versions: {
|
versions: {
|
||||||
drafts: true,
|
drafts: true,
|
||||||
@@ -42,6 +42,7 @@ export const Products: CollectionConfig = {
|
|||||||
name: 'title',
|
name: 'title',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
required: true,
|
required: true,
|
||||||
|
localized: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'sku',
|
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',
|
name: 'slug',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
required: true,
|
required: true,
|
||||||
@@ -63,19 +65,7 @@ export const Products: CollectionConfig = {
|
|||||||
name: 'description',
|
name: 'description',
|
||||||
type: 'textarea',
|
type: 'textarea',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
localized: true,
|
||||||
{
|
|
||||||
name: 'locale',
|
|
||||||
type: 'select',
|
|
||||||
required: true,
|
|
||||||
admin: {
|
|
||||||
position: 'sidebar',
|
|
||||||
},
|
|
||||||
options: [
|
|
||||||
{ label: 'English', value: 'en' },
|
|
||||||
{ label: 'German', value: 'de' },
|
|
||||||
],
|
|
||||||
defaultValue: 'de',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'categories',
|
name: 'categories',
|
||||||
@@ -112,11 +102,13 @@ export const Products: CollectionConfig = {
|
|||||||
{
|
{
|
||||||
name: 'application',
|
name: 'application',
|
||||||
type: 'richText',
|
type: 'richText',
|
||||||
|
localized: true,
|
||||||
editor: lexicalEditor({}),
|
editor: lexicalEditor({}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'content',
|
name: 'content',
|
||||||
type: 'richText',
|
type: 'richText',
|
||||||
|
localized: true,
|
||||||
editor: lexicalEditor({
|
editor: lexicalEditor({
|
||||||
features: ({ defaultFeatures }) => [
|
features: ({ defaultFeatures }) => [
|
||||||
...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)...');
|
payload.logger.info('📦 No products found. Creating smoke test product (NAY2Y)...');
|
||||||
await payload.create({
|
await payload.create({
|
||||||
collection: 'products',
|
collection: 'products',
|
||||||
|
locale: 'de',
|
||||||
data: {
|
data: {
|
||||||
title: 'NAY2Y Smoke Test',
|
title: 'NAY2Y Smoke Test',
|
||||||
sku: 'SMOKE-TEST-001',
|
sku: 'SMOKE-TEST-001',
|
||||||
slug: 'nay2y',
|
slug: 'nay2y',
|
||||||
description: 'A dummy product for CI/CD smoke testing and OG image verification.',
|
description: 'A dummy product for CI/CD smoke testing and OG image verification.',
|
||||||
locale: 'de',
|
|
||||||
categories: [{ category: 'Power Cables' }],
|
categories: [{ category: 'Power Cables' }],
|
||||||
_status: 'published',
|
_status: 'published',
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user