refactor: Centralize PDF styling with COLORS and FONT_SIZES and enhance module content and dynamic title generation.

This commit is contained in:
2026-02-03 22:35:01 +01:00
parent 38f2b871b9
commit ce421eb8d2
13 changed files with 346 additions and 254 deletions

View File

@@ -78,43 +78,50 @@ export const EstimationPDF = ({
}
// Full Portfolio Mode
let pageCounter = 1;
const getPageNum = () => (pageCounter++).toString().padStart(2, '0');
return (
<>
<PDFPage size="A4" style={pdfStyles.page}>
<PDFPage size="A4" style={pdfStyles.titlePage}>
<FrontPageModule state={state} headerIcon={headerIcon} date={date} />
</PDFPage>
<SimpleLayout {...commonProps} pageNumber="01">
<SimpleLayout {...commonProps} pageNumber={getPageNum()}>
<BriefingModule state={state} />
</SimpleLayout>
{state.sitemap && state.sitemap.length > 0 && (
<SimpleLayout {...commonProps} pageNumber="03">
<SimpleLayout {...commonProps} pageNumber={getPageNum()}>
<SitemapModule state={state} />
</SimpleLayout>
)}
<SimpleLayout {...commonProps} pageNumber="04">
<SimpleLayout {...commonProps} pageNumber={getPageNum()}>
<EstimationModule state={state} positions={positions} totalPrice={totalPrice} date={date} />
</SimpleLayout>
<SimpleLayout {...commonProps} pageNumber="05">
<SimpleLayout {...commonProps} pageNumber={getPageNum()}>
<TransparenzModule pricing={pricing} />
</SimpleLayout>
<SimpleLayout {...commonProps} pageNumber="06">
<TechPageModule techDetails={techDetails} headerIcon={headerIcon} />
</SimpleLayout>
{techDetails && techDetails.length > 0 && (
<SimpleLayout {...commonProps} pageNumber={getPageNum()}>
<TechPageModule techDetails={techDetails} headerIcon={headerIcon} />
</SimpleLayout>
)}
<SimpleLayout {...commonProps} pageNumber="07">
<PrinciplesModule principles={principles} />
</SimpleLayout>
{principles && principles.length > 0 && (
<SimpleLayout {...commonProps} pageNumber={getPageNum()}>
<PrinciplesModule principles={principles} />
</SimpleLayout>
)}
<SimpleLayout {...commonProps} pageNumber="08">
<SimpleLayout {...commonProps} pageNumber={getPageNum()}>
<AboutModule />
</SimpleLayout>
<SimpleLayout {...commonProps} pageNumber="09">
<SimpleLayout {...commonProps} pageNumber={getPageNum()}>
<CrossSellModule state={state} />
</SimpleLayout>
</>

View File

@@ -1,23 +1,51 @@
'use client';
import * as React from 'react';
import {
Text as PDFText,
View as PDFView,
StyleSheet as PDFStyleSheet,
Image as PDFImage
} from '@react-pdf/renderer';
import { View as PDFView, Text as PDFText, StyleSheet, Link as PDFLink, Image as PDFImage, Font } from '@react-pdf/renderer';
export const pdfStyles = PDFStyleSheet.create({
// INDUSTRIAL DESIGN SYSTEM TOKENS
export const COLORS = {
CHARCOAL: '#0f172a', // Slate 900
TEXT_MAIN: '#334155', // Slate 700
TEXT_DIM: '#64748b', // Slate 500
TEXT_LIGHT: '#94a3b8', // Slate 400
DIVIDER: '#cbd5e1', // Slate 300
GRID: '#f1f5f9', // Slate 100
BLUEPRINT: '#e2e8f0', // Slate 200
WHITE: '#ffffff'
};
export const FONT_SIZES = {
H1: 24,
H2: 18,
H3: 12,
BODY: 9,
TINY: 7,
SUB: 8,
BLUEPRINT: 5
};
// Register a more technical font if possible, or use Helvetica with varying weights
// Note: helvetica-bold is standard in react-pdf
export const pdfStyles = StyleSheet.create({
page: {
paddingTop: 45, // DIN 5008
paddingLeft: 70, // ~25mm
paddingRight: 57, // ~20mm
paddingBottom: 80, // Safe buffer for absolute footer
backgroundColor: '#ffffff',
backgroundColor: COLORS.WHITE,
fontFamily: 'Helvetica',
fontSize: 10,
color: '#000000',
fontSize: FONT_SIZES.BODY,
color: COLORS.CHARCOAL,
},
titlePage: {
width: '100%',
height: '100%',
backgroundColor: COLORS.WHITE,
fontFamily: 'Helvetica',
color: COLORS.CHARCOAL,
padding: 0, // NO PADDING to prevent inner overflow page breaks
},
header: {
flexDirection: 'row',
@@ -31,13 +59,13 @@ export const pdfStyles = PDFStyleSheet.create({
marginTop: 45, // DIN 5008 positioning for window
},
senderLine: {
fontSize: 7,
fontSize: FONT_SIZES.TINY,
textDecoration: 'underline',
color: '#666666',
color: COLORS.TEXT_DIM,
marginBottom: 8,
},
recipientAddress: {
fontSize: 10,
fontSize: FONT_SIZES.BODY,
lineHeight: 1.4,
},
brandLogoContainer: {
@@ -47,14 +75,14 @@ export const pdfStyles = PDFStyleSheet.create({
brandIconContainer: {
width: 40,
height: 40,
backgroundColor: '#000000',
backgroundColor: '#0f172a',
borderRadius: 8,
alignItems: 'center',
justifyContent: 'center',
marginBottom: 12,
},
brandIconText: {
color: '#ffffff',
color: COLORS.WHITE,
fontSize: 20,
fontWeight: 'bold',
},
@@ -62,27 +90,27 @@ export const pdfStyles = PDFStyleSheet.create({
marginBottom: 24,
},
mainTitle: {
fontSize: 12,
fontSize: FONT_SIZES.H3,
fontWeight: 'bold',
marginBottom: 4,
color: '#000000',
color: COLORS.CHARCOAL,
textTransform: 'uppercase',
letterSpacing: 1,
},
subTitle: {
fontSize: 9,
color: '#666666',
fontSize: FONT_SIZES.BODY,
color: COLORS.TEXT_DIM,
marginTop: 2,
},
section: {
marginBottom: 32,
},
sectionTitle: {
fontSize: 8,
fontSize: FONT_SIZES.SUB,
fontWeight: 'bold',
textTransform: 'uppercase',
letterSpacing: 1,
color: '#999999',
color: COLORS.TEXT_LIGHT,
marginBottom: 8,
},
footer: {
@@ -91,7 +119,7 @@ export const pdfStyles = PDFStyleSheet.create({
left: 70,
right: 57,
borderTopWidth: 1,
borderTopColor: '#f1f5f9',
borderTopColor: COLORS.GRID,
paddingTop: 16,
flexDirection: 'row',
justifyContent: 'space-between',
@@ -108,17 +136,17 @@ export const pdfStyles = PDFStyleSheet.create({
marginBottom: 8,
},
footerText: {
fontSize: 7,
color: '#94a3b8',
fontSize: FONT_SIZES.TINY,
color: COLORS.TEXT_LIGHT,
lineHeight: 1.5,
},
footerLabel: {
fontWeight: 'bold',
color: '#64748b',
color: COLORS.TEXT_DIM,
},
pageNumber: {
fontSize: 7,
color: '#cbd5e1',
fontSize: FONT_SIZES.TINY,
color: COLORS.DIVIDER,
fontWeight: 'bold',
marginTop: 8,
textAlign: 'right',
@@ -128,14 +156,41 @@ export const pdfStyles = PDFStyleSheet.create({
left: 20,
width: 10,
borderTopWidth: 0.5,
borderTopColor: '#cbd5e1',
borderTopColor: COLORS.DIVIDER,
},
divider: {
width: '100%',
height: 1,
backgroundColor: '#f1f5f9',
backgroundColor: COLORS.DIVIDER,
marginVertical: 12,
},
blueprintGrid: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
zIndex: -10,
},
gridLineH: {
width: '100%',
height: 0.5,
backgroundColor: COLORS.GRID,
position: 'absolute',
},
gridLineV: {
width: 0.5,
height: '100%',
backgroundColor: COLORS.GRID,
position: 'absolute',
},
technicalMarker: {
position: 'absolute',
fontSize: FONT_SIZES.BLUEPRINT,
color: COLORS.BLUEPRINT,
fontFamily: 'Helvetica',
letterSpacing: 1,
},
// Atoms
industrialListItem: {
flexDirection: 'row',
@@ -145,47 +200,47 @@ export const pdfStyles = PDFStyleSheet.create({
industrialBulletBox: {
width: 6,
height: 6,
backgroundColor: '#0f172a',
backgroundColor: COLORS.CHARCOAL,
marginRight: 8,
marginTop: 5,
},
industrialTitle: {
fontSize: 24,
fontSize: FONT_SIZES.H1,
fontWeight: 'bold',
color: '#0f172a',
color: COLORS.CHARCOAL,
marginBottom: 6,
letterSpacing: -0.5,
letterSpacing: 0, // Reset for clarity
},
industrialSubtitle: {
fontSize: 8,
fontSize: FONT_SIZES.SUB,
fontWeight: 'bold',
color: '#94a3b8',
color: COLORS.TEXT_LIGHT,
textTransform: 'uppercase',
marginBottom: 16,
letterSpacing: 2,
},
industrialTextLead: {
fontSize: 10,
color: '#334155',
fontSize: FONT_SIZES.BODY,
color: COLORS.TEXT_MAIN,
lineHeight: 1.6,
marginBottom: 12,
},
industrialText: {
fontSize: 9,
color: '#64748b',
fontSize: FONT_SIZES.BODY,
color: COLORS.TEXT_DIM,
lineHeight: 1.6,
marginBottom: 8,
},
industrialCard: {
padding: 16,
borderWidth: 1,
borderColor: '#e2e8f0',
borderColor: COLORS.BLUEPRINT,
marginBottom: 12,
},
industrialCardTitle: {
fontSize: 10,
fontSize: FONT_SIZES.BODY + 1, // 10
fontWeight: 'bold',
color: '#0f172a',
color: COLORS.CHARCOAL,
marginBottom: 4,
textTransform: 'uppercase',
letterSpacing: 0.5,
@@ -193,22 +248,30 @@ export const pdfStyles = PDFStyleSheet.create({
darkBox: {
marginTop: 32,
padding: 24,
backgroundColor: '#0f172a',
color: '#ffffff',
backgroundColor: COLORS.CHARCOAL,
color: COLORS.WHITE,
},
darkTitle: {
fontSize: 18,
fontSize: FONT_SIZES.H2,
fontWeight: 'bold',
color: '#ffffff',
color: COLORS.WHITE,
marginBottom: 8,
},
darkText: {
fontSize: 9,
color: '#94a3b8',
fontSize: FONT_SIZES.BODY,
color: COLORS.TEXT_LIGHT,
lineHeight: 1.6,
},
});
const styles = pdfStyles;
export const BlueprintBackground = () => (
<PDFView style={styles.blueprintGrid} fixed>
{/* Clean background - grid lines removed per user request */}
</PDFView>
);
export const IndustrialListItem = ({ children }: { children: React.ReactNode }) => (
<PDFView style={pdfStyles.industrialListItem}>
<PDFView style={pdfStyles.industrialBulletBox} />
@@ -238,5 +301,5 @@ export const Header = ({ sender, recipient, icon, showAddress = true }: { sender
);
export const DocumentTitle = ({ title, subLines }: { title: string; subLines?: string[] }) => (
<PDFView style={pdfStyles.titleInfo}><PDFText style={pdfStyles.mainTitle}>{title}</PDFText>{subLines?.map((line, i) => (<PDFText key={i} style={[pdfStyles.subTitle, i === 1 ? { fontWeight: 'bold', color: '#000000' } : {}]}>{line}</PDFText>))}</PDFView>
<PDFView style={pdfStyles.titleInfo}><PDFText style={pdfStyles.mainTitle}>{title}</PDFText>{subLines?.map((line, i) => (<PDFText key={i} style={[pdfStyles.subTitle, i === 1 ? { fontWeight: 'bold', color: '#0f172a' } : {}]}>{line}</PDFText>))}</PDFView>
);

View File

@@ -2,7 +2,7 @@
import * as React from 'react';
import { Page as PDFPage, View as PDFView, Text as PDFText, StyleSheet } from '@react-pdf/renderer';
import { Header, Footer, pdfStyles } from './SharedUI';
import { Header, Footer, pdfStyles, BlueprintBackground } from './SharedUI';
const simpleStyles = StyleSheet.create({
industrialPage: {
@@ -47,6 +47,7 @@ export const SimpleLayout = ({
}: SimpleLayoutProps) => {
return (
<PDFPage size="A4" style={[pdfStyles.page, simpleStyles.industrialPage]}>
<BlueprintBackground />
<Header icon={icon} showAddress={false} />
{pageNumber && <PDFText style={simpleStyles.industrialNumber}>{pageNumber}</PDFText>}
<PDFView style={simpleStyles.industrialSection}>

View File

@@ -2,22 +2,22 @@
import * as React from 'react';
import { View as PDFView, Text as PDFText, StyleSheet } from '@react-pdf/renderer';
import { IndustrialListItem, IndustrialCard, Divider } from '../SharedUI';
import { IndustrialListItem, IndustrialCard, Divider, COLORS, FONT_SIZES } from '../SharedUI';
const styles = StyleSheet.create({
industrialTitle: { fontSize: 24, fontWeight: 'bold', color: '#0f172a', marginBottom: 6, letterSpacing: -0.5 },
industrialSubtitle: { fontSize: 8, fontWeight: 'bold', color: '#94a3b8', textTransform: 'uppercase', marginBottom: 16, letterSpacing: 2 },
industrialTextLead: { fontSize: 10, color: '#334155', lineHeight: 1.6, marginBottom: 12 },
industrialText: { fontSize: 9, color: '#64748b', lineHeight: 1.6, marginBottom: 8 },
industrialTitle: { fontSize: FONT_SIZES.H1, fontWeight: 'bold', color: COLORS.CHARCOAL, marginBottom: 6, letterSpacing: -1 },
industrialSubtitle: { fontSize: FONT_SIZES.SUB, fontWeight: 'bold', color: COLORS.TEXT_LIGHT, textTransform: 'uppercase', marginBottom: 16, letterSpacing: 2 },
industrialTextLead: { fontSize: FONT_SIZES.H3, color: COLORS.TEXT_MAIN, lineHeight: 1.6, marginBottom: 12 },
industrialText: { fontSize: FONT_SIZES.BODY, color: COLORS.TEXT_DIM, lineHeight: 1.6, marginBottom: 8 },
industrialGrid2: { flexDirection: 'row', gap: 32 },
industrialCol: { width: '48%' },
darkBox: { marginTop: 32, padding: 24, backgroundColor: '#0f172a', color: '#ffffff' },
darkTitle: { fontSize: 18, fontWeight: 'bold', color: '#ffffff', marginBottom: 8 },
darkText: { fontSize: 9, color: '#94a3b8', lineHeight: 1.6 },
darkBox: { marginTop: 32, padding: 24, backgroundColor: COLORS.CHARCOAL, color: COLORS.WHITE },
darkTitle: { fontSize: FONT_SIZES.H2, fontWeight: 'bold', color: COLORS.WHITE, marginBottom: 8 },
darkText: { fontSize: FONT_SIZES.BODY, color: COLORS.TEXT_LIGHT, lineHeight: 1.6 },
industrialBulletBox: {
width: 6,
height: 6,
backgroundColor: '#0f172a',
backgroundColor: COLORS.CHARCOAL,
marginRight: 8,
marginTop: 5,
},
@@ -27,16 +27,16 @@ export const AboutModule = () => (
<>
<PDFText style={styles.industrialTitle}>Über mich</PDFText>
<PDFText style={styles.industrialSubtitle}>Direkt. Sauber. Verantwortlich.</PDFText>
<Divider style={{ marginVertical: 16, backgroundColor: '#f8fafc' }} />
<Divider style={{ marginVertical: 16, backgroundColor: COLORS.GRID }} />
<PDFView style={[styles.industrialGrid2, { marginTop: 20 }]}>
<PDFView style={styles.industrialCol}>
<PDFText style={styles.industrialTextLead}>Ich entwickle Websysteme seit 15 Jahren. Ich kenne Agenturen, Konzerne und Startups. Ich arbeite alleine, weil ich Verantwortung nicht teilen will.</PDFText>
<PDFText style={styles.industrialText}>Technik scheitert selten an Bits und Bytes. Sie scheitert an unklaren Zuständigkeiten. Ich bin Ihr einziger Ansprechpartner. Ich treffe die Entscheidungen und ich löse die Probleme.</PDFText>
<PDFText style={styles.industrialTextLead}>Seit 15 Jahren entstehen unter meiner Leitung Websysteme für Agenturen, Konzerne und Startups. Ich arbeite bewusst alleine, um die volle Verantwortung für jedes Projekt zu tragen.</PDFText>
<PDFText style={styles.industrialText}>Technik scheitert selten an Bits und Bytes, sondern meist an unklaren Zuständigkeiten. Als Ihr direkter Ansprechpartner treffe ich die notwendigen Entscheidungen und löse technische Probleme ohne Umwege.</PDFText>
<PDFView style={{ marginTop: 16 }}>
<PDFText style={[styles.industrialText, { fontWeight: 'bold' }]}>Mein Standard:</PDFText>
<IndustrialListItem><PDFText style={styles.industrialText}>Ich liefere Code ohne Altlasten.</PDFText></IndustrialListItem>
<IndustrialListItem><PDFText style={styles.industrialText}>Ich baue Systeme, die ohne mich laufen.</PDFText></IndustrialListItem>
<IndustrialListItem><PDFText style={styles.industrialText}>Ich arbeite ohne Overhead und Stille Post.</PDFText></IndustrialListItem>
<IndustrialListItem><PDFText style={styles.industrialText}>Code-Lieferung ohne technische Altlasten.</PDFText></IndustrialListItem>
<IndustrialListItem><PDFText style={styles.industrialText}>Entwicklung von Systemen, die autark operieren.</PDFText></IndustrialListItem>
<IndustrialListItem><PDFText style={styles.industrialText}>Verzicht auf Overhead und Kommunikationsverluste.</PDFText></IndustrialListItem>
</PDFView>
</PDFView>
<PDFView style={styles.industrialCol}>
@@ -45,10 +45,10 @@ export const AboutModule = () => (
<IndustrialListItem><PDFText style={styles.industrialText}>Ich antworte direkt auf technische Fragen.</PDFText></IndustrialListItem>
<IndustrialListItem><PDFText style={styles.industrialText}>Ich garantiere die Umsetzung.</PDFText></IndustrialListItem>
</IndustrialCard>
<IndustrialCard title="KEIN BALLAST" style={{ backgroundColor: '#ffffff', borderColor: '#cbd5e1' }}>
<PDFText style={[styles.industrialText, { color: '#94a3b8' }]}>Keine Projektmanager.</PDFText>
<PDFText style={[styles.industrialText, { color: '#94a3b8' }]}>Keine Vertriebler.</PDFText>
<PDFText style={[styles.industrialText, { color: '#94a3b8' }]}>Kein Ticket-Chaos.</PDFText>
<IndustrialCard title="KEIN BALLAST" style={{ backgroundColor: COLORS.WHITE, borderColor: COLORS.DIVIDER }}>
<PDFText style={[styles.industrialText, { color: COLORS.TEXT_LIGHT }]}>Keine Projektmanager.</PDFText>
<PDFText style={[styles.industrialText, { color: COLORS.TEXT_LIGHT }]}>Keine Vertriebler.</PDFText>
<PDFText style={[styles.industrialText, { color: COLORS.TEXT_LIGHT }]}>Kein Ticket-Chaos.</PDFText>
</IndustrialCard>
</PDFView>
</PDFView>
@@ -64,16 +64,16 @@ export const CrossSellModule = ({ state }: any) => {
<>
<PDFText style={styles.industrialTitle}>{title}</PDFText>
<PDFText style={styles.industrialSubtitle}>{subtitle}</PDFText>
<Divider style={{ marginVertical: 16, backgroundColor: '#f8fafc' }} />
<Divider style={{ marginVertical: 16, backgroundColor: COLORS.GRID }} />
<PDFView style={[styles.industrialGrid2, { marginTop: 12 }]} >
{isWebsite ? (
<>
<PDFView style={styles.industrialCol}>
<PDFText style={styles.industrialTextLead}>Ich identifiziere manuelle Abläufe in Ihrem Unternehmen und ersetze sie durch Software. Ich eliminiere Tippfehler und Zeitfresser.</PDFText>
<PDFText style={[styles.industrialText, { fontWeight: 'bold' }]}>Ich schaffe Zeit für wertschöpfende Arbeit. Ich digitalisiere das Chaos.</PDFText>
<PDFText style={styles.industrialTextLead}>Manuelle Abläufe binden Kapazitäten. Durch maßgeschneiderte Software ersetze ich fehleranfällige Prozesse und eliminiere Zeitfresser in Ihrem Unternehmen.</PDFText>
<PDFText style={[styles.industrialText, { fontWeight: 'bold' }]}>Ziel ist der Gewinn wertvoller Arbeitszeit. Digitalisierung ordnet das Chaos.</PDFText>
<PDFView style={styles.darkBox}>
<PDFText style={styles.darkTitle}>Direktes Feedback</PDFText>
<PDFText style={styles.darkText}>Ich prüfe Ihren Prozess innerhalb von 48 Stunden. Ich sage Ihnen sofort, ob eine Automatisierung wirtschaftlich sinnvoll ist.</PDFText>
<PDFText style={styles.darkTitle}>Individuelle Prüfung</PDFText>
<PDFText style={styles.darkText}>Ich analysiere Ihren spezifischen Prozess auf technisches Automatisierungspotenzial. Das Ergebnis liefert Klarheit darüber, ob eine Umsetzung wirtschaftlich sinnvoll ist.</PDFText>
</PDFView>
</PDFView>
<PDFView style={styles.industrialCol}>

View File

@@ -2,16 +2,16 @@
import * as React from 'react';
import { View as PDFView, Text as PDFText, StyleSheet } from '@react-pdf/renderer';
import { DocumentTitle } from '../SharedUI';
import { DocumentTitle, COLORS, FONT_SIZES } from '../SharedUI';
const styles = StyleSheet.create({
section: { marginBottom: 24 },
sectionTitle: { fontSize: 10, fontWeight: 'bold', marginBottom: 8, color: '#0f172a' },
sectionTitle: { fontSize: FONT_SIZES.BODY + 1, fontWeight: 'bold', marginBottom: 8, color: COLORS.CHARCOAL },
configGrid: { flexDirection: 'row', flexWrap: 'wrap', gap: 6, marginTop: 6 },
configItem: { width: '24%', marginBottom: 4 },
configLabel: { fontSize: 5, color: '#94a3b8', textTransform: 'uppercase', marginBottom: 2 },
configValue: { fontSize: 7, color: '#0f172a', fontWeight: 'bold' },
visionText: { fontSize: 9, color: '#334155', lineHeight: 1.8, textAlign: 'justify' },
configLabel: { fontSize: FONT_SIZES.BLUEPRINT, color: COLORS.TEXT_LIGHT, textTransform: 'uppercase', marginBottom: 2 },
configValue: { fontSize: FONT_SIZES.TINY, color: COLORS.CHARCOAL, fontWeight: 'bold' },
visionText: { fontSize: FONT_SIZES.BODY, color: COLORS.TEXT_MAIN, lineHeight: 1.8, textAlign: 'justify' },
});
export const BriefingModule = ({ state }: any) => (
@@ -26,15 +26,15 @@ export const BriefingModule = ({ state }: any) => (
<PDFView style={styles.section}>
<PDFText style={styles.sectionTitle}>Kern-Informationen</PDFText>
<PDFView style={styles.configGrid}>
<PDFView style={styles.configItem}><PDFText style={styles.configLabel}>Ansprechpartner</PDFText><PDFText style={styles.configValue}>{state.personName || "Sie"}</PDFText></PDFView>
<PDFView style={styles.configItem}><PDFText style={styles.configLabel}>Projektart</PDFText><PDFText style={styles.configValue}>{state.statusQuo || (state.existingWebsite ? 'Relaunch' : 'Neuentwicklung')}</PDFText></PDFView>
<PDFView style={styles.configItem}><PDFText style={styles.configLabel}>Kontakt</PDFText><PDFText style={styles.configValue}>{state.personName || ""}</PDFText></PDFView>
<PDFView style={styles.configItem}><PDFText style={styles.configLabel}>Projekttyp</PDFText><PDFText style={styles.configValue}>{state.isRelaunch ? 'Website Evolution' : (state.statusQuo || 'Neukonzeption')}</PDFText></PDFView>
<PDFView style={styles.configItem}><PDFText style={styles.configLabel}>Mitarbeiter</PDFText><PDFText style={styles.configValue}>{state.employeeCount || "—"}</PDFText></PDFView>
<PDFView style={styles.configItem}><PDFText style={styles.configLabel}>Zeitplan</PDFText><PDFText style={styles.configValue}>{state.deadline || 'Flexibel'}</PDFText></PDFView>
</PDFView>
</PDFView>
{state.designVision && (
<PDFView style={[styles.section, { padding: 16, borderLeftWidth: 2, borderLeftColor: '#000000', backgroundColor: '#f9fafb' }]}>
<PDFText style={[styles.sectionTitle, { color: '#000000' }]}>Strategische Vision</PDFText>
<PDFView style={[styles.section, { padding: 16, borderLeftWidth: 2, borderLeftColor: COLORS.DIVIDER, backgroundColor: COLORS.GRID }]}>
<PDFText style={[styles.sectionTitle, { color: COLORS.CHARCOAL }]}>Strategische Vision</PDFText>
<PDFText style={styles.visionText}>{state.designVision}</PDFText>
</PDFView>
)}

View File

@@ -2,16 +2,16 @@
import * as React from 'react';
import { View as PDFView, Text as PDFText, StyleSheet, Image as PDFImage } from '@react-pdf/renderer';
import { DocumentTitle, Divider } from '../SharedUI';
import { DocumentTitle, Divider, COLORS, FONT_SIZES } from '../SharedUI';
const styles = StyleSheet.create({
section: { marginBottom: 24 },
pricingGrid: { marginTop: 24 },
pricingRow: { flexDirection: 'row', borderBottomWidth: 1, borderBottomColor: '#f1f5f9', paddingVertical: 10, alignItems: 'flex-start' },
pricingTitle: { width: '30%', fontSize: 9, fontWeight: 'bold', color: '#0f172a' },
pricingDesc: { width: '55%', fontSize: 8, color: '#64748b', lineHeight: 1.4 },
pricingTag: { width: '15%', fontSize: 9, fontWeight: 'bold', textAlign: 'right' },
configLabel: { fontSize: 5, color: '#94a3b8', textTransform: 'uppercase', marginBottom: 8 },
pricingRow: { flexDirection: 'row', borderBottomWidth: 1, borderBottomColor: COLORS.DIVIDER, paddingVertical: 10, alignItems: 'flex-start' },
pricingTitle: { width: '30%', fontSize: FONT_SIZES.BODY, fontWeight: 'bold', color: COLORS.CHARCOAL },
pricingDesc: { width: '55%', fontSize: FONT_SIZES.SUB, color: COLORS.TEXT_DIM, lineHeight: 1.4 },
pricingTag: { width: '15%', fontSize: FONT_SIZES.BODY, fontWeight: 'bold', textAlign: 'right', color: COLORS.CHARCOAL },
configLabel: { fontSize: FONT_SIZES.BLUEPRINT, color: COLORS.TEXT_LIGHT, textTransform: 'uppercase', marginBottom: 8 },
});
const CHROME_ICON = '/Users/marcmintel/Projects/mintel.me/src/assets/browser/chrome.png'; // Fallback to a placeholder if not found
@@ -21,7 +21,7 @@ export const techPageModule = ({ techDetails, headerIcon }: any) => (
<>
<DocumentTitle title="Technische Umsetzung" />
<PDFView style={styles.section}>
<PDFText style={{ fontSize: 8, color: '#64748b', lineHeight: 1.6, marginBottom: 16 }}>Ich entwickle Websites als moderne, performante Websysteme.</PDFText>
<PDFText style={{ fontSize: FONT_SIZES.SUB, color: COLORS.TEXT_DIM, lineHeight: 1.6, marginBottom: 16 }}>Ich entwickle Websites als moderne, performante Websysteme.</PDFText>
<PDFView style={styles.pricingGrid}>
{techDetails?.map((item: any, i: number) => (
<PDFView key={i} style={styles.pricingRow}>
@@ -38,9 +38,9 @@ export const TransparenzModule = ({ pricing }: any) => (
<>
<DocumentTitle title="Preis-Transparenz & Modell" />
<PDFView style={styles.section}>
<PDFText style={{ fontSize: 10, fontWeight: 'bold', color: '#000000', marginBottom: 8 }}>Festpreise statt Stundenabrechnung</PDFText>
<PDFText style={{ fontSize: 8, color: '#64748b', lineHeight: 1.6, marginBottom: 12 }}>Ich biete Planungssicherheit. Ich kalkuliere nach einem modularen Festpreis-System. Sie zahlen für Ergebnisse, nicht für die Zeit. Ich schließe versteckte Kosten aus.</PDFText>
<Divider style={{ backgroundColor: '#f8fafc', marginTop: 12 }} />
<PDFText style={{ fontSize: FONT_SIZES.BODY + 1, fontWeight: 'bold', color: COLORS.CHARCOAL, marginBottom: 8 }}>Festpreise statt Stundenabrechnung</PDFText>
<PDFText style={{ fontSize: FONT_SIZES.SUB, color: COLORS.TEXT_DIM, lineHeight: 1.6, marginBottom: 12 }}>Ich biete Planungssicherheit. Ich kalkuliere nach einem modularen Festpreis-System. Sie zahlen für Ergebnisse, nicht für die Zeit. Ich schließe versteckte Kosten aus.</PDFText>
<Divider style={{ marginTop: 12 }} />
</PDFView>
<PDFView style={styles.section}>
<PDFView style={styles.pricingGrid}>
@@ -72,7 +72,7 @@ export const TransparenzModule = ({ pricing }: any) => (
<PDFView style={styles.pricingRow}>
<PDFText style={styles.pricingTitle}>6. Integrationen</PDFText>
<PDFText style={styles.pricingDesc}>Ich binde Drittsysteme wie CRM, ERP oder Stripe an. Ich richte CMS-Schnittstellen zur unabhängigen Inhaltsverwaltung ein.</PDFText>
<PDFText style={styles.pricingTag}>ab {pricing.API_INTEGRATION?.toLocaleString('de-DE')} </PDFText>
<PDFText style={styles.pricingTag}>ab {pricing.API_INTEGRATION?.toLocaleString('de-DE')} / Stück</PDFText>
</PDFView>
<PDFView style={styles.pricingRow}>
<PDFText style={styles.pricingTitle}>7. Betrieb (12 Monate)</PDFText>

View File

@@ -6,7 +6,7 @@ import { DocumentTitle } from '../SharedUI';
const styles = StyleSheet.create({
table: { marginTop: 12 },
tableHeader: { flexDirection: 'row', paddingBottom: 8, borderBottomWidth: 1, borderBottomColor: '#000000', marginBottom: 12 },
tableHeader: { flexDirection: 'row', paddingBottom: 8, borderBottomWidth: 1, borderBottomColor: '#334155', marginBottom: 12 },
tableRow: { flexDirection: 'row', paddingVertical: 8, borderBottomWidth: 1, borderBottomColor: '#f8fafc', alignItems: 'flex-start' },
colPos: { width: '8%' },
colDesc: { width: '62%' },
@@ -17,11 +17,11 @@ const styles = StyleSheet.create({
itemTitle: { fontSize: 10, fontWeight: 'bold', marginBottom: 4 },
itemDesc: { fontSize: 8, color: '#666666', lineHeight: 1.4 },
priceText: { fontSize: 10, fontWeight: 'bold' },
summaryContainer: { borderTopWidth: 1, borderTopColor: '#000000', paddingTop: 8 },
summaryContainer: { borderTopWidth: 1, borderTopColor: '#334155', paddingTop: 8 },
summaryRow: { flexDirection: 'row', justifyContent: 'flex-end', paddingVertical: 4, alignItems: 'baseline' },
summaryLabel: { fontSize: 7, color: '#64748b', textTransform: 'uppercase', letterSpacing: 1, fontWeight: 'bold', marginRight: 12 },
summaryValue: { fontSize: 9, fontWeight: 'bold', width: 100, textAlign: 'right' },
totalRow: { flexDirection: 'row', justifyContent: 'flex-end', paddingTop: 12, marginTop: 8, borderTopWidth: 2, borderTopColor: '#000000', alignItems: 'baseline' },
totalRow: { flexDirection: 'row', justifyContent: 'flex-end', paddingTop: 12, marginTop: 8, borderTopWidth: 2, borderTopColor: '#334155', alignItems: 'baseline' },
});
export const EstimationModule = ({ state, positions, totalPrice, date }: any) => (

View File

@@ -2,18 +2,20 @@
import * as React from 'react';
import { View as PDFView, Text as PDFText, Image as PDFImage, StyleSheet } from '@react-pdf/renderer';
import { COLORS, FONT_SIZES } from '../SharedUI';
const styles = StyleSheet.create({
titlePage: {
flex: 1, // Fill the whole page
padding: 60,
justifyContent: 'center',
alignItems: 'center',
height: '90%',
backgroundColor: COLORS.WHITE,
},
titleBrandIcon: {
width: 80,
height: 80,
backgroundColor: '#000000',
backgroundColor: COLORS.CHARCOAL,
borderRadius: 16,
alignItems: 'center',
justifyContent: 'center',
@@ -21,53 +23,59 @@ const styles = StyleSheet.create({
},
brandIconText: {
fontSize: 40,
color: '#ffffff',
color: COLORS.WHITE,
fontWeight: 'bold'
},
titleProjectName: {
fontSize: 24,
fontSize: FONT_SIZES.H1,
fontWeight: 'bold',
color: '#0f172a',
color: COLORS.CHARCOAL,
marginBottom: 16,
textAlign: 'center',
textTransform: 'uppercase',
letterSpacing: 2,
maxWidth: '85%',
lineHeight: 1.2,
},
titleCustomerName: {
fontSize: 14,
color: '#64748b',
fontSize: FONT_SIZES.H3,
color: COLORS.TEXT_DIM,
marginBottom: 40,
textAlign: 'center',
maxWidth: '80%',
},
titleDocumentType: {
fontSize: 10,
color: '#94a3b8',
fontSize: FONT_SIZES.BODY + 1, // ~10
color: COLORS.TEXT_LIGHT,
textTransform: 'uppercase',
letterSpacing: 4,
marginBottom: 8,
marginBottom: 12,
},
titleDivider: {
width: 40,
height: 2,
backgroundColor: '#000000',
backgroundColor: COLORS.CHARCOAL,
marginBottom: 40,
},
titleDate: {
fontSize: 9,
color: '#94a3b8',
marginTop: 'auto',
fontSize: FONT_SIZES.BODY,
color: COLORS.TEXT_LIGHT,
marginTop: 40,
},
});
export const FrontPageModule = ({ state, headerIcon, date }: any) => (
<PDFView style={styles.titlePage}>
<PDFView style={styles.titleBrandIcon}>
{headerIcon ? <PDFImage src={headerIcon} style={{ width: 40, height: 40 }} /> : <PDFText style={styles.brandIconText}>M</PDFText>}
export const FrontPageModule = ({ state, headerIcon, date }: any) => {
const fullTitle = `Digitale Webpräsenz für\n${state.companyName || "Ihr Projekt"}`;
// Responsive font size based on length
const fontSize = fullTitle.length > 60 ? 14 : fullTitle.length > 40 ? 18 : 22;
return (
<PDFView style={styles.titlePage}>
<PDFView style={styles.titleBrandIcon}>
{headerIcon ? <PDFImage src={headerIcon} style={{ width: 40, height: 40 }} /> : <PDFText style={styles.brandIconText}>M</PDFText>}
</PDFView>
<PDFText style={[styles.titleProjectName, { fontSize }]}>{fullTitle}</PDFText>
<PDFView style={styles.titleDivider} />
<PDFText style={styles.titleDate}>{date} | Marc Mintel</PDFText>
</PDFView>
<PDFText style={styles.titleDocumentType}>Konzept & Kostenschätzung</PDFText>
<PDFText style={styles.titleProjectName}>{state.projectType === 'website' ? 'Digitale Präsenz' : 'Digitale Applikation'}</PDFText>
<PDFView style={styles.titleDivider} />
<PDFText style={styles.titleCustomerName}>für {state.companyName || "Ihr Projekt"}</PDFText>
<PDFText style={styles.titleDate}>{date} | Marc Mintel</PDFText>
</PDFView>
);
);
};

View File

@@ -2,32 +2,32 @@
import * as React from 'react';
import { View as PDFView, Text as PDFText, StyleSheet } from '@react-pdf/renderer';
import { DocumentTitle, Divider } from '../SharedUI';
import { DocumentTitle, Divider, COLORS, FONT_SIZES } from '../SharedUI';
const styles = StyleSheet.create({
section: { marginBottom: 24 },
sitemapTree: { marginTop: 20 },
sitemapRootNode: { flexDirection: 'row', alignItems: 'center', marginBottom: 12 },
sitemapRootDot: { width: 6, height: 6, borderRadius: 3, backgroundColor: '#000000', marginRight: 10 },
sitemapRootTitle: { fontSize: 10, fontWeight: 'bold', textTransform: 'uppercase', letterSpacing: 1 },
sitemapMainLine: { position: 'absolute', left: 2, top: 20, bottom: 0, width: 0.5, backgroundColor: '#cbd5e1' },
sitemapRootDot: { width: 6, height: 6, borderRadius: 3, backgroundColor: COLORS.CHARCOAL, marginRight: 10 },
sitemapRootTitle: { fontSize: FONT_SIZES.H3, fontWeight: 'bold', textTransform: 'uppercase', letterSpacing: 1, color: COLORS.CHARCOAL },
sitemapMainLine: { position: 'absolute', left: 2, top: 20, bottom: 0, width: 0.5, backgroundColor: COLORS.DIVIDER },
sitemapBranch: { marginLeft: 20, marginBottom: 12, position: 'relative' },
sitemapNode: { flexDirection: 'row', alignItems: 'center', marginBottom: 4 },
sitemapRootIcon: { width: 4, height: 4, backgroundColor: '#000000', marginRight: 8 },
sitemapBranchTitle: { fontSize: 8, fontWeight: 'bold' },
sitemapLeaf: { marginLeft: 12, borderLeftWidth: 0.5, borderLeftColor: '#cbd5e1', paddingLeft: 12, marginTop: 4 },
sitemapRootIcon: { width: 4, height: 4, backgroundColor: COLORS.CHARCOAL, marginRight: 8 },
sitemapBranchTitle: { fontSize: FONT_SIZES.BODY, fontWeight: 'bold', color: COLORS.TEXT_MAIN },
sitemapLeaf: { marginLeft: 12, borderLeftWidth: 0.5, borderLeftColor: COLORS.DIVIDER, paddingLeft: 12, marginTop: 4 },
sitemapLeafNode: { flexDirection: 'row', alignItems: 'flex-start', marginBottom: 6 },
sitemapLeafPointer: { fontSize: 7, color: '#94a3b8', marginRight: 6 },
sitemapLeafTitle: { fontSize: 7, fontWeight: 'bold' },
sitemapLeafDesc: { fontSize: 6, color: '#64748b', lineHeight: 1.3, marginTop: 1 },
sitemapLeafPointer: { fontSize: FONT_SIZES.BODY, color: COLORS.TEXT_LIGHT, marginRight: 6 },
sitemapLeafTitle: { fontSize: FONT_SIZES.BODY, fontWeight: 'bold', color: COLORS.TEXT_MAIN },
sitemapLeafDesc: { fontSize: FONT_SIZES.TINY, color: COLORS.TEXT_DIM, lineHeight: 1.3, marginTop: 1 },
});
export const SitemapModule = ({ state }: any) => (
<>
<DocumentTitle title="Seitenstruktur" />
<PDFView style={styles.section}>
<PDFText style={{ fontSize: 8, color: '#64748b', lineHeight: 1.6, marginBottom: 16 }}>Die folgende Struktur bildet das Fundament für die Benutzerführung und Informationsarchitektur Ihres Projekts.</PDFText>
<Divider style={{ backgroundColor: '#f8fafc', marginBottom: 20 }} />
<PDFText style={{ fontSize: FONT_SIZES.BODY, color: COLORS.TEXT_DIM, lineHeight: 1.6, marginBottom: 16 }}>Die folgende Struktur bildet das Fundament für die Benutzerführung und Informationsarchitektur Ihres Projekts.</PDFText>
<Divider style={{ marginBottom: 20 }} />
<PDFView style={styles.sitemapTree}>
<PDFView style={styles.sitemapRootNode}><PDFView style={styles.sitemapRootDot} /><PDFText style={styles.sitemapRootTitle}>{state.websiteTopic || 'Digitales Ökosystem'}</PDFText></PDFView>
<PDFView style={styles.sitemapMainLine} />

View File

@@ -14,16 +14,25 @@ export function calculatePositions(state: FormState, pricing: any): Position[] {
price: pricing.BASE_WEBSITE
});
const totalPagesCount = (state.selectedPages?.length || 0) + (state.otherPages?.length || 0) + (state.otherPagesCount || 0);
const sitemapPagesCount = state.sitemap?.reduce((acc: number, cat: any) => acc + (cat.pages?.length || 0), 0) || 0;
const totalPagesCount = Math.max(
(state.selectedPages?.length || 0) + (state.otherPages?.length || 0) + (state.otherPagesCount || 0),
sitemapPagesCount
);
const allPages = [
...(state.selectedPages || []).map((p: string) => PAGE_LABELS[p] || p),
...(state.otherPages || [])
...(state.otherPages || []),
...(state.sitemap?.flatMap((cat: any) => cat.pages?.map((p: any) => p.title)) || [])
];
// Deduplicate labels
const uniquePages = Array.from(new Set(allPages));
positions.push({
pos: pos++,
title: 'Individuelle Seiten',
desc: `Gestaltung und Umsetzung von ${totalPagesCount} individuellen Seiten-Layouts (${allPages.join(', ')}).`,
desc: `Gestaltung und Umsetzung von ${totalPagesCount} individuellen Seiten-Layouts (${uniquePages.join(', ')}).`,
qty: totalPagesCount,
price: totalPagesCount * pricing.PAGE
});