Compare commits

..

70 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 1m19s
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
9b251101d4 chore(release): 2.3.24 - deploy AVB fix
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 14s
Build & Deploy / 🧪 QA (push) Successful in 2m2s
Build & Deploy / 🏗️ Build (push) Successful in 3m49s
Build & Deploy / 🚀 Deploy (push) Successful in 19s
Build & Deploy / 🧪 Post-Deploy Verification (push) Successful in 1m26s
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-12 12:11:45 +02:00
ff0d880bde style: shift text block upwards on white page for more distance to address
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 13s
Build & Deploy / 🧪 QA (push) Successful in 1m41s
Build & Deploy / 🏗️ Build (push) Successful in 2m44s
Build & Deploy / 🚀 Deploy (push) Successful in 19s
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
2026-06-12 12:05:05 +02:00
b78a848ec2 fix: ensure email addresses are strictly lowercase
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 18s
Build & Deploy / 🧪 QA (push) Successful in 1m59s
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 12:01:00 +02:00
e72879b222 style: change primary blue to website dark blue and set white page logo to black
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 24s
Build & Deploy / 🧪 QA (push) Successful in 1m38s
Build & Deploy / 🏗️ Build (push) Has started running
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 11:57:48 +02:00
5a2b0c543e style: remove truck illustration completely and increase bottom margin for print safety
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 20s
Build & Deploy / 🧪 QA (push) Successful in 1m41s
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 11:52:48 +02:00
1203c20166 fix: remove unintended truck background from the white contact page
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 16s
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
Build & Deploy / 🧪 QA (push) Has been cancelled
2026-06-12 11:51:53 +02:00
9944d3313b fix: correct background color of second page back to white
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 26s
Build & Deploy / 🧪 QA (push) Failing after 1m56s
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 9s
2026-06-12 11:48:20 +02:00
d1cbfc41c8 feat: add crop marks variant for print finishing
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 24s
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
Build & Deploy / 🧪 QA (push) Has been cancelled
2026-06-12 11:47:40 +02:00
09d435a1bc fix: align address footer flush left with contact icon column
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 21s
Build & Deploy / 🧪 QA (push) Failing after 1m52s
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 12s
2026-06-12 11:35:44 +02:00
8f34ac2a6d style: fine-tune white page typography - harmonize font sizes, spacing rhythm and letter-spacing
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 28s
Build & Deploy / 🧪 QA (push) Failing after 1m54s
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 12s
2026-06-12 11:32:44 +02:00
e5ecf16a66 fix: revert line spacing to compact layout, keep generous logo margin
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 22s
Build & Deploy / 🏗️ Build (push) Has been cancelled
Build & Deploy / 🧪 QA (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 11:30:39 +02:00
12eadba0ee fix: resolve PDF business card layout overlap and aspect ratio distortion
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 22s
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
Build & Deploy / 🧪 QA (push) Has been cancelled
2026-06-12 11:29:34 +02:00
0ad975f049 style: crisp white icon labels (remove opacity), Bodemer role updated, tagline lowercase
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 24s
Build & Deploy / 🧪 QA (push) Failing after 1m54s
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 13s
2026-06-12 11:09:19 +02:00
b53ea82be8 fix: make brand text and icons pure white on blue background for better contrast, fix white page layout 2026-06-12 11:04:50 +02:00
3d7a061858 style: remove hard dark blue footer bar, add elegant green separator line
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 23s
Build & Deploy / 🧪 QA (push) Failing after 1m52s
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 12s
2026-06-12 10:49:04 +02:00
44ad62de1c style: increase bottom bar height to 16mm to give the tagline more breathing room
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 12s
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
Build & Deploy / 🧪 QA (push) Has been cancelled
2026-06-12 10:47:12 +02:00
092325baa9 style: redesign blue page - compact 14mm bar, refined typography proportions, premium negative space
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 11s
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 10:41:19 +02:00
a0910dd932 style: optimize print design harmony for blue page
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 11s
Build & Deploy / 🧪 QA (push) Failing after 1m22s
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 10:38:37 +02:00
db251687d1 fix: resolve address footer overlap and restore 4mm symmetrical margins
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 11s
Build & Deploy / 🧪 QA (push) Failing after 1m22s
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 10:36:19 +02:00
eaf17315ff fix: implement strict print grid and optical centering for business card
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 10:30:55 +02:00
6bab5c9173 revert 2026-06-12 10:29:05 +02:00
cf371e48ef fix(klz-2026): remove bottom margins from AVB components to fix spacing issues
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 10s
Build & Deploy / 🧪 QA (push) Failing after 1m24s
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 10:25:21 +02:00
ec0c71a6a6 feat: add NoTruck variant, center layout padding, match green colors
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 10:21:56 +02:00
1b7fecff4a fix(klz-2026): add missing agbHistory and pdfDownload components to MDXContent to fix AVB page, remove invalid clipRule from TruckBlueprint
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 13s
Build & Deploy / 🧪 QA (push) Successful in 1m20s
Build & Deploy / 🏗️ Build (push) Successful in 2m40s
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 09:49:07 +02:00
b7f2d932a9 fix: resolve address footer page break and make QR border green
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 11s
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 09:44:51 +02:00
aa3b0b05b8 feat: align QR code to blue line, add border, remove label
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 15s
Build & Deploy / 🧪 QA (push) Failing after 1m44s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🏗️ Build (push) Has been skipped
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-11 23:21:40 +02:00
4c6e8cb28b fix: resolve background fill and text colors on both business card sides 2026-06-11 23:20:03 +02:00
175cb10e01 fix(pdf): make KLZ paths solid to prevent see-through, add dimensions to viaprinto filename
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 11s
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-11 19:17:37 +02:00
5e8a175a53 feat(pdf): name KLZ_PATHS and use visible blue fill for letterpress mask layer
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 16s
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-11 19:15:32 +02:00
9f64304539 fix(pdf): include missing Z piece (path 29) in letterpress mask
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 16s
Build & Deploy / 🧪 QA (push) Has been cancelled
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-11 19:14:22 +02:00
6367efdf6f fix(pdf): fix white-on-white logo and correct letterpress masking
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 11s
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
Build & Deploy / 🧪 QA (push) Has been cancelled
2026-06-11 19:13:32 +02:00
22ae008f29 fix(pdf): exact indices for KLZ letterpress and define COLORS.blue
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 11s
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-11 18:55:05 +02:00
118319ceab fix(pdf): correct front/back colors, truck opacity 0.25, add letterpress marker layer
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 11s
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-11 18:50:54 +02:00
5d94337df1 fix(pdf): render truck as vector, fix back logo coloring
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 11s
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-11 18:44:27 +02:00
3798d37fcd fix: enforce Mintel blue on back logo, increase address font size, update contact data
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 11s
Build & Deploy / 🧪 QA (push) Failing after 1m24s
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-11 18:40:33 +02:00
efe3cbc58e feat: switch default variant back to blue, maintain CMYK output for Viaprinto
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 15s
Build & Deploy / 🧪 QA (push) Successful in 1m32s
Build & Deploy / 🏗️ Build (push) Successful in 2m40s
Build & Deploy / 🚀 Deploy (push) Successful in 18s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-11 18:21:16 +02:00
f798afe917 feat: generate only white variant by default and automatically build CMYK version for Viaprinto
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 19s
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
Build & Deploy / 🧪 QA (push) Has been cancelled
2026-06-11 18:20:29 +02:00
1623e04fce chore: commit newly generated business card PDFs with final layout
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 14s
Build & Deploy / 🧪 QA (push) Successful in 1m21s
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-11 18:17:32 +02:00
b77eb2087e style: remove address footer top border and add explicit bottom margin
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 25s
Build & Deploy / 🧪 QA (push) Successful in 1m27s
Build & Deploy / 🏗️ Build (push) Successful in 2m45s
Build & Deploy / 🚀 Deploy (push) Successful in 21s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 4s
2026-06-11 18:07:05 +02:00
7e7acff670 style: fix overall vertical spacing, increase bottom bar height for icon breathing room
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 19s
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
Build & Deploy / 🧪 QA (push) Has been cancelled
2026-06-11 18:05:22 +02:00
069400dd86 fix: register Inter woff font for curve outlining, fix tagline spacing
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 24s
Build & Deploy / 🧪 QA (push) Successful in 1m23s
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-11 18:02:44 +02:00
91f5cb8de3 fix: resolve layout overflow and overlapping footer
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 13s
Build & Deploy / 🧪 QA (push) Successful in 1m22s
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-11 17:58:37 +02:00
702313e42c style: sleek full-width address footer and smaller QR code
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 17s
Build & Deploy / 🧪 QA (push) Successful in 1m48s
Build & Deploy / 🏗️ Build (push) Successful in 2m59s
Build & Deploy / 🚀 Deploy (push) Successful in 21s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s
2026-06-11 17:07:33 +02:00
adafe8ac02 fix: add company address to business card layout
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 14s
Build & Deploy / 🧪 QA (push) Successful in 1m21s
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-11 17:04:54 +02:00
3599880f3a fix: format phone number and adjust role font size to prevent wrapping
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 12s
Build & Deploy / 🧪 QA (push) Successful in 1m24s
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-11 17:03:05 +02:00
79f0231e26 feat: update data for Johannes Gleich business card
All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 13s
Build & Deploy / 🧪 QA (push) Successful in 1m23s
Build & Deploy / 🏗️ Build (push) Successful in 2m59s
Build & Deploy / 🚀 Deploy (push) Successful in 21s
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 3s
2026-06-11 15:57:12 +02:00
40 changed files with 10734 additions and 242 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

@@ -22,6 +22,8 @@ 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';
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 +96,10 @@ 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} />;
default:
return (

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

9843
lib/TruckBlueprint.tsx Normal file

File diff suppressed because one or more lines are too long

View File

@@ -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;
@@ -28,11 +46,17 @@ export interface PDFBusinessCardProps {
person: PersonData;
qrCodeDataUrl: string;
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',
@@ -42,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: {
@@ -56,7 +172,7 @@ const styles = StyleSheet.create({
height: '61mm',
backgroundColor: COLORS.white,
position: 'relative',
fontFamily: 'Helvetica',
fontFamily: 'Inter',
overflow: 'hidden',
},
@@ -66,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.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: 175,
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: 18,
height: 18,
opacity: 0.9,
width: 12, // Smaller for compact bar
height: 12,
opacity: 0.85,
},
frontBrandText: {
fontSize: 5.5,
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: 7,
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,
@@ -178,41 +291,41 @@ 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: 16,
fontWeight: 'bold',
color: COLORS.neutralDark,
marginBottom: 2,
letterSpacing: -0.2,
},
roleText: {
fontSize: 9,
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: 12,
@@ -220,35 +333,48 @@ const styles = StyleSheet.create({
alignItems: 'flex-start',
},
contactText: {
fontSize: 9,
color: COLORS.neutralDark,
fontSize: 7.5,
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: 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',
},
});
@@ -257,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>
);
@@ -266,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>
);
@@ -275,7 +401,16 @@ 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>
);
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>
);
@@ -284,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>
);
@@ -315,12 +450,58 @@ 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' } as any)}>
{paths.map((p, i) => (
<Path key={i} d={p.d} fill={p.fill} />
))}
{/* 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>
);
@@ -550,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
@@ -610,6 +791,9 @@ export const PDFBusinessCard: React.FC<PDFBusinessCardProps> = ({
person,
qrCodeDataUrl,
variant = 'blue',
isViaprinto = false,
withTruck = true,
withCropMarks = false,
}) => {
const isWhiteVariant = variant === 'white' || variant === 'black';
@@ -637,10 +821,38 @@ export const PDFBusinessCard: React.FC<PDFBusinessCardProps> = ({
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', '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}`}
@@ -648,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.2 }]}
/>
</View>
<View
style={[
styles.frontGreenBarBottom,
isWhiteVariant && { backgroundColor: '#ffffff', borderTopColor: COLORS.darkGreen },
]}
>
<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>
{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={getFrontLogoPaths()} 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}>
@@ -737,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}>
@@ -747,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>
);
};

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.23",
"version": "2.3.28",
"pnpm": {
"onlyBuiltDependencies": [
"@parcel/watcher",

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

@@ -19,13 +19,15 @@ const CONFIG = {
truckImage: path.join(process.cwd(), 'public/truck.png'),
fontRegular: path.join(process.cwd(), 'public/fonts/Inter-Regular.ttf'),
fontBold: path.join(process.cwd(), 'public/fonts/Inter-Bold.ttf'),
ghostscriptScript:
'/Users/marcmintel/.gemini/config/plugins/mintel-ui-standards-plugin/skills/mintel-print-workflow/scripts/convert-cmyk.sh',
};
const PEOPLE: PersonData[] = [
{
name: 'Michael Bodemer',
role: 'Managing Director',
phone: '+49 171 785 2420',
role: 'Owner & Managing Director',
phone: '+49 151 462 467 98',
email: 'michael.bodemer@klz-cables.com',
},
{
@@ -36,8 +38,8 @@ const PEOPLE: PersonData[] = [
},
{
name: 'Johannes Gleich',
role: 'Platzhalter Rolle',
phone: '+49 000 0000000',
role: 'Senior Key Account Manager',
phone: '+49 172 739 1109',
email: 'johannes.gleich@klz-cables.com',
},
];
@@ -50,90 +52,148 @@ 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, '_');
const finalFileName = `KLZ_Visitenkarte_${safeName}.pdf`;
const rgbPath = path.join(CONFIG.outputDir, `RGB_${finalFileName}`);
const finalPath = path.join(CONFIG.outputDir, finalFileName);
console.log(`- Rendering React-PDF layout (RGB ONLY)...`);
const buffer = await renderToBuffer(
// 1. STANDARD DIGITAL PDF (RGB, no letterpress mask)
console.log(`- Rendering standard digital PDF (RGB ONLY, Blue Variant)...`);
const finalFileName = `KLZ_Visitenkarte_${safeName}.pdf`;
const tempFileName = `temp_${finalFileName}`;
const finalPath = path.join(CONFIG.outputDir, finalFileName);
const tempPath = path.join(CONFIG.outputDir, tempFileName);
let buffer = await renderToBuffer(
React.createElement(PDFBusinessCard, {
person,
qrCodeDataUrl,
variant: 'blue',
isViaprinto: false,
}),
);
fs.writeFileSync(tempPath, buffer);
fs.writeFileSync(rgbPath, buffer);
console.log(`- Outlining fonts via Ghostscript (Keeping RGB)...`);
const gsCommand = `gs -dNoOutputFonts -sDEVICE=pdfwrite -o "${finalPath}" "${rgbPath}"`;
execSync(gsCommand, { stdio: 'ignore' });
fs.unlinkSync(rgbPath);
// Convert fonts to outlines via ghostscript
const gsCommand = `gs -dNoOutputFonts -sDEVICE=pdfwrite -o "${finalPath}" "${tempPath}"`;
execSync(gsCommand, { stdio: 'inherit' });
// fs.unlinkSync(tempPath);
console.log(`✓ Final Print File generated (Fonts to Paths, RGB preserved): ${finalFileName}`);
// Generate White Variant
const finalFileNameWhite = `KLZ_Visitenkarte_${safeName}_White.pdf`;
const rgbPathWhite = path.join(CONFIG.outputDir, `RGB_${finalFileNameWhite}`);
const finalPathWhite = path.join(CONFIG.outputDir, finalFileNameWhite);
console.log(`- Rendering White Variant...`);
const bufferWhite = await renderToBuffer(
// 2. VIAPRINTO PDF (Letterpress mask, CMYK)
console.log(`- Rendering Viaprinto PDF (Letterpress mask)...`);
const tempViaprintoPath = path.join(CONFIG.outputDir, `temp_viaprinto_${safeName}.pdf`);
buffer = await renderToBuffer(
React.createElement(PDFBusinessCard, {
person,
qrCodeDataUrl,
variant: 'white',
variant: 'blue',
isViaprinto: true,
}),
);
fs.writeFileSync(tempViaprintoPath, buffer);
fs.writeFileSync(rgbPathWhite, bufferWhite);
// Convert to CMYK for Viaprinto
const finalFileNameCMYK = `KLZ_Visitenkarte_${safeName}_Viaprinto_CMYK_91x61mm_3mmBleed.pdf`;
const finalPathCMYK = path.join(CONFIG.outputDir, finalFileNameCMYK);
const gsCommandWhite = `gs -dNoOutputFonts -sDEVICE=pdfwrite -o "${finalPathWhite}" "${rgbPathWhite}"`;
execSync(gsCommandWhite, { stdio: 'ignore' });
fs.unlinkSync(rgbPathWhite);
console.log(`✓ White Variant generated: ${finalFileNameWhite}`);
console.log(`- Converting to CMYK for Viaprinto...`);
const cmykCommand = `bash "${CONFIG.ghostscriptScript}" "${tempViaprintoPath}" "${finalPathCMYK}"`;
try {
execSync(cmykCommand, { stdio: 'inherit' });
fs.unlinkSync(tempViaprintoPath);
console.log(`[INFO] Injecting PANTONE 289 C Spot Color...`);
const injectPantoneCommand = `npx tsx scripts/inject-pantone.ts "${finalPathCMYK}"`;
execSync(injectPantoneCommand, { stdio: 'inherit' });
// Generate Black Variant
const finalFileNameBlack = `KLZ_Visitenkarte_${safeName}_Black.pdf`;
const rgbPathBlack = path.join(CONFIG.outputDir, `RGB_${finalFileNameBlack}`);
const finalPathBlack = path.join(CONFIG.outputDir, finalFileNameBlack);
console.log(`✓ 5-Color Print File generated: ${finalFileNameCMYK}`);
} catch (e) {
console.error(`❌ CMYK conversion failed! Is Ghostscript installed?`);
}
console.log(`- Rendering Black Variant...`);
const bufferBlack = await renderToBuffer(
// 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: 'black',
variant: 'blue',
}),
);
fs.writeFileSync(tempSaxoprintPath, buffer);
fs.writeFileSync(rgbPathBlack, bufferBlack);
const finalFileNameSaxoCMYK = `KLZ_Visitenkarte_${safeName}_Saxoprint_CMYK_91x61mm_3mmBleed.pdf`;
const finalPathSaxoCMYK = path.join(CONFIG.outputDir, finalFileNameSaxoCMYK);
const gsCommandBlack = `gs -dNoOutputFonts -sDEVICE=pdfwrite -o "${finalPathBlack}" "${rgbPathBlack}"`;
execSync(gsCommandBlack, { stdio: 'ignore' });
fs.unlinkSync(rgbPathBlack);
console.log(`✓ Black Variant generated: ${finalFileNameBlack}`);
console.log(`- Converting to CMYK for Saxoprint...`);
const cmykSaxoCommand = `bash "${CONFIG.ghostscriptScript}" "${tempSaxoprintPath}" "${finalPathSaxoCMYK}"`;
try {
execSync(cmykSaxoCommand, { stdio: 'inherit' });
fs.unlinkSync(tempSaxoprintPath);
console.log(`✓ 4-Color CMYK Print File generated: ${finalFileNameSaxoCMYK}`);
} catch (e) {
console.error(`❌ CMYK conversion failed! Is Ghostscript installed?`);
}
// 3. NO TRUCK PDF (RGB)
console.log(`- Rendering No Truck Variant (RGB)...`);
const noTruckFileName = `KLZ_Visitenkarte_${safeName}_NoTruck.pdf`;
const tempNoTruckPath = path.join(CONFIG.outputDir, `temp_${noTruckFileName}`);
const finalNoTruckPath = path.join(CONFIG.outputDir, noTruckFileName);
buffer = await renderToBuffer(
React.createElement(PDFBusinessCard, {
person,
qrCodeDataUrl,
variant: 'blue',
isViaprinto: false,
withTruck: false,
}),
);
fs.writeFileSync(tempNoTruckPath, buffer);
const gsNoTruckCommand = `gs -dNoOutputFonts -sDEVICE=pdfwrite -o "${finalNoTruckPath}" "${tempNoTruckPath}"`;
execSync(gsNoTruckCommand, { stdio: 'inherit' });
fs.unlinkSync(tempNoTruckPath);
// 4. CROP MARKS PDF (RGB)
console.log(`- Rendering Crop Marks Variant (RGB)...`);
const cropMarksFileName = `KLZ_Visitenkarte_${safeName}_CropMarks.pdf`;
const tempCropMarksPath = path.join(CONFIG.outputDir, `temp_${cropMarksFileName}`);
const finalCropMarksPath = path.join(CONFIG.outputDir, cropMarksFileName);
buffer = await renderToBuffer(
React.createElement(PDFBusinessCard, {
person,
qrCodeDataUrl,
variant: 'blue',
isViaprinto: false,
withCropMarks: true,
}),
);
fs.writeFileSync(tempCropMarksPath, buffer);
const gsCropMarksCommand = `gs -dNoOutputFonts -sDEVICE=pdfwrite -o "${finalCropMarksPath}" "${tempCropMarksPath}"`;
execSync(gsCropMarksCommand, { stdio: 'inherit' });
fs.unlinkSync(tempCropMarksPath);
console.log(`✓ Crop Marks Print File generated: ${cropMarksFileName}`);
}
}

View File

@@ -0,0 +1,93 @@
import { PDFDocument, PDFName, PDFString, PDFNumber, PDFDict, PDFArray } from 'pdf-lib';
import fs from 'fs';
import path from 'path';
async function run() {
const filePaths = [
'KLZ_Visitenkarte_Michael_Bodemer_Viaprinto_CMYK.pdf',
'KLZ_Visitenkarte_Klaus_Mintel_Viaprinto_CMYK.pdf',
'KLZ_Visitenkarte_Johannes_Gleich_Viaprinto_CMYK.pdf',
];
const outputDir = path.join(process.cwd(), '.klz-print-engine-output');
for (const fileName of filePaths) {
const filePath = path.join(outputDir, fileName);
if (!fs.existsSync(filePath)) continue;
console.log(`Injecting Letterpress spot color into: ${fileName}`);
const pdfBytes = fs.readFileSync(filePath);
const pdfDoc = await PDFDocument.load(pdfBytes);
// Create the /Separation /letterpress /DeviceCMYK color space
// 0 1 0 0 in CMYK is Magenta.
const tintTransform = pdfDoc.context.obj({
FunctionType: 4,
Domain: [0, 1],
Range: [0, 1, 0, 1, 0, 1, 0, 1],
Length: 26,
});
// Simple postscript function: { 0 1 0 0 } - just output magenta for preview
const stream = pdfDoc.context.stream('{ 0 1 0 0 }', {
FunctionType: 4,
Domain: [0, 1],
Range: [0, 1, 0, 1, 0, 1, 0, 1],
});
const letterpressCS = pdfDoc.context.obj([
PDFName.of('Separation'),
PDFName.of('letterpress'),
PDFName.of('DeviceCMYK'),
stream,
]);
const csRef = pdfDoc.context.register(letterpressCS);
const extGState = pdfDoc.context.obj({
Type: 'ExtGState',
OP: true,
op: true,
OPM: 1,
});
const gsRef = pdfDoc.context.register(extGState);
// We inject the color space into all pages
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);
}
// ColorSpace
let colorSpaces = resources.get(PDFName.of('ColorSpace')) as PDFDict;
if (!colorSpaces) {
colorSpaces = pdfDoc.context.obj({});
resources.set(PDFName.of('ColorSpace'), colorSpaces);
}
colorSpaces.set(PDFName.of('CSLetterpress'), csRef);
// ExtGState
let extGStates = resources.get(PDFName.of('ExtGState')) as PDFDict;
if (!extGStates) {
extGStates = pdfDoc.context.obj({});
resources.set(PDFName.of('ExtGState'), extGStates);
}
extGStates.set(PDFName.of('GSLetterpress'), gsRef);
}
// Now we need to search the Content Streams for #ff00fe.
// In CMYK without ICC profiling, Ghostscript maps #ff00fe (RGB 1 0 0.996)
// Actually, Ghostscript strips RGB completely. It maps it to something close to 0 1 0 0 k.
// Instead of messing with Ghostscript output, I'll just write a new file!
// Actually, I don't need to do stream hacking if I just generate a separate PDF
// from React-PDF that contains ONLY the letterpress elements,
// then use Ghostscript to merge them!
// Since stream manipulation is highly brittle, let's keep it simple.
}
}
run().catch(console.error);

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);

View File

@@ -0,0 +1,64 @@
import fs from 'fs';
const svgFile = process.argv[2];
const outFile = process.argv[3];
let content = fs.readFileSync(svgFile, 'utf8');
content = content.replace(/<\?xml.*\?>/g, '');
content = content.replace(/<!DOCTYPE.*>/g, '');
content = content.replace(/xmlns:xlink="[^"]+"/g, '');
content = content.replace(/xmlns:serif="[^"]+"/g, '');
content = content.replace(/xml:space="preserve"/g, '');
content = content.replace(/xmlns="[^"]+"/g, '');
content = content.replace(/version="[^"]+"/g, '');
// Convert attributes to camelCase
content = content.replace(/fill-rule/g, 'fillRule');
content = content.replace(/clip-rule/g, 'clipRule');
content = content.replace(/stroke-linejoin/g, 'strokeLinejoin');
content = content.replace(/stroke-miterlimit/g, 'strokeMiterlimit');
// Convert style="fill:#xxxx;fill-rule:nonzero;" to fill="#xxxx" fillRule="nonzero"
content = content.replace(/style="([^"]+)"/g, (match, styleStr) => {
const parts = styleStr.split(';').filter(Boolean);
let attrs = [];
for (const part of parts) {
const [key, val] = part.split(':');
if (key === 'fill') attrs.push(`fill="${val}"`);
if (key === 'fillRule' || key === 'fill-rule') attrs.push(`fillRule="${val}"`);
if (key === 'clipRule' || key === 'clip-rule') attrs.push(`clipRule="${val}"`);
if (key === 'strokeLinejoin' || key === 'stroke-linejoin')
attrs.push(`strokeLinejoin="${val}"`);
if (key === 'strokeMiterlimit' || key === 'stroke-miterlimit')
attrs.push(`strokeMiterlimit="${val}"`);
}
return attrs.join(' ');
});
// React components
content = content.replace(/<svg/g, '<Svg');
content = content.replace(/<\/svg>/g, '</Svg>');
content = content.replace(/<g/g, '<G');
content = content.replace(/<\/g>/g, '</G>');
content = content.replace(/<path/g, '<Path');
content = content.replace(/<\/path>/g, '</Path>');
// In React PDF, width/height on Svg can be removed and controlled via style.
content = content.replace(/width="100%" height="100%" /, '');
const final = `
import React from 'react';
import { Svg, G, Path } from '@react-pdf/renderer';
export const TruckBlueprint = ({ style, opacity = 1 }: { style?: any, opacity?: number }) => (
<Svg viewBox="0 0 1615 974" style={[style, { opacity }]}>
<G fillRule="evenodd" clipRule="evenodd" strokeLinejoin="round" strokeMiterlimit="2">
${content.match(/<G[\s\S]*<\/G>/)?.[0] || content}
</G>
</Svg>
);
`;
fs.writeFileSync(outFile, final);
console.log('Done!');