Compare commits

...

24 Commits

Author SHA1 Message Date
bff150b1a7 feat: add new business card assets to public downloads directory
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 12s
Build & Deploy / 🧪 QA (push) Successful in 1m32s
Build & Deploy / 🏗️ Build (push) Successful in 3m15s
Build & Deploy / 🚀 Deploy (push) Successful in 20s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m38s
Build & Deploy / 🔔 Notify (push) Successful in 2s
Nightly QA / 💨 Smoke & Health (push) Successful in 1m24s
2026-06-22 09:23:15 +02:00
8ffb1e5f6a chore(release): 2.3.28 - Add Gotify notifications
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 11s
Build & Deploy / 🧪 QA (push) Successful in 1m27s
Build & Deploy / 🏗️ Build (push) Successful in 3m15s
Build & Deploy / 🚀 Deploy (push) Successful in 18s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m30s
Build & Deploy / 🔔 Notify (push) Successful in 3s
Nightly QA / 💨 Smoke & Health (push) Successful in 1m23s
2026-06-21 09:34:05 +02:00
42ff5b6226 chore(release): 2.3.27
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 10s
Build & Deploy / 🧪 QA (push) Successful in 1m29s
Build & Deploy / 🏗️ Build (push) Successful in 2m49s
Build & Deploy / 🚀 Deploy (push) Successful in 17s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m10s
Build & Deploy / 🔔 Notify (push) Successful in 2s
Nightly QA / 💨 Smoke & Health (push) Successful in 3m16s
2026-06-20 21:51:28 +02:00
539c503d0d fix(blog): remove unused metadata import causing lint warning 2026-06-20 21:51:11 +02:00
4c473a2ffd chore(release): 2.3.26
Some checks failed
Build & Deploy / 🔍 Prepare (push) Failing after 0s
Build & Deploy / 🧪 QA (push) Has been skipped
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Failing after 3s
2026-06-20 09:32:53 +02:00
4f64a94a15 fix(blog): make category filter badges functional and preserve filter in pagination 2026-06-20 09:32:42 +02:00
5a82243965 fix(pdf): calculate exact page size points to avoid bleed zone gap for Saxoprint
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 8s
Build & Deploy / 🧪 QA (push) Successful in 55s
Build & Deploy / 🏗️ Build (push) Successful in 1m53s
Build & Deploy / 🚀 Deploy (push) Successful in 15s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m3s
Build & Deploy / 🔔 Notify (push) Successful in 1s
Nightly QA / 💨 Smoke & Health (push) Successful in 1m17s
2026-06-14 09:44:50 +02:00
ebb14ee302 feat(pdf): add Saxoprint CMYK variant (no mask, no pantone)
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 7s
Build & Deploy / 🧪 QA (push) Successful in 53s
Build & Deploy / 🏗️ Build (push) Successful in 3m14s
Build & Deploy / 🚀 Deploy (push) Successful in 14s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m6s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-14 09:37:31 +02:00
80f5e46d88 fix(pdf): fix logo color override on front side
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 10s
Build & Deploy / 🧪 QA (push) Successful in 1m19s
Build & Deploy / 🏗️ Build (push) Successful in 2m42s
Build & Deploy / 🚀 Deploy (push) Successful in 18s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m22s
Build & Deploy / 🔔 Notify (push) Successful in 4s
Nightly QA / 💨 Smoke & Health (push) Successful in 2m6s
2026-06-12 16:08:18 +02:00
b826b5b741 fix(pdf): change pantone fallback function to type 2 for better viewer compatibility
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 15s
Build & Deploy / 🧪 QA (push) Successful in 1m50s
Build & Deploy / 🏗️ Build (push) Successful in 2m51s
Build & Deploy / 🚀 Deploy (push) Successful in 18s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m24s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-12 15:44:54 +02:00
41228aca53 fix(pdf): remove letterpress mask and set correct logo color for white page
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 12s
Build & Deploy / 🧪 QA (push) Successful in 1m33s
Build & Deploy / 🏗️ Build (push) Has been cancelled
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
2026-06-12 15:42:14 +02:00
cd3b7c1e3b style(pdf): convert all text and icons on front side to klz-blau
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 13s
Build & Deploy / 🧪 QA (push) Successful in 1m33s
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled
2026-06-12 15:38:26 +02:00
ecd525d782 feat(pdf): build stream-hacking pipeline for 5-color pantone 289 c export
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 10s
Build & Deploy / 🧪 QA (push) Successful in 1m20s
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
2026-06-12 15:35:19 +02:00
52439cc5aa chore(release): bump version to 2.3.25
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 12s
Build & Deploy / 🧪 QA (push) Successful in 1m20s
Build & Deploy / 🏗️ Build (push) Successful in 2m37s
Build & Deploy / 🚀 Deploy (push) Successful in 17s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m7s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-12 14:58:31 +02:00
61bf8517d8 fix(pdf): add explicit type assertions to avoid TS2698 on spread
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 10s
Build & Deploy / 🧪 QA (push) Successful in 1m21s
Build & Deploy / 🏗️ Build (push) Successful in 2m39s
Build & Deploy / 🚀 Deploy (push) Successful in 19s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m23s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-12 14:48:52 +02:00
85d9e3a930 fix(pdf): revert address footer bottom to 7mm
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 10s
Build & Deploy / 🧪 QA (push) Failing after 1m21s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-12 14:45:51 +02:00
2f7d6befe4 fix(pdf): reduce gap under role and increase footer distance from bottom edge
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 10s
Build & Deploy / 🧪 QA (push) Failing after 1m22s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
2026-06-12 14:44:07 +02:00
a6ba484faa fix(pdf): increase role text spacing and link agbs pdf directly to static file
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 10s
Build & Deploy / 🧪 QA (push) Failing after 1m20s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-12 14:11:46 +02:00
d7178a9100 chore: clean up garbage files from business card generation and update PDFs
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 10s
Build & Deploy / 🧪 QA (push) Failing after 1m19s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-12 13:31:53 +02:00
1e39dc8567 fix(klz-2026): correctly hide tagline via path overrides without breaking KLZ letters
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 10s
Build & Deploy / 🧪 QA (push) Failing after 1m20s
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-12 13:24:24 +02:00
3c2aa8b71e fix(klz-2026): remove tagline from front page (white) while preserving it on back page (blue)
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 14s
Build & Deploy / 🧪 QA (push) Successful in 1m52s
Build & Deploy / 🏗️ Build (push) Successful in 2m53s
Build & Deploy / 🚀 Deploy (push) Successful in 18s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m25s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-12 13:10:38 +02:00
6e3befdfd9 fix(klz-2026): increase top padding to push header and content down on business card back
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 14s
Build & Deploy / 🧪 QA (push) Successful in 1m50s
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled
2026-06-12 13:07:46 +02:00
5377b3c81a feat(klz-2026): use external URL and static .vcf files for ultra-minimalist QR codes
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 13s
Build & Deploy / 🧪 QA (push) Successful in 1m25s
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled
2026-06-12 13:05:24 +02:00
941625a4e0 feat(klz-2026): use MECARD and level L for smaller, highly scannable QR codes on business cards
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 10s
Build & Deploy / 🧪 QA (push) Successful in 1m20s
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled
2026-06-12 13:01:49 +02:00
46 changed files with 324 additions and 81 deletions

