Compare commits
100 Commits
feat/busin
...
v2.3.36
| Author | SHA1 | Date | |
|---|---|---|---|
| ae381442aa | |||
| 3334937b67 | |||
| 246e6540d4 | |||
| 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 | |||
| aa3b0b05b8 | |||
| 4c6e8cb28b | |||
| 175cb10e01 | |||
| 5e8a175a53 | |||
| 9f64304539 | |||
| 6367efdf6f | |||
| 22ae008f29 | |||
| 118319ceab | |||
| 5d94337df1 | |||
| 3798d37fcd | |||
| efe3cbc58e | |||
| f798afe917 | |||
| 1623e04fce | |||
| b77eb2087e | |||
| 7e7acff670 | |||
| 069400dd86 | |||
| 91f5cb8de3 | |||
| 702313e42c | |||
| adafe8ac02 | |||
| 3599880f3a | |||
| 79f0231e26 | |||
| cdf2af79ca | |||
| 428b223020 | |||
| 6214678433 | |||
| 6e2a959972 | |||
| a20ba52f7f | |||
| 292557eca5 | |||
| 6c14ceec3e | |||
| d11c27fda0 | |||
| 3cbb4ba700 | |||
| 5a9c6e4a0a |
@@ -256,6 +256,10 @@ jobs:
|
||||
# Analytics
|
||||
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' }}
|
||||
|
||||
# Notifications
|
||||
GOTIFY_URL: ${{ secrets.GOTIFY_URL }}
|
||||
GOTIFY_TOKEN: ${{ secrets.GOTIFY_TOKEN }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
@@ -309,6 +313,10 @@ jobs:
|
||||
echo "UMAMI_WEBSITE_ID=$UMAMI_WEBSITE_ID"
|
||||
echo "UMAMI_API_ENDPOINT=$UMAMI_API_ENDPOINT"
|
||||
echo ""
|
||||
echo "# Notifications"
|
||||
echo "GOTIFY_URL=$GOTIFY_URL"
|
||||
echo "GOTIFY_TOKEN=$GOTIFY_TOKEN"
|
||||
echo ""
|
||||
echo "TARGET=$TARGET"
|
||||
echo "SENTRY_ENVIRONMENT=$TARGET"
|
||||
echo "PROJECT_NAME=$PROJECT_NAME"
|
||||
@@ -357,7 +365,7 @@ jobs:
|
||||
ssh root@alpha.mintel.me "cd $SITE_DIR && docker compose -p '${{ needs.prepare.outputs.project_name }}' --env-file '$ENV_FILE' pull"
|
||||
ssh root@alpha.mintel.me "cd $SITE_DIR && docker compose -p '${{ needs.prepare.outputs.project_name }}' --env-file '$ENV_FILE' up -d --remove-orphans"
|
||||
|
||||
|
||||
ssh root@alpha.mintel.me "docker network prune -f"
|
||||
ssh root@alpha.mintel.me "docker system prune -f --filter 'until=24h'"
|
||||
|
||||
- name: 🧹 Post-Deploy Cleanup (Runner)
|
||||
|
||||
@@ -44,7 +44,6 @@ jobs:
|
||||
env:
|
||||
NEXT_PUBLIC_BASE_URL: ${{ env.TARGET_URL }}
|
||||
GATEKEEPER_PASSWORD: ${{ secrets.GATEKEEPER_PASSWORD }}
|
||||
PUPPETEER_EXECUTABLE_PATH: /usr/bin/chromium
|
||||
run: npx tsx scripts/smoke.ts
|
||||
- name: 🔔 Notify
|
||||
if: always()
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -25,3 +25,7 @@ html-errors*.json
|
||||
reference/
|
||||
# Database backups
|
||||
backups/
|
||||
|
||||
# Local data / backups
|
||||
.data/
|
||||
scratch/
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ImageResponse } from 'next/og';
|
||||
import { getPageBySlug } from '@/lib/pages';
|
||||
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 contentType = 'image/png';
|
||||
@@ -20,9 +20,12 @@ export default async function Image({
|
||||
}
|
||||
|
||||
const fonts = await getOgFonts();
|
||||
const { logo, bgImage } = getOgAssets();
|
||||
|
||||
return new ImageResponse(
|
||||
<OGImageTemplate
|
||||
image={bgImage}
|
||||
logo={logo}
|
||||
title={pageData.frontmatter.title}
|
||||
description={pageData.frontmatter.excerpt}
|
||||
label="Information"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ImageResponse } from 'next/og';
|
||||
import { getPostBySlug } from '@/lib/blog';
|
||||
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';
|
||||
|
||||
export const size = OG_IMAGE_SIZE;
|
||||
@@ -35,6 +35,7 @@ export default async function Image({
|
||||
}
|
||||
|
||||
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
|
||||
// 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(
|
||||
<OGImageTemplate
|
||||
logo={logo}
|
||||
title={post.frontmatter.title}
|
||||
description={post.frontmatter.excerpt}
|
||||
label={post.frontmatter.category || 'Blog'}
|
||||
image={base64Image || featuredImage}
|
||||
image={base64Image || featuredImage || bgImage}
|
||||
/>,
|
||||
{
|
||||
...OG_IMAGE_SIZE,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ImageResponse } from 'next/og';
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
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 contentType = 'image/png';
|
||||
@@ -11,9 +11,16 @@ export default async function Image({ params }: { params: Promise<{ locale: stri
|
||||
const { locale } = await params;
|
||||
const t = await getTranslations({ locale, namespace: 'Blog.meta' });
|
||||
const fonts = await getOgFonts();
|
||||
const { logo, bgImage } = getOgAssets();
|
||||
|
||||
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,
|
||||
fonts,
|
||||
|
||||
@@ -3,7 +3,7 @@ import Image from 'next/image';
|
||||
import { getAllPosts } from '@/lib/blog';
|
||||
import { Section, Container, Heading, Card, Badge, Button } from '@/components/ui';
|
||||
import Reveal from '@/components/Reveal';
|
||||
import { Metadata } from 'next';
|
||||
|
||||
import { getTranslations, setRequestLocale } from 'next-intl/server';
|
||||
import { SITE_URL } from '@/lib/schema';
|
||||
import { BlogPaginationKeyboardObserver } from '@/components/blog/BlogPaginationKeyboardObserver';
|
||||
@@ -12,6 +12,7 @@ interface BlogIndexProps {
|
||||
params: Promise<{
|
||||
locale: string;
|
||||
}>;
|
||||
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
|
||||
}
|
||||
|
||||
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 resolvedSearchParams = await searchParams;
|
||||
const categoryParam = resolvedSearchParams.category as string | undefined;
|
||||
setRequestLocale(locale);
|
||||
const t = await getTranslations('Blog');
|
||||
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(),
|
||||
);
|
||||
|
||||
const featuredPost = sortedPosts[0];
|
||||
const remainingPosts = sortedPosts.slice(1);
|
||||
const filteredPosts = categoryParam
|
||||
? sortedPosts.filter(
|
||||
(post) => post.frontmatter.category?.toLowerCase() === categoryParam.toLowerCase(),
|
||||
)
|
||||
: sortedPosts;
|
||||
|
||||
const featuredPost = filteredPosts[0];
|
||||
const remainingPosts = filteredPosts.slice(1);
|
||||
|
||||
return (
|
||||
<div className="bg-neutral-light min-h-screen">
|
||||
@@ -123,31 +132,38 @@ export default async function BlogIndex({ params }: BlogIndexProps) {
|
||||
{t('allArticles')}
|
||||
</Heading>
|
||||
<div className="flex flex-wrap gap-2 md:gap-4">
|
||||
{/* Category filters could go here */}
|
||||
<Badge
|
||||
variant="primary"
|
||||
className="cursor-pointer hover:bg-primary hover:text-white transition-colors touch-target px-3 md:px-4"
|
||||
>
|
||||
{t('categories.all')}
|
||||
</Badge>
|
||||
<Badge
|
||||
variant="neutral"
|
||||
className="cursor-pointer hover:bg-primary hover:text-white transition-colors touch-target px-3 md:px-4"
|
||||
>
|
||||
{t('categories.industry')}
|
||||
</Badge>
|
||||
<Badge
|
||||
variant="neutral"
|
||||
className="cursor-pointer hover:bg-primary hover:text-white transition-colors touch-target px-3 md:px-4"
|
||||
>
|
||||
{t('categories.technical')}
|
||||
</Badge>
|
||||
<Badge
|
||||
variant="neutral"
|
||||
className="cursor-pointer hover:bg-primary hover:text-white transition-colors touch-target px-3 md:px-4"
|
||||
>
|
||||
{t('categories.sustainability')}
|
||||
</Badge>
|
||||
<Link href={`/${locale}/blog`}>
|
||||
<Badge
|
||||
variant={!categoryParam ? 'primary' : 'neutral'}
|
||||
className="cursor-pointer hover:bg-primary hover:text-white transition-colors touch-target px-3 md:px-4"
|
||||
>
|
||||
{t('categories.all')}
|
||||
</Badge>
|
||||
</Link>
|
||||
<Link href={`/${locale}/blog?category=industry`}>
|
||||
<Badge
|
||||
variant={categoryParam === 'industry' ? 'primary' : 'neutral'}
|
||||
className="cursor-pointer hover:bg-primary hover:text-white transition-colors touch-target px-3 md:px-4"
|
||||
>
|
||||
{t('categories.industry')}
|
||||
</Badge>
|
||||
</Link>
|
||||
<Link href={`/${locale}/blog?category=technical`}>
|
||||
<Badge
|
||||
variant={categoryParam === 'technical' ? 'primary' : 'neutral'}
|
||||
className="cursor-pointer hover:bg-primary hover:text-white transition-colors touch-target px-3 md:px-4"
|
||||
>
|
||||
{t('categories.technical')}
|
||||
</Badge>
|
||||
</Link>
|
||||
<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>
|
||||
</Reveal>
|
||||
@@ -260,7 +276,7 @@ export default async function BlogIndex({ params }: BlogIndexProps) {
|
||||
{t('prev')}
|
||||
</Button>
|
||||
<Button
|
||||
href={`/${locale}/blog?page=1`}
|
||||
href={`/${locale}/blog?page=1${categoryParam ? `&category=${categoryParam}` : ''}`}
|
||||
variant="primary"
|
||||
size="sm"
|
||||
className="md:h-11 md:px-6 md:text-base"
|
||||
@@ -269,7 +285,7 @@ export default async function BlogIndex({ params }: BlogIndexProps) {
|
||||
1
|
||||
</Button>
|
||||
<Button
|
||||
href={`/${locale}/blog?page=2`}
|
||||
href={`/${locale}/blog?page=2${categoryParam ? `&category=${categoryParam}` : ''}`}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="md:h-11 md:px-6 md:text-base"
|
||||
@@ -277,7 +293,7 @@ export default async function BlogIndex({ params }: BlogIndexProps) {
|
||||
2
|
||||
</Button>
|
||||
<Button
|
||||
href={`/${locale}/blog?page=2`}
|
||||
href={`/${locale}/blog?page=2${categoryParam ? `&category=${categoryParam}` : ''}`}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="md:h-11 md:px-6 md:text-base"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ImageResponse } from 'next/og';
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
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 contentType = 'image/png';
|
||||
@@ -11,12 +11,19 @@ export default async function Image({ params }: { params: Promise<{ locale: stri
|
||||
const { locale } = await params;
|
||||
const t = await getTranslations({ locale, namespace: 'Contact' });
|
||||
const fonts = await getOgFonts();
|
||||
const { logo, bgImage } = getOgAssets();
|
||||
|
||||
const title = t('meta.title') || t('title');
|
||||
const description = t('meta.description') || t('subtitle');
|
||||
|
||||
return new ImageResponse(
|
||||
<OGImageTemplate title={title} description={description} label="Contact" />,
|
||||
<OGImageTemplate
|
||||
image={bgImage}
|
||||
logo={logo}
|
||||
title={title}
|
||||
description={description}
|
||||
label="Contact"
|
||||
/>,
|
||||
{
|
||||
...OG_IMAGE_SIZE,
|
||||
fonts,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ImageResponse } from 'next/og';
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
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 contentType = 'image/png';
|
||||
@@ -11,9 +11,12 @@ export default async function Image({ params }: { params: Promise<{ locale: stri
|
||||
const { locale } = await params;
|
||||
const t = await getTranslations({ locale, namespace: 'Index.meta' });
|
||||
const fonts = await getOgFonts();
|
||||
const { logo, bgImage } = getOgAssets();
|
||||
|
||||
return new ImageResponse(
|
||||
<OGImageTemplate
|
||||
image={bgImage}
|
||||
logo={logo}
|
||||
title={t('title')}
|
||||
description={t('description')}
|
||||
label="Reliable Energy Infrastructure"
|
||||
|
||||
@@ -266,18 +266,7 @@ export default async function ProductPage({ params }: ProductPageProps) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
let technicalItems = [];
|
||||
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
|
||||
}
|
||||
let technicalItems: any[] = [];
|
||||
|
||||
const datasheetPath = getDatasheetPath(productSlug, locale);
|
||||
const isFallback = (product.frontmatter as any).isFallback;
|
||||
@@ -296,8 +285,20 @@ export default async function ProductPage({ params }: ProductPageProps) {
|
||||
if (typeof product.content === 'string') {
|
||||
const tabBlockMatch = product.content.match(/<Block type="productTabs".*?\/>/s);
|
||||
if (tabBlockMatch) {
|
||||
descriptionContent = product.content.replace(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 { getTranslations } from 'next-intl/server';
|
||||
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 contentType = 'image/png';
|
||||
@@ -11,6 +11,7 @@ export default async function Image({ params }: { params: Promise<{ locale: stri
|
||||
const { locale } = await params;
|
||||
const t = await getTranslations({ locale, namespace: 'Products' });
|
||||
const fonts = await getOgFonts();
|
||||
const { logo, bgImage } = getOgAssets();
|
||||
|
||||
const title = t.has('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');
|
||||
|
||||
return new ImageResponse(
|
||||
<OGImageTemplate title={title} description={description} label="Products" />,
|
||||
<OGImageTemplate
|
||||
image={bgImage}
|
||||
logo={logo}
|
||||
title={title}
|
||||
description={description}
|
||||
label="Products"
|
||||
/>,
|
||||
{
|
||||
...OG_IMAGE_SIZE,
|
||||
fonts,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ImageResponse } from 'next/og';
|
||||
import { getTranslations } from 'next-intl/server';
|
||||
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 contentType = 'image/png';
|
||||
@@ -11,12 +11,19 @@ export default async function Image({ params }: { params: Promise<{ locale: stri
|
||||
const { locale } = await params;
|
||||
const t = await getTranslations({ locale, namespace: 'Team' });
|
||||
const fonts = await getOgFonts();
|
||||
const { logo, bgImage } = getOgAssets();
|
||||
|
||||
const title = t('meta.title') || t('hero.subtitle');
|
||||
const description = t('meta.description') || t('hero.title');
|
||||
|
||||
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,
|
||||
fonts,
|
||||
|
||||
@@ -61,7 +61,7 @@ export async function sendContactFormAction(formData: FormData) {
|
||||
productName,
|
||||
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 });
|
||||
} catch (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)
|
||||
await services.notifications.notify({
|
||||
title: `📩 ${notificationSubject}`,
|
||||
title: `📩 [KLZ] ${notificationSubject}`,
|
||||
message: `New message from ${name} (${email}):\n\n${message}`,
|
||||
priority: 5,
|
||||
});
|
||||
|
||||
@@ -450,6 +450,13 @@ export default function Header() {
|
||||
variant="accent"
|
||||
size="lg"
|
||||
className="w-full py-6 text-lg md:text-xl shadow-2xl hover:scale-105 transition-transform"
|
||||
onClick={() => {
|
||||
setIsMobileMenuOpen(false);
|
||||
trackEvent(AnalyticsEvents.BUTTON_CLICK, {
|
||||
label: t('contact'),
|
||||
location: 'mobile_menu_cta',
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t('contact')}
|
||||
</Button>
|
||||
|
||||
@@ -22,6 +22,9 @@ import MeetTheTeam from './home/MeetTheTeam';
|
||||
import GallerySection from './home/GallerySection';
|
||||
import VideoSection from './home/VideoSection';
|
||||
import CTA from './home/CTA';
|
||||
import { AgbHistoryBlock } from './AgbHistoryBlock';
|
||||
import { PDFDownloadBlock } from './PDFDownloadBlock';
|
||||
import ProductTechnicalData from './ProductTechnicalData';
|
||||
function ContactSection(props: any) {
|
||||
return (
|
||||
<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} />;
|
||||
case 'homeCTA':
|
||||
return <CTA data={data} />;
|
||||
case 'agbHistory':
|
||||
return <AgbHistoryBlock {...data} />;
|
||||
case 'pdfDownload':
|
||||
return <PDFDownloadBlock {...data} />;
|
||||
case 'productTabs':
|
||||
return <ProductTechnicalData data={data} />;
|
||||
|
||||
default:
|
||||
return (
|
||||
|
||||
@@ -5,6 +5,7 @@ interface OGImageTemplateProps {
|
||||
description?: string;
|
||||
label?: string;
|
||||
image?: string;
|
||||
logo?: string;
|
||||
mode?: 'dark' | 'light' | 'image';
|
||||
}
|
||||
|
||||
@@ -13,6 +14,7 @@ export function OGImageTemplate({
|
||||
description,
|
||||
label,
|
||||
image,
|
||||
logo,
|
||||
mode = 'dark',
|
||||
}: OGImageTemplateProps) {
|
||||
const primaryBlue = '#001a4d';
|
||||
@@ -152,21 +154,26 @@ export function OGImageTemplate({
|
||||
height: '6px',
|
||||
backgroundColor: accentGreen,
|
||||
borderRadius: '3px',
|
||||
marginRight: '24px',
|
||||
marginRight: '32px',
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
fontSize: '24px',
|
||||
fontWeight: 700,
|
||||
color: 'white',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.15em',
|
||||
display: 'flex',
|
||||
}}
|
||||
>
|
||||
KLZ Cables
|
||||
</div>
|
||||
{logo ? (
|
||||
/* eslint-disable-next-line @next/next/no-img-element */
|
||||
<img src={logo} alt="KLZ Cables" height="40" style={{ objectFit: 'contain' }} />
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
fontSize: '24px',
|
||||
fontWeight: 700,
|
||||
color: 'white',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.15em',
|
||||
display: 'flex',
|
||||
}}
|
||||
>
|
||||
KLZ Cables
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Saturated Blue Brand Strip */}
|
||||
|
||||
@@ -3,7 +3,11 @@
|
||||
import React from 'react';
|
||||
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();
|
||||
|
||||
// Extract slug from pathname
|
||||
@@ -12,7 +16,7 @@ export const PDFDownloadBlock: React.FC<{ label: string; style: string }> = ({ l
|
||||
// We want the page slug.
|
||||
const slug = segments[segments.length - 1] || 'home';
|
||||
|
||||
const href = `/api/pages/${slug}/pdf`;
|
||||
const href = url || `/api/pages/${slug}/pdf`;
|
||||
|
||||
return (
|
||||
<div className="my-8">
|
||||
|
||||
@@ -82,8 +82,13 @@ export function Button({
|
||||
);
|
||||
|
||||
if (href) {
|
||||
const { type, disabled, ...linkProps } = props as any;
|
||||
return (
|
||||
<Link href={href} className={styles}>
|
||||
<Link
|
||||
href={disabled ? '#' : href}
|
||||
className={cn(styles, disabled && 'pointer-events-none opacity-50')}
|
||||
{...linkProps}
|
||||
>
|
||||
{content}
|
||||
</Link>
|
||||
);
|
||||
|
||||
@@ -247,7 +247,7 @@ _status: published
|
||||
|
||||
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"}} />
|
||||
|
||||
|
||||
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
BIN
data/excel/high-voltage.xlsx
Normal file
BIN
data/excel/high-voltage.xlsx
Normal file
Binary file not shown.
BIN
data/excel/low-voltage-KM.xlsx
Normal file
BIN
data/excel/low-voltage-KM.xlsx
Normal file
Binary file not shown.
BIN
data/excel/medium-voltage-KM 170126.xlsx
Normal file
BIN
data/excel/medium-voltage-KM 170126.xlsx
Normal file
Binary file not shown.
BIN
data/excel/medium-voltage-KM.xlsx
Normal file
BIN
data/excel/medium-voltage-KM.xlsx
Normal file
Binary file not shown.
BIN
data/excel/solar-cables.xlsx
Normal file
BIN
data/excel/solar-cables.xlsx
Normal file
Binary file not shown.
9843
lib/TruckBlueprint.tsx
Normal file
9843
lib/TruckBlueprint.tsx
Normal file
File diff suppressed because one or more lines are too long
22
lib/blog.ts
22
lib/blog.ts
@@ -56,17 +56,17 @@ export function isPostVisible(post: { frontmatter: { date: string; public?: bool
|
||||
return !(postDate > now && config.isProduction);
|
||||
}
|
||||
|
||||
import { mapSlugToFileSlug, mapFileSlugToTranslated } from './slugs';
|
||||
|
||||
export async function getPostBySlug(slug: string, locale: string): Promise<PostData | null> {
|
||||
try {
|
||||
const filePath = path.join(process.cwd(), 'content', 'posts', `${slug}.mdx`);
|
||||
const fileSlug = await mapSlugToFileSlug(slug, locale);
|
||||
const filePath = path.join(process.cwd(), 'content', 'posts', `${fileSlug}.mdx`);
|
||||
const fileContent = await fs.readFile(filePath, 'utf-8');
|
||||
const { data, content } = matter(fileContent);
|
||||
|
||||
// Fix MDX data props dropped by next-mdx-remote
|
||||
// Payload serializes as data={{"items":...}} which Acorn treats as a Block statement.
|
||||
const fixedContent = content.replace(/data=\{\{([\s\S]*?)\}\}/g, (match, p1) => {
|
||||
return 'data="{' + p1.replace(/"/g, '"') + '}"';
|
||||
});
|
||||
const { fixMdxDataProps } = await import('./mdx-utils');
|
||||
const fixedContent = fixMdxDataProps(content);
|
||||
|
||||
let parsedContent = fixedContent;
|
||||
try {
|
||||
@@ -77,8 +77,10 @@ export async function getPostBySlug(slug: string, locale: string): Promise<PostD
|
||||
// Not JSON
|
||||
}
|
||||
|
||||
const resolvedSlug = await mapFileSlugToTranslated(fileSlug, locale);
|
||||
|
||||
return {
|
||||
slug,
|
||||
slug: resolvedSlug,
|
||||
frontmatter: {
|
||||
title: data.title || '',
|
||||
date: data.date || '',
|
||||
@@ -98,8 +100,10 @@ export async function getPostBySlug(slug: string, locale: string): Promise<PostD
|
||||
}
|
||||
|
||||
export async function getPostSlugs(slug: string, locale: string): Promise<Record<string, string>> {
|
||||
// Mock function, simply returns the slug for all locales
|
||||
return { de: slug, en: slug };
|
||||
const fileSlug = await mapSlugToFileSlug(slug, locale);
|
||||
const de = await mapFileSlugToTranslated(fileSlug, 'de');
|
||||
const en = await mapFileSlugToTranslated(fileSlug, 'en');
|
||||
return { de, en };
|
||||
}
|
||||
|
||||
export async function getAllPosts(locale: string): Promise<PostData[]> {
|
||||
|
||||
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,
|
||||
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',
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
import matter from 'gray-matter';
|
||||
import { config } from '@/lib/config';
|
||||
import { mapSlugToFileSlug } from './slugs';
|
||||
|
||||
export interface PageFrontmatter {
|
||||
title: string;
|
||||
@@ -47,11 +48,15 @@ function mapDoc(doc: any): PageData {
|
||||
|
||||
export async function getPageBySlug(slug: string, locale: string): Promise<PageData | null> {
|
||||
try {
|
||||
const filePath = path.join(process.cwd(), 'content', 'pages', `${slug}.mdx`);
|
||||
const fileSlug = await mapSlugToFileSlug(slug, locale);
|
||||
const filePath = path.join(process.cwd(), 'content', 'pages', `${fileSlug}.mdx`);
|
||||
const fileContent = await fs.readFile(filePath, 'utf-8');
|
||||
const { data, content } = matter(fileContent);
|
||||
|
||||
let parsedContent = content;
|
||||
const { fixMdxDataProps } = await import('./mdx-utils');
|
||||
const fixedContent = fixMdxDataProps(content);
|
||||
|
||||
let parsedContent = fixedContent;
|
||||
try {
|
||||
if (content.trim().startsWith('{')) {
|
||||
parsedContent = JSON.parse(content);
|
||||
|
||||
@@ -16,6 +16,24 @@ import {
|
||||
Circle,
|
||||
} from '@react-pdf/renderer';
|
||||
import * as path from 'path';
|
||||
import { TruckBlueprint } from './TruckBlueprint';
|
||||
|
||||
// Register embedded fonts so Ghostscript -dNoOutputFonts can convert them to
|
||||
// outlines (curves). Built-in PDF fonts like Helvetica are NEVER embedded and
|
||||
// therefore cannot be outlined — always use a physical font file here.
|
||||
Font.register({
|
||||
family: 'Inter',
|
||||
fonts: [
|
||||
{
|
||||
src: path.join(process.cwd(), 'public', 'fonts', 'Inter-Regular.woff'),
|
||||
fontWeight: 'normal',
|
||||
},
|
||||
{
|
||||
src: path.join(process.cwd(), 'public', 'fonts', 'Inter-Bold.woff'),
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
export interface PersonData {
|
||||
name: string;
|
||||
@@ -27,13 +45,20 @@ export interface PersonData {
|
||||
export interface PDFBusinessCardProps {
|
||||
person: PersonData;
|
||||
qrCodeDataUrl: string;
|
||||
isWhiteVariant?: boolean;
|
||||
variant?: 'blue' | 'white' | 'black';
|
||||
isViaprinto?: boolean;
|
||||
withTruck?: boolean;
|
||||
withCropMarks?: boolean;
|
||||
}
|
||||
|
||||
// Mintel Aesthetics Colors - EXACT PROD COLORS
|
||||
// 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];
|
||||
|
||||
const COLORS = {
|
||||
primary: '#011dff', // Brand Royal Blue
|
||||
primary: '#000a66', // Brand Dark Blue (from website)
|
||||
blue: '#000a66',
|
||||
green: '#82ed20', // Brand Accent Green
|
||||
darkGreen: '#4A9E00', // Darker green for white background visibility
|
||||
white: '#ffffff',
|
||||
grayText: '#6c757d',
|
||||
black: '#0a0a0a',
|
||||
@@ -41,13 +66,105 @@ const COLORS = {
|
||||
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({
|
||||
pageBleed: {
|
||||
width: '91mm',
|
||||
height: '61mm',
|
||||
backgroundColor: COLORS.primary,
|
||||
position: 'relative',
|
||||
fontFamily: 'Helvetica',
|
||||
fontFamily: 'Inter',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
pageBleedBack: {
|
||||
@@ -55,7 +172,7 @@ const styles = StyleSheet.create({
|
||||
height: '61mm',
|
||||
backgroundColor: COLORS.white,
|
||||
position: 'relative',
|
||||
fontFamily: 'Helvetica',
|
||||
fontFamily: 'Inter',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
|
||||
@@ -65,100 +182,97 @@ const styles = StyleSheet.create({
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: '16mm', // Leave space for the bar below
|
||||
bottom: '20mm', // Truck stops above the footer area to avoid text conflict
|
||||
zIndex: 1,
|
||||
},
|
||||
frontBackgroundImage: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'contain',
|
||||
objectPositionX: 'center',
|
||||
objectPositionY: 'center',
|
||||
opacity: 0.2, // Much more transparent
|
||||
opacity: 0.4, // Less transparent, more contrast
|
||||
},
|
||||
frontContainer: {
|
||||
flex: 1,
|
||||
paddingBottom: '16mm', // 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',
|
||||
alignItems: 'center',
|
||||
},
|
||||
frontLogo: {
|
||||
width: 140,
|
||||
width: 145, // Reduced for a more elegant, premium look with more negative space
|
||||
zIndex: 100,
|
||||
},
|
||||
frontTagline: {
|
||||
color: COLORS.white,
|
||||
fontSize: 6,
|
||||
marginTop: 10,
|
||||
letterSpacing: 1.5,
|
||||
fontSize: 5.5,
|
||||
marginTop: 8,
|
||||
letterSpacing: 2,
|
||||
fontWeight: 'bold',
|
||||
textTransform: 'uppercase',
|
||||
zIndex: 100,
|
||||
},
|
||||
frontGreenBarBottom: {
|
||||
frontFooterArea: {
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: '16mm',
|
||||
borderTop: `2pt solid ${COLORS.green}`,
|
||||
backgroundColor: '#000a66',
|
||||
height: '22mm', // Increased height to accommodate more bottom padding
|
||||
paddingBottom: '10mm', // Increased bottom padding to move icons further from edge
|
||||
zIndex: 50,
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
gap: 6, // Increased gap for more spacing under tagline
|
||||
gap: 6, // Harmonious spacing to icons
|
||||
},
|
||||
frontIconsGroup: {
|
||||
flexDirection: 'row',
|
||||
gap: 12,
|
||||
gap: 14, // Wider gaps between icon groups for visual clarity at small size
|
||||
},
|
||||
frontBrandIconWrapper: {
|
||||
alignItems: 'center',
|
||||
gap: 2,
|
||||
gap: 1.5,
|
||||
},
|
||||
frontBrandIcon: {
|
||||
width: 14,
|
||||
height: 14,
|
||||
opacity: 0.9,
|
||||
width: 12, // Smaller for compact bar
|
||||
height: 12,
|
||||
opacity: 0.85,
|
||||
},
|
||||
frontBrandText: {
|
||||
fontSize: 4,
|
||||
fontSize: 4, // Tiny but readable at 300dpi print
|
||||
color: COLORS.white,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 0.5,
|
||||
opacity: 0.9,
|
||||
letterSpacing: 0.6,
|
||||
// Full opacity — white on blue needs to pop cleanly
|
||||
},
|
||||
frontTaglineBar: {
|
||||
color: COLORS.green,
|
||||
fontSize: 5.5,
|
||||
letterSpacing: 1.5,
|
||||
fontSize: 7, // Larger — as requested
|
||||
letterSpacing: 0.5, // Harmonious tracking for mixed case
|
||||
fontWeight: 'bold',
|
||||
textTransform: 'uppercase',
|
||||
},
|
||||
|
||||
// -- WHITE PAGE (Vorderseite) --
|
||||
backContainer: {
|
||||
flex: 1,
|
||||
padding: '8mm',
|
||||
paddingTop: '6mm', // Pushed down as requested
|
||||
paddingLeft: '6mm',
|
||||
paddingRight: '6mm',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
backHeader: {
|
||||
marginBottom: 'auto',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginBottom: '5mm', // Generous breathing room
|
||||
width: '100%',
|
||||
},
|
||||
backLogo: {
|
||||
width: 70,
|
||||
width: 60,
|
||||
},
|
||||
accentLineHorizontal: {
|
||||
flex: 1,
|
||||
height: 1.5,
|
||||
backgroundColor: COLORS.primary, // Brand blue line
|
||||
marginLeft: 15,
|
||||
marginRight: 0,
|
||||
marginLeft: 12,
|
||||
marginRight: 12,
|
||||
},
|
||||
accentDot: {
|
||||
width: 4,
|
||||
@@ -177,113 +291,135 @@ const styles = StyleSheet.create({
|
||||
top: -5,
|
||||
},
|
||||
backBody: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'flex-end',
|
||||
flex: 1,
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'flex-start',
|
||||
alignItems: 'flex-start',
|
||||
width: '100%',
|
||||
},
|
||||
leftColumn: {
|
||||
flex: 1,
|
||||
width: '100%', // Take full width since right column is removed
|
||||
flexDirection: 'column',
|
||||
paddingRight: '4mm',
|
||||
paddingBottom: '2mm',
|
||||
},
|
||||
nameSection: {
|
||||
marginBottom: '5mm',
|
||||
marginBottom: '2.5mm', // Reduced to fix the 'Lücke' under the role
|
||||
},
|
||||
nameText: {
|
||||
fontSize: 14,
|
||||
fontWeight: 'bold',
|
||||
color: COLORS.neutralDark,
|
||||
marginBottom: 2,
|
||||
letterSpacing: -0.2,
|
||||
},
|
||||
roleText: {
|
||||
fontSize: 7.5,
|
||||
fontSize: 13,
|
||||
fontWeight: 'bold',
|
||||
color: COLORS.primary,
|
||||
letterSpacing: 0.5,
|
||||
marginBottom: 1.5,
|
||||
letterSpacing: -0.15,
|
||||
},
|
||||
roleText: {
|
||||
fontSize: 6.5,
|
||||
fontWeight: 'bold',
|
||||
color: COLORS.darkGreen,
|
||||
letterSpacing: 1.2,
|
||||
textTransform: 'uppercase',
|
||||
},
|
||||
contactSection: {
|
||||
flexDirection: 'column',
|
||||
gap: 5,
|
||||
gap: 0,
|
||||
},
|
||||
contactRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginBottom: 5, // Even rhythm between contact lines
|
||||
},
|
||||
iconWrapper: {
|
||||
width: 10,
|
||||
width: 12,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'flex-start',
|
||||
},
|
||||
contactText: {
|
||||
fontSize: 7.5,
|
||||
color: COLORS.neutralDark,
|
||||
color: COLORS.primary,
|
||||
fontWeight: 'normal',
|
||||
paddingTop: 1,
|
||||
paddingTop: 0.5,
|
||||
letterSpacing: 0.1,
|
||||
},
|
||||
rightColumn: {
|
||||
flexDirection: 'column',
|
||||
alignItems: 'flex-end',
|
||||
justifyContent: 'flex-end',
|
||||
justifyContent: 'flex-start',
|
||||
},
|
||||
// Ultra Minimalist QR Area
|
||||
qrContainer: {
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
gap: 3,
|
||||
},
|
||||
qrCodeWrapper: {
|
||||
width: '15mm',
|
||||
height: '15mm',
|
||||
width: '10mm',
|
||||
height: '10mm',
|
||||
border: `0.75pt solid ${COLORS.darkGreen}`,
|
||||
padding: 2,
|
||||
backgroundColor: '#ffffff',
|
||||
},
|
||||
qrCode: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
},
|
||||
qrLabel: {
|
||||
fontSize: 3.5,
|
||||
color: COLORS.neutralDark, // Subtle grey instead of loud blue
|
||||
fontWeight: 'bold',
|
||||
letterSpacing: 1.2,
|
||||
addressFooter: {
|
||||
width: '100%',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-start',
|
||||
alignItems: 'center',
|
||||
position: 'absolute',
|
||||
bottom: '7mm', // 3mm bleed + 4mm safe margin
|
||||
left: '6mm', // Match backContainer paddingLeft for flush alignment
|
||||
},
|
||||
addressFooterText: {
|
||||
fontSize: 6.5,
|
||||
color: COLORS.primary,
|
||||
letterSpacing: 0.4,
|
||||
fontWeight: 'normal',
|
||||
},
|
||||
});
|
||||
|
||||
// SVG Icons
|
||||
const PhoneIcon = () => (
|
||||
<Svg viewBox="0 0 24 24" width={7} height={7}>
|
||||
<Svg viewBox="0 0 24 24" width={9} height={9}>
|
||||
<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"
|
||||
fill="#000000"
|
||||
fill={COLORS.primary}
|
||||
/>
|
||||
</Svg>
|
||||
);
|
||||
|
||||
const EmailIcon = () => (
|
||||
<Svg viewBox="0 0 24 24" width={7} height={7}>
|
||||
<Svg viewBox="0 0 24 24" width={9} height={9}>
|
||||
<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"
|
||||
fill="#000000"
|
||||
fill={COLORS.primary}
|
||||
/>
|
||||
</Svg>
|
||||
);
|
||||
|
||||
const GlobeIcon = () => (
|
||||
<Svg viewBox="0 0 24 24" width={7} height={7}>
|
||||
<Svg viewBox="0 0 24 24" width={9} height={9}>
|
||||
<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"
|
||||
fill="#000000"
|
||||
fill={COLORS.primary}
|
||||
/>
|
||||
</Svg>
|
||||
);
|
||||
|
||||
const LocationIcon = () => (
|
||||
<Svg viewBox="0 0 24 24" width={9} height={9}>
|
||||
<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"
|
||||
fill={COLORS.primary}
|
||||
/>
|
||||
</Svg>
|
||||
);
|
||||
|
||||
const ScanIcon = () => (
|
||||
<Svg viewBox="0 0 24 24" width={7} height={7}>
|
||||
<Svg viewBox="0 0 24 24" width={9} height={9}>
|
||||
<Path
|
||||
d="M3 3h6v2H5v4H3V3zm18 0h-6v2h4v4h2V3zM3 21h6v-2H5v-4H3v6zm18 0h-6v-2h4v-4h2v6zM7 7h10v10H7V7z"
|
||||
fill="#000000"
|
||||
fill={COLORS.primary}
|
||||
/>
|
||||
</Svg>
|
||||
);
|
||||
@@ -314,18 +450,64 @@ const parseSvgPaths = (filePath: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
const VectorLogo = ({ paths, style }: { paths: { d: string; fill: string }[]; style: any }) => (
|
||||
const VectorLogo = ({
|
||||
paths,
|
||||
style,
|
||||
overrideColor,
|
||||
overrideWhiteColor,
|
||||
overrideBlackColor,
|
||||
pathOverrides,
|
||||
solidPaths,
|
||||
solidBackgroundColor = '#000a66',
|
||||
}: {
|
||||
paths: { d: string; fill: string }[];
|
||||
style: any;
|
||||
overrideColor?: string;
|
||||
overrideWhiteColor?: string;
|
||||
overrideBlackColor?: string;
|
||||
pathOverrides?: Record<number, string>;
|
||||
solidPaths?: number[];
|
||||
solidBackgroundColor?: string;
|
||||
}) => (
|
||||
<Svg viewBox="0 0 295 99" style={style}>
|
||||
<G fillRule="evenodd" clipRule="evenodd">
|
||||
{paths.map((p, i) => (
|
||||
<Path key={i} d={p.d} fill={p.fill} />
|
||||
))}
|
||||
<G {...({ fillRule: 'evenodd', clipRule: 'evenodd' } as any)}>
|
||||
{/* Background layer: solid fills to prevent see-through */}
|
||||
{solidPaths &&
|
||||
solidBackgroundColor &&
|
||||
paths.map((p, i) => {
|
||||
if (!solidPaths.includes(i)) return null;
|
||||
let d = p.d;
|
||||
const zIndex = d.toUpperCase().indexOf('Z');
|
||||
if (zIndex !== -1) {
|
||||
d = d.substring(0, zIndex + 1);
|
||||
}
|
||||
return <Path key={`bg-${i}`} d={d} fill={solidBackgroundColor} />;
|
||||
})}
|
||||
{paths.map((p, i) => {
|
||||
let fill = overrideColor || p.fill;
|
||||
const d = p.d;
|
||||
|
||||
if (pathOverrides && pathOverrides[i]) {
|
||||
fill = pathOverrides[i];
|
||||
} else if (
|
||||
overrideWhiteColor &&
|
||||
(p.fill === 'white' || p.fill === '#ffffff' || p.fill === '#fff')
|
||||
) {
|
||||
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} />;
|
||||
})}
|
||||
</G>
|
||||
</Svg>
|
||||
);
|
||||
|
||||
export const IconSolar = ({ style, color = 'white' }: { style: any; color?: string }) => (
|
||||
<Svg viewBox="0 0 46 45" width={14} height={14} style={style}>
|
||||
<Svg viewBox="0 0 46 45" width={18} height={18} style={style}>
|
||||
<G>
|
||||
<Path
|
||||
d="M44.798,22.471c-0,-11.988 -9.733,-21.721 -21.722,-21.721c-11.988,0 -21.721,9.733 -21.721,21.721c-0,11.989 9.733,21.722 21.721,21.722c11.989,0 21.722,-9.733 21.722,-21.722Z"
|
||||
@@ -435,7 +617,7 @@ export const AbstractCable = ({
|
||||
);
|
||||
|
||||
export const IconLowVoltage = ({ style, color = 'white' }: { style: any; color?: string }) => (
|
||||
<Svg viewBox="0 0 45 45" width={14} height={14} style={style}>
|
||||
<Svg viewBox="0 0 45 45" width={18} height={18} style={style}>
|
||||
<G>
|
||||
<Path
|
||||
d="M44.193,22.471c0,-11.988 -9.733,-21.721 -21.722,-21.721c-11.988,0 -21.721,9.733 -21.721,21.721c0,11.989 9.733,21.722 21.722,21.722c11.988,0 21.721,-9.733 21.721,-21.722Z"
|
||||
@@ -462,7 +644,7 @@ export const IconLowVoltage = ({ style, color = 'white' }: { style: any; color?:
|
||||
);
|
||||
|
||||
export const IconMediumVoltage = ({ style, color = 'white' }: { style: any; color?: string }) => (
|
||||
<Svg viewBox="0 0 46 45" width={14} height={14} style={style}>
|
||||
<Svg viewBox="0 0 46 45" width={18} height={18} style={style}>
|
||||
<G>
|
||||
<Path
|
||||
d="M44.475,22.471c0,-11.988 -9.733,-21.721 -21.721,-21.721c-11.988,0 -21.722,9.733 -21.722,21.721c0,11.989 9.734,21.722 21.722,21.722c11.988,0 21.721,-9.733 21.721,-21.722Z"
|
||||
@@ -505,7 +687,7 @@ export const IconMediumVoltage = ({ style, color = 'white' }: { style: any; colo
|
||||
);
|
||||
|
||||
export const IconHighVoltage = ({ style, color = 'white' }: { style: any; color?: string }) => (
|
||||
<Svg viewBox="0 0 46 46" width={14} height={14} style={style}>
|
||||
<Svg viewBox="0 0 46 46" width={18} height={18} style={style}>
|
||||
<G>
|
||||
<Path
|
||||
d="M44.612,22.931c0,-11.988 -9.733,-21.722 -21.721,-21.722c-11.988,0 -21.722,9.734 -21.722,21.722c0,11.988 9.734,21.721 21.722,21.721c11.988,0 21.721,-9.733 21.721,-21.721Z"
|
||||
@@ -549,7 +731,7 @@ export const IconHighVoltage = ({ style, color = 'white' }: { style: any; color?
|
||||
style={{ fill: 'none', stroke: color, strokeWidth: '0.33px' }}
|
||||
/>
|
||||
<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' }}
|
||||
/>
|
||||
<Path
|
||||
@@ -608,8 +790,13 @@ export const IconHighVoltage = ({ style, color = 'white' }: { style: any; color?
|
||||
export const PDFBusinessCard: React.FC<PDFBusinessCardProps> = ({
|
||||
person,
|
||||
qrCodeDataUrl,
|
||||
isWhiteVariant = false,
|
||||
variant = 'blue',
|
||||
isViaprinto = false,
|
||||
withTruck = true,
|
||||
withCropMarks = false,
|
||||
}) => {
|
||||
const isWhiteVariant = variant === 'white' || variant === 'black';
|
||||
|
||||
const logoWhitePath = path.join(process.cwd(), 'public', 'logo-green-text.svg');
|
||||
const logoBluePath = path.join(process.cwd(), 'public', 'logo-green-text-blue.svg');
|
||||
const logoBlueWithTaglinePath = path.join(
|
||||
@@ -617,14 +804,55 @@ export const PDFBusinessCard: React.FC<PDFBusinessCardProps> = ({
|
||||
'public',
|
||||
'logo-green-text-blue-with-tagline.svg',
|
||||
);
|
||||
const logoBlackWithTaglinePath = path.join(
|
||||
process.cwd(),
|
||||
'public',
|
||||
'logo-green-text-black-with-tagline.svg',
|
||||
);
|
||||
|
||||
const logoWhitePaths = parseSvgPaths(logoWhitePath);
|
||||
const logoBluePaths = parseSvgPaths(logoBluePath);
|
||||
const logoBlueWithTaglinePaths = parseSvgPaths(logoBlueWithTaglinePath);
|
||||
const logoBlackWithTaglinePaths = parseSvgPaths(logoBlackWithTaglinePath);
|
||||
|
||||
const getFrontLogoPaths = () => {
|
||||
if (variant === 'black') return logoBlackWithTaglinePaths;
|
||||
if (variant === 'white') return logoBlueWithTaglinePaths;
|
||||
return logoWhitePaths;
|
||||
};
|
||||
|
||||
const getBackLogoPaths = () => {
|
||||
// We use the original black logo to preserve the exact colors of the Truck and KLZ.
|
||||
// The tagline will be hidden via pathOverrides in the JSX.
|
||||
return logoBlackWithTaglinePaths;
|
||||
};
|
||||
|
||||
const bgImagePath = isWhiteVariant
|
||||
? path.join(process.cwd(), 'public', 'truck_original_rasterized.png')
|
||||
? path.join(process.cwd(), 'public', 'media', 'cable_drums_truck.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 (
|
||||
<Document
|
||||
title={`Business Card - ${person.name}`}
|
||||
@@ -632,75 +860,90 @@ export const PDFBusinessCard: React.FC<PDFBusinessCardProps> = ({
|
||||
creator="KLZ Print Engine"
|
||||
>
|
||||
{/* VORDERSEITE (Blue page with logo, truck and tagline) */}
|
||||
<Page
|
||||
size={[258, 173]}
|
||||
style={[styles.pageBleed, isWhiteVariant && { backgroundColor: '#ffffff' }]}
|
||||
>
|
||||
<View style={styles.truckWrapper}>
|
||||
<Image
|
||||
src={bgImagePath}
|
||||
style={[styles.frontBackgroundImage, isWhiteVariant && { opacity: 0.08 }]}
|
||||
/>
|
||||
</View>
|
||||
<View
|
||||
style={[styles.frontGreenBarBottom, isWhiteVariant && { backgroundColor: '#ffffff' }]}
|
||||
>
|
||||
<Text style={[styles.frontTaglineBar, isWhiteVariant && { color: COLORS.black }]}>
|
||||
VON 0,6 KV BIS 110 KV
|
||||
</Text>
|
||||
<View style={styles.frontIconsGroup}>
|
||||
<View style={styles.frontBrandIconWrapper}>
|
||||
<IconLowVoltage
|
||||
style={styles.frontBrandIcon}
|
||||
color={isWhiteVariant ? COLORS.black : 'white'}
|
||||
/>
|
||||
<Text style={[styles.frontBrandText, isWhiteVariant && { color: COLORS.black }]}>
|
||||
Niederspannung
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.frontBrandIconWrapper}>
|
||||
<IconMediumVoltage
|
||||
style={styles.frontBrandIcon}
|
||||
color={isWhiteVariant ? COLORS.black : 'white'}
|
||||
/>
|
||||
<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}>
|
||||
<IconSolar
|
||||
style={styles.frontBrandIcon}
|
||||
color={isWhiteVariant ? COLORS.black : 'white'}
|
||||
/>
|
||||
<Text style={[styles.frontBrandText, isWhiteVariant && { color: COLORS.black }]}>
|
||||
Solar
|
||||
</Text>
|
||||
{renderPageWrapper(
|
||||
<>
|
||||
<View style={styles.frontFooterArea}>
|
||||
<Text style={[styles.frontTaglineBar, isWhiteVariant && { color: COLORS.black }]}>
|
||||
Von 0,6/1,0 kV bis 64/110 kV
|
||||
</Text>
|
||||
<View style={styles.frontIconsGroup}>
|
||||
<View style={styles.frontBrandIconWrapper}>
|
||||
<IconLowVoltage
|
||||
style={styles.frontBrandIcon}
|
||||
color={isWhiteVariant ? COLORS.black : 'white'}
|
||||
/>
|
||||
<Text style={[styles.frontBrandText, isWhiteVariant && { color: COLORS.black }]}>
|
||||
Niederspannung
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.frontBrandIconWrapper}>
|
||||
<IconMediumVoltage
|
||||
style={styles.frontBrandIcon}
|
||||
color={isWhiteVariant ? COLORS.black : 'white'}
|
||||
/>
|
||||
<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}>
|
||||
<IconSolar
|
||||
style={styles.frontBrandIcon}
|
||||
color={isWhiteVariant ? COLORS.black : 'white'}
|
||||
/>
|
||||
<Text style={[styles.frontBrandText, isWhiteVariant && { color: COLORS.black }]}>
|
||||
Solar
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.frontContainer}>
|
||||
<VectorLogo
|
||||
paths={isWhiteVariant ? logoBlueWithTaglinePaths : logoWhitePaths}
|
||||
style={styles.frontLogo}
|
||||
/>
|
||||
</View>
|
||||
</Page>
|
||||
<View style={styles.frontContainer}>
|
||||
{/* 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' : COLORS.primary}
|
||||
/>
|
||||
</View>
|
||||
</>,
|
||||
[styles.pageBleed, isWhiteVariant && { backgroundColor: '#ffffff' }],
|
||||
)}
|
||||
|
||||
{/* RÜCKSEITE (White page with contact info) */}
|
||||
<Page size={[258, 173]} style={styles.pageBleedBack}>
|
||||
{renderPageWrapper(
|
||||
<View style={styles.backContainer}>
|
||||
<View style={styles.backHeader}>
|
||||
<VectorLogo paths={logoBluePaths} style={styles.backLogo} />
|
||||
<VectorLogo
|
||||
paths={getBackLogoPaths()}
|
||||
style={styles.backLogo}
|
||||
overrideBlackColor={COLORS.primary}
|
||||
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.qrContainer}>
|
||||
<View style={[styles.qrCodeWrapper, { borderColor: COLORS.darkGreen }]}>
|
||||
<Image src={qrCodeDataUrl} style={styles.qrCode} />
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.backBody}>
|
||||
@@ -721,7 +964,7 @@ export const PDFBusinessCard: React.FC<PDFBusinessCardProps> = ({
|
||||
<View style={styles.iconWrapper}>
|
||||
<EmailIcon />
|
||||
</View>
|
||||
<Text style={styles.contactText}>{person.email}</Text>
|
||||
<Text style={styles.contactText}>{person.email.toLowerCase()}</Text>
|
||||
</View>
|
||||
<View style={styles.contactRow}>
|
||||
<View style={styles.iconWrapper}>
|
||||
@@ -731,18 +974,16 @@ export const PDFBusinessCard: React.FC<PDFBusinessCardProps> = ({
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.rightColumn}>
|
||||
<View style={styles.qrContainer}>
|
||||
<View style={styles.qrCodeWrapper}>
|
||||
<Image src={qrCodeDataUrl} style={styles.qrCode} />
|
||||
</View>
|
||||
<Text style={styles.qrLabel}>ABSPEICHERN</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</Page>
|
||||
|
||||
<View style={styles.addressFooter}>
|
||||
<Text style={styles.addressFooterText}>
|
||||
KLZ Vertriebs GmbH · Raiffeisenstraße 22 · 73630 Remshalden
|
||||
</Text>
|
||||
</View>
|
||||
</View>,
|
||||
styles.pageBleedBack,
|
||||
)}
|
||||
</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 {
|
||||
page: any;
|
||||
locale?: string;
|
||||
@@ -360,9 +396,11 @@ export const PDFPage: React.FC<PDFPageProps> = ({ page, locale = 'de' }) => {
|
||||
|
||||
<View style={styles.content}>
|
||||
<View>
|
||||
{page.content?.root?.children?.map((node: any, i: number) =>
|
||||
renderLexicalNode(node, i),
|
||||
)}
|
||||
{typeof page.content === 'string'
|
||||
? renderMarkdown(page.content)
|
||||
: page.content?.root?.children?.map((node: any, i: number) =>
|
||||
renderLexicalNode(node, i),
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
|
||||
@@ -39,7 +39,10 @@ export async function getProductBySlug(slug: string, locale: string): Promise<Pr
|
||||
const fileContent = await fs.readFile(filePath, 'utf-8');
|
||||
const { data, content } = matter(fileContent);
|
||||
|
||||
let parsedContent = content;
|
||||
const { fixMdxDataProps } = await import('./mdx-utils');
|
||||
const fixedContent = fixMdxDataProps(content);
|
||||
|
||||
let parsedContent = fixedContent;
|
||||
try {
|
||||
if (content.trim().startsWith('{')) {
|
||||
parsedContent = JSON.parse(content);
|
||||
|
||||
@@ -56,6 +56,11 @@ export class GlitchtipErrorReportingService implements ErrorReportingService {
|
||||
tracesSampleRate: this.options.tracesSampleRate ?? 0.1,
|
||||
replaysOnErrorSampleRate: 1.0,
|
||||
replaysSessionSampleRate: 0.1,
|
||||
ignoreErrors: [
|
||||
'ChunkLoadError',
|
||||
'Failed to fetch dynamically imported module',
|
||||
'Failed to find Server Action',
|
||||
],
|
||||
});
|
||||
}
|
||||
return Sentry;
|
||||
|
||||
13
lib/slugs.ts
13
lib/slugs.ts
@@ -31,6 +31,11 @@ export async function mapSlugToFileSlug(translatedSlug: string, locale: string):
|
||||
return slugs.categories[translatedSlug as keyof typeof slugs.categories];
|
||||
}
|
||||
|
||||
// Check posts
|
||||
if ((slugs as any).posts && translatedSlug in (slugs as any).posts) {
|
||||
return (slugs as any).posts[translatedSlug];
|
||||
}
|
||||
|
||||
// Return original slug if no mapping found
|
||||
return translatedSlug;
|
||||
}
|
||||
@@ -45,7 +50,7 @@ export async function mapFileSlugToTranslated(fileSlug: string, locale: string):
|
||||
const messages = getMessages(locale);
|
||||
const slugs = messages.Slugs;
|
||||
|
||||
const sections = [slugs.pages, slugs.products, slugs.categories];
|
||||
const sections = [slugs.pages, slugs.products, slugs.categories, (slugs as any).posts];
|
||||
|
||||
for (const sectionData of sections) {
|
||||
if (sectionData && typeof sectionData === 'object') {
|
||||
@@ -63,16 +68,16 @@ export async function mapFileSlugToTranslated(fileSlug: string, locale: string):
|
||||
|
||||
/**
|
||||
* Gets all translated slugs for a section
|
||||
* @param section - The section name (pages, products, categories)
|
||||
* @param section - The section name (pages, products, categories, posts)
|
||||
* @param locale - The current locale
|
||||
* @returns Object mapping translated slugs to file slugs
|
||||
*/
|
||||
export async function getSlugMappings(
|
||||
section: 'pages' | 'products' | 'categories',
|
||||
section: 'pages' | 'products' | 'categories' | 'posts',
|
||||
locale: string,
|
||||
): Promise<Record<string, string>> {
|
||||
const messages = getMessages(locale);
|
||||
const sectionData = messages.Slugs[section];
|
||||
const sectionData = (messages.Slugs as any)[section];
|
||||
|
||||
if (sectionData && typeof sectionData === 'object') {
|
||||
return sectionData as Record<string, string>;
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
"pages": {
|
||||
"impressum": "impressum",
|
||||
"datenschutz": "datenschutz",
|
||||
"agbs": "terms",
|
||||
"kontakt": "contact",
|
||||
"agbs": "agbs",
|
||||
"kontakt": "kontakt",
|
||||
"team": "team",
|
||||
"blog": "blog",
|
||||
"produkte": "products",
|
||||
"produkte": "produkte",
|
||||
"start": "start",
|
||||
"danke": "thanks",
|
||||
"niederspannungskabel": "low-voltage-cables",
|
||||
@@ -47,6 +47,39 @@
|
||||
"mittelspannungskabel": "medium-voltage-cables",
|
||||
"hochspannungskabel": "high-voltage-cables",
|
||||
"solarkabel": "solar-cables"
|
||||
},
|
||||
"posts": {
|
||||
"100-erneuerbare-energie-nur-mit-der-richtigen-kabelinfrastruktur": "100-erneuerbare-energie-nur-mit-der-richtigen-kabelinfrastruktur",
|
||||
"das-muessen-sie-ueber-erneuerbare-energien-im-jahr-2025-wissen": "das-muessen-sie-ueber-erneuerbare-energien-im-jahr-2025-wissen",
|
||||
"die-besten-erdkabel-fuer-windkraft-und-solar-jetzt-bei-uns-bestellen": "die-besten-erdkabel-fuer-windkraft-und-solar-jetzt-bei-uns-bestellen",
|
||||
"die-kunst-der-kabellogistik-der-transport-des-fundamentes-moderner-energienetze": "die-kunst-der-kabellogistik-der-transport-des-fundamentes-moderner-energienetze",
|
||||
"die-perfekte-kabelanfrage-so-sparen-sie-sich-unnoetige-rueckfragen": "die-perfekte-kabelanfrage-so-sparen-sie-sich-unnoetige-rueckfragen",
|
||||
"erkenntnisse-ueber-die-gruene-energiewende-herausforderungen-und-chancen": "erkenntnisse-ueber-die-gruene-energiewende-herausforderungen-und-chancen",
|
||||
"gruene-energie-beginnt-unter-der-erde-und-zwar-mit-plan": "gruene-energie-beginnt-unter-der-erde-und-zwar-mit-plan",
|
||||
"johannes-gleich-startet-als-senior-key-account-manager-durch": "johannes-gleich-startet-als-senior-key-account-manager-durch",
|
||||
"kabelabkuerzungen-entschluesselt-der-schluessel-zur-richtigen-kabelwahl": "kabelabkuerzungen-entschluesselt-der-schluessel-zur-richtigen-kabelwahl",
|
||||
"kabeltrommelqualitaet-die-grundlage-der-kabelzuverlaessigkeit": "kabeltrommelqualitaet-die-grundlage-der-kabelzuverlaessigkeit",
|
||||
"klimaneutral-bis-2050-was-wir-tun-muessen-um-das-ziel-zu-erreichen": "klimaneutral-bis-2050-was-wir-tun-muessen-um-das-ziel-zu-erreichen",
|
||||
"klz-im-adressbuch-der-windenergie-2025": "klz-im-adressbuch-der-windenergie-2025",
|
||||
"klz-waechst-weiter-neue-staerke-im-bereich-financial-sales": "klz-waechst-weiter-neue-staerke-im-bereich-financial-sales",
|
||||
"kostenvergleich-kupfer-vs-aluminiumkabel-in-windparks-was-lohnt-sich-langfristig": "kostenvergleich-kupfer-vs-aluminiumkabel-in-windparks-was-lohnt-sich-langfristig",
|
||||
"milliarden-paket-fuer-infrastruktur-der-kabel-boom-steht-bevor": "milliarden-paket-fuer-infrastruktur-der-kabel-boom-steht-bevor",
|
||||
"n2xsf2y-mittelspannungskabel-energieprojekt": "n2xsf2y-mittelspannungskabel-energieprojekt",
|
||||
"na2xsf2y-dreileiter-mittelspannungskabel-lieferbar": "na2xsf2y-dreileiter-mittelspannungskabel-lieferbar",
|
||||
"netzausbau-2025-warum-jede-neue-leitung-ein-schritt-zur-energiewende-ist": "netzausbau-2025-warum-jede-neue-leitung-ein-schritt-zur-energiewende-ist",
|
||||
"recycling-von-kabeltrommeln-nachhaltigkeit-im-windkraftprojekt": "recycling-von-kabeltrommeln-nachhaltigkeit-im-windkraftprojekt",
|
||||
"reicht-windenergie-wirklich-nicht-ein-blick-hinter-die-schlagzeilen": "reicht-windenergie-wirklich-nicht-ein-blick-hinter-die-schlagzeilen",
|
||||
"sicherheit-bei-kabeltrommeln-unfallfrei-und-effizient-arbeiten": "sicherheit-bei-kabeltrommeln-unfallfrei-und-effizient-arbeiten",
|
||||
"so-waehlen-sie-das-richtige-kabel-fuer-ihr-naechstes-projekt-aus": "so-waehlen-sie-das-richtige-kabel-fuer-ihr-naechstes-projekt-aus",
|
||||
"von-smart-bis-nachhaltig-so-sieht-die-energiewirtschaft-in-naher-zukunft-aus": "von-smart-bis-nachhaltig-so-sieht-die-energiewirtschaft-in-naher-zukunft-aus",
|
||||
"warum-die-richtigen-kabel-der-geheime-held-der-gruenen-energie-sind": "warum-die-richtigen-kabel-der-geheime-held-der-gruenen-energie-sind",
|
||||
"warum-windpark-netzanschlusskabel-extremen-belastungen-standhalten-muessen": "warum-windpark-netzanschlusskabel-extremen-belastungen-standhalten-muessen",
|
||||
"was-macht-ein-erstklassiges-kabel-aus-finden-sie-es-hier-heraus": "was-macht-ein-erstklassiges-kabel-aus-finden-sie-es-hier-heraus",
|
||||
"welche-kabel-fuer-windkraft-unterschiede-von-nieder-bis-hoechstspannung-erklaert": "welche-kabel-fuer-windkraft-unterschiede-von-nieder-bis-hoechstspannung-erklaert",
|
||||
"wie-die-kabelbranche-nachhaltigkeit-und-erneuerbare-energien-vorantreibt": "wie-die-kabelbranche-nachhaltigkeit-und-erneuerbare-energien-vorantreibt",
|
||||
"willkommen-in-der-zukunft-von-klz-unsere-neue-website-ist-online": "willkommen-in-der-zukunft-von-klz-unsere-neue-website-ist-online",
|
||||
"windparkbau-im-fokus-drei-typische-kabelherausforderungen": "windparkbau-im-fokus-drei-typische-kabelherausforderungen",
|
||||
"zukunft-sichern-mit-h1z2z2-k-unser-solarkabel-zur-intersolar-2025": "zukunft-sichern-mit-h1z2z2-k-unser-solarkabel-zur-intersolar-2025"
|
||||
}
|
||||
},
|
||||
"Index": {
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
"pages": {
|
||||
"legal-notice": "impressum",
|
||||
"privacy-policy": "datenschutz",
|
||||
"terms": "terms",
|
||||
"contact": "contact",
|
||||
"terms": "agbs",
|
||||
"contact": "kontakt",
|
||||
"team": "team",
|
||||
"blog": "blog",
|
||||
"products": "products",
|
||||
"products": "produkte",
|
||||
"start": "start",
|
||||
"thanks": "thanks",
|
||||
"low-voltage-cables": "low-voltage-cables",
|
||||
@@ -47,6 +47,37 @@
|
||||
"medium-voltage-cables": "medium-voltage-cables",
|
||||
"high-voltage-cables": "high-voltage-cables",
|
||||
"solar-cables": "solar-cables"
|
||||
},
|
||||
"posts": {
|
||||
"100-renewable-energy-only-with-the-right-cable-infrastructure": "100-erneuerbare-energie-nur-mit-der-richtigen-kabelinfrastruktur",
|
||||
"billion-euro-package-for-infrastructure-the-cable-boom-is-coming": "milliarden-paket-fuer-infrastruktur-der-kabel-boom-steht-bevor",
|
||||
"cable-abbreviations-decoded-the-key-to-choosing-the-right-cable": "kabelabkuerzungen-entschluesselt-der-schluessel-zur-richtigen-kabelwahl",
|
||||
"cable-drum-quality-the-foundation-of-cable-reliability": "kabeltrommelqualitaet-die-grundlage-der-kabelzuverlaessigkeit",
|
||||
"cable-drum-safety-ensuring-smooth-operations-and-accident-free-environments": "sicherheit-bei-kabeltrommeln-unfallfrei-und-effizient-arbeiten",
|
||||
"climate-neutral-by-2050-what-we-need-to-do-to-achieve-this-goal": "klimaneutral-bis-2050-was-wir-tun-muessen-um-das-ziel-zu-erreichen",
|
||||
"cost-comparison-copper-vs-aluminum-cables-in-wind-farms-which-is-worthwhile-in-the-long-term": "kostenvergleich-kupfer-vs-aluminiumkabel-in-windparks-was-lohnt-sich-langfristig",
|
||||
"expanding-the-grid-by-2025-building-the-foundation-for-a-successful-energy-transition": "netzausbau-2025-warum-jede-neue-leitung-ein-schritt-zur-energiewende-ist",
|
||||
"eye-opening-realities-of-green-energy-transformation": "erkenntnisse-ueber-die-gruene-energiewende-herausforderungen-und-chancen",
|
||||
"focus-on-wind-farm-construction-three-typical-cable-challenges": "windparkbau-im-fokus-drei-typische-kabelherausforderungen",
|
||||
"from-smart-to-sustainable-this-is-what-the-energy-industry-will-look-like-in-the-near-future": "von-smart-bis-nachhaltig-so-sieht-die-energiewirtschaft-in-naher-zukunft-aus",
|
||||
"green-energy-starts-underground-and-with-a-plan": "gruene-energie-beginnt-unter-der-erde-und-zwar-mit-plan",
|
||||
"how-the-cable-industry-is-driving-sustainability-and-renewable-energies-forward": "wie-die-kabelbranche-nachhaltigkeit-und-erneuerbare-energien-vorantreibt",
|
||||
"how-the-right-cables-quietly-power-the-green-energy-revolution": "warum-die-richtigen-kabel-der-geheime-held-der-gruenen-energie-sind",
|
||||
"how-to-choose-the-right-cable-for-your-next-project": "so-waehlen-sie-das-richtige-kabel-fuer-ihr-naechstes-projekt-aus",
|
||||
"is-wind-energy-really-enough-a-deeper-dive-behind-the-headlines": "reicht-windenergie-wirklich-nicht-ein-blick-hinter-die-schlagzeilen",
|
||||
"klz-in-the-directory-of-wind-energy-2025": "klz-im-adressbuch-der-windenergie-2025",
|
||||
"na2xsf2y-three-conductor-medium-voltage-cable-available": "na2xsf2y-dreileiter-mittelspannungskabel-lieferbar",
|
||||
"recycling-of-cable-drums-sustainability-in-wind-power-projects": "recycling-von-kabeltrommeln-nachhaltigkeit-im-windkraftprojekt",
|
||||
"securing-the-future-with-h1z2z2-k-our-solar-cable-for-intersolar-2025": "zukunft-sichern-mit-h1z2z2-k-unser-solarkabel-zur-intersolar-2025",
|
||||
"the-art-of-cable-logistics-moving-the-backbone-of-modern-energy-networks": "die-kunst-der-kabellogistik-der-transport-des-fundamentes-moderner-energienetze",
|
||||
"the-best-underground-cables-for-wind-power-and-solar-order-from-us-now": "die-besten-erdkabel-fuer-windkraft-und-solar-jetzt-bei-uns-bestellen",
|
||||
"the-perfect-cable-inquiry-how-to-save-yourself-unnecessary-queries": "die-perfekte-kabelanfrage-so-sparen-sie-sich-unnoetige-rueckfragen",
|
||||
"this-what-you-need-to-know-about-renewable-energies-in-2025": "das-muessen-sie-ueber-erneuerbare-energien-im-jahr-2025-wissen",
|
||||
"welcome-to-the-future-of-klz-our-new-website-is-live": "willkommen-in-der-zukunft-von-klz-unsere-neue-website-ist-online",
|
||||
"what-makes-a-first-class-cable-find-out-here": "was-macht-ein-erstklassiges-kabel-aus-finden-sie-es-hier-heraus",
|
||||
"which-cables-for-wind-power-differences-from-low-to-extra-high-voltage-explained-2": "welche-kabel-fuer-windkraft-unterschiede-von-nieder-bis-hoechstspannung-erklaert",
|
||||
"why-the-n2xsf2y-is-the-ideal-cable-for-your-energy-project": "n2xsf2y-mittelspannungskabel-energieprojekt",
|
||||
"why-wind-farm-grid-connection-cables-must-withstand-extreme-loads": "warum-windpark-netzanschlusskabel-extremen-belastungen-standhalten-muessen"
|
||||
}
|
||||
},
|
||||
"Index": {
|
||||
|
||||
@@ -116,7 +116,7 @@
|
||||
"prepare": "husky",
|
||||
"preinstall": "npx only-allow pnpm"
|
||||
},
|
||||
"version": "2.3.23",
|
||||
"version": "2.3.36",
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
"@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.
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.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user