fix: improve PDF export - vector icons, wider section lines, address line break, simplified language levels

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Thomas Reitz 2026-03-09 12:06:37 +01:00
parent 2e5a697224
commit 4d5aa84ac9
2 changed files with 78 additions and 25 deletions

View file

@ -174,17 +174,23 @@ export class ProfileExportService {
yLeft = this.pdfSectionTitle(doc, 'KONTAKT', leftColX, yLeft, leftColWidth, accentColor);
if (data.phone) {
yLeft = this.pdfContactLine(doc, '\u260E', data.phone, leftColX, yLeft, leftColWidth);
this.drawPhoneIcon(doc, leftColX, yLeft + 1, accentColor);
yLeft = this.pdfContactText(doc, data.phone, leftColX + 16, yLeft, leftColWidth - 16);
}
if (data.mobile) {
yLeft = this.pdfContactLine(doc, '\u260E', data.mobile, leftColX, yLeft, leftColWidth);
this.drawPhoneIcon(doc, leftColX, yLeft + 1, accentColor);
yLeft = this.pdfContactText(doc, data.mobile, leftColX + 16, yLeft, leftColWidth - 16);
}
if (data.email) {
yLeft = this.pdfContactLine(doc, '\u2709', data.email, leftColX, yLeft, leftColWidth);
this.drawEmailIcon(doc, leftColX, yLeft + 1, accentColor);
yLeft = this.pdfContactText(doc, data.email, leftColX + 16, yLeft, leftColWidth - 16);
}
if (data.street || data.city) {
const address = [data.street, [data.postalCode, data.city].filter(Boolean).join(' ')].filter(Boolean).join(', ');
yLeft = this.pdfContactLine(doc, '\u2302', address, leftColX, yLeft, leftColWidth);
this.drawLocationIcon(doc, leftColX + 1, yLeft, accentColor);
const line1 = data.street || '';
const line2 = [data.postalCode, data.city].filter(Boolean).join(' ');
const addressText = [line1, line2].filter(Boolean).join('\n');
yLeft = this.pdfContactText(doc, addressText, leftColX + 16, yLeft, leftColWidth - 16);
}
yLeft += 10;
@ -457,20 +463,15 @@ export class ProfileExportService {
// Kontakt-Sektion
leftParagraphs.push(this.docxSectionHeading('KONTAKT', accentHex));
const contactItems: Array<{ icon: string; text: string }> = [];
if (data.phone) contactItems.push({ icon: '\u260E', text: data.phone });
if (data.mobile) contactItems.push({ icon: '\u260E', text: data.mobile });
if (data.email) contactItems.push({ icon: '\u2709', text: data.email });
if (data.street || data.city) {
const address = [data.street, [data.postalCode, data.city].filter(Boolean).join(' ')].filter(Boolean).join(', ');
contactItems.push({ icon: '\u2302', text: address });
}
const contactItems: Array<{ label: string; text: string }> = [];
if (data.phone) contactItems.push({ label: 'Tel.', text: data.phone });
if (data.mobile) contactItems.push({ label: 'Mobil', text: data.mobile });
if (data.email) contactItems.push({ label: 'Mail', text: data.email });
for (const item of contactItems) {
leftParagraphs.push(
new Paragraph({
children: [
new TextRun({ text: `${item.icon} `, size: 18, color: accentHex }),
new TextRun({ text: `${item.label} `, bold: true, size: 14, color: accentHex }),
new TextRun({ text: item.text, size: 16, color: '555555' }),
],
spacing: { after: 60 },
@ -478,6 +479,23 @@ export class ProfileExportService {
);
}
// Adresse separat (mit Zeilenumbruch)
if (data.street || data.city) {
const addrRuns: TextRun[] = [
new TextRun({ text: 'Adr. ', bold: true, size: 14, color: accentHex }),
];
if (data.street) {
addrRuns.push(new TextRun({ text: data.street, size: 16, color: '555555' }));
}
const cityLine = [data.postalCode, data.city].filter(Boolean).join(' ');
if (cityLine) {
addrRuns.push(new TextRun({ text: cityLine, size: 16, color: '555555', break: 1 }));
}
leftParagraphs.push(
new Paragraph({ children: addrRuns, spacing: { after: 60 } }),
);
}
// Sprachen
if (profile && profile.languages.length > 0) {
leftParagraphs.push(this.docxSectionHeading('SPRACHEN', accentHex));
@ -684,29 +702,64 @@ export class ProfileExportService {
title: string,
x: number,
y: number,
width: number,
_width: number,
accentColor: string,
): number {
doc.font('Helvetica-Bold').fontSize(10).fillColor('#333333');
doc.text(title, x, y, { width });
doc.text(title, x, y);
const titleWidth = doc.widthOfString(title);
y += 14;
doc.moveTo(x, y).lineTo(x + Math.min(width, 60), y)
.strokeColor(accentColor).lineWidth(1.5).stroke();
doc.moveTo(x, y).lineTo(x + titleWidth + 4, y)
.strokeColor(accentColor).lineWidth(2).stroke();
y += 8;
return y;
}
private pdfContactLine(
private pdfContactText(
doc: PDFKit.PDFDocument,
icon: string,
text: string,
x: number,
y: number,
width: number,
): number {
doc.font('Helvetica').fontSize(8).fillColor('#777777');
doc.text(`${icon} ${text}`, x, y, { width });
return y + doc.heightOfString(`${icon} ${text}`, { width }) + 3;
doc.font('Helvetica').fontSize(8).fillColor('#555555');
doc.text(text, x, y, { width });
const textHeight = doc.heightOfString(text, { width });
return y + Math.max(textHeight, 10) + 3;
}
// --- Vektor-Icons für Kontakt ---
private drawPhoneIcon(doc: PDFKit.PDFDocument, x: number, y: number, color: string): void {
doc.save();
// Telefon-Hörer: abgerundetes Rechteck
doc.roundedRect(x + 1, y, 4, 8, 1.5).fillAndStroke(color, color);
// Hörer-Bogen oben
doc.moveTo(x, y + 1).quadraticCurveTo(x - 1, y + 4, x + 1, y + 7)
.strokeColor(color).lineWidth(1.5).stroke();
doc.restore();
}
private drawEmailIcon(doc: PDFKit.PDFDocument, x: number, y: number, color: string): void {
doc.save();
// Briefumschlag
doc.rect(x, y + 1, 10, 7).strokeColor(color).lineWidth(1).stroke();
// V-Linie oben
doc.moveTo(x, y + 1).lineTo(x + 5, y + 5).lineTo(x + 10, y + 1)
.strokeColor(color).lineWidth(0.8).stroke();
doc.restore();
}
private drawLocationIcon(doc: PDFKit.PDFDocument, x: number, y: number, color: string): void {
doc.save();
// Pin-Kopf (Kreis)
doc.circle(x + 4, y + 4, 3).fillAndStroke(color, color);
// Pin-Spitze (Dreieck)
doc.moveTo(x + 1.5, y + 5.5).lineTo(x + 4, y + 11).lineTo(x + 6.5, y + 5.5)
.fill(color);
// Innerer Kreis (weiß)
doc.circle(x + 4, y + 4, 1.2).fill('#ffffff');
doc.restore();
}
// ============================================================

View file

@ -8,7 +8,7 @@ interface LanguagesSectionProps {
onUpdate: () => Promise<void>;
}
const LANGUAGE_LEVELS = ['Muttersprache', 'C2', 'C1', 'B2', 'B1', 'A2', 'A1'];
const LANGUAGE_LEVELS = ['Muttersprache', 'Fließend', 'Gut'];
export function LanguagesSection({ languages, onUpdate }: LanguagesSectionProps) {
const [language, setLanguage] = useState('');