View File

@@ -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"

View File

@@ -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"

View File

@@ -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">

View File

@@ -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"}} />

View File

@@ -215,8 +215,8 @@ const styles = StyleSheet.create({
bottom: 0,
left: 0,
right: 0,
height: '20mm', // Generous area for the footer
paddingBottom: '7mm', // 3mm bleed + 4mm safe margin
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',
@@ -253,7 +253,7 @@ const styles = StyleSheet.create({
// -- WHITE PAGE (Vorderseite) --
backContainer: {
flex: 1,
paddingTop: '3mm', // Reduced significantly to pull content up
paddingTop: '6mm', // Pushed down as requested
paddingLeft: '6mm',
paddingRight: '6mm',
flexDirection: 'column',
@@ -302,12 +302,12 @@ const styles = StyleSheet.create({
flexDirection: 'column',
},
nameSection: {
marginBottom: '2.5mm', // Clear separation between name block and contact block
marginBottom: '2.5mm', // Reduced to fix the 'Lücke' under the role
},
nameText: {
fontSize: 13,
fontWeight: 'bold',
color: COLORS.neutralDark,
color: COLORS.primary,
marginBottom: 1.5,
letterSpacing: -0.15,
},
@@ -334,7 +334,7 @@ const styles = StyleSheet.create({
},
contactText: {
fontSize: 7.5,
color: COLORS.neutralDark,
color: COLORS.primary,
fontWeight: 'normal',
paddingTop: 0.5,
letterSpacing: 0.1,
@@ -351,10 +351,10 @@ const styles = StyleSheet.create({
gap: 3,
},
qrCodeWrapper: {
width: '12mm',
height: '12mm',
width: '10mm',
height: '10mm',
border: `0.75pt solid ${COLORS.darkGreen}`,
padding: 2.5,
padding: 2,
backgroundColor: '#ffffff',
},
qrCode: {
@@ -372,7 +372,7 @@ const styles = StyleSheet.create({
},
addressFooterText: {
fontSize: 6.5,
color: COLORS.grayText,
color: COLORS.primary,
letterSpacing: 0.4,
fontWeight: 'normal',
},
@@ -383,7 +383,7 @@ const PhoneIcon = () => (
<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>
);
@@ -392,7 +392,7 @@ const EmailIcon = () => (
<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>
);
@@ -401,7 +401,7 @@ const GlobeIcon = () => (
<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>
);
@@ -410,7 +410,7 @@ 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="#000000"
fill={COLORS.primary}
/>
</Svg>
);
@@ -419,7 +419,7 @@ const ScanIcon = () => (
<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>
);
@@ -455,6 +455,7 @@ const VectorLogo = ({
style,
overrideColor,
overrideWhiteColor,
overrideBlackColor,
pathOverrides,
solidPaths,
solidBackgroundColor = '#000a66',
@@ -463,6 +464,7 @@ const VectorLogo = ({
style: any;
overrideColor?: string;
overrideWhiteColor?: string;
overrideBlackColor?: string;
pathOverrides?: Record<number, string>;
solidPaths?: number[];
solidBackgroundColor?: string;
@@ -492,6 +494,11 @@ const VectorLogo = ({
(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} />;
})}
@@ -815,6 +822,8 @@ export const PDFBusinessCard: React.FC<PDFBusinessCardProps> = ({
};
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;
};
@@ -822,6 +831,8 @@ export const PDFBusinessCard: React.FC<PDFBusinessCardProps> = ({
? 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 (
@@ -836,7 +847,7 @@ export const PDFBusinessCard: React.FC<PDFBusinessCardProps> = ({
);
}
return (
<Page size={[258, 173]} style={bleedStyle}>
<Page size={[mmToPt(91), mmToPt(61)]} style={bleedStyle}>
{children}
</Page>
);
@@ -903,16 +914,6 @@ export const PDFBusinessCard: React.FC<PDFBusinessCardProps> = ({
solidPaths={KLZ_PATHS}
solidBackgroundColor={isWhiteVariant ? '#ffffff' : COLORS.primary}
/>
{/* Letterpress Mask Layer: We use #ff00fe as a marker color. Only the "KLZ" letters! */}
{isViaprinto && (
<VectorLogo
paths={getFrontLogoPaths().filter((_, i) => KLZ_PATHS.includes(i))}
style={[styles.frontLogo, { position: 'absolute' }]}
overrideColor="#ff00fe"
solidPaths={KLZ_PATHS.map((_, i) => i)}
/>
)}
</View>
</>,
[styles.pageBleed, isWhiteVariant && { backgroundColor: '#ffffff' }],
@@ -924,8 +925,18 @@ export const PDFBusinessCard: React.FC<PDFBusinessCardProps> = ({
<VectorLogo
paths={getBackLogoPaths()}
style={styles.backLogo}
overrideWhiteColor={COLORS.black}
pathOverrides={KLZ_PATHS.reduce((acc, idx) => ({ ...acc, [idx]: 'none' }), {})}
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 File

@@ -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>

View File

@@ -116,7 +116,7 @@
"prepare": "husky",
"preinstall": "npx only-allow pnpm"
},
"version": "2.3.24",
"version": "2.3.28",
"pnpm": {
"onlyBuiltDependencies": [
"@parcel/watcher",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 KiB

View 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
View 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

View 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

View File

@@ -52,24 +52,26 @@ async function generateBusinessCards() {
for (const person of PEOPLE) {
console.log(`\nGenerating Business Card for: ${person.name}`);
// Generate VCard QR Code
const lastName = person.name.split(' ').slice(1).join(' ');
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,
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, '_');
@@ -119,7 +121,36 @@ END:VCARD`;
try {
execSync(cmykCommand, { stdio: 'inherit' });
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) {
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?`);
}

105
scripts/inject-pantone.ts Normal file
View 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);