feat: complete MDX migration, stabilize environment & verify via E2E tests
Former-commit-id: ec3e64156a2e182535cbdcf0d975cd54603a517d
This commit is contained in:
@@ -1,86 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { AlertCircle, RefreshCw } from 'lucide-react';
|
||||
import { config } from '../lib/config';
|
||||
|
||||
export default function CMSConnectivityNotice() {
|
||||
const [, setStatus] = useState<'checking' | 'ok' | 'error'>('checking');
|
||||
const [errorMsg, setErrorMsg] = useState('');
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Only show if we've detected an issue AND we are in a context where we want to see it
|
||||
const checkCMS = async () => {
|
||||
const isDebug = new URLSearchParams(window.location.search).has('cms_debug');
|
||||
const isLocal = config.isDevelopment;
|
||||
const isTesting = config.isTesting;
|
||||
const target = process.env.NEXT_PUBLIC_TARGET || '';
|
||||
const isBranch = target === 'branch';
|
||||
|
||||
// Only proceed with check if it's developer context (Local, Testing, or Branch preview)
|
||||
// Staging and Production should NEVER see this unless forced with ?cms_debug
|
||||
if (!isLocal && !isTesting && !isBranch && !isDebug) return;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/health/cms');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.status !== 'ok') {
|
||||
setStatus('error');
|
||||
setErrorMsg(data.message);
|
||||
setIsVisible(true);
|
||||
} else {
|
||||
setStatus('ok');
|
||||
setIsVisible(false);
|
||||
}
|
||||
} catch {
|
||||
// If it's a connection error, only show if we are really debugging
|
||||
if (isDebug || isLocal) {
|
||||
setStatus('error');
|
||||
setErrorMsg('Could not connect to CMS health endpoint');
|
||||
setIsVisible(true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
checkCMS();
|
||||
}, []);
|
||||
|
||||
if (!isVisible) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed bottom-4 right-4 z-[9999] animate-slide-up">
|
||||
<div className="bg-red-500/90 backdrop-blur-md border border-red-400 text-white p-4 rounded-2xl shadow-2xl max-w-sm">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="bg-white/20 p-2 rounded-lg">
|
||||
<AlertCircle className="w-5 h-5" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h4 className="font-bold text-sm mb-1">CMS Issue Detected</h4>
|
||||
<p className="text-xs opacity-90 leading-relaxed mb-3">
|
||||
{errorMsg === 'relation "products" does not exist'
|
||||
? 'The database schema is missing. Please run migrations for this environment.'
|
||||
: 'A content service is unavailable. Check the deployment logs for details.'}
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => window.location.reload()}
|
||||
className="bg-white text-red-600 text-[10px] font-bold uppercase tracking-wider px-3 py-1.5 rounded-lg flex items-center gap-2 hover:bg-neutral-100 transition-colors"
|
||||
>
|
||||
<RefreshCw className="w-3 h-3" />
|
||||
Retry
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setIsVisible(false)}
|
||||
className="bg-black/20 text-white text-[10px] font-bold uppercase tracking-wider px-3 py-1.5 rounded-lg hover:bg-black/30 transition-colors"
|
||||
>
|
||||
Dismiss
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
import { defaultJSXConverters, RichText } from '@payloadcms/richtext-lexical/react';
|
||||
import type { JSXConverters } from '@payloadcms/richtext-lexical/react';
|
||||
import Image from 'next/image';
|
||||
import { Fragment } from 'react';
|
||||
import { allBlocks } from '@/src/payload/blocks/allBlocks';
|
||||
import ObfuscatedEmail from '@/components/ObfuscatedEmail';
|
||||
import ObfuscatedPhone from '@/components/ObfuscatedPhone';
|
||||
|
||||
/**
|
||||
* Splits a text string on \n and intersperses <br /> elements.
|
||||
*/
|
||||
function textWithLineBreaks(text: string, key: string) {
|
||||
const parts = text.split('\n');
|
||||
if (parts.length === 1) return text;
|
||||
return parts.map((part, i) => (
|
||||
<Fragment key={`${key}-${i}`}>
|
||||
{part}
|
||||
{i < parts.length - 1 && <br />}
|
||||
</Fragment>
|
||||
));
|
||||
}
|
||||
|
||||
const jsxConverters: JSXConverters = {
|
||||
...defaultJSXConverters,
|
||||
linebreak: () => <br />,
|
||||
text: ({ node }: any) => {
|
||||
let content: React.ReactNode = node.text || '';
|
||||
if (typeof content === 'string' && content.includes('\n')) {
|
||||
content = textWithLineBreaks(content, `t-${(node.text || '').slice(0, 8)}`);
|
||||
}
|
||||
|
||||
if (typeof content === 'string' && content.includes('@')) {
|
||||
const emailRegex = /([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/g;
|
||||
const parts = content.split(emailRegex);
|
||||
content = parts.map((part, i) => {
|
||||
if (part.match(emailRegex)) {
|
||||
return <ObfuscatedEmail key={`e-${i}`} email={part} />;
|
||||
}
|
||||
return part;
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof content === 'string' && content.match(/\+\d+/)) {
|
||||
const phoneRegex = /(\+\d{1,4}[\d\s-]{5,15})/g;
|
||||
const parts = content.split(phoneRegex);
|
||||
content = parts.map((part, i) => {
|
||||
if (part.match(phoneRegex)) {
|
||||
return <ObfuscatedPhone key={`p-${i}`} phone={part} />;
|
||||
}
|
||||
return part;
|
||||
});
|
||||
}
|
||||
|
||||
return content;
|
||||
},
|
||||
upload: ({ node }: any) => {
|
||||
return (
|
||||
<div className="relative w-full aspect-video my-8 rounded-3xl overflow-hidden shadow-2xl group">
|
||||
<Image
|
||||
src={node.value.url}
|
||||
alt={node.value.alt || ''}
|
||||
fill
|
||||
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
|
||||
className="object-cover transition-transform duration-700 group-hover:scale-105"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
blocks: allBlocks.reduce((acc, block) => {
|
||||
const Component = block.render;
|
||||
if (!Component) return acc;
|
||||
|
||||
const renderFn = ({ node }: any) => {
|
||||
// Pass all fields as props to the component, and also as 'data' prop
|
||||
return <Component {...node.fields} data={node.fields} />;
|
||||
};
|
||||
|
||||
// Map both the direct slug and the block- prefixed slug
|
||||
return {
|
||||
...acc,
|
||||
[block.slug]: renderFn,
|
||||
[`block-${block.slug}`]: renderFn,
|
||||
};
|
||||
}, {}),
|
||||
};
|
||||
|
||||
export default function PayloadRichText({ data, className, references }: { data: any; className?: string; references?: any[] }) {
|
||||
if (!data) return null;
|
||||
return (
|
||||
<div className={className}>
|
||||
<RichText data={data} converters={jsxConverters} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import Link from 'next/link';
|
||||
import Image from 'next/image';
|
||||
import { useLocale } from 'next-intl';
|
||||
|
||||
interface CompanyInfo {
|
||||
contactEmail: string;
|
||||
@@ -12,11 +13,13 @@ interface FooterProps {
|
||||
}
|
||||
|
||||
export function Footer({ companyInfo }: FooterProps) {
|
||||
const locale = useLocale();
|
||||
|
||||
return (
|
||||
<footer className="bg-neutral-dark text-neutral-light py-16">
|
||||
<div className="container grid grid-cols-1 md:grid-cols-4 gap-12">
|
||||
<div className="col-span-1 md:col-span-2">
|
||||
<Link href="/" className="relative block h-14 w-56 mb-6">
|
||||
<Link href={`/${locale}`} className="relative block h-14 w-56 mb-6">
|
||||
<Image
|
||||
src="/assets/logo-white.png"
|
||||
alt="E-TIB Gruppe"
|
||||
@@ -38,17 +41,18 @@ export function Footer({ companyInfo }: FooterProps) {
|
||||
<div>
|
||||
<h4 className="font-heading font-semibold text-lg mb-4">Unternehmen</h4>
|
||||
<ul className="space-y-2">
|
||||
<li><Link href="/kompetenzen" className="text-text-light hover:text-primary-light transition-colors">Kompetenzen</Link></li>
|
||||
<li><Link href="/ueber-uns" className="text-text-light hover:text-primary-light transition-colors">Über uns</Link></li>
|
||||
<li><Link href="/karriere" className="text-text-light hover:text-primary-light transition-colors">Karriere / Jobs</Link></li>
|
||||
<li><Link href="/kontakt" className="text-text-light hover:text-primary-light transition-colors">Kontakt</Link></li>
|
||||
<li><Link href={`/${locale}/kompetenzen`} className="text-text-light hover:text-primary-light transition-colors">Kompetenzen</Link></li>
|
||||
<li><Link href={`/${locale}/ueber-uns`} className="text-text-light hover:text-primary-light transition-colors">Über uns</Link></li>
|
||||
<li><Link href={`/${locale}/karriere`} className="text-text-light hover:text-primary-light transition-colors">Karriere / Jobs</Link></li>
|
||||
<li><Link href={`/${locale}/kontakt`} className="text-text-light hover:text-primary-light transition-colors">Kontakt</Link></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-heading font-semibold text-lg mb-4">Rechtliches</h4>
|
||||
<ul className="space-y-2">
|
||||
<li><Link href="/impressum" className="text-text-light hover:text-primary-light transition-colors">Impressum</Link></li>
|
||||
<li><Link href="/datenschutz" className="text-text-light hover:text-primary-light transition-colors">Datenschutz</Link></li>
|
||||
<li><Link href={`/${locale}/impressum`} className="text-text-light hover:text-primary-light transition-colors">Impressum</Link></li>
|
||||
<li><Link href={`/${locale}/datenschutz`} className="text-text-light hover:text-primary-light transition-colors">Datenschutz</Link></li>
|
||||
<li><Link href={`/${locale}/${locale === 'de' ? 'agb' : 'terms'}`} className="text-text-light hover:text-primary-light transition-colors">{locale === 'de' ? 'AGB' : 'Terms'}</Link></li>
|
||||
<li><button className="text-text-light hover:text-primary-light transition-colors">Cookie-Einstellungen</button></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -95,10 +95,10 @@ export function Header({ navLinks }: HeaderProps) {
|
||||
<div className="h-4 w-px bg-white/20 mx-2" />
|
||||
<LanguageSwitcher />
|
||||
<Link
|
||||
href={`/${currentLocale}/kontakt`}
|
||||
href={`/${currentLocale}/${currentLocale === 'de' ? 'kontakt' : 'contact'}`}
|
||||
className="rounded-xl bg-primary hover:bg-primary-dark text-neutral-light px-8 py-3 font-bold text-sm uppercase tracking-wider transition-all shadow-xl hover:shadow-primary/20 hover:-translate-y-0.5 active:translate-y-0"
|
||||
>
|
||||
Kontakt
|
||||
{currentLocale === 'de' ? 'Kontakt' : 'Contact'}
|
||||
</Link>
|
||||
</nav>
|
||||
|
||||
@@ -154,10 +154,10 @@ export function Header({ navLinks }: HeaderProps) {
|
||||
})}
|
||||
|
||||
<Link
|
||||
href={`/${currentLocale}/kontakt`}
|
||||
href={`/${currentLocale}/${currentLocale === 'de' ? 'kontakt' : 'contact'}`}
|
||||
className="text-2xl font-heading font-bold text-primary border-b border-neutral-100 pb-4"
|
||||
>
|
||||
Kontakt
|
||||
{currentLocale === 'de' ? 'Kontakt' : 'Contact'}
|
||||
</Link>
|
||||
|
||||
<div className="mt-8 pt-6">
|
||||
|
||||
Reference in New Issue
Block a user