Compare commits
66 Commits
feat/busin
...
v2.3.34
| Author | SHA1 | Date | |
|---|---|---|---|
| aa299b285d | |||
| 2be0d1ea92 | |||
| 0554460153 | |||
| 4ae45b1aa7 | |||
| 76cbdaa37e | |||
| 5b1a2eff7f | |||
| 60422da644 | |||
| fc6da4e9aa | |||
| f7411d3dc4 | |||
| 7be2ec605c | |||
| bd00175912 | |||
| 9c9663eb88 | |||
| 402b5c30c6 | |||
| 2bb0381de4 | |||
| 614b0a5ffd | |||
| b1569a1ce2 | |||
| 2c35c6b8aa | |||
| bff150b1a7 | |||
| 8ffb1e5f6a | |||
| 42ff5b6226 | |||
| 539c503d0d | |||
| 4c473a2ffd | |||
| 4f64a94a15 | |||
| 5a82243965 | |||
| ebb14ee302 | |||
| 80f5e46d88 | |||
| b826b5b741 | |||
| 41228aca53 | |||
| cd3b7c1e3b | |||
| ecd525d782 | |||
| 52439cc5aa | |||
| 61bf8517d8 | |||
| 85d9e3a930 | |||
| 2f7d6befe4 | |||
| a6ba484faa | |||
| d7178a9100 | |||
| 1e39dc8567 | |||
| 3c2aa8b71e | |||
| 6e3befdfd9 | |||
| 5377b3c81a | |||
| 941625a4e0 | |||
| 9b251101d4 | |||
| ff0d880bde | |||
| b78a848ec2 | |||
| e72879b222 | |||
| 5a2b0c543e | |||
| 1203c20166 | |||
| 9944d3313b | |||
| d1cbfc41c8 | |||
| 09d435a1bc | |||
| 8f34ac2a6d | |||
| e5ecf16a66 | |||
| 12eadba0ee | |||
| 0ad975f049 | |||
| b53ea82be8 | |||
| 3d7a061858 | |||
| 44ad62de1c | |||
| 092325baa9 | |||
| a0910dd932 | |||
| db251687d1 | |||
| eaf17315ff | |||
| 6bab5c9173 | |||
| cf371e48ef | |||
| ec0c71a6a6 | |||
| 1b7fecff4a | |||
| b7f2d932a9 |
@@ -256,6 +256,10 @@ jobs:
|
|||||||
# Analytics
|
# Analytics
|
||||||
UMAMI_WEBSITE_ID: ${{ secrets.UMAMI_WEBSITE_ID || vars.UMAMI_WEBSITE_ID }}
|
UMAMI_WEBSITE_ID: ${{ secrets.UMAMI_WEBSITE_ID || vars.UMAMI_WEBSITE_ID }}
|
||||||
UMAMI_API_ENDPOINT: ${{ secrets.UMAMI_API_ENDPOINT || vars.UMAMI_API_ENDPOINT || 'https://analytics.infra.mintel.me' }}
|
UMAMI_API_ENDPOINT: ${{ secrets.UMAMI_API_ENDPOINT || vars.UMAMI_API_ENDPOINT || 'https://analytics.infra.mintel.me' }}
|
||||||
|
|
||||||
|
# Notifications
|
||||||
|
GOTIFY_URL: ${{ secrets.GOTIFY_URL }}
|
||||||
|
GOTIFY_TOKEN: ${{ secrets.GOTIFY_TOKEN }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@@ -309,6 +313,10 @@ jobs:
|
|||||||
echo "UMAMI_WEBSITE_ID=$UMAMI_WEBSITE_ID"
|
echo "UMAMI_WEBSITE_ID=$UMAMI_WEBSITE_ID"
|
||||||
echo "UMAMI_API_ENDPOINT=$UMAMI_API_ENDPOINT"
|
echo "UMAMI_API_ENDPOINT=$UMAMI_API_ENDPOINT"
|
||||||
echo ""
|
echo ""
|
||||||
|
echo "# Notifications"
|
||||||
|
echo "GOTIFY_URL=$GOTIFY_URL"
|
||||||
|
echo "GOTIFY_TOKEN=$GOTIFY_TOKEN"
|
||||||
|
echo ""
|
||||||
echo "TARGET=$TARGET"
|
echo "TARGET=$TARGET"
|
||||||
echo "SENTRY_ENVIRONMENT=$TARGET"
|
echo "SENTRY_ENVIRONMENT=$TARGET"
|
||||||
echo "PROJECT_NAME=$PROJECT_NAME"
|
echo "PROJECT_NAME=$PROJECT_NAME"
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -28,3 +28,4 @@ backups/
|
|||||||
|
|
||||||
# Local data / backups
|
# Local data / backups
|
||||||
.data/
|
.data/
|
||||||
|
scratch/
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { ImageResponse } from 'next/og';
|
import { ImageResponse } from 'next/og';
|
||||||
import { getPageBySlug } from '@/lib/pages';
|
import { getPageBySlug } from '@/lib/pages';
|
||||||
import { OGImageTemplate } from '@/components/OGImageTemplate';
|
import { OGImageTemplate } from '@/components/OGImageTemplate';
|
||||||
import { getOgFonts, OG_IMAGE_SIZE } from '@/lib/og-helper';
|
import { getOgFonts, OG_IMAGE_SIZE, getOgAssets } from '@/lib/og-helper';
|
||||||
|
|
||||||
export const size = OG_IMAGE_SIZE;
|
export const size = OG_IMAGE_SIZE;
|
||||||
export const contentType = 'image/png';
|
export const contentType = 'image/png';
|
||||||
@@ -20,9 +20,12 @@ export default async function Image({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fonts = await getOgFonts();
|
const fonts = await getOgFonts();
|
||||||
|
const { logo, bgImage } = getOgAssets();
|
||||||
|
|
||||||
return new ImageResponse(
|
return new ImageResponse(
|
||||||
<OGImageTemplate
|
<OGImageTemplate
|
||||||
|
image={bgImage}
|
||||||
|
logo={logo}
|
||||||
title={pageData.frontmatter.title}
|
title={pageData.frontmatter.title}
|
||||||
description={pageData.frontmatter.excerpt}
|
description={pageData.frontmatter.excerpt}
|
||||||
label="Information"
|
label="Information"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { ImageResponse } from 'next/og';
|
import { ImageResponse } from 'next/og';
|
||||||
import { getPostBySlug } from '@/lib/blog';
|
import { getPostBySlug } from '@/lib/blog';
|
||||||
import { OGImageTemplate } from '@/components/OGImageTemplate';
|
import { OGImageTemplate } from '@/components/OGImageTemplate';
|
||||||
import { getOgFonts, OG_IMAGE_SIZE } from '@/lib/og-helper';
|
import { getOgFonts, OG_IMAGE_SIZE, getOgAssets } from '@/lib/og-helper';
|
||||||
import { SITE_URL } from '@/lib/schema';
|
import { SITE_URL } from '@/lib/schema';
|
||||||
|
|
||||||
export const size = OG_IMAGE_SIZE;
|
export const size = OG_IMAGE_SIZE;
|
||||||
@@ -35,6 +35,7 @@ export default async function Image({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fonts = await getOgFonts();
|
const fonts = await getOgFonts();
|
||||||
|
const { logo, bgImage } = getOgAssets();
|
||||||
|
|
||||||
// We don't have request.url here, but we can assume the domain from SITE_URL or config
|
// We don't have request.url here, but we can assume the domain from SITE_URL or config
|
||||||
// For local images during dev, relative paths in <img> might not work in Satori
|
// For local images during dev, relative paths in <img> might not work in Satori
|
||||||
@@ -55,10 +56,11 @@ export default async function Image({
|
|||||||
|
|
||||||
return new ImageResponse(
|
return new ImageResponse(
|
||||||
<OGImageTemplate
|
<OGImageTemplate
|
||||||
|
logo={logo}
|
||||||
title={post.frontmatter.title}
|
title={post.frontmatter.title}
|
||||||
description={post.frontmatter.excerpt}
|
description={post.frontmatter.excerpt}
|
||||||
label={post.frontmatter.category || 'Blog'}
|
label={post.frontmatter.category || 'Blog'}
|
||||||
image={base64Image || featuredImage}
|
image={base64Image || featuredImage || bgImage}
|
||||||
/>,
|
/>,
|
||||||
{
|
{
|
||||||
...OG_IMAGE_SIZE,
|
...OG_IMAGE_SIZE,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { ImageResponse } from 'next/og';
|
import { ImageResponse } from 'next/og';
|
||||||
import { getTranslations } from 'next-intl/server';
|
import { getTranslations } from 'next-intl/server';
|
||||||
import { OGImageTemplate } from '@/components/OGImageTemplate';
|
import { OGImageTemplate } from '@/components/OGImageTemplate';
|
||||||
import { getOgFonts, OG_IMAGE_SIZE } from '@/lib/og-helper';
|
import { getOgFonts, OG_IMAGE_SIZE, getOgAssets } from '@/lib/og-helper';
|
||||||
|
|
||||||
export const size = OG_IMAGE_SIZE;
|
export const size = OG_IMAGE_SIZE;
|
||||||
export const contentType = 'image/png';
|
export const contentType = 'image/png';
|
||||||
@@ -11,9 +11,16 @@ export default async function Image({ params }: { params: Promise<{ locale: stri
|
|||||||
const { locale } = await params;
|
const { locale } = await params;
|
||||||
const t = await getTranslations({ locale, namespace: 'Blog.meta' });
|
const t = await getTranslations({ locale, namespace: 'Blog.meta' });
|
||||||
const fonts = await getOgFonts();
|
const fonts = await getOgFonts();
|
||||||
|
const { logo, bgImage } = getOgAssets();
|
||||||
|
|
||||||
return new ImageResponse(
|
return new ImageResponse(
|
||||||
<OGImageTemplate title={t('title')} description={t('description')} label="Blog" />,
|
<OGImageTemplate
|
||||||
|
image={bgImage}
|
||||||
|
logo={logo}
|
||||||
|
title={t('title')}
|
||||||
|
description={t('description')}
|
||||||
|
label="Blog"
|
||||||
|
/>,
|
||||||
{
|
{
|
||||||
...OG_IMAGE_SIZE,
|
...OG_IMAGE_SIZE,
|
||||||
fonts,
|
fonts,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import Image from 'next/image';
|
|||||||
import { getAllPosts } from '@/lib/blog';
|
import { getAllPosts } from '@/lib/blog';
|
||||||
import { Section, Container, Heading, Card, Badge, Button } from '@/components/ui';
|
import { Section, Container, Heading, Card, Badge, Button } from '@/components/ui';
|
||||||
import Reveal from '@/components/Reveal';
|
import Reveal from '@/components/Reveal';
|
||||||
import { Metadata } from 'next';
|
|
||||||
import { getTranslations, setRequestLocale } from 'next-intl/server';
|
import { getTranslations, setRequestLocale } from 'next-intl/server';
|
||||||
import { SITE_URL } from '@/lib/schema';
|
import { SITE_URL } from '@/lib/schema';
|
||||||
import { BlogPaginationKeyboardObserver } from '@/components/blog/BlogPaginationKeyboardObserver';
|
import { BlogPaginationKeyboardObserver } from '@/components/blog/BlogPaginationKeyboardObserver';
|
||||||
@@ -12,6 +12,7 @@ interface BlogIndexProps {
|
|||||||
params: Promise<{
|
params: Promise<{
|
||||||
locale: string;
|
locale: string;
|
||||||
}>;
|
}>;
|
||||||
|
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function generateMetadata({ params }: BlogIndexProps) {
|
export async function generateMetadata({ params }: BlogIndexProps) {
|
||||||
@@ -41,8 +42,10 @@ export async function generateMetadata({ params }: BlogIndexProps) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function BlogIndex({ params }: BlogIndexProps) {
|
export default async function BlogIndex({ params, searchParams }: BlogIndexProps) {
|
||||||
const { locale } = await params;
|
const { locale } = await params;
|
||||||
|
const resolvedSearchParams = await searchParams;
|
||||||
|
const categoryParam = resolvedSearchParams.category as string | undefined;
|
||||||
setRequestLocale(locale);
|
setRequestLocale(locale);
|
||||||
const t = await getTranslations('Blog');
|
const t = await getTranslations('Blog');
|
||||||
const posts = await getAllPosts(locale);
|
const posts = await getAllPosts(locale);
|
||||||
@@ -52,8 +55,14 @@ export default async function BlogIndex({ params }: BlogIndexProps) {
|
|||||||
(a, b) => new Date(b.frontmatter.date).getTime() - new Date(a.frontmatter.date).getTime(),
|
(a, b) => new Date(b.frontmatter.date).getTime() - new Date(a.frontmatter.date).getTime(),
|
||||||
);
|
);
|
||||||
|
|
||||||
const featuredPost = sortedPosts[0];
|
const filteredPosts = categoryParam
|
||||||
const remainingPosts = sortedPosts.slice(1);
|
? sortedPosts.filter(
|
||||||
|
(post) => post.frontmatter.category?.toLowerCase() === categoryParam.toLowerCase(),
|
||||||
|
)
|
||||||
|
: sortedPosts;
|
||||||
|
|
||||||
|
const featuredPost = filteredPosts[0];
|
||||||
|
const remainingPosts = filteredPosts.slice(1);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-neutral-light min-h-screen">
|
<div className="bg-neutral-light min-h-screen">
|
||||||
@@ -123,31 +132,38 @@ export default async function BlogIndex({ params }: BlogIndexProps) {
|
|||||||
{t('allArticles')}
|
{t('allArticles')}
|
||||||
</Heading>
|
</Heading>
|
||||||
<div className="flex flex-wrap gap-2 md:gap-4">
|
<div className="flex flex-wrap gap-2 md:gap-4">
|
||||||
{/* Category filters could go here */}
|
<Link href={`/${locale}/blog`}>
|
||||||
<Badge
|
<Badge
|
||||||
variant="primary"
|
variant={!categoryParam ? 'primary' : 'neutral'}
|
||||||
className="cursor-pointer hover:bg-primary hover:text-white transition-colors touch-target px-3 md:px-4"
|
className="cursor-pointer hover:bg-primary hover:text-white transition-colors touch-target px-3 md:px-4"
|
||||||
>
|
>
|
||||||
{t('categories.all')}
|
{t('categories.all')}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Badge
|
</Link>
|
||||||
variant="neutral"
|
<Link href={`/${locale}/blog?category=industry`}>
|
||||||
className="cursor-pointer hover:bg-primary hover:text-white transition-colors touch-target px-3 md:px-4"
|
<Badge
|
||||||
>
|
variant={categoryParam === 'industry' ? 'primary' : 'neutral'}
|
||||||
{t('categories.industry')}
|
className="cursor-pointer hover:bg-primary hover:text-white transition-colors touch-target px-3 md:px-4"
|
||||||
</Badge>
|
>
|
||||||
<Badge
|
{t('categories.industry')}
|
||||||
variant="neutral"
|
</Badge>
|
||||||
className="cursor-pointer hover:bg-primary hover:text-white transition-colors touch-target px-3 md:px-4"
|
</Link>
|
||||||
>
|
<Link href={`/${locale}/blog?category=technical`}>
|
||||||
{t('categories.technical')}
|
<Badge
|
||||||
</Badge>
|
variant={categoryParam === 'technical' ? 'primary' : 'neutral'}
|
||||||
<Badge
|
className="cursor-pointer hover:bg-primary hover:text-white transition-colors touch-target px-3 md:px-4"
|
||||||
variant="neutral"
|
>
|
||||||
className="cursor-pointer hover:bg-primary hover:text-white transition-colors touch-target px-3 md:px-4"
|
{t('categories.technical')}
|
||||||
>
|
</Badge>
|
||||||
{t('categories.sustainability')}
|
</Link>
|
||||||
</Badge>
|
<Link href={`/${locale}/blog?category=sustainability`}>
|
||||||
|
<Badge
|
||||||
|
variant={categoryParam === 'sustainability' ? 'primary' : 'neutral'}
|
||||||
|
className="cursor-pointer hover:bg-primary hover:text-white transition-colors touch-target px-3 md:px-4"
|
||||||
|
>
|
||||||
|
{t('categories.sustainability')}
|
||||||
|
</Badge>
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
@@ -260,7 +276,7 @@ export default async function BlogIndex({ params }: BlogIndexProps) {
|
|||||||
{t('prev')}
|
{t('prev')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
href={`/${locale}/blog?page=1`}
|
href={`/${locale}/blog?page=1${categoryParam ? `&category=${categoryParam}` : ''}`}
|
||||||
variant="primary"
|
variant="primary"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="md:h-11 md:px-6 md:text-base"
|
className="md:h-11 md:px-6 md:text-base"
|
||||||
@@ -269,7 +285,7 @@ export default async function BlogIndex({ params }: BlogIndexProps) {
|
|||||||
1
|
1
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
href={`/${locale}/blog?page=2`}
|
href={`/${locale}/blog?page=2${categoryParam ? `&category=${categoryParam}` : ''}`}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="md:h-11 md:px-6 md:text-base"
|
className="md:h-11 md:px-6 md:text-base"
|
||||||
@@ -277,7 +293,7 @@ export default async function BlogIndex({ params }: BlogIndexProps) {
|
|||||||
2
|
2
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
href={`/${locale}/blog?page=2`}
|
href={`/${locale}/blog?page=2${categoryParam ? `&category=${categoryParam}` : ''}`}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="md:h-11 md:px-6 md:text-base"
|
className="md:h-11 md:px-6 md:text-base"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { ImageResponse } from 'next/og';
|
import { ImageResponse } from 'next/og';
|
||||||
import { getTranslations } from 'next-intl/server';
|
import { getTranslations } from 'next-intl/server';
|
||||||
import { OGImageTemplate } from '@/components/OGImageTemplate';
|
import { OGImageTemplate } from '@/components/OGImageTemplate';
|
||||||
import { getOgFonts, OG_IMAGE_SIZE } from '@/lib/og-helper';
|
import { getOgFonts, OG_IMAGE_SIZE, getOgAssets } from '@/lib/og-helper';
|
||||||
|
|
||||||
export const size = OG_IMAGE_SIZE;
|
export const size = OG_IMAGE_SIZE;
|
||||||
export const contentType = 'image/png';
|
export const contentType = 'image/png';
|
||||||
@@ -11,12 +11,19 @@ export default async function Image({ params }: { params: Promise<{ locale: stri
|
|||||||
const { locale } = await params;
|
const { locale } = await params;
|
||||||
const t = await getTranslations({ locale, namespace: 'Contact' });
|
const t = await getTranslations({ locale, namespace: 'Contact' });
|
||||||
const fonts = await getOgFonts();
|
const fonts = await getOgFonts();
|
||||||
|
const { logo, bgImage } = getOgAssets();
|
||||||
|
|
||||||
const title = t('meta.title') || t('title');
|
const title = t('meta.title') || t('title');
|
||||||
const description = t('meta.description') || t('subtitle');
|
const description = t('meta.description') || t('subtitle');
|
||||||
|
|
||||||
return new ImageResponse(
|
return new ImageResponse(
|
||||||
<OGImageTemplate title={title} description={description} label="Contact" />,
|
<OGImageTemplate
|
||||||
|
image={bgImage}
|
||||||
|
logo={logo}
|
||||||
|
title={title}
|
||||||
|
description={description}
|
||||||
|
label="Contact"
|
||||||
|
/>,
|
||||||
{
|
{
|
||||||
...OG_IMAGE_SIZE,
|
...OG_IMAGE_SIZE,
|
||||||
fonts,
|
fonts,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { ImageResponse } from 'next/og';
|
import { ImageResponse } from 'next/og';
|
||||||
import { getTranslations } from 'next-intl/server';
|
import { getTranslations } from 'next-intl/server';
|
||||||
import { OGImageTemplate } from '@/components/OGImageTemplate';
|
import { OGImageTemplate } from '@/components/OGImageTemplate';
|
||||||
import { getOgFonts, OG_IMAGE_SIZE } from '@/lib/og-helper';
|
import { getOgFonts, OG_IMAGE_SIZE, getOgAssets } from '@/lib/og-helper';
|
||||||
|
|
||||||
export const size = OG_IMAGE_SIZE;
|
export const size = OG_IMAGE_SIZE;
|
||||||
export const contentType = 'image/png';
|
export const contentType = 'image/png';
|
||||||
@@ -11,9 +11,12 @@ export default async function Image({ params }: { params: Promise<{ locale: stri
|
|||||||
const { locale } = await params;
|
const { locale } = await params;
|
||||||
const t = await getTranslations({ locale, namespace: 'Index.meta' });
|
const t = await getTranslations({ locale, namespace: 'Index.meta' });
|
||||||
const fonts = await getOgFonts();
|
const fonts = await getOgFonts();
|
||||||
|
const { logo, bgImage } = getOgAssets();
|
||||||
|
|
||||||
return new ImageResponse(
|
return new ImageResponse(
|
||||||
<OGImageTemplate
|
<OGImageTemplate
|
||||||
|
image={bgImage}
|
||||||
|
logo={logo}
|
||||||
title={t('title')}
|
title={t('title')}
|
||||||
description={t('description')}
|
description={t('description')}
|
||||||
label="Reliable Energy Infrastructure"
|
label="Reliable Energy Infrastructure"
|
||||||
|
|||||||
@@ -266,18 +266,7 @@ export default async function ProductPage({ params }: ProductPageProps) {
|
|||||||
notFound();
|
notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
let technicalItems = [];
|
let technicalItems: any[] = [];
|
||||||
try {
|
|
||||||
const tabMatch =
|
|
||||||
typeof product.content === 'string' &&
|
|
||||||
product.content.match(/<Block type="productTabs" data={({.*?})}\s*\/>/s);
|
|
||||||
if (tabMatch && tabMatch[1]) {
|
|
||||||
const data = JSON.parse(tabMatch[1]);
|
|
||||||
technicalItems = data.technicalItems || [];
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// Ignore JSON parse errors for AST
|
|
||||||
}
|
|
||||||
|
|
||||||
const datasheetPath = getDatasheetPath(productSlug, locale);
|
const datasheetPath = getDatasheetPath(productSlug, locale);
|
||||||
const isFallback = (product.frontmatter as any).isFallback;
|
const isFallback = (product.frontmatter as any).isFallback;
|
||||||
@@ -296,8 +285,20 @@ export default async function ProductPage({ params }: ProductPageProps) {
|
|||||||
if (typeof product.content === 'string') {
|
if (typeof product.content === 'string') {
|
||||||
const tabBlockMatch = product.content.match(/<Block type="productTabs".*?\/>/s);
|
const tabBlockMatch = product.content.match(/<Block type="productTabs".*?\/>/s);
|
||||||
if (tabBlockMatch) {
|
if (tabBlockMatch) {
|
||||||
descriptionContent = product.content.replace(tabBlockMatch[0], '');
|
|
||||||
technicalContent = tabBlockMatch[0];
|
technicalContent = tabBlockMatch[0];
|
||||||
|
descriptionContent = product.content.replace(tabBlockMatch[0], '');
|
||||||
|
|
||||||
|
const dataMatch =
|
||||||
|
technicalContent.match(/data="({.*?})"/s) || technicalContent.match(/data={({.*?})}/s);
|
||||||
|
if (dataMatch && dataMatch[1]) {
|
||||||
|
try {
|
||||||
|
const rawJsonStr = dataMatch[1].replace(/"/g, '"');
|
||||||
|
const data = JSON.parse(rawJsonStr);
|
||||||
|
technicalItems = data.technicalItems || [];
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to parse productTabs data for page:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { ImageResponse } from 'next/og';
|
import { ImageResponse } from 'next/og';
|
||||||
import { getTranslations } from 'next-intl/server';
|
import { getTranslations } from 'next-intl/server';
|
||||||
import { OGImageTemplate } from '@/components/OGImageTemplate';
|
import { OGImageTemplate } from '@/components/OGImageTemplate';
|
||||||
import { getOgFonts, OG_IMAGE_SIZE } from '@/lib/og-helper';
|
import { getOgFonts, OG_IMAGE_SIZE, getOgAssets } from '@/lib/og-helper';
|
||||||
|
|
||||||
export const size = OG_IMAGE_SIZE;
|
export const size = OG_IMAGE_SIZE;
|
||||||
export const contentType = 'image/png';
|
export const contentType = 'image/png';
|
||||||
@@ -11,6 +11,7 @@ export default async function Image({ params }: { params: Promise<{ locale: stri
|
|||||||
const { locale } = await params;
|
const { locale } = await params;
|
||||||
const t = await getTranslations({ locale, namespace: 'Products' });
|
const t = await getTranslations({ locale, namespace: 'Products' });
|
||||||
const fonts = await getOgFonts();
|
const fonts = await getOgFonts();
|
||||||
|
const { logo, bgImage } = getOgAssets();
|
||||||
|
|
||||||
const title = t.has('meta.title')
|
const title = t.has('meta.title')
|
||||||
? t('meta.title')
|
? t('meta.title')
|
||||||
@@ -20,7 +21,13 @@ export default async function Image({ params }: { params: Promise<{ locale: stri
|
|||||||
const description = t('meta.description') || t('subtitle');
|
const description = t('meta.description') || t('subtitle');
|
||||||
|
|
||||||
return new ImageResponse(
|
return new ImageResponse(
|
||||||
<OGImageTemplate title={title} description={description} label="Products" />,
|
<OGImageTemplate
|
||||||
|
image={bgImage}
|
||||||
|
logo={logo}
|
||||||
|
title={title}
|
||||||
|
description={description}
|
||||||
|
label="Products"
|
||||||
|
/>,
|
||||||
{
|
{
|
||||||
...OG_IMAGE_SIZE,
|
...OG_IMAGE_SIZE,
|
||||||
fonts,
|
fonts,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { ImageResponse } from 'next/og';
|
import { ImageResponse } from 'next/og';
|
||||||
import { getTranslations } from 'next-intl/server';
|
import { getTranslations } from 'next-intl/server';
|
||||||
import { OGImageTemplate } from '@/components/OGImageTemplate';
|
import { OGImageTemplate } from '@/components/OGImageTemplate';
|
||||||
import { getOgFonts, OG_IMAGE_SIZE } from '@/lib/og-helper';
|
import { getOgFonts, OG_IMAGE_SIZE, getOgAssets } from '@/lib/og-helper';
|
||||||
|
|
||||||
export const size = OG_IMAGE_SIZE;
|
export const size = OG_IMAGE_SIZE;
|
||||||
export const contentType = 'image/png';
|
export const contentType = 'image/png';
|
||||||
@@ -11,12 +11,19 @@ export default async function Image({ params }: { params: Promise<{ locale: stri
|
|||||||
const { locale } = await params;
|
const { locale } = await params;
|
||||||
const t = await getTranslations({ locale, namespace: 'Team' });
|
const t = await getTranslations({ locale, namespace: 'Team' });
|
||||||
const fonts = await getOgFonts();
|
const fonts = await getOgFonts();
|
||||||
|
const { logo, bgImage } = getOgAssets();
|
||||||
|
|
||||||
const title = t('meta.title') || t('hero.subtitle');
|
const title = t('meta.title') || t('hero.subtitle');
|
||||||
const description = t('meta.description') || t('hero.title');
|
const description = t('meta.description') || t('hero.title');
|
||||||
|
|
||||||
return new ImageResponse(
|
return new ImageResponse(
|
||||||
<OGImageTemplate title={title} description={description} label="Our Team" />,
|
<OGImageTemplate
|
||||||
|
image={bgImage}
|
||||||
|
logo={logo}
|
||||||
|
title={title}
|
||||||
|
description={description}
|
||||||
|
label="Our Team"
|
||||||
|
/>,
|
||||||
{
|
{
|
||||||
...OG_IMAGE_SIZE,
|
...OG_IMAGE_SIZE,
|
||||||
fonts,
|
fonts,
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ export async function sendContactFormAction(formData: FormData) {
|
|||||||
productName,
|
productName,
|
||||||
message,
|
message,
|
||||||
};
|
};
|
||||||
fs.appendFileSync(backupFile, JSON.stringify(leadData) + '\\n');
|
fs.appendFileSync(backupFile, JSON.stringify(leadData) + '\n');
|
||||||
logger.info('Successfully saved lead to local backup file', { backupFile });
|
logger.info('Successfully saved lead to local backup file', { backupFile });
|
||||||
} catch (backupError) {
|
} catch (backupError) {
|
||||||
logger.error('Failed to write to local leads backup', { error: String(backupError) });
|
logger.error('Failed to write to local leads backup', { error: String(backupError) });
|
||||||
@@ -156,7 +156,7 @@ export async function sendContactFormAction(formData: FormData) {
|
|||||||
|
|
||||||
// Notify via Gotify (Internal)
|
// Notify via Gotify (Internal)
|
||||||
await services.notifications.notify({
|
await services.notifications.notify({
|
||||||
title: `📩 ${notificationSubject}`,
|
title: `📩 [KLZ] ${notificationSubject}`,
|
||||||
message: `New message from ${name} (${email}):\n\n${message}`,
|
message: `New message from ${name} (${email}):\n\n${message}`,
|
||||||
priority: 5,
|
priority: 5,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ import MeetTheTeam from './home/MeetTheTeam';
|
|||||||
import GallerySection from './home/GallerySection';
|
import GallerySection from './home/GallerySection';
|
||||||
import VideoSection from './home/VideoSection';
|
import VideoSection from './home/VideoSection';
|
||||||
import CTA from './home/CTA';
|
import CTA from './home/CTA';
|
||||||
|
import { AgbHistoryBlock } from './AgbHistoryBlock';
|
||||||
|
import { PDFDownloadBlock } from './PDFDownloadBlock';
|
||||||
|
import ProductTechnicalData from './ProductTechnicalData';
|
||||||
function ContactSection(props: any) {
|
function ContactSection(props: any) {
|
||||||
return (
|
return (
|
||||||
<div className="p-8 border-2 border-dashed border-primary my-8 text-center text-primary font-bold">
|
<div className="p-8 border-2 border-dashed border-primary my-8 text-center text-primary font-bold">
|
||||||
@@ -94,6 +97,12 @@ async function Block(props: any) {
|
|||||||
return <VideoSection data={data} />;
|
return <VideoSection data={data} />;
|
||||||
case 'homeCTA':
|
case 'homeCTA':
|
||||||
return <CTA data={data} />;
|
return <CTA data={data} />;
|
||||||
|
case 'agbHistory':
|
||||||
|
return <AgbHistoryBlock {...data} />;
|
||||||
|
case 'pdfDownload':
|
||||||
|
return <PDFDownloadBlock {...data} />;
|
||||||
|
case 'productTabs':
|
||||||
|
return <ProductTechnicalData data={data} />;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ interface OGImageTemplateProps {
|
|||||||
description?: string;
|
description?: string;
|
||||||
label?: string;
|
label?: string;
|
||||||
image?: string;
|
image?: string;
|
||||||
|
logo?: string;
|
||||||
mode?: 'dark' | 'light' | 'image';
|
mode?: 'dark' | 'light' | 'image';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -13,6 +14,7 @@ export function OGImageTemplate({
|
|||||||
description,
|
description,
|
||||||
label,
|
label,
|
||||||
image,
|
image,
|
||||||
|
logo,
|
||||||
mode = 'dark',
|
mode = 'dark',
|
||||||
}: OGImageTemplateProps) {
|
}: OGImageTemplateProps) {
|
||||||
const primaryBlue = '#001a4d';
|
const primaryBlue = '#001a4d';
|
||||||
@@ -152,21 +154,26 @@ export function OGImageTemplate({
|
|||||||
height: '6px',
|
height: '6px',
|
||||||
backgroundColor: accentGreen,
|
backgroundColor: accentGreen,
|
||||||
borderRadius: '3px',
|
borderRadius: '3px',
|
||||||
marginRight: '24px',
|
marginRight: '32px',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div
|
{logo ? (
|
||||||
style={{
|
/* eslint-disable-next-line @next/next/no-img-element */
|
||||||
fontSize: '24px',
|
<img src={logo} alt="KLZ Cables" height="40" style={{ objectFit: 'contain' }} />
|
||||||
fontWeight: 700,
|
) : (
|
||||||
color: 'white',
|
<div
|
||||||
textTransform: 'uppercase',
|
style={{
|
||||||
letterSpacing: '0.15em',
|
fontSize: '24px',
|
||||||
display: 'flex',
|
fontWeight: 700,
|
||||||
}}
|
color: 'white',
|
||||||
>
|
textTransform: 'uppercase',
|
||||||
KLZ Cables
|
letterSpacing: '0.15em',
|
||||||
</div>
|
display: 'flex',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
KLZ Cables
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Saturated Blue Brand Strip */}
|
{/* Saturated Blue Brand Strip */}
|
||||||
|
|||||||
@@ -3,7 +3,11 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { usePathname } from 'next/navigation';
|
import { usePathname } from 'next/navigation';
|
||||||
|
|
||||||
export const PDFDownloadBlock: React.FC<{ label: string; style: string }> = ({ label, style }) => {
|
export const PDFDownloadBlock: React.FC<{ label: string; style: string; url?: string }> = ({
|
||||||
|
label,
|
||||||
|
style,
|
||||||
|
url,
|
||||||
|
}) => {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
|
||||||
// Extract slug from pathname
|
// Extract slug from pathname
|
||||||
@@ -12,7 +16,7 @@ export const PDFDownloadBlock: React.FC<{ label: string; style: string }> = ({ l
|
|||||||
// We want the page slug.
|
// We want the page slug.
|
||||||
const slug = segments[segments.length - 1] || 'home';
|
const slug = segments[segments.length - 1] || 'home';
|
||||||
|
|
||||||
const href = `/api/pages/${slug}/pdf`;
|
const href = url || `/api/pages/${slug}/pdf`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="my-8">
|
<div className="my-8">
|
||||||
|
|||||||
@@ -247,7 +247,7 @@ _status: published
|
|||||||
|
|
||||||
Stand: April 2026
|
Stand: April 2026
|
||||||
|
|
||||||
<Block type="pdfDownload" data={{"id":"69a9b09ea220fecfb9d54bb8","label":"Als PDF herunterladen","style":"primary","blockName":"","blockType":"pdfDownload"}} />
|
<Block type="pdfDownload" data={{"id":"69a9b09ea220fecfb9d54bb8","label":"Als PDF herunterladen","style":"primary","url":"/AVB-KLZ-4-2026.pdf","blockName":"","blockType":"pdfDownload"}} />
|
||||||
|
|
||||||
<Block type="agbHistory" data={{"id":"69a9b09ea220fecfb9d54bb9","blockName":"AGB History","blockType":"agbHistory","title":"Vorherige Versionen"}} />
|
<Block type="agbHistory" data={{"id":"69a9b09ea220fecfb9d54bb9","blockName":"AGB History","blockType":"agbHistory","title":"Vorherige Versionen"}} />
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -65,11 +65,8 @@ export async function getPostBySlug(slug: string, locale: string): Promise<PostD
|
|||||||
const fileContent = await fs.readFile(filePath, 'utf-8');
|
const fileContent = await fs.readFile(filePath, 'utf-8');
|
||||||
const { data, content } = matter(fileContent);
|
const { data, content } = matter(fileContent);
|
||||||
|
|
||||||
// Fix MDX data props dropped by next-mdx-remote
|
const { fixMdxDataProps } = await import('./mdx-utils');
|
||||||
// Payload serializes as data={{"items":...}} which Acorn treats as a Block statement.
|
const fixedContent = fixMdxDataProps(content);
|
||||||
const fixedContent = content.replace(/data=\{\{([\s\S]*?)\}\}/g, (match, p1) => {
|
|
||||||
return 'data="{' + p1.replace(/"/g, '"') + '}"';
|
|
||||||
});
|
|
||||||
|
|
||||||
let parsedContent = fixedContent;
|
let parsedContent = fixedContent;
|
||||||
try {
|
try {
|
||||||
|
|||||||
146
lib/mdx-utils.ts
Normal file
146
lib/mdx-utils.ts
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
export function fixMdxDataProps(content: string): string {
|
||||||
|
let fixedContent = content;
|
||||||
|
let dataIndex = fixedContent.indexOf('data={{');
|
||||||
|
|
||||||
|
while (dataIndex !== -1) {
|
||||||
|
let openCount = 0;
|
||||||
|
let endIndex = -1;
|
||||||
|
const startObj = dataIndex + 6; // index of the first '{' in 'data={{'
|
||||||
|
|
||||||
|
let inString = false;
|
||||||
|
let isEscaped = false;
|
||||||
|
|
||||||
|
for (let i = startObj; i < fixedContent.length; i++) {
|
||||||
|
const char = fixedContent[i];
|
||||||
|
|
||||||
|
if (isEscaped) {
|
||||||
|
isEscaped = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (char === '\\') {
|
||||||
|
isEscaped = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (char === '"') {
|
||||||
|
inString = !inString;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!inString) {
|
||||||
|
if (char === '{') {
|
||||||
|
openCount++;
|
||||||
|
} else if (char === '}') {
|
||||||
|
openCount--;
|
||||||
|
if (openCount === 0) {
|
||||||
|
endIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (endIndex !== -1) {
|
||||||
|
// jsonStr is the content INSIDE the outer curly braces
|
||||||
|
const jsonStr = fixedContent.substring(startObj + 1, endIndex);
|
||||||
|
const safeJsonStr = jsonStr.replace(/"/g, '"');
|
||||||
|
const replacement = `data="{${safeJsonStr}}"`;
|
||||||
|
|
||||||
|
// We also need to consume the closing `}` of the `data={{`
|
||||||
|
const nextCharIndex = endIndex + 1;
|
||||||
|
const skipChars =
|
||||||
|
nextCharIndex < fixedContent.length && fixedContent[nextCharIndex] === '}' ? 2 : 1;
|
||||||
|
|
||||||
|
fixedContent =
|
||||||
|
fixedContent.substring(0, dataIndex) +
|
||||||
|
replacement +
|
||||||
|
fixedContent.substring(endIndex + skipChars);
|
||||||
|
|
||||||
|
dataIndex = fixedContent.indexOf('data={{', dataIndex + replacement.length);
|
||||||
|
} else {
|
||||||
|
// Malformed braces, prevent infinite loop
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fixedContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function lexicalToMarkdown(node: any, depth = 0): string {
|
||||||
|
if (!node) return '';
|
||||||
|
|
||||||
|
if (typeof node === 'string') return node;
|
||||||
|
|
||||||
|
if (node.type === 'text') {
|
||||||
|
let text = node.text || '';
|
||||||
|
if (node.format === 1) text = `**${text}**`; // Bold
|
||||||
|
if (node.format === 2) text = `*${text}*`; // Italic
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.type === 'heading') {
|
||||||
|
const level = parseInt((node.tag || 'h1').replace('h', '')) || 1;
|
||||||
|
const prefix = '#'.repeat(level);
|
||||||
|
const text = (node.children || []).map((c: any) => lexicalToMarkdown(c, depth)).join('');
|
||||||
|
return `${prefix} ${text}\n\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.type === 'paragraph') {
|
||||||
|
const text = (node.children || []).map((c: any) => lexicalToMarkdown(c, depth)).join('');
|
||||||
|
return text.trim() ? `${text}\n\n` : '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.type === 'list') {
|
||||||
|
const isOrdered = node.tag === 'ol';
|
||||||
|
const items = (node.children || [])
|
||||||
|
.map((c: any, index: number) => {
|
||||||
|
const prefix = isOrdered ? `${index + 1}.` : '-';
|
||||||
|
return `${prefix} ${lexicalToMarkdown(c, depth + 1).trim()}`;
|
||||||
|
})
|
||||||
|
.join('\n');
|
||||||
|
return `${items}\n\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.type === 'listitem') {
|
||||||
|
return (node.children || []).map((c: any) => lexicalToMarkdown(c, depth)).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.type === 'quote') {
|
||||||
|
const text = (node.children || []).map((c: any) => lexicalToMarkdown(c, depth)).join('');
|
||||||
|
return `> ${text}\n\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.type === 'link') {
|
||||||
|
const url = node.fields?.url || node.url || '';
|
||||||
|
const text = (node.children || []).map((c: any) => lexicalToMarkdown(c, depth)).join('');
|
||||||
|
return `[${text}](${url})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.type === 'upload') {
|
||||||
|
const url = node.value?.url || '';
|
||||||
|
const alt = node.value?.alt || node.value?.filename || '';
|
||||||
|
return `\n\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.type === 'block') {
|
||||||
|
const blockType = node.fields?.blockType;
|
||||||
|
if (blockType === 'contactSection') {
|
||||||
|
return `<ContactSection showMap={${!!node.fields?.showMap}} showForm={${!!node.fields?.showForm}} showHours={${!!node.fields?.showHours}} />\n\n`;
|
||||||
|
}
|
||||||
|
if (blockType === 'heroSection') {
|
||||||
|
return `<HeroSection badge="${node.fields?.badge || ''}" title="${node.fields?.title || ''}" subtitle="${node.fields?.subtitle || ''}" alignment="${node.fields?.alignment || 'left'}" />\n\n`;
|
||||||
|
}
|
||||||
|
if (blockType === 'features') {
|
||||||
|
return `<FeaturesSection layout="${node.fields?.layout || ''}" />\n\n`;
|
||||||
|
}
|
||||||
|
// Generic MDX block wrapper if unknown
|
||||||
|
return `<Block type="${blockType}" data={${JSON.stringify(node.fields)}} />\n\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.children && Array.isArray(node.children)) {
|
||||||
|
return node.children.map((c: any) => lexicalToMarkdown(c, depth)).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
@@ -55,3 +55,36 @@ export const OG_IMAGE_SIZE = {
|
|||||||
width: 1200,
|
width: 1200,
|
||||||
height: 630,
|
height: 630,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads a local image and converts it to a base64 data URI for Satori
|
||||||
|
*/
|
||||||
|
export function getLocalImageBase64(filename: string): string | undefined {
|
||||||
|
try {
|
||||||
|
const filePath = join(process.cwd(), 'public', filename);
|
||||||
|
const buffer = readFileSync(filePath);
|
||||||
|
const ext = filename.split('.').pop()?.toLowerCase();
|
||||||
|
const mimeType =
|
||||||
|
ext === 'png'
|
||||||
|
? 'image/png'
|
||||||
|
: ext === 'jpg' || ext === 'jpeg'
|
||||||
|
? 'image/jpeg'
|
||||||
|
: 'image/svg+xml';
|
||||||
|
return `data:${mimeType};base64,${buffer.toString('base64')}`;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[OG] Failed to load local image ${filename}:`, error);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns standard assets for OG images
|
||||||
|
*/
|
||||||
|
export function getOgAssets() {
|
||||||
|
return {
|
||||||
|
logo: getLocalImageBase64('logo-white.png'),
|
||||||
|
bgImage: getLocalImageBase64(
|
||||||
|
'media/aerial-view-of-electricity-station-surrounded-with-2023-11-27-05-33-40-utc-scaled.jpg',
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -53,11 +53,8 @@ export async function getPageBySlug(slug: string, locale: string): Promise<PageD
|
|||||||
const fileContent = await fs.readFile(filePath, 'utf-8');
|
const fileContent = await fs.readFile(filePath, 'utf-8');
|
||||||
const { data, content } = matter(fileContent);
|
const { data, content } = matter(fileContent);
|
||||||
|
|
||||||
// Fix MDX data props dropped by next-mdx-remote
|
const { fixMdxDataProps } = await import('./mdx-utils');
|
||||||
// Payload serializes as data={{"items":...}} which Acorn treats as a Block statement.
|
const fixedContent = fixMdxDataProps(content);
|
||||||
const fixedContent = content.replace(/data=\{\{([\s\S]*?)\}\}/g, (match, p1) => {
|
|
||||||
return 'data="{' + p1.replace(/"/g, '"') + '}"';
|
|
||||||
});
|
|
||||||
|
|
||||||
let parsedContent = fixedContent;
|
let parsedContent = fixedContent;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -47,14 +47,15 @@ export interface PDFBusinessCardProps {
|
|||||||
qrCodeDataUrl: string;
|
qrCodeDataUrl: string;
|
||||||
variant?: 'blue' | 'white' | 'black';
|
variant?: 'blue' | 'white' | 'black';
|
||||||
isViaprinto?: boolean;
|
isViaprinto?: boolean;
|
||||||
|
withTruck?: boolean;
|
||||||
|
withCropMarks?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Die Buchstaben K, L, Z im SVG (inklusive dem kleinen Stück vom Z oben links)
|
// Die Buchstaben K, L, Z im SVG (inklusive dem kleinen Stück vom Z oben links)
|
||||||
export const KLZ_PATHS = [25, 26, 27, 28, 29];
|
export const KLZ_PATHS = [25, 26, 27, 28, 29];
|
||||||
|
|
||||||
// Mintel Aesthetics Colors - EXACT PROD COLORS
|
|
||||||
const COLORS = {
|
const COLORS = {
|
||||||
primary: '#011dff', // Brand Royal Blue
|
primary: '#000a66', // Brand Dark Blue (from website)
|
||||||
blue: '#000a66',
|
blue: '#000a66',
|
||||||
green: '#82ed20', // Brand Accent Green
|
green: '#82ed20', // Brand Accent Green
|
||||||
darkGreen: '#4A9E00', // Darker green for white background visibility
|
darkGreen: '#4A9E00', // Darker green for white background visibility
|
||||||
@@ -65,6 +66,98 @@ const COLORS = {
|
|||||||
lightGray: '#f8f9fa',
|
lightGray: '#f8f9fa',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const CropMarks = () => (
|
||||||
|
<View style={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, zIndex: 9999 }}>
|
||||||
|
{/* Top Left */}
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
left: '13mm',
|
||||||
|
top: '0mm',
|
||||||
|
width: '0.25pt',
|
||||||
|
height: '10mm',
|
||||||
|
backgroundColor: '#000',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
left: '0mm',
|
||||||
|
top: '13mm',
|
||||||
|
width: '10mm',
|
||||||
|
height: '0.25pt',
|
||||||
|
backgroundColor: '#000',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Top Right */}
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
right: '13mm',
|
||||||
|
top: '0mm',
|
||||||
|
width: '0.25pt',
|
||||||
|
height: '10mm',
|
||||||
|
backgroundColor: '#000',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
right: '0mm',
|
||||||
|
top: '13mm',
|
||||||
|
width: '10mm',
|
||||||
|
height: '0.25pt',
|
||||||
|
backgroundColor: '#000',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Bottom Left */}
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
left: '13mm',
|
||||||
|
bottom: '0mm',
|
||||||
|
width: '0.25pt',
|
||||||
|
height: '10mm',
|
||||||
|
backgroundColor: '#000',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
left: '0mm',
|
||||||
|
bottom: '13mm',
|
||||||
|
width: '10mm',
|
||||||
|
height: '0.25pt',
|
||||||
|
backgroundColor: '#000',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Bottom Right */}
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
right: '13mm',
|
||||||
|
bottom: '0mm',
|
||||||
|
width: '0.25pt',
|
||||||
|
height: '10mm',
|
||||||
|
backgroundColor: '#000',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
right: '0mm',
|
||||||
|
bottom: '13mm',
|
||||||
|
width: '10mm',
|
||||||
|
height: '0.25pt',
|
||||||
|
backgroundColor: '#000',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
pageBleed: {
|
pageBleed: {
|
||||||
width: '91mm',
|
width: '91mm',
|
||||||
@@ -89,7 +182,7 @@ const styles = StyleSheet.create({
|
|||||||
top: 0,
|
top: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
bottom: '18mm', // Leave space for the bar below
|
bottom: '20mm', // Truck stops above the footer area to avoid text conflict
|
||||||
zIndex: 1,
|
zIndex: 1,
|
||||||
},
|
},
|
||||||
frontBackgroundImage: {
|
frontBackgroundImage: {
|
||||||
@@ -99,78 +192,76 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
frontContainer: {
|
frontContainer: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
paddingBottom: '18mm', // Push the logo up to center it in the area above the bar
|
paddingTop: '3mm', // Compensate for top bleed for perfect optical centering
|
||||||
|
paddingBottom: '20mm', // Leave space for the footer area
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
},
|
},
|
||||||
frontLogo: {
|
frontLogo: {
|
||||||
width: 175,
|
width: 145, // Reduced for a more elegant, premium look with more negative space
|
||||||
zIndex: 100,
|
zIndex: 100,
|
||||||
},
|
},
|
||||||
frontTagline: {
|
frontTagline: {
|
||||||
color: COLORS.white,
|
color: COLORS.white,
|
||||||
fontSize: 6,
|
fontSize: 5.5,
|
||||||
marginTop: 10,
|
marginTop: 8,
|
||||||
letterSpacing: 1.5,
|
letterSpacing: 2,
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
textTransform: 'uppercase',
|
textTransform: 'uppercase',
|
||||||
zIndex: 100,
|
zIndex: 100,
|
||||||
},
|
},
|
||||||
frontGreenBarBottom: {
|
frontFooterArea: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
height: '18mm',
|
height: '22mm', // Increased height to accommodate more bottom padding
|
||||||
borderTop: `2pt solid ${COLORS.green}`,
|
paddingBottom: '10mm', // Increased bottom padding to move icons further from edge
|
||||||
backgroundColor: '#000a66',
|
|
||||||
zIndex: 50,
|
zIndex: 50,
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: 5,
|
gap: 6, // Harmonious spacing to icons
|
||||||
},
|
},
|
||||||
frontIconsGroup: {
|
frontIconsGroup: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
gap: 12,
|
gap: 14, // Wider gaps between icon groups for visual clarity at small size
|
||||||
},
|
},
|
||||||
frontBrandIconWrapper: {
|
frontBrandIconWrapper: {
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: 2,
|
gap: 1.5,
|
||||||
},
|
},
|
||||||
frontBrandIcon: {
|
frontBrandIcon: {
|
||||||
width: 18,
|
width: 12, // Smaller for compact bar
|
||||||
height: 18,
|
height: 12,
|
||||||
opacity: 0.9,
|
opacity: 0.85,
|
||||||
},
|
},
|
||||||
frontBrandText: {
|
frontBrandText: {
|
||||||
fontSize: 5.5,
|
fontSize: 4, // Tiny but readable at 300dpi print
|
||||||
color: COLORS.white,
|
color: COLORS.white,
|
||||||
textTransform: 'uppercase',
|
textTransform: 'uppercase',
|
||||||
letterSpacing: 0.5,
|
letterSpacing: 0.6,
|
||||||
opacity: 0.9,
|
// Full opacity — white on blue needs to pop cleanly
|
||||||
},
|
},
|
||||||
frontTaglineBar: {
|
frontTaglineBar: {
|
||||||
color: COLORS.green,
|
color: COLORS.green,
|
||||||
fontSize: 7,
|
fontSize: 7, // Larger — as requested
|
||||||
letterSpacing: 1.5,
|
letterSpacing: 0.5, // Harmonious tracking for mixed case
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
textTransform: 'uppercase',
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// -- WHITE PAGE (Vorderseite) --
|
// -- WHITE PAGE (Vorderseite) --
|
||||||
backContainer: {
|
backContainer: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
paddingTop: '6mm',
|
paddingTop: '6mm', // Pushed down as requested
|
||||||
paddingBottom: '6mm',
|
|
||||||
paddingLeft: '6mm',
|
paddingLeft: '6mm',
|
||||||
paddingRight: '6mm',
|
paddingRight: '6mm',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
},
|
},
|
||||||
backHeader: {
|
backHeader: {
|
||||||
marginBottom: '5mm',
|
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
marginBottom: '5mm', // Generous breathing room
|
||||||
width: '100%',
|
width: '100%',
|
||||||
},
|
},
|
||||||
backLogo: {
|
backLogo: {
|
||||||
@@ -179,9 +270,9 @@ const styles = StyleSheet.create({
|
|||||||
accentLineHorizontal: {
|
accentLineHorizontal: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
height: 1.5,
|
height: 1.5,
|
||||||
backgroundColor: '#011dff', // Brand blue line
|
backgroundColor: COLORS.primary, // Brand blue line
|
||||||
marginLeft: 15,
|
marginLeft: 12,
|
||||||
marginRight: 10,
|
marginRight: 12,
|
||||||
},
|
},
|
||||||
accentDot: {
|
accentDot: {
|
||||||
width: 4,
|
width: 4,
|
||||||
@@ -201,40 +292,40 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
backBody: {
|
backBody: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
flexDirection: 'row',
|
flexDirection: 'column',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'flex-start',
|
||||||
alignItems: 'flex-start',
|
alignItems: 'flex-start',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
},
|
},
|
||||||
leftColumn: {
|
leftColumn: {
|
||||||
flex: 1,
|
width: '100%', // Take full width since right column is removed
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
paddingRight: '4mm',
|
|
||||||
},
|
},
|
||||||
nameSection: {
|
nameSection: {
|
||||||
marginBottom: '4mm',
|
marginBottom: '2.5mm', // Reduced to fix the 'Lücke' under the role
|
||||||
},
|
},
|
||||||
nameText: {
|
nameText: {
|
||||||
fontSize: 14,
|
fontSize: 13,
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
color: COLORS.neutralDark,
|
color: COLORS.primary,
|
||||||
marginBottom: 2,
|
marginBottom: 1.5,
|
||||||
letterSpacing: -0.2,
|
letterSpacing: -0.15,
|
||||||
},
|
},
|
||||||
roleText: {
|
roleText: {
|
||||||
fontSize: 7.5,
|
fontSize: 6.5,
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
color: '#011dff',
|
color: COLORS.darkGreen,
|
||||||
letterSpacing: 0.5,
|
letterSpacing: 1.2,
|
||||||
textTransform: 'uppercase',
|
textTransform: 'uppercase',
|
||||||
},
|
},
|
||||||
contactSection: {
|
contactSection: {
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
gap: 4,
|
gap: 0,
|
||||||
},
|
},
|
||||||
contactRow: {
|
contactRow: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
marginBottom: 5, // Even rhythm between contact lines
|
||||||
},
|
},
|
||||||
iconWrapper: {
|
iconWrapper: {
|
||||||
width: 12,
|
width: 12,
|
||||||
@@ -242,10 +333,11 @@ const styles = StyleSheet.create({
|
|||||||
alignItems: 'flex-start',
|
alignItems: 'flex-start',
|
||||||
},
|
},
|
||||||
contactText: {
|
contactText: {
|
||||||
fontSize: 8,
|
fontSize: 7.5,
|
||||||
color: COLORS.neutralDark,
|
color: COLORS.primary,
|
||||||
fontWeight: 'normal',
|
fontWeight: 'normal',
|
||||||
paddingTop: 1,
|
paddingTop: 0.5,
|
||||||
|
letterSpacing: 0.1,
|
||||||
},
|
},
|
||||||
rightColumn: {
|
rightColumn: {
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
@@ -259,10 +351,10 @@ const styles = StyleSheet.create({
|
|||||||
gap: 3,
|
gap: 3,
|
||||||
},
|
},
|
||||||
qrCodeWrapper: {
|
qrCodeWrapper: {
|
||||||
width: '12mm',
|
width: '10mm',
|
||||||
height: '12mm',
|
height: '10mm',
|
||||||
border: '0.75pt solid #011dff',
|
border: `0.75pt solid ${COLORS.darkGreen}`,
|
||||||
padding: 2.5,
|
padding: 2,
|
||||||
backgroundColor: '#ffffff',
|
backgroundColor: '#ffffff',
|
||||||
},
|
},
|
||||||
qrCode: {
|
qrCode: {
|
||||||
@@ -272,16 +364,16 @@ const styles = StyleSheet.create({
|
|||||||
addressFooter: {
|
addressFooter: {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'center',
|
justifyContent: 'flex-start',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
paddingTop: '2.5mm',
|
position: 'absolute',
|
||||||
marginTop: 'auto',
|
bottom: '7mm', // 3mm bleed + 4mm safe margin
|
||||||
marginBottom: '2mm', // Force extra space from the bottom edge
|
left: '6mm', // Match backContainer paddingLeft for flush alignment
|
||||||
},
|
},
|
||||||
addressFooterText: {
|
addressFooterText: {
|
||||||
fontSize: 7, // increased from 6
|
fontSize: 6.5,
|
||||||
color: COLORS.grayText,
|
color: COLORS.primary,
|
||||||
letterSpacing: 0.3,
|
letterSpacing: 0.4,
|
||||||
fontWeight: 'normal',
|
fontWeight: 'normal',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -291,7 +383,7 @@ const PhoneIcon = () => (
|
|||||||
<Svg viewBox="0 0 24 24" width={9} height={9}>
|
<Svg viewBox="0 0 24 24" width={9} height={9}>
|
||||||
<Path
|
<Path
|
||||||
d="M6.62 10.79c1.44 2.83 3.76 5.14 6.59 6.59l2.2-2.2c.27-.27.67-.36 1.02-.24 1.12.37 2.33.57 3.57.57.55 0 1 .45 1 1V20c0 .55-.45 1-1 1-9.39 0-17-7.61-17-17 0-.55.45-1 1-1h3.5c.55 0 1 .45 1 1 0 1.25.2 2.45.57 3.57.11.35.03.74-.25 1.02l-2.2 2.2z"
|
d="M6.62 10.79c1.44 2.83 3.76 5.14 6.59 6.59l2.2-2.2c.27-.27.67-.36 1.02-.24 1.12.37 2.33.57 3.57.57.55 0 1 .45 1 1V20c0 .55-.45 1-1 1-9.39 0-17-7.61-17-17 0-.55.45-1 1-1h3.5c.55 0 1 .45 1 1 0 1.25.2 2.45.57 3.57.11.35.03.74-.25 1.02l-2.2 2.2z"
|
||||||
fill="#000000"
|
fill={COLORS.primary}
|
||||||
/>
|
/>
|
||||||
</Svg>
|
</Svg>
|
||||||
);
|
);
|
||||||
@@ -300,7 +392,7 @@ const EmailIcon = () => (
|
|||||||
<Svg viewBox="0 0 24 24" width={9} height={9}>
|
<Svg viewBox="0 0 24 24" width={9} height={9}>
|
||||||
<Path
|
<Path
|
||||||
d="M20 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 4l-8 5-8-5V6l8 5 8-5v2z"
|
d="M20 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 4l-8 5-8-5V6l8 5 8-5v2z"
|
||||||
fill="#000000"
|
fill={COLORS.primary}
|
||||||
/>
|
/>
|
||||||
</Svg>
|
</Svg>
|
||||||
);
|
);
|
||||||
@@ -309,7 +401,7 @@ const GlobeIcon = () => (
|
|||||||
<Svg viewBox="0 0 24 24" width={9} height={9}>
|
<Svg viewBox="0 0 24 24" width={9} height={9}>
|
||||||
<Path
|
<Path
|
||||||
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z"
|
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z"
|
||||||
fill="#000000"
|
fill={COLORS.primary}
|
||||||
/>
|
/>
|
||||||
</Svg>
|
</Svg>
|
||||||
);
|
);
|
||||||
@@ -318,7 +410,7 @@ const LocationIcon = () => (
|
|||||||
<Svg viewBox="0 0 24 24" width={9} height={9}>
|
<Svg viewBox="0 0 24 24" width={9} height={9}>
|
||||||
<Path
|
<Path
|
||||||
d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z"
|
d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z"
|
||||||
fill="#000000"
|
fill={COLORS.primary}
|
||||||
/>
|
/>
|
||||||
</Svg>
|
</Svg>
|
||||||
);
|
);
|
||||||
@@ -327,7 +419,7 @@ const ScanIcon = () => (
|
|||||||
<Svg viewBox="0 0 24 24" width={9} height={9}>
|
<Svg viewBox="0 0 24 24" width={9} height={9}>
|
||||||
<Path
|
<Path
|
||||||
d="M3 3h6v2H5v4H3V3zm18 0h-6v2h4v4h2V3zM3 21h6v-2H5v-4H3v6zm18 0h-6v-2h4v-4h2v6zM7 7h10v10H7V7z"
|
d="M3 3h6v2H5v4H3V3zm18 0h-6v2h4v4h2V3zM3 21h6v-2H5v-4H3v6zm18 0h-6v-2h4v-4h2v6zM7 7h10v10H7V7z"
|
||||||
fill="#000000"
|
fill={COLORS.primary}
|
||||||
/>
|
/>
|
||||||
</Svg>
|
</Svg>
|
||||||
);
|
);
|
||||||
@@ -363,6 +455,7 @@ const VectorLogo = ({
|
|||||||
style,
|
style,
|
||||||
overrideColor,
|
overrideColor,
|
||||||
overrideWhiteColor,
|
overrideWhiteColor,
|
||||||
|
overrideBlackColor,
|
||||||
pathOverrides,
|
pathOverrides,
|
||||||
solidPaths,
|
solidPaths,
|
||||||
solidBackgroundColor = '#000a66',
|
solidBackgroundColor = '#000a66',
|
||||||
@@ -371,6 +464,7 @@ const VectorLogo = ({
|
|||||||
style: any;
|
style: any;
|
||||||
overrideColor?: string;
|
overrideColor?: string;
|
||||||
overrideWhiteColor?: string;
|
overrideWhiteColor?: string;
|
||||||
|
overrideBlackColor?: string;
|
||||||
pathOverrides?: Record<number, string>;
|
pathOverrides?: Record<number, string>;
|
||||||
solidPaths?: number[];
|
solidPaths?: number[];
|
||||||
solidBackgroundColor?: string;
|
solidBackgroundColor?: string;
|
||||||
@@ -400,6 +494,11 @@ const VectorLogo = ({
|
|||||||
(p.fill === 'white' || p.fill === '#ffffff' || p.fill === '#fff')
|
(p.fill === 'white' || p.fill === '#ffffff' || p.fill === '#fff')
|
||||||
) {
|
) {
|
||||||
fill = overrideWhiteColor;
|
fill = overrideWhiteColor;
|
||||||
|
} else if (
|
||||||
|
overrideBlackColor &&
|
||||||
|
(p.fill === 'black' || p.fill === '#000000' || p.fill === '#0a0a0a' || p.fill === '#000')
|
||||||
|
) {
|
||||||
|
fill = overrideBlackColor;
|
||||||
}
|
}
|
||||||
return <Path key={i} d={d} fill={fill} />;
|
return <Path key={i} d={d} fill={fill} />;
|
||||||
})}
|
})}
|
||||||
@@ -632,7 +731,7 @@ export const IconHighVoltage = ({ style, color = 'white' }: { style: any; color?
|
|||||||
style={{ fill: 'none', stroke: color, strokeWidth: '0.33px' }}
|
style={{ fill: 'none', stroke: color, strokeWidth: '0.33px' }}
|
||||||
/>
|
/>
|
||||||
<Path
|
<Path
|
||||||
d="M28.597,16.426c-0,-1.03 -0.837,-1.867 -1.868,-1.867c-1.03,0 -1.867,0.837 -1.867,1.867c0,1.031 0.837,1.868 1.867,1.868c1.031,-0 1.868,-0.837 1.868,-1.868Z"
|
d="M28.597,16.426c-0,-1.03 -0.837,-1.868 -1.868,-1.867c-1.03,0 -1.867,0.837 -1.867,1.867c0,1.031 0.837,1.868 1.867,1.868c1.031,-0 1.868,-0.837 1.868,-1.868Z"
|
||||||
style={{ fill: 'none', stroke: color, strokeWidth: '0.33px' }}
|
style={{ fill: 'none', stroke: color, strokeWidth: '0.33px' }}
|
||||||
/>
|
/>
|
||||||
<Path
|
<Path
|
||||||
@@ -693,6 +792,8 @@ export const PDFBusinessCard: React.FC<PDFBusinessCardProps> = ({
|
|||||||
qrCodeDataUrl,
|
qrCodeDataUrl,
|
||||||
variant = 'blue',
|
variant = 'blue',
|
||||||
isViaprinto = false,
|
isViaprinto = false,
|
||||||
|
withTruck = true,
|
||||||
|
withCropMarks = false,
|
||||||
}) => {
|
}) => {
|
||||||
const isWhiteVariant = variant === 'white' || variant === 'black';
|
const isWhiteVariant = variant === 'white' || variant === 'black';
|
||||||
|
|
||||||
@@ -721,14 +822,37 @@ export const PDFBusinessCard: React.FC<PDFBusinessCardProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getBackLogoPaths = () => {
|
const getBackLogoPaths = () => {
|
||||||
if (variant === 'black') return logoBlackWithTaglinePaths;
|
// We use the original black logo to preserve the exact colors of the Truck and KLZ.
|
||||||
return logoBlueWithTaglinePaths;
|
// The tagline will be hidden via pathOverrides in the JSX.
|
||||||
|
return logoBlackWithTaglinePaths;
|
||||||
};
|
};
|
||||||
|
|
||||||
const bgImagePath = isWhiteVariant
|
const bgImagePath = isWhiteVariant
|
||||||
? path.join(process.cwd(), 'public', 'media', 'cable_drums_truck.png')
|
? path.join(process.cwd(), 'public', 'media', 'cable_drums_truck.png')
|
||||||
: path.join(process.cwd(), 'public', 'cable_drums_bg.png');
|
: path.join(process.cwd(), 'public', 'cable_drums_bg.png');
|
||||||
|
|
||||||
|
const mmToPt = (mm: number) => (mm * 72) / 25.4;
|
||||||
|
|
||||||
|
const renderPageWrapper = (children: React.ReactNode, bleedStyle: any) => {
|
||||||
|
if (withCropMarks) {
|
||||||
|
return (
|
||||||
|
<Page size={[315, 230]} style={{ backgroundColor: '#ffffff' }}>
|
||||||
|
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
|
||||||
|
<CropMarks />
|
||||||
|
<View style={[bleedStyle, { overflow: 'hidden', position: 'relative' }]}>
|
||||||
|
{children}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Page size={[mmToPt(91), mmToPt(61)]} style={bleedStyle}>
|
||||||
|
{children}
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Document
|
<Document
|
||||||
title={`Business Card - ${person.name}`}
|
title={`Business Card - ${person.name}`}
|
||||||
@@ -736,100 +860,87 @@ export const PDFBusinessCard: React.FC<PDFBusinessCardProps> = ({
|
|||||||
creator="KLZ Print Engine"
|
creator="KLZ Print Engine"
|
||||||
>
|
>
|
||||||
{/* VORDERSEITE (Blue page with logo, truck and tagline) */}
|
{/* VORDERSEITE (Blue page with logo, truck and tagline) */}
|
||||||
<Page
|
{renderPageWrapper(
|
||||||
size={[258, 173]}
|
<>
|
||||||
style={[styles.pageBleed, isWhiteVariant && { backgroundColor: '#ffffff' }]}
|
<View style={styles.frontFooterArea}>
|
||||||
>
|
<Text style={[styles.frontTaglineBar, isWhiteVariant && { color: COLORS.black }]}>
|
||||||
<View style={styles.truckWrapper}>
|
Von 0,6/1,0 kV bis 64/110 kV
|
||||||
<TruckBlueprint
|
</Text>
|
||||||
style={styles.frontBackgroundImage}
|
<View style={styles.frontIconsGroup}>
|
||||||
opacity={isWhiteVariant ? 0.2 : 0.25}
|
<View style={styles.frontBrandIconWrapper}>
|
||||||
backgroundColor={isWhiteVariant ? '#ffffff' : '#011dff'}
|
<IconLowVoltage
|
||||||
/>
|
style={styles.frontBrandIcon}
|
||||||
</View>
|
color={isWhiteVariant ? COLORS.black : 'white'}
|
||||||
<View
|
/>
|
||||||
style={[
|
<Text style={[styles.frontBrandText, isWhiteVariant && { color: COLORS.black }]}>
|
||||||
styles.frontGreenBarBottom,
|
Niederspannung
|
||||||
isWhiteVariant && { backgroundColor: '#ffffff', borderTopColor: COLORS.darkGreen },
|
</Text>
|
||||||
]}
|
</View>
|
||||||
>
|
<View style={styles.frontBrandIconWrapper}>
|
||||||
<Text style={[styles.frontTaglineBar, isWhiteVariant && { color: COLORS.black }]}>
|
<IconMediumVoltage
|
||||||
VON 0,6/1,0 KV BIS 64/110 KV
|
style={styles.frontBrandIcon}
|
||||||
</Text>
|
color={isWhiteVariant ? COLORS.black : 'white'}
|
||||||
<View style={styles.frontIconsGroup}>
|
/>
|
||||||
<View style={styles.frontBrandIconWrapper}>
|
<Text style={[styles.frontBrandText, isWhiteVariant && { color: COLORS.black }]}>
|
||||||
<IconLowVoltage
|
Mittelspannung
|
||||||
style={styles.frontBrandIcon}
|
</Text>
|
||||||
color={isWhiteVariant ? COLORS.black : 'white'}
|
</View>
|
||||||
/>
|
<View style={styles.frontBrandIconWrapper}>
|
||||||
<Text style={[styles.frontBrandText, isWhiteVariant && { color: COLORS.black }]}>
|
<IconHighVoltage
|
||||||
Niederspannung
|
style={styles.frontBrandIcon}
|
||||||
</Text>
|
color={isWhiteVariant ? COLORS.black : 'white'}
|
||||||
</View>
|
/>
|
||||||
<View style={styles.frontBrandIconWrapper}>
|
<Text style={[styles.frontBrandText, isWhiteVariant && { color: COLORS.black }]}>
|
||||||
<IconMediumVoltage
|
Hochspannung
|
||||||
style={styles.frontBrandIcon}
|
</Text>
|
||||||
color={isWhiteVariant ? COLORS.black : 'white'}
|
</View>
|
||||||
/>
|
|
||||||
<Text style={[styles.frontBrandText, isWhiteVariant && { color: COLORS.black }]}>
|
|
||||||
Mittelspannung
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
<View style={styles.frontBrandIconWrapper}>
|
|
||||||
<IconHighVoltage
|
|
||||||
style={styles.frontBrandIcon}
|
|
||||||
color={isWhiteVariant ? COLORS.black : 'white'}
|
|
||||||
/>
|
|
||||||
<Text style={[styles.frontBrandText, isWhiteVariant && { color: COLORS.black }]}>
|
|
||||||
Hochspannung
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View style={styles.frontBrandIconWrapper}>
|
<View style={styles.frontBrandIconWrapper}>
|
||||||
<IconSolar
|
<IconSolar
|
||||||
style={styles.frontBrandIcon}
|
style={styles.frontBrandIcon}
|
||||||
color={isWhiteVariant ? COLORS.black : 'white'}
|
color={isWhiteVariant ? COLORS.black : 'white'}
|
||||||
/>
|
/>
|
||||||
<Text style={[styles.frontBrandText, isWhiteVariant && { color: COLORS.black }]}>
|
<Text style={[styles.frontBrandText, isWhiteVariant && { color: COLORS.black }]}>
|
||||||
Solar
|
Solar
|
||||||
</Text>
|
</Text>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
<View style={styles.frontContainer}>
|
||||||
<View style={styles.frontContainer}>
|
{/* Base Logo: text is white, truck is green. Fill KLZ background to hide truck, keep outline white */}
|
||||||
{/* Base Logo: text is white, truck is green. Fill KLZ background to hide truck, keep outline white */}
|
|
||||||
<VectorLogo
|
|
||||||
paths={getFrontLogoPaths()}
|
|
||||||
style={styles.frontLogo}
|
|
||||||
solidPaths={KLZ_PATHS}
|
|
||||||
solidBackgroundColor={isWhiteVariant ? '#ffffff' : '#011dff'}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Letterpress Mask Layer: We use #ff00fe as a marker color. Only the "KLZ" letters! */}
|
|
||||||
{isViaprinto && (
|
|
||||||
<VectorLogo
|
<VectorLogo
|
||||||
paths={getFrontLogoPaths().filter((_, i) => KLZ_PATHS.includes(i))}
|
paths={getFrontLogoPaths()}
|
||||||
style={[styles.frontLogo, { position: 'absolute' }]}
|
style={styles.frontLogo}
|
||||||
overrideColor="#ff00fe"
|
solidPaths={KLZ_PATHS}
|
||||||
solidPaths={KLZ_PATHS.map((_, i) => i)}
|
solidBackgroundColor={isWhiteVariant ? '#ffffff' : COLORS.primary}
|
||||||
/>
|
/>
|
||||||
)}
|
</View>
|
||||||
</View>
|
</>,
|
||||||
</Page>
|
[styles.pageBleed, isWhiteVariant && { backgroundColor: '#ffffff' }],
|
||||||
|
)}
|
||||||
|
|
||||||
{/* VORDERSEITE (White page with contact info) */}
|
{renderPageWrapper(
|
||||||
<Page size={[258, 173]} style={styles.pageBleedBack}>
|
|
||||||
<View style={styles.backContainer}>
|
<View style={styles.backContainer}>
|
||||||
<View style={styles.backHeader}>
|
<View style={styles.backHeader}>
|
||||||
<VectorLogo
|
<VectorLogo
|
||||||
paths={getBackLogoPaths()}
|
paths={getBackLogoPaths()}
|
||||||
style={styles.backLogo}
|
style={styles.backLogo}
|
||||||
overrideWhiteColor={'#011dff'}
|
overrideBlackColor={COLORS.primary}
|
||||||
pathOverrides={KLZ_PATHS.reduce((acc, idx) => ({ ...acc, [idx]: 'none' }), {})}
|
pathOverrides={{
|
||||||
|
...(KLZ_PATHS.reduce(
|
||||||
|
(acc: Record<number, string>, idx) => ({ ...acc, [idx]: 'none' }),
|
||||||
|
{} as Record<number, string>,
|
||||||
|
) as any),
|
||||||
|
// Paths 0-24 are the "Cables for a greener tomorrow" tagline in the 40-path logo.
|
||||||
|
...(Array.from({ length: 25 }).reduce(
|
||||||
|
(acc: Record<number, string>, _, i) => ({ ...acc, [i]: 'none' }),
|
||||||
|
{} as Record<number, string>,
|
||||||
|
) as any),
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<View style={styles.accentLineHorizontal} />
|
<View style={styles.accentLineHorizontal} />
|
||||||
<View style={styles.qrContainer}>
|
<View style={styles.qrContainer}>
|
||||||
<View style={styles.qrCodeWrapper}>
|
<View style={[styles.qrCodeWrapper, { borderColor: COLORS.darkGreen }]}>
|
||||||
<Image src={qrCodeDataUrl} style={styles.qrCode} />
|
<Image src={qrCodeDataUrl} style={styles.qrCode} />
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@@ -853,7 +964,7 @@ export const PDFBusinessCard: React.FC<PDFBusinessCardProps> = ({
|
|||||||
<View style={styles.iconWrapper}>
|
<View style={styles.iconWrapper}>
|
||||||
<EmailIcon />
|
<EmailIcon />
|
||||||
</View>
|
</View>
|
||||||
<Text style={styles.contactText}>{person.email}</Text>
|
<Text style={styles.contactText}>{person.email.toLowerCase()}</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.contactRow}>
|
<View style={styles.contactRow}>
|
||||||
<View style={styles.iconWrapper}>
|
<View style={styles.iconWrapper}>
|
||||||
@@ -864,14 +975,15 @@ export const PDFBusinessCard: React.FC<PDFBusinessCardProps> = ({
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
|
||||||
|
|
||||||
<View style={styles.addressFooter}>
|
<View style={styles.addressFooter}>
|
||||||
<Text style={styles.addressFooterText}>
|
<Text style={styles.addressFooterText}>
|
||||||
KLZ Vertriebs GmbH · Raiffeisenstraße 22 · 73630 Remshalden
|
KLZ Vertriebs GmbH · Raiffeisenstraße 22 · 73630 Remshalden
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</Page>
|
</View>,
|
||||||
|
styles.pageBleedBack,
|
||||||
|
)}
|
||||||
</Document>
|
</Document>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -329,6 +329,42 @@ const renderLexicalNode = (node: any, idx: number): React.ReactNode => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const renderMarkdown = (markdown: string) => {
|
||||||
|
const blocks = markdown.split('\n\n').filter((b) => b.trim());
|
||||||
|
return blocks.map((block, idx) => {
|
||||||
|
if (block.startsWith('### ')) {
|
||||||
|
return (
|
||||||
|
<Text key={idx} style={styles.heading3} wrap={false} minPresenceAhead={100}>
|
||||||
|
{block.replace('### ', '').trim()}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (block.startsWith('## ')) {
|
||||||
|
return (
|
||||||
|
<Text key={idx} style={styles.heading2} wrap={false} minPresenceAhead={100}>
|
||||||
|
{block.replace('## ', '').trim()}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (block.startsWith('# ')) {
|
||||||
|
return (
|
||||||
|
<View key={idx} style={styles.heading1Wrapper} wrap={false} minPresenceAhead={100}>
|
||||||
|
<Text style={styles.heading1}>{block.replace('# ', '').trim()}</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore block components like <Block ... />
|
||||||
|
if (block.startsWith('<Block')) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Text key={idx} style={styles.paragraph} minPresenceAhead={15}>
|
||||||
|
{block.trim()}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
interface PDFPageProps {
|
interface PDFPageProps {
|
||||||
page: any;
|
page: any;
|
||||||
locale?: string;
|
locale?: string;
|
||||||
@@ -360,9 +396,11 @@ export const PDFPage: React.FC<PDFPageProps> = ({ page, locale = 'de' }) => {
|
|||||||
|
|
||||||
<View style={styles.content}>
|
<View style={styles.content}>
|
||||||
<View>
|
<View>
|
||||||
{page.content?.root?.children?.map((node: any, i: number) =>
|
{typeof page.content === 'string'
|
||||||
renderLexicalNode(node, i),
|
? renderMarkdown(page.content)
|
||||||
)}
|
: page.content?.root?.children?.map((node: any, i: number) =>
|
||||||
|
renderLexicalNode(node, i),
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
|||||||
@@ -39,11 +39,8 @@ export async function getProductBySlug(slug: string, locale: string): Promise<Pr
|
|||||||
const fileContent = await fs.readFile(filePath, 'utf-8');
|
const fileContent = await fs.readFile(filePath, 'utf-8');
|
||||||
const { data, content } = matter(fileContent);
|
const { data, content } = matter(fileContent);
|
||||||
|
|
||||||
// Fix MDX data props dropped by next-mdx-remote
|
const { fixMdxDataProps } = await import('./mdx-utils');
|
||||||
// Payload serializes as data={{"items":...}} which Acorn treats as a Block statement.
|
const fixedContent = fixMdxDataProps(content);
|
||||||
const fixedContent = content.replace(/data=\{\{([\s\S]*?)\}\}/g, (match, p1) => {
|
|
||||||
return 'data="{' + p1.replace(/"/g, '"') + '}"';
|
|
||||||
});
|
|
||||||
|
|
||||||
let parsedContent = fixedContent;
|
let parsedContent = fixedContent;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -56,6 +56,11 @@ export class GlitchtipErrorReportingService implements ErrorReportingService {
|
|||||||
tracesSampleRate: this.options.tracesSampleRate ?? 0.1,
|
tracesSampleRate: this.options.tracesSampleRate ?? 0.1,
|
||||||
replaysOnErrorSampleRate: 1.0,
|
replaysOnErrorSampleRate: 1.0,
|
||||||
replaysSessionSampleRate: 0.1,
|
replaysSessionSampleRate: 0.1,
|
||||||
|
ignoreErrors: [
|
||||||
|
'ChunkLoadError',
|
||||||
|
'Failed to fetch dynamically imported module',
|
||||||
|
'Failed to find Server Action',
|
||||||
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return Sentry;
|
return Sentry;
|
||||||
|
|||||||
@@ -116,7 +116,7 @@
|
|||||||
"prepare": "husky",
|
"prepare": "husky",
|
||||||
"preinstall": "npx only-allow pnpm"
|
"preinstall": "npx only-allow pnpm"
|
||||||
},
|
},
|
||||||
"version": "2.3.23",
|
"version": "2.3.34",
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"onlyBuiltDependencies": [
|
"onlyBuiltDependencies": [
|
||||||
"@parcel/watcher",
|
"@parcel/watcher",
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 6.4 KiB |
10
public/v/johannesgleich.vcf
Normal file
10
public/v/johannesgleich.vcf
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
BEGIN:VCARD
|
||||||
|
VERSION:3.0
|
||||||
|
N:Gleich;Johannes;;;
|
||||||
|
FN:Johannes Gleich
|
||||||
|
ORG:KLZ Vertriebs GmbH
|
||||||
|
TITLE:Senior Key Account Manager
|
||||||
|
TEL;TYPE=WORK,VOICE:+49 172 739 1109
|
||||||
|
EMAIL;TYPE=PREF,INTERNET:johannes.gleich@klz-cables.com
|
||||||
|
URL:https://klz-cables.com
|
||||||
|
END:VCARD
|
||||||
10
public/v/klausmintel.vcf
Normal file
10
public/v/klausmintel.vcf
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
BEGIN:VCARD
|
||||||
|
VERSION:3.0
|
||||||
|
N:Mintel;Klaus;;;
|
||||||
|
FN:Klaus Mintel
|
||||||
|
ORG:KLZ Vertriebs GmbH
|
||||||
|
TITLE:Managing Director
|
||||||
|
TEL;TYPE=WORK,VOICE:+49 151 1775 2873
|
||||||
|
EMAIL;TYPE=PREF,INTERNET:klaus.mintel@klz-cables.com
|
||||||
|
URL:https://klz-cables.com
|
||||||
|
END:VCARD
|
||||||
10
public/v/michaelbodemer.vcf
Normal file
10
public/v/michaelbodemer.vcf
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
BEGIN:VCARD
|
||||||
|
VERSION:3.0
|
||||||
|
N:Bodemer;Michael;;;
|
||||||
|
FN:Michael Bodemer
|
||||||
|
ORG:KLZ Vertriebs GmbH
|
||||||
|
TITLE:Owner & Managing Director
|
||||||
|
TEL;TYPE=WORK,VOICE:+49 151 462 467 98
|
||||||
|
EMAIL;TYPE=PREF,INTERNET:michael.bodemer@klz-cables.com
|
||||||
|
URL:https://klz-cables.com
|
||||||
|
END:VCARD
|
||||||
@@ -26,7 +26,7 @@ const CONFIG = {
|
|||||||
const PEOPLE: PersonData[] = [
|
const PEOPLE: PersonData[] = [
|
||||||
{
|
{
|
||||||
name: 'Michael Bodemer',
|
name: 'Michael Bodemer',
|
||||||
role: 'Managing Director',
|
role: 'Owner & Managing Director',
|
||||||
phone: '+49 151 462 467 98',
|
phone: '+49 151 462 467 98',
|
||||||
email: 'michael.bodemer@klz-cables.com',
|
email: 'michael.bodemer@klz-cables.com',
|
||||||
},
|
},
|
||||||
@@ -34,7 +34,7 @@ const PEOPLE: PersonData[] = [
|
|||||||
name: 'Klaus Mintel',
|
name: 'Klaus Mintel',
|
||||||
role: 'Managing Director',
|
role: 'Managing Director',
|
||||||
phone: '+49 151 1775 2873',
|
phone: '+49 151 1775 2873',
|
||||||
email: 'Klaus.mintel@klz-cables.com',
|
email: 'klaus.mintel@klz-cables.com',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Johannes Gleich',
|
name: 'Johannes Gleich',
|
||||||
@@ -52,24 +52,26 @@ async function generateBusinessCards() {
|
|||||||
for (const person of PEOPLE) {
|
for (const person of PEOPLE) {
|
||||||
console.log(`\nGenerating Business Card for: ${person.name}`);
|
console.log(`\nGenerating Business Card for: ${person.name}`);
|
||||||
|
|
||||||
// Generate VCard QR Code
|
|
||||||
const lastName = person.name.split(' ').slice(1).join(' ');
|
const lastName = person.name.split(' ').slice(1).join(' ');
|
||||||
const firstName = person.name.split(' ')[0];
|
const firstName = person.name.split(' ')[0];
|
||||||
const vcard = `BEGIN:VCARD
|
|
||||||
VERSION:3.0
|
|
||||||
N:${lastName};${firstName};;;
|
|
||||||
FN:${person.name}
|
|
||||||
ORG:KLZ Vertriebs GmbH
|
|
||||||
TITLE:${person.role}
|
|
||||||
TEL;TYPE=WORK,VOICE:${person.phone}
|
|
||||||
EMAIL;TYPE=PREF,INTERNET:${person.email}
|
|
||||||
URL:https://klz-cables.com
|
|
||||||
END:VCARD`;
|
|
||||||
|
|
||||||
const qrCodeDataUrl = await QRCode.toDataURL(vcard, {
|
// To make the QR code extremely coarse (minimalist), we cannot encode all contact details into it directly.
|
||||||
|
// Instead, we create a physical .vcf file and encode ONLY its URL into the QR Code (e.g. 35 chars vs 140 chars).
|
||||||
|
const shortId = person.name.toLowerCase().replace(/[^a-z0-9]/g, ''); // "michaelbodemer"
|
||||||
|
const vcfUrl = `https://klz-cables.com/v/${shortId}.vcf`;
|
||||||
|
|
||||||
|
const vcardFull = `BEGIN:VCARD\r\nVERSION:3.0\r\nN:${lastName};${firstName};;;\r\nFN:${person.name}\r\nORG:KLZ Vertriebs GmbH\r\nTITLE:${person.role}\r\nTEL;TYPE=WORK,VOICE:${person.phone}\r\nEMAIL;TYPE=PREF,INTERNET:${person.email}\r\nURL:https://klz-cables.com\r\nEND:VCARD`;
|
||||||
|
|
||||||
|
// Save the physical .vcf file so it can be served by Next.js from /public/v/
|
||||||
|
const vcardDir = path.join(process.cwd(), 'public/v');
|
||||||
|
if (!fs.existsSync(vcardDir)) fs.mkdirSync(vcardDir, { recursive: true });
|
||||||
|
fs.writeFileSync(path.join(vcardDir, `${shortId}.vcf`), vcardFull);
|
||||||
|
|
||||||
|
const qrCodeDataUrl = await QRCode.toDataURL(vcfUrl, {
|
||||||
width: 400,
|
width: 400,
|
||||||
margin: 0,
|
margin: 0,
|
||||||
color: { dark: '#000000', light: '#ffffff' },
|
errorCorrectionLevel: 'L', // Low error correction = less dots
|
||||||
|
color: { dark: '#000a66', light: '#ffffff' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const safeName = person.name.replace(/\s+/g, '_');
|
const safeName = person.name.replace(/\s+/g, '_');
|
||||||
@@ -94,7 +96,7 @@ END:VCARD`;
|
|||||||
// Convert fonts to outlines via ghostscript
|
// Convert fonts to outlines via ghostscript
|
||||||
const gsCommand = `gs -dNoOutputFonts -sDEVICE=pdfwrite -o "${finalPath}" "${tempPath}"`;
|
const gsCommand = `gs -dNoOutputFonts -sDEVICE=pdfwrite -o "${finalPath}" "${tempPath}"`;
|
||||||
execSync(gsCommand, { stdio: 'inherit' });
|
execSync(gsCommand, { stdio: 'inherit' });
|
||||||
fs.unlinkSync(tempPath);
|
// fs.unlinkSync(tempPath);
|
||||||
console.log(`✓ Final Print File generated (Fonts to Paths, RGB preserved): ${finalFileName}`);
|
console.log(`✓ Final Print File generated (Fonts to Paths, RGB preserved): ${finalFileName}`);
|
||||||
|
|
||||||
// 2. VIAPRINTO PDF (Letterpress mask, CMYK)
|
// 2. VIAPRINTO PDF (Letterpress mask, CMYK)
|
||||||
@@ -119,10 +121,79 @@ END:VCARD`;
|
|||||||
try {
|
try {
|
||||||
execSync(cmykCommand, { stdio: 'inherit' });
|
execSync(cmykCommand, { stdio: 'inherit' });
|
||||||
fs.unlinkSync(tempViaprintoPath);
|
fs.unlinkSync(tempViaprintoPath);
|
||||||
console.log(`✓ CMYK Print File generated: ${finalFileNameCMYK}`);
|
console.log(`[INFO] Injecting PANTONE 289 C Spot Color...`);
|
||||||
|
const injectPantoneCommand = `npx tsx scripts/inject-pantone.ts "${finalPathCMYK}"`;
|
||||||
|
execSync(injectPantoneCommand, { stdio: 'inherit' });
|
||||||
|
|
||||||
|
console.log(`✓ 5-Color Print File generated: ${finalFileNameCMYK}`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`❌ CMYK conversion failed! Is Ghostscript installed?`);
|
console.error(`❌ CMYK conversion failed! Is Ghostscript installed?`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 2.5 SAXOPRINT PDF (Pure CMYK, no mask, no Pantone)
|
||||||
|
console.log(`- Rendering Saxoprint PDF (Pure CMYK)...`);
|
||||||
|
const tempSaxoprintPath = path.join(CONFIG.outputDir, `temp_saxoprint_${safeName}.pdf`);
|
||||||
|
buffer = await renderToBuffer(
|
||||||
|
React.createElement(PDFBusinessCard, {
|
||||||
|
person,
|
||||||
|
qrCodeDataUrl,
|
||||||
|
variant: 'blue',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
fs.writeFileSync(tempSaxoprintPath, buffer);
|
||||||
|
|
||||||
|
const finalFileNameSaxoCMYK = `KLZ_Visitenkarte_${safeName}_Saxoprint_CMYK_91x61mm_3mmBleed.pdf`;
|
||||||
|
const finalPathSaxoCMYK = path.join(CONFIG.outputDir, finalFileNameSaxoCMYK);
|
||||||
|
|
||||||
|
console.log(`- Converting to CMYK for Saxoprint...`);
|
||||||
|
const cmykSaxoCommand = `bash "${CONFIG.ghostscriptScript}" "${tempSaxoprintPath}" "${finalPathSaxoCMYK}"`;
|
||||||
|
try {
|
||||||
|
execSync(cmykSaxoCommand, { stdio: 'inherit' });
|
||||||
|
fs.unlinkSync(tempSaxoprintPath);
|
||||||
|
console.log(`✓ 4-Color CMYK Print File generated: ${finalFileNameSaxoCMYK}`);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`❌ CMYK conversion failed! Is Ghostscript installed?`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. NO TRUCK PDF (RGB)
|
||||||
|
console.log(`- Rendering No Truck Variant (RGB)...`);
|
||||||
|
const noTruckFileName = `KLZ_Visitenkarte_${safeName}_NoTruck.pdf`;
|
||||||
|
const tempNoTruckPath = path.join(CONFIG.outputDir, `temp_${noTruckFileName}`);
|
||||||
|
const finalNoTruckPath = path.join(CONFIG.outputDir, noTruckFileName);
|
||||||
|
|
||||||
|
buffer = await renderToBuffer(
|
||||||
|
React.createElement(PDFBusinessCard, {
|
||||||
|
person,
|
||||||
|
qrCodeDataUrl,
|
||||||
|
variant: 'blue',
|
||||||
|
isViaprinto: false,
|
||||||
|
withTruck: false,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
fs.writeFileSync(tempNoTruckPath, buffer);
|
||||||
|
const gsNoTruckCommand = `gs -dNoOutputFonts -sDEVICE=pdfwrite -o "${finalNoTruckPath}" "${tempNoTruckPath}"`;
|
||||||
|
execSync(gsNoTruckCommand, { stdio: 'inherit' });
|
||||||
|
fs.unlinkSync(tempNoTruckPath);
|
||||||
|
// 4. CROP MARKS PDF (RGB)
|
||||||
|
console.log(`- Rendering Crop Marks Variant (RGB)...`);
|
||||||
|
const cropMarksFileName = `KLZ_Visitenkarte_${safeName}_CropMarks.pdf`;
|
||||||
|
const tempCropMarksPath = path.join(CONFIG.outputDir, `temp_${cropMarksFileName}`);
|
||||||
|
const finalCropMarksPath = path.join(CONFIG.outputDir, cropMarksFileName);
|
||||||
|
|
||||||
|
buffer = await renderToBuffer(
|
||||||
|
React.createElement(PDFBusinessCard, {
|
||||||
|
person,
|
||||||
|
qrCodeDataUrl,
|
||||||
|
variant: 'blue',
|
||||||
|
isViaprinto: false,
|
||||||
|
withCropMarks: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
fs.writeFileSync(tempCropMarksPath, buffer);
|
||||||
|
const gsCropMarksCommand = `gs -dNoOutputFonts -sDEVICE=pdfwrite -o "${finalCropMarksPath}" "${tempCropMarksPath}"`;
|
||||||
|
execSync(gsCropMarksCommand, { stdio: 'inherit' });
|
||||||
|
fs.unlinkSync(tempCropMarksPath);
|
||||||
|
console.log(`✓ Crop Marks Print File generated: ${cropMarksFileName}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
105
scripts/inject-pantone.ts
Normal file
105
scripts/inject-pantone.ts
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
import { PDFDocument, PDFName, decodePDFRawStream, PDFRawStream } from 'pdf-lib';
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
const inputPath = process.argv[2];
|
||||||
|
const outputPath = process.argv[3] || inputPath;
|
||||||
|
|
||||||
|
if (!inputPath || !fs.existsSync(inputPath)) {
|
||||||
|
console.error(`[ERROR] Input file not found: ${inputPath}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[INFO] Injecting PANTONE 289 C into: ${path.basename(inputPath)}`);
|
||||||
|
const pdfBytes = fs.readFileSync(inputPath);
|
||||||
|
const pdfDoc = await PDFDocument.load(pdfBytes);
|
||||||
|
|
||||||
|
// CMYK tint transform for PANTONE 289 C.
|
||||||
|
// Approximation: 100C, 64M, 0Y, 60K (1.0 0.64 0.0 0.60)
|
||||||
|
// The function maps an input x (0 to 1) to CMYK: x*1.0, x*0.64, x*0.0, x*0.60
|
||||||
|
const tintTransform = pdfDoc.context.obj({
|
||||||
|
FunctionType: 2,
|
||||||
|
Domain: [0, 1],
|
||||||
|
C0: [0, 0, 0, 0],
|
||||||
|
C1: [1.0, 0.64, 0.0, 0.6],
|
||||||
|
N: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create the /Separation color space array
|
||||||
|
const pantoneCS = pdfDoc.context.obj([
|
||||||
|
PDFName.of('Separation'),
|
||||||
|
PDFName.of('PANTONE#20289#20C'),
|
||||||
|
PDFName.of('DeviceCMYK'),
|
||||||
|
tintTransform,
|
||||||
|
]);
|
||||||
|
const csRef = pdfDoc.context.register(pantoneCS);
|
||||||
|
|
||||||
|
const pages = pdfDoc.getPages();
|
||||||
|
for (const page of pages) {
|
||||||
|
let resources = page.node.Resources();
|
||||||
|
if (!resources) {
|
||||||
|
resources = pdfDoc.context.obj({});
|
||||||
|
page.node.set(PDFName.of('Resources'), resources);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure ColorSpace dict exists
|
||||||
|
let colorSpaces = resources.get(PDFName.of('ColorSpace')) as any;
|
||||||
|
if (!colorSpaces) {
|
||||||
|
colorSpaces = pdfDoc.context.obj({});
|
||||||
|
resources.set(PDFName.of('ColorSpace'), colorSpaces);
|
||||||
|
}
|
||||||
|
// Register the PANTONE color space as /CSPantone in the page's resources
|
||||||
|
colorSpaces.set(PDFName.of('CSPantone'), csRef);
|
||||||
|
|
||||||
|
// Read all content streams
|
||||||
|
const contents = page.node.Contents();
|
||||||
|
let streams: any[] = [];
|
||||||
|
if (contents) {
|
||||||
|
const resolvedContents = pdfDoc.context.lookup(contents);
|
||||||
|
if (resolvedContents instanceof PDFRawStream) {
|
||||||
|
streams = [resolvedContents];
|
||||||
|
} else if (resolvedContents.constructor.name === 'PDFArray') {
|
||||||
|
const arr = resolvedContents as any;
|
||||||
|
for (let j = 0; j < arr.size(); j++) {
|
||||||
|
streams.push(pdfDoc.context.lookup(arr.get(j)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let newStr = '';
|
||||||
|
for (let i = 0; i < streams.length; i++) {
|
||||||
|
const stream = streams[i];
|
||||||
|
if (stream instanceof PDFRawStream) {
|
||||||
|
const decoded = decodePDFRawStream(stream).decode();
|
||||||
|
const str = Buffer.from(decoded).toString('utf-8');
|
||||||
|
newStr += str + '\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The CMYK values for the KLZ Blue created by Ghostscript from #000a66
|
||||||
|
// is '1 0.98 0.224 0.298'. We use a slightly dynamic regex to catch small precision diffs.
|
||||||
|
const dynamicRegexK = /1 0\.98[0-9]* 0\.224[0-9]* 0\.298[0-9]* k/g;
|
||||||
|
const dynamicRegexStrokeK = /1 0\.98[0-9]* 0\.224[0-9]* 0\.298[0-9]* K/g;
|
||||||
|
|
||||||
|
let modified = false;
|
||||||
|
if (dynamicRegexK.test(newStr) || dynamicRegexStrokeK.test(newStr)) {
|
||||||
|
newStr = newStr.replace(dynamicRegexK, '/CSPantone cs 1 scn');
|
||||||
|
newStr = newStr.replace(dynamicRegexStrokeK, '/CSPantone CS 1 SCN');
|
||||||
|
modified = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modified) {
|
||||||
|
const newStream = pdfDoc.context.flateStream(newStr);
|
||||||
|
const newRef = pdfDoc.context.register(newStream);
|
||||||
|
page.node.set(PDFName.of('Contents'), newRef);
|
||||||
|
console.log(`[INFO] Spot color injected into page ${pages.indexOf(page) + 1}.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const finalPdfBytes = await pdfDoc.save();
|
||||||
|
fs.writeFileSync(outputPath, finalPdfBytes);
|
||||||
|
console.log(`[SUCCESS] Saved 5-color PDF to ${outputPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
run().catch(console.error);
|
||||||
@@ -12,4 +12,5 @@ Sentry.init({
|
|||||||
|
|
||||||
// Setting this option to true will print useful information to the console while you're setting up Sentry.
|
// Setting this option to true will print useful information to the console while you're setting up Sentry.
|
||||||
debug: false,
|
debug: false,
|
||||||
|
ignoreErrors: ['failed to pipe response'],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,4 +12,5 @@ Sentry.init({
|
|||||||
|
|
||||||
// Setting this option to true will print useful information to the console while you're setting up Sentry.
|
// Setting this option to true will print useful information to the console while you're setting up Sentry.
|
||||||
debug: false,
|
debug: false,
|
||||||
|
ignoreErrors: ['failed to pipe response'],
|
||||||
});
|
});
|
||||||
|
|||||||
47
tests/glitchtip-error-reporting.test.ts
Normal file
47
tests/glitchtip-error-reporting.test.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
|
import { GlitchtipErrorReportingService } from '../lib/services/errors/glitchtip-error-reporting-service';
|
||||||
|
|
||||||
|
// Mock the LoggerService
|
||||||
|
const mockLogger = {
|
||||||
|
child: vi.fn().mockReturnThis(),
|
||||||
|
info: vi.fn(),
|
||||||
|
error: vi.fn(),
|
||||||
|
warn: vi.fn(),
|
||||||
|
debug: vi.fn(),
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
const mockSentryInit = vi.fn();
|
||||||
|
|
||||||
|
vi.mock('@sentry/nextjs', () => ({
|
||||||
|
init: (...args: any[]) => mockSentryInit(...args),
|
||||||
|
captureException: vi.fn(),
|
||||||
|
captureMessage: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('GlitchtipErrorReportingService', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ignore version drift errors (ChunkLoadError and Server Action) in Sentry config', async () => {
|
||||||
|
// Simulate window to force client-side init behavior instantly
|
||||||
|
const originalWindow = global.window;
|
||||||
|
global.window = {
|
||||||
|
requestIdleCallback: (cb: Function) => cb(),
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
const service = new GlitchtipErrorReportingService({ enabled: true }, mockLogger);
|
||||||
|
|
||||||
|
// Give the dynamic import a moment to resolve
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||||
|
|
||||||
|
expect(mockSentryInit).toHaveBeenCalledOnce();
|
||||||
|
const initCallArgs = mockSentryInit.mock.calls[0][0];
|
||||||
|
|
||||||
|
expect(initCallArgs.ignoreErrors).toBeDefined();
|
||||||
|
expect(initCallArgs.ignoreErrors).toContain('ChunkLoadError');
|
||||||
|
expect(initCallArgs.ignoreErrors).toContain('Failed to find Server Action');
|
||||||
|
|
||||||
|
global.window = originalWindow;
|
||||||
|
});
|
||||||
|
});
|
||||||
27
tests/lexical.test.ts
Normal file
27
tests/lexical.test.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { lexicalToMarkdown } from '../lib/mdx-utils';
|
||||||
|
|
||||||
|
describe('lexicalToMarkdown', () => {
|
||||||
|
it('should convert lexical AST to markdown', () => {
|
||||||
|
const ast = {
|
||||||
|
type: 'root',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
type: 'paragraph',
|
||||||
|
children: [
|
||||||
|
{ type: 'text', text: 'Hello ' },
|
||||||
|
{ type: 'text', text: 'World', format: 1 }, // bold
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'heading',
|
||||||
|
tag: 'h2',
|
||||||
|
children: [{ type: 'text', text: 'Title' }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = lexicalToMarkdown(ast);
|
||||||
|
expect(result).toBe('Hello **World**\n\n## Title\n\n');
|
||||||
|
});
|
||||||
|
});
|
||||||
14
tests/mdx-content-mapping.test.tsx
Normal file
14
tests/mdx-content-mapping.test.tsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
|
describe('MDXContent Mapping', () => {
|
||||||
|
it('should have productTabs in the switch statement to prevent Unknown Block Type errors', () => {
|
||||||
|
const filePath = path.join(process.cwd(), 'components/MDXContent.tsx');
|
||||||
|
const fileContent = fs.readFileSync(filePath, 'utf-8');
|
||||||
|
|
||||||
|
// Check if the switch case for productTabs exists
|
||||||
|
expect(fileContent).toContain("case 'productTabs':");
|
||||||
|
expect(fileContent).toContain('<ProductTechnicalData');
|
||||||
|
});
|
||||||
|
});
|
||||||
22
tests/mdx-regex.test.ts
Normal file
22
tests/mdx-regex.test.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { fixMdxDataProps } from '../lib/mdx-utils';
|
||||||
|
|
||||||
|
describe('MDX Data Props Fixer', () => {
|
||||||
|
it('should correctly parse MDX with nested JSON containing }}', () => {
|
||||||
|
// This string simulates the exact structure that causes the bug in n2x2y.mdx
|
||||||
|
const mdxInput = `<Block type="productTabs" data={{"content":{"root":{"children":[]}},"id":"123"}} />`;
|
||||||
|
|
||||||
|
const result = fixMdxDataProps(mdxInput);
|
||||||
|
|
||||||
|
const expected = `<Block type="productTabs" data="{"content":{"root":{"children":[]}},"id":"123"}" />`;
|
||||||
|
|
||||||
|
expect(result).toBe(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not prematurely truncate if } appears inside a string literal', () => {
|
||||||
|
const mdxInput = `<Block type="productTabs" data={{"content":{"text":"}>"}}} />`;
|
||||||
|
const result = fixMdxDataProps(mdxInput);
|
||||||
|
const expected = `<Block type="productTabs" data="{"content":{"text":"}>"}}" />`;
|
||||||
|
expect(result).toBe(expected.replace('>', '>'));
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